amber-tick
A complete tick architecture on the Amber engine: a realistic US-equity market simulator, a partitioned column store the engine reads natively, trade-and-quote analytics, a live tickerplant / RDB / HDB pipeline, a Jupyter notebook, and provisioned Grafana dashboards.
Nothing here modifies the engine. amber-tick consumes Amber the way any application
should — through libamber.so, the src/ext.h C API seam, and
amberd's TCP port.
Quick start
git clone https://github.com/BonucciAndrea/amber
git clone https://github.com/BonucciAndrea/python-amber
git clone https://github.com/BonucciAndrea/amber-arrow
git clone https://github.com/BonucciAndrea/amber-tick
# 1. the engine, as a shared library
(cd amber && chmod +x build.sh a && ./build.sh --shared)
# 2. the Python bindings and the Arrow bridge
python -m venv .venv && . .venv/bin/activate
AMBER_SRC=$PWD/amber pip install ./python-amber ./amber-arrow/python
pip install numpy pyarrow pandas matplotlib jupyter pytest
(cd amber-arrow && ./build.sh --amberd)
# 3. a tick store
cd amber-tick
python -m amber_tick.generate --out store --symbols 500 --sessions 5 --rows 100M
python scripts/build_marts.py --store store --qhome q
# 4. check it looks like a market
python tests/validate.py --store storeWhat the generator produces
--symbols 500 --sessions 5 --rows 100M takes about four minutes on two cores and
writes roughly 10 GB.
| Table | Rows | What it is |
|---|---|---|
trades | ~38 M | every print: price, size, aggressor side, venue, sale condition, sequence, exchange timestamp |
quotes | ~89 M | the NBBO: bid, ask, sizes, the venue on each side, lock/cross flags |
depth | ~5.8 M | consolidated book snapshots, ten levels a side, every ten seconds |
halts | ~120 | LULD trading halts, with resume times |
master/* | small | securities, venues, sale conditions, sessions, scheduled earnings |
The realism is the point
tests/validate.py checks twenty-one published stylized facts on every
generated store, and all twenty-one pass:
PASS continuous prints lie inside the NBBO 99.998% of 8,874,103 prints
PASS effective spread <= quoted spread eff/quoted: mean 0.847, p99.9 1.000
PASS odd lots are ~half of prints 54.5% of prints
PASS odd lots are a small share of volume 5.9% of volume
PASS mean print is well above the median mean 250 vs median 92 shares
PASS volume is U-shaped first 30m 23.6%, midday 4.1%, last 30m 14.3%
PASS trade arrivals cluster (Fano > 1) variance/mean = 74.3
PASS tick returns are fat-tailed kurtosis 530
PASS tick returns mean-revert (bid-ask bounce) lag-1 autocorrelation -0.491
PASS off-exchange share is realistic 46.1% of volume prints to a TRF
PASS off-exchange improves price far more often TRF 61.0% vs lit 1.2%
...
21 passed, 0 warned, 0 failedThose come out of the model rather than being imposed on it. The generation order is what makes that work:
A market factor
Per second, that every name loads on through its beta, so the cross-section is correlated rather than independent.
An efficient price per symbol
A stochastic-volatility diffusion with jumps, scaled by the intraday volatility curve. Never observed directly.
Quotes first
The NBBO is a function of the efficient price and a spread state, quantised to the symbol's tick — a penny, a half penny under the sub-penny rule, or an oh-one for names under a dollar.
Trades second, priced off the standing quote
At the bid, at the ask, or inside it. This is why a print can never fall outside the NBBO, and why effective spread ≤ quoted spread is a property of the data rather than a constraint bolted on afterwards.
Arrivals are a discrete-time Hawkes process on a one-second grid, vectorised across symbols: one trade makes the next more likely for about the next twenty seconds, which is what produces bursts instead of flat Poisson noise. A second AR(1) state carries the latent order-flow bias, so signed volume autocorrelates the way real order flow does when large orders are worked in slices.
The universe itself is calibrated as a cross-section: market cap follows a power law in rank, volatility falls with size, spread falls with liquidity and then hits the tick floor, turnover sits near 0.6% of shares outstanding a day. The tickers are generated and any resemblance to a listed symbol is coincidence.
Scaling
--rows is a target, and the generator solves for the arrival intensity that hits it —
analytically, before drawing a single random number. --rows natural generates the full
tape the universe implies (about 260 M rows for 500 symbols over 5 sessions). Scaling changes the
message rate only; the cross-sectional distribution, the clustering, the intraday curves and
the trade-quote consistency are all invariant to it, and the generator says so on startup.
The store
store/
sym the symbol enumeration domain
par.txt the partition list
master/… securities, venues, conditions, sessions, events
2026.08.21/
trades/{.d,sym,time,px,sz,side,venue,cond,seq,extime}
quotes/{.d,sym,time,bid,ask,bsz,asz,bex,aex,cond}
depth/… halts/… bars1m/… symday/… venueday/… mktminute/…
arrow/2026.08.21/*.arrow the same data as Arrow IPCSplayed, one file per column
A query for px and sz reads two files and never
opens the other seven.
Each file is an Amber value
Four bytes of magic, a type tag, an attribute byte, a 64-bit count, then the
raw payload in native layout — the format -8! emits and -9! reads.
amber_tick/amberbin.py writes it directly from NumPy, and its output is
byte-identical to the engine's own for every type it emits.
Symbols are enumerated
sym is an int32 index into one store-wide file, not
a hundred million copies of four hundred distinct strings.
Time is ns since 2000-01-01
Amber's own timestamp epoch, not the Unix one. q/tick.k teaches
amber.k's grid formatter about it, without touching amber.k. See
Temporal mechanics.
The Amber library
| File | What it is |
|---|---|
q/tick.k | loads the store: partitions, columns, enumeration, attributes |
q/taq.k | trade-and-quote analytics — where the substance is |
q/grafana.k | the query surface the dashboards call, one function per panel |
q/amber-tick.k | loads all three in the right order |
Everything in q/taq.k takes and returns a plain Amber table:
taq.nbbo[t;q] / as-of join: every print gets the standing NBBO
taq.enrich[t;q] / the join plus mid, sign, effective spread, improvement
taq.sign[m] / Lee-Ready, with a tick-test fallback at the midpoint
taq.effbps / taq.pibps / execution quality in basis points
taq.realized / taq.impact / realized spread and price impact at a horizon
taq.sbars[w;m] / w-minute bars with the buy/sell split
taq.symstats / taq.venuestats
taq.lambda / taq.amihud / taq.roll / liquidity and impact estimatorsAnd the store is laid out to serve it: rows sorted by (sym, time), `s#
stamped on time so aj binary-searches instead of scanning.
The live pipeline
python -m amber_tick.runtime.tickerplant --port 5010 --log tplog --date 9729 &
python -m amber_tick.runtime.rdb --port 5010 --store store --qhome q &
python -m amber_tick.runtime.feed --store store --part 2026.08.21 --speed 60| Process | What it does |
|---|---|
tickerplant | Assigns the sequence numbers and is the only thing that decides what order events happened in. It writes the log before it publishes, so a subscriber can never see a message the log does not already contain — which is what makes recovery just a replay from the last acknowledged sequence number. Each subscriber gets a bounded queue and its own writer thread; a slow consumer is dropped rather than allowed to stall the tape. |
rdb | Keeps today's tape in NumPy and materialises it into the engine only when a query needs it — Amber tables are immutable values, so appending a batch at a time into the engine would be quadratic. At end of day it writes what it holds straight into a new partition and empties. |
feed | Replays a stored partition, either as fast as the socket will take it or on the tape's own clock compressed by --speed. |
gateway | Routes a query across the RDB and the historical partitions and folds the results, with the combine rule as an explicit parameter — because you cannot average five partitions' averages and get a VWAP. |
The log is not a log format. It is a file of Amber values, so replaying it, inspecting it, and querying it are operations you already know.
Dashboards
Three, provisioned — see the Grafana datasource page and the end-to-end tutorial.
- Market overview — consolidated notional per minute, the intraday volume curve, effective versus quoted spread, where the volume printed, and the day's most traded names.
- Symbol microstructure — one name: one-minute bars against VWAP, the buy/sell split, effective spread paid, and the ten-level book.
- Execution quality — effective spread and price improvement by venue, a venue league table, and the widest spreads by name.
Every panel is a one-line call into q/grafana.k, so any panel's query can be run in the
REPL and give the same answer. Panels read the marts, never the raw tape:
scripts/build_marts.py derives four small tables once (about 145×
smaller than the tape) using the same taq.* functions, because a dashboard refreshing
every ten seconds cannot re-run an as-of join over ten million prints.
Tests
AMBER_SRC=/path/to/amber python -m pytest tests/ -q # 41 passed
python tests/validate.py --store store # 21 passed