Exchange calendars
Implemented (GLE-380, increment I11 unit A1 of the order-lifetime programme): the wire schema, the SDK types and the adapter contract. A data adapter can serve EXCHANGE_CALENDAR (data schema 15): for one futures root, one TradingDay record per exchange trade date, carrying the instants the date owns, its trading windows, its closure and early-close facts and the session at whose close a DAY order is cancelled, under a root-level header that carries the venue’s clock, a content revision, the coverage and the provenance. The adapter computes every window (one implementation of the window laws, marketfeed’s TradingWindows); the engine derives nothing from bar stamps. Refusal over guess: a date the holiday table does not know, an early close with an unknown time and a placeholder RTH travel as explicit states rather than a guessed window, and the engine decides admission. One calendar serves a root: the bare root, each dated contract and the :cont composite share it. The contract and its decisions D-I11-1 to D-I11-10 are recorded under the order-lifetime ticket; the wire numbers and the framing are in the protocol reference. The engine fetches the schema and pushes it to a strategy that asks for it since unit A2a (Engine side); see Status below.
The messages. ExchangeCalendarBatch is header (1, an ExchangeCalendar) and days (2, repeated TradingDay). The header:
| # | Field | Meaning |
|---|---|---|
| 1 | root | the owner, e.g. fut:XCME:ES |
| 2 | exchange_timezone | the IANA name of the venue’s clock, e.g. America/Chicago |
| 3 | revision | 64 lowercase hex characters, the content digest of the calendar inputs; equal revisions mean equal TradingDay records for every date |
| 4 | law | the rules the revision digests, mf-cal-v1 |
| 5 | trading_day_kind | eth or rth: the symbol’s own trading day, what the session alias names |
| 6 | rth_semantics | how regular hours are defined; globex-day-portion records the decision of 2026-09-13 |
| 7 | eras_recorded | false when the root’s current session: block was back-applied to all of its history, unverified |
| 8 | eras | repeated SessionEraInfo: the session eras of the root’s timeline, for provenance |
| 9 | holiday_table | HolidayTableInfo; unset when the venue has no table |
| 10 | coverage_from | the first trade date the calendar answers; "" for an unbounded first era and table start |
| 11 | coverage_to | the last; "" when there is no table |
| 12 | grid_revisions | boundary to intraday grid revision, so the engine can tie its grid requests to this calendar |
| 13 | day_cancel_rule | globex-trade-date-session or none: the assumed rule behind every day_cancel |
One trade date:
| # | Field | Meaning |
|---|---|---|
| 1 | trade_date | YYYY-MM-DD, the identity |
| 2, 3 | span_start, span_end | the instants the date owns, half-open: [span_start, span_end) |
| 4 | closed | a full-day closure, or a closed weekday of a per-weekday era |
| 5 | closed_known | the holiday table knows this date; false outside its range, or without a table |
| 6, 7 | early_close, early_close_known | an early close, and whether its time is known; an early close whose time is unknown must refuse precise use |
| 8 | early_close_at | set iff early_close and early_close_known |
| 9 | eth | repeated TradingWindow: the extended-hours legs, ascending, early close applied; empty when closed or none declared |
| 10 | rth | TradingWindow; set iff rth_state is open |
| 11 | rth_state | open, closed, none-declared, placeholder or unknown |
| 12 | day_cancel | TradingWindow: the session at whose close a DAY order is cancelled; set only under basis assumed |
| 13 | day_cancel_basis | assumed or unknown (ibkr-verified exists live only and does not travel on this schema) |
| 14 | era_from | the era in force; "" for an unbounded first era |
| 15 | weekday_state | trading or closed-weekday |
TradingWindow is open (1), close (2) and early (3: close is the table’s early close). SessionEraInfo, provenance for windows already computed under it, is from (1, "" for an unbounded first era), to (2, "" for the era in force), days (3), rth (4), eth (5), cut (6), continuous (7) and source (8). HolidayTableInfo is stem (1, e.g. cme_equity), version (2, the table’s content digest), range_from (3) and range_to (4).
The window convention. close is the close instant and the closing second belongs to the window, exactly as an intraday grid places its last bar (which ends one second after the close): consumers treat a window as the closed interval [open, close]. An empty window (open == close) is well-formed. The span is half-open, [span_start, span_end).
The record rule. One TradingDay per trade date that owns a span. Under an overnight session a weekend owns no span and has no record; its instants belong to Monday. A holiday that owns a span is a record with closed = true and no windows. A closed weekday of a per-weekday era is a record with weekday_state = "closed-weekday" and closed = true. A date the holiday table does not know is a record with closed_known = false, rth_state = "unknown" and no windows, not a refusal: the engine decides admission.
The confidence fields. Five fields say how far a record can be trusted, so that the engine’s admission rule (unit A2b) reads them instead of guessing: closed_known (the table knows the date); early_close_known (the early close’s time is known; without it an early close refuses precise use); rth_state (only open carries a window; closed, none-declared, placeholder and unknown are explicit states in place of a guessed one); day_cancel_basis (assumed, the Globex trade-date session, is the only basis under which day_cancel is set; unknown labels equities and venues without tables); and the header’s eras_recorded (false for a root whose current hours were back-applied to all of its history; D-I11-4 fails precise-time admission on such a root unless it is listed in calendar-accept-unverified).
The SDK types. sdk/data and sdk/strategy each declare TradingWindow, SessionEraInfo, HolidayTableInfo, TradingDay and ExchangeCalendar (the header plus Days []TradingDay), the wire names in Go casing (RTHState, RTH, ETH, RTHSemantics), and re-export the vocabulary of internal/wire: CalendarKindETH/CalendarKindRTH, the five RTHState* constants, DayCancelAssumed/DayCancelUnknown, WeekdayTrading/WeekdayClosedWeekday and TradeDateLayout (2006-01-02). The converters — data.ToPBExchangeCalendar and FromPBExchangeCalendar with the per-day ToPBTradingDay and FromPBTradingDay, the same four in internal/pbconv over the strategy SDK’s types, and the strategy SDK’s own unexported fromPBExchangeCalendar (it cannot import pbconv, and its public surface carries no proto type) — share one rule set: a zero time.Time is an unset timestamp and an unset timestamp the zero time, not the Unix epoch; a set timestamp decodes as its instant in UTC with no monotonic reading (the wire carries no location, so compare decoded instants with Equal; a round trip is reflect.DeepEqual for UTC times); a nil RTH, DayCancel or HolidayTable is an unset message; empty Eras, Days, ETH and GridRevisions are absent on the wire and decode as nil; a nil element of a repeated wire field decodes as the zero value of its kind; maps and slices are copied, not aliased; ToPB always sets the header; and FromPB(nil) is the zero value. The strategy SDK’s name Calendar is unit A3’s lookup view (Strategy SDK lookups).
Adapter side (Go SDK). An adapter serves the schema by advertising SchemaCapability{Schema: EXCHANGE_CALENDAR, SupportsHistorical: true} and implementing data.ExchangeCalendarSource:
OnGetExchangeCalendar(ctx *data.DataContext, req data.ExchangeCalendarRequest) (data.ExchangeCalendar, error)ExchangeCalendarRequest carries Symbol (verbatim; the adapter resolves the root), Start and End (zero = unbounded; the window scopes which trade dates are of interest, and an adapter may serve the whole calendar) and BatchSize (the request’s batch_size, 4096 when it sent 0). Unlike BarSource and RollScheduleSource, which push slices on an emit channel, the source returns the whole calendar in one value: the header must precede the days on the wire, a count-based re-batcher would split a header-carrying chunk, and marketfeed’s library call returns the header and the days together. This is a deliberate deviation from the contract’s unit row, which wrote the signature with emit, and is recorded against the contract. The SDK frames the response: it validates the wire form, then sends BatchSize days per envelope with the header on the first, under the batching law in the protocol reference; the snapshot token the adapter sets on the context during the call rides the final envelope, as on every schema. The capability and the interface must agree, in both directions, and data.Run refuses to start on a mismatch as it does for every other schema: capabilities advertise EXCHANGE_CALENDAR but the adapter does not implement data.ExchangeCalendarSource, or adapter implements data.ExchangeCalendarSource but capabilities do not advertise EXCHANGE_CALENDAR. The pinned marketfeed adapter implements none of this and builds and serves unchanged.
Refusals. Every refusal on the request’s path is the request’s own (a request-scoped Error; the connection stays up and serves the next request), checked in this order:
| Code | Text | When |
|---|---|---|
unsupported_schema | adapter does not serve EXCHANGE_CALENDAR | the adapter does not advertise the schema |
bad_request | EXCHANGE_CALENDAR cannot travel on stdio-dbn-v1 (DBN carries only OHLCV/trades); negotiate stdio-pb-v1 | the DBN wire; the source is not called |
source_error | data source: <the source's text> | OnGetExchangeCalendar returned an error |
source_error | the validator’s text, e.g. invalid exchange calendar "fut:XCME:ES": day 2026-03-12: rth is unset but rth_state is "open" | the calendar fails wire.ValidateExchangeCalendar; the refusal is the request’s first envelope, nothing having been sent |
unsupported_schema | schema EXCHANGE_CALENDAR is not servable | the CSV data wire (a refused line), through its existing default case, before the source is reached |
A write failure while framing is fatal, as on every schema: the SDK exits, because nothing can be answered.
The validator. wire.ValidateExchangeCalendar is one rule set on the pb form, shared by both ends: the data SDK runs it on what an adapter produced before the first envelope leaves, and the engine (unit A2a) runs it on what it received, after joining a request’s envelopes with wire.MergeExchangeCalendarBatches (the first batch carries the header, no later one repeats it, the days are concatenated). It returns the first violation in a pinned order (header before days, earlier days first, within a day the order below), wrapped in wire.ErrCalendarInvalid and worded invalid exchange calendar "<root>": <detail> with day <trade_date>: prefixed to a day’s detail, or nil. The header rules: root, exchange_timezone and law non-empty; revision 64 lowercase hex; trading_day_kind eth or rth; coverage_from and coverage_to dates when set. The day rules: trade_date a date, strictly after the previous day’s; the span set and non-empty, and not starting before the previous span ends (overlap is refused, a gap is not: a continuous era owns no weekend span); rth_state one of the five, with rth set iff open; every eth leg, rth and day_cancel well-formed (both instants set, close not before open) and the legs not overlapping (a leg may open exactly at the previous close); a closed day with no eth legs, no day_cancel and an rth_state other than open; early_close_known only with early_close, and early_close_at set iff both; day_cancel_basis assumed or unknown, with day_cancel set only under assumed; weekday_state trading or closed-weekday, the latter only when closed; era_from a date when set. Provenance is not checked: rth_semantics, day_cancel_rule, the eras, the holiday table and grid_revisions. Deliberately not checked either, and therefore unit A2’s to tolerate or check when it consumes the calendar: gaps between consecutive spans, a window lying outside its date’s span, a trade_date outside [coverage_from, coverage_to], closed_known against the holiday table, and nil elements in eras. marketfeed’s calendar (M1) must satisfy the structural rules, in particular: revision 64 lowercase hex, rth set iff rth_state is open, early_close_at set iff a known early close, day_cancel only under basis assumed and not on a closed day, closed-weekday only with closed.
Status. A1 is the schema, the types and the adapter’s serving path. marketfeed M1 (the library: Feed.ExchangeCalendar and Feed.CalendarRevision) and M2 (the adapter: the capability, OnGetExchangeCalendar, InstrumentInfo.exchange_timezone) serve it since marketfeed PR 147 (GLE-417); the adapter carries it from the next marketfeed install, and algolang’s golden pin moves when a golden needs it. Unit A3, the strategy SDK’s side, has landed (Strategy SDK lookups, below), and so has unit A2a, the engine’s side up to the push (Engine side, below): the engine’s DataClient reads the capability and fetches the schema on the pb data wire (the capability still satisfies neither RequireBars nor RequireTrades), checks and builds each root’s calendar at fetch time, applies the calendar-revisions: pins and --strict-calendar, records ResultHeader.Calendars and pushes the calendars to a strategy that asks for them. Unit A2b has landed too (Conformance checks and admission, below): the three conformance checks against session and grid bars, and admission over Complete. Unit A4 (the live check against IBKR’s contract rules) consumes it next.
Implementation anchors: internal/wire/calendar.go, sdk/data/calendar.go, internal/pbconv/calendar.go, sdk/strategy/calendar.go, their tournament_calendar_spec_test.go files and engine/tournament_calendar_host_spec_test.go.
Strategy SDK lookups
Implemented (GLE-429, increment I11 unit A3 of the order-lifetime programme): the strategy SDK’s side. sdk/strategy/calendarview.go gives a strategy, and the engine that imports the SDK, one implementation of the calendar lookups: Calendar, a read-only view over one root’s ExchangeCalendar, built by NewCalendar; CalendarRoot, the root a symbol’s calendar is filed under; ctx.Calendar(symbol); the optional CalendarObserver hook and CalendarRequirer declaration, which set InitAck.needs_calendar; and the pb loop’s receipt of the engine’s push. Unit A2b calls the same lookups for its own checks (TradeDate once per bar, Complete for admission), and later increments build on NextBoundary (I12’s scheduled decision points) and Complete (I13b’s calendar-refined GTC cancel), so the lookups are pure, exact and cheap. The engine pushes the calendars since unit A2a (see Status below).
Building the view. NewCalendar(facts ExchangeCalendar) (Calendar, error) checks only the facts the lookups depend on and returns the view, or the zero Calendar and an error that wraps ErrInvalidCalendar (errors.Is holds). The first violation wins, in this order: the root; the header rules H1–H3; then the days in index order, each through D1–D11 in order, and within D7 the legs in index order, each leg’s window rule before its order rule. An empty root reads invalid calendar: root is empty; every other violation reads invalid calendar "<root>": <detail>. A date is a string that time.Parse(TradeDateLayout, s) accepts, so 2026-02-30, 2026-4-01 and "" are not; dates compare as strings, and "<v>" below is a value quoted as Go’s %q quotes it.
| Rule | Refused when | <detail> |
|---|---|---|
| H1 | CoverageFrom is set and not a date | coverage_from "<v>" is not a YYYY-MM-DD date |
| H2 | CoverageTo is set and not a date | coverage_to "<v>" is not a YYYY-MM-DD date |
| H3 | both are set and CoverageFrom sorts after CoverageTo | coverage_from <from> is after coverage_to <to> |
| D1 | Days[i].TradeDate is not a date | days[<i>]: trade_date "<v>" is not a YYYY-MM-DD date (i 0-based) |
Every later day rule reads day <trade_date>: <text>, with the day’s own trade date (p is the previous day; Days[0] has none):
| Rule | Refused when | <text> |
|---|---|---|
| D2 | the trade date is not after p’s | not after the previous day <p's trade date> |
| D3 | SpanStart or SpanEnd is zero, or SpanStart is not before SpanEnd | span is unset or empty |
| D4 | SpanStart is before p.SpanEnd (an overlap; touching spans and gaps pass) | span starts before the previous span ends |
| D5 | RTHState is not one of the five states | rth_state "<v>" is not one of open, closed, none-declared, placeholder, unknown |
| D6 | RTH is set and RTHState is not open; or RTH is nil and RTHState is open | rth is set but rth_state is "<v>"; rth is unset but rth_state is "open" |
| D7 | for each ETH[j]: the window rule for eth[<j>], then, for j > 0, ETH[j].Open before ETH[j-1].Close (a leg may open exactly at the previous close) | the window rule’s text, or eth[<j>] opens before eth[<j-1>] closes |
| D8 | RTH is set: the window rule for rth | the window rule’s text |
| D9 | DayCancel is set: the window rule for day_cancel | the window rule’s text |
| D10 | Closed, and, in this order, ETH has legs, DayCancel is set, RTHState is open | closed but has eth legs; closed but has day_cancel; closed but rth_state is "open" |
| D11 | DayCancel is set and DayCancelBasis is not assumed | day_cancel is set but day_cancel_basis is "<v>" |
The window rule, for a window named <name>: first, when Open or Close is zero or Close is before Open, <name> is unset or ends before it opens (an empty window, Open == Close, passes); then, when Open is before the day’s SpanStart or Close is not before its SpanEnd, <name> lies outside the span. So every window lies inside its span, SpanStart <= Open <= Close < SpanEnd, and its close instant belongs to its own trade date. A1’s validator leaves this to the consumer; the lookups depend on it (TradeDate of a window’s close is the window’s own date, and the NextBoundary walk needs boundaries that do not decrease from one day to the next), and marketfeed places every window with Close + 1s <= SpanEnd, so a real calendar passes. Examples: invalid calendar "fut:XCME:ES": day 2026-04-01: eth[1] opens before eth[0] closes; invalid calendar "fut:XCME:ES": days[2]: trade_date "2026-02-30" is not a YYYY-MM-DD date. A calendar with no days is valid.
What it does not check. NewCalendar checks the lookups’ subset and does not repeat the A1 wire validator (above), which covers the rest on the engine’s side. It keeps as given, unchecked: the revision, the zone, the law, TradingDayKind, RTHSemantics, DayCancelRule, the eras, the holiday table and GridRevisions; and on a day, EarlyCloseAt and EarlyCloseKnown against EarlyClose, WeekdayState, EraFrom, ClosedKnown, and the DayCancelBasis of a day with no DayCancel. Equal coverage bounds are valid. A date whose ClosedKnown is false but which carries open windows is accepted, and the lookups still read it as unknown. The two rule sets overlap, with different texts; the engine runs both at fetch time, the validator first (Engine side).
The stored copy. On success the view holds a deep copy of facts in which every instant (SpanStart, SpanEnd, EarlyCloseAt, and every window’s Open and Close) is converted with .UTC(), a zero time staying the zero time.Time{} (the conversion also drops any monotonic reading); every empty slice or map (Eras, GridRevisions, Days, a day’s ETH) is nil; and every slice, map and pointer is copied. So a calendar built from instants located in America/Chicago answers exactly as one built from the same instants in UTC, and returns UTC instants; and a later change to the caller’s facts, to anything they point to, or to a value a method returned, changes no answer. Facts() and Day() return deep copies under the same rules: Facts() after NewCalendar(f) is reflect.DeepEqual to f whenever f’s instants are UTC and it holds no empty non-nil container, which is true of a calendar decoded from the wire.
The zero Calendar is the empty calendar: Root, Zone and Revision return "", Facts returns ExchangeCalendar{}, TradeDate, Day, Span and NextBoundary report false with zero values, and Complete reports no-calendar.
The methods.
| Method | Answer |
|---|---|
Root() | facts.Root |
Zone() | facts.ExchangeTimezone, the IANA name of the venue’s clock; load it with time.LoadLocation to render local wall clocks. Every lookup works in UTC instants, so the host’s time-zone database cannot change an answer. |
Revision() | facts.Revision |
Facts() | the deep copy (above) |
TradeDate(t) (string, bool) | the trade date whose span contains t, SpanStart <= t < SpanEnd compared as instants (the location of t does not matter); ("", false) before the first span, at or after the last span’s end, or inside a gap |
Day(date) (TradingDay, bool) | a deep copy of the record whose TradeDate is exactly date; TradingDay{} and false for a date with no record, a malformed string or "" |
Span(date) (start, end time.Time, ok bool) | that record’s SpanStart and SpanEnd; two zero times and false otherwise |
Span ownership follows the record rule. A span is half-open: its start belongs to the date and so does the last nanosecond before its end, while the end instant itself belongs to the next date when the spans touch. On the ES calendar Monday 2026-03-30 owns [03-27 22:00Z, 03-30 22:00Z) (CDT), so Friday 17:00 CT is Monday’s first instant and the weekend has no record, and 03-30 22:00Z is Tuesday’s. In an era where only business days own spans, the instants between two spans belong to no date: TradeDate reports false there, and Day and Span have no record for a date that owns no span.
NextBoundary(t, kinds) returns the earliest boundary of a requested kind strictly after t, for timers that fire at a calendar instant: a (time.Time, Boundary, bool) carrying the instant in UTC and the set of requested kinds found at exactly that instant. Boundary is a uint16 set, one bit per kind; six bits are defined and the other ten are free, so later kinds (the legs of a halted session, for example) fit without changing the type:
| Kind | Bit | Instant on day d | Present when |
|---|---|---|---|
BoundaryRTHOpen | 1 | d.RTH.Open | RTH is set |
BoundaryRTHClose | 2 | d.RTH.Close | RTH is set |
BoundaryETHOpen | 4 | d.ETH[0].Open, the session’s first open | ETH has legs |
BoundaryETHClose | 8 | the last leg’s Close, the session’s last close | ETH has legs |
BoundaryDayCancelClose | 16 | d.DayCancel.Close | DayCancel is set |
BoundarySpanEnd | 32 | d.SpanEnd | always |
BoundaryAll (63) is every kind. Only the defined bits count (kinds & BoundaryAll): other bits are ignored, and no defined bit is no answer. A halt between ETH legs (one leg’s close followed by the next leg’s open) is not a boundary of any kind; it is visible through Day(date).ETH. Strictly after means that an instant equal to t is passed over: from Monday’s RTH open itself, BoundaryRTHOpen answers Tuesday’s. Ties are unioned, within a day and across days: from 03-30 15:00Z the RTH, ETH and DAY-cancel closes all fall at 21:00Z and come back as one answer with three bits, and BoundaryAll from 21:00Z answers (03-30 22:00Z, BoundaryETHOpen|BoundarySpanEnd, true), Monday’s span end and Tuesday’s ETH open.
The knowledge rules. Refusal over guess: a day whose facts cannot place a requested kind ends the search with no answer rather than being skipped, because it might hold an earlier boundary than any later day. A day’s windows are known when ClosedKnown is true and the day is either Closed or has no early close of unknown time and an RTHState other than unknown. Given known windows, the ETH kinds are known; the RTH kinds are known unless the day is not closed and its RTHState is placeholder; and the DAY-cancel close is known when the day is closed or its DayCancelBasis is assumed. The span end is known on every day. So a closed day the table knows is known to have no windows, a none-declared day is known to have no RTH, a placeholder day places its ETH and DAY-cancel boundaries but not its RTH ones, and a day of a root with no DAY-cancel basis cannot place a DAY-cancel close unless it is closed. From Tuesday 2026-04-07 21:00Z, with Wednesday a date the table does not know, BoundaryRTHOpen reports false, BoundarySpanEnd reports Tuesday’s 22:00Z, and BoundaryETHOpen|BoundarySpanEnd reports false, because Wednesday’s span starts at that same instant and its ETH open cannot be placed. An instant before the first served span has no answer either.
The walk. A binary search finds the first day whose span ends after t; the walk goes forward from there and stops at the first day whose span starts after the best instant found so far (a day whose span starts exactly at it is still read, since it can add a tied kind). Days with no requested boundary after t, such as a holiday, are walked over, and so are gaps. Because every window lies inside its span, boundaries do not decrease from one day to the next, so the walk is exact and usually stops within a day or two.
Coverage is not consulted. NextBoundary answers from the served days and ignores CoverageFrom, CoverageTo and ErasRecorded: it can place a boundary on a date outside the coverage or on a root whose eras are not recorded. A caller that needs a precise instant (a timer, a GTC cancellation) checks Complete over the window first, which refuses both.
Complete(from, to, acceptUnverified) reports whether the facts of the instant window [from, to) are complete enough for precise-time use (decision D-I11-4). It returns an untyped nil when they are, else a *CalendarIncompleteError whose Root is the calendar’s root ("" only for no-calendar), TradeDate the first incomplete trade date ("" for a reason that names none), Reason one of the nine CalendarReason* constants and Detail the text below. acceptUnverified admits a root whose session eras are not recorded: the engine will pass its run’s calendar-accept-unverified membership, and a strategy passes its own policy. The days in the window are those whose span meets it (SpanStart < to and SpanEnd > from), in order. A gap inside the window is not a failure, since no date owns those instants, so a window lying wholly in a gap is complete. The first failure in this order wins:
| # | Fails when | Reason | Detail |
|---|---|---|---|
| 1 | the zero Calendar | no-calendar | no exchange calendar was served |
| 2 | to is not after from | empty-window | the window [<from>, <to>) is empty |
| 3 | ErasRecorded and acceptUnverified are both false | eras-unrecorded | the session eras are not recorded: the current hours were back-applied to all history |
| 4 | no days | not-served | no trade date is served |
| 5 | from is before the first day’s SpanStart | not-served | the window starts at <from>, before the first served span starts at <span start> |
| 6 | to is after the last day’s SpanEnd | not-served | the window ends at <to>, after the last served span ends at <span end> |
| 7a | for each day in the window: its trade date is before CoverageFrom | outside-coverage | trade date <D> is before coverage_from <CoverageFrom> |
| 7b | its trade date is after CoverageTo | outside-coverage | trade date <D> is after coverage_to <CoverageTo> |
| 7c | ClosedKnown is false | date-unknown | trade date <D> is not known to the holiday table |
| 7d | Closed: the day is complete, and the next day is read | ||
| 7e | EarlyClose without EarlyCloseKnown | early-close-unknown | trade date <D> closes early at an unknown time |
| 7f | RTHState is placeholder | rth-placeholder | trade date <D> has a placeholder RTH |
| 7g | RTHState is not open, closed or none-declared | rth-unknown | trade date <D> has rth_state "<state>" |
Rows 1–6 name no date; rows 7a–7g name the day’s. Instants render as t.UTC().Format(time.RFC3339Nano) (2026-03-27T21:00:00Z, 2026-04-20T22:00:00.000000001Z), coverage bounds are inclusive and compare as strings, and a window that ends exactly at the last span’s end is served. Error() is exchange calendar incomplete: <Detail> when Root is empty and exchange calendar "<root>" incomplete: <Detail> otherwise, for example exchange calendar "fut:XCME:ES" incomplete: trade date 2026-04-08 is not known to the holiday table. The signature deviates from the contract, which wrote Complete(from, to) returning a bool: the strategy SDK does not see the engine’s configuration, so each caller states its own policy, and an error lets the engine’s calendar_incomplete refusal (unit A2b) name the first incomplete date and the reason, as the contract requires of it. The deviation is recorded in the contract’s amendments at unit freeze, beside A1’s.
The root of a symbol. CalendarRoot(symbol) names the root whose calendar serves a symbol (one calendar serves a root: the bare root, each dated contract and the :cont composite share it). It takes the text before the first @ (all of it when there is none) and, when that has at least three :-separated fields and none of the first three is empty, returns the first three as written (a prefix of the text, with no case folding); otherwise "". A ~zone follows the @ only, as marketfeed’s symbol parser reads it, so cutting at the @ removes it too; a relative ticker such as ES+1 is not resolved. The engine deduplicates its per-root fetch with it (unit A2a).
| Symbol | Root |
|---|---|
fut:XCME:ES, fut:XCME:ES:M26, fut:XCME:ES:cont:adj=ratio, fut:XCME:ES@rth, fut:XCME:ES:M26@rth~America/Chicago | fut:XCME:ES |
eq:XNAS:AAPL:adj=split | eq:XNAS:AAPL |
eq:XASX:IVV, idx:XCME:BRR | the symbol itself |
ES, fut:XCME, fut:XCME:, fut::ES, :XCME:ES, fut:XCME::M26, @fut:XCME:ES, fut:XCME@x:ES, "" | "" |
ctx.Calendar(symbol) (Calendar, bool) is the strategy’s lookup, local like ctx.Instrument: the calendar the engine pushed whose root is CalendarRoot(symbol), and true; or the zero Calendar and false when nothing was pushed for that root (or at all) or the symbol has no root.
Asking for the calendar. InitAck.needs_calendar (14) is a flag in the needs_rolls and needs_instruments idiom, not a capability: nothing is offered or checked for it. initStrategy sets it once, after OnInit (so a strategy can decide from its inputs): true when the strategy implements CalendarObserver, whose NeedsCalendar, if it has one, is then not called; otherwise the result of exactly one call to NeedsCalendar when it implements CalendarRequirer; otherwise false, which marshals as before, so a strategy that implements neither sends a byte-identical InitAck.
type CalendarObserver interface {
OnCalendar(ctx *Context, cal Calendar) error
}
type CalendarRequirer interface {
NeedsCalendar() bool
}The push. The engine pushes one ExchangeCalendarBatch per root (Envelope.exchange_calendar, 61) after InstrumentsResolved and before the first warm-up bar. A push has no reply. For each one, the SDK’s pb loop:
- converts the batch and builds the view with
NewCalendar; an error refuses the run with codeprotocol_errorandNewCalendar’s text unchanged (a batch without a header has an empty root:invalid calendar: root is empty); - refuses a second push for a root it already holds, with code
protocol_errorand the textexchange calendar for root "<root>" pushed twice; the first calendar stays and the hook is not called; - stores the view under its root, so
ctx.Calendaranswers from then on, inside the hook included; - calls
OnCalendar(ctx, cal)when the strategy implementsCalendarObserver; a non-nil error refuses the run with codecalendar_rejectedand the textOnCalendar: <the error>, which wraps it, soerrors.Isanderrors.AsonRun’s error still find the strategy’s cause.
A refusal sends an Error envelope with that code and text, and Run returns algolang: <text>. A strategy that did not ask still accepts and stores a push. The SDK does not check the push’s place in the stream; the engine owns the ordering.
The CSV wire. stdio-csv-v1 has no grammar for calendar facts: neither needs_calendar nor the push travels on it (A1’s CSV init_ack formatter drops the flag). So a strategy whose InitAck sets needs_calendar refuses the engine’s CSV wire rather than run without a calendar: after OnInit it answers the init line, in place of the init_ack, with an error,csv_unsupported,<text> line whose text directs it to stdio-pb-v1, and Run returns a non-nil error. A raw-CSV replay is exempt: it has no engine to push a calendar either way, so it runs and ctx.Calendar reports none. stdio-csv-v2 leaves the calendar out too: its init_ack writer refuses needs_calendar rather than drop it, and the Go SDK’s v2 runner answers such a strategy as the v1 runner does, with the same error,csv_unsupported,<text> line (Go over stdio-csv-v2).
Concurrency. A Calendar built by NewCalendar is immutable: no method writes to it, nothing is filled in lazily (no index built on first use, behind a sync.Once or otherwise), and it holds no memory a caller can reach. It is safe to copy and safe for concurrent use by several goroutines. The Context is not: ctx.Calendar reads a map that a push writes, so call it from the strategy’s callbacks and hand other goroutines the Calendar value itself. The push is due before the first warm-up bar, so none lands while bars are flowing.
Cost. TradeDate, Day and Span find their day by binary search, O(log n) in the days served; NextBoundary costs O(log n) plus the days it walks, and Complete O(log n) plus the days in the window. TradeDate, Span, NextBoundary, a Complete that returns nil, Root, Zone and Revision allocate nothing (testing.AllocsPerRun reports 0, and the tests pin it); Day and Facts return deep copies and allocate, as do NewCalendar and a failing Complete. The engine (unit A2b) calls TradeDate once per bar over calendars of up to about 8,000 days.
Status. The engine’s side of the push has landed with unit A2a (Engine side, below): the engine fetches one calendar per root (deduplicated with CalendarRoot), validates it at fetch time with wire.ValidateExchangeCalendar and builds it with NewCalendar, refusing the run with calendar_invalid and the original text (so the SDK’s protocol_error is only a backstop), and pushes the served calendars to a strategy that set needs_calendar, after InstrumentsResolved and before the first warm-up bar. A2a also covers the calendar-revisions: pins with --strict-calendar, calendar-accept-unverified: and ResultHeader.Calendars. Unit A2b has added the three conformance checks and the engine’s admission over Complete (Conformance checks and admission); unit A4 is the live check against IBKR’s contract rules. A strategy that sets needs_calendar gets no calendar for a root the run is served none for (an adapter without the schema, a data wire other than pb, the auto wire’s DBN among them, or a refused request): ctx.Calendar reports none and Complete on the zero Calendar reports no-calendar, the contract’s no-calendar state, in which precise-time features refuse admission. No bundled strategy sets the flag, so every bundled strategy’s InitAck and the KBD masters are byte-identical.
Implementation anchors: sdk/strategy/calendarview.go; its call sites in context.go (Context.calendars), run.go (initStrategy), run_pb.go (the push) and run_csv.go (the refusal); tournament_calview_spec_test.go and calview_review_test.go.
Engine side
Implemented (GLE-433, increment I11 unit A2a of the order-lifetime programme): the engine’s fetch, pins, provenance and push. engine/calendar.go fetches the exchange calendar of every root a run reads, checks and builds each one as it arrives, applies the run’s pins and strict policy, records what the run read in its artefacts and pushes the calendars to a strategy that asked for them. The conformance checks of bars and grids against the calendars, and the engine’s precise-time admission, are unit A2b (Conformance checks and admission, below). A run that asks for nothing fetches nothing and writes byte-identical artefacts.
When the engine fetches. The run’s calendar stage runs when any one of these asks for it, and otherwise not at all:
- the strategy’s
InitAcksetsneeds_calendar(aCalendarObserveror aCalendarRequirer, above); --exchange-calendar(configexchange-calendar): fetch and record the calendars without pinning them;- a pin in
--calendar-revisions(configcalendar-revisions); --strict-calendar(configstrict-calendar).
calendar-accept-unverified and calendar-margin-quarters alone ask for nothing.
The fetch. DataClient.ServesExchangeCalendar reads the adapter’s capability, and DataClient.FetchExchangeCalendar makes one GetData request for the schema. It is pb only: on another data wire nothing is sent. A run fetches each distinct root once, deduplicating its symbols with CalendarRoot, so the bare root, each dated contract and the :cont composite of one root share one request, which names the root itself. There is no persistent cache: a calendar is not written to the series cache, as a roll schedule is not, and the adapter is asked on every run, including one whose bars the cache serves. The reason, recorded in the contract’s amendments at the unit’s freeze: the adapter offers no revision probe, and a registry deploy moves a calendar’s revision without changing the adapter build id the cache keys on, so a cached calendar could not be told stale, and the pins and the provenance would record a revision the feed no longer serves; one request per root per run is cheap. The fetch is not counted in the BENCH line, whose records stay bars and trades. In auto negotiation --exchange-calendar, a pin or --strict-calendar makes the run prefer stdio-pb-v1; a strategy’s needs_calendar cannot, since the data wire is chosen before the strategy’s Init (see The push, below).
The fetch slot. On the plain path (runOne) the fetch runs once the warm-up bars are in, while the adapter is still alive: the window must cover the warm-up reach-back, as the corporate-action window does, and the reach-back is known only after the warm-up fetch (a grid warm-up counts bars back across sessions, and an event pre-roll widens until it holds its depth). The push still comes before the first warm-up bar is sent. The order is the bar fetch, the strategy’s Init, the warm-up fetch and the calendar fetch, then InstrumentsResolved, the calendars and the first warm-up bar; a live run pushes at the same place, after AdoptedState and InstrumentsResolved. A continuous series has run through runOne’s phases since GLE-423, so the same slot serves it: the fetch after the strategy’s Init, where needs_calendar becomes known, and the push after InstrumentsResolved, before the first bar and its warm-up (a continuous series warms up since GLE-393, so its warm-up bars widen the calendar’s window as a plain series’ do).
The window. The request asks for [from - 14 days, to + 3q months), computed in UTC with calendar arithmetic (AddDate, so 2026-08-31 plus six months is 2027-03-03), where q is calendar-margin-quarters (0 selects the default, 2; at most 40). On the plain path from and to are the run’s data span, the earliest ts_open and the latest ts_close over the main and the warm-up bars, not the configured range: a config without dates runs from 1970 to 2100, which would ask a venue with no holiday table for 130 years of unknown days. A run with no bar falls back to its --start and --end. When the run has a continuous series the window starts no later than the strategy’s Init window (the first and last bar of the built series), because a continuous loop bar carries only its close; the plain and warm-up bars widen it as before. All the roots of a run share one window. A run whose bars open at 2026-03-08 22:00Z and close by 2026-03-20 21:00Z asks for [2026-02-22T22:00:00Z, 2026-09-20T21:00:00Z). Two quarters past the last bar always reach the end of the quarter after the one that holds it, which I13b’s following-quarter close needs. A caveat for I13b: the end is exclusive and reaches the end of that quarter, not necessarily the first trade date after it (a CME trade date opens the evening before its date); a unit that needs that date confirms that the adapter serves dates straddling the end, or pads the window itself. The conformance checks (A2b) need no such date: every instant they look up is a bar’s stamp, inside the window.
Validation at fetch. FetchExchangeCalendar joins the request’s envelopes (wire.MergeExchangeCalendarBatches), runs the wire validator (wire.ValidateExchangeCalendar), checks that the served header’s root is the root the request named, and builds the view with NewCalendar, whose stricter rules put every window inside its span. The first step to fail refuses the run with the reason calendar_invalid, its text carried verbatim after the adapter’s name:
engine: calendar_invalid: marketfeed-adapter: invalid exchange calendar "fut:XCME:ES": day 2026-03-10: rth is unset but rth_state is "open"
engine: calendar_invalid: marketfeed-adapter: invalid calendar "fut:XCME:ES": day 2026-03-10: eth[0] lies outside the span
engine: calendar_invalid: marketfeed-adapter: invalid exchange calendar "fut:XCME:NQ": served for "fut:XCME:ES", whose root is "fut:XCME:ES"
engine: calendar_invalid: marketfeed-adapter: wire: exchange calendar batch 1 carries no header
The refusal is an *engine.CalendarRefusal (errors.As) naming the root, and errors.Is reaches its cause’s sentinel, wire.ErrCalendarInvalid or ErrInvalidCalendar. The four reasons a CalendarRefusal carries (calendar_invalid, calendar_pin_drift, calendar_data_disagree and calendar_incomplete) are tabled under Conformance checks and admission. The response was read in full, so the connection stays usable: the stream is still aligned and the next request is served. A framing fault (an envelope carrying other records, a final total_count that differs from the days received) or a broken connection is a plain error that fails the run. The adapter’s request-scoped refusal is not a failure: it leaves the root unserved (below). Only a calendar that passed both rule sets reaches a strategy, so the strategy SDK’s protocol_error on an invalid push is a backstop.
Unserved roots. A root the run is served no calendar for is recorded with its cause, noted on stderr as algo: calendar: no exchange calendar for <root>: <cause>, and the run goes on: ctx.Calendar reports none for it. The causes, checked in this order:
| Cause | When |
|---|---|
adapter <name> does not advertise the EXCHANGE_CALENDAR schema | the adapter’s DataInitAck lists no such capability; every root of the run |
the stdio-dbn-v1 data wire cannot carry EXCHANGE_CALENDAR (it needs stdio-pb-v1) | the negotiated data wire is not pb (the CSV wire reads the same with its own name); every root of the run |
<code>: <message> | the adapter refused the root’s request with a request-scoped Error, for example for an irregular class or a window outside the coverage |
A symbol with no root (CalendarRoot returns "") has no calendar either, noted as algo: calendar: symbol "<symbol>" names no calendar root; it has no exchange calendar, and has no provenance entry. An unserved root is fatal only when it is pinned or the run is strict (below); the refusal then replaces the note.
Pins and strict mode. calendar-revisions maps a root to the revision the run requires. --strict-calendar requires every symbol the run reads to have a root, and every root a pin and a served calendar that matches it. The roots are taken in ascending order, and the first failure refuses the run with the reason calendar_pin_drift before any later root is fetched:
| Text | When |
|---|---|
engine: calendar_pin_drift: <root>: pinned revision <pin>, served revision <revision> | the served revision differs from the pin |
engine: calendar_pin_drift: <root>: pinned revision <pin>, but no calendar was served: <cause> | a pinned root is unserved |
engine: calendar_pin_drift: <root>: strict-calendar needs a pin for every root; the root is not pinned and serves revision <revision> | strict, and a served root has no pin |
engine: calendar_pin_drift: <root>: strict-calendar needs a pinned calendar for every root; none was served: <cause> | strict, and an unpinned root is unserved |
engine: calendar_pin_drift: strict-calendar: symbol "<symbol>" names no calendar root | strict, and a symbol has no root; checked before anything is fetched |
A pin refuses on drift whether or not the run is strict, as a snapshot pin does (SNAPSHOT_PIN_DRIFT), and a strict run’s refusal names the root and the revision served, so it tells the operator what to pin. A pin for a root the run does not read constrains nothing and is ignored, so one pin map can serve a multi-run config whose runs read fewer roots; strict mode catches a mistyped pin, because the real root is then unpinned. calendar-accept-unverified lists the roots whose unrecorded session eras precise-time admission accepts (D-I11-4). A2a checks the list, records it on the run for A2b’s admission and in the provenance (AcceptUnverified), and does nothing else with it; an entry need not name a root the run reads.
Provenance. The stage’s outcome is recorded in three artefacts, one entry per root the run reads, ascending by root: ResultHeader.Calendars in result.json; calendars, the first key of the --fills-json blotter and of the fills.json that report.fills writes; and run.calendars in the --lifecycle-json trace. Each is omitempty and filled only when the stage ran and the run read a root, so a run that asked for no calendar writes the same bytes as before (D-I11-9). An entry is a report.CalendarInfo, whose keys are its Go field names:
| Field | Meaning |
|---|---|
Root | the calendar root |
Revision, Law | the served revision and the law it digests (mf-cal-v1) |
Zone | the IANA name of the venue’s clock |
HolidayTable | Stem, Version, RangeFrom and RangeTo of the holiday table; absent when the venue has none |
CoverageFrom, CoverageTo | the coverage the header states |
ErasRecorded | the header’s eras_recorded; always present |
DayCancelRule | the assumed rule behind every DAY-cancel window |
FirstDate, LastDate, Days | the first and last trade dates served, and their number |
Pinned | the run pinned the root and the served revision matched |
AcceptUnverified | the root is listed in calendar-accept-unverified |
Unserved | why no calendar was served; every other field but Root and AcceptUnverified is then empty or false |
FirstDate, LastDate and Days describe what this run read over its window, so they depend on the run’s span: two runs with equal revisions can differ in result.json, as intended. The revision is the identity of the facts. The HTML report does not render the block.
The push. sendRunCalendars pushes the served calendars, one ExchangeCalendarBatch per root in ascending order of root (Envelope.exchange_calendar, 61, through StdioHost.SendExchangeCalendar; no reply), on the backtest, live and continuous paths, and only to a strategy that set needs_calendar. A stage that a run option turned on for a strategy that did not ask fetches and records but pushes nothing, so that strategy’s wire is byte-identical. A strategy that sets needs_calendar on the default data wire is recorded as unserved: its flag is known only after its Init, by which time auto negotiation has chosen the data wire, and for daily and minute bars marketfeed negotiates DBN, which cannot carry the schema. The run goes on, the cause the stdio-dbn-v1 data wire cannot carry EXCHANGE_CALENDAR (it needs stdio-pb-v1) is noted and recorded, and the strategy’s ctx.Calendar reports none; add --exchange-calendar or --data-protocol pb to have it served. One calendar travels in one envelope, so a calendar above wire.MaxFrame (16 MiB, roughly 50,000 days) fails the write.
Several roots in one run. Each distinct root is fetched once, one at a time and in ascending order of root, over the run’s one window, and the push and the provenance follow the same order. A run of the continuous fut:XCME:NQ and fut:XCME:ES beside fut:XCME:ES:M26 makes two requests, fut:XCME:ES and then fut:XCME:NQ, and records two entries.
Configuration. Five algo run flags (algo run flags) and the data keys of the same names (Config files and run matrices). The flags are single-run flags, refused with --config like the others:
| Flag | Config key | Meaning |
|---|---|---|
--exchange-calendar | exchange-calendar | fetch and record the calendars, with no pin |
--calendar-revisions root=rev[,root=rev...] | calendar-revisions, an object of root to revision | the pins |
--strict-calendar | strict-calendar | require a pinned, served calendar for every symbol |
--calendar-accept-unverified root[,root...] | calendar-accept-unverified, an array of roots | the roots whose unrecorded session eras admission accepts (A2b) |
--calendar-margin-quarters N | calendar-margin-quarters, a whole number | how many calendar quarters past the last bar the window reaches; 0 selects the default, 2, and at most 40 |
"data": {
"adapter": "bin/marketfeed-adapter",
"calendar-revisions": {"fut:XCME:ES": "<64 lowercase hex characters>"},
"strict-calendar": true,
"calendar-accept-unverified": ["fut:XCME:ES"]
}Each key may be set at any level (global, a strategies entry, a run). exchange-calendar and strict-calendar turn on at any level (a false does not turn off an outer true); calendar-revisions merges root by root, the more specific level winning a root; a more specific calendar-accept-unverified list replaces an outer one, and [] clears it; a non-zero calendar-margin-quarters wins (an explicit 0 does not override an outer value). A value of the wrong JSON type is refused naming its key. The flags’ parsers check the syntax: --calendar-revisions: bad calendar pin "fut:XCME:ES" (want root=revision[,root=revision...]), --calendar-revisions: calendar pin for fut:XCME:ES given twice, --calendar-accept-unverified: bad root list "a,,b": an empty entry and --calendar-accept-unverified: root fut:XCME:ES listed twice. The engine then checks the values as each run’s plan is built (so a bad value stops algo run --config, and --verify before it prints the plan) and again before any spawn, the first failure winning:
- any calendar option on a
--schema TRADESrun:engine: the exchange-calendar options apply to BARS runs, not schema TRADES; - the pins in ascending order of root, each root and then its revision (64 lowercase hex characters):
engine: calendar-revisions: "ES" is not a calendar root (want class:venue:ticker, for example fut:XCME:ES),engine: calendar-revisions: fut:XCME:ES: revision "ABC" is not 64 lowercase hex characters; - the
calendar-accept-unverifiedentries in order (a repeated entry is accepted):engine: calendar-accept-unverified: "fut:XCME:ES:M26" is not a calendar root (want class:venue:ticker, for example fut:XCME:ES); - the margin:
engine: calendar-margin-quarters must be between 0 and 40, got 41.
Status. A2a has landed: the fetch, the pins, strict-calendar, the provenance and the push, on the backtest, live and continuous paths. The stage is off by default, so the KBD masters and every golden are byte-identical; the spec tests run scripted fixture adapters, and algolang’s golden pin of the marketfeed adapter has not moved. Unit A2b has landed since (Conformance checks and admission, below): a run that fetches a calendar now checks its bars against it (calendar_data_disagree), and the admission helper refuses with calendar_incomplete. Unit A4 is the live check against IBKR’s contract rules. GLE-434 covers a pin check in the --verify preflight, which stops before the strategy and fetches no calendar, so a drifted pin shows only when the run starts; and a real-process test of the live push, which has an in-process test today.
Implementation anchors: engine/calendar.go; its call sites in engine/backtest.go (seriesPlan, runOne’s stage and push, the result header and the fill blotter), engine/liverun.go (the live push), engine/contseries.go (the continuous header), engine/lifecycle.go (run.calendars), engine/simrun.go (runInstance.calendars), engine/runplan.go, engine/runconfig/runconfig.go and cmd/algo/main.go; engine/report/artefacts.go (CalendarInfo); the tournament_calendar_*_spec_test.go files in engine, engine/runconfig and cmd/algo, engine/gle433_calendar_roots_test.go and engine/calendar_fetch_review_test.go.
Conformance checks and admission
Implemented (GLE-439, increment I11 unit A2b of the order-lifetime programme): the conformance checks and precise-time admission. engine/calendar_conformance.go binds each served calendar to the bars the run reads. Once the calendar stage (Engine side) has fetched a root’s calendar, every bar the run delivers for that root is checked against it, and the first bar that disagrees refuses the run with the reason calendar_data_disagree, before the strategy is sent the calendar or any bar. The same file holds the engine’s admission rule for precise-time features, which refuses with calendar_incomplete; no feature calls it yet. Neither has a switch of its own: a run that fetches a calendar is checked against it, and a run that fetches none checks nothing, so the defaults and every golden are unchanged.
When the checks run. Once per run, in runOne, the path that backtest, live and continuous series all take: after the calendar fetch and before InstrumentsResolved, the calendar push and the first warm-up bar. Every bar the run will deliver has been fetched by then, so one pass covers them all. For each series the checks read:
- its own warm-up batch, a continuous series’ included (it warms up since GLE-393, and its warm-up bars are built from the original contract bars, with both stamps and the trade-date block);
- for a series that is the source of an aligned view, the reach-back batch fetched for the view (aligned series are refused beside a continuous series, so a continuous series has none);
- its main bars, or for a continuous series its composite’s contract bars from the first live bar on, one batch per contract, because its loop bars carry only a close. The composite’s earlier bars are the warm-up’s originals, checked through the warm-up batch, and bars from before the warm-up that its wider read returned, which the strategy is not sent and which are not checked.
The three checks. They are the contract’s, with the refinements accepted at the unit’s freeze:
- A session bar’s trade date. A bar that carries a session block (
BarSession: a1dor1wbar under a trading-day session, or a custom session window) must carry the trade date the calendar gives itsts_close:Calendar.TradeDate(ts_close)is itstrade_date. Itsts_closeis its last constituent instant, inside its date’s span, so no adjustment is made. A weekly bar is dated by its week’s Monday (Session bars), so on a1wseries the check compares the Monday of the calendar’s trade date withtrade_date. Weeks run Monday to Sunday, as marketfeed folds them, so a Sunday trade date belongs to the Monday six days before it. - An intraday grid bar’s window. A bar of an intraday grid whose boundary is
eth,rthorsessionmust lie inside one trading window of that boundary on the trade date of itsts_open: some window[open, close]holds bothts_openandts_close - 1s, since a grid’s last bar in a window ends one second after the window’s close. The windows are the date’s ETH legs forethand its RTH window forrth. Forsessionthey are chosen per date, as the grid places that boundary: the date’s ETH legs when it has any, else its RTH window (not the header’strading_day_kind, which states only the root’s current hours). A date with no window of the boundary fails: a holiday, a date the holiday table does not know, an RTH-only date undereth.utc-daygrids are not checked, because the calendar states no UTC-day window and no UTC-day grid revision. - A grid series’ revision. An
eth,rthorsessiongrid series must have carried, in its grid requests, the revision that the header’sgrid_revisionsstates for its boundary (the adapter’s capability probe answers the same digest for every symbol of the root); a missing key fails. This is one check per series, not per bar. A registry or holiday-table change landing between the probe and the calendar fetch, both at the start of the run, fails it, correctly: run again.
The refusal. Check 3 runs first, over every eth, rth and session grid series of a served root; when one fails, the refusal names the failing series whose label sorts first (byte order), and no bar is read. Otherwise every bar is checked, check 1 on a bar with a session block, then check 2 on a bar of an eth, rth or session grid series unless check 1 failed, and the refusal names the offending bar whose ts_close is earliest across every series, batch and root; ties go to the series label that sorts first, then to plan order. The refusal is a property of the data, not of the order of --symbols: it names the earliest disagreement, where an operator starts looking. It is an *engine.CalendarRefusal naming the root, returned unwrapped, and errors.As reaches its cause, an *engine.CalendarDisagreement (Root, Series, Check, Boundary, TsOpen, TsClose, TradeDate, Answer). Stamps render as RFC 3339 in UTC, or unset:
| Check | Text |
|---|---|
| 1 | engine: calendar_data_disagree: <root>: series <label>: bar ts_open <open> ts_close <close> trade_date "<date>": check 1 (session trade date): <answer> |
| 2 | engine: calendar_data_disagree: <root>: series <label>: bar ts_open <open> ts_close <close>: check 2 (<boundary> grid window): <answer> |
| 3 | engine: calendar_data_disagree: <root>: series <label>: check 3 (<boundary> grid revision): <answer> |
The calendar’s answers:
| Check | <answer> | When |
|---|---|---|
| 1 | the calendar's trade date of ts_close is <date> | the bar is dated otherwise |
| 1 | the calendar's trade date of ts_close is <date>, whose week starts <Monday> | a weekly bar is dated otherwise |
| 2 | no <boundary> window of trade date <date> holds [<ts_open>, <ts_close - 1s>]: its <boundary> windows are [<open>, <close>], ... | the bar lies outside every window of its date |
| 2 | trade date <date> states no <boundary> window (closed <bool>, closed_known <bool>, rth_state "<state>") | the date has no window of the boundary |
| 1, 2 | no served trade date's span holds ts_close (check 1) or ts_open (check 2) | no served date owns the instant |
| 1, 2 | the bar's ts_close is unset, the bar's ts_open is unset | the stamp is missing; check 2 names ts_open first |
| 3 | the series' grid requests carried revision "<rev>"; the calendar's grid revision for <boundary> is "<rev>" | the revisions differ |
| 3 | the series' grid requests carried revision "<rev>"; the calendar states no grid revision for <boundary> | the header states none for the boundary |
The fetch window starts 14 days before the run’s earliest bar, so no served trade date's span holds means a bar on instants the calendar does not cover (before a bounded first era, for example), not a window drawn too narrow. An example, a daily ETH bar dated 2026-03-12 whose close falls half an hour into the span of 2026-03-13 (CME’s trade date opens at 17:00 Chicago time, 22:00Z once the clocks have changed in March):
engine: calendar_data_disagree: fut:XCME:ES: series fut:XCME:ES:M26@1d-eth: bar ts_open 2026-03-11T22:00:00Z ts_close 2026-03-12T22:30:00Z trade_date "2026-03-12": check 1 (session trade date): the calendar's trade date of ts_close is 2026-03-13
What is not checked. A series whose root has no served calendar: the adapter serves none (no capability, a data wire other than pb, or a refused request), the symbol names no root, or the run asked for no calendar. A pinned or strict run has already refused an unserved root with calendar_pin_drift. Bars that are neither session nor grid bars: fixed intervals such as 1m or 1h without a session, event bars, a 1d series without a session. They sit on a grid that ignores the calendar and carry no trade date, and the contract states no check for them. And, as above, utc-day grids.
Stored session bars derived under an older calendar. A session bar marketfeed derived under an older registry or holiday table can carry a trade date or a close that the served calendar disputes. Check 1 refuses it by design: the run would otherwise hand the strategy a trade date its own calendar contradicts. marketfeed’s doctor reports such a series as session-recipe-stale (its stored coverage carries a recipe the current registry and calendar do not produce); re-derive its session bars as the finding says and run again. The doctor’s session-stale, a session derive that has fallen behind its 1s parent, reports bars missing, not bars disputed.
What a passing run guarantees. A run that gets past the checks has its bars and its calendars bound: every checked session bar’s trade_date is the trade date ctx.Calendar gives its ts_close (for a weekly bar, the Monday of that date’s week), every checked grid bar lies inside a window of its boundary that Day returns for the trade date of its ts_open, and each grid series’ windows were placed from the same inputs as the calendar’s. A strategy can key on a bar’s trade date or on the calendar and get the same answer.
Live runs. This build’s live mode is the emulated harness over bars the engine fetched before the loop (The venue seam and the current live path), so the same pass covers it, and a disagreeing live run is refused before the venue is reached. A streaming live feed, which delivers bars as they arrive, must check each bar as it arrives, with the same three checks, and refuse at the first offending bar to arrive. The obligation was recorded at the unit’s freeze; nothing streams yet.
Cost. One pass per run, before the first bar is sent, over every bar, which on a 1s grid can be millions. A passing daily or grid bar allocates nothing: TradeDate is a binary search over the served days, check 2 copies a date’s record once per run of consecutive bars of a series on that date, check 3 reads the header once per root, and an answer is formatted only for the bar reported. The provenance is unchanged: a run that passes writes what it wrote before, with no count of the bars checked.
Precise-time admission. The engine’s admission rule for a feature that needs precise calendar instants (I12’s session timers, I13b’s calendar-stamped GTC close, a precise DAY order) is admitPreciseTime(feature, symbol, from, to) on the run’s calendar stage. Nothing calls it yet; those increments will, before they rely on a calendar instant. It returns nil when the calendar of the symbol’s root is complete over the instant window [from, to) (Calendar.Complete, above; decision D-I11-4), and otherwise refuses with the reason calendar_incomplete, naming the first incomplete date and the reason. The completeness reasons, the first failure winning:
| Reason | Refused when |
|---|---|
no-calendar | the run has no calendar for the root |
empty-window | to is not after from |
eras-unrecorded | the root’s session eras are not recorded (its current hours were back-applied to all its history) and the root is not listed in calendar-accept-unverified |
not-served | no trade date is served, or the window starts before the first served span or ends after the last |
outside-coverage | a trade date in the window lies outside the header’s coverage |
date-unknown | the holiday table does not know a trade date in the window |
early-close-unknown | a trade date in the window closes early at an unknown time |
rth-placeholder | a trade date in the window has a placeholder RTH |
rth-unknown | a trade date in the window has rth_state unknown |
calendar-accept-unverified waives only eras-unrecorded, and only for the roots it lists; every other reason refuses whatever the list says. A strategy’s needs_calendar does not consult admission (the flag says the strategy reads the calendar, not that the engine relies on its instants, and a strategy states its own policy through Calendar.Complete), and neither does --strict-calendar.
The refusal reads engine: calendar_incomplete: <feature>: <root> over [<from>, <to>): <detail> (<reason>), with Complete’s detail and reason, the window’s instants in RFC 3339 UTC, and symbol "<symbol>" in place of the root when the symbol names none. A no-calendar refusal adds why the run has no calendar for the root: the symbol names no calendar root, the run asked for no exchange calendar, the cause the stage recorded for an unserved root, or the run reads no symbol of <root>. It is an *engine.CalendarRefusal, returned unwrapped, and errors.As reaches Complete’s *algolang.CalendarIncompleteError (Root, TradeDate, Reason, Detail):
engine: calendar_incomplete: session timers: fut:XCME:ES over [2026-03-16T00:00:00Z, 2026-03-17T00:00:00Z): trade date 2026-03-16 is not known to the holiday table (date-unknown)
engine: calendar_incomplete: session timers: fut:XCME:RTY over [2026-03-09T00:00:00Z, 2026-03-12T00:00:00Z): no exchange calendar was served (no-calendar): bad_request: no calendar for fut:XCME:RTY
The calendar refusals. Every refusal of the calendar stage, the checks and admission is an *engine.CalendarRefusal (Reason, Root, Detail, Err), returned unwrapped, whose Error() is engine: <reason>: <detail>:
| Reason | Unit | Refused when | Unwrap returns |
|---|---|---|---|
calendar_invalid | A2a | a served calendar fails the wire validator, names another root or fails NewCalendar (Engine side) | the failing step’s error; errors.Is reaches wire.ErrCalendarInvalid or ErrInvalidCalendar |
calendar_pin_drift | A2a | a pin or --strict-calendar refuses the stage’s outcome (Engine side) | nil |
calendar_data_disagree | A2b | a bar or a grid series disagrees with its root’s calendar | the *engine.CalendarDisagreement |
calendar_incomplete | A2b | a precise-time feature’s window is not complete | the *algolang.CalendarIncompleteError |
Status. A2b has landed: the three checks on the backtest, live and continuous paths, and the admission helper, which nothing calls yet. No configuration, flag, proto field or artefact field changed, and no golden asks for a calendar, so the KBD masters and every golden are byte-identical. What remains of I11 is unit A4, the live comparison of the calendar with IBKR’s contract rules (D-I11-6); unit A5, the documentation of the calendar in Session bars, Intraday grids and the run provenance; marketfeed’s M3, its own documentation and an mf-doctor check that every root the adapter would serve reports its confidence; and the install of marketfeed PR 147, after which the installed marketfeed adapter serves the schema. A run against an adapter without it records every root unserved and checks nothing.
Implementation anchors: engine/calendar_conformance.go; its call site in engine/backtest.go (runOne, calendarCheckPlan and appendContLiveBars); engine/aa1_calendar_conformance_tournament_spec_test.go, engine/aa2_calendar_conformance_run_tournament_spec_test.go, engine/gle439_continuous_warmup_calendar_test.go and engine/calendar_conformance_review_test.go.