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:
momentum devandClaude Opus 4.8 committed 2026-07-06 12:25:06 -04:00
1 parent 857c64ea48
commit 842b54c802
2 files changed
+38 -1

No files matched your search

+7 -1
View File
@@ -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
+31
View File
@@ -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 == []