Files
Trading-Bot/scripts/research/portfolio_backtest.py
DaM f85c522f22 feat: finalize portfolio system and quantitative validation- Finalized MA_Crossover(30,100) and TrendFiltered_MA(30,100,ADX=15)
- Implemented portfolio engine with risk-based allocation (50/50)
- Added equity-based metrics for system-level evaluation
- Validated portfolio against standalone strategies
- Reduced max drawdown and volatility at system level
- Quantitative decision closed before paper trading phase
2026-02-02 14:38:05 +01:00

200 lines
5.7 KiB
Python

# 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()