Raxx · internal docs

internal · gated ↑ index

Proposal — Market Calendar Service

Status: Draft proposal (2026-04-24), awaiting Kristerpher approval Author: Claude (on Kristerpher's direction, 2026-04-24 review of PR #242) Supersedes (if approved): ADR 0019 (business-day calendar — env-var choice) remains valid as the v0 stopgap Related: PR #242 (founders grace transition, business-day compute), MarketTimeWidget, /market-hours endpoint, #254 raxx-console


1. Why this service exists

Calendar logic — what day the market is open, when it opens/closes, when a holiday shifts a trading window, whether a half-day applies — is needed in at least four places across the Raxx stack today:

  1. Grace-window computation (PR #242 + ADR 0019) — compute_grace_end(entry_date, window_business_days) needs a business-day calendar.
  2. MarketTimeWidget (Antlers header + ConfigurationBar) — already renders pre-market / open-market / after-hours / time-to-open / time-to-close via /api/system/market-hours.
  3. Trade-lifecycle engine (future, per #245) — entry/exit timing, roll windows, earnings blackouts, DTE calculations.
  4. Backtest + StrategyComparison — historical session boundaries, holiday gaps, half-days.

Today the logic is scattered: some of it is in backend_v2/api/routes/system.py's /market-hours handler, some of it lives inline in the grace-window compute planned for PR #242, some of it is implicit in how the frontend widget interpolates the session. There is no single source of truth, and no mechanism to push an ad-hoc alert (NYSE closure, circuit breaker, presidential proclamation) to the system.

Kristerpher's 2026-04-24 direction: write a proposal for a lightweight service that isolates this logic, updates daily, and can receive ad-hoc alerts from the exchanges. This doc is that proposal.


2. Goals + non-goals

Goals

Non-goals


3. Scope + capabilities

v1 capabilities

Capability Description
Session clock Return current session state: closed / pre-market / open / after-hours / holiday / early-close.
Next transition When does the current session state end? Return the next timestamp + the state we'll enter.
Holiday calendar Full list of NYSE/NASDAQ holidays for a given year + whether each is a full close or half-day.
Business-day math add_business_days(date, n), business_days_between(start, end), is_business_day(date). Directly replaces PR #242's env-var calendar.
Session boundaries for a date Given 2026-11-27 (Black Friday half-day), return { pre_market_open, open, close: 13:00 ET, after_hours_close }. Used by backtest + lifecycle engines.
Ad-hoc alerts Accept a POST from an operator (via raxx-console) or a webhook (from a future exchange-feed subscription) indicating an ad-hoc closure. Stores the alert, flags affected sessions, pushes to subscribers.

v2+ capabilities (stretch)


4. Architecture

4a. Deployment shape

Two realistic options. I recommend Option 1 for v1 on cost + operability; Option 2 for v2 once the service has multiple independent consumers.

Option 1 — Raptor Blueprint (recommended for v1). - A Flask Blueprint inside Raptor: backend_v2/api/blueprints/calendar/ - Isolated module boundary (dedicated package, dedicated tests, dedicated ADR list) - Shares Raptor's Heroku dyno + Postgres - Pro: zero additional infrastructure, zero additional cost, zero additional rotation surface - Con: not physically separate from Raptor — if Raptor goes down, the calendar goes with it

Option 2 — Standalone service (v2+). - Small Python service deployed on Fly.io or Render (~$5/mo) - Dedicated repo or a subdirectory in this monorepo (services/calendar/) - Exposes an HTTP API consumed by Raptor + Antlers + raxx-console + any future service - Pro: independent deploy cadence, survives Raptor outages, can serve public endpoints to future consumers - Con: new service = new rotation target, new deploy pipeline, new observability surface

Rationale for Option 1 at v1: Kristerpher called this "lightweight" explicitly, and the current dollar-cost discipline (founders-pricing rationale, Dreamhost at $22.99, no over-provisioning) means we don't want another $5/mo line item until a consumer actually needs calendar access independently of Raptor. The raxx-console and Antlers both talk to Raptor today, so they can read from this Blueprint. The service boundary is a module boundary, not a process boundary — still gets us isolation in the code sense, and Option 2 is a straightforward lift-and-shift later.

4b. Data sources

Layered by fidelity + cost:

  1. exchange_calendars Python package (BSD license, MIT-compatible) — covers NYSE, NASDAQ, CBOE, LSE, TSE, many more. Holiday tables + session boundaries + early-close rules. This is the Python-ecosystem standard. v1 baseline.
  2. pandas_market_calendars — similar coverage, sometimes more up-to-date on rare-event rules. Alternative if exchange_calendars lags.
  3. Alpaca /v2/clock endpoint — returns is_open + next_open + next_close. Good cross-check against the package data. v1 cross-check for current-session questions.
  4. Polygon calendar API / Tradier calendar API — commercial; not needed at v1.
  5. NYSE Trader Update RSS + NASDAQ Trader RSS — authoritative for ad-hoc alerts (closures, halts, system issues). v1 for alert ingestion.
  6. Manual operator override — via raxx-console (future card), for cases where the RSS lag matters. v1 for emergency use.

4c. Refresh cadence

4d. Storage

Three tables in Raptor's existing Postgres (or SQLite in dev):

-- Source-of-truth schedule, refreshed daily
market_sessions (
  id                UUID PK,
  exchange          TEXT NOT NULL,        -- 'nyse' | 'nasdaq'
  session_date      DATE NOT NULL,
  pre_market_open   TIMESTAMPTZ,
  regular_open      TIMESTAMPTZ,
  regular_close     TIMESTAMPTZ,
  after_hours_close TIMESTAMPTZ,
  session_type      TEXT NOT NULL,         -- 'regular' | 'early_close' | 'holiday' | 'closed'
  source            TEXT NOT NULL,         -- 'exchange_calendars' | 'manual_override' | 'alert_feed'
  loaded_at         TIMESTAMPTZ NOT NULL,
  UNIQUE (exchange, session_date)
);

-- Holiday metadata for annotation + UI display
market_holidays (
  id            UUID PK,
  exchange      TEXT NOT NULL,
  holiday_date  DATE NOT NULL,
  holiday_name  TEXT NOT NULL,             -- "Thanksgiving Day", "Christmas (observed)"
  observed      BOOLEAN NOT NULL,          -- TRUE if the observed date differs from the actual date
  full_close    BOOLEAN NOT NULL,
  UNIQUE (exchange, holiday_date)
);

-- Ad-hoc alerts (operator-entered or feed-derived)
market_alerts (
  id              UUID PK,
  exchange        TEXT NOT NULL,
  alert_type      TEXT NOT NULL,           -- 'closure' | 'halt' | 'schedule_change' | 'early_close'
  effective_from  TIMESTAMPTZ NOT NULL,
  effective_to    TIMESTAMPTZ,             -- NULL = indefinite
  source          TEXT NOT NULL,           -- 'nyse_rss' | 'nasdaq_rss' | 'operator' | 'external_webhook'
  raw_payload     JSONB,                   -- full source record for forensics
  message         TEXT,
  created_at      TIMESTAMPTZ NOT NULL,
  acknowledged_by TEXT,                    -- operator user id, if human-ack required
  acknowledged_at TIMESTAMPTZ
);

Indexes: (exchange, session_date) on sessions, (exchange, effective_from) on alerts.

4e. API surface (consumed by Raptor, Antlers, future raxx-console)

Versioned prefix: /api/calendar/v1/

Method Path Returns
GET /session?at=<ts>&exchange=nyse { state, opened_at, closes_at, next_transition_at, next_state, active_alerts[] }
GET /next-open?exchange=nyse { timestamp, session_type }
GET /next-close?exchange=nyse { timestamp, session_type }
GET /is-open?at=<ts>&exchange=nyse { open: bool }
GET /holidays?year=2026&exchange=nyse [ { date, name, full_close, observed } ]
GET /session-date?date=2026-11-27&exchange=nyse { session_type, open, close, pre_market_open, after_hours_close }
GET /business-days/add?date=2026-04-24&n=5&exchange=nyse { date: "2026-05-01" }
GET /business-days/between?start=...&end=...&exchange=nyse { count: 12 }
GET /business-days/is-business-day?date=...&exchange=nyse { is_business_day: bool }
POST /alerts (operator-auth, via raxx-console) Create an ad-hoc alert
GET /alerts?since=...&active_only=true List alerts
POST /refresh (operator-auth) Force immediate daily refresh

All endpoints live under Cloudflare Access with the standard Raxx RBAC. No customer endpoint exposes write capability; reads can be cached aggressively (24h TTL on everything except /session and /alerts).

4f. Consumers


5. Alert flow (the part Kristerpher specifically asked about)

[NYSE Trader RSS]   [NASDAQ Trader RSS]   [Operator (raxx-console)]
         \                  |                       /
          \                 |                      /
           \                v                     /
            +--> [ Calendar service /alerts ] <--+
                            |
                            v
            +---------------+----------------+
            |                                |
            v                                v
    [ market_alerts table ]         [ Slack alert channel ]
            |                                |
            v                                v
    [ /session returns        [ Ops DM on high-severity alerts ]
      active_alerts[] ]
            |
            v
    [ Antlers MarketTimeWidget       [ Raptor trade-lifecycle
      surfaces "Market closed —         engine respects alerts
      FOMC emergency session"           when evaluating entries ]
      banner ]

Alert severity tiers (simplified per Section 11a decision): - Informational (log only) — schedule clarification, minor venue update. No banner. - User-visible banner — early close, emergency closure, halt. Banner in MarketTimeWidget.

Blocking trade initiation is NOT the alert system's job — that responsibility sits with Alpaca's real-time clock (per decision 11.3). The alert layer is communication only.

Severity is determined by keyword matching in v1 (operator can override via raxx-console). An ML classifier would be overkill.


6. Migration path

Phase 0 — v0 (status quo)

Phase 1 — v1 Blueprint ships

Phase 2 — v1.1 cutover

Phase 3 — v2 standalone service (conditional)


7. Operational posture


8. Cost + footprint

Line item v1 (Blueprint) v2 (standalone)
Compute $0 (shares Raptor dyno) ~$5/mo (Fly.io basic)
Storage <10 MB/yr in Postgres same (Fly volume or shared Postgres)
External data $0 (exchange_calendars is free; Alpaca already paid for) same
Rotation surface 0 new credentials 1 (service-to-service token)
Operator time ~2d initial, ~2h/qtr +1d migration, ~2h/qtr

v1 is effectively free; v2 is a $60/yr upgrade when we outgrow Option 1.


9. Risks + mitigations


10. Acceptance criteria (if this proposal is approved)

The shipped v1 Calendar Blueprint must:


11. Decisions locked (2026-04-24 — Kristerpher)

  1. Cutover strategy for MarketTimeWidgetBoth. Land backend-first, then cut the frontend over behind a feature flag. Progressive rollout with the flag flipped on a day we're actively watching.

  2. Alert notification channelBanner only. No customer email, no Slack paging layer for alerts. Alpaca real-time status is the go/no-go authority; the calendar service's alert layer is purely for user-visible banner copy. This simplifies the severity model — see Section 5 revision below.

  3. Authority when sources disagreeAlpaca always wins the tiebreak. When exchange_calendars and Alpaca disagree at any level (current session OR forward-looking), Alpaca's answer is authoritative and exchange_calendars is logged as the dissent. Rationale: everything else in the Raxx stack already sources from Alpaca; a single source of truth reduces the "which one is right" question to a non-question.

  4. Retention on alertsLog for audit/review only. No customer-facing alert history UI. Alerts persist in the market_alerts table forever (small table, valuable forensic record), but there is no customer-facing timeline or operator history browser. Logs + direct DB queries are sufficient for the cases where someone needs to look.

  5. raxx-console UI for alert entry at v1Yes. Operators get a simple form to post a manual alert (exchange, effective_from/to, alert_type, message). Matches the existing raxx-console pattern (#254 rotation UI, #212 Founders admin view).

11a. Severity model — simplified

Given the "Alpaca is the go/no-go authority" decision above, the original three-tier severity model (L1/L2/L3) collapses to two tiers:

The L3 "block-trade-initiation" tier is removed. Blocking is Alpaca's job, not the alert feed's job.


12. Recommendation

Approve the v1 Blueprint scope (Section 3, Option 1 in Section 4a, Phase 1 in Section 6). File as a new epic. Keep PR #242's env-var calendar as the v0 stopgap — it ships as planned. Calendar service then migrates PR #242's caller in Phase 2 as a low-risk follow-on.

Implementation estimate (after Kristerpher approval): ~8 engineering-days for Phase 1, one developer, behind a feature flag. Phase 2 cutover is ~2 days. Phase 3 standalone service is ~3 days if and when a second consumer appears.