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.
- PrimaryTickA 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.
- ImportantDuplicate RecordA duplicate record is an additional representation of the same logical event or state under a declared identity rule.
- 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.
- ImportantSequence GapA sequence gap is a discontinuity in an expected ordered identifier sequence for a specified data stream.
- ImportantTrading SessionA trading session is a venue-defined period during which specified market activities are scheduled to occur.
Practical promise. Build and audit bars that close after exactly
Neligible trade records, including deterministic ordering, session resets, and partial-tail handling.
Tick bars replace the clock with an event counter. A complete 12-tick bar contains 12 accepted trade records whether those records arrived in one second or several minutes. This can make record count uniform across bars, but it does not make volume, liquidity, volatility, or information uniform.
The central engineering question is simple to state and easy to implement incorrectly:
Has the current session-local bar accumulated exactly the configured number of eligible trade records?
The word eligible carries most of the production risk. A correction counted as a fresh execution, a duplicate record, an undocumented odd-lot filter, or an unstable tie order changes every later boundary.
What counts as one tick?
In this package, one tick means one pre-cleaned execution record accepted by an upstream policy. It does not mean one share or one price change. A 10-share record and a 10,000-share record each add one to the bar count if both are eligible.
Before calling the constructor, the pipeline must have:
- resolved cancels and corrections according to the source protocol;
- removed duplicates while retaining an audit trail;
- applied a documented sale-condition and odd-lot policy;
- ordered equal timestamps using the source sequence rule;
- assigned an explicit session key.
This separation is intentional. FINRA's reporting guidance treats execution time, quantity, and cancel/correction reporting as distinct concerns; a generic bar function cannot safely infer all of those rules (FINRA Trade Reporting FAQ). NYSE Daily TAQ likewise exposes timestamp and sequence information and documents historical timestamp-precision changes (NYSE Daily TAQ v4.3, sections 1.5.3–1.5.5).
The exact contract
The constructor receives one symbol and one currency in authoritative array order. Timestamps must be valid UTC strings and globally nondecreasing. Equal timestamps are allowed because the caller has already applied the source sequence rule; the constructor preserves their order and never sorts.
Two configuration fields control membership:
| Field | Rule |
|---|---|
targetTicks | Required positive integer; every complete bar contains exactly this many records. |
closePartial | Defaults to true; emit non-empty session and stream tails when true, otherwise discard them. |
Session keys are explicit. When a key changes, the current bar is emitted as session_end or discarded, then state resets before the first trade of the new session. A session may not disappear and later reappear in the same call.
The output includes OHLC, total quantity, exact reported notional, trade count, session, first/last trade IDs, and one of three closure reasons: threshold, session_end, or stream_end.
Mathematics: equality is the rule
For open bar B_j, count the admitted records:
Close when:
where N is the positive integer targetTicks.
There is no threshold overshoot in this algorithm. Every accepted record increases the state by exactly one, so a valid counter reaches N exactly. The record that makes T_j=N closes the current bar; the following record opens the next one.
After membership is fixed:
D is summed record by record. It is not reconstructed from a rounded average price.
Work the boundary by hand
Use targetTicks=3 and closePartial=true:
| Trade | Price | Shares | Count after admission | Result |
|---|---|---|---|---|
| E01 | 100.00 | 10 | 1 | Bar 0 remains open |
| E02 | 101.00 | 20 | 2 | Bar 0 remains open |
| E03 | 99.00 | 15 | 3 | Bar 0 closes |
| E04 | 100.00 | 25 | 1 | Bar 1 opens |
| E05 | 102.00 | 10 | 2 | Bar 1 remains open |
| E06 | 101.00 | 30 | 3 | Bar 1 closes |
| E07 | 103.00 | 5 | 1 | Partial bar emitted at stream end |
Bar 0 is O/H/L/C = 100/101/99/99, volume is 45, and dollar value is:
Bar 1 is 100/102/100/101, volume is 65, and dollar value is 6,550. E07 becomes a one-record stream_end bar with dollar value 515. With closePartial=false, E07 produces no output.
The diagram shows the key dependency: decide membership first, then calculate OHLCV.
Causal construction sequence
Only the current and earlier records affect a closure. Execution time validates ordering and labels bounds; arrival and availability times are separate dimensions that a live point-in-time system should preserve.
The static boundary visual isolates the three policies most likely to drift:
Notice that a session boundary never carries an unfinished count into the next session.
Implementation walkthrough
The Python implementation and TypeScript implementation follow the same sequence:
- Validate
targetTicksas a positive integer andclosePartialas a boolean. - Validate required fields, finite positive numbers, unique IDs, one instrument/currency, chronological time, and contiguous sessions.
- Preserve the supplied order; never sort tied timestamps.
- Flush or discard a session tail, then reset.
- Append one whole record and close only when count equals the target.
- Flush or discard the stream tail.
Both implementations are O(n) in the number of trades. The readable version stores at most one open bar, so working memory is O(N). A streaming production version can retain scalar accumulators while preserving first/last lineage.
Explore the 240-trade lab
Open the Tick Bars interactive playground. Its deterministic synthetic tape contains 240 trades across two sessions—enough to reveal repeated boundaries, activity changes, and session behavior without redistributing licensed transaction data.
Use it in this order:
- Inspect the complete 12-trade canonical bar shown on load.
- Step backward and forward across its equality boundary.
- Change
targetTicksand watch all membership recompute from trade one. - Filter to either session and verify the reset.
- Run to the end with a target that does not divide 120 and inspect
stream_end.
The NYSE specification states that historical Daily TAQ access is licensed and subject to product agreements, so a named historical trade tape is not included here without verified redistribution rights (NYSE Daily TAQ v4.3, preface and section 1). Synthetic data is the safer evidence choice for this mechanical lesson.
Tests that expose policy drift
The shared 240-trade fixture produces 20 complete bars at targetTicks=12: ten per session. Python and TypeScript must match the same stored expected output exactly.
Additional tests cover:
- the seven-record worked example with partial tails emitted and dropped;
- integer-only threshold validation;
NaN, infinity, nonpositive numbers, and reversed timestamps;- duplicates and mixed instruments;
- stable input order for equal timestamps;
- rejection of a session that reappears;
- session-end partial closure and reset.
These checks establish definition-level correctness. They do not establish that 12 is a useful market parameter or that any downstream strategy is profitable.
Common failure modes
| Failure | Why it changes results | Safe response |
|---|---|---|
| Count a correction as a new trade | Adds a false tick and shifts every later boundary | Resolve corrections before construction; version rebuilt bars. |
| Sort only by timestamp | Equal-time records may reorder | Preserve the source sequence key. |
| Filter odd lots silently | Changes count and duration | Document eligibility and apply it upstream. |
| Carry a partial bar overnight | Blends independent sessions | Flush or drop, then reset. |
Accept targetTicks=2.5 | Python/JavaScript coercion can disagree | Require a positive integer. |
| Interpret equal count as equal liquidity | Quantity and market conditions remain variable | Compare volume, notional, spread, and duration separately. |
Tick bars versus nearby clocks
| Method | Bar closes when | What is uniform | What remains variable |
|---|---|---|---|
| Time bars | Clock interval ends | Scheduled duration | Trade count and volume |
| Tick bars | Accepted record count reaches N | Trade count | Duration, volume, and notional |
| Volume bars | Reported quantity reaches/crosses target | Approximate accumulated quantity | Trade count and duration |
| Dollar bars | Reported notional reaches/crosses target | Approximate reported notional | Trade count and duration |
Choose the clock whose invariant matches the downstream question. Greater complexity does not imply greater validity.
Summary and next topic
A reliable tick bar is defined as much by its data contract as by its counter. Specify eligibility, preserve source order, close at exact equality, reset by session, and make the partial-tail decision visible. Keep lineage in the output so every candle can be reconstructed.
Continue with Volume Bars, where a crossing trade can overshoot the target and forces a different boundary discussion.
Primary references and source roles
Source versions, jurisdictions, supported claims, and limitations are recorded in REFERENCES.md. The canonical factual and algorithmic contract is README.md.
Rendered from the canonical Mermaid sources linked by this article.
Causal construction flow — Tick Bars
Purpose: show how one already-cleaned trade advances session-local count and why a later trade cannot change an earlier membership decision.
Takeaway: valid fixed-count state reaches the integer target exactly; the equality trade closes the current bar, and only the following trade can open the next one.
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 — Advances in Financial Machine Learning, Chapter 1 manuscript
- Organization or authors: Marcos López de Prado
- Source type: Author manuscript for a published book chapter
- Publication or effective date: 2018-01-18
- Version: First-edition chapter manuscript
- URL or DOI: https://ssrn.com/abstract=3104847
- Accessed: 2026-07-22
- Jurisdiction: Method is market-agnostic; examples concern financial market data.
- Supports: Comparison of standard time sampling with activity- and information-driven bar families, including tick bars.
- Limitations: The source motivates sampling families. This package's exact validation, session, tied-order, and partial-tail policies are explicit implementation choices, not universal defaults.
R02 — NYSE Daily TAQ Client Specifications
- Organization or authors: New York Stock Exchange, an Intercontinental Exchange company
- Source type: Official market-data product specification
- Publication or effective date: 2026-03-03
- Version: 4.3 (March 3, 2026)
- 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 SIP-derived Daily TAQ product.
- Supports: A Daily TAQ record represents one SIP event; trade data has price, volume, sale-condition, timestamp, and sequence fields; sequence conventions differ by tape; timestamp precision changed historically; historical access is licensed.
- Limitations: Daily TAQ is an end-of-day licensed product and does not prescribe this package's eligibility filter,
targetTicks, session partition, or partial-tail policy. The specification is cited for source semantics, not redistributed here.
R03 — Trade Reporting Frequently Asked Questions
- Organization or authors: Financial Industry Regulatory Authority
- Source type: Official reporting guidance
- Publication or effective date: Living guidance
- Version: Page accessed 2026-07-22
- URL or DOI: https://www.finra.org/filing-reporting/market-transparency-reporting/trade-reporting-faq
- Accessed: 2026-07-22
- Jurisdiction: United States FINRA-reportable trades
- Supports: Execution-time, quantity, odd-lot, cancellation, and correction concerns are source/reporting-policy questions that must be resolved before generic bar construction.
- Limitations: Rules vary by facility and security, and regulatory reporting guidance is not a tick-bar algorithm.
R04 — MIDAS: Market Information Data Analytics System
- Organization or authors: U.S. Securities and Exchange Commission
- Source type: Official regulator market-data overview
- Publication or effective date: 2024-06-14
- Version: Last reviewed 2024-06-28
- URL or DOI: https://www.sec.gov/securities-topics/market-structure-analytics/midas-market-information-data-analytics-system
- Accessed: 2026-07-22
- Jurisdiction: United States securities markets
- Supports: High-resolution order, quote, and execution data is operationally large and requires specialized processing.
- Limitations: MIDAS coverage does not prescribe trade eligibility, tick-bar thresholds, or predictive usefulness.
Evidence and data decision
The seven-record worked example and 240-record teaching tape are deterministic synthetic data distributed under the repository's project license. They model field shapes, equality, activity changes, session boundaries, and partial tails; they do not represent a real venue, security, investor, or historical episode.
A named historical tape was considered but not used. The official NYSE specification makes clear that Daily TAQ access is licensed and subject to product agreements. No transaction-level dataset with verified public redistribution rights was established during this review. Synthetic data is therefore the appropriate evidence for reproducible mechanics. This decision should be revisited only when both source authenticity and redistribution rights can be documented.
Full dependency-light reference implementations in both supported languages.
/**
* Readable reference implementation for fixed-count tick bars.
*
* Input order is authoritative. Corrections/cancels, duplicate removal,
* eligibility filtering, and equal-timestamp source sequencing happen upstream.
*/
export type Trade = {
tradeId: string;
timestamp: string;
session: string;
symbol: string;
price: number;
volume: number;
currency: string;
};
export type TickBarConfig = {
targetTicks: number;
closePartial?: boolean;
};
export type TickBar = {
barIndex: number;
session: string;
startTime: string;
endTime: string;
lastTradeTime: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
dollarValue: number;
tickCount: number;
firstTradeId: string;
lastTradeId: string;
closeReason: "threshold" | "session_end" | "stream_end";
};
const REQUIRED_FIELDS: (keyof Trade)[] = [
"tradeId", "timestamp", "session", "symbol", "price", "volume", "currency",
];
function timestampMs(value: unknown): number {
if (typeof value !== "string" || !value.endsWith("Z")) {
throw new Error("timestamp must be an ISO-8601 UTC string ending in Z");
}
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error("timestamp must be valid ISO-8601");
return parsed;
}
function positiveNumber(value: unknown, field: string): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new Error(`${field} must be a finite positive number`);
}
return value;
}
function validate(trades: Trade[], config: TickBarConfig): Required<TickBarConfig> {
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");
if (!Number.isInteger(config.targetTicks) || config.targetTicks <= 0) {
throw new Error("targetTicks must be a positive integer");
}
const closePartial = config.closePartial ?? true;
if (typeof closePartial !== "boolean") throw new Error("closePartial must be boolean");
const ids = new Set<string>();
const closedSessions = new Set<string>();
let previousTime = -Infinity;
let currentSession: string | null = null;
let symbol: string | null = null;
let currency: string | null = null;
for (const trade of trades) {
if (!trade || typeof trade !== "object" || REQUIRED_FIELDS.some(field => !(field in trade))) {
throw new Error("trade is missing a required field");
}
for (const field of ["tradeId", "session", "symbol", "currency"] as const) {
if (typeof trade[field] !== "string" || !trade[field].trim()) {
throw new Error(`${field} must be a non-empty string`);
}
}
const now = timestampMs(trade.timestamp);
if (now < previousTime) throw new Error("trades must be globally chronological");
previousTime = now;
if (ids.has(trade.tradeId)) {
throw new Error("tradeId must be unique after corrections and cancels");
}
ids.add(trade.tradeId);
positiveNumber(trade.price, "price");
positiveNumber(trade.volume, "volume");
if (symbol === null) {
symbol = trade.symbol;
currency = trade.currency;
} else if (trade.symbol !== symbol || trade.currency !== currency) {
throw new Error("one symbol and one currency are required per call");
}
if (currentSession === null) {
currentSession = trade.session;
} else if (trade.session !== currentSession) {
closedSessions.add(currentSession);
currentSession = trade.session;
if (closedSessions.has(currentSession)) {
throw new Error("each session must occupy one contiguous input block");
}
}
}
return { targetTicks: config.targetTicks, closePartial };
}
function rounded(value: number): number {
return Number(value.toFixed(8));
}
export function constructBars(trades: Trade[], config: TickBarConfig): TickBar[] {
const { targetTicks, closePartial } = validate(trades, config);
const result: TickBar[] = [];
let current: Trade[] = [];
let session: string | null = null;
const emit = (reason: TickBar["closeReason"]): void => {
if (current.length === 0) return;
const prices = current.map(trade => trade.price);
const volumes = current.map(trade => trade.volume);
result.push({
barIndex: result.length,
session: current[0].session,
startTime: current[0].timestamp,
endTime: current.at(-1)!.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((total, value) => total + value, 0)),
dollarValue: rounded(current.reduce(
(total, trade) => total + trade.price * trade.volume, 0,
)),
tickCount: current.length,
firstTradeId: current[0].tradeId,
lastTradeId: current.at(-1)!.tradeId,
closeReason: reason,
});
current = [];
};
for (const trade of trades) {
if (session !== null && trade.session !== session) {
if (closePartial) emit("session_end");
else current = [];
}
session = trade.session;
current.push(trade);
// Each accepted record increments the count by one, so equality is exact.
if (current.length === targetTicks) emit("threshold");
}
if (current.length > 0 && closePartial) emit("stream_end");
return result;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.