Lee-Ready tick-rule trade classification, validated against exchange ground truth, plus alternative bar sampling (time, tick, volume, dollar) on real Binance BTCUSDT data.
Every trade has two sides: someone buys, someone sells. But only one side is in a hurry, the one that crosses the spread to get the trade done right now. That side is the aggressor, and it is the side that actually moves the price.
Exchanges usually do not tell you who the aggressor was. The trick: if the price ticked up, the buyer was probably in a hurry (they bought at whatever price was offered). If it ticked down, the seller was. That is the whole rule, and it gets it right 97.6% of the time. We know because Binance secretly stamps the answer on every single trade, so we can check.
Second idea: how do you chop one day of trades into chunks (bars)? Like slicing a pizza: same pizza, different cuts. Cut by the clock: every minute gets a slice, even the quiet ones. Cut by money: every slice is the same dollar amount, so busy hours get more slices. The cut changes what the data looks like. Clock-cut returns look like a spiky hedgehog (kurtosis 8.37). Money-cut returns look almost like a bell curve. Models trained on one cut behave differently on the other, so choosing the cut is a modeling decision you make before any model exists.
Market microstructure (Jansen, Machine Learning for Trading, ch03) says price data is not a neutral series: every trade is an economic event with an aggressor. Two skills matter before any ML on market data:
- Trade classification: knowing whether a trade was buyer- or seller-initiated, which you usually have to infer because venues rarely stamp the side.
- Sampling design: the bar type you choose changes the statistical properties of your training data (and therefore your features and labels).
Binance does stamp the aggressor side (is_buyer_maker), so this repo lets you implement the inference and check it against the truth.
- Downloads one day of BTCUSDT aggTrades from data.binance.vision (free, no API key).
- Classifies every trade with the tick rule: price up -> buy, price down -> sell, flat -> carry previous.
- Validates against Binance's own flag and prints accuracy + confusion matrix.
- Builds time, tick, volume and dollar bars, then compares return distributions (kurtosis, autocorrelation, skew).
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# 1. fetch one day of data (~60-120 MB)
.venv/bin/python fetch_data.py --date 2026-08-10
# 2. classify + validate + compare bar types
.venv/bin/python analyze.py
# 3. regenerate the storytelling charts (charts/*.png, Plotly dark theme;
# interactive charts/*.html are also written locally but not committed)
.venv/bin/python plots.pyTunable: --tick-size, --volume-size (BTC), --dollar-size (USD), --time-size.
530,873 aggTrades, full UTC day (00:00:00.012 -> 23:59:59.715).
Trade classification
| Metric | Value |
|---|---|
| Tick-rule accuracy vs exchange ground truth | 97.62% |
| True sells classified correctly | 235,560 / 239,798 |
| True buys classified correctly | 282,694 / 291,074 |
Errors concentrate at flat (zero-tick) prints and at large prints that cross several price levels; the tick rule can only see the last tick, not the quote book.
Bar return statistics (normal distribution -> kurtosis 0, autocorr 0)
| Bar type | Bars | Std ret (bps) | Skew | Kurtosis | Autocorr(1) |
|---|---|---|---|---|---|
| time (1 min) | 1,440 | 3.49 | -0.107 | 8.37 | 0.035 |
| tick (1,000 trades) | 531 | 5.81 | 0.042 | -0.41 | 0.119 |
| volume (100 BTC) | 137 | 12.66 | 0.055 | 0.06 | -0.223 |
| dollar ($5M) | 177 | 10.65 | 0.624 | 1.45 | -0.123 |
Time bars have fat tails (kurtosis 8.4x normal). Sampling on activity (volume/dollar bars) flattens the distribution toward i.i.d. normal, which is why model features built on time bars behave differently from features built on dollar bars.
Reproduce: python fetch_data.py --date 2026-08-10 && python analyze.py
530,873 prints in 24 hours. Volume is not uniform: it concentrates in the US/Europe overlap (13-17 UTC peak) and thins out in the late evening (21-22 UTC trough). A "minute" at 14:00 UTC and a "minute" at 22:00 UTC are different things.
97.62% overall accuracy. The error rate by price move size is U-shaped: 0.11% for exactly 1-tick moves (direction is obvious), rising to 5.69% at flat prints (the rule must guess by carry-forward) and 7.11% for prints >100 ticks (big sweeps where one aggregate can bundle opposite-side fills).
Time bars have fat tails (excess kurtosis 8.37, about 8x a normal distribution). Activity-based bars approach normal: volume 0.06, dollar 1.45. Models that assume i.i.d. samples see very different data depending on the bar type you hand them.
Time bars mint exactly 60/hour, always. Dollar bars mint when money moves: bursts during active hours, almost nothing in dead ones. Same day, two datasets with different information content per row.
- The tick rule recovers the aggressor side at 97.62% against the exchange's own flag; errors concentrate at flat prints and >100-tick sweeps (story 2).
- Bar type is a modeling decision, not bookkeeping: kurtosis drops from 8.37 (time) to 0.06 (volume) and 1.45 (dollar) (story 3).
- Dollar bars concentrate information in active hours, so each row carries a more comparable amount of information (story 4). Features and labels inherit whatever the sampling scheme produces.
MIT




