# D01-F01-A02 — Tick Bars

> **Domain:** D01 — Market Data Engineering  
> **Family:** D01-F01 — Bar Construction  
> **Status:** Video-ready; implementations, fixtures, tests, and visuals validated

## Executive summary

A tick bar closes after exactly `N` eligible trade records. It normalizes the number of accepted events per complete bar, not clock time, share volume, liquidity, or information content. Each accepted record increments the open bar's count by one; when the count equals `targetTicks`, that record becomes the close of the completed bar and the next accepted record opens a new one.

This package makes the hidden production choices explicit: eligibility is resolved upstream, input array order is authoritative, equal timestamps retain source sequence order, identifiers are unique after correction processing, sessions never share state, and a session or stream tail is either emitted or dropped by configuration. The output carries OHLCV, exact reported notional, closure reason, and first/last trade lineage.

Construction correctness is not evidence of predictive value or investment usefulness.

## Problem and learning contract

Trade events arrive irregularly. Clock-time bars therefore contain different numbers of accepted executions. Tick bars answer one narrower question:

> Has the current session-local bar accumulated exactly the configured number of eligible trade records?

After this topic, a reader can:

- specify the eligible-trade and ordering contract before counting;
- calculate complete and partial tick bars by hand;
- implement identical session, equality, and tail policies in Python and TypeScript;
- distinguish execution time from source sequence and ingestion time;
- test duplicates, corrections, session boundaries, non-finite inputs, and partial bars;
- explain what uniform trade count does—and does not—make comparable.

Prerequisites are trade records, OHLCV aggregation, and exchange sessions. **Time Bars** is the recommended neighboring tutorial.

## Definitions and scope

| Term | Package definition |
|---|---|
| Eligible trade | One execution record retained by an upstream, documented venue and sale-condition policy. |
| Tick | One accepted trade record. It is not a price change and not one share. |
| Complete bar | A bar containing exactly `targetTicks` accepted records. |
| Partial bar | A non-empty session or stream tail containing fewer than `targetTicks` records. |
| Session | An explicit state-partition key supplied by the caller; it is not inferred from a UTC date. |
| Authoritative order | Input array order after the source timestamp and sequence policy has been applied. |

The canonical algorithm counts every accepted record exactly once and never carries records across a session boundary. It does not decide which sale conditions are eligible, repair corrections, infer sessions, split records, sort tied timestamps, or combine instruments.

### Canonical policies and alternatives

| Decision | Canonical policy | Alternative | Consequence |
|---|---|---|---|
| Threshold | Close when count equals positive integer `N` | None within fixed-count tick bars | There is no overshoot: the count rises by one. |
| Crossing record | Belongs to the closing bar | Not applicable | The next accepted record starts the next bar. |
| Session boundary | Flush a partial bar if `closePartial=true`; otherwise drop it; then reset | Carry across sessions | Carrying changes membership and is outside this package. |
| Stream end | Same configured partial policy | Store open state for streaming continuation | A production stream may checkpoint instead of finalizing. |
| Equal timestamps | Preserve already-sequenced input order | Reject ties or use another venue sequence | Timestamp alone may not fully order events. |
| Odd lots | Count if the upstream eligibility policy admits them | Exclude under a documented policy | Bar membership and duration change. |

## Input and configuration contract

| Field | Type | Nullable | Unit or convention | Validation |
|---|---|:---:|---|---|
| `tradeId` | string | No | Post-repair event identifier | Non-empty and globally unique per call. |
| `timestamp` | string | No | ISO-8601 execution/event time normalized to UTC and ending in `Z` | Valid and globally nondecreasing. |
| `session` | string | No | Caller-defined session key | Non-empty; each session occupies one contiguous input block. |
| `symbol` | string | No | Instrument identifier | Exactly one non-empty symbol per call. |
| `price` | number | No | USD per share in the fixture | Finite and strictly positive. |
| `volume` | number | No | Reported eligible quantity in shares | Finite and strictly positive; it does not affect the tick threshold. |
| `currency` | string | No | `USD` in the fixture | Exactly one non-empty currency per call. |

Configuration:

| Field | Type | Default | Meaning |
|---|---|---:|---|
| `targetTicks` | positive integer | Required | Exact accepted-record count in every complete bar. |
| `closePartial` | boolean | `true` | Emit non-empty session and stream tails when true; discard them when false. |

Input order is part of the data contract. The implementation rejects a decreasing timestamp but deliberately does not sort. If two events have the same timestamp, their existing array order must already follow the source's sequence convention. NYSE Daily TAQ, for example, specifies both timestamps and sequence-number fields and notes that timestamp precision has changed historically ([NYSE Daily TAQ v4.3, sections 1.5.3–1.5.5](https://www.nyse.com/publicdocs/nyse/data/Daily_TAQ_Client_Spec_v4.3.pdf)). A production schema should preserve that source sequence even though this teaching function receives only the resolved order.

### Eligibility, duplicate, and correction boundary

| Incoming condition | This constructor's behavior | Required upstream action |
|---|---|---|
| Exact duplicate `tradeId` | Reject | Deduplicate and retain an audit record. |
| Cancel or correction message | Not interpreted | Resolve the original economic execution according to the source protocol before construction. |
| Ineligible sale condition | Not interpreted | Apply a documented market/source-specific filter. |
| Odd-lot execution | Count once if supplied | Decide eligibility upstream; quantity size does not change the count. |
| Late record that reverses event time | Reject | Rebuild from the affected point or use a versioned correction pipeline. |
| Same timestamp | Preserve input order | Tie-break with source sequence before calling. |

FINRA's reporting guidance treats execution time, quantity, and cancel/correction reporting as distinct concerns, which is why a generic bar constructor should not guess their repair semantics ([FINRA Trade Reporting FAQ](https://www.finra.org/filing-reporting/market-transparency-reporting/trade-reporting-faq)).

## Output contract

| Field | Meaning | Invariant |
|---|---|---|
| `barIndex` | Zero-based output order | Consecutive from zero. |
| `session` | State partition of all members | One value per bar; no bar crosses a session. |
| `startTime`, `endTime` | First and last contributing execution timestamps | `startTime <= endTime`. |
| `lastTradeTime` | Explicit last contributing execution time | Equals `endTime` for tick bars. |
| `open`, `high`, `low`, `close` | First, maximum, minimum, and last member price | `low <= open, close <= high`. |
| `volume` | Sum of member quantities | Positive. |
| `dollarValue` | Sum of unrounded `price × volume` | Positive USD in the fixture. |
| `tickCount` | Number of member records | Equals `targetTicks` for `threshold`; smaller for a partial closure. |
| `firstTradeId`, `lastTradeId` | Membership lineage | Match the first and last input members. |
| `closeReason` | `threshold`, `session_end`, or `stream_end` | Explains why the bar became output. |

## Mathematical formulation

For the open bar `B_j`, after admitting record `i`, define:

$$
T_j = \sum_{i \in B_j} 1.
$$

Close the bar when:

$$
T_j = N,
$$

where `N` is the positive integer `targetTicks`. Because each accepted record adds exactly one, `T_j` cannot jump over `N`. Using `>=` gives the same result only when the state and parameter are valid, but equality states the fixed-count rule more precisely.

Once membership is known, for ordered members `(p_i, v_i)`:

$$
O=p_1,\quad H=\max_i p_i,\quad L=\min_i p_i,\quad C=p_T,
$$

$$
V=\sum_i v_i,\qquad D=\sum_i p_i v_i.
$$

`D` is an exact sum over records before the reference output is rounded to eight decimal places; it is not `average price × total volume`.

## Worked numerical example

The synthetic worked example uses `targetTicks=3` and `closePartial=true`:

| Trade | Price | Shares | Running count | Decision |
|---|---:|---:|---:|---|
| E01 | 100.00 | 10 | 1 | Keep open |
| E02 | 101.00 | 20 | 2 | Keep open |
| E03 | 99.00 | 15 | 3 | Close bar 0 |
| E04 | 100.00 | 25 | 1 | Open bar 1 |
| E05 | 102.00 | 10 | 2 | Keep open |
| E06 | 101.00 | 30 | 3 | Close bar 1 |
| E07 | 103.00 | 5 | 1 | Emit partial at stream end |

Results:

| Bar | Members | O/H/L/C | Volume | Dollar value | Reason |
|---:|---|---|---:|---:|---|
| 0 | E01–E03 | 100 / 101 / 99 / 99 | 45 | 4,505 | `threshold` |
| 1 | E04–E06 | 100 / 102 / 100 / 101 | 65 | 6,550 | `threshold` |
| 2 | E07 | 103 / 103 / 103 / 103 | 5 | 515 | `stream_end` |

If `closePartial=false`, only bars 0 and 1 are returned. The independently executable record is [`examples/worked-example.json`](./examples/worked-example.json).

## Algorithm and causality

```text
validate targetTicks and closePartial
validate the already-cleaned, authoritative-order trade stream
current = empty
for each trade in input order:
    if the session changed:
        emit or discard current according to closePartial
        current = empty
    append the whole trade to current
    if count(current) == targetTicks:
        emit current with closeReason = threshold
        current = empty
at stream end:
    emit or discard current according to closePartial
```

Membership is causal with respect to the supplied stream: bar `j` uses only records admitted through its closing trade. A later correction can require a versioned rebuild, but the constructor does not silently rewrite already emitted history. Execution time is used for validation and output bounds; ingestion and availability timestamps are not present and must be modeled separately in a live system.

Complexity is `O(n)` time. This readable implementation retains the current bar's records, so working memory is `O(N)`. A production implementation can keep scalar OHLCV accumulators while preserving the same lineage and closure semantics.

## Visual explanations and playground

The [construction anatomy](./visuals/static/construction-anatomy.svg) maps the seven worked-example trades to two complete bars and one explicit tail. The [boundary and reset visual](./visuals/static/boundary-and-state.svg) separates exact equality, session-tail policy, and state reset. The [causal flow](./visuals/mermaid/construction-flow.md) shows the one-way state transition.

The self-contained [interactive playground](./visuals/animated/playground.html) uses 240 labeled synthetic trades across two sessions. It starts with a complete canonical bar, supports back/step/play/pause/reset, changes `targetTicks`, filters sessions, exposes closure lineage, and emits the final partial according to the canonical policy. It uses synthetic data because the official NYSE specification describes licensed historical TAQ access and product agreements; this repository does not redistribute that proprietary trade stream ([NYSE Daily TAQ v4.3, preface and section 1](https://www.nyse.com/publicdocs/nyse/data/Daily_TAQ_Client_Spec_v4.3.pdf)).

## Implementation and validation

- Python: [`implementations/python/bars.py`](./implementations/python/bars.py)
- TypeScript: [`implementations/typescript/bars.ts`](./implementations/typescript/bars.ts)
- Shared 240-trade fixture: [`tests/shared_fixture.json`](./tests/shared_fixture.json)
- Synthetic CSV: [`datasets/synthetic-trades.csv`](./datasets/synthetic-trades.csv)

Tests cover:

- exact Python/TypeScript parity on 20 canonical 12-tick bars;
- the hand-worked 3-tick example and both partial-tail choices;
- equality and all-trade conservation;
- empty input, invalid target types, non-finite values, and reversed time;
- duplicate identifiers, mixed instruments, and noncontiguous sessions;
- equal-timestamp stable ordering;
- session-tail closure and full state reset.

## Limitations and misuse risks

- Uniform trade count does not imply uniform elapsed time, volume, dollar value, liquidity, volatility, or information.
- One tiny eligible execution and one large eligible execution each count as one tick.
- Results depend on source-specific eligibility, correction, sequencing, and session policies.
- Historical timestamp precision and feed conventions can change; preserve source metadata and schema versions.
- Floating-point arithmetic is adequate for the synthetic parity fixture, but accounting or ledger-grade systems should use an explicit decimal representation.
- The synthetic regimes test mechanics and boundaries, not market realism, economic significance, or predictive power.
- A completed bar is a sampling output, not a trading signal.

## Related catalog topics

- `D01-F01-A01` — Time Bars: fixes elapsed clock time.
- `D01-F01-A03` — Volume Bars: fixes accumulated reported quantity.
- `D01-F01-A04` — Dollar Bars: fixes accumulated reported notional.
- `D01-F01-A05` — Tick-Imbalance Bars: uses signed adaptive state rather than fixed count.
- `D01-F02-A01` — OHLC Consistency Validator: validates constructed-bar invariants.

## References

See [`REFERENCES.md`](./REFERENCES.md) for source roles, dates, jurisdictions, applicability, and redistribution limitations.
