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:
108
tests/backtest/test_engine_percent_risk.py
Normal file
108
tests/backtest/test_engine_percent_risk.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
@@ -9,8 +9,8 @@ from datetime import datetime, timedelta
|
||||
# Añadir raíz del proyecto al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.backtest.engine import BacktestEngine
|
||||
from src.backtest.strategy import Strategy, Signal
|
||||
from src.core.engine import Engine
|
||||
from src.core.strategy import Strategy, Signal
|
||||
from src.risk.sizing.fixed import FixedPositionSizer
|
||||
|
||||
class BuyOnceStrategy(Strategy):
|
||||
@@ -66,7 +66,7 @@ def test_engine_uses_fixed_position_sizer():
|
||||
|
||||
sizer = FixedPositionSizer(capital_fraction=0.5)
|
||||
|
||||
engine = BacktestEngine(
|
||||
engine = Engine(
|
||||
strategy=strategy,
|
||||
initial_capital=10000,
|
||||
commission=0.0,
|
||||
|
||||
@@ -6,9 +6,9 @@ from datetime import datetime
|
||||
# Añadir raíz del proyecto al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.backtest.engine import BacktestEngine
|
||||
from src.backtest.strategy import Strategy, Signal
|
||||
from src.backtest.trade import TradeStatus
|
||||
from src.core.engine import Engine
|
||||
from src.core.strategy import Strategy, Signal
|
||||
from src.core.trade import TradeStatus
|
||||
from src.risk.stops.fixed_stop import FixedStop
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def test_engine_closes_position_on_stop_hit():
|
||||
data = _build_test_data()
|
||||
strategy = AlwaysBuyStrategy()
|
||||
|
||||
engine = BacktestEngine(
|
||||
engine = Engine(
|
||||
strategy=strategy,
|
||||
initial_capital=10_000,
|
||||
commission=0.0,
|
||||
@@ -82,7 +82,7 @@ def test_engine_closes_position_at_end_without_stop():
|
||||
data = _build_test_data()
|
||||
strategy = AlwaysBuyStrategy()
|
||||
|
||||
engine = BacktestEngine(
|
||||
engine = Engine(
|
||||
strategy=strategy,
|
||||
initial_capital=10_000,
|
||||
commission=0.0,
|
||||
|
||||
@@ -7,9 +7,9 @@ from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.backtest.engine import BacktestEngine
|
||||
from src.backtest.strategy import Strategy, Signal
|
||||
from src.backtest.trade import TradeStatus
|
||||
from src.core.engine import Engine
|
||||
from src.core.strategy import Strategy, Signal
|
||||
from src.core.trade import TradeStatus
|
||||
from src.risk.stops.trailing_stop import TrailingStop
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_trailing_stop_moves_and_closes_position():
|
||||
|
||||
strategy = AlwaysBuyStrategy()
|
||||
|
||||
engine = BacktestEngine(
|
||||
engine = Engine(
|
||||
strategy=strategy,
|
||||
initial_capital=10000,
|
||||
commission=0.0,
|
||||
|
||||
@@ -12,7 +12,7 @@ import pandas as pd
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.data.storage import StorageManager
|
||||
from src.backtest.walk_forward import WalkForwardValidator
|
||||
from src.core.walk_forward import WalkForwardValidator
|
||||
from src.strategies import MovingAverageCrossover
|
||||
|
||||
def setup_environment():
|
||||
|
||||
@@ -9,9 +9,9 @@ import pytest
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from src.risk.stops.base import StopLoss
|
||||
from src.backtest.trade import TradeType
|
||||
from src.core.trade import TradeType
|
||||
from src.risk.stops.atr_stop import ATRStop
|
||||
from src.backtest.trade import TradeType
|
||||
from src.core.trade import TradeType
|
||||
|
||||
|
||||
def atr_data():
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from src.risk.stops.fixed_stop import FixedStop
|
||||
from src.backtest.trade import TradeType
|
||||
from src.core.trade import TradeType
|
||||
|
||||
|
||||
def dummy_data():
|
||||
|
||||
@@ -14,7 +14,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from src.utils.logger import log
|
||||
from src.data.storage import StorageManager
|
||||
from src.strategies import MovingAverageCrossover
|
||||
from src.backtest.optimizer import ParameterOptimizer
|
||||
from src.core.optimizer import ParameterOptimizer
|
||||
|
||||
def setup_environment():
|
||||
"""Carga variables de entorno"""
|
||||
|
||||
@@ -14,8 +14,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from src.utils.logger import log
|
||||
from src.data.storage import StorageManager
|
||||
from src.strategies import MovingAverageCrossover
|
||||
from src.backtest import BacktestEngine
|
||||
from src.backtest.visualizers.visualizer import BacktestVisualizer
|
||||
from src.core import BacktestEngine
|
||||
from src.core.visualizers.visualizer import BacktestVisualizer
|
||||
|
||||
def setup_environment():
|
||||
"""Carga variables de entorno"""
|
||||
|
||||
@@ -15,7 +15,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from src.utils.logger import log
|
||||
from src.data.storage import StorageManager
|
||||
from src.strategies import MovingAverageCrossover
|
||||
from src.backtest.walk_forward import WalkForwardValidator
|
||||
from src.core.walk_forward import WalkForwardValidator
|
||||
|
||||
|
||||
def setup_environment():
|
||||
|
||||
@@ -7,7 +7,7 @@ import pandas as pd
|
||||
# Añadir raíz del proyecto al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.backtest.visualizers.walk_forward_visualizer import WalkForwardVisualizer
|
||||
from src.core.visualizers.walk_forward_visualizer import WalkForwardVisualizer
|
||||
|
||||
|
||||
def test_wf_visualizer():
|
||||
|
||||
Reference in New Issue
Block a user