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.
- PrimaryTrading VolumeTrading volume is the aggregate eligible quantity executed over a stated instrument, source, and interval.
- PrerequisiteTradeA trade is an executed transaction in which a quantity of a financial instrument changes hands at agreed terms.
- ImportantMeasurement UnitA measurement unit states the quantity scale represented by a numeric value, such as currency, shares, contracts, seconds, or percent.
- ImportantVolume Adjustment BasisA volume adjustment basis declares how reported volume values are represented and adjusted within a series.
- MentionedAdjusted PriceAdjusted price is a transformed price expressed on a declared comparison basis after applying specified corporate-action or other adjustment factors.
- MentionedUnadjusted PriceUnadjusted price is the reported or normalized market price before retrospective corporate-action adjustment.
Practical promise. You will learn exactly which trade closes a volume bar, what happens to overshoot, how sessions and incomplete tails behave, and how to verify identical Python and TypeScript output.
A time bar asks whether the clock interval ended. A volume bar asks a different question: has cumulative eligible share volume reached the target? That sounds simple, but a production-quality answer depends on eligibility, units, ordering, threshold equality, crossing-trade treatment, session resets, and partial tails.
This article uses a documented whole-trade convention. It teaches construction mechanics, not a trading signal. A correctly formed volume bar does not imply that its close predicts a future price or that volume sampling improves returns.
Start with the feed, not the candle
The input is an ordered stream of executions. Official NYSE Daily TAQ documentation includes trade time, symbol, volume, price, sale condition, and message-sequence fields; it defines trade volume as the number of shares traded and describes files sorted by symbol, time, and message sequence (NYSE Daily TAQ Client Specification v4.3, pp. 16-18). FINRA's reporting guidance separately distinguishes execution time, fractional and odd-lot quantities, corrections, cancellations, and reversals (FINRA Trade Reporting FAQ).
Those sources show why a raw tape needs a policy; they do not prescribe one universal filter. The reference function therefore expects corrections, cancellations, duplicates, and sale-condition eligibility to be resolved before construction.
Each accepted record has:
| Field | Meaning |
|---|---|
tradeId | Unique lineage ID after upstream correction handling |
timestamp | UTC execution time; equal timestamps retain caller order |
session | Explicit, contiguous reset partition |
symbol | One instrument per function call |
price | Finite positive currency-per-share value |
volume | Finite positive eligible shares |
currency | One currency per function call |
Do not substitute lots, contracts, or price * quantity for shares without renaming the statistic and revising the contract. price * volume is reported as a diagnostic dollar sum, but it does not close this bar.
The rule and every boundary choice
For target V* > 0, bar j closes on the first trade T_j whose cumulative eligible shares meet or exceed the target:
| Symbol | Meaning | Unit |
|---|---|---|
v_i | Eligible quantity on trade i | shares |
s_j | First trade index in bar j | input position |
T_j | Threshold-crossing trade index | input position |
V* | Configured target | shares |
The comparison includes equality. The crossing trade stays whole in the closing bar. The next bar begins with T_j + 1, so overshoot is neither split nor carried forward as a credit.
After membership is fixed, prices p_i produce:
D is calculated from each trade, not from an average-price shortcut.
Work the example by hand
Use a 1,000-share target and keep partial tails:
| Trade | Price | Shares | Cumulative in open bar | Result |
|---|---|---|---|---|
| W001 | 100.00 | 400 | 400 | Continue |
| W002 | 101.00 | 350 | 750 | Continue |
| W003 | 99.00 | 500 | 1,250 | Close bar 0 with 250 overshoot |
| W004 | 99.50 | 600 | 600 | New bar starts from zero |
| W005 | 100.50 | 400 | 1,000 | Close bar 1 at equality |
| W006 | 101.00 | 250 | 250 | Emit partial bar 2 at stream end |
Bar 0 contains W001-W003: O=100, H=101, L=99, C=99, V=1,250, and D=124,850. Bar 1 starts at W004, not with a 250-share credit. That one lineage fact distinguishes the package convention from residual-carry or split-trade variants.
The machine-readable input and expected output are in worked-example.json, so the prose arithmetic is also a test oracle.
Construct bars causally
The implementation validates the full input before emitting output. It then maintains scalar open, high, low, close, share volume, dollar value, count, and lineage fields. A session change either emits a below-target tail as session_end or discards it, depending on closePartial. The final tail is similarly emitted as stream_end or dropped.
Equal execution timestamps are allowed because real feeds can provide a separate sequence. In this compact schema, caller order is authoritative for ties. A production pipeline should preserve the feed's sequence and receive/availability timestamps as separate provenance.
Inspect the exact membership
The diagram shows quantities inside the trade nodes instead of drawing anonymous dots. It makes three facts visible: W003 creates overshoot but is not split; W004 begins from zero; and W006 is explicitly partial rather than disguised as complete.
Then open the interactive Volume Bars laboratory. It offers a dense 240-trade synthetic tape, session scenarios, target changes, deterministic back/step/play/reset controls, closure diagnostics, and an audit ledger. Parameter changes recompute membership from the first observation. Exact partial-tail keep/drop behavior remains directly executable in the reference implementations and tests.
Implementation and verification
Both implementations use one causal pass. Tests lock down:
- exact expected output for all 240 trades;
- the six-trade overshoot/equality/partial example;
- no numerical overshoot carry;
- keep-versus-drop partial-tail policy;
- session isolation and rejection of a reappearing session;
- stable caller order for tied timestamps;
- rejection of duplicates, reversed time, non-finite values, mixed symbols, and mixed currencies.
Construction tests answer whether the code matches the definition. They do not answer whether the target is economically optimal or a downstream strategy remains useful after costs and bias controls.
Policies that change the result
| Policy question | This package | What changes under another choice |
|---|---|---|
| Does equality close? | Yes, >= | Using > delays an exact-boundary close. |
| Is a crossing trade split? | No | Splitting creates synthetic fragments and exact target totals. |
| Is overshoot carried? | No | Credit carry changes later boundaries without a new contributing trade. |
| Does state cross sessions? | No | Continuous state can combine unrelated trading regimes or maintenance gaps. |
| Are partial tails kept? | Configurable | Dropping tails removes valid trades from output totals by design. |
| Are corrections applied inside the builder? | No | Upstream versioning determines the eligible ordered tape. |
Failure modes and responsible use
- A sale-condition policy can change which prints count. Pin it by feed and version.
- A late correction can shift the affected bar and every later boundary. Rebuild from a versioned tape.
- Quantity units differ across asset classes. Do not label lots or contracts as shares.
- A threshold that is useful for one instrument or regime is not automatically portable.
- OHLCV is lossy: it cannot reconstruct the original within-bar event path.
- A visually plausible historical chart is not evidence of prediction or profitability.
Why the teaching tape is synthetic
A real historical session would be valuable only if readers could reproduce transaction membership from redistributable prints with documented sequence, sale conditions, corrections, session rules, and share units. Aggregate provider OHLCV cannot identify the threshold-crossing trade. No transaction-level source meeting those requirements was established for this review, so the package keeps a dense, explicitly synthetic fixture rather than presenting a named security with unverifiable mechanics.
The SEC's MIDAS overview documents the scale and specialized processing demands of high-resolution orders, cancellations, and executions; it does not validate this package's target or imply investment usefulness (SEC MIDAS).
Summary and next topic
You can now form a volume bar without hidden boundary assumptions: count eligible shares, keep the crossing trade whole, close on equality or overshoot, reset at the declared session, and label incomplete tails. The result remains auditable through first/last trade IDs and a close reason.
Continue with Dollar Bars to see how replacing shares with reported notional changes membership while preserving the same provenance discipline.
Primary references and source roles
See REFERENCES.md for versions, jurisdictions, supported claims, and limitations. The canonical factual contract remains README.md.
Rendered from the canonical Mermaid sources linked by this article.
Causal construction flow - Volume Bars
Purpose: show how the whole-trade and session-reset conventions prevent future records or numerical overshoot credit from changing prior membership.
Takeaway: the crossing trade belongs wholly to the completed bar, and the next bar starts with the next eligible trade.
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
- Organization or authors: Marcos Lopez de Prado
- Source type: Original author manuscript for the book chapter
- Publication or effective date: 2018-01-18
- Version: Wiley first-edition chapter manuscript
- URL or DOI: https://ssrn.com/abstract=3104847
- Accessed: 2026-07-22
- Jurisdiction: Methodological; not venue-specific
- Supports: Attribution and comparison of standard and information-driven financial-bar sampling families.
- Limitations: The source does not make this package's whole-trade, session-reset, or partial-tail choices universal. Those are explicit implementation conventions.
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-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 trades file's symbol/time/message-sequence ordering; trade-time, symbol, volume, price, sale-condition, participant, and related fields; trade volume as number of shares.
- Limitations: Product access and redistribution are governed by NYSE terms. The specification describes feed records, not a universal eligibility filter or volume-bar algorithm.
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 meaning, share-quantity reporting, fractional and odd-lot treatment, and correction/cancellation/reversal distinctions.
- Limitations: Facility- and security-specific reporting rules are not themselves a bar-construction algorithm or an instruction to include every disseminated print.
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 market data include posted orders, modifications/cancellations, on-exchange executions, and off-exchange executions and require specialized processing.
- Limitations: MIDAS coverage and examples do not prescribe trade eligibility, the package's target, or predictive usefulness.
Evidence and data note
All distributed observations are deterministic synthetic records under the repository's project license. They model field shapes and boundary cases, not a real venue, security, investor, or historical event. This review deliberately did not substitute aggregate provider bars for transaction-level evidence. Correct construction does not establish economic or predictive value.
Full dependency-light reference implementations in both supported languages.
/** Reference implementation of whole-trade, session-reset volume bars. */
export type Trade = {
tradeId: string;
timestamp: string;
session: string;
symbol: string;
price: number;
volume: number;
currency: string;
};
export type VolumeBarConfig = {
targetVolume: number;
closePartial?: boolean;
};
export type VolumeBar = {
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";
};
type State = Omit<VolumeBar, "barIndex" | "endTime" | "closeReason">;
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 a valid ISO-8601 UTC string");
return parsed;
}
function positiveFinite(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: VolumeBarConfig): { target: number; closePartial: boolean } {
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");
const target = positiveFinite(config.targetVolume, "targetVolume");
const closePartial = config.closePartial ?? true;
if (typeof closePartial !== "boolean") throw new Error("closePartial must be boolean");
const ids = new Set<string>();
const finishedSessions = new Set<string>();
let previousTime = -Infinity;
let currentSession: string | null = null;
let symbol: string | null = null;
let currency: string | null = null;
trades.forEach((trade, position) => {
if (!trade || typeof trade !== "object" || REQUIRED_FIELDS.some(field => !(field in trade))) {
throw new Error(`trade at position ${position} is missing a required field`);
}
for (const field of ["tradeId", "session", "symbol", "currency"] as const) {
if (typeof trade[field] !== "string" || trade[field].length === 0) {
throw new Error(`${field} must be a non-empty string`);
}
}
const eventTime = timestampMs(trade.timestamp);
if (eventTime < previousTime) throw new Error("trades must be globally chronological");
previousTime = eventTime;
if (ids.has(trade.tradeId)) throw new Error("tradeId must be unique after corrections and cancels");
ids.add(trade.tradeId);
positiveFinite(trade.price, "price");
positiveFinite(trade.volume, "volume");
symbol ??= trade.symbol;
currency ??= trade.currency;
if (trade.symbol !== symbol) throw new Error("all trades in one call must have the same symbol");
if (trade.currency !== currency) throw new Error("all trades in one call must have the same currency");
if (currentSession !== null && trade.session !== currentSession) {
finishedSessions.add(currentSession);
if (finishedSessions.has(trade.session)) {
throw new Error("each session must occupy one contiguous input block");
}
}
currentSession = trade.session;
});
return { target, closePartial };
}
function rounded(value: number): number { return Number(value.toFixed(8)); }
function start(trade: Trade): State {
return {
session: trade.session,
startTime: trade.timestamp,
lastTradeTime: trade.timestamp,
open: trade.price,
high: trade.price,
low: trade.price,
close: trade.price,
volume: trade.volume,
dollarValue: trade.price * trade.volume,
tickCount: 1,
firstTradeId: trade.tradeId,
lastTradeId: trade.tradeId,
};
}
function add(state: State, trade: Trade): void {
state.lastTradeTime = trade.timestamp;
state.high = Math.max(state.high, trade.price);
state.low = Math.min(state.low, trade.price);
state.close = trade.price;
state.volume += trade.volume;
state.dollarValue += trade.price * trade.volume;
state.tickCount += 1;
state.lastTradeId = trade.tradeId;
}
/**
* Input order is authoritative when timestamps tie. The crossing trade stays
* whole; overshoot is neither split nor carried numerically to the next bar.
*/
export function constructBars(trades: Trade[], config: VolumeBarConfig): VolumeBar[] {
const { target, closePartial } = validate(trades, config);
const bars: VolumeBar[] = [];
let state: State | null = null;
let session: string | null = null;
const emit = (current: State, closeReason: VolumeBar["closeReason"]): void => {
bars.push({
barIndex: bars.length,
session: current.session,
startTime: current.startTime,
endTime: current.lastTradeTime,
lastTradeTime: current.lastTradeTime,
open: rounded(current.open),
high: rounded(current.high),
low: rounded(current.low),
close: rounded(current.close),
volume: rounded(current.volume),
dollarValue: rounded(current.dollarValue),
tickCount: current.tickCount,
firstTradeId: current.firstTradeId,
lastTradeId: current.lastTradeId,
closeReason,
});
};
for (const trade of trades) {
if (session !== null && trade.session !== session) {
if (state !== null && closePartial) emit(state, "session_end");
state = null;
}
session = trade.session;
if (state === null) state = start(trade);
else add(state, trade);
if (state.volume >= target) {
emit(state, "threshold");
state = null;
}
}
if (state !== null && closePartial) emit(state, "stream_end");
return bars;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.