analysis: apply execution-adjusted cost model in v2 backtest pipeline (#368)
This commit is contained in:
@@ -10,6 +10,7 @@ def test_valid_backtest_cost_model_passes() -> None:
|
||||
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},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1, "US_PRE": 0.2},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG", "US_PRE"])
|
||||
@@ -20,6 +21,7 @@ def test_missing_required_slippage_session_raises() -> None:
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.01, "US_PRE": 0.08},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1, "US_PRE": 0.2},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="missing slippage_bps_by_session.*US_PRE"):
|
||||
@@ -31,6 +33,7 @@ def test_missing_required_failure_rate_session_raises() -> None:
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0, "US_PRE": 50.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.01},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1, "US_PRE": 0.2},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="missing failure_rate_by_session.*US_PRE"):
|
||||
@@ -42,6 +45,7 @@ def test_invalid_failure_rate_range_raises() -> None:
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 1.2},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="failure rate must be within"):
|
||||
@@ -53,6 +57,7 @@ def test_unfavorable_fill_requirement_cannot_be_disabled() -> None:
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.02},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=False,
|
||||
)
|
||||
with pytest.raises(ValueError, match="unfavorable_fill_required must be True"):
|
||||
@@ -65,6 +70,7 @@ def test_non_finite_commission_rejected(bad_commission: float) -> None:
|
||||
commission_bps=bad_commission,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.02},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="commission_bps"):
|
||||
@@ -77,7 +83,33 @@ def test_non_finite_slippage_rejected(bad_slippage: float) -> None:
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": bad_slippage},
|
||||
failure_rate_by_session={"KRX_REG": 0.02},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="slippage bps"):
|
||||
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||
|
||||
|
||||
def test_missing_required_partial_fill_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, "US_PRE": 0.08},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="missing partial_fill_rate_by_session.*US_PRE"):
|
||||
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG", "US_PRE"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_rate", [-0.1, 1.1, float("nan")])
|
||||
def test_invalid_partial_fill_rate_range_raises(bad_rate: float) -> None:
|
||||
model = BacktestCostModel(
|
||||
commission_bps=5.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.02},
|
||||
partial_fill_rate_by_session={"KRX_REG": bad_rate},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
with pytest.raises(ValueError, match="partial fill rate must be within"):
|
||||
validate_backtest_cost_model(model=model, required_sessions=["KRX_REG"])
|
||||
|
||||
@@ -35,6 +35,7 @@ def _cost_model() -> BacktestCostModel:
|
||||
commission_bps=3.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},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1, "US_PRE": 0.2},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
|
||||
@@ -71,6 +72,9 @@ def test_pipeline_happy_path_returns_fold_and_artifact_contract() -> None:
|
||||
assert names == {"B0", "B1", "M1"}
|
||||
for score in fold.baseline_scores:
|
||||
assert 0.0 <= score.accuracy <= 1.0
|
||||
assert fold.execution_adjusted_trade_count >= 0
|
||||
assert fold.execution_rejected_count >= 0
|
||||
assert fold.execution_partial_count >= 0
|
||||
|
||||
|
||||
def test_pipeline_cost_guard_fail_fast() -> None:
|
||||
@@ -78,6 +82,7 @@ def test_pipeline_cost_guard_fail_fast() -> None:
|
||||
commission_bps=3.0,
|
||||
slippage_bps_by_session={"KRX_REG": 10.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.01},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.1},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
try:
|
||||
@@ -166,3 +171,56 @@ def test_pipeline_rejects_minutes_spec_when_timestamp_missing() -> None:
|
||||
assert "BacktestBar.timestamp is required" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected timestamp validation error")
|
||||
|
||||
|
||||
def test_pipeline_execution_adjusted_returns_reflect_cost_and_fill_assumptions() -> None:
|
||||
base_cfg = dict(
|
||||
bars=_bars(),
|
||||
entry_indices=[0, 1, 2, 3, 4, 5, 6, 7],
|
||||
side=1,
|
||||
triple_barrier_spec=TripleBarrierSpec(
|
||||
take_profit_pct=0.02,
|
||||
stop_loss_pct=0.01,
|
||||
max_holding_minutes=3,
|
||||
),
|
||||
walk_forward=WalkForwardConfig(
|
||||
train_size=4,
|
||||
test_size=2,
|
||||
step_size=2,
|
||||
purge_size=1,
|
||||
embargo_size=1,
|
||||
min_train_size=3,
|
||||
),
|
||||
)
|
||||
|
||||
optimistic = BacktestCostModel(
|
||||
commission_bps=0.0,
|
||||
slippage_bps_by_session={"KRX_REG": 0.0, "US_PRE": 0.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.0, "US_PRE": 0.0},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.0, "US_PRE": 0.0},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
conservative = BacktestCostModel(
|
||||
commission_bps=10.0,
|
||||
slippage_bps_by_session={"KRX_REG": 20.0, "US_PRE": 60.0},
|
||||
failure_rate_by_session={"KRX_REG": 0.2, "US_PRE": 0.4},
|
||||
partial_fill_rate_by_session={"KRX_REG": 0.5, "US_PRE": 0.7},
|
||||
unfavorable_fill_required=True,
|
||||
)
|
||||
|
||||
opt_out = run_v2_backtest_pipeline(cost_model=optimistic, **base_cfg)
|
||||
cons_out = run_v2_backtest_pipeline(cost_model=conservative, **base_cfg)
|
||||
|
||||
opt_avg = sum(
|
||||
f.execution_adjusted_avg_return_bps for f in opt_out.folds
|
||||
) / len(opt_out.folds)
|
||||
cons_avg = sum(
|
||||
f.execution_adjusted_avg_return_bps for f in cons_out.folds
|
||||
) / len(cons_out.folds)
|
||||
assert cons_avg < opt_avg
|
||||
|
||||
opt_trades = sum(f.execution_adjusted_trade_count for f in opt_out.folds)
|
||||
cons_trades = sum(f.execution_adjusted_trade_count for f in cons_out.folds)
|
||||
cons_rejected = sum(f.execution_rejected_count for f in cons_out.folds)
|
||||
assert cons_trades <= opt_trades
|
||||
assert cons_rejected >= 0
|
||||
|
||||
Reference in New Issue
Block a user