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:
186
scripts/research/compare_stops.py
Normal file
186
scripts/research/compare_stops.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# scripts/research/compare_stops.py
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from loguru import logger
|
||||
|
||||
# Añadir raíz del proyecto al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.utils.logger import log
|
||||
from src.data.storage import StorageManager
|
||||
from src.core.engine import Engine
|
||||
from src.strategies import MovingAverageCrossover
|
||||
from src.risk.sizing.fixed import FixedPositionSizer
|
||||
from src.risk.stops.fixed_stop import FixedStop
|
||||
from src.risk.stops.trailing_stop import TrailingStop
|
||||
from src.risk.stops.atr_stop import ATRStop
|
||||
|
||||
# --------------------------------------------------
|
||||
# Configuración común
|
||||
# --------------------------------------------------
|
||||
SYMBOL = "BTC/USDT"
|
||||
TIMEFRAME = "1d"
|
||||
DAYS_BACK = 180
|
||||
|
||||
INITIAL_CAPITAL = 10_000
|
||||
COMISSION = 0.001
|
||||
SLIPPAGE = 0.0005
|
||||
|
||||
# Estrategia
|
||||
STRATEGY = MovingAverageCrossover(
|
||||
fast_period=10,
|
||||
slow_period=30,
|
||||
ma_type='ema',
|
||||
use_adx=False,
|
||||
adx_threshold=20
|
||||
)
|
||||
|
||||
# Position sizing fijo (para aislar el efecto del stop)
|
||||
POSITION_SIZER = FixedPositionSizer(0.95)
|
||||
|
||||
# Stops a comparar
|
||||
STOPS = {
|
||||
"Fixed 2%": FixedStop(0.02),
|
||||
"Trailing 2%": TrailingStop(0.02),
|
||||
"ATR 14 x 2.0": ATRStop(atr_period=14, multiplier=2.0),
|
||||
}
|
||||
|
||||
def setup_environment():
|
||||
"""Carga variables de entorno"""
|
||||
env_path = Path(__file__).parent.parent.parent / 'config' / 'secrets.env'
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
def load_data():
|
||||
"""
|
||||
Carga datos desde la base de datos
|
||||
"""
|
||||
# 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"),
|
||||
)
|
||||
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(DAYS_BACK)
|
||||
|
||||
data = storage.load_ohlcv(
|
||||
symbol=SYMBOL,
|
||||
timeframe=TIMEFRAME,
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
use_cache=False,
|
||||
)
|
||||
|
||||
storage.close()
|
||||
|
||||
if data.empty:
|
||||
raise RuntimeError("No se cargaron datos")
|
||||
|
||||
return data
|
||||
|
||||
def run():
|
||||
log.info("=" * 80)
|
||||
log.info("🧪 COMPARACIÓN DE STOPS (Research)")
|
||||
log.info("=" * 80)
|
||||
|
||||
data = load_data()
|
||||
|
||||
log.info(f"Símbolo: {SYMBOL}")
|
||||
log.info(f"Timeframe: {TIMEFRAME}")
|
||||
log.info(f"Velas: {len(data)}")
|
||||
log.info(f"Periodo: {data.index[0]} → {data.index[-1]}")
|
||||
print()
|
||||
|
||||
results = {}
|
||||
|
||||
# --------------------------------------------------
|
||||
# Ejecutar backtest por cada stop
|
||||
# --------------------------------------------------
|
||||
for name, stop in STOPS.items():
|
||||
log.info(f"▶️ Ejecutando con stop: {name}")
|
||||
|
||||
# Silenciar logs del engine (solo warnings o errores)
|
||||
logger.disable("src.backtest.engine")
|
||||
|
||||
engine = Engine(
|
||||
strategy=STRATEGY,
|
||||
initial_capital=INITIAL_CAPITAL,
|
||||
commission=COMISSION,
|
||||
slippage=SLIPPAGE,
|
||||
position_sizer=POSITION_SIZER,
|
||||
stop_loss=stop,
|
||||
)
|
||||
|
||||
res = engine.run(data)
|
||||
|
||||
results[name] = res
|
||||
|
||||
logger.enable("src.backtest.engine")
|
||||
|
||||
log.info(
|
||||
f"Trades: {res['total_trades']:>4} | "
|
||||
f"Max DD: {res['max_drawdown_pct']:>7.2f}% | "
|
||||
f"Return: {res['total_return_pct']:>7.2f}%"
|
||||
)
|
||||
print()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 RESUMEN COMPARATIVO DE STOPS")
|
||||
print("=" * 70)
|
||||
|
||||
for name, res in results.items():
|
||||
print(
|
||||
f"{name:<15} | "
|
||||
f"Trades: {res['total_trades']:>4} | "
|
||||
f"Max DD: {res['max_drawdown_pct']:>7.2f}% | "
|
||||
f"Return: {res['total_return_pct']:>7.2f}%"
|
||||
)
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Plot equity curves (comparativa visual)
|
||||
# --------------------------------------------------
|
||||
plt.figure(figsize=(14, 7))
|
||||
|
||||
for name, res in results.items():
|
||||
plt.plot(
|
||||
res["timestamps"],
|
||||
res["equity_curve"],
|
||||
label=name,
|
||||
)
|
||||
|
||||
plt.title(f"Equity Curve Comparison – {SYMBOL} {TIMEFRAME}")
|
||||
plt.xlabel("Time")
|
||||
plt.ylabel("Equity")
|
||||
plt.legend()
|
||||
plt.grid(alpha=0.3)
|
||||
plt.tight_layout()
|
||||
|
||||
# --------------------------------------------------
|
||||
# Guardar gráfico
|
||||
# --------------------------------------------------
|
||||
output_dir = Path(__file__).parent / "output"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_path = output_dir / f"equity_comparison_{SYMBOL.replace('/', '_')}_{TIMEFRAME}.png"
|
||||
|
||||
plt.savefig(output_path, dpi=150)
|
||||
plt.close()
|
||||
|
||||
log.success(f"📈 Equity curve guardada en: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user