EOD flatten: close-relative + holiday-aware window (no naked carry over closures)

Diagnosed the ATPC -$651 drag: on 6/18 (last session before Juneteenth) the EOD
flatten FIRED at 15:55 but every close was rejected — Alpaca 403 "insufficient
qty... held_for_orders" because the bracket stop/TP legs reserved the shares.
That root cause was already fixed by 4520c69 (cancel legs before close; verified
the current self-fetch finds the active leg). What remained were two real
naked-carry holes this closes:

- Early-close half-days NEVER flattened: the window was a fixed [15:55, close),
  but half-days close at 13:00 (15:55 > 13:00) — and half-days (Jul 3, Black
  Friday, Dec 24) are exactly the pre-holiday sessions. The window is now
  computed RELATIVE TO THE ACTUAL CLOSE, so it fires on early closes too.
- No margin / silent failure before a multi-day gap. Before a weekend/holiday
  closure the flatten lead widens (TRADING_PRE_CLOSURE_FLATTEN_LEAD_MIN, def 20)
  so the book is flat with room to retry, and a flatten that can't fully close
  before a multi-day gap now SCREAMS ("‼ NAKED-CARRY RISK (N-day gap ahead)")
  instead of failing silently like 6/18 did.

New calendar helpers next_trading_day / days_to_next_session / eod_flatten_status
(pure, unit-tested: normal window, pre-holiday widening, early-close firing,
closed-day skip). Normal full days are unchanged (still 15:55-16:00). Dormant
until the next pre-closure session, so no effect on today's behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-22 11:45:19 -04:00
1 parent 16c759e334
commit 16715369c0
3 files changed
+120 -14

No files matched your search

+41 -1
View File
@@ -14,7 +14,7 @@ rules and should be re-confirmed when the year is current. Update annually.
from __future__ import annotations
from datetime import date, datetime
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
_ET = ZoneInfo("America/New_York")
@@ -69,3 +69,43 @@ def is_regular_hours(now_et: datetime | None = None) -> bool:
if close is None:
return False
return (9, 30) <= (n.hour, n.minute) < close
def next_trading_day(d: date) -> date:
"""The first trading day strictly after ``d`` (skips weekends + holidays)."""
nd = d + timedelta(days=1)
while not is_trading_day(nd):
nd += timedelta(days=1)
return nd
def days_to_next_session(d: date) -> int:
"""Calendar days from ``d`` to the next session. 1 on a normal weeknight; 3
over a weekend; 4+ before a holiday-extended weekend. >1 == a MULTI-DAY
closure follows, i.e. carrying overnight means carrying across a gap."""
return (next_trading_day(d) - d).days
def eod_flatten_status(
now_et: datetime,
normal_lead_min: int = 5,
pre_closure_lead_min: int = 20,
) -> tuple[bool, bool]:
"""(should_fire, is_pre_closure) for the end-of-day flatten.
The window is ``[close - lead, close)`` and is computed RELATIVE TO THE
ACTUAL CLOSE — so it fires on early-close (1pm) half-days too, which a fixed
"15:55" never did (15:55 > 13:00 left half-day positions naked into the
holiday they precede). Before a MULTI-DAY closure (weekend/holiday) the lead
widens to ``pre_closure_lead_min`` so the book is flat with margin to spare
and a transient hiccup still has a wide retry window — never carry 3-4 days
naked. Returns (False, _) outside the window or on a non-trading day."""
close = session_close_hm(now_et.date())
if close is None:
return (False, False)
close_min = close[0] * 60 + close[1]
pre_closure = days_to_next_session(now_et.date()) > 1
lead = max(normal_lead_min, pre_closure_lead_min) if pre_closure else normal_lead_min
start_min = close_min - lead
cur_min = now_et.hour * 60 + now_et.minute
return (start_min <= cur_min < close_min, pre_closure)