The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryBarA bar is an aggregate summary of eligible market events grouped by a declared sampling rule.
- PrerequisiteTickA tick is one atomic market-data event or update in an ordered event stream.
- PrerequisiteTradeA trade is an executed transaction in which a quantity of a financial instrument changes hands at agreed terms.
- ImportantEvent TimeEvent time is the timestamp assigned to when a market or business event occurred at its source.
- ImportantLate-Arriving DataLate-arriving data is a record that reaches a processing boundary after the expected time for its event, partition, or decision window.
- ImportantMeasurement UnitA measurement unit states the quantity scale represented by a numeric value, such as currency, shares, contracts, seconds, or percent.
- ImportantObservation TimeObservation time is the timestamp used to place a measured value on a time axis.
- ImportantOHLCOHLC is a four-field summary containing the open, high, low, and close prices for a declared interval and eligibility policy.
- ImportantOHLCVOHLCV is a bar record containing open, high, low, close, and trading-volume fields for one declared interval.
- ImportantPrecisionPrecision describes the fineness or number of digits with which a value or timestamp is represented or computed.
- ImportantSeries IdentitySeries identity is the complete key and semantic contract that states what a sequence of observations represents.
- ImportantTime ZoneA time zone is a governed set of civil-time offset rules used to interpret local dates and times.
- 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.
- ImportantTrading VolumeTrading volume is the aggregate eligible quantity executed over a stated instrument, source, and interval.
- MentionedMarket CloseMarket close is the venue-defined boundary or auction event that ends a specified trading session.
- MentionedMarket OpenMarket open is the venue-defined boundary or auction event that starts a specified trading session.
Turn an irregular, finalized trade tape into clock-aligned OHLCV bars without confusing the first trade with the interval start, crossing a session, or hiding empty time.
A candle is easy to draw and surprisingly easy to define badly. “One-minute bar” does not answer which timestamp field drives membership, where the minute grid starts, what happens exactly at 09:31:00, how auctions and halts are treated, or whether a late correction may rewrite history.
Time Bars solve one narrow problem: assign each eligible trade to a fixed session-relative interval, then aggregate the members. They do not clean the feed, discover sessions, or predict price.
The interval comes before OHLCV
For trade time t, session anchor a_s, and interval length Δ, define
The left bracket includes the start. The right parenthesis excludes the end. A trade exactly at the right boundary belongs to the next interval.
Only after membership is fixed do we calculate
D is the sum of each unrounded trade price times quantity. It is not close price times total volume.
A four-trade example that exposes three edge cases
Use a synthetic anchor of 14:30:00.000Z and a 60-second interval.
| Trade | Time | Price | Volume | Interval |
|---|---|---|---|---|
| W1 | 14:30:00.000 | 100 | 10 | 0 |
| W2 | 14:30:59.999 | 101 | 5 | 0 |
| W3 | 14:31:00.000 | 99 | 8 | 1 |
| W4 | 14:33:15.000 | 102 | 2 | 3 |
W1 and W2 form interval 0. Its OHLC is 100/101/100/101, volume is 15, and dollar value is 1,505. W3 begins interval 1 because it lies exactly on interval 0's excluded end. Interval 2 contains no trade, so this package omits it. W4 lands in interval 3 and reports one omitted interval before it.
The final bar is emitted from a finite input only when closePartial=true. Its stream_end label describes why buffered records were returned; it is not evidence that a live interval was complete.
Do not label the first trade as the interval start
Suppose interval 1 is [14:31:00, 14:32:00) but its first trade arrives at 14:31:01. These are two different facts:
startTime = 14:31:00.000Zis the clock boundary;firstTradeTime = 14:31:01.000Zis the first contributing event.
Collapsing them makes bars appear irregular and breaks joins to a canonical clock grid. The repaired output contract preserves both, along with intervalIndex, lastTradeTime, trade IDs, closure reason, and skipped-empty count.
The source timestamp must be named precisely
“Timestamp” is not a portable definition. A source may expose execution time, matching-engine publication time, SIP dissemination time, vendor receipt time, or a later corrected value. The current NYSE Daily TAQ specification, for example, describes its exchange Timestamp 1 as the matching-engine publication timestamp and documents nanosecond field precision and historical timestamp changes (NYSE Daily TAQ Client Specification v4.3, §§1.5.4–1.5.5).
This package's field is explicitly the selected execution-event time in UTC, at millisecond precision. A production adapter must prove that mapping. If it uses publication time instead, that is another valid dataset—but it is not silently the same dataset.
Exact timestamp ties need a source sequence. Otherwise “open” and “close” can depend on whichever file row happened to appear first.
Sessions are calendar data, not UTC dates
The reference takes an explicit UTC anchor for every session key. It never infers a session from midnight, and it never assumes that one fixed UTC clock works through daylight-saving changes or early closes.
NYSE currently publishes its Tape A core session as 9:30 a.m.–4:00 p.m. ET and identifies core open and closing auctions separately (NYSE Trading Information). NYSE's auction material also distinguishes open, close, and trading-halt auctions and notes that a listed security may open after 9:30 a.m. (NYSE Auctions). Those facts illustrate why a versioned venue calendar and phase policy belong upstream; they are not a universal market schedule.
Before building production bars, decide:
- whether auction prints join the continuous session or use separate phase keys;
- whether early, core, and late sessions share a grid;
- what calendar version produced each UTC anchor;
- whether a halt leaves empty wall-clock buckets or changes the clock.
This package keeps the wall clock running through a halt. With emptyBarPolicy=omit, halted intervals produce no fabricated OHLC. The first eligible resumption trade stays in its actual interval and records the skipped count.
Late reports and corrections need a version policy
Rule 613's timestamp and clock-synchronization requirements demonstrate that event identity and time provenance are foundational to market audit data (SEC Rule 613 overview). They do not select a bar timestamp or resolve a correction for us.
The reference constructor consumes a finalized batch. Corrections, cancels, and sale-condition filtering must already be resolved. A live pipeline must instead choose a policy such as:
- wait for an event-time watermark;
- publish provisional, versioned bars that can be retracted and replaced;
- quarantine observations later than a declared tolerance.
Inserting a late trade into historical bars without recording the revision creates look-ahead for anyone replaying the earlier view. Ignoring it creates a different error. The safe response is explicit versioning, not an undocumented compromise.
The causal construction loop
Takeaway: the clock selects membership; the trade updates OHLCV only after that decision.
Both reference implementations make one linear pass. They reject non-finite values, malformed UTC timestamps, duplicate IDs, decreasing event time, ambiguous ties, mixed symbols or currencies, missing anchors, and reappearing sessions.
Use the playground to inspect membership
Open the Time Bars playground. Its 240 observations are deterministic and synthetic. The initial state shows the complete canonical result, so the chart is informative before interaction. Then:
- choose both sessions or one session;
- change the interval length;
- reset, step, or play from the first trade;
- compare exact interval bounds with first and last trade times;
- inspect closure reasons and skipped empty intervals.
Changing the interval recomputes membership from observation one. It is an educational comparison, not parameter optimization.
Tests that matter
The shared 240-trade fixture produces 18 nonempty bars at 60 seconds. Python and TypeScript must reproduce the same stored objects. Additional tests target the failure modes a visually plausible chart can hide:
- a trade at
59.999stays in the current interval and one at60.000moves; - an interval start is not replaced by its first trade time;
- empty intervals are omitted but counted;
NaNand infinity are rejected;- equal timestamps require increasing source sequence;
- no output crosses a session key;
- dropping the finite tail is deliberate and testable.
Passing these tests proves conformance to this definition. It says nothing about whether a 60-second parameter is economically useful.
Why the package remains synthetic
A historical session could be valuable only if it includes licensed trade-level timestamps, conditions, corrections, a versioned venue calendar, and a reproducible field mapping. An end-of-day provider bar is already aggregated, so it cannot establish which trades formed it. Tick-data redistribution rights also vary by source.
For this revision, synthetic data are the stronger educational evidence: every record can be published, every boundary can be checked, and no vendor observation is presented as a universal market fact. A future historical case should supplement—not replace—this fixture after independent evidence and licensing review.
Practical checklist
- Name the selected timestamp and retain receive or availability time separately.
- Generate anchors from a versioned venue calendar.
- State auction, halt, sale-condition, correction, empty-bar, and tail policies.
- Preserve interval boundaries and actual trade-time lineage as separate fields.
- Require a source sequence for exact time ties.
- Use watermarks or revisions for live data.
- Treat OHLCV as lossy and construction correctness as distinct from trading usefulness.
Summary and next topic
A trustworthy Time Bar is a governed interval-membership decision, not merely a candle shape. You can now calculate its boundaries, implement its output contract, and identify the upstream policies that change the result.
Continue with Tick Bars, where elapsed time stops controlling membership and eligible trade count becomes the stopping rule.
Rendered from the canonical Mermaid sources linked by this article.
Causal construction flow — Time Bars
Purpose: show that session and interval membership are decided before a trade changes OHLCV.
Takeaway: the clock selects membership; accumulation happens afterward, and no interval crosses a session key.
References4 primary 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.
R01 — NYSE Daily TAQ Client Specification
- Organization or authors: New York Stock Exchange, an Intercontinental Exchange company
- Source type: Official market-data product specification
- Publication or effective date: 2026
- Version: 4.3
- URL or DOI: https://www.nyse.com/publicdocs/nyse/data/Daily_TAQ_Client_Spec_v4.3.pdf
- Accessed: 2026-07-22
- Jurisdiction: United States listed equities; CTA and UTP historical products
- Supports: Timestamp field format and precision; historical changes in timestamp coverage; the meaning of exchange
Timestamp 1; trade-field and condition provenance. - Limitations: NYSE product definitions do not prescribe this package's timestamp selection, interval grid, or eligibility policy. Access and redistribution remain subject to source terms.
R02 — Trading Information
- Organization or authors: New York Stock Exchange
- Source type: Official venue schedule and trading-calendar page
- Publication or effective date: Living page
- Version: Page accessed 2026-07-22
- URL or DOI: https://www.nyse.com/trade/trading-information
- Accessed: 2026-07-22
- Jurisdiction: NYSE Group venues and products
- Supports: Venue-specific early, core, and late trading sessions; currently published NYSE Tape A core hours; separately identified opening and closing auction times; calendar links.
- Limitations: Hours differ by venue, tape, product, date, holiday, and rule version. The package's UTC anchors are synthetic and are not derived from this page.
R03 — Auctions
- Organization or authors: New York Stock Exchange
- Source type: Official venue auction overview
- Publication or effective date: Living page
- Version: Page accessed 2026-07-22
- URL or DOI: https://www.nyse.com/trade/auctions
- Accessed: 2026-07-22
- Jurisdiction: NYSE Group equity venues
- Supports: Distinct opening, closing, and trading-halt auctions; venue-specific timelines; the fact that the NYSE DMM opening may occur after 9:30 a.m.
- Limitations: The overview directs readers to venue Rule 7.35 for governing details. It does not determine whether a downstream bar dataset should include an auction print in a continuous-session bar.
R04 — Rule 613: Consolidated Audit Trail
- Organization or authors: U.S. Securities and Exchange Commission
- Source type: Official regulator overview of adopted rule
- Publication or effective date: 2012 rule; page updated as a living overview
- Version: Page accessed 2026-07-22
- URL or DOI: https://www.sec.gov/about/divisions-offices/division-trading-markets/rule-613-consolidated-audit-trail
- Accessed: 2026-07-22
- Jurisdiction: United States National Market System securities
- Supports: Reportable event lineage, timestamp granularity, and synchronized business-clock requirements as evidence that event-time provenance is operationally material.
- Limitations: CAT requirements do not define this package's bar timestamp field, correction policy, session calendar, or aggregation convention.
Evidence and data decision
All distributed observations are deterministic synthetic records under the repository's project license. They imitate field shapes and irregular activity, not a venue, security, historical session, or investor. No FMP or other provider data are embedded.
A named historical example was considered and deferred. End-of-day provider bars cannot verify trade-to-interval membership because they are already aggregated. A defensible later case requires licensed trade-level records, source-field documentation, sale-condition and correction treatment, a versioned exchange calendar, and an archived calculation. Correct construction does not establish predictive or economic value.
Full dependency-light reference implementations in both supported languages.
/** Reference implementation of session-anchored, left-closed Time Bars. */
export type Trade = {
tradeId: string;
timestamp: string;
session: string;
symbol: string;
price: number;
volume: number;
currency: string;
sequence?: number;
};
export type Config = {
intervalSeconds: number;
sessionStarts: Record<string, string>;
closePartial?: boolean;
emptyBarPolicy?: "omit";
};
export type Bar = {
barIndex: number;
session: string;
intervalIndex: number;
startTime: string;
endTime: string;
firstTradeTime: string;
lastTradeTime: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
dollarValue: number;
tickCount: number;
firstTradeId: string;
lastTradeId: string;
emptyIntervalsBefore: number;
closeReason: "interval" | "session_end" | "stream_end";
};
const UTC_MILLISECOND = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
function milliseconds(value: unknown): number {
if (typeof value !== "string" || !UTC_MILLISECOND.test(value)) {
throw new Error("timestamp must use YYYY-MM-DDTHH:mm:ss.sssZ");
}
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error("timestamp must be a valid UTC instant");
return parsed;
}
function positive(value: unknown, name: string): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new Error(`${name} must be a finite positive number`);
}
return value;
}
function rounded(value: number): number {
return Number(value.toFixed(8));
}
function validate(trades: unknown, config: Config): asserts trades is Trade[] {
if (!Array.isArray(trades)) throw new Error("trades must be an array");
if (!config || typeof config !== "object") throw new Error("config must be an object");
positive(config.intervalSeconds, "intervalSeconds");
if (!config.sessionStarts || typeof config.sessionStarts !== "object" || !Object.keys(config.sessionStarts).length) {
throw new Error("sessionStarts must be a non-empty mapping");
}
for (const [session, anchor] of Object.entries(config.sessionStarts)) {
if (!session || typeof anchor !== "string") throw new Error("sessionStarts contains an invalid entry");
milliseconds(anchor);
}
if ((config.emptyBarPolicy ?? "omit") !== "omit") {
throw new Error("this reference implementation supports emptyBarPolicy='omit' only");
}
if (config.closePartial !== undefined && typeof config.closePartial !== "boolean") {
throw new Error("closePartial must be boolean");
}
const required = ["tradeId", "timestamp", "session", "symbol", "price", "volume", "currency"] as const;
const ids = new Set<string>();
const completedSessions = new Set<string>();
let activeSession: string | null = null;
let previousTime = -Infinity;
let previousSequence: number | undefined;
let symbol: string | undefined;
let currency: string | undefined;
for (const raw of trades) {
if (!raw || typeof raw !== "object") throw new Error("trade must be an object");
const trade = raw as Record<string, unknown>;
if (required.some((field) => !(field in trade))) throw new Error("trade is missing a required field");
for (const field of ["tradeId", "timestamp", "session", "symbol", "currency"] as const) {
if (typeof trade[field] !== "string" || !trade[field]) throw new Error(`${field} must be a non-empty string`);
}
const now = milliseconds(trade.timestamp);
if (now < previousTime) throw new Error("trades must be globally ordered by execution event time");
if (now === previousTime) {
if (!Number.isInteger(trade.sequence) || previousSequence === undefined || Number(trade.sequence) <= previousSequence) {
throw new Error("equal timestamps require a strictly increasing integer sequence");
}
} else if (trade.sequence !== undefined && !Number.isInteger(trade.sequence)) {
throw new Error("sequence must be an integer when supplied");
}
previousTime = now;
previousSequence = Number.isInteger(trade.sequence) ? Number(trade.sequence) : undefined;
const id = trade.tradeId as string;
if (ids.has(id)) throw new Error("tradeId must be unique after corrections and cancels are resolved");
ids.add(id);
positive(trade.price, "price");
positive(trade.volume, "volume");
const nextSession = trade.session as string;
if (activeSession !== nextSession) {
if (activeSession !== null) completedSessions.add(activeSession);
if (completedSessions.has(nextSession)) throw new Error("a session cannot reappear after a later session begins");
activeSession = nextSession;
}
if (!(activeSession in config.sessionStarts)) throw new Error("sessionStarts must contain every session");
if (now < milliseconds(config.sessionStarts[activeSession])) throw new Error("trade precedes its declared session anchor");
symbol ??= trade.symbol as string;
currency ??= trade.currency as string;
if (trade.symbol !== symbol || trade.currency !== currency) {
throw new Error("one constructBars call must contain one symbol and one currency");
}
}
}
export function constructBars(trades: Trade[], config: Config): Bar[] {
validate(trades, config);
if (!trades.length) return [];
const bars: Bar[] = [];
let current: Trade[] = [];
let session: string | null = null;
let bucket: number | null = null;
let previousEmittedSession: string | null = null;
let previousEmittedBucket: number | null = null;
const emit = (reason: Bar["closeReason"]): void => {
if (!current.length || session === null || bucket === null) return;
const intervalMs = config.intervalSeconds * 1000;
const startMs = milliseconds(config.sessionStarts[session]) + bucket * intervalMs;
const prices = current.map((trade) => trade.price);
const volumes = current.map((trade) => trade.volume);
const emptyIntervalsBefore = previousEmittedSession === session && previousEmittedBucket !== null
? Math.max(0, bucket - previousEmittedBucket - 1)
: bucket;
bars.push({
barIndex: bars.length,
session,
intervalIndex: bucket,
startTime: new Date(startMs).toISOString(),
endTime: new Date(startMs + intervalMs).toISOString(),
firstTradeTime: current[0].timestamp,
lastTradeTime: current.at(-1)!.timestamp,
open: rounded(prices[0]),
high: rounded(Math.max(...prices)),
low: rounded(Math.min(...prices)),
close: rounded(prices.at(-1)!),
volume: rounded(volumes.reduce((sum, value) => sum + value, 0)),
dollarValue: rounded(current.reduce((sum, trade) => sum + trade.price * trade.volume, 0)),
tickCount: current.length,
firstTradeId: current[0].tradeId,
lastTradeId: current.at(-1)!.tradeId,
emptyIntervalsBefore,
closeReason: reason,
});
previousEmittedSession = session;
previousEmittedBucket = bucket;
current = [];
};
for (const trade of trades) {
if (session !== null && trade.session !== session) {
if (current.length && config.closePartial !== false) emit("session_end");
else current = [];
session = trade.session;
bucket = null;
} else if (session === null) {
session = trade.session;
}
const offset = milliseconds(trade.timestamp) - milliseconds(config.sessionStarts[session]);
const nextBucket = Math.floor(offset / (config.intervalSeconds * 1000));
if (bucket !== null && nextBucket !== bucket) emit("interval");
bucket = nextBucket;
current.push(trade);
}
if (current.length && config.closePartial !== false) emit("stream_end");
return bars;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.