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.

21 stylized facts, all passing 41 unit tests ~400 lines of Amber GitHub →

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.

universe · microstructure Hawkes arrivals, SV+jumps amber_tick.generate --symbols 500 --rows 100M build_marts.py 145× smaller than the tape store/ splayed · one file per column -8! binary · ns since 2000-01-01 marts bars1m · symday venueday · mktminute taq.* python-amber notebook, NumPy views amber-arrow Arrow IPC, Flight amberd TCP qSQL :5012 Grafana 3 provisioned dashboards live pipeline feed tickerplant rdb store/ (EOD) writes the sequenced log before it publishes — replayable

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 store

What the generator produces

--symbols 500 --sessions 5 --rows 100M takes about four minutes on two cores and writes roughly 10 GB.

TableRowsWhat it is
trades~38 Mevery print: price, size, aggressor side, venue, sale condition, sequence, exchange timestamp
quotes~89 Mthe NBBO: bid, ask, sizes, the venue on each side, lock/cross flags
depth~5.8 Mconsolidated book snapshots, ten levels a side, every ten seconds
halts~120LULD trading halts, with resume times
master/*smallsecurities, 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:

python tests/validate.py --store store
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 failed

Those come out of the model rather than being imposed on it. The generation order is what makes that work:

1

A market factor

Per second, that every name loads on through its beta, so the cross-section is correlated rather than independent.

2

An efficient price per symbol

A stochastic-volatility diffusion with jumps, scaled by the intraday volatility curve. Never observed directly.

3

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.

4

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 IPC

Splayed, 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

FileWhat it is
q/tick.kloads the store: partitions, columns, enumeration, attributes
q/taq.ktrade-and-quote analytics — where the substance is
q/grafana.kthe query surface the dashboards call, one function per panel
q/amber-tick.kloads 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 estimators
The as-of join is the load-bearing operation

And 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
ProcessWhat it does
tickerplantAssigns 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.
rdbKeeps 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.
feedReplays a stored partition, either as fast as the socket will take it or on the tape's own clock compressed by --speed.
gatewayRoutes 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