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.
- PrerequisiteTradeA trade is an executed transaction in which a quantity of a financial instrument changes hands at agreed terms.
- ImportantLast Traded PriceLast traded price is the price of the most recent trade that qualifies under a stated source and eligibility policy.
- ImportantMeasurement UnitA measurement unit states the quantity scale represented by a numeric value, such as currency, shares, contracts, seconds, or percent.
- ImportantPrecisionPrecision describes the fineness or number of digits with which a value or timestamp is represented or computed.
- ImportantTrading VolumeTrading volume is the aggregate eligible quantity executed over a stated instrument, source, and interval.
Practical promise. You will build a bar that closes on exact reported notional, explain which trade crossed the boundary, and reject ambiguous currency, precision, and ordering before they corrupt membership.
Time bars answer “what traded during this interval?” Dollar Bars answer a different question: when did the sum of each eligible trade's price times quantity first reach the target? That is useful when the amount of reported notional should control sampling rather than the clock.
The word “Dollar” is conventional. This package works with one declared currency at a time. It does not silently convert EUR, SAR, or any other currency into USD.
Start with the unit, not the candle
For one cash-equity trade, suppose price is 100 USD per share and eligible quantity is 40 shares. Reported notional is:
That dimensional cancellation matters. A futures price may need a contract multiplier; a bond price may be quoted per 100 of face value; a crypto feed may report base or quote quantity. Multiplying ungoverned numbers can produce a plausible but meaningless threshold.
The canonical input therefore has one symbol, one currency, a normalized price unit, a normalized quantity unit, and explicit decimal scales. Cross-asset normalization happens upstream.
The exact stopping rule
For trade , let be normalized reported price and be eligible quantity. Its contribution is . For open bar :
The bar closes at the first trade satisfying:
where is targetDollar.
| Symbol | Meaning | Unit |
|---|---|---|
| reported normalized price | currency per quantity unit | |
eligible quantity (volume) | quantity unit | |
| open-bar cumulative reported notional | currency | |
| target reported notional | currency |
The equality is inclusive. The crossing trade is indivisible and stays wholly in the bar. Consequently, complete bars can overshoot the target.
Calculate one bar by hand
Use a 10,000 USD target:
| Trade | Price | Quantity | Contribution | Cumulative | State |
|---|---|---|---|---|---|
| W1 | 100 USD/share | 40 shares | 4,000 USD | 4,000 USD | Open |
| W2 | 90 USD/share | 70 shares | 6,300 USD | 10,300 USD | Close |
The output bar has open 100, high 100, low 90, close 90, volume 110 shares, dollarValue=10300, and excessDollar=300. Splitting W2 to force exactly 10,000 USD would manufacture a trade fragment and is a different algorithm.
An exact-boundary case is just as explicit: 40 shares at 100 USD plus 60 shares at 100 USD equals 10,000 USD, so the second trade closes the bar with zero excess.
Construction is a causal state machine
The algorithm never needs a future trade. It validates the tape, processes one record, updates exact cumulative state, and decides. At a session boundary it either emits the below-target tail or drops it, according to closePartial, then starts fresh.
Four policies that change membership
1. Eligibility and corrections
The input must already represent the chosen tape truth. Corrections, cancels, duplicates, late reports, and sale-condition policy belong upstream. FINRA's current trade-reporting guidance treats execution time, price and quantity precision, and correction/cancellation handling as distinct concerns. That is evidence that those fields need governance; it is not a Dollar Bar recipe.
2. Order and tied timestamps
Records must be globally chronological. If two trades have the same event timestamp, both need source-order sequence values and those values must strictly increase. Sorting ties by an arbitrary local row number can move a threshold-crossing trade between bars.
3. Decimal precision
The reference implementations do not compare binary floating-point sums. Let be priceDecimals and be quantityDecimals; the implementations transform price and quantity into integers and accumulate the product at scale . A value with extra decimals is rejected rather than rounded. Accumulations above the cross-language safe-integer bound are also rejected.
4. Currency
Every trade must equal config.currency. The exact USD/EUR failure scenario stops before bar construction. If a research design requires FX, it must first define the rate source, currency pair direction, point-in-time timestamp, availability rule, and rounding policy. That transformation produces a new governed input tape.
Why sum every trade's product?
Do not replace the definition with close price × total volume. For the worked example, that shortcut gives and incorrectly leaves the bar open. Even average price × total volume is safe only if it is the exact quantity-weighted average carried at sufficient precision—which is simply another route back to the trade-level sum.
Explore three scenarios
Open the Dollar Bars guided playground. It begins with an informative preview and provides:
- Canonical tape: 240 synthetic trades across two sessions;
- Exact boundary: two trades that land exactly on 10,000 USD; and
- Mixed currency: a USD trade followed by EUR, visibly rejected before aggregation.
Choose a scenario, use Back or Step, observe cumulative notional and the decision boundary, read the explanation, then compare another target. Play/Pause uses the same step transition as manual controls, Reset is deterministic, and the audit rows expose contribution and cumulative state.
Implementation contract
The Python implementation and TypeScript implementation share the same behavior:
- validate configuration and record fields;
- enforce unique IDs, one symbol, one currency, chronological order, deterministic ties, and contiguous sessions;
- convert price and quantity to exact scaled integers;
- process trades whole and in order;
- emit threshold, session-end, or stream-end bars with source lineage; and
- emit signed
excessDollarso complete and partial bars are distinguishable.
The pass is . A production streaming implementation can keep scalar OHLCV/notional state, but must retain enough first/last lineage to audit membership.
What the tests prove
The shared synthetic fixture has 240 trades, two sessions, varied price direction, varied quantity, and larger prints. At 140,000 USD it produces 21 bars: 19 threshold closes, one session-end partial, and one stream-end partial.
Both languages reproduce the same stored outputs. Additional tests cover:
- inclusive equality and whole-trade overshoot;
- exact mixed-currency rejection;
- non-finite and over-scale values;
- duplicate IDs and multiple symbols;
- reversed time and ambiguous equal timestamps;
- session reappearance;
- tail emission versus tail suppression; and
- a single trade larger than the target.
These tests prove definition-level consistency. They do not prove that 140,000 USD is an optimal target, that the synthetic tape matches a real venue, or that a downstream signal is profitable.
Historical examples need transaction evidence
A real dated example could make the boundary memorable, but it must be more than a chart anecdote. A defensible case must make the eligible transaction records reproducible with lawful redistribution: instrument identity, event time, tie sequence, price, quantity, currency, sale-condition/correction state, session, target, scales, and first/last member lineage.
Daily OHLCV and vendor EOD aggregates cannot show which prints entered a Dollar Bar. Financial Modeling Prep data may help identify a date worth investigating, but it is not transaction-membership evidence. This article therefore uses a transparent synthetic example and labels the historical case as a medium-priority research opportunity, not a fact.
Failure modes to keep visible
- mixed quote currencies or hidden FX conversion;
- wrong contract multiplier or quantity unit;
- rounded inputs whose precision policy is unknown;
- reordered timestamp ties;
- session leakage;
- silent dropping of partial tails;
- revised tapes without an as-of version; and
- interpreting bar completion as a buy or sell signal.
Dollar Bars normalize sampling by reported notional under a contract. They do not automatically normalize risk, liquidity, or economic exposure.
Summary and next topic
You can now calculate, implement, and audit a Dollar Bar without hiding the decisions that determine membership. The essential disciplines are simple: sum each trade's own price-times-quantity, compare exactly, keep the crossing trade whole, reset sessions, and reject unit ambiguity.
Continue with Tick-Imbalance Bars to see how a signed, adaptive stopping statistic differs from this fixed reported-notional threshold.
Primary references
- NYSE Daily TAQ Client Specification, version 4.3
- FINRA Trade Reporting Frequently Asked Questions
- López de Prado, Advances in Financial Machine Learning, Chapter 1 manuscript
Source roles, access dates, and limitations are recorded in REFERENCES.md.
Rendered from the canonical Mermaid sources linked by this article.
Causal construction flow — Dollar Bars
Purpose: show the complete dollar-only state path, including the session-tail choice and the inclusive threshold.
Takeaway: input normalization happens before construction; each whole eligible trade changes exact cumulative state once, and equality closes the bar.
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, Chapter 1 manuscript
- Organization or authors: Marcos López de Prado
- Source type: Original author manuscript for the Wiley 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: Methodological; market-agnostic
- Supports: Classification of standard bar-sampling approaches and the motivation for sampling by dollar value rather than clock time alone.
- Limitations: The source does not make this package's fixed-point scale, one-currency rejection, tie sequencing, session reset, tail emission, or whole-trade overshoot conventions universal.
R02 — NYSE Daily TAQ Client Specification v4.3
- 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
- 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
- Supports: The Daily Trades file's distinct time, symbol, sequence, trade-volume, trade-price, sale-condition, and related fields; trade volume is described as shares and trade price as share price.
- Limitations: Product access and redistribution are controlled by NYSE terms. The specification does not prescribe a Dollar Bar algorithm or this package's eligibility policy.
R03 — Trade Reporting Frequently Asked Questions
- Organization or authors: Financial Industry Regulatory Authority (FINRA)
- 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 meaning; price and fractional-share precision; quantity reporting; and correction, cancellation, and reversal handling as material trade-record concerns.
- Limitations: Reporting rules vary by facility and security. FINRA guidance is not a Dollar Bar methodology and does not endorse this package's threshold.
Evidence classification
- Source facts: the cited documents describe a dollar-value sampling concept or real trade-record fields and reporting rules.
- Package conventions: inclusive threshold, fixed-point scales, one currency/symbol per call, whole-trade assignment, tie sequence, session reset, and partial-tail behavior.
- Derived result: the 10,300 USD worked example and all 240-trade fixture bars are independently calculated from distributed synthetic inputs.
- Interpretation: Dollar Bars can normalize sampling by reported notional, but do not automatically normalize economic exposure or establish predictive value.
Data and historical-case note
All distributed observations are deterministic synthetic records under the repository's project license. No real transaction tape or licensed vendor payload is redistributed. A historical case remains deferred until the actual eligible transaction membership can be reproduced with documented redistribution rights. EOD OHLCV or provider aggregates are not accepted as evidence of Dollar Bar membership.
Full dependency-light reference implementations in both supported languages.
/** Whole-trade, single-currency Dollar Bars with fixed-point membership math. */
export type Trade = {
tradeId: string;
timestamp: string;
session: string;
symbol: string;
price: number;
volume: number;
currency: string;
sequence?: number;
};
export type DollarBarConfig = {
targetDollar: number;
currency: string;
priceDecimals?: number;
quantityDecimals?: number;
closePartial?: boolean;
};
export type DollarBar = {
barIndex: number;
session: string;
symbol: string;
currency: string;
startTime: string;
endTime: string;
lastTradeTime: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
dollarValue: number;
targetDollar: number;
excessDollar: number;
tickCount: number;
firstTradeId: string;
lastTradeId: string;
closeReason: "threshold" | "session_end" | "stream_end";
};
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
const DECIMAL_TEXT = /^(?:0|[1-9]\d*)(?:\.(\d+))?$/;
function timestamp(value: unknown): number {
if (typeof value !== "string" || !value.endsWith("Z")) throw new Error("timestamp must be ISO-8601 UTC ending in Z");
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error("timestamp must be valid ISO-8601 UTC");
return parsed;
}
function decimalPlaces(value: unknown, label: string, fallback: number): number {
const resolved = value ?? fallback;
if (!Number.isInteger(resolved) || Number(resolved) < 0 || Number(resolved) > 8) {
throw new Error(`${label} must be an integer from 0 through 8`);
}
return Number(resolved);
}
function scaledInt(value: unknown, places: number, label: string): bigint {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number`);
const text = String(value);
const match = DECIMAL_TEXT.exec(text);
if (!match) throw new Error(`${label} must be positive and written without exponent notation`);
const fraction = match[1] ?? "";
if (fraction.length > places) throw new Error(`${label} exceeds the configured ${places}-decimal scale`);
const [whole] = text.split(".");
const scaled = BigInt(whole) * 10n ** BigInt(places) + BigInt(fraction.padEnd(places, "0") || "0");
if (scaled <= 0n || scaled > MAX_SAFE) throw new Error(`${label} is outside the supported positive fixed-point range`);
return scaled;
}
function numberFromScaled(value: bigint, places: number): number {
if (places === 0) return Number(value);
const sign = value < 0n ? "-" : "";
const digits = (value < 0n ? -value : value).toString().padStart(places + 1, "0");
return Number(`${sign}${digits.slice(0, -places)}.${digits.slice(-places)}`);
}
export function constructBars(trades: Trade[], config: DollarBarConfig): DollarBar[] {
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 (config.closePartial !== undefined && typeof config.closePartial !== "boolean") throw new Error("closePartial must be boolean");
if (typeof config.currency !== "string" || !/^[A-Z]{3}$/.test(config.currency)) {
throw new Error("config.currency must be one three-letter uppercase currency code");
}
const pricePlaces = decimalPlaces(config.priceDecimals, "priceDecimals", 2);
const quantityPlaces = decimalPlaces(config.quantityDecimals, "quantityDecimals", 0);
const notionalPlaces = pricePlaces + quantityPlaces;
const target = scaledInt(config.targetDollar, notionalPlaces, "targetDollar");
const closePartial = config.closePartial !== false;
const ids = new Set<string>();
const closedSessions = new Set<string>();
const prepared: Array<{ trade: Trade; time: number; price: bigint; quantity: bigint }> = [];
let priorTime: number | null = null;
let priorSequence: number | undefined;
let validationSession: string | null = null;
let symbol: string | null = null;
for (const trade of trades) {
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`);
}
if (ids.has(trade.tradeId)) throw new Error("tradeId must be unique after corrections are resolved");
ids.add(trade.tradeId);
if (trade.currency !== config.currency) throw new Error("mixed or unexpected currency is not allowed; normalize upstream");
if (symbol === null) symbol = trade.symbol;
else if (trade.symbol !== symbol) throw new Error("one constructBars call may contain only one symbol");
const time = timestamp(trade.timestamp);
if (trade.sequence !== undefined && !Number.isInteger(trade.sequence)) throw new Error("sequence must be an integer when supplied");
if (priorTime !== null) {
if (time < priorTime) throw new Error("trades must be chronological");
if (time === priorTime && (priorSequence === undefined || trade.sequence === undefined || trade.sequence <= priorSequence)) {
throw new Error("equal timestamps require strictly increasing integer sequence values");
}
}
priorTime = time;
priorSequence = trade.sequence;
if (validationSession === null) validationSession = trade.session;
else if (trade.session !== validationSession) {
closedSessions.add(validationSession);
if (closedSessions.has(trade.session)) throw new Error("a session may not reappear after its state has closed");
validationSession = trade.session;
}
prepared.push({
trade,
time,
price: scaledInt(trade.price, pricePlaces, "price"),
quantity: scaledInt(trade.volume, quantityPlaces, "volume"),
});
}
const result: DollarBar[] = [];
let current: typeof prepared = [];
let activeSession: string | null = null;
let cumulative = 0n;
const emit = (reason: DollarBar["closeReason"]): void => {
if (!current.length) return;
const prices = current.map(item => item.price);
const quantities = current.map(item => item.quantity);
const first = current[0].trade;
const last = current[current.length - 1].trade;
result.push({
barIndex: result.length,
session: first.session,
symbol: first.symbol,
currency: config.currency,
startTime: first.timestamp,
endTime: last.timestamp,
lastTradeTime: last.timestamp,
open: numberFromScaled(prices[0], pricePlaces),
high: numberFromScaled(prices.reduce((a, b) => a > b ? a : b), pricePlaces),
low: numberFromScaled(prices.reduce((a, b) => a < b ? a : b), pricePlaces),
close: numberFromScaled(prices[prices.length - 1], pricePlaces),
volume: numberFromScaled(quantities.reduce((a, b) => a + b, 0n), quantityPlaces),
dollarValue: numberFromScaled(cumulative, notionalPlaces),
targetDollar: numberFromScaled(target, notionalPlaces),
excessDollar: numberFromScaled(cumulative - target, notionalPlaces),
tickCount: current.length,
firstTradeId: first.tradeId,
lastTradeId: last.tradeId,
closeReason: reason,
});
current = [];
cumulative = 0n;
};
for (const item of prepared) {
if (activeSession !== null && item.trade.session !== activeSession) {
if (current.length && closePartial) emit("session_end");
else { current = []; cumulative = 0n; }
}
activeSession = item.trade.session;
const contribution = item.price * item.quantity;
if (cumulative + contribution > MAX_SAFE) throw new Error("bar notional exceeds the cross-language safe-integer range");
current.push(item);
cumulative += contribution;
if (cumulative >= target) emit("threshold");
}
if (current.length && closePartial) emit("stream_end");
return result;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.