feat: add daily reentry policy matrix
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user