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
This commit is contained in:
199
scripts/research/portfolio_backtest.py
Normal file
199
scripts/research/portfolio_backtest.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user