Archivos añadidos/actuaizados para comentar el bot con ChatGPT

This commit is contained in:
DaM
2026-01-28 09:42:16 +01:00
parent 9ab1f7fadc
commit 1add69eb56
5 changed files with 353 additions and 85 deletions

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ __pycache__/
# Logs
logs/
# Archivos temporales
# Resultados
backtest_results/

View File

@@ -2,6 +2,7 @@
"""
Optimizador de parámetros para estrategias
"""
import os
import pandas as pd
from typing import Dict, List, Any, Type
from itertools import product
@@ -18,18 +19,24 @@ class ParameterOptimizer:
strategy_class: Type[Strategy],
data: pd.DataFrame,
initial_capital: float = 10000,
commission: float = 0.001):
commission: float = 0.001,
slippage: float = 0.0005,
position_size: float = 0.95):
"""
Args:
strategy_class: Clase de estrategia (no instancia)
data: Datos para backtest
initial_capital: Capital inicial
commission: Comisión por trade
slippage: Slippage simulado
position_size: Fracción del capital por trade
"""
self.strategy_class = strategy_class
self.data = data
self.initial_capital = initial_capital
self.commission = commission
self.slippage = slippage
self.position_size = position_size
self.results: List[Dict] = []
@@ -46,21 +53,29 @@ class ParameterOptimizer:
}
Returns:
DataFrame con resultados ordenados por retorno
DataFrame con resultados (sin ordenar)
"""
# Limpiar resultados previos
self.results = []
# Generar todas las combinaciones posibles
param_names = list(param_grid.keys())
param_values = list(param_grid.values())
combinations = list(product(*param_values))
total_tests = len(combinations)
log.info(f"Iniciando optimización: {total_tests} combinaciones a probar")
log.info(f"🔧 Iniciando optimización: {total_tests} combinaciones")
# Probar cada combinación
successful = 0
failed = 0
for i, values in enumerate(combinations, 1):
params = dict(zip(param_names, values))
log.debug(f"[{i}/{total_tests}] Probando: {params}")
# Mostrar progreso cada 10 tests o en el primero
if i % 10 == 0 or i == 1 or i == total_tests:
log.info(f" [{i}/{total_tests}] Probando: {params}")
try:
# Crear estrategia con estos parámetros
@@ -70,7 +85,9 @@ class ParameterOptimizer:
engine = BacktestEngine(
strategy=strategy,
initial_capital=self.initial_capital,
commission=self.commission
commission=self.commission,
slippage=self.slippage,
position_size=self.position_size
)
results = engine.run(self.data)
@@ -84,30 +101,57 @@ class ParameterOptimizer:
'total_trades': results['total_trades'],
'win_rate_pct': results['win_rate_pct'],
'profit_factor': results['profit_factor'],
'final_equity': results['final_equity'],
}
self.results.append(result_entry)
successful += 1
except Exception as e:
log.error(f"Error con parámetros {params}: {e}")
log.error(f"Error con {params}: {e}")
failed += 1
continue
# Convertir a DataFrame
if not self.results:
log.error("❌ No se obtuvieron resultados válidos")
return pd.DataFrame()
df_results = pd.DataFrame(self.results)
# Ordenar por retorno (mejor primero)
df_results = df_results.sort_values('total_return_pct', ascending=False)
log.success(f"✅ Optimización completa: {successful} exitosos, {failed} fallidos")
log.success(f"Optimización completa: {len(df_results)} resultados válidos")
# Mostrar top 5 por Sharpe Ratio
self._print_top_results(df_results, param_names, metric='sharpe_ratio', top_n=5)
return df_results
def get_best_params(self, metric: str = 'total_return_pct') -> Dict:
def _print_top_results(self, df: pd.DataFrame, param_names: List[str],
metric: str = 'sharpe_ratio', top_n: int = 5):
"""
Muestra los mejores resultados según una métrica
"""
# Ordenar por la métrica (descendente para la mayoría)
ascending = True if metric in ['max_drawdown_pct'] else False
df_sorted = df.sort_values(metric, ascending=ascending)
log.info(f"\n🏆 TOP {top_n} POR {metric.upper()}:")
for i, (idx, row) in enumerate(df_sorted.head(top_n).iterrows(), 1):
param_str = ", ".join([f"{k}={row[k]}" for k in param_names])
log.info(f" #{i}: {param_str}")
log.info(f"{metric}: {row[metric]:.2f}, "
f"Return: {row['total_return_pct']:.2f}%, "
f"Trades: {int(row['total_trades'])}, "
f"Win Rate: {row['win_rate_pct']:.1f}%")
def get_best_params(self, metric: str = 'sharpe_ratio') -> Dict:
"""
Obtiene los mejores parámetros según una métrica
Args:
metric: Métrica a optimizar ('total_return_pct', 'sharpe_ratio', etc)
metric: Métrica a optimizar
('sharpe_ratio', 'total_return_pct', 'profit_factor', etc)
Returns:
Diccionario con los mejores parámetros
@@ -117,9 +161,8 @@ class ParameterOptimizer:
df_results = pd.DataFrame(self.results)
# Ordenar por la métrica elegida
# Para drawdown queremos el MENOR (menos negativo = más cercano a 0)
if metric in ['max_drawdown_pct']:
# Para drawdown, queremos el MENOR (menos negativo)
best_idx = df_results[metric].idxmax()
else:
# Para otras métricas, queremos el MAYOR
@@ -127,69 +170,97 @@ class ParameterOptimizer:
best_result = df_results.loc[best_idx]
# Extraer solo los parámetros (no las métricas)
param_names = [col for col in df_results.columns
if col not in ['total_return_pct', 'sharpe_ratio',
'max_drawdown_pct', 'total_trades',
'win_rate_pct', 'profit_factor']]
# Extraer solo parámetros (no métricas)
metric_cols = ['total_return_pct', 'sharpe_ratio', 'max_drawdown_pct',
'total_trades', 'win_rate_pct', 'profit_factor', 'final_equity']
param_names = [col for col in df_results.columns if col not in metric_cols]
best_params = {param: best_result[param] for param in param_names}
log.info(f"Mejores parámetros según {metric}: {best_params}")
log.info(f"\n🎯 MEJORES PARÁMETROS SEGÚN {metric.upper()}:")
log.info(f" Parámetros: {best_params}")
log.info(f" {metric}: {best_result[metric]:.2f}")
log.info(f" Retorno: {best_result['total_return_pct']:.2f}%")
log.info(f" Sharpe: {best_result['sharpe_ratio']:.2f}")
log.info(f" Max DD: {best_result['max_drawdown_pct']:.2f}%")
log.info(f" Trades: {int(best_result['total_trades'])}")
return best_params
# ============================================================================
# Ejemplo de Uso
# ============================================================================
def get_top_n_params(self, metric: str = 'sharpe_ratio', n: int = 5) -> List[Dict]:
"""
from src.data.storage import StorageManager
from src.strategies.moving_average import MovingAverageCrossover
from src.backtest.optimizer import ParameterOptimizer
Obtiene los N mejores conjuntos de parámetros
# Cargar datos
storage = StorageManager(...)
data = storage.load_ohlcv('BTC/USDT', '1h')
Args:
metric: Métrica a optimizar
n: Número de mejores configuraciones
# Crear optimizador
optimizer = ParameterOptimizer(
strategy_class=MovingAverageCrossover, # Clase, no instancia
data=data,
initial_capital=10000,
commission=0.001
)
# Definir parámetros a probar
param_grid = {
'fast_period': [5, 10, 15, 20, 25],
'slow_period': [30, 50, 70, 100, 150],
'ma_type': ['sma', 'ema']
}
# Ejecutar optimización (probará 5 × 5 × 2 = 50 combinaciones)
results_df = optimizer.optimize(param_grid)
# Ver mejores resultados
print(results_df.head(10))
# Obtener mejores parámetros
best_params = optimizer.get_best_params(metric='sharpe_ratio')
print(f"Mejores parámetros: {best_params}")
# Usar los mejores parámetros
best_strategy = MovingAverageCrossover(**best_params)
```
---
## 📊 Output esperado:
```
fast_period slow_period ma_type total_return_pct sharpe_ratio max_drawdown_pct
0 15 50 ema 45.20 2.10 -12.30
1 10 30 sma 42.80 1.95 -15.20
2 20 70 ema 38.50 1.85 -14.10
3 5 30 sma 35.20 1.75 -18.50
...
Returns:
Lista de diccionarios con parámetros
"""
if not self.results:
raise ValueError("No hay resultados. Ejecuta optimize() primero.")
df_results = pd.DataFrame(self.results)
# Ordenar
ascending = True if metric in ['max_drawdown_pct'] else False
df_sorted = df_results.sort_values(metric, ascending=ascending)
# Extraer parámetros
metric_cols = ['total_return_pct', 'sharpe_ratio', 'max_drawdown_pct',
'total_trades', 'win_rate_pct', 'profit_factor', 'final_equity']
param_names = [col for col in df_results.columns if col not in metric_cols]
top_params = []
for idx, row in df_sorted.head(n).iterrows():
params = {param: row[param] for param in param_names}
top_params.append(params)
return top_params
def save_results(self, filename: str = 'backtest_results/optimization_results.csv'):
"""
Guarda resultados en CSV
Args:
filename: Nombre del archivo (puede incluir ruta)
"""
if not self.results:
log.warning("⚠️ No hay resultados para guardar")
return
# Crear directorio si no existe
os.makedirs(os.path.dirname(filename), exist_ok=True)
df = pd.DataFrame(self.results)
# Ordenar por Sharpe Ratio antes de guardar
df = df.sort_values('sharpe_ratio', ascending=False)
df.to_csv(filename, index=False)
log.success(f"💾 Resultados guardados en: {filename}")
log.info(f" Total filas: {len(df)}")
def compare_metrics(self, metrics: List[str] = None):
"""
Compara mejores parámetros según diferentes métricas
Args:
metrics: Lista de métricas a comparar
"""
if not self.results:
raise ValueError("No hay resultados. Ejecuta optimize() primero.")
if metrics is None:
metrics = ['sharpe_ratio', 'total_return_pct', 'profit_factor', 'max_drawdown_pct']
log.info("\n📊 COMPARACIÓN DE MÉTRICAS:")
log.info("="*70)
for metric in metrics:
try:
best_params = self.get_best_params(metric)
log.info("") # Línea en blanco para separar
except Exception as e:
log.error(f"Error con métrica {metric}: {e}")

View File

@@ -262,18 +262,18 @@ class BacktestVisualizer:
MÉTRICAS DE PERFORMANCE
━━━━━━━━━━━━━━━━━━━━━━━━━
💰 Capital Inicial: ${self.results['initial_capital']:,.0f}
💰 Capital Final: ${self.results['final_equity']:,.0f}
📈 Retorno Total: {self.results['total_return_pct']:.2f}%
+ Capital Inicial: ${self.results['initial_capital']:,.0f}
+ Capital Final: ${self.results['final_equity']:,.0f}
Retorno Total: {self.results['total_return_pct']:.2f}%
📊 Total Trades: {self.results['total_trades']}
Trades Ganadores: {self.results['winning_trades']}
Trades Perdedores: {self.results['losing_trades']}
🎯 Win Rate: {self.results['win_rate_pct']:.2f}%
· Total Trades: {self.results['total_trades']}
Trades Ganadores: {self.results['winning_trades']}
× Trades Perdedores: {self.results['losing_trades']}
Win Rate: {self.results['win_rate_pct']:.2f}%
📉 Max Drawdown: {self.results['max_drawdown_pct']:.2f}%
📈 Sharpe Ratio: {self.results['sharpe_ratio']:.2f}
💵 Profit Factor: {self.results['profit_factor']:.2f}
Max Drawdown: {self.results['max_drawdown_pct']:.2f}%
· Sharpe Ratio: {self.results['sharpe_ratio']:.2f}
$ Profit Factor: {self.results['profit_factor']:.2f}
"""
ax4.text(0.1, 0.5, metrics_text, fontsize=11,

100
tests/test_optimizer.py Normal file
View File

@@ -0,0 +1,100 @@
# test_optimizer.py
"""
Script para probar el optimizador de parámetros
"""
import os
import sys
from dotenv import load_dotenv
from pathlib import Path
from datetime import datetime, timedelta
# Añadir raíz del proyecto al path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.logger import log
from src.data.storage import StorageManager
from src.strategies import MovingAverageCrossover
from src.backtest.optimizer import ParameterOptimizer
def setup_environment():
"""Carga variables de entorno"""
env_path = Path(__file__).parent.parent / 'config' / 'secrets.env'
load_dotenv(dotenv_path=env_path)
def test_optimizer():
"""
Prueba el optimizador con Moving Average Crossover
"""
log.info("="*70)
log.info("🔧 TEST: OPTIMIZADOR DE PARÁMETROS")
log.info("="*70)
# Setup
setup_environment()
# Cargar datos
storage = StorageManager(
db_host=os.getenv('DB_HOST'),
db_port=int(os.getenv('DB_PORT', 5432)),
db_name=os.getenv('DB_NAME'),
db_user=os.getenv('DB_USER'),
db_password=os.getenv('DB_PASSWORD'),
)
log.info("\n📥 Cargando datos...")
end_date = datetime.now()
start_date = end_date - timedelta(days=60)
data = storage.load_ohlcv(
symbol='BTC/USDT',
timeframe='1h',
start_date=start_date,
end_date=end_date,
use_cache=False
)
log.success(f"✓ Datos cargados: {len(data)} velas")
# Crear optimizador
log.info("\n🔧 Creando optimizador...")
optimizer = ParameterOptimizer(
strategy_class=MovingAverageCrossover,
data=data,
initial_capital=10000,
commission=0.001
)
# Definir parámetros a probar (pequeño para empezar)
param_grid = {
'fast_period': [5, 10, 15],
'slow_period': [30, 50],
'ma_type': ['sma', 'ema']
}
log.info(f"\n📊 Grid de parámetros:")
log.info(f" fast_period: {param_grid['fast_period']}")
log.info(f" slow_period: {param_grid['slow_period']}")
log.info(f" ma_type: {param_grid['ma_type']}")
log.info(f" Total combinaciones: {3 * 2 * 2} = 12")
# Optimizar
log.info("\n🚀 Iniciando optimización...")
results_df = optimizer.optimize(param_grid)
# Mostrar resultados
log.info("\n📊 RESULTADOS COMPLETOS:")
print(results_df.to_string(index=False))
# Mejores parámetros
log.info("\n🏆 ANÁLISIS:")
best_params = optimizer.get_best_params(metric='sharpe_ratio')
# Guardar resultados (OPCIONAL - ya se guarda por defecto)
optimizer.save_results()
storage.close()
log.success("\n✅ TEST COMPLETADO")
if __name__ == "__main__":
test_optimizer()

96
tests/test_visualizer.py Normal file
View File

@@ -0,0 +1,96 @@
# test_visualizer.py
"""
Script para probar las visualizaciones
"""
import os
import sys
from dotenv import load_dotenv
from pathlib import Path
from datetime import datetime, timedelta
# Añadir raíz del proyecto al path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.logger import log
from src.data.storage import StorageManager
from src.strategies import MovingAverageCrossover
from src.backtest import BacktestEngine
from src.backtest.visualizer import BacktestVisualizer
def setup_environment():
"""Carga variables de entorno"""
env_path = Path(__file__).parent.parent / 'config' / 'secrets.env'
load_dotenv(dotenv_path=env_path)
def test_visualizer():
"""
Prueba las visualizaciones con un backtest
"""
log.info("="*70)
log.info("📊 TEST: VISUALIZACIONES")
log.info("="*70)
# Setup
setup_environment()
# Cargar datos
storage = StorageManager(
db_host=os.getenv('DB_HOST'),
db_port=int(os.getenv('DB_PORT', 5432)),
db_name=os.getenv('DB_NAME'),
db_user=os.getenv('DB_USER'),
db_password=os.getenv('DB_PASSWORD'),
)
log.info("\n📥 Cargando datos...")
end_date = datetime.now()
start_date = end_date - timedelta(days=60)
data = storage.load_ohlcv(
symbol='BTC/USDT',
timeframe='1h',
start_date=start_date,
end_date=end_date,
use_cache=False
)
log.success(f"✓ Datos cargados: {len(data)} velas")
# Ejecutar backtest
log.info("\n🧪 Ejecutando backtest...")
strategy = MovingAverageCrossover(fast_period=15, slow_period=50, ma_type='sma')
engine = BacktestEngine(
strategy=strategy,
initial_capital=10000,
commission=0.001,
position_size=0.95
)
results = engine.run(data)
log.info(f" Retorno: {results['total_return_pct']:.2f}%")
log.info(f" Trades: {results['total_trades']}")
# Crear visualizador
log.info("\n📊 Generando visualizaciones...")
viz = BacktestVisualizer(results, data)
# Generar todos los gráficos
viz.generate_all_plots('backtest_results')
log.info("\n💡 Los gráficos se guardaron en: backtest_results/")
log.info(" Archivos generados:")
log.info(" - equity_curve.png")
log.info(" - drawdown.png")
log.info(" - returns_distribution.png")
log.info(" - trades_chart.png")
log.info(" - dashboard.png")
storage.close()
log.success("\n✅ TEST COMPLETADO")
log.info("\n👀 Abre la carpeta 'backtest_results/' para ver los gráficos")
if __name__ == "__main__":
test_visualizer()