Engine: add stop loss integration (fixed & trailing) with tests

This commit is contained in:
DaM
2026-01-30 17:05:47 +01:00
parent af7b862f60
commit c569170fcc
24 changed files with 1121 additions and 137 deletions

View File

@@ -0,0 +1,39 @@
import sys
from pathlib import Path
import pytest
# Añadir raíz del proyecto al path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from src.risk.sizing.fixed import FixedPositionSizer
def test_fixed_position_size_basic():
sizer = FixedPositionSizer(capital_fraction=0.5)
capital = 10_000
entry_price = 100
units = sizer.calculate_size(
capital=capital,
entry_price=entry_price
)
# 50% de 10k = 5k / 100 = 50 unidades
assert units == 50
def test_fixed_position_size_full_capital():
sizer = FixedPositionSizer(capital_fraction=1.0)
units = sizer.calculate_size(
capital=10_000,
entry_price=200
)
assert units == 50
def test_fixed_invalid_fraction():
with pytest.raises(ValueError):
FixedPositionSizer(capital_fraction=1.5)

View File

@@ -0,0 +1,50 @@
import sys
from pathlib import Path
import pytest
# Añadir raíz del proyecto al path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from src.risk.sizing.percent_risk import PercentRiskSizer
def test_percent_risk_basic():
sizer = PercentRiskSizer(risk_fraction=0.01) # 1%
capital = 10_000
entry_price = 100
stop_price = 95
units = sizer.calculate_size(
capital=capital,
entry_price=entry_price,
stop_price=stop_price
)
# riesgo = 100€
# riesgo por unidad = 5
# unidades = 100 / 5 = 20
assert units == 20
def test_percent_risk_zero_distance():
sizer = PercentRiskSizer(0.01)
units = sizer.calculate_size(
capital=10_000,
entry_price=100,
stop_price=100
)
assert units == 0.0
def test_percent_risk_requires_stop():
sizer = PercentRiskSizer(0.01)
with pytest.raises(ValueError):
sizer.calculate_size(
capital=10_000,
entry_price=100
)

View File

@@ -0,0 +1,42 @@
import sys
from pathlib import Path
import pytest
# Añadir raíz del proyecto al path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from src.risk.sizing.volatility import VolatilitySizer
def test_volatility_sizer_basic():
sizer = VolatilitySizer(volatility_multiplier=2.0)
capital = 10_000
entry_price = 100
volatility = 0.02 # 2%
units = sizer.calculate_size(
capital=capital,
entry_price=entry_price,
volatility=volatility
)
# riesgo por unidad = 0.02 * 100 * 2 = 4
# unidades = 10_000 / 4 = 2500
assert units == 2500
def test_volatility_requires_volatility():
sizer = VolatilitySizer()
with pytest.raises(ValueError):
sizer.calculate_size(
capital=10_000,
entry_price=100
)
def test_volatility_invalid_multiplier():
with pytest.raises(ValueError):
VolatilitySizer(volatility_multiplier=0)

View File

@@ -0,0 +1,73 @@
# src/risk/stops/test_atr_stop.py
import sys
from pathlib import Path
import pandas as pd
import numpy as np
import pytest
# Añadir raíz del proyecto al path
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.risk.stops.atr_stop import ATRStop
from src.backtest.trade import TradeType
def atr_data():
return pd.DataFrame(
{
"high": [10, 11, 12, 13, 14],
"low": [9, 10, 11, 12, 13],
"close": [9.5, 10.5, 11.5, 12.5, 13.5],
},
index=pd.date_range("2024-01-01", periods=5, freq="D"),
)
def test_atr_stop_long():
stop = ATRStop(atr_period=3, multiplier=2.0)
price = stop.get_stop_price(
data=atr_data(),
idx=4,
entry_price=14,
trade_type=TradeType.LONG,
)
assert price < 14 # stop por debajo
def test_atr_stop_short():
stop = ATRStop(atr_period=3, multiplier=2.0)
price = stop.get_stop_price(
data=atr_data(),
idx=4,
entry_price=14,
trade_type=TradeType.SHORT,
)
assert price > 14 # stop por encima
def test_atr_stop_requires_columns():
stop = ATRStop()
bad_data = pd.DataFrame({"close": [1, 2, 3]})
with pytest.raises(ValueError):
stop.get_stop_price(
data=bad_data,
idx=2,
entry_price=3,
trade_type=TradeType.LONG,
)
def test_atr_stop_invalid_params():
with pytest.raises(ValueError):
ATRStop(atr_period=0)
with pytest.raises(ValueError):
ATRStop(multiplier=0)

View File

@@ -0,0 +1,51 @@
# tests/risk/stops/test_fixed_stop.py
import sys
from pathlib import Path
import pandas as pd
import pytest
# Añadir raíz del proyecto al path
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
def dummy_data():
return pd.DataFrame(
{"close": [100, 101, 102]},
index=pd.date_range("2024-01-01", periods=3, freq="D")
)
def test_fixed_stop_long():
stop = FixedStop(0.02)
price = stop.get_stop_price(
data=dummy_data(),
idx=1,
entry_price=100,
trade_type=TradeType.LONG
)
assert price == 98
def test_fixed_stop_short():
stop = FixedStop(0.02)
price = stop.get_stop_price(
data=dummy_data(),
idx=1,
entry_price=100,
trade_type=TradeType.SHORT
)
assert price == 102
def test_fixed_stop_invalid_fraction():
with pytest.raises(ValueError):
FixedStop(0)
with pytest.raises(ValueError):
FixedStop(1)
with pytest.raises(ValueError):
FixedStop(-0.1)