- 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
109 lines
2.9 KiB
Python
109 lines
2.9 KiB
Python
# tests/backtest/test_engine_percent_risk.py
|
|
import sys
|
|
from pathlib import Path
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
|
|
# Añadir raíz del proyecto al path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
from src.core.engine import Engine
|
|
from src.core.strategy import Strategy, Signal
|
|
from src.core.trade import TradeStatus
|
|
from src.risk.sizing.percent_risk import PercentRiskSizer
|
|
from src.risk.stops.fixed_stop import FixedStop
|
|
|
|
|
|
# --------------------------------------------------
|
|
# Estrategia dummy
|
|
# --------------------------------------------------
|
|
class AlwaysBuyStrategy(Strategy):
|
|
"""
|
|
Compra en la primera vela y nunca vende.
|
|
El stop debe cerrar la posición.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super().__init__(name="AlwaysBuy", params={})
|
|
|
|
def init_indicators(self, data):
|
|
return data
|
|
|
|
def generate_signal(self, idx: int):
|
|
if idx == 0:
|
|
return Signal.BUY
|
|
return Signal.HOLD
|
|
|
|
|
|
# --------------------------------------------------
|
|
# Test de integración real
|
|
# --------------------------------------------------
|
|
def test_engine_percent_risk_with_fixed_stop():
|
|
"""
|
|
Verifica que:
|
|
- El size se calcula usando el stop
|
|
- El riesgo por trade ≈ % configurado
|
|
- El stop cierra la posición
|
|
"""
|
|
|
|
# -----------------------------
|
|
# Datos simulados
|
|
# -----------------------------
|
|
timestamps = pd.date_range(
|
|
start=datetime(2024, 1, 1),
|
|
periods=5,
|
|
freq="1h",
|
|
)
|
|
|
|
data = pd.DataFrame(
|
|
{
|
|
"open": [100, 100, 100, 100, 100],
|
|
"high": [101, 101, 101, 101, 101],
|
|
"low": [99, 97, 95, 93, 90],
|
|
"close": [100, 98, 96, 94, 91], # rompe stop
|
|
"volume": [1, 1, 1, 1, 1],
|
|
},
|
|
index=timestamps,
|
|
)
|
|
|
|
# -----------------------------
|
|
# Configuración
|
|
# -----------------------------
|
|
initial_capital = 10_000
|
|
risk_fraction = 0.01 # 1% por trade
|
|
stop_fraction = 0.02 # stop al 2%
|
|
|
|
strategy = AlwaysBuyStrategy()
|
|
|
|
engine = Engine(
|
|
strategy=strategy,
|
|
initial_capital=initial_capital,
|
|
commission=0.0,
|
|
slippage=0.0,
|
|
position_sizer=PercentRiskSizer(risk_fraction),
|
|
stop_loss=FixedStop(stop_fraction),
|
|
)
|
|
|
|
# -----------------------------
|
|
# Ejecutar backtest
|
|
# -----------------------------
|
|
engine.run(data)
|
|
|
|
# -----------------------------
|
|
# Assertions
|
|
# -----------------------------
|
|
assert len(engine.trades) == 1
|
|
|
|
trade = engine.trades[0]
|
|
|
|
# Trade cerrado por stop
|
|
assert trade.status == TradeStatus.CLOSED
|
|
assert trade.exit_reason == "Stop Loss"
|
|
|
|
# Riesgo real ≈ riesgo esperado
|
|
expected_risk = initial_capital * risk_fraction
|
|
actual_loss = abs(trade.pnl)
|
|
|
|
# Permitimos pequeño error numérico
|
|
assert abs(actual_loss - expected_risk) / expected_risk < 0.05
|