Compare commits
3 Commits
74a4784b7a
...
2798558bf3
| Author | SHA1 | Date | |
|---|---|---|---|
| 2798558bf3 | |||
|
|
2331d80915 | ||
|
|
7d72669cb8 |
52
src/analysis/backtest_cost_guard.py
Normal file
52
src/analysis/backtest_cost_guard.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Backtest cost/slippage/failure validation guard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BacktestCostModel:
|
||||||
|
commission_bps: float | None = None
|
||||||
|
slippage_bps_by_session: dict[str, float] | None = None
|
||||||
|
failure_rate_by_session: dict[str, float] | None = None
|
||||||
|
unfavorable_fill_required: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_backtest_cost_model(
|
||||||
|
*,
|
||||||
|
model: BacktestCostModel,
|
||||||
|
required_sessions: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Raise ValueError when required cost assumptions are missing/invalid."""
|
||||||
|
if (
|
||||||
|
model.commission_bps is None
|
||||||
|
or not math.isfinite(model.commission_bps)
|
||||||
|
or model.commission_bps < 0
|
||||||
|
):
|
||||||
|
raise ValueError("commission_bps must be provided and >= 0")
|
||||||
|
if not model.unfavorable_fill_required:
|
||||||
|
raise ValueError("unfavorable_fill_required must be True")
|
||||||
|
|
||||||
|
slippage = model.slippage_bps_by_session or {}
|
||||||
|
failure = model.failure_rate_by_session or {}
|
||||||
|
|
||||||
|
missing_slippage = [s for s in required_sessions if s not in slippage]
|
||||||
|
if missing_slippage:
|
||||||
|
raise ValueError(
|
||||||
|
f"missing slippage_bps_by_session for sessions: {', '.join(missing_slippage)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_failure = [s for s in required_sessions if s not in failure]
|
||||||
|
if missing_failure:
|
||||||
|
raise ValueError(
|
||||||
|
f"missing failure_rate_by_session for sessions: {', '.join(missing_failure)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for sess, bps in slippage.items():
|
||||||
|
if not math.isfinite(bps) or bps < 0:
|
||||||
|
raise ValueError(f"slippage bps must be >= 0 for session={sess}")
|
||||||
|
for sess, rate in failure.items():
|
||||||
|
if not math.isfinite(rate) or rate < 0 or rate > 1:
|
||||||
|
raise ValueError(f"failure rate must be within [0,1] for session={sess}")
|
||||||
83
tests/test_backtest_cost_guard.py
Normal file
83
tests/test_backtest_cost_guard.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.analysis.backtest_cost_guard import BacktestCostModel, validate_backtest_cost_model
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_backtest_cost_model_passes() -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0, "US_PRE": 50.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.01, "US_PRE": 0.08},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG", "US_PRE"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_required_slippage_session_raises() -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.01, "US_PRE": 0.08},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="missing slippage_bps_by_session.*US_PRE"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG", "US_PRE"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_required_failure_rate_session_raises() -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0, "US_PRE": 50.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.01},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="missing failure_rate_by_session.*US_PRE"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG", "US_PRE"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_failure_rate_range_raises() -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 1.2},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="failure rate must be within"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_unfavorable_fill_requirement_cannot_be_disabled() -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.02},
|
||||||
|
unfavorable_fill_required=False,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="unfavorable_fill_required must be True"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_commission", [float("nan"), float("inf"), float("-inf")])
|
||||||
|
def test_non_finite_commission_rejected(bad_commission: float) -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=bad_commission,
|
||||||
|
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.02},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="commission_bps"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_slippage", [float("nan"), float("inf"), float("-inf")])
|
||||||
|
def test_non_finite_slippage_rejected(bad_slippage: float) -> None:
|
||||||
|
model = BacktestCostModel(
|
||||||
|
commission_bps=5.0,
|
||||||
|
slippage_bps_by_session={"KRX_REG": bad_slippage},
|
||||||
|
failure_rate_by_session={"KRX_REG": 0.02},
|
||||||
|
unfavorable_fill_required=True,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="slippage bps"):
|
||||||
|
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||||
Reference in New Issue
Block a user