Skip to content

feat: sign last_session_close out of hours (fix RAI-693)#12

Merged
hardyjosh merged 1 commit into
mainfrom
feat/market-hours-publish-time
Jun 1, 2026
Merged

feat: sign last_session_close out of hours (fix RAI-693)#12
hardyjosh merged 1 commit into
mainfrom
feat/market-hours-publish-time

Conversation

@hardyjosh

Copy link
Copy Markdown
Contributor

The broker positions endpoint exposes current_price with no
per-symbol timestamp. The d841a31 broker-mark cutover stamped
Utc::now() at fetch time and called that publish_time, which only
bounds the server's own freshness — max-staleness in the strategy
therefore always sees a fresh stamp regardless of whether the
underlying market data is hours stale.

This week's verification (45h crontab sampler across all four market
phases) confirmed:

  • During the extended session (04:00–20:00 ET on a trading day) the
    broker mark legitimately moves with pre-market and after-hours
    quotes. Signing now is correct.
  • During a full close (overnight, weekend, holiday) it freezes hard
    (31h × 4 symbols × zero changes in the weekend sample). Signing
    now lies; the strategy accepts an old close as fresh.

The original "stopgap" (only re-stamp when current_price changes) was
ruled out because broker marks can freeze for 15–45 min during real
RTH on mid-volume names (SGOV/COIN/AMZN), which would false-positive
strategies with a tight max-staleness window.

This PR sources the truth from Alpaca's /v1/calendar instead:

  • New market_hours module: MarketHoursCache holds the extended
    session windows around today; publish_time_for(now) returns now
    inside an active window, the most recent past session_close
    outside. anchor_session_to_utc converts the calendar's HHMM ET
    strings through chrono-tz so EST/EDT transitions are handled
    correctly.
  • AlpacaClient::fetch_calendar(start, end) reads /v1/calendar and
    materialises each row into a SessionWindow via the new module.
  • Primed at startup with ±7 days around today (cold-start during a
    holiday still resolves to the previous trading day's close), then
    refreshed hourly in the background. A failed fetch logs and keeps
    the previous cache — matching the quote-poll loop's behavior.
  • build_response_from_quote consults the cache at sign time. The
    fetch_time on QuoteData is no longer used for the signed stamp.

Cache stays empty until the first refresh succeeds, in which case
publish_time_for falls back to now — preserving pre-fix
behaviour rather than blocking the server during a transient calendar
outage.

Tests:

  • Unit tests cover every branch of publish_time_for (active session,
    out of session, holiday Monday with no entry falling through to the
    prior Friday close, weekday overnight, both session boundaries,
    empty cache) and anchor_session_to_utc (EDT, EST, malformed
    HHMM).
  • Integration tests confirm /context/v1 signs last_session_close
    in the default out-of-session test app, and signs the request's
    wall-clock now when the app is configured with an active session
    window.

Closes RAI-693.

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

@linear-code

linear-code Bot commented May 31, 2026

Copy link
Copy Markdown

RAI-693

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hardyjosh, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ebf3ea30-fa38-46c3-9afa-c2e3c38de92b

📥 Commits

Reviewing files that changed from the base of the PR and between 297a2e8 and d1732ad.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • src/alpaca.rs
  • src/lib.rs
  • src/main.rs
  • src/market_hours.rs
  • tests/integration.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/market-hours-publish-time

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

The broker positions endpoint exposes `current_price` with no
per-symbol timestamp. The d841a31 broker-mark cutover stamped
`Utc::now()` at fetch time and called that `publish_time`, which only
bounds the server's own freshness — `max-staleness` in the strategy
therefore always sees a fresh stamp regardless of whether the
underlying market data is hours stale.

This week's verification (45h crontab sampler across all four market
phases) confirmed:

- During the extended session (04:00–20:00 ET on a trading day) the
  broker mark legitimately moves with pre-market and after-hours
  quotes. Signing `now` is correct.
- During a full close (overnight, weekend, holiday) it freezes hard
  (31h × 4 symbols × zero changes in the weekend sample). Signing
  `now` lies; the strategy accepts an old close as fresh.

The original "stopgap" (only re-stamp when current_price changes) was
ruled out because broker marks can freeze for 15–45 min during real
RTH on mid-volume names (SGOV/COIN/AMZN), which would false-positive
strategies with a tight max-staleness window.

This PR sources the truth from Alpaca's `/v1/calendar` instead:

- New `market_hours` module: `MarketHoursCache` holds the extended
  session windows around today; `publish_time_for(now)` returns `now`
  inside an active window, the most recent past `session_close`
  outside. `anchor_session_to_utc` converts the calendar's HHMM ET
  strings through chrono-tz so EST/EDT transitions are handled
  correctly.
- `AlpacaClient::fetch_calendar(start, end)` reads `/v1/calendar` and
  materialises each row into a `SessionWindow` via the new module.
- Primed at startup with ±7 days around today (cold-start during a
  holiday still resolves to the previous trading day's close), then
  refreshed hourly in the background. A failed fetch logs and keeps
  the previous cache — matching the quote-poll loop's behavior.
- `build_response_from_quote` consults the cache at sign time. The
  fetch_time on `QuoteData` is no longer used for the signed stamp.

Cache stays empty until the first refresh succeeds, in which case
`publish_time_for` falls back to `now` — preserving pre-fix
behaviour rather than blocking the server during a transient calendar
outage.

Tests:

- Unit tests cover every branch of `publish_time_for` (active session,
  out of session, holiday Monday with no entry falling through to the
  prior Friday close, weekday overnight, both session boundaries,
  empty cache) and `anchor_session_to_utc` (EDT, EST, malformed
  HHMM).
- Integration tests confirm `/context/v1` signs `last_session_close`
  in the default out-of-session test app, and signs the request's
  wall-clock `now` when the app is configured with an active session
  window.

Closes RAI-693.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@hardyjosh hardyjosh force-pushed the feat/market-hours-publish-time branch from b6add3f to d1732ad Compare May 31, 2026 11:58
@hardyjosh hardyjosh merged commit 991dd0a into main Jun 1, 2026
4 checks passed
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