From 7d72669cb838f9657e652b34176ff6dbeef991b3 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:34:44 +0900 Subject: [PATCH] feat: enforce mandatory backtest cost assumptions (TASK-CODE-006) --- src/analysis/backtest_cost_guard.py | 47 +++++++++++++++++++++++ tests/test_backtest_cost_guard.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/analysis/backtest_cost_guard.py create mode 100644 tests/test_backtest_cost_guard.py diff --git a/src/analysis/backtest_cost_guard.py b/src/analysis/backtest_cost_guard.py new file mode 100644 index 0000000..2f4a5bb --- /dev/null +++ b/src/analysis/backtest_cost_guard.py @@ -0,0 +1,47 @@ +"""Backtest cost/slippage/failure validation guard.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@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 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 bps < 0: + raise ValueError(f"slippage bps must be >= 0 for session={sess}") + for sess, rate in failure.items(): + if rate < 0 or rate > 1: + raise ValueError(f"failure rate must be within [0,1] for session={sess}") diff --git a/tests/test_backtest_cost_guard.py b/tests/test_backtest_cost_guard.py new file mode 100644 index 0000000..417925f --- /dev/null +++ b/tests/test_backtest_cost_guard.py @@ -0,0 +1,59 @@ +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"])