Skip to content

fix(sessions): read Alpaca bars as open-stamped - #529

Merged
FlyM1ss merged 5 commits into
mainfrom
fix/session-close-boundary
Sep 23, 2026
Merged

FlyM1ss merged 5 commits into
mainfrom
fix/session-close-boundary

Conversation

@FlyM1ss

@FlyM1ss FlyM1ss commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Alpaca stamps bars at their open and serves extended hours, but the session filter treated every stamp as a close. So the US filter kept the after-hours 16:00 bar.

  • The stamp convention travels with the data. AlpacaDataLoader.fetch_bars stamps sessions.FRAME_ATTR_OPEN_STAMPED_MINUTES on every frame it returns, cache hits included. Each session filter reads it with frames_open_stamped_minutes(), and aggregation drops it (decision bars are stamped at their close). There is no per-caller parameter: the first revision passed one by hand, and the protocol baseline worker, which never calls load_data, filtered aggregated bars as raw 5m and lost every 16:00 close. A loader without the attr (iFinD, vnpy, legacy doubles) keeps the close-stamp rule.
  • An open-stamped bar is in session only if it lies wholly inside the session. For 5m bars that means 09:30 through 15:55. For 60m bars it means 10:00 through 15:00. The 09:00 bar is out because half of it is pre-market.
  • Aggregation refuses source bars not stamped at their open. Its bucketing assumes open stamps. Because of that, every verified intraday contract can truthfully claim session_close_fill.
  • plan_execution_fills returns one ExecutionFill(bar, price_field, filled_at) per step. It replaces the parallel lists in the dataset, the protocol session and the engine. The 16:00 ET decision fills at the close of the bar ending then (15:55), and the trade is stamped 16:00, not 15:55. A 5m-source run now makes 7 decisions a day (it was 6).
  • An unaggregated dataset's default ExecutionFill names the bar's close, matching decision_bar_close and the engine. The executor already filled there; the recorded open was a latent look-ahead.
  • The API allow-list now passes session_close_fill, so the details label renders it. The label suffix appears only under next_source_bar_open.

⚠ Cached leaderboard rows were built with the old 10:00–16:00 filter. A force refresh (POST /api/v1/leaderboard/refresh?force=true) is billable and is intentionally not part of this PR.

Tests: full backend suite green (5411 passed, 165 skipped). The new tests were mutation-checked, and all 11 mutations were caught.

🤖 Generated with Claude Code

FlyM1ss and others added 3 commits September 23, 2026 16:07
…sitive

#520 keys market_data_store on sorted symbols; the docstring still said
tuple(symbols).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Alpaca stamps a bar at its open and serves extended hours, but every raw-bar
session filter applied the close-stamp rule (inclusive 16:00). So:

- minute mode kept the 16:00-16:05 after-hours source bar: the day's last
  equity mark was an after-hours price, and the 16:00 decision filled only if
  the tape happened to serve that bar (SIP does, carrying the closing auction;
  IEX often not);
- the leaderboard's raw 1h path kept 10:00-16:00, i.e. the 16:00-17:00
  after-hours bar in and the 09:00 bar holding the 09:30 open out.

is_in_session takes open_stamped_minutes (an open-stamped bar is in session
when it closes in one), profiles.bars_open_stamped says which sources need it
(Alpaca; not iFinD or vnpy), and bar_aggregation.plan_execution_fills is the
one fill planner for the engine and the protocol store: the final bucket fills
at the last in-session source bar's close. Seven decisions a day either way;
the hourly board becomes 09:00-15:00.

Also imports DEFAULT_MARKET into market_data_store from sessions instead of
restating it.

Cached leaderboard rows keep the old rule until a force refresh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 16:00 decision now fills at the 15:55 close instead of being dropped,
so a 5m-source run executes 7 decisions a day, not 6. The verified
contract gains `session_close_fill: last_source_bar_close` (additive, so
stored runs keep their label) and the backtest details label names it.

The planner also returned the decision stamp on an exact match instead
of the equal source bar, which re-stamped every trade in UTC.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 23, 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 Sep 23, 2026 10:14pm UTC

@FlyM1ss
FlyM1ss marked this pull request as draft September 23, 2026 20:41
Address PR #529 review findings: the loader stamps open-stamped frames,
session filters read that attr instead of a per-caller parameter (the
baseline worker path lost every 16:00 close), aggregation refuses
unlabelled sources, and one ExecutionFill record replaces the parallel
execution lists. The session-close fill is stamped 16:00, and
session_close_fill now passes the API allow-list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dataset whose decision bars are its source bars has no execution plan,
and its default ExecutionFill named each bar's open. The executor ignored
that field outside intraday mode and filled at the close, matching the
decision_bar_close contract, so no run was mispriced. But the record
contradicted both the contract and engine._plan_executions, and one
unconditional execution_prices would have turned it into look-ahead.

Default to close in MarketDataset and _effective_execution_fills, and pin
the unaggregated fill price with a regression test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@FlyM1ss
FlyM1ss merged commit 5e09de9 into main Sep 23, 2026
8 checks passed
@FlyM1ss
FlyM1ss deleted the fix/session-close-boundary branch September 27, 2026 04:02

This branch was successfully deployed

1 active deployment
Preview — f120ece3 Deployed Sep 23, 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.

1 participant