feat: add daily reentry policy matrix

This commit is contained in:
2026-07-17 16:11:18 +02:00
parent 13f57b2525
commit 0cd9ee7689
4 changed files with 764 additions and 5 deletions
+53
View File
@@ -543,6 +543,7 @@ class TestSimulatePortfolio:
sim = bt._simulate_portfolio([cand], prices, None, "hold", 3)
assert sim is not None
assert sim["trades"] == 1
assert sim["cost_per_side_pct"] == pytest.approx(0.1)
# 20 shares (1% risk / $5 stop distance), exit at the day-3 close 106:
# pnl = 2120 2000 2.00 entry cost 2.12 exit cost = 115.88
assert sim["final_equity"] == pytest.approx(10_115.88, abs=0.01)
@@ -556,6 +557,29 @@ class TestSimulatePortfolio:
{"year": 2025, "return_pct": pytest.approx(1.2, abs=0.05)}
]
def test_cost_parameter_changes_cash_and_position_path(self):
closes = [100.0, 102.0, 104.0, 106.0]
prices = {"AAA": _sim_prices(self.ORD, closes)}
cand = _sim_cand("AAA", self.ORD, entry=100.0, stop=95.0, target=130.0)
free = bt._simulate_portfolio(
[cand], prices, None, "hold", 3, cost_per_side=0.0
)
stressed = bt._simulate_portfolio(
[cand], prices, None, "hold", 3, cost_per_side=0.002
)
assert free is not None and stressed is not None
assert free["final_equity"] == pytest.approx(10_120.0, abs=0.01)
assert stressed["cost_per_side_pct"] == pytest.approx(0.2)
assert stressed["final_equity"] == pytest.approx(10_111.76, abs=0.01)
def test_cost_parameter_rejects_invalid_rate(self):
with pytest.raises(ValueError, match="cost_per_side"):
bt._simulate_portfolio(
[], {}, None, "hold", 3, cost_per_side=-0.001
)
def test_target_policy_exits_at_target(self):
closes = [100.0, 102.0, 104.0, 106.0, 108.0, 110.0]
prices = {"AAA": _sim_prices(self.ORD, closes)}
@@ -626,6 +650,35 @@ class TestSimulatePortfolio:
self.ORD + 6
).isoformat()
def test_post_stop_reentry_cannot_cross_holdout_end(self):
prices = {"AAA": _sim_prices(self.ORD, [100.0, 94.0, 96.0, 98.0])}
candidate = _sim_cand(
"AAA", self.ORD, entry=100.0, stop=95.0, target=120.0
)
callback_dates: list[int] = []
def reenter_after_split(symbol, asof_ord, _state, _bar):
callback_dates.append(asof_ord)
if asof_ord < self.ORD + 2:
return None
return _sim_cand(
symbol, asof_ord, entry=96.0, stop=90.0, target=115.0
)
sim = bt._simulate_portfolio(
[candidate],
prices,
None,
"hold",
3,
end_date=date.fromordinal(self.ORD + 2),
post_stop_reentry_fn=reenter_after_split,
)
assert sim is not None
assert sim["trades"] == 1
assert callback_dates == [self.ORD + 1]
def test_production_monitor_applies_live_reentry_lockdown(self, monkeypatch):
def fake_simulator(*_args, **kwargs):
return {
+97
View File
@@ -0,0 +1,97 @@
from datetime import date
from scripts.run_daily_reentry_matrix import PrecomputedDailyEngine, ReentryPolicy
RANKING_KEY = "strategy_rank"
ORD = date(2025, 1, 6).toordinal()
def _candidate(day_ord: int, *, stop: float = 91.0, rank: float = 81.0) -> dict:
return {
"qualified": True,
"direction": "long",
"symbol": "AAA",
"date": date.fromordinal(day_ord).isoformat(),
"entry": 100.0,
"stop": stop,
"target": 120.0,
RANKING_KEY: rank,
}
def _state(sessions: int = 0) -> dict:
return {
"sessions_since_stop": sessions,
"previous_stop": 90.0,
"previous_rank": 80.0,
"gate_went_unqualified": False,
}
def _call(policy: ReentryPolicy, day_ord: int, state: dict, sessions: int):
state["sessions_since_stop"] = sessions
return policy("AAA", day_ord, state, object())
def test_next_session_blocks_only_stop_day():
engine = PrecomputedDailyEngine([_candidate(ORD), _candidate(ORD + 1)])
policy = ReentryPolicy("next_session", engine, RANKING_KEY)
state = _state()
assert _call(policy, ORD, state, 0) is None
assert _call(policy, ORD + 1, state, 1) is not None
def test_five_session_cooldown_unlocks_at_exact_boundary():
engine = PrecomputedDailyEngine([_candidate(ORD + 4), _candidate(ORD + 5)])
policy = ReentryPolicy("cooldown_5", engine, RANKING_KEY)
state = _state()
assert _call(policy, ORD + 4, state, 4) is None
assert _call(policy, ORD + 5, state, 5) is not None
def test_gate_reset_requires_failure_before_requalification():
engine = PrecomputedDailyEngine([_candidate(ORD), _candidate(ORD + 2)])
policy = ReentryPolicy("gate_reset", engine, RANKING_KEY)
state = _state()
assert _call(policy, ORD, state, 0) is None
assert _call(policy, ORD + 1, state, 1) is None
emitted = _call(policy, ORD + 2, state, 2)
assert emitted is not None
assert emitted["_reentry_reason"] == "gate_failed_then_requalified"
def test_improved_gate_reset_requires_better_stop_and_non_weaker_rank():
engine = PrecomputedDailyEngine([
_candidate(ORD + 1, stop=89.0, rank=82.0),
_candidate(ORD + 2, stop=92.0, rank=79.0),
_candidate(ORD + 3, stop=92.0, rank=81.0),
])
policy = ReentryPolicy("gate_reset_improved", engine, RANKING_KEY)
state = _state()
assert _call(policy, ORD, state, 0) is None
assert _call(policy, ORD + 1, state, 1) is None
assert _call(policy, ORD + 2, state, 2) is None
emitted = _call(policy, ORD + 3, state, 3)
assert emitted is not None
assert emitted["_reentry_reason"] == "gate_reset_with_improved_stop_and_rank"
def test_two_session_confirmation_excludes_stop_day_close():
engine = PrecomputedDailyEngine([
_candidate(ORD),
_candidate(ORD + 1),
_candidate(ORD + 2),
])
policy = ReentryPolicy("two_session_confirmation", engine, RANKING_KEY)
state = _state()
assert _call(policy, ORD, state, 0) is None
assert _call(policy, ORD + 1, state, 1) is None
emitted = _call(policy, ORD + 2, state, 2)
assert emitted is not None
assert emitted["_reentry_reason"] == "two_qualified_post_stop_closes"