Shared Market Calendar Contract
Issue #331 replaces hand-written weekday and local-clock decisions with one generated, fail-closed cash-equity calendar contract.
Authority and compatibility
config/market-calendar-dependency.yaml is the only authority for the direct
runtime pin and consumer inventory. scripts/generate_market_calendar.py
generates each standalone skill’s _market_calendar.py, contract test, and
requirements.txt; each requirements file also retains that consumer’s direct
runtime dependencies. It also generates scripts/market_calendar/consumers.txt,
which the clean-room workflow reads directly, and keeps the two weekly-strategy
example mirrors in sync. The generated runtime is self-contained and never
reads repository files.
The direct calendar dependency is pandas-market-calendars==5.2.2. Releases 5.2.3 and
5.2.4 declare Python 3.8+ compatibility but fail while importing on Python 3.9
because a PEP 604 union is evaluated at runtime. Version 5.2.2 is therefore
pinned and exercised in a required Python 3.9 clean-room CI job. uv.lock
records repository resolutions, but standalone compatibility is guarded by the
direct pin plus behavioral tests rather than a fully locked transitive graph.
The Python 3.9 compatibility job creates one isolated virtual environment per consumer and installs only that skill’s generated requirements. This prevents one skill’s PyYAML, requests, or market-data dependencies from masking another standalone package’s missing declaration.
The calendar library is an external runtime dependency. Its package is installed
from requirements.txt; its source or wheel is not embedded in .skill files.
Supported venue contract
| Venue | Provider name | IANA timezone | Notes |
|---|---|---|---|
XNYS |
NYSE |
America/New_York |
US cash-equity session set |
XNAS |
NASDAQ |
America/New_York |
The provider resolves this alias to its NYSE calendar/session set |
XTKS |
JPX |
Asia/Tokyo |
Includes the lunch break and the 2024-11-05 close change |
XLON |
LSE |
Europe/London |
GMT/BST derives from the session instant |
JPX’s official cash-equity hours are 09:00-11:30 and 12:30-15:30 JST (JPX trading hours). US holiday and early-close fixtures are checked against the NYSE hours and calendars.
CME/futures and crypto are intentionally not mapped. The audited consumers use
cash-equity sessions only; adding an unused generic 24/7 or CME calendar would
hide product-specific session-label and maintenance-break rules. Add a venue only
with a real consumer and product-specific fixtures.
session_for_date() returns None for a valid exchange holiday or weekend.
Missing dependencies, unknown venues, provider failures, and incomplete schedule
rows raise an explicit error. Search functions use a 370-day finite horizon.
All datetimes are timezone-aware. Opens are inclusive, closes are exclusive, and
the JPX lunch break is closed.
Day-count boundaries
| Consumer | Start | End | Reverse/past behavior |
|---|---|---|---|
| Market environment event countdown | inclusive | exclusive | past event returns 0 |
| Market-top freshness | exclusive | inclusive | reverse returns legacy sentinel -1 |
| Parabolic earnings age | exclusive | inclusive | reverse keeps the legacy negative calendar-day invalid sentinel |
| Theme/uptrend freshness | exclusive | inclusive | future source date is stale/anomalous |
The shared count_sessions() requires both inclusion flags as keyword arguments
and rejects reverse ranges. Consumer adapters own their documented legacy
sentinels so no ambiguous default can introduce an off-by-one error.
as-of and point-in-time boundaries
| CLI | Contract |
|---|---|
| Market environment | Offset-bearing ISO-8601 timestamp or Z; date-only and naive values are rejected because multi-market status needs one instant |
| Breakout planner | Date means 00:00 America/New_York; an offset-bearing timestamp is also accepted |
| Drawdown circuit breaker | Existing date/datetime contract; halt expiry is the next XNYS session date at 00:00 ET, not the session open |
| Market-top detector | YYYY-MM-DD; historical live replay is rejected because current quotes are not point-in-time |
| Parabolic screener | Strict YYYY-MM-DD; fixture runs are deterministic, while historical live runs fail because universe/profile endpoints are not PIT |
| Theme detector | Strict YYYY-MM-DD; non-current live runs fail because FINVIZ, quote/profile, and uptrend sources are not PIT |
Provider query windows are not themselves official exchange calendars. Generated
FMP clients may use the live date to bound a request, so consumers filter bars
newer than as-of; insufficient history becomes an explicit incomplete/no-data
result. Current quote/profile endpoints are not advertised as historical PIT
sources. Provenance timestamps and output filenames record when an artifact was
created and are not inputs to signal, expiry, event, or freshness calculations.
Repository audit
| Location | Classification and disposition |
|---|---|
market-environment-analysis/market_utils.py and two example mirrors |
Status, session labels, and event countdown use the shared calendar and one aware instant; weekday formatting is display-only |
breakout-trade-planner/plan_breakout_trades.py |
Plan validity uses current-or-next XNYS session; default clock is aware UTC |
drawdown-circuit-breaker/check_circuit_breaker.py |
Halt dates use XNYS sessions; weekday() remains only for calendar-week accounting |
market-top-detector/utils.py, breadth_csv_client.py |
Freshness uses XNYS sessions; auto breadth selects the latest row at/before as-of, and future-dated scored CLI values fail closed |
market-top-detector/market_top_detector.py |
Historical CLI replay fails closed; index, leading, and sector histories are all ceiling-filtered before calculators receive them |
parabolic-short-trade-planner/market_clock.py, screen_parabolic.py |
Intraday state and earnings age use XNYS; live/fixture bars are ceiling-filtered |
parabolic-short-trade-planner/adapters/*_market_data_adapter.py |
Alpaca query windows and both live/fixture bar filters use the authoritative XNYS open/close; holidays are empty and early-close after-hours bars are excluded |
parabolic-short-trade-planner/ssr_state_tracker.py |
Rule 201 carryover loads the previous XNYS session, including weekend and holiday gaps |
Other parabolic datetime.now(timezone.utc) calls |
Provenance, state-write, or live-monitor evaluation timestamps; timezone-aware and not session-day inference |
parabolic-short-trade-planner/generate_pre_market_plan.py |
Explicit/Phase-1 as_of owns plan date; default clock is a live-only fallback |
theme-detector/uptrend_client.py, report_generator.py |
Both freshness decisions use identical XNYS boundaries; uptrend rows newer than the validated run date are excluded before scoring |
theme-detector/etf_scanner.py |
Provider history windows receive the run-date ceiling and returned FMP/yfinance rows are independently re-filtered before caching or scoring; no-as-of date.today() is library live-mode only |
| Theme/market-top generated timestamps and filenames | Provenance only; never used for scoring or freshness |
| Shanghai, Hong Kong, Singapore rows in market environment | Display-only reference text; no session/status/trading-day calculation consumes them |
The drift gate covers the authority file, generated CI inventory, workflow, generator/smoke inventory agreement, canonical implementation, all six consumers, both example mirrors, the optional extra, and CI policy.