The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryMissing ValueA missing value indicates that the expected information is unavailable, unobserved, inapplicable, or withheld under a stated data contract.
- ImportantImputationImputation is the governed estimation or substitution of values where observations are missing.
- ImportantLate-Arriving DataLate-arriving data is a record that reaches a processing boundary after the expected time for its event, partition, or decision window.
- ImportantNo-Imputation Missing-Session PolicyA policy that blocks or pauses calculation instead of converting a missing required session into a zero observation.
- ImportantNull ValueA null value is an explicit data representation indicating that no ordinary value is present in a field.
- ImportantTrading CalendarA trading calendar is a versioned schedule of venue sessions, holidays, early closes, and exceptional closures.
- ImportantTrading DateA trading date is the venue-calendar date to which a trading session or market record is assigned.
- ImportantTrading SessionA trading session is a venue-defined period during which specified market activities are scheduled to occur.
- ImportantValid ZeroA valid zero is an observed or derived numeric value of exactly zero that is meaningful under the field's contract.
Practical promise. Given an empty time-bar slot, determine what the evidence actually supports: closed session, expected halt, feed outage, inactivity, bar-builder error, unknown, or conflict. You will also learn when the honest answer is “we cannot know yet.”
A blank candle is visually simple and operationally ambiguous. It might be absent because the market was closed. Trading might have been halted. The market might have been open but inactive. Messages might have been lost. Or the trades might be present while the bar builder failed.
Those causes lead to different actions. Removing an officially closed interval from an expected grid is sensible. Reconstructing a bar after a confirmed feed outage requires message recovery. Rebuilding a missing bar from independently captured trades addresses a construction defect. Forward-filling every blank interval does none of those things; it only makes uncertainty look numeric.
This article builds a conservative classifier that keeps observation, evidence, diagnosis, and repair separate. It is an operational data-quality method, not a trading signal.
One blank slot, five evidence layers
The first design decision is to stop calling all evidence “market data.” The classifier asks five different questions in order:
- Is a validated bar actually present?
- Does the versioned session calendar include the slot?
- Does authoritative market-status evidence show a halt?
- Was the source transport healthy and sequentially complete?
- Did a source independent of the missing bar path observe trades?
NYSE, for example, publishes session/calendar information and halt information as distinct products and pages (NYSE hours and calendars, NYSE trading halts). At the transport layer, Nasdaq's MoldUDP64 specification defines sequence numbers and retransmission requests after a receiver detects a sequence-number gap (official specification).
Those sources establish that the evidence layers are real. They do not mandate this package's ordering. The precedence ladder is a conservative engineering policy and is tested as such.
Why unknown and conflicting are real outputs
A common classifier stores booleans such as halt=false or
sequence_gap=false. That representation cannot distinguish “checked and
false” from “not checked,” “late,” or “two sources disagree.” The result looks
decisive even when the system never observed the decisive fact.
This package uses explicit states:
- a layer can be known and affirmative;
- known and negative;
unknown; orconflicting.
Unknown means the evidence needed at that layer is absent, late, or not observable. Conflicting means retained sources disagree at the same layer. The classifier never treats either state as false.
This distinction matters most for inactivity. If a bar is absent from feed A,
then “feed A reported zero trades” is not independent proof of no activity.
Feed A may simply be the path that failed. The inactive class requires healthy
transport and activity evidence independent of the missing bar path.
The input and output contracts
Each candidate slot supplies six categorical states, one independence flag, and immutable evidence identifiers. The exact schema is in the canonical README. In production, the record should additionally carry instrument, venue, event-time interval, receive and availability times, feed session, calendar version, correction version, and classifier version.
The primary output is an audit record:
| Output | Why it exists |
|---|---|
classification | The terminal diagnostic state. |
decisive_layer | The layer that stopped evaluation. |
reason | A plain-language explanation. |
evidence_ids | Lineage for every contributing observation. |
warnings | A caution that does not justify a stronger class. |
The eight possible classes are present, closed_session, expected_halt,
feed_outage, inactive, bar_builder_error, unknown, and
conflicting_evidence.
Notice what is not in the output: a synthetic price. Classification should happen before any repair policy.
The conservative decision ladder
Let , , , , and denote the consolidated bar, session, halt, transport, and activity states for slot . The classifier is an ordered function:
The notation is deliberately compact because the enum contracts carry the meaning. Evaluation proceeds as follows:
- A present bar returns
present. Unknown presence returnsunknown. - A session conflict returns
conflicting_evidence; unknown returnsunknown; closed returnsclosed_session. - A halt conflict or unknown remains explicit; an active halt returns
expected_halt. - A transport conflict remains explicit. A lost heartbeat or sequence gap is
positive evidence for
feed_outage. If neither is positive but continuity is incomplete, the result isunknown. - Activity conflict or unknown remains explicit. Dependent activity evidence
returns
unknownwith a warning. - Independent no-trade evidence returns
inactive. Independent trades with no bar returnbar_builder_error.
Takeaway: unknown at a stronger layer stops descent to a weaker story. A conflict is quarantined, not resolved by whichever source happens to arrive last.
Three cases that look similar on a chart
The static timeline compares evidence, not candle geometry:
Consider three missing in-session bars from the synthetic fixture.
Case 1: independent no-trade evidence
At 13:50 UTC, the session is open, no halt is active, heartbeat is healthy,
sequence continuity is intact, and an independent activity source reports no
trades. The classifier reaches the fifth layer and returns inactive.
Case 2: the same zero comes from the missing path
At 14:20 UTC, the visible states look similar, but the activity evidence is not
independent. The classifier returns unknown and warns against using a failed
path to prove inactivity.
Case 3: two calendars disagree
At 14:25 UTC, one retained calendar version says open and another says closed.
The result is conflicting_evidence at the session layer. Healthy transport or
zero activity cannot settle a disagreement about whether the slot belonged in
the expected universe.
A historical example: 78 gaps that should never exist
Historical examples add value only when fact, provider observation, derivation, and interpretation stay separate. This one needs no vendor price record.
Sourced fact
On 1 December 2018, ICE/NYSE announced that the named NYSE Group markets would be closed on Wednesday, 5 December 2018 for the National Day of Mourning for President George H. W. Bush (official ICE/NYSE notice).
Provider observation
None. This package did not query or license a provider's historical bars. It therefore makes no claim about whether a particular vendor emitted, omitted, or later corrected records.
Engineering assumption and derivation
To expose the classifier's value, compare the official closure with an
intentionally naive weekday template: 09:30–16:00 America/New_York in
five-minute half-open slots. On that December date the stored example expresses
the interval as [14:30Z, 21:00Z). The arithmetic is:
The naive template generates 78 candidate slots. The official closure produces zero expected slots.
Interpretation
A weekday-only grid would manufacture 78 apparent gaps for every instrument in
scope. Those absences are not evidence of 78 feed failures. They are one
calendar-model defect. With the closure notice attached, each naive candidate
terminates at the session layer as closed_session.
The complete case metadata lives in
historical-closure-case.json.
The example demonstrates calendar diagnosis, not provider accuracy, price
behavior, or economic impact.
Implementation: pure logic with auditable output
The Python and TypeScript implementations follow the same pattern:
- validate every enum, the literal boolean independence flag, and non-empty evidence identifiers;
- evaluate the ladder without mutating input;
- copy evidence lineage into the diagnosis; and
- return immediately at the first decisive layer.
The work is time for candidate slots and output space when the audit records are retained. The per-slot logic is constant space.
Both languages expose diagnoseGap or diagnose_gap for the audit record and a
convenience classifyGap or classify_gap for the label. An audited production
pipeline should keep the full result.
Use the lab to challenge the evidence
Open the interactive missing-bar evidence lab. It offers three scenarios:
- Canonical reaches the six confident outcomes;
- Unknowns shows where missing or dependent evidence stops evaluation; and
- Conflicts shows same-layer disagreements that require reconciliation.
Use Step and Back to move one decision at a time. Play and Pause call the same transition used by Step. Reset restores the same first case. The active ladder layer, exact field values, reason, evidence identifiers, and accumulated audit log remain synchronized. Reduced-motion mode converts Play to one deterministic step.
Tests that matter
The shared fixture contains 15 meaningful decision states rather than a long run of visually identical normal bars. Python and TypeScript tests verify:
- all eight terminal classes;
- full cross-language fixture parity;
- evidence lineage and decisive-layer preservation;
- a positive feed fault when activity is unknown;
- stronger unknown evidence blocking a weaker no-trade claim;
- dependent zero activity remaining unknown;
- invalid enum, type, missing-field, and lineage failures; and
- the exact 78-slot historical derivation with
provider_observation: null.
Passing those tests proves that the implementation matches this package's definition. It does not prove how often each cause occurs in live markets or authorize a repair.
Production failure modes
- Boolean unknowns: storing absent halt metadata as
falsecreates false confidence. - Calendar drift: recomputing old slots with today's calendar silently changes history.
- Dependent inactivity evidence: zero messages on the failed path are not proof of zero trades.
- Heartbeat overclaim: a live connection does not prove that every payload arrived; sequence continuity remains separate.
- Halt timing: late or corrected market-status records require a new point-in-time diagnosis version.
- Venue consolidation: a consolidated bar can hide a single-venue failure.
- Repair inside diagnosis: filling a bar before preserving the cause destroys auditability.
Keep a repair decision in a separate field and workflow. closed_session might
remove a slot from the expected grid. feed_outage might trigger replay and
reconciliation. bar_builder_error might trigger deterministic reconstruction.
unknown and conflicting_evidence should block automatic repair unless an
explicit risk policy says otherwise.
Summary and next topic
A missing bar is a question. The evidence ladder answers only as strongly as the retained session, market-status, transport, and independent activity records allow. It can return a useful cause, but it can also preserve unknown or conflicting evidence without cosmetic certainty.
Continue with Feed-Latency Monitor to distinguish data that is absent from data that has merely not arrived yet.
The primary source roles, dates, and limitations are recorded in
REFERENCES.md. The canonical README owns
the contracts, and the implementations and shared fixture make them executable.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
NYSE-HOURS — Holidays and trading hours
- Organization or authors: New York Stock Exchange
- Source type: Official exchange calendar and session page
- Publication or effective date: Current page; future calendars are versioned on the site
- Version: Page retrieved 2026-07-22
- URL or DOI: https://www.nyse.com/trade/hours-calendars
- Accessed: 2026-07-22
- Jurisdiction: United States
- Supports: Session membership is venue-, product-, date-, and hours-scope-dependent; current NYSE Tape A core-session information is published separately from halt status.
- Limitations: This current page is not used as proof of a 2018 regular-session template. Production systems need the calendar version applicable at the historical knowledge cutoff.
NYSE-HALTS — Trading halts
- Organization or authors: New York Stock Exchange
- Source type: Official exchange market-status page
- Publication or effective date: Continuously updated
- Version: Page retrieved 2026-07-22
- URL or DOI: https://www.nyse.com/trade/trading-halts
- Accessed: 2026-07-22
- Jurisdiction: United States
- Supports: Halt state is an operational evidence layer distinct from session membership and trade activity.
- Limitations: The page is not a redistributable complete historical halt archive. Retention, late updates, and product scope require production controls.
NASDAQ-MOLDUDP64 — MoldUDP64 Protocol Specification
- Organization or authors: Nasdaq
- Source type: Official market-data transport protocol specification
- Publication or effective date: Document current at retrieval
- Version: 1.00
- URL or DOI: https://nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/moldudp64.pdf
- Accessed: 2026-07-22
- Jurisdiction: United States / protocol-specific
- Supports: Downstream packets carry sequence numbers; a receiver can detect a sequence-number gap and request retransmission.
- Limitations: The package uses protocol sequence gaps only as an example of positive transport evidence. Recovery procedures and product applicability depend on the subscribed feed.
NASDAQ-ITCH — Nasdaq TotalView-ITCH 5.0
- Organization or authors: Nasdaq
- Source type: Official market-data message specification
- Publication or effective date: Document current at retrieval
- Version: 5.0
- URL or DOI: https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/NQTVITCHSpecification.pdf
- Accessed: 2026-07-22
- Jurisdiction: United States / Nasdaq TotalView-ITCH
- Supports: Market-data records include event timestamps and defined trade-message semantics, illustrating why activity evidence has a documented protocol meaning.
- Limitations: ITCH tracking numbers are not substituted for MoldUDP64 transport sequence numbers. A subscriber's capture completeness remains an operational fact to establish.
ICE-NYSE-BUSH-CLOSURE — NYSE Group closure for National Day of Mourning
- Organization or authors: Intercontinental Exchange / New York Stock Exchange
- Source type: Official exchange press release
- Publication or effective date: Published 2018-12-01; closure on 2018-12-05
- Version: Archived official notice retrieved 2026-07-22
- URL or DOI: https://ir.theice.com/press/news-details/2018/New-York-Stock-Exchange-to-Honor-President-George-H-W-Bush/default.aspx
- Accessed: 2026-07-22
- Jurisdiction: United States
- Supports: The named NYSE Group markets were closed on Wednesday, 5 December 2018 for the National Day of Mourning.
- Limitations: It does not provide security-level bars, prove a vendor's coverage, or establish this package's intentionally naive regular-session template. Those distinctions remain explicit in the historical case.
Package conventions — engineering choices, not external facts
- Unknown at a stronger evidence layer stops descent to weaker explanations.
- Same-layer disagreement becomes
conflicting_evidencerather than a tie-break. - A lost heartbeat or sequence gap is positive transport-fault evidence.
inactiveandbar_builder_errorrequire activity evidence independent of the missing bar path.- These are conservative reference-implementation policies. A production owner may choose a different auditable policy but must not present it as an exchange rule.
Full dependency-light reference implementations in both supported languages.
export type GapClass =
| "present"
| "closed_session"
| "expected_halt"
| "feed_outage"
| "inactive"
| "bar_builder_error"
| "unknown"
| "conflicting_evidence";
export type BarState = "present" | "absent" | "unknown";
export type SessionStatus = "open" | "closed" | "unknown" | "conflicting";
export type HaltStatus = "active" | "inactive" | "unknown" | "conflicting";
export type HeartbeatStatus = "healthy" | "lost" | "unknown" | "conflicting";
export type SequenceStatus = "continuous" | "gap" | "unknown" | "conflicting";
export type ActivityStatus = "trades" | "no_trades" | "unknown" | "conflicting";
export interface GapRow {
timestamp?: string;
bar_state: BarState;
session_status: SessionStatus;
halt_status: HaltStatus;
heartbeat_status: HeartbeatStatus;
sequence_status: SequenceStatus;
activity_status: ActivityStatus;
activity_independent: boolean;
evidence_ids: string[];
[key: string]: unknown;
}
export interface GapDiagnosis {
classification: GapClass;
decisive_layer: string;
reason: string;
evidence_ids: string[];
warnings: string[];
}
const allowed: Record<string, Set<string>> = {
bar_state: new Set(["present", "absent", "unknown"]),
session_status: new Set(["open", "closed", "unknown", "conflicting"]),
halt_status: new Set(["active", "inactive", "unknown", "conflicting"]),
heartbeat_status: new Set(["healthy", "lost", "unknown", "conflicting"]),
sequence_status: new Set(["continuous", "gap", "unknown", "conflicting"]),
activity_status: new Set(["trades", "no_trades", "unknown", "conflicting"]),
};
function validate(row: GapRow): void {
for (const [field, values] of Object.entries(allowed)) {
if (!values.has(row[field] as string)) {
throw new Error(`${field} has an invalid value`);
}
}
if (typeof row.activity_independent !== "boolean") {
throw new Error("activity_independent must be a boolean");
}
if (
!Array.isArray(row.evidence_ids) ||
row.evidence_ids.length === 0 ||
row.evidence_ids.some((value) => typeof value !== "string" || value.trim() === "")
) {
throw new Error("evidence_ids must be a non-empty list of strings");
}
}
function result(
classification: GapClass,
decisive_layer: string,
reason: string,
row: GapRow,
warnings: string[] = [],
): GapDiagnosis {
return { classification, decisive_layer, reason, evidence_ids: [...row.evidence_ids], warnings };
}
export function diagnoseGap(row: GapRow): GapDiagnosis {
validate(row);
if (row.bar_state === "present") return result("present", "bar", "A validated bar is materialized.", row);
if (row.bar_state === "unknown") return result("unknown", "bar", "Bar presence has not been established.", row);
if (row.session_status === "conflicting") return result("conflicting_evidence", "session", "Calendar sources disagree about whether the slot belongs to a session.", row);
if (row.session_status === "unknown") return result("unknown", "session", "Session membership is unavailable.", row);
if (row.session_status === "closed") return result("closed_session", "session", "Authoritative session evidence excludes this slot.", row);
if (row.halt_status === "conflicting") return result("conflicting_evidence", "market_state", "Market-status sources disagree about an active halt.", row);
if (row.halt_status === "unknown") return result("unknown", "market_state", "Halt state is not known.", row);
if (row.halt_status === "active") return result("expected_halt", "market_state", "An authoritative halt interval covers the missing slot.", row);
if (row.heartbeat_status === "conflicting" || row.sequence_status === "conflicting") return result("conflicting_evidence", "transport", "Transport-health evidence conflicts.", row);
if (row.heartbeat_status === "lost" || row.sequence_status === "gap") return result("feed_outage", "transport", "A lost heartbeat or sequenced-message gap is positive feed-failure evidence.", row);
if (row.heartbeat_status === "unknown" || row.sequence_status === "unknown") return result("unknown", "transport", "Feed continuity is not fully observed.", row);
if (row.activity_status === "conflicting") return result("conflicting_evidence", "activity", "Activity sources disagree about whether trades occurred.", row);
if (row.activity_status === "unknown") return result("unknown", "activity", "Trade activity is not known.", row);
if (!row.activity_independent) return result("unknown", "activity", "Activity evidence is not independent of the missing bar path.", row, ["Do not use the failed path to prove that no trades occurred."]);
if (row.activity_status === "no_trades") return result("inactive", "activity", "Independent activity evidence reports no trades in a healthy, open interval.", row);
return result("bar_builder_error", "construction", "Independent trade evidence exists, but the expected bar is absent.", row);
}
export function classifyGap(row: GapRow): GapClass {
return diagnoseGap(row).classification;
}
export function classifyRows(rows: GapRow[]) {
return rows.map((row) => ({ ...row, ...diagnoseGap(row) }));
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.