Skip to content

fix(live): NYSE early closes on the live board - #638

Merged
FlyM1ss merged 15 commits into
mainfrom
fix/live-early-close
Oct 15, 2026
Merged

FlyM1ss merged 15 commits into
mainfrom
fix/live-early-close

Conversation

@FlyM1ss

@FlyM1ss FlyM1ss commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Merge and deploy by 2026-11-20: 2026-11-27 (13:00 ET close) is the first NYSE early close the live board has to trade.

Do not merge between 22:15 and 23:30 UTC on a weekday. The merge auto-deploys prod, and the Render restart kills the 22:30 UTC live refresh thread mid-run, so that night's session is billed again on the next night.

  • Calendar: nyse_early_closes / is_early_close in us_market_calendar.py, by rule (day after Thanksgiving; Dec 24 Mon–Thu; Jul 3 when Jul 4 is Tue–Fri). Pinned to NYSE's 2022–2028 releases and to exchange_calendars XNYS for 2004–2035; the XNYS pin is a literal generated offline, not a dependency. Dated UNSCHEDULED_CLOSURES (2025-01-09 and five more since 2004) and an empty UNSCHEDULED_EARLY_CLOSES. Sessions per year now match XNYS for 2004–2035. The close times live in sessions.py, the only module test_no_module_restates_the_session_bounds lets spell a session bound.
  • Sessions: day_session_windows(day, market) is the one per-day session source; on a US early close it ends at 13:00. The open-stamped filter (is_in_session), the session-close fill (is_session_close) and aggregate_bars all read it, so an early close buckets 10:30/11:30/12:30/13:00 and fills at 13:00. Bars stamped 13:00 and later are not decision bars: they hold the closing auction, then late sessions to 17:00 (on main, 3 extra billed model calls filled off those prints). Applies to My Agents and protocol runs too. The close-stamped rule (time_in_session) still ends every US day at 16:00 (D19, engine rewrite). Conformance: T05 now fails on D2/D16 instead of D19/D8 (known failures re-recorded); 25 strict xfails, no XPASS.
  • Index baselines: on an early close Yahoo sends the 12:30 half bar null and the closing level as a separate point stamped 13:00. _yahoo restamps that point to 12:30, on early closes only, so ^DJI and ^GSPC end the day on the 13:00 node instead of 12:00. Pinned with recorded responses. Accepts America/New_York and US/Eastern; a missing meta.exchangeTimezoneName logs one ERROR: yahoo.exchange_zone_missing, and missing-level ERRORs dedupe per (symbol, day).
  • Live board: 4 axis nodes on an early close (10:00–13:00), settles at 13:30, and the clock, node mapping and reindex read the per-day close. The window description and chart hint say so. leaderboard/service.py's as-of and daily-window cutoffs read session_close_time(day) too. The Competition board is unaffected: its window has no early close.
  • Cadence pinned by tests: 6 decisions per full session, 3 per early close (Aug 2026 126, Nov 117, Dec 129 calls before retries).
  • Basis: excluding post-bell bars follows Barclay & Hendershott (2003, RFS), who find after-hours prices less efficient. Marking every symbol at one session cut avoids the nonsynchronous-trading bias in Lo & MacKinlay (1990, J. Econometrics).
  • Known gaps, stated in the docstrings:
    • Agent marks are pre-auction on every day, because the auction print opens the dropped bar.
    • The index baselines end a half day on the official close, about 0.02% off their full-day basis.
    • Indicators still read extended-hours bars on every day. This is pre-existing; a follow-up (regular-hours indicator inputs) is proposed, not filed.
  • No stored curve changes, because no seed row or live checkpoint spans an early close. If this deploys after the 2026-11-27 13:30 ET settle, force-refresh November through the refresh route's force=true (live.py force_refresh). That replays the month from the 1st and bills it again.
  • No fee, slippage or prompt change.

Tests: pytest dashboard/backend/tests -q -p no:cacheprovider (test_report_pdf.py needs reportlab locally).

🤖 Generated with Claude Code

FlyM1ss and others added 13 commits October 15, 2026 00:24
On an NYSE early close Yahoo returns the 12:30-13:00 half bar as null and
sends the closing level as a point stamped 13:00, which an open-stamped
reader drops as after-hours. Restamp it into the half bar so the Live
board's ^DJI/^GSPC baselines end the day on the official close. Pinned
by two recorded ^DJI responses (2025-11-28 early close, 2025-11-26 full).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…4-2035

The rule was pinned to four published years. A list generated once from
exchange_calendars 4.13.2 (no new dependency) widens that to 32: 22 years
of sessions that happened and 10 years of an independent implementation.
States where the rule stops holding (pre-2004 policy) and corrects the
post-close bar wording: on SIP the 13:00 bar opens with the closing
auction, it is not just thin after-hours prints.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…025-2028

Every trading day, each span the Alpaca loader stamps (1, 5, 60 min): a bar
is in session iff it opens at or after 09:30 and closes by that day's close.
The pattern is exchange_calendars' TestTradingIndex; the hourly-shape pin is
the pandas_market_calendars #138 failure mode (a short session losing bars).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rules missed the days NYSE closed at short notice, so is_trading_day
called 2025-01-09 (President Carter's day of mourning) a session, and so did
everything that walks the calendar: backtest window sizing, the default
window, the Live board's expected sessions. Dated data beside the rules, as
exchange_calendars keeps its XNYS adhoc_holidays: Reagan, Ford, Sandy (2),
G. H. W. Bush, Carter.

Pinned by sessions per year against exchange_calendars 4.13.2 XNYS,
2004-2035 (generated once offline; not a dependency).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… mark is

Research corrections, no behaviour change:
- the bars after a 13:00 bell carry real prints (late sessions and Alpaca's
  half-day extended hours run to 17:00); the filter drops them by stamp;
- every US day is marked pre-auction: the auction print opens the dropped
  bar. Yahoo's index level is the exception on a half day (official close,
  ~0.02% off a full day's basis), the lesser gap against a stale 11:30 level;
- freeze_settle_time settles against the SIP delay clamp; Alpaca can still
  revise bars after it;
- the dense 04:00-19:00 test grids are a superset of Alpaca's output, not
  its shape; the node-key test names Yahoo's restamped closing level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Yahoo appends the latest close to every hourly response, stamped at the
close itself, so on a full session a fetch that reaches today held a point at
16:00 ET and the restamp moved it onto the 15:30 half bar: today marked on
the official close, the same day replayed later marked pre-auction. The
restamp now needs is_early_close as well as the close time.

Pinned with the recorded response run through a window that keeps the
appended point, and with three more recorded early closes (^GSPC 2025-11-28,
^DJI and ^GSPC 2025-12-24) whose last level is the published close.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…elease

- UNSCHEDULED_EARLY_CLOSES (empty: none since 2004) joins the rule's
  candidates ahead of the trading-day filter, so the next short-notice half
  day is a one-line addition that can shorten a session but never open a
  closed one. The docstring now says what nothing catches: a missing one,
  since bars still arrive after the bell.
- PUBLISHED_EARLY_CLOSES gains 2022-2024 from NYSE's 2021-12-27 release
  (checked against the release text), so the hand-copied pin covers
  2022-2028, with sources, retrieval date and a re-pin note.
- The rules are described as what they are: NYSE Rule 7.2 lists holidays
  only, early closes are announced year by year, later years extrapolate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… third check

- Six more recorded index responses (^DJI and ^GSPC on 2024-11-29,
  2024-12-24 and 2025-07-03), so all five early closes from 2024-11-29 to
  2025-12-24 are pinned on both indices, each ending on the published close.
  The 2024 ones leave Yahoo's 730-day hourly window in late 2026 and could
  not be recorded after that.
- The fixture comment no longer says the window filter always drops Yahoo's
  appended close; it does unless the window reaches the latest session.
- LEAN's market-hours database agrees with every early close and holiday
  2004-2028 (diffed offline); it is not the pin because of its Sunday
  2017-12-24 early close.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…erved claims

Review follow-ups:
- If Yahoo ever drops or shifts its 13:00 point, the restamp does nothing and
  the board's 13:00 node repeats the 11:30 level: plausible and wrong. One
  flushed ERROR (yahoo.early_close_level_missing) per settled early close
  with no level at its last half bar; a day within an hour of its close is
  skipped. Pinned with the recorded response minus that point, and the ten
  recorded early closes must stay silent.
- Docstrings no longer state as observed what nobody has seen on an early
  close (Alpaca's late-session bars, the 13:00 auction print's bar); the
  16:00 case is the measured one.
- The market-local-time test now bites: a naive 12:00 stamp must be kept,
  which reading it as UTC would not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agentic-trading-lab Ready Ready Preview Oct 15, 2026 4:59pm UTC

Comment thread dashboard/backend/infrastructure/market_data/sessions.py Fixed
FlyM1ss and others added 2 commits October 15, 2026 12:44
…er it

Only the open-stamped filter knew the 13:00 close. Aggregation still built a
13:30 bucket from 12:30-12:59 and is_session_close still matched 16:00 only,
so on an early close a minute-source run lost its closing decision.

day_session_windows is now the per-day owner read by is_in_session,
is_session_close and aggregate_bars. Drops REGULAR_CLOSE_TIME (unused; the
CodeQL note) and decides US by market name, not by window shape. Conformance
T05 now fails as T02 does (D2/D16) instead of losing the order (D19/D8).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- leaderboard/service.py: as-of and daily-window cutoffs use
  session_close_time(day, "US") instead of a fixed 16:00.
- _yahoo.py: accept both New York zone spellings; a missing
  meta.exchangeTimezoneName logs one ERROR instead of silently skipping
  the restamp; ERRORs dedupe per (symbol, day) per process.
- Live board copy names the 13:00 early close (leaderboard.js v=38).
- Tests: early-close daily window, Yahoo zone/dedupe cases, copy guard;
  is_session_close asserts carry no side effects.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@FlyM1ss
FlyM1ss merged commit 2576c82 into main Oct 15, 2026
8 checks passed
@FlyM1ss
FlyM1ss deleted the fix/live-early-close branch October 15, 2026 18:58
@FlyM1ss

FlyM1ss commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Dated post-deploy checks for 2026-11-27 and 2026-12-24 are tracked in #639.

This branch was successfully deployed

1 active deployment
Preview — 87ba5117 Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants