# D01-F01-A07 — Tick-Run Bars

## Executive summary

A Tick-Run Bar closes when the larger **cumulative count** of positive and negative tick-rule signs reaches a threshold estimated from earlier complete bars. Here, “run” does **not** mean the longest unbroken streak of identical signs. A sequence `+1, -1, +1, -1, +1` has a positive run count of three even though its longest consecutive positive streak is one.

The method changes the sampling clock; it does not reveal buyer identity, certify informed trading, predict returns, or guarantee a useful strategy. This package implements one transparent, causal convention with cross-language parity and audit fields.

## Learning contract

After this topic, a reader can:

- assign tick signs, including the seed and flat-price carry rule;
- distinguish dominant cumulative count from net imbalance and consecutive streak length;
- calculate the frozen threshold, equality close, and fractional overshoot by hand;
- implement session-local adaptive state without look-ahead;
- explain which rules come from the named methodology and which are package choices;
- test membership, tails, resets, and Python/TypeScript parity.

Prerequisites: eligible trade records, OHLCV aggregation, the tick rule, and exponential moving averages (EMAs). **Tick-Imbalance Bars** is the closest comparison: it accumulates signed net count, whereas Tick-Run Bars compares the two side counts.

## Facts, conventions, calculations, and interpretation

| Classification | What belongs here |
|---|---|
| Sourced methodology | Tick-rule signs; positive/negative cumulative counts; expected bar length and side probability in an adaptive first-passage threshold. |
| Package convention | Seed `+1`; flats carry the prior sign; a final threshold floor; one-step EMAs after complete bars; session reset; whole-trade assignment; optional partial tails. |
| Exact calculation | The two counts, frozen threshold, OHLCV, lineage, and post-close EMA values. |
| Interpretation only | A dominant-side count says the sampled signs accumulated asymmetrically. It does not identify motive, information, profitability, or the next price move. |

## Input data contract

Each call accepts one symbol and one currency.

| Field | Type | Nullable | Meaning and rule |
|---|---|:---:|---|
| `tradeId` | non-empty string | No | Unique after corrections and cancellations are resolved. |
| `timestamp` | UTC ISO-8601 string ending in `Z` | No | Event ordering field used by this package. Equal times need increasing integer `sequence` values. |
| `sequence` | integer | Conditional | Required to disambiguate equal timestamps. |
| `session` | non-empty string | No | Explicit state-reset key; sessions must be contiguous and may not reappear. |
| `symbol` | non-empty string | No | Exactly one per call. |
| `price` | finite number | No | Strictly positive, in `currency` per share. |
| `volume` | finite number | No | Strictly positive shares. Used in OHLCV, not in the stopping count. |
| `currency` | non-empty string | No | Exactly one per call. |

Input must already be corrected, deduplicated, sorted, and filtered under a documented trade-eligibility policy. The current official [NYSE Daily TAQ Client Specification v4.3](https://www.nyse.com/publicdocs/nyse/data/Daily_TAQ_Client_Spec_v4.3.pdf), dated 2026-03-03, documents fields such as SIP publication time, symbol, sale condition, share volume, price, correction indicator, sequence number, and trade ID. It also describes contractual access. Those facts motivate the contract; TAQ does not define this package’s eligibility filter or bar parameters.

## Configuration contract

| Key | Domain | Canonical fixture | Role |
|---|---|---:|---|
| `closePartial` | boolean | `true` | Emit an incomplete bar at session/stream end when true; otherwise drop it. |
| `initialTickSign` | `-1` or `+1` | `+1` | Sign of the first trade in every session. Package choice. |
| `initialExpectedTicks` | ticks, `> 0` | `20` | Session seed for expected complete-bar length. |
| `initialBuyProbability` | `[0,1]` | `0.60` | Session seed for the expected fraction of positive signs. |
| `alphaTicks` | `(0,1]` | `0.20` | One-step EMA coefficient for complete-bar length. |
| `alphaBuyProbability` | `(0,1]` | `0.20` | One-step EMA coefficient for positive-sign fraction. |
| `thresholdFloorTicks` | ticks, `> 0` | `4` | Final absolute minimum threshold. A small multiplier cannot reduce the threshold below it. |
| `thresholdMultiplier` | dimensionless, `> 0` | `1` | Scenario multiplier applied to the adaptive component before the final floor. |

The numerical seeds and EMA coefficients are reproducible teaching choices, not universal constants or calibrated recommendations.

## Tick-sign rule

For session-local prices $p_t$, define the sign $b_t$ as

$$
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}
$$

The first session trade uses `initialTickSign`. A flat price therefore contributes the preceding nonzero package sign; it does not contribute zero. Price and sign carry across a bar boundary because the trade stream is continuous, but both reset at a session boundary.

The tick rule is an inference from price changes, not observed aggressor identity. [Lee and Ready](https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1540-6261.1991.tb02683.x) discuss the tick test and its limitations; the package does not relabel `+1` as proven buyer initiation.

## Dominant-count stopping rule

For the open bar after $T$ trades, count positive and negative signs:

$$
N_T^+ = \sum_{t=1}^{T}\mathbf{1}[b_t=+1],
\qquad
N_T^- = \sum_{t=1}^{T}\mathbf{1}[b_t=-1].
$$

At the bar’s start, freeze the previously estimated length $\widehat E_0[T]$ and positive-sign probability $\widehat p_0^+$. The package threshold is

$$
h = \max\!\left(
h_{\min},
s\,\widehat E_0[T]\max(\widehat p_0^+,1-\widehat p_0^+)
\right).
$$

Here, $h_{\min}$ is the final floor in ticks and $s$ is the multiplier. The bar closes on the first trade for which

$$
\max(N_T^+,N_T^-) \ge h.
$$

This is a dominant cumulative-count rule. Tick-Imbalance Bars instead compare $|N_T^+-N_T^-|$ with an imbalance threshold. A “longest consecutive streak” algorithm would be a third, different method.

The threshold stays frozen within the bar. Equality closes. The crossing trade belongs wholly to the closing bar. Because the state increments by one and $h$ may be fractional, complete-bar overshoot is

$$
o = \max(N_T^+,N_T^-)-h,
\qquad 0 \le o < 1.
$$

## Post-close expectation update

Only a threshold-complete bar updates expectations. If it contains $T_k$ trades and $N_k^+$ positive signs:

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

$$
\widehat p_{k+1}^+
=(1-\alpha_p)\widehat p_k^+ + \alpha_p\frac{N_k^+}{T_k}.
$$

The next bar freezes its threshold after these updates. A `session_end` or `stream_end` partial does not update either EMA. At a new session, prices, sign, expectations, and open-bar accumulators all return to the disclosed seeds.

## Worked example

Use expected length `4`, expected positive probability `0.625`, floor `2`, multiplier `1`, and EMA coefficients `0.5`. The first threshold is

$$
h_1=\max(2,4\times0.625)=2.5\text{ ticks}.
$$

| Trade | Price | Sign reason | $b_t$ | $N^+$ | $N^-$ | Frozen $h$ | Result |
|---|---:|---|---:|---:|---:|---:|---|
| W01 | 100.00 | session seed | +1 | 1 | 0 | 2.50 | open |
| W02 | 100.00 | flat carries +1 | +1 | 2 | 0 | 2.50 | open |
| W03 | 99.90 | downtick | -1 | 2 | 1 | 2.50 | open |
| W04 | 100.00 | uptick | +1 | 3 | 1 | 2.50 | close; overshoot 0.50 |

The first bar has length four and positive fraction $3/4$. Expected length stays `4`; expected positive probability becomes `0.6875`. Thus $h_2=4\times0.6875=2.75$.

W05 is flat versus W04 and carries `+1` **across the bar boundary**. W06 is `-1`, W07 is flat and carries `-1`, and W08 is `-1`. The second bar closes with $(N^+,N^-)=(1,3)$ and overshoot `0.25`.

The exact records and expected boundaries are in [`examples/worked-example.json`](./examples/worked-example.json).

## Algorithm

```text
validate config, schema, ordering, identity, symbol, currency, and sessions
initialize session price/sign seeds and adaptive expectation seeds
freeze threshold = max(final floor, multiplier × E[T] × max(p+, 1-p+))
for each trade:
    if session changed:
        optionally emit the old partial without updating expectations
        reset price, sign, expectations, and open-bar state
    assign +1/-1 from price change; flat carries the previous sign
    append the whole trade; update OHLCV and the matching side count
    if max(positive count, negative count) >= frozen threshold:
        emit a complete bar with lineage, threshold, side counts, and overshoot
        update both EMAs from the completed bar
        begin a new bar and freeze its threshold
optionally emit the final partial without updating expectations
```

## Output contract

Alongside OHLCV, timestamps, tick count, and first/last trade IDs, every bar exposes:

| Field | Meaning |
|---|---|
| `buyTicks`, `sellTicks` | Cumulative positive/negative tick-rule counts. Names are conventional; they do not prove aggressor identity. |
| `dominantCount`, `dominantSide` | Larger count and label `buy`, `sell`, or `tie`. |
| `thresholdTicks` | Threshold frozen before the bar’s first trade. |
| `overshootTicks` | Amount above the threshold for complete bars; zero for partials. |
| `frozenExpectedTicks` | Expected length used at the bar start. |
| `frozenBuyProbability` | Expected positive-sign probability used at the bar start. |
| `thresholdMet`, `isComplete` | True only for a threshold closure. |
| `closeReason` | `threshold`, `session_end`, or `stream_end`. |

Every input record belongs to at most one output bar. With `closePartial=true`, total output `tickCount` equals the number of valid input trades.

## Edge cases and validation

- A threshold of `3` closes when the dominant count equals `3`; the operator is `>=`.
- Alternating `+,-,+,-,+` closes a threshold-three run bar at trade five even though no same-sign streak is longer than one.
- Flat prices carry the prior sign across bar boundaries; they use the seed at session start.
- The final floor remains binding even when `thresholdMultiplier < 1`.
- The crossing trade is indivisible and cannot appear in the next bar.
- Partial tails never update expectations. Dropping them is explicit data loss under `closePartial=false`.
- Nonfinite/nonpositive values, invalid probabilities/alphas, duplicate IDs, reversed order, ambiguous ties, mixed symbol/currency, or reappearing sessions reject before construction.

The 240-trade synthetic fixture covers two sessions, flats, alternating signs, dominant regimes, boundary equality, and partial tails. It produces 14 bars and is identical in Python and TypeScript. Tests distinguish definition-level correctness from economic usefulness.

## Limitations and misuse risks

- Tick signs infer direction from price changes; they are not direct observations of who initiated a trade.
- Corrections, cancellations, late reports, duplicate prints, and sale-condition eligibility belong upstream and can change membership.
- Adaptive bars are path-dependent: ordering, seeds, EMA coefficients, session policy, floor, and multiplier matter.
- Changing the sampling clock can alter downstream distributions and comparison sets; benchmark this separately.
- Synthetic data test mechanics, not market realism, statistical advantage, or predictive power.
- Bar OHLCV cannot reconstruct the original event order.
- A real historical membership example is deferred because daily aggregates cannot prove tick signs, adaptive state, or the exact crossing trade. A redistributable, sequence-aware transaction tape and documented cleaning rules are required.

## Visual explanations and implementations

The [construction flow](./visuals/mermaid/construction-flow.md) separates session reset, sign assignment, frozen decision state, and post-close update. [Run versus streak](./visuals/static/construction-anatomy.svg) shows the terminology trap. [Frozen threshold and state](./visuals/static/boundary-and-state.svg) traces the eight-trade example. The [guided playground](./visuals/animated/playground.html) provides canonical, alternating, flat-carry, and session-reset scenarios with deterministic Back/Step/Play/Pause/Reset controls.

- [Python implementation](./implementations/python/bars.py)
- [TypeScript implementation](./implementations/typescript/bars.ts)
- [Shared parity fixture](./tests/shared_fixture.json)
- [Synthetic dataset note](./datasets/DATASET.md)

Both reference implementations are $O(n)$ in trades and retain the current bar’s records for readability. Production implementations may keep scalar accumulators, but must preserve membership, lineage, and validation semantics.

Related topics: **Tick-Imbalance Bars** (net signed count), **Volume-Imbalance Bars** (net signed shares), **Volume Bars** (gross shares), and **OHLC Consistency Validator** (downstream invariants).

## References

See [`REFERENCES.md`](./REFERENCES.md) for source roles, versions, evidence classifications, licensing, and the historical-example decision.
