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.
- 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.
- ImportantOutlierAn outlier is an observation that is unusually distant or inconsistent under a specified comparison model.
Practical promise. You will build a causal Tick-Imbalance Bar, calculate its first boundary by hand, and know which conventions must travel with the result.
A candle can look convincing while hiding a membership problem. With Tick-Imbalance Bars, the decisive object is not the candle shape. It is a running sum of tick-rule signs and the threshold that was frozen before the bar opened.
This article implements the named method from Section 2.3.2.1 of Marcos López de Prado's Advances in Financial Machine Learning as one disclosed, session-local EMA variant. The distinction matters: the source provides the stopping idea, while seeds, exponential-moving-average coefficients, floors, session resets, and tail handling remain implementation choices.
The one question the algorithm answers
For each eligible trade, assign a causal sign:
- +1 after a price increase;
- −1 after a price decrease;
- the previous sign after an unchanged price.
Add those signs inside the open bar. Close at the first trade for which the absolute sum reaches the expected-magnitude threshold known at bar opening.
That is a sampling decision. It is not evidence that a buyer or seller was “informed,” and it does not predict the next return. An adaptive clock changes where observations fall; downstream usefulness needs separate testing.
Source method and teaching variant
The book defines the tick rule, cumulative imbalance, an expected imbalance based on expected bar length and expected sign, and the first-crossing rule. It carries the prior sign across bar boundaries and suggests exponential weighting for expectations.
This package makes the operational gaps explicit:
| Convention | Package decision |
|---|---|
| First sign | Configured as −1 or +1; +1 in the stored fixture |
| Flat trade | Carries the last sign |
| Bar boundary | Price and sign state carry within a session |
| Session boundary | Price, sign, expectations, and open-bar state reset |
| Expected length | One-step EMA of threshold-closed bar lengths |
| Expected sign | One-step EMA of each threshold-closed bar's mean sign |
| Open-bar threshold | Frozen until closure |
| Floor | Positive lower bound added by this package |
| Multiplier | Applied to expected magnitude before the floor |
| Partial tail | Emitted or dropped; never updates expectations |
Calling these “the default” would be misinformation. They are reproducible choices for this package.
Data comes before the formula
Tick signs depend on exact transaction order. The NYSE Daily TAQ specification documents a trade file ordered by symbol, time, and message sequence and includes sale condition, correction indicator, sequence number, trade ID, volume, and price. Its time field is SIP publication time, which is one reason a system must name the time semantic it uses.
FINRA's trade-reporting FAQ separately documents execution-time meaning and correction/cancellation workflows. A production pipeline must resolve those records before bar construction.
This package accepts one symbol and currency per call. It rejects duplicate IDs, missing or non-finite values, decreasing time, ambiguous equal timestamps, mixed instruments, and a session that reappears after another session starts. When timestamps tie, both records need a strictly increasing integer source sequence. The implementation never guesses a sort order.
The mathematics, with every state named
For price , the tick sign is
The first trade of a session uses the configured seed . Within a session, the previous price and sign continue across complete bars.
For trades in the open bar,
Before bar opens, freeze
Here is expected bar length, is expected mean tick sign, is the package floor, and is the package multiplier. The first-crossing boundary is
Equality closes. Since changes in integer steps, a threshold of 2.5 is crossed at 3. The crossing trade stays whole.
After a complete bar—and only then—the package updates:
Updating either expectation while the current bar is open would move the goalpost and define a different algorithm.
Work one boundary by hand
Start with expected length 8, expected mean sign 0.5, floor 3, multiplier 1, and session seed +1. The frozen threshold is
The prices are 100.00, 100.00, 100.10, 100.05, 100.10, and 100.15. Their signs are +1, +1, +1, −1, +1, +1. The cumulative imbalance is 1, 2, 3, 2, 3, 4. Trade six closes by equality.
With , expected length becomes
With , expected mean sign becomes
The next threshold is therefore 4.375. That value is frozen before the next trade arrives.
Three boundaries that change output
First, the threshold does not drift inside an open bar. Second, an integer statistic can exceed a fractional boundary, but the crossing trade is not split. Third, a session reset returns to disclosed seeds. Carrying adaptive state overnight would be a different variant, not a harmless optimization.
The same discipline applies to tails. A threshold close is complete and updates expectations. A session_end or stream_end close is partial and does not. With closePartial=false, the tail is deliberately omitted.
Walk the state, do not trust the final candle
Open the interactive Tick-Imbalance Bar lab. Its canonical scenario starts with a useful preview, not an empty chart. Use Back and Step to move across the six-trade boundary. Then compare:
- a flat-tick scenario, where the carried sign is visible;
- a session-reset scenario, where the same opening price receives the configured seed again;
- multiplier changes, which recompute every boundary from observation one.
The price path is labeled context only. The signed-tick path, frozen threshold, sign decision, and audit rows are the actual closing evidence.
Implementation and parity
The Python and TypeScript implementations use the same validation and state order:
- validate configuration and the full stream;
- reset explicit session state;
- freeze a bar threshold;
- classify one trade using only present and prior state;
- update OHLCV and imbalance;
- emit on
abs(imbalance) >= threshold; - update expectations from a complete bar;
- emit or drop an incomplete tail.
The shared fixture has 240 synthetic trades across two sessions and produces 12 stored bars. Focused tests cover first-sign seeding, flat carry, cross-bar carry, threshold freezing, equality, fractional overshoot, session reset, tails, ties, invalid numbers, duplicates, and configuration domains.
Passing those tests proves definition-level consistency. It does not prove statistical superiority, information discovery, or profitability.
Why there is no “Company X on Date Y” episode yet
A real historical explanation could make the value concrete—but only with evidence that actually observes the algorithm's inputs. Tick-Imbalance Bars require the exact transaction tape, event-time and sequence semantics, correction state, sale-condition policy, session calendar, and full parameter configuration.
End-of-day OHLCV from FMP or another provider cannot reconstruct transaction membership. Using it to claim that a historical episode “caused” particular Tick-Imbalance Bars would be false precision.
The package therefore marks a real order-flow episode as medium priority, evidence-gated:
- obtain a licensed, reproducible tick tape;
- archive the source and entitlement note privately;
- record cleaning, corrections, ordering, and configuration;
- reproduce bars in both languages;
- publish only data and derived evidence allowed by the license;
- label observed facts, package choices, derivations, and interpretation separately.
Until those conditions are met, deterministic synthetic data is the honest way to teach construction.
Practical checklist
- Name the methodology source and the exact variant.
- Store the first-sign, flat-trade, session, and tail conventions.
- Preserve stable sequence for timestamp ties.
- Resolve corrections and sale conditions upstream.
- Freeze expected state for the duration of an open bar.
- Keep the crossing trade whole.
- Recompute from the beginning after any parameter change.
- Treat adaptation as sampling state, not a forecast.
- Validate real episodes with transaction data, not EOD proxies.
Summary and next topic
You can now explain every TIB boundary with four objects: the signed trades, cumulative imbalance, frozen threshold, and package state. Continue with Volume-Imbalance Bars to see how weighting the same signs by share quantity changes the statistic and the data-quality risks.
Primary references
Full source roles, versions, and limitations are in REFERENCES.md. The canonical contract is the topic README.
Rendered from the canonical Mermaid sources linked by this article.
Causal construction flow
Purpose: separate preprocessed evidence, open-bar state, post-close learning, and session reset.
Takeaway: an open bar cannot learn from itself. Expected state changes only after a complete threshold closure, while a new session returns to disclosed seeds.
References3 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, Section 2.3.2.1
- Organization or authors: Marcos López de Prado
- Source type: Original published methodology for this named bar construction
- Publication or effective date: February 2018
- Version: First edition; Section 2.3.2.1, “Tick Imbalance Bars”
- URL or DOI: https://uat.store.wiley.com/en-us/advances-in-financial-machine-learning-p-9781119482109
- ISBN: 978-1-119-48210-9 (e-book); 978-1-119-48208-6 (hardcover)
- Accessed: 2026-07-22
- Jurisdiction: Market-agnostic methodology
- Supports: Tick-rule signs; cumulative tick imbalance; expected-length and expected-sign threshold; first-crossing definition; use of exponential weighting on prior state.
- Limitations: The cited section does not make this package's session seed/reset, one-step EMA coefficients, threshold floor, multiplier, or partial-tail rule universal. The publisher page confirms the edition; readers need the book section for the complete derivation.
R02 — 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: U.S. consolidated equity trades
- Supports: File ordering by symbol, time, and message sequence; time as SIP publication time; sale conditions; trade volume and price; correction indicator; sequence number; trade ID.
- Limitations: TAQ access and redistribution are governed by NYSE terms. Its published time is not automatically the execution-time field for every use case.
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: FINRA-reportable U.S. transactions
- Supports: Execution-time meaning and granularity; price and quantity reporting; correction, cancellation, reversal, and as-of workflows.
- Limitations: Reporting rules vary by facility and are not a Tick-Imbalance Bar methodology.
Evidence classification
| Claim type | Evidence |
|---|---|
| Named TIB method and defining stopping idea | R01 |
| Market-data fields and stable source ordering | R02 |
| Why correction and execution-time policies must be upstream and explicit | R03 |
| Seeds, EMA coefficients, floor, multiplier, reset, and tails | Package choices, tested in this repository |
| Worked arithmetic | Package derivation, stored in examples/worked-example.json |
| Predictive or profitable behavior | Not claimed and not evidenced |
Data and licensing note
The distributed dataset is deterministic synthetic data under the repository's project license. No FMP response, NYSE transaction tape, proprietary venue data, or investor record is redistributed. A real historical case remains out of scope until transaction-level evidence and its publication rights are verified.
Full dependency-light reference implementations in both supported languages.
/** Causal Tick-Imbalance Bars under this package's disclosed EMA convention. */
export type Trade = {
tradeId: string;
timestamp: string;
session: string;
symbol: string;
price: number;
volume: number;
currency: string;
sequence?: number;
};
export type Config = {
closePartial: boolean;
initialTickSign: -1 | 1;
initialExpectedTicks: number;
initialExpectedTickImbalance: number;
alphaTicks: number;
alphaTickImbalance: number;
thresholdFloor: number;
thresholdMultiplier: number;
};
export type Bar = {
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";
imbalance: number; threshold: number;
};
function timestamp(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 finite(value: unknown, name: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`${name} must be a finite number`);
}
return value;
}
function positive(value: unknown, name: string): number {
const parsed = finite(value, name);
if (parsed <= 0) throw new Error(`${name} must be positive`);
return parsed;
}
function rounded(value: number): number { return Number(value.toFixed(8)); }
function validate(trades: Trade[], config: Config): void {
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 (typeof config.closePartial !== "boolean") throw new Error("closePartial must be boolean");
if (config.initialTickSign !== -1 && config.initialTickSign !== 1) {
throw new Error("initialTickSign must be -1 or +1");
}
positive(config.initialExpectedTicks, "initialExpectedTicks");
const seed = finite(config.initialExpectedTickImbalance, "initialExpectedTickImbalance");
if (seed < -1 || seed > 1) throw new Error("initialExpectedTickImbalance must be in [-1, 1]");
for (const [name, value] of [
["alphaTicks", config.alphaTicks], ["alphaTickImbalance", config.alphaTickImbalance],
] as const) {
const alpha = finite(value, name);
if (alpha <= 0 || alpha > 1) throw new Error(`${name} must be in (0, 1]`);
}
positive(config.thresholdFloor, "thresholdFloor");
positive(config.thresholdMultiplier, "thresholdMultiplier");
const ids = new Set<string>();
const closedSessions = new Set<string>();
let activeSession: string | null = null;
let priorTime: number | null = null;
let priorSequence: number | null = null;
let symbol: string | null = null;
let currency: string | null = null;
for (const trade of trades) {
if (!trade || typeof trade !== "object") throw new Error("each trade must be an object");
for (const name of ["tradeId", "timestamp", "session", "symbol", "currency"] as const) {
if (typeof trade[name] !== "string" || !trade[name].trim()) {
throw new Error(`${name} must be a non-empty string`);
}
}
if (ids.has(trade.tradeId)) throw new Error("tradeId must be unique");
ids.add(trade.tradeId);
const currentTime = timestamp(trade.timestamp);
if (priorTime !== null && currentTime < priorTime) throw new Error("trades must be chronological");
if (priorTime !== null && currentTime === priorTime) {
if (!Number.isInteger(trade.sequence) || priorSequence === null || trade.sequence! <= priorSequence) {
throw new Error("equal timestamps require strictly increasing integer sequence values");
}
}
priorTime = currentTime;
priorSequence = Number.isInteger(trade.sequence) ? trade.sequence! : null;
if (positive(trade.price, "price") <= 0 || positive(trade.volume, "volume") <= 0) {
throw new Error("price and volume must be positive");
}
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 allowed per call");
}
if (activeSession === null) activeSession = trade.session;
else if (trade.session !== activeSession) {
closedSessions.add(activeSession);
if (closedSessions.has(trade.session)) {
throw new Error("a session may not reappear after another session begins");
}
activeSession = trade.session;
}
}
}
export function constructBars(trades: Trade[], config: Config): Bar[] {
validate(trades, config);
if (trades.length === 0) return [];
const result: Bar[] = [];
let current: Trade[] = [];
let activeSession: string | null = null;
let previousPrice: number | null = null;
let tickSign = config.initialTickSign;
let expectedTicks = config.initialExpectedTicks;
let expectedImbalance = config.initialExpectedTickImbalance;
let imbalance = 0;
let threshold = 0;
const beginBar = (): void => {
current = []; imbalance = 0;
threshold = Math.max(
config.thresholdFloor,
config.thresholdMultiplier * expectedTicks * Math.abs(expectedImbalance),
);
};
const resetSessionState = (): void => {
previousPrice = null;
tickSign = config.initialTickSign;
expectedTicks = config.initialExpectedTicks;
expectedImbalance = config.initialExpectedTickImbalance;
};
const emit = (reason: Bar["closeReason"]): void => {
if (!current.length) 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((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, closeReason: reason,
imbalance: rounded(imbalance), threshold: rounded(threshold),
});
if (reason === "threshold") {
const observedTicks = current.length;
const observedImbalance = imbalance / observedTicks;
expectedTicks = (1 - config.alphaTicks) * expectedTicks + config.alphaTicks * observedTicks;
expectedImbalance = (1 - config.alphaTickImbalance) * expectedImbalance
+ config.alphaTickImbalance * observedImbalance;
}
beginBar();
};
beginBar();
for (const trade of trades) {
if (activeSession !== null && trade.session !== activeSession) {
if (current.length && config.closePartial) emit("session_end");
else beginBar();
resetSessionState();
beginBar();
}
activeSession = trade.session;
if (previousPrice !== null) {
if (trade.price > previousPrice) tickSign = 1;
else if (trade.price < previousPrice) tickSign = -1;
}
previousPrice = trade.price;
current.push(trade);
imbalance += tickSign;
if (Math.abs(imbalance) >= threshold) emit("threshold");
}
if (current.length && config.closePartial) emit("stream_end");
return result;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.