Exit manager: never manage a fill with zero post-entry bars
Live 2026-07-06: every fresh entry was market-exited seconds after filling (ELAB round-tripped in 3s, SEER in 77s, PLUG in 85s) — all logged as exit_trail. Root cause: seconds after a fill there is no completed minute bar with timestamp >= the fill time, and _bars_since's empty result fell back to the WHOLE DAY's bars, so manage_live's high-water included the pre-entry HOD and the trail read as already breached on any name trading off its high. The whole-day fallback is removed: with no post-entry bars the manager now leaves the position alone (the bracket's initial stop is the protection) until the first post-entry bar lands. Regression test verified to fail on the old code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
857c64ea48
commit
842b54c802
2 files changed
+38
-1
No files matched your search
@@ -311,7 +311,13 @@ class LiveExitManager:
|
||||
|
||||
since = _bars_since(bars, tr.entry_ts)
|
||||
if len(since) == 0:
|
||||
since = bars
|
||||
# no completed bar since the entry fill yet: the old whole-day fallback
|
||||
# fed manage_live PRE-entry bars, so its high-water included the day's
|
||||
# HOD and the trail read as already breached on any name trading off
|
||||
# its high — instantly market-exiting fresh entries (3-second round
|
||||
# trips on 2026-07-06). The bracket's initial stop already protects
|
||||
# the position; do nothing until the first post-entry bar lands.
|
||||
continue
|
||||
d = manage_live(tr.entry_price, tr.init_stop, since, self.cfg, tr.scaled)
|
||||
|
||||
# (1) first-red: exit the whole position now
|
||||
|
||||
@@ -270,3 +270,34 @@ def test_manager_noop_for_static_bracket():
|
||||
cfg=ExitConfig(target_r=2.0), session_id="t")
|
||||
assert mgr.manage() == []
|
||||
assert not broker.replaced and not broker.closed
|
||||
|
||||
|
||||
def test_fresh_fill_with_no_post_entry_bars_is_left_alone():
|
||||
"""Regression (2026-07-06 live): seconds after a fill there is no completed bar
|
||||
with timestamp >= the entry time, and the old whole-day fallback fed manage_live
|
||||
PRE-entry bars — the day's HOD read as post-entry high-water, the trail computed
|
||||
as already breached, and fresh entries were market-exited within seconds (ELAB
|
||||
round-tripped in 3s). With no post-entry bars the manager must do NOTHING: no
|
||||
flatten, no scale-out, no stop replace — the bracket's initial stop protects."""
|
||||
store = EventStore(":memory:")
|
||||
pos = [{"symbol": "AAA", "avg_entry_price": "2.25", "qty": "100",
|
||||
"current_price": "2.24"}]
|
||||
# ran to 2.40 (HOD, > +1R past our later 2.25 entry) BEFORE the entry and pulled
|
||||
# back to 2.24 — under the whole-day fallback the trail reads armed AND breached
|
||||
bars = _bars([(2.20, 2.15, 2.19), (2.40, 2.19, 2.38), (2.39, 2.30, 2.35)])
|
||||
leg = {"id": "stop1", "symbol": "AAA", "type": "stop", "side": "sell",
|
||||
"stop_price": "2.19", "status": "held"}
|
||||
# the BUY fill happened AFTER the last completed bar -> zero bars since entry
|
||||
fill = {"id": "buy1", "symbol": "AAA", "side": "buy", "filled_qty": "100",
|
||||
"filled_at": "2026-06-17 13:32:14"}
|
||||
broker = _FakeBroker(pos, leg)
|
||||
broker.get_orders = lambda **kw: [leg, fill]
|
||||
mgr = LiveExitManager(
|
||||
broker, store, lambda s: bars,
|
||||
cfg=ExitConfig(breakeven_at_r=1.0, trail_mode=TRAIL_PRIOR_LOW, trail_after_r=1.0),
|
||||
session_id="t",
|
||||
)
|
||||
mgr.manage()
|
||||
assert broker.closed == [], "fresh fill must not be flattened before any post-entry bar"
|
||||
assert broker.replaced == [], "stop must not move before any post-entry bar"
|
||||
assert broker.canceled == []
|
||||
Reference in new issue
Block a user