diff --git a/.gitignore b/.gitignore index 4c17f0a..1f4069a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ __pycache__/ # Logs logs/ -# Archivos temporales \ No newline at end of file +# Resultados +backtest_results/ \ No newline at end of file diff --git a/src/backtest/optimizer.py b/src/backtest/optimizer.py index 89af2bf..5914d83 100644 --- a/src/backtest/optimizer.py +++ b/src/backtest/optimizer.py @@ -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" {metric}: {best_result[metric]:.2f}") + 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 -# ============================================================================ - -""" -from src.data.storage import StorageManager -from src.strategies.moving_average import MovingAverageCrossover -from src.backtest.optimizer import ParameterOptimizer - -# Cargar datos -storage = StorageManager(...) -data = storage.load_ohlcv('BTC/USDT', '1h') - -# 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 -... -""" \ No newline at end of file + + def get_top_n_params(self, metric: str = 'sharpe_ratio', n: int = 5) -> List[Dict]: + """ + Obtiene los N mejores conjuntos de parámetros + + Args: + metric: Métrica a optimizar + n: Número de mejores configuraciones + + 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}") \ No newline at end of file diff --git a/src/backtest/visualizer.py b/src/backtest/visualizer.py index 5a74bde..05ec5c2 100644 --- a/src/backtest/visualizer.py +++ b/src/backtest/visualizer.py @@ -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, diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py new file mode 100644 index 0000000..4d3069f --- /dev/null +++ b/tests/test_optimizer.py @@ -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() \ No newline at end of file diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py new file mode 100644 index 0000000..1cf34fc --- /dev/null +++ b/tests/test_visualizer.py @@ -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() \ No newline at end of file