From 7d72669cb838f9657e652b34176ff6dbeef991b3 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:34:44 +0900 Subject: [PATCH 1/2] 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"]) -- 2.49.1 From 2331d80915d7ad4cd100ccd501fd524c387249e0 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:36:38 +0900 Subject: [PATCH 2/2] fix: reject non-finite backtest cost assumptions --- src/analysis/backtest_cost_guard.py | 11 ++++++++--- tests/test_backtest_cost_guard.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/analysis/backtest_cost_guard.py b/src/analysis/backtest_cost_guard.py index 2f4a5bb..8f2cf98 100644 --- a/src/analysis/backtest_cost_guard.py +++ b/src/analysis/backtest_cost_guard.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +import math @dataclass(frozen=True) @@ -19,7 +20,11 @@ def validate_backtest_cost_model( 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: + 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") @@ -40,8 +45,8 @@ def validate_backtest_cost_model( ) for sess, bps in slippage.items(): - if bps < 0: + 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 rate < 0 or rate > 1: + if not math.isfinite(rate) or 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 index 417925f..6c73a30 100644 --- a/tests/test_backtest_cost_guard.py +++ b/tests/test_backtest_cost_guard.py @@ -57,3 +57,27 @@ def test_unfavorable_fill_requirement_cannot_be_disabled() -> None: ) 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"]) -- 2.49.1