# D01-F03-A04 - Exchange-Calendar Alignment

> **Status:** Video-ready | **Domain:** Market Data Engineering | **Reviewed:** 2026-07-22

## Executive summary and learning contract

Exchange-calendar alignment answers a narrower and more important question than
"Is this a weekday?": **under one pinned venue, market, calendar version, time
zone, and sampling policy, is this event eligible for this session?** The
schedule is an input. The algorithm does not invent sessions from weekdays,
fixed UTC hours, or missing rows.

After this topic, a reader can:

- distinguish a known closure from an unsupported calendar date;
- apply an open-inclusive, close-exclusive interval;
- explain why a named local open moves in UTC across daylight saving time;
- keep official exchange facts separate from an application's auction and
  extended-hours policy;
- reject nonexistent local times and require a fold for ambiguous ones; and
- build a session-reset grid with a partial final bar on an early close.

The reference package is pinned to **New York Stock Exchange, NYSE Tape A core
equities**, the `America/New_York` zone, IANA tzdb `2026c`, and a teaching
calendar retrieved/versioned `2026-07-22`. It is deliberately sparse and is not
a production NYSE calendar.

## Scope and policy boundaries

Official NYSE facts and package choices are different layers:

| Layer | Declared rule |
|---|---|
| Official schedule fact | The current NYSE page states a Tape A core session of 09:30-16:00 Eastern Time, with a 09:30 Core Open Auction and a 16:00 Closing Auction. |
| Official dated fact | The official 2025 schedule identified 2025-07-03 as an early-close day at 13:00 Eastern Time. |
| Package interval policy | Core membership uses `[open, close)`: the open instant is included and the close instant is excluded. |
| Package auction policy | Include the opening auction at the declared open; exclude the closing auction at the declared close for separate processing. |
| Package extended-hours policy | Exclude pre-market and post-market events. |
| Package coverage policy | A local date absent from the pinned bundle is `calendar_unsupported`, not assumed closed. |

This topic does not define order acceptance, auction eligibility by symbol,
halt logic, options hours, settlement dates, or a consolidated multi-venue
session. Those need their own venue/product-specific inputs.

## Input data contract

### Calendar bundle

| Field | Type | Meaning |
|---|---|---|
| `calendar_id` | nonempty string | Stable identifier for this compiled calendar |
| `calendar_version` | date/version string | Version used to reproduce the result |
| `retrieved_at` | date string | When official schedule sources were checked |
| `venue`, `market` | nonempty strings | Exact exchange and market/product scope |
| `time_zone` | IANA zone name | Local civil-time rule set, not a fixed offset |
| `tzdb_version` | release string | Time-zone evidence version (`2026c` here) |
| `supported_dates` | unique local dates | Exact coverage; must equal open sessions plus explicit closures |
| `sessions` | records | Explicit local and UTC open/close pairs, type, and source IDs |
| `closed_dates` | records | Explicit holiday or exceptional closure with reason and source IDs |

Every session has one `session_date`, `session_id`, local wall boundaries, UTC
boundaries, `session_type`, and nonempty source provenance. The validator rejects
overlap, duplicates, local/UTC disagreement, implicit coverage, and an
open-at-or-after-close interval.

### Events

| Field | Type | Rule |
|---|---|---|
| `event_id` | nonempty string | Preserved source identity |
| `timestamp` | RFC 3339 UTC instant ending in `Z` | Event time used for membership |
| `event_kind` | enum | `continuous`, `opening_auction`, `closing_auction`, `pre_market`, or `post_market` |

Input order is preserved. Market-data payloads may add instrument, sequence,
receive time, price, and size, but those fields do not change calendar
membership unless a separately versioned policy says so.

## Output contract

Each output preserves the event and adds local date, venue, market, time zone,
calendar ID/version, session lineage when known, source IDs, `eligible`,
`status`, and a reason.

| Status | Meaning |
|---|---|
| `in_session` | Inside `[open, close)` and included by event-kind policy |
| `outside_session` | Supported open date, but instant is outside the core interval |
| `excluded_by_policy` | Known session/date, but auction or extended-hours policy excludes the kind |
| `closed_date` | Explicit holiday or exceptional closure with source evidence |
| `calendar_unsupported` | Date is absent from the pinned bundle; eligibility fails closed |

`closed_date` and `calendar_unsupported` must never be collapsed. The former is
a sourced fact; the latter is an admission that the calendar has no answer.

## Mathematical formulation

For session $s$ with UTC open $o_s$ and close $c_s$, event instant $t_e$, and
included event-kind set $K$, the package uses:

$$
\operatorname{eligible}(e,s)
= \mathbf{1}\{o_s \le t_e < c_s\}
  \mathbf{1}\{k_e \in K\}.
$$

The first indicator is time geometry. The second is application policy. A
holiday has no session interval, and an unsupported date is not evaluated as a
holiday.

For interval length $\Delta>0$, grid cell $j$ is:

$$
B_{s,j}=
[o_s+j\Delta,\min(o_s+(j+1)\Delta,c_s)).
$$

The last cell is partial when its duration is less than $\Delta$. A grid resets
at every session open and never bridges a known or unknown closure.

## Algorithm and invariants

```text
validate venue, market, calendar version, named zone, and policy metadata
validate explicit session and closure dates; reject overlap and local/UTC drift
for each UTC event:
    derive its local date using the declared IANA zone
    if the date is unsupported: emit calendar_unsupported and eligible=false
    else if it is an explicit closure: emit closed_date and evidence
    else load the explicit session
         apply event-kind policy
         apply open <= timestamp < close
         emit status, reason, session lineage, and calendar lineage
```

Invariants:

1. No output is eligible without an explicit session row.
2. No missing date is labeled a holiday.
3. An exact open may be eligible; an exact close is not.
4. Pre/post-market and auction inclusion are explicit policy, not side effects.
5. Local wall-time conversion rejects DST gaps and unresolved folds.
6. Python and TypeScript consume the same fixture and emit the same status
   sequence.

```mermaid
flowchart LR
  A["Pinned venue and versioned calendar"] --> B["Convert UTC event to named-zone date"]
  B --> C{"Date explicitly supported?"}
  C -->|No| D["Fail closed: calendar_unsupported"]
  C -->|Closure| E["closed_date plus source evidence"]
  C -->|Open session| F{"Kind allowed and open <= t < close?"}
  F -->|Yes| G["in_session plus lineage"]
  F -->|No| H["outside_session or excluded_by_policy"]
```

## Worked historical example: 2025-07-03

This example is historical schedule evidence, not a market-performance claim.
The official NYSE 2025 materials identified Thursday 2025-07-03 as an early
close at **13:00 Eastern Time**. New York was on daylight time, so the teaching
bundle records:

- local core interval: `[2025-07-03 09:30, 2025-07-03 13:00)`
- UTC core interval: `[2025-07-03 13:30Z, 2025-07-03 17:00Z)`
- package policy: include the opening auction at 13:30Z and exclude the closing
  auction at 17:00Z for separate processing.

With $\Delta=60$ minutes, the grid contains three 60-minute cells and one
30-minute cell ending at 17:00Z. A naive 09:30-16:00 local template would add
three nonexistent core hours. The evidence proves the schedule exception; it
does not say anything about prices, returns, prediction, or profitability.

## DST, holidays, and exceptional dates

The local 09:30 open maps to 14:30Z on Friday 2025-03-07 and to 13:30Z on Monday
2025-03-10 after the U.S. spring DST transition. The local rule did not change;
the UTC offset did. The fixture also distinguishes:

- 2025-07-04: explicit Independence Day closure;
- 2018-12-05: explicit exceptional closure for the National Day of Mourning;
- 2025-04-01: unsupported in this sparse teaching bundle, even though it is a
  weekday.

For raw local wall times, `2025-03-09T02:30:00` in `America/New_York` does not
exist. `2025-11-02T01:30:00` occurs twice and requires fold 0 or 1. Event inputs
should preferably be unambiguous UTC instants; named-zone local values remain
necessary for schedule provenance.

## Implementation, examples, and tests

- `implementations/python/calendar_alignment.py`
- `implementations/typescript/calendarAlignment.ts`
- `examples/python_usage.py`
- `examples/typescript_usage.ts`
- `tests/test_calendar_alignment.py`
- `tests/calendar_alignment.test.ts`

The shared 34-event fixture covers normal/DST sessions, exact boundaries,
historical and future published early closes, ordinary and exceptional
closures, auction/extended-hours policy, and unsupported dates. Passing these
tests proves definition-level parity, not completeness of the teaching
calendar.

## Failure modes and limitations

| Failure | Required response |
|---|---|
| Weekday or fixed-UTC template | Reject as a calendar source |
| Date absent from partial bundle | `calendar_unsupported`, never inferred closed/open |
| Calendar source revision | Create a new version and retain recomputation lineage |
| Nonexistent local wall time | Reject |
| Ambiguous local wall time | Require explicit fold |
| Unknown auction/extended-hours requirement | Define another versioned sampling policy |
| Emergency/special session | Add an explicit sourced session or closure row |
| Runtime tzdb differs from metadata | Treat as an operational deployment mismatch and verify before production |

The code records a tzdb version but cannot prove that a host runtime actually
contains that release. Production deployment should pin and audit its time-zone
runtime, snapshot official calendar artifacts under their permitted terms,
monitor exchange notices, and rebuild affected outputs when inputs change.

Calendar alignment labels session eligibility. It does not prove tradability,
clean a price, validate a feed sequence, or predict a return.

## Visual explanations and related topics

The [session-policy SVG](visuals/static/dst-session-alignment.svg) compares a
normal offset, DST offset, historical early close, holiday, and unsupported
date. The [guided lab](visuals/animated/calendar-lab.html) steps through six
scenarios and keeps official facts visibly separate from the package policy.

**Previous-Tick Interpolation**, **Linear Quote Interpolation**, and
**Refresh-Time Sampling** should reset within these explicit partitions.
Continue with **Asynchronous Return Alignment**, which compares the actual time
intervals occupied by returns after calendar boundaries are correct.

## References

See [REFERENCES.md](REFERENCES.md) for source roles, versions, access dates,
applicability, and limitations.
