Sistema de trading bot - Semanas 1-2 completadas

- Infraestructura de datos completa
- Descarga desde exchanges (CCXT)
- Procesamiento y limpieza de datos
- Almacenamiento en PostgreSQL
- Sistema anti-duplicados
- Script de descarga masiva
- Tests unitarios
- Documentación completa
This commit is contained in:
DaM
2026-01-26 22:16:27 +01:00
parent dcffd9dfad
commit ccd1fb3e42
6 changed files with 674 additions and 158 deletions

View File

@@ -1,4 +1,5 @@
# src/data/fetcher.py
# src/data/fetcher.py
"""
Módulo para obtener datos de exchanges usando CCXT
"""
@@ -13,7 +14,7 @@ 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
@@ -24,24 +25,39 @@ class DataFetcher:
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,
# Configuración base
config = {
'enableRateLimit': True, # Importante para evitar bans
'options': {
'defaultType': 'spot', # spot, future, etc
}
})
log.info(f"Conectado al exchange: {exchange_name}")
}
# Solo añadir API keys si están presentes y no vacías
if api_key and api_secret:
config['apiKey'] = api_key
config['secret'] = api_secret
log.info(f"Conectado al exchange: {exchange_name} (con API keys)")
else:
log.info(f"Conectado al exchange: {exchange_name} (modo público - sin API keys)")
self.exchange = exchange_class(config)
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:
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)
@@ -55,20 +71,20 @@ class DataFetcher:
DataFrame con los datos OHLCV
"""
try:
# Convertir datetime a timestamp en ms
# Convertir datetime a timestamp en milisegundos
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,
@@ -85,13 +101,18 @@ class DataFetcher:
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:
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)
@@ -109,7 +130,11 @@ class DataFetcher:
log.info(f"Iniciando descarga histórica: {symbol} desde {since.date()}")
iteration = 0
while True:
iteration += 1
log.debug(f"Iteración {iteration}: Obteniendo datos desde {since}")
retry_count = 0
success = False
@@ -120,7 +145,7 @@ class DataFetcher:
if df.empty:
log.warning(f"No hay más datos disponibles para {symbol}")
success = True
break
break # Salir del while interno
all_data.append(df)
@@ -131,7 +156,7 @@ class DataFetcher:
# Verificar si ya llegamos al presente
if since >= datetime.now():
success = True
break
break # Salir del while interno
success = True
time.sleep(self.exchange.rateLimit / 1000) # Respetar rate limit
@@ -143,10 +168,10 @@ class DataFetcher:
if not success:
log.error(f"Falló después de {max_retries} intentos")
break
break # Salir del while externo
if since >= datetime.now():
break
if since >= datetime.now() or df.empty:
break # Salir del while externo si no hay más datos
if not all_data:
log.error("No se pudo obtener ningún dato histórico")