# D01-F01-A06 — Volume-Imbalance Bars

> **Status:** Article-ready. Python, TypeScript, shared fixtures, and teaching visuals are validated.

## Executive summary

A volume-imbalance bar closes when the magnitude of accumulated **tick-signed trade quantity** reaches a threshold estimated from earlier completed bars. The method samples directional net shares rather than elapsed time, trade count, or gross shares.

This package answers an engineering question: **given a cleaned, ordered tape and explicit initialization, exactly which trades belong to each bar?** It does not claim that a closure predicts a price move or produces a profitable signal.

## Learning contract

After this topic, a reader can:

- compute tick signs, signed shares, the frozen threshold, and overshoot by hand;
- distinguish sourced method concepts from this package's seeds, floor, reset, and tail conventions;
- implement the same causal state machine in Python and TypeScript;
- explain why corrections, trade eligibility, tied timestamps, and session boundaries can change membership;
- audit a bar from its first and last trade IDs and frozen expectation state.

Prerequisites are eligible trade records, OHLCV aggregation, the tick rule, and exponential moving averages (EMAs). **Tick-Imbalance Bars** is the closest prior tutorial.

## Facts, conventions, calculations, and interpretation

| Class | What belongs here |
|---|---|
| Sourced method fact | Volume imbalance uses signed quantity `b_t v_t`; an expectation-based rule defines a stopping time. |
| Package convention | Initial seeds, EMA coefficients, a positive threshold floor, whole-trade assignment, session reset, and partial-tail policy. |
| Calculation | The trace in `examples/worked-example.json` and expected outputs in `tests/shared_fixture.json`. |
| Interpretation | A close means the configured mechanical boundary was reached. It is not evidence of informed trading, direction, or future returns. |

## Input data contract

The function accepts one symbol and one currency per call.

| Field | Type | Unit | Null? | Contract |
|---|---|---:|:---:|---|
| `tradeId` | non-empty string | identifier | No | Unique after cancels and corrections are resolved. |
| `timestamp` | ISO-8601 string ending `Z` | UTC event time | No | Nondecreasing; equal timestamps preserve caller-supplied array order. |
| `session` | non-empty string | reset key | No | Contiguous; a session cannot reappear after a later session begins. |
| `symbol` | non-empty string | instrument ID | No | Constant within one call. |
| `price` | finite number | currency/share | No | Strictly positive. |
| `volume` | finite number | shares | No | Strictly positive eligible executed quantity. |
| `currency` | non-empty string | currency code | No | Constant within one call. |

`volume` means shares in this equity-style package. Contracts, lots, tokens, or notional require a separately documented unit conversion; they must not be silently labeled shares.

The canonical input is already corrected, deduplicated, filtered for eligible sale conditions, and ordered using source sequence information when timestamps tie. A correction is not another trade. Rebuild affected bars from the corrected tape; do not append the correction as fresh volume.

The current official NYSE Daily TAQ Client Specification v4.3 (2026-03-03) says the trades file is sorted by symbol, time, and message sequence number; its trade schema includes SIP publication time, symbol, sale condition, share volume, share price, correction indicator, sequence number, and trade ID. The specification also states that access requires an appropriate agreement. Consequently, this public package distributes synthetic observations rather than a real named session.

## Configuration contract

| Field | Unit/range | Canonical value | Meaning |
|---|---:|---:|---|
| `initialTickSign` | `-1` or `+1` | `+1` | Sign used for the first trade of each session and carried across initial flat prices. |
| `initialExpectedTicks` | trades, `> 0` | `16` | Seed for expected complete-bar length. |
| `initialExpectedSignedVolume` | signed shares/trade, finite | `35` | Seed for expected signed quantity per trade. Its sign is retained in state; the threshold uses its magnitude. |
| `alphaTicks` | `(0,1]` | `0.20` | EMA coefficient for observed complete-bar tick count. |
| `alphaSignedVolume` | `(0,1]` | `0.20` | EMA coefficient for observed signed shares per trade. |
| `thresholdFloorShares` | shares, `> 0` | `300` | Final absolute minimum threshold. Scaling can never reduce the threshold below this floor. Not a universal method constant. |
| `thresholdScale` | dimensionless, `> 0` | `1` | Scenario multiplier applied to the adaptive expected-imbalance component before the final floor. |
| `closePartial` | boolean | `true` | Emit an incomplete bar at session or stream end. |

All adaptive expectation state resets to these seeds when `session` changes. This is a deliberate package convention, not a universal requirement for continuous markets.

## Tick-sign and signed-volume rules

For ordered trade prices `p_t`, the package's causal tick sign is

$$
b_t =
\begin{cases}
+1, & p_t > p_{t-1},\\
-1, & p_t < p_{t-1},\\
b_{t-1}, & p_t = p_{t-1}.
\end{cases}
$$

At each session start, `b_1 = initialTickSign`. This flat-trade convention follows the tick-test idea described by Lee and Ready, but it remains an inference from prices rather than observed aggressor identity.

Signed volume per trade and cumulative signed volume within the forming bar are

$$
x_t = b_t v_t, \qquad \theta_T = \sum_{t=1}^{T} x_t.
$$

`v_t` is shares, so `x_t`, `theta_T`, the floor, threshold, and overshoot all have units of shares.

## Frozen stopping rule

At the start of bar `k`, freeze the expectation state learned only from earlier complete bars:

$$
h_k = \max\left(h_{\min},s\,\widehat{E}_{k-1}[T]\,\left|\widehat{E}_{k-1}[x]\right|\right).
$$

The bar closes on the first trade count `T_k` satisfying

$$
T_k = \min\{T \ge 1 : |\theta_T| \ge h_k\}.
$$

Here `s` is the dimensionless scale, `h_min` is the final absolute floor in shares, `E[T]` is trades/bar, and `E[x]` is signed shares/trade. The scaled adaptive product has units of shares, and the outer maximum guarantees `h_k >= h_min` even when `s < 1`.

The threshold is frozen for the entire bar. The crossing trade belongs wholly to the closing bar, equality closes, and overshoot is

$$
o_k = \max(|\theta_{T_k}|-h_k,0).
$$

No portion of the crossing trade or overshoot is carried into the next bar.

## Post-close expectation update

Only a threshold-complete bar updates expectations:

$$
\widehat{E}_{k}[T]=(1-\alpha_T)\widehat{E}_{k-1}[T]+\alpha_T T_k,
$$

$$
\widehat{E}_{k}[x]=(1-\alpha_x)\widehat{E}_{k-1}[x]+\alpha_x\frac{\theta_{T_k}}{T_k}.
$$

The next threshold is computed after those updates. Session-end and stream-end partial bars do not update either EMA.

This second update is an explicit package variant: it applies one EMA observation per complete bar, using that bar's mean signed shares per trade. Other implementations estimate `E[b_t v_t]` from a tick-level EMA or a rolling history whose window depends on expected bar length. Those estimators are not numerically interchangeable, so production metadata must name the estimator rather than merely saying "volume-imbalance bars."

## Worked example

Use `initialTickSign=+1`, expected length `4`, expected signed volume `50 shares/trade`, a `120-share` floor, and both EMA coefficients `0.25`. The first frozen threshold is `max(120, 4 x 50) = 200 shares`.

| Trade | Price | Shares | Sign | Signed shares | Cumulative | Decision |
|---|---:|---:|---:|---:|---:|---|
| W01 | 100.00 | 60 | +1 | +60 | +60 | Continue |
| W02 | 100.00 | 40 | +1 | +40 | +100 | Flat price carries +1 |
| W03 | 99.99 | 70 | -1 | -70 | +30 | Continue |
| W04 | 99.98 | 90 | -1 | -90 | -60 | Continue |
| W05 | 99.98 | 50 | -1 | -50 | -110 | Flat price carries -1 |
| W06 | 99.97 | 95 | -1 | -95 | -205 | Close; 5-share overshoot |

After closure, expected ticks become `4.5`. Expected signed volume becomes `28.95833333 shares/trade`, so the next frozen threshold is `130.3125 shares`. W07 contributes `+80`; W08 reaches `+135`, closes the second bar, and overshoots by `4.6875 shares`.

The two W01/W02 timestamps tie. Their array order is authoritative and both remain in that order. The full machine-readable trace is in [`examples/worked-example.json`](./examples/worked-example.json).

## Algorithm

```text
validate configuration and the already-cleaned trade stream
initialize session seeds and freeze the first threshold
for each trade in caller-supplied chronological order:
    if session changed:
        optionally emit the old partial without EMA update
        reset tick sign and both expectations to session seeds
    infer sign from the current and previous price; carry sign on a flat price
    append the whole trade and add sign * shares to cumulative signed volume
    if absolute cumulative signed volume >= frozen threshold:
        emit a complete bar with lineage, threshold state, and overshoot
        update both EMAs from that completed bar
        reset accumulation and freeze the next threshold
optionally emit the final partial without EMA update
```

## Output contract

Every bar includes OHLC, gross `volume`, exact unrounded-sum `dollarValue`, trade count, session, first/last trade IDs, and these diagnostics:

| Field | Unit | Meaning |
|---|---:|---|
| `signedVolume` | signed shares | Final `theta` for the bar. |
| `thresholdShares` | shares | Threshold frozen before the first contributing trade. |
| `expectedTicksBefore` | trades/bar | Frozen length expectation. |
| `expectedSignedVolumeBefore` | signed shares/trade | Frozen signed-volume expectation. |
| `overshootShares` | shares | Amount above the threshold magnitude; zero for non-crossing partials. |
| `thresholdMet` | boolean | Whether the final state met the frozen rule. |
| `isComplete` | boolean | True only for a threshold close. |
| `closeReason` | enum | `threshold`, `session_end`, or `stream_end`. |

OHLCV is downstream of membership: `O` is first price, `H` maximum, `L` minimum, `C` last, gross volume is `sum(v)`, and dollar value is `sum(p v)`. Output numbers are rounded to eight decimals for cross-language fixtures.

## Edge and invalid-input rules

| Case | Package behavior |
|---|---|
| Exact equality | Close immediately. |
| Crossing trade | Assign it wholly to the closing bar; report overshoot; no carry. |
| Flat price | Carry the previous sign; use the configured seed at session start. |
| Tied event time | Preserve caller order. Source sequence resolution is upstream. |
| Session change | Optionally emit partial; then reset sign and both expectations. |
| Stream tail | Emit only when `closePartial=true`; never update expectations from it. |
| Duplicate/correction | Reject duplicate IDs; corrections must already be resolved. |
| Reappearing session | Reject because state lineage would be ambiguous. |
| Empty input | Return an empty list. |
| Non-finite/nonpositive price or volume | Reject. |
| Invalid sign, EMA, floor, scale, symbol/currency mixture | Reject before construction. |

## Validation and limitations

The shared oracle contains 240 deterministic synthetic trades across two sessions and produces 18 bars under the canonical configuration. Python and TypeScript tests cover exact parity, the worked trace, equality, overshoot, flat and tied trades, session isolation, partial policy, invalid fields, non-finite values, mixed instruments/currencies, and session reappearance.

Important limitations:

- The tick rule is an inference and may differ from quote- or venue-based aggressor classification.
- Sale conditions, corrections, cancels, late reports, and sequence rules are source-specific upstream work.
- Seeds, EMA coefficients, floor, scale, and reset policy materially change membership.
- A volume-imbalance bar is not the same as order-book imbalance, footprint-chart imbalance, or VPIN.
- Synthetic data prove mechanics, not market realism, historical association, predictive power, or profitability.
- A named historical session is intentionally omitted: daily aggregates cannot prove which transaction crossed an adaptive threshold, and this package has no redistributable transaction tape that would support that claim.

## Visual explanations

- [`construction-anatomy.svg`](./visuals/static/construction-anatomy.svg) maps the exact eight-trade example into two bars.
- [`boundary-and-state.svg`](./visuals/static/boundary-and-state.svg) shows the 200-share close, overshoot, post-close update, and session reset.
- [`playground.html`](./visuals/animated/playground.html) provides canonical, equality/overshoot, and invalid-input scenarios with Back, Step, Play/Pause, and deterministic Reset.
- [`construction-flow.md`](./visuals/mermaid/construction-flow.md) isolates the causal update order.

The article hero includes a clearly labeled **illustrative secondary signed-volume overlay**. It is not order-book depth, observed aggressor flow, or a claim about a real session.

## Implementations and related topics

- [Python reference](./implementations/python/bars.py)
- [TypeScript reference](./implementations/typescript/bars.ts)
- [Dataset note](./datasets/DATASET.md)
- [Tests and shared fixture](./tests/)

Related catalog topics: **Volume Bars** (gross-share threshold), **Tick-Imbalance Bars** (signed trade count), **Tick-Run Bars** (dominant-side run), and **OHLC Consistency Validator** (downstream bar invariants).

## References

See [`REFERENCES.md`](./REFERENCES.md) for source roles, versions, licensing limitations, and the boundary between sourced definitions and package conventions.
