# scripts/research/portfolio_backtest.py """ Portfolio Backtest Script (50/50 Allocation) Este script ejecuta un backtest de un portfolio multi-estrategia compuesto por: - MA_Crossover (EMA 30 / EMA 100) - TrendFiltered_MA (EMA 30 / EMA 100 + ADX 15) Cada estrategia opera de forma independiente, pero comparte el riesgo total mediante una asignación fija 50/50 (0.5% de riesgo por estrategia). Objetivos principales del script: - Validar el comportamiento conjunto del portfolio - Evaluar la reducción de drawdown y volatilidad frente a estrategias individuales - Calcular métricas de sistema basadas únicamente en la equity curve (CAGR, Max Drawdown, Calmar Ratio, Time in Drawdown, Ulcer Index) - Generar una visualización de la equity del portfolio y su drawdown Este script representa el último paso cuantitativo antes de: - Walk-Forward del portfolio - Paper trading en tiempo real - Construcción de la UI de monitorización No modifica parámetros, estrategias ni lógica de ejecución. Su propósito es exclusivamente de validación y análisis del sistema. """ import os import sys from pathlib import Path from dotenv import load_dotenv import matplotlib.pyplot as plt import pandas as pd sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from src.core.engine import Engine from src.data.storage import StorageManager from src.risk.sizing.percent_risk import PercentRiskSizer from src.risk.stops.trailing_stop import TrailingStop from src.strategies.moving_average import MovingAverageCrossover from src.strategies.trend_filtered import TrendFilteredMACrossover from src.portfolio.portfolio_engine import PortfolioEngine from src.portfolio.allocation import Allocation from src.metrics.equity_metrics import ( compute_equity_metrics, calculate_drawdown_series, ) # -------------------------------------------------- # CONFIG # -------------------------------------------------- SYMBOL = "BTC/USDT" TIMEFRAME = "1h" INITIAL_CAPITAL = 10_000 STOP = TrailingStop(0.02) # 50/50 risk split RISK_A = PercentRiskSizer(0.005) RISK_B = PercentRiskSizer(0.005) OUTPUT_DIR = Path(__file__).parent / "output/portfolio_backtest" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # -------------------------------------------------- def load_data(): load_dotenv("config/secrets.env") 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"), ) data = storage.load_ohlcv( symbol=SYMBOL, timeframe=TIMEFRAME, use_cache=True, ) storage.close() if data.empty: raise RuntimeError("No data loaded") return data # -------------------------------------------------- def run(): data = load_data() # ----------------------------- # Engines # ----------------------------- engines = { "MA_Crossover": Engine( strategy=MovingAverageCrossover(30, 100, "ema", False), initial_capital=INITIAL_CAPITAL, position_sizer=RISK_A, stop_loss=STOP, commission=0.001, slippage=0.0005, ), "TrendFiltered_MA": Engine( strategy=TrendFilteredMACrossover(30, 100, "ema", 15), initial_capital=INITIAL_CAPITAL, position_sizer=RISK_B, stop_loss=STOP, commission=0.001, slippage=0.0005, ), } allocation = Allocation( weights={ "MA_Crossover": 0.5, "TrendFiltered_MA": 0.5, } ) portfolio = PortfolioEngine( engines=engines, allocation=allocation, initial_capital=INITIAL_CAPITAL, ) # ----------------------------- # Run portfolio # ----------------------------- result = portfolio.run(data) equity = pd.Series(result.equity_curve, index=pd.to_datetime(data.index[:len(result.equity_curve)])) # ----------------------------- # Metrics # ----------------------------- metrics = compute_equity_metrics( equity_curve=result.equity_curve, timestamps=equity.index.tolist(), ) # ----------------------------- # Report # ----------------------------- print("\n" + "=" * 80) print("📊 PORTFOLIO BACKTEST REPORT (50/50)") print("=" * 80) print(f"Final Capital: ${result.final_capital:,.2f}") print(f"CAGR: {metrics['cagr'] * 100:>8.2f}%") print(f"Max Drawdown: {metrics['max_drawdown'] * 100:>8.2f}%") print(f"Calmar Ratio: {metrics['calmar_ratio']:>8.2f}") print(f"Volatility (ann.): {metrics['volatility'] * 100:>8.2f}%") print(f"Time in Drawdown: {metrics['time_in_drawdown'] * 100:>8.2f}%") print(f"Ulcer Index: {metrics['ulcer_index']:>8.2f}") print("=" * 80) # ----------------------------- # Plots # ----------------------------- dd_series = calculate_drawdown_series(equity) fig, (ax1, ax2) = plt.subplots( 2, 1, figsize=(13, 7), sharex=True, gridspec_kw={"height_ratios": [3, 1]} ) ax1.plot(equity.index, equity.values, label="Portfolio Equity") ax1.set_title("Portfolio Equity Curve (50/50)") ax1.set_ylabel("Capital") ax1.grid(alpha=0.3) ax1.legend() ax2.fill_between(dd_series.index, dd_series.values * 100, 0, color="red", alpha=0.3) ax2.set_ylabel("Drawdown (%)") ax2.set_xlabel("Time") ax2.grid(alpha=0.3) plt.tight_layout() path = OUTPUT_DIR / "portfolio_equity_drawdown.png" plt.savefig(path) plt.close() print(f"\n📈 Saved plot: {path}") # -------------------------------------------------- if __name__ == "__main__": run()