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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user