D01-F01-A03 / Complete engineering topic

Volume Bars

A production-minded guide to Volume Bars.

D01 · MARKET DATA ENGINEERING
D01-F01-A03Canonical / Tested / Open
D01 / D01-F01
Key concepts

The governed definitions this build depends on. Read them first if a term is unfamiliar.

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.

Volume Bars overview showing share-volume sampling

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:

FieldMeaning
tradeIdUnique lineage ID after upstream correction handling
timestampUTC execution time; equal timestamps retain caller order
sessionExplicit, contiguous reset partition
symbolOne instrument per function call
priceFinite positive currency-per-share value
volumeFinite positive eligible shares
currencyOne 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:

Tj=min{T:i=sjTviV}.T_j = \min\left\{T : \sum_{i=s_j}^{T}v_i \ge V^*\right\}.
SymbolMeaningUnit
v_iEligible quantity on trade ishares
s_jFirst trade index in bar jinput position
T_jThreshold-crossing trade indexinput position
V*Configured targetshares

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:

O=psj,H=maxpi,L=minpi,C=pTj,O=p_{s_j},\quad H=\max p_i,\quad L=\min p_i,\quad C=p_{T_j}, V=vi,D=pivi.V=\sum v_i,\qquad D=\sum p_i v_i.

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:

TradePriceSharesCumulative in open barResult
W001100.00400400Continue
W002101.00350750Continue
W00399.005001,250Close bar 0 with 250 overshoot
W00499.50600600New bar starts from zero
W005100.504001,000Close bar 1 at equality
W006101.00250250Emit 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.

Exact overshoot, equality, and reset behavior

The machine-readable input and expected output are in worked-example.json, so the prose arithmetic is also a test oracle.

Construct bars causally

Rendering system map…

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

Six trades mapped to two complete bars and one partial bar

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 questionThis packageWhat changes under another choice
Does equality close?Yes, >=Using > delays an exact-boundary close.
Is a crossing trade split?NoSplitting creates synthetic fragments and exact target totals.
Is overshoot carried?NoCredit carry changes later boundaries without a new contributing trade.
Does state cross sessions?NoContinuous state can combine unrelated trading regimes or maintenance gaps.
Are partial tails kept?ConfigurableDropping tails removes valid trades from output totals by design.
Are corrections applied inside the builder?NoUpstream 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.

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.

Rendering system map…

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 notes

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.

bars.ts
/** 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;
}
Full-height labplaygroundOpen full screen