Initial commit

This commit is contained in:
DaM
2026-01-26 18:57:42 +01:00
commit dcffd9dfad
23 changed files with 1639 additions and 0 deletions

212
src/data/fetcher.py Normal file
View File

@@ -0,0 +1,212 @@
# src/data/fetcher.py
"""
Módulo para obtener datos de exchanges usando CCXT
"""
import ccxt
import pandas as pd
from datetime import datetime, timedelta
from typing import List, Optional, Dict
import time
from ..monitoring.logger import log
class DataFetcher:
"""
Clase para obtener datos históricos y en tiempo real de exchanges
"""
def __init__(self, exchange_name: str, api_key: str = None, api_secret: str = None):
"""
Inicializa la conexión con el exchange
Args:
exchange_name: Nombre del exchange (binance, kraken, etc)
api_key: API key (opcional para datos públicos)
api_secret: API secret (opcional para datos públicos)
"""
self.exchange_name = exchange_name
try:
exchange_class = getattr(ccxt, exchange_name)
self.exchange = exchange_class({
'apiKey': api_key,
'secret': api_secret,
'enableRateLimit': True, # Importante para evitar bans
'options': {
'defaultType': 'spot', # spot, future, etc
}
})
log.info(f"Conectado al exchange: {exchange_name}")
except Exception as e:
log.error(f"Error conectando a {exchange_name}: {e}")
raise
def fetch_ohlcv(self, symbol: str, timeframe: str = '1h', since: Optional[datetime] = None,
limit: int = 500) -> pd.DataFrame:
"""
Obtiene datos OHLCV (Open, High, Low, Close, Volume)
Args:
symbol: Par de trading (ej: 'BTC/USDT')
timeframe: Intervalo de tiempo ('1m', '5m', '1h', '1d')
since: Fecha desde la que obtener datos
limit: Número máximo de velas a obtener
Returns:
DataFrame con los datos OHLCV
"""
try:
# Convertir datetime a timestamp en ms
since_ms = None
if since:
since_ms = int(since.timestamp() * 1000)
log.info(f"Obteniendo datos OHLCV: {symbol} {timeframe}")
ohlcv = self.exchange.fetch_ohlcv(
symbol,
timeframe=timeframe,
since=since_ms,
limit=limit
)
# Convertir a DataFrame
df = pd.DataFrame(
ohlcv,
columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
)
# Convertir timestamp a datetime
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
# Añadir metadata
df['symbol'] = symbol
df['timeframe'] = timeframe
log.success(f"Obtenidos {len(df)} registros de {symbol}")
return df
except Exception as e:
log.error(f"Error obteniendo OHLCV para {symbol}: {e}")
raise
def fetch_historical(self, symbol: str, timeframe: str = '1h', days: int = 30,
max_retries: int = 3) -> pd.DataFrame:
"""
Obtiene datos históricos completos (puede requerir múltiples llamadas)
Args:
symbol: Par de trading
timeframe: Intervalo de tiempo
days: Días hacia atrás
max_retries: Intentos máximos por request
Returns:
DataFrame con todos los datos históricos
"""
all_data = []
since = datetime.now() - timedelta(days=days)
log.info(f"Iniciando descarga histórica: {symbol} desde {since.date()}")
while True:
retry_count = 0
success = False
while retry_count < max_retries and not success:
try:
df = self.fetch_ohlcv(symbol, timeframe, since, limit=1000)
if df.empty:
log.warning(f"No hay más datos disponibles para {symbol}")
success = True
break
all_data.append(df)
# Actualizar 'since' al último timestamp + 1
last_timestamp = df.index[-1]
since = last_timestamp + pd.Timedelta(seconds=1)
# Verificar si ya llegamos al presente
if since >= datetime.now():
success = True
break
success = True
time.sleep(self.exchange.rateLimit / 1000) # Respetar rate limit
except Exception as e:
retry_count += 1
log.warning(f"Intento {retry_count}/{max_retries} falló: {e}")
time.sleep(5 * retry_count) # Backoff exponencial
if not success:
log.error(f"Falló después de {max_retries} intentos")
break
if since >= datetime.now():
break
if not all_data:
log.error("No se pudo obtener ningún dato histórico")
return pd.DataFrame()
# Combinar todos los DataFrames
final_df = pd.concat(all_data).drop_duplicates()
final_df.sort_index(inplace=True)
log.success(f"Descarga completa: {len(final_df)} velas de {symbol}")
return final_df
def fetch_ticker(self, symbol: str) -> Dict:
"""
Obtiene el precio actual y información del ticker
Args:
symbol: Par de trading
Returns:
Diccionario con información del ticker
"""
try:
ticker = self.exchange.fetch_ticker(symbol)
log.debug(f"Ticker de {symbol}: {ticker['last']}")
return ticker
except Exception as e:
log.error(f"Error obteniendo ticker de {symbol}: {e}")
raise
def fetch_order_book(self, symbol: str, limit: int = 20) -> Dict:
"""
Obtiene el libro de órdenes (order book)
Args:
symbol: Par de trading
limit: Profundidad del order book
Returns:
Diccionario con bids y asks
"""
try:
order_book = self.exchange.fetch_order_book(symbol, limit)
return order_book
except Exception as e:
log.error(f"Error obteniendo order book de {symbol}: {e}")
raise
def get_available_symbols(self) -> List[str]:
"""
Obtiene lista de símbolos disponibles en el exchange
Returns:
Lista de símbolos
"""
try:
markets = self.exchange.load_markets()
symbols = list(markets.keys())
log.info(f"Símbolos disponibles: {len(symbols)}")
return symbols
except Exception as e:
log.error(f"Error obteniendo símbolos: {e}")
raise