feat: log Phase A decisions and add execution-recovery matrix

Document Phase A (max-hold/vol/corr closed; next-open as decision baseline).
Add stale_close and next_open gap-cap fill modes plus a small matrix to test
whether near-close scheduling recovers overnight momentum drift.
This commit is contained in:
2026-07-18 16:27:10 +02:00
parent 723d47338e
commit 3eb6192a1e
5 changed files with 768 additions and 26 deletions
+87
View File
@@ -1066,6 +1066,93 @@ class TestSimulatePortfolio:
# end should be near entry + hold (trading days ≈ calendar for synthetic series)
assert (end - entry).days <= 10
def test_stale_close_fills_next_session_close_with_reanchored_stop(self):
n = 40
closes = [100.0 + 0.1 * i for i in range(n)]
opens = list(closes)
highs = [c + 2.0 for c in closes]
lows = [c - 2.0 for c in closes]
ords = list(range(self.ORD, self.ORD + n))
signal_i = n - 2
fill_i = n - 1
closes[fill_i] = 110.0
opens[fill_i] = 105.0
highs[fill_i] = 111.0
lows[fill_i] = 104.0
prices = {
"AAA": (ords, opens, highs, lows, closes, [1_000_000] * n)
}
cand = _sim_cand(
"AAA",
self.ORD + signal_i,
entry=closes[signal_i],
stop=closes[signal_i] - 5.0,
target=200.0,
)
sim = bt._simulate_portfolio(
[cand],
prices,
None,
"hold",
5,
fill_mode=bt.FILL_MODE_STALE_CLOSE,
cost_per_side=0.0,
include_trades=True,
)
assert sim is not None
assert sim["fill_mode"] == "stale_close"
assert sim["trades"] == 1
trade = sim["trade_details"][0]
assert trade["entry"] == pytest.approx(110.0)
# ATR ~4 on this synthetic series → stop = 110 1.5×4 = 104
assert trade["initial_stop"] == pytest.approx(110.0 - 1.5 * 4.0, abs=0.5)
assert "signal_to_fill_drift" in sim
def test_next_open_gap_cap_skips_large_gap_ups(self):
n = 40
closes = [100.0] * n
opens = [100.0] * n
highs = [102.0] * n
lows = [98.0] * n
ords = list(range(self.ORD, self.ORD + n))
signal_i = n - 2
fill_i = n - 1
opens[fill_i] = 110.0 # +10% gap vs signal close 100
highs[fill_i] = 111.0
lows[fill_i] = 109.0
closes[fill_i] = 110.5
prices = {
"AAA": (ords, opens, highs, lows, closes, [1_000_000] * n)
}
cand = _sim_cand(
"AAA", self.ORD + signal_i, entry=100.0, stop=95.0, target=130.0
)
blocked = bt._simulate_portfolio(
[cand],
prices,
None,
"hold",
5,
fill_mode=bt.FILL_MODE_NEXT_OPEN,
max_entry_gap_pct=0.02,
cost_per_side=0.0,
)
allowed = bt._simulate_portfolio(
[cand],
prices,
None,
"hold",
5,
fill_mode=bt.FILL_MODE_NEXT_OPEN,
cost_per_side=0.0,
include_trades=True,
)
assert blocked is None or blocked.get("trades", 0) == 0
if blocked is not None:
assert blocked.get("skipped_gap_cap", 0) >= 1
assert allowed is not None and allowed["trades"] == 1
assert allowed["trade_details"][0]["entry"] == pytest.approx(110.0)
def test_fip_id_sign_convention_steady_climber_vs_jump():
# Steady climber: many up days, continuous path → lower (more negative) ID.