Open catch-up flatten: close stale overnight carries at the open
The backstop behind the EOD-flatten fix: even with the EOD flatten working, a carry can survive (loop down at EOD, or the held-qty 403 before it was fixed) and bleed for days — ATPC sat 4 days over the Juneteenth gap, -$651. This closes any position carried from a PRIOR session, at the open, before it bleeds. - runtime/flatten.find_overnight_carries (pure, tested): open positions with no BUY filled today (ET) = carried overnight. Today's fresh names have a buy today, so they're never touched. - ExecutionService.flatten_overnight_carries: detects carries from the broker (positions + filled buys) and closes them via the robust cancel-legs-then-close path — WITHOUT halting the session (unlike close_session), so today's trading continues. Emits an open_catchup_flatten risk event. - run_live_paper step_open_catchup_flatten: once per session, during RTH, gated by TRADING_OPEN_CATCHUP_FLATTEN_ENABLED (def 1). Loud log when it flattens a carry. Dry-run against the live book: correctly targets ONLY ATPC (6/18 carry), leaves today's DAMD/ECX. 38 runtime/safety tests pass. Needs a loop restart to activate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
16715369c0
commit
1be2dfd042
4 files changed
+178
-3
No files matched your search
@@ -21,9 +21,46 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import date, datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ET = ZoneInfo("America/New_York")
|
||||
|
||||
|
||||
def _fill_et_date(iso) -> date | None:
|
||||
"""ET calendar date of a broker fill timestamp (stored UTC), or None."""
|
||||
try:
|
||||
dt = datetime.fromisoformat(str(iso).replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(_ET).date()
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def find_overnight_carries(open_symbols, buy_fills, today_et: date) -> list[str]:
|
||||
"""Open positions whose lot was NOT entered today (ET) — i.e. carried across
|
||||
a prior session. A day-trading book should be flat by the open, so anything
|
||||
still held that wasn't bought today survived overnight (the EOD flatten was
|
||||
missed/failed) and should be caught up at the open before it bleeds for days
|
||||
(the ATPC-over-Juneteenth case).
|
||||
|
||||
Args:
|
||||
open_symbols: iterable of currently-open position symbols.
|
||||
buy_fills: iterable of {"symbol", "filled_at"} for FILLED BUY orders.
|
||||
today_et: today's ET date.
|
||||
Returns sorted symbols open today with no buy filled today.
|
||||
"""
|
||||
bought_today: set[str] = set()
|
||||
for f in buy_fills or []:
|
||||
if _fill_et_date(f.get("filled_at")) == today_et:
|
||||
sym = f.get("symbol")
|
||||
if sym:
|
||||
bought_today.add(sym)
|
||||
return sorted(s for s in (open_symbols or ()) if s and s not in bought_today)
|
||||
|
||||
# Order states in which a sell order still RESERVES position quantity. While an
|
||||
# order is in one of these states, that many shares show as ``held_for_orders``
|
||||
# and are unavailable to a separate close. (A ``filled``/``canceled`` leg no
|
||||
|
||||
Reference in new issue
Block a user