- 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
193 lines
5.4 KiB
Python
193 lines
5.4 KiB
Python
# scripts/research/compare_systems.py
|
|
|
|
"""
|
|
Comparación cuantitativa final entre:
|
|
- MA_Crossover (standalone)
|
|
- TrendFiltered_MA (standalone)
|
|
- Portfolio 50/50
|
|
|
|
Este script cierra la decisión cuantitativa antes de pasar a paper trading.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from dotenv import load_dotenv
|
|
|
|
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
|
|
|
|
|
|
# --------------------------------------------------
|
|
# CONFIG
|
|
# --------------------------------------------------
|
|
SYMBOL = "BTC/USDT"
|
|
TIMEFRAME = "1h"
|
|
INITIAL_CAPITAL = 10_000
|
|
|
|
STOP = TrailingStop(0.02)
|
|
|
|
|
|
# --------------------------------------------------
|
|
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_engine(engine: Engine, data) -> dict:
|
|
res = engine.run(data)
|
|
equity = res["equity_curve"]
|
|
timestamps = res["timestamps"]
|
|
|
|
metrics = compute_equity_metrics(
|
|
equity_curve=equity,
|
|
timestamps=timestamps,
|
|
)
|
|
|
|
return {
|
|
"final_capital": equity[-1],
|
|
**metrics,
|
|
}
|
|
|
|
|
|
# --------------------------------------------------
|
|
def run():
|
|
data = load_data()
|
|
|
|
systems = {}
|
|
|
|
# --------------------------------------------------
|
|
# MA_Crossover standalone (1% risk)
|
|
# --------------------------------------------------
|
|
systems["MA_Crossover"] = run_engine(
|
|
Engine(
|
|
strategy=MovingAverageCrossover(30, 100, "ema", False),
|
|
initial_capital=INITIAL_CAPITAL,
|
|
position_sizer=PercentRiskSizer(0.01),
|
|
stop_loss=STOP,
|
|
commission=0.001,
|
|
slippage=0.0005,
|
|
),
|
|
data,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# TrendFiltered_MA standalone (1% risk)
|
|
# --------------------------------------------------
|
|
systems["TrendFiltered_MA"] = run_engine(
|
|
Engine(
|
|
strategy=TrendFilteredMACrossover(30, 100, "ema", 15),
|
|
initial_capital=INITIAL_CAPITAL,
|
|
position_sizer=PercentRiskSizer(0.01),
|
|
stop_loss=STOP,
|
|
commission=0.001,
|
|
slippage=0.0005,
|
|
),
|
|
data,
|
|
)
|
|
|
|
# --------------------------------------------------
|
|
# Portfolio 50/50 (0.5% + 0.5%)
|
|
# --------------------------------------------------
|
|
engines = {
|
|
"MA_Crossover": Engine(
|
|
strategy=MovingAverageCrossover(30, 100, "ema", False),
|
|
initial_capital=INITIAL_CAPITAL,
|
|
position_sizer=PercentRiskSizer(0.005),
|
|
stop_loss=STOP,
|
|
commission=0.001,
|
|
slippage=0.0005,
|
|
),
|
|
"TrendFiltered_MA": Engine(
|
|
strategy=TrendFilteredMACrossover(30, 100, "ema", 15),
|
|
initial_capital=INITIAL_CAPITAL,
|
|
position_sizer=PercentRiskSizer(0.005),
|
|
stop_loss=STOP,
|
|
commission=0.001,
|
|
slippage=0.0005,
|
|
),
|
|
}
|
|
|
|
portfolio = PortfolioEngine(
|
|
engines=engines,
|
|
allocation=Allocation(
|
|
weights={
|
|
"MA_Crossover": 0.5,
|
|
"TrendFiltered_MA": 0.5,
|
|
}
|
|
),
|
|
initial_capital=INITIAL_CAPITAL,
|
|
)
|
|
|
|
portfolio_res = portfolio.run(data)
|
|
|
|
systems["Portfolio_50_50"] = {
|
|
"final_capital": portfolio_res.final_capital,
|
|
**compute_equity_metrics(
|
|
equity_curve=portfolio_res.equity_curve,
|
|
timestamps=data.index[: len(portfolio_res.equity_curve)],
|
|
),
|
|
}
|
|
|
|
# --------------------------------------------------
|
|
# COMPARISON TABLE
|
|
# --------------------------------------------------
|
|
df = pd.DataFrame(systems).T
|
|
|
|
df["final_capital"] = df["final_capital"].round(2)
|
|
df["cagr"] = (df["cagr"] * 100).round(2)
|
|
df["max_drawdown"] = (df["max_drawdown"] * 100).round(2)
|
|
df["volatility"] = (df["volatility"] * 100).round(2)
|
|
df["time_in_drawdown"] = (df["time_in_drawdown"] * 100).round(2)
|
|
df["calmar_ratio"] = df["calmar_ratio"].round(2)
|
|
df["ulcer_index"] = df["ulcer_index"].round(2)
|
|
|
|
print("\n" + "=" * 100)
|
|
print("📊 SYSTEM COMPARISON (FINAL DECISION)")
|
|
print("=" * 100)
|
|
print(df)
|
|
print("=" * 100)
|
|
|
|
print("\n📌 Interpretación:")
|
|
print("- El mejor sistema NO es el que tiene mayor capital final.")
|
|
print("- Prioriza menor drawdown, mejor Calmar y menor Ulcer Index.")
|
|
print("- Si el portfolio domina en riesgo/retorno → decisión cerrada.\n")
|
|
|
|
|
|
# --------------------------------------------------
|
|
if __name__ == "__main__":
|
|
run()
|