Building a 5-million-row in-memory tick store in 60 seconds
By the end of this you will have five million trades and ten million quotes in memory, joined as-of, bucketed into one-minute bars, and you will know exactly how much RAM it cost.
What you need
git clone https://github.com/BonucciAndrea/amber && cd amber
chmod +x a build.sh
AMBER_NATIVE=1 ./a # build machine-tuned, then open the REPLEverything below is typed at the amber> prompt.
Generate the session
gentq is in fin.k, auto-loaded at startup. It sets two globals:
trades and quotes, with realistic microstructure — an efficient price, a
spread state, and prints that sit inside the standing quote.
\ts gentq 50000002381 1258291200 / milliseconds, bytes\ts times an expression and reports the space it used. About 2.4 seconds and 1.2 GB
for five million trades and ten million quotes.
Look at what you got
count trades / 5000000
count quotes / 10000000
meta tradesc | t a
------|-----
sym | s
time | j
px | f
sz | iThe a column is empty — no attributes yet. That is the single most important thing to
fix, and it is the next step.
Sort and stamp the attributes
The as-of kernel binary-searches each symbol group's time slice. For that to be legal
the rows must be sorted by (sym, time) and time must carry the
`s attribute. xasc does both.
trades:xasc[`sym`time; trades]
quotes:xasc[`sym`time; quotes]
meta tradesc | t a
------|-----
sym | s p
time | j s
px | f
sz | isym picked up `p (parted — equal values are contiguous) and
time picked up `s (sorted). Together those are what make the join
O(log n) per row instead of O(n).
O(1) symbol slicing too?
Add the grouped attribute and the group index: `ga trades`sym, then
bysym / symrows / gidx from fin.k give you a
per-symbol row range without a scan.
The as-of join
Every trade gets the quote that was standing when it printed. This is TAQ, and it is the operation the whole layout exists to serve.
\ts m:aj[`sym`time; trades; quotes]
count m / 5000000
5#m704 402653184
sym time px sz bid ask
-------------------------------------------------
AAPL 34200000000000 187.31 100 187.30 187.32
AAPL 34200004120000 187.31 200 187.30 187.32
AAPL 34200011870000 187.32 50 187.31 187.33
AAPL 34200019330000 187.32 300 187.31 187.33
AAPL 34200026010000 187.31 100 187.30 187.33704 milliseconds for five million binary searches over a ten-million-row quote table. The kernel is
in src/a.c, marshalled from amber.k, and it runs off a thread-local 16 MB bump
arena so malloc jitter stays out of the path.
Microstructure columns
m:update mid:0.5*bid+ask from m
m:update side:tsign m from m / Lee-Ready: +1 buy, -1 sell
m:update eff:effspread m from m / 2 * |px - mid|
select n:#px, eff:avg eff, bps:10000*avg eff%mid by sym from msym | n eff bps
-----|---------------------------
AAPL | 1284203 0.0121 0.646
MSFT | 1091177 0.0184 0.447
GOOG | 918884 0.0102 0.722
...One-minute VWAP and OHLCV bars
The headline query, bare at the prompt:
\ts select vwap:wavg[sz;px] by time:1m xbar time from trades151 8388608
time vwap
---------------------
09:30:00.000 187.2841
09:31:00.000 187.3106
09:32:00.000 187.2955
09:33:00.000 187.4012
..
[390 rows x 2 cols]And the full OHLCV, spelled out with qby so you can see the shape:
tb: +@[+trades; ,`time; minbar[1]@] / snap time onto 1-minute buckets
b: qby[tb; `sym`time;
`o`h`l`c`v!({first x`px};{max x`px};{min x`px};{last x`px};{sum x`sz})]
5#bOr, if you just want the standard bars, fin.k has it in one call:
b:bars[1; trades] / 1-minute OHLCV
candle b / Unicode candlesticks, in colourMeasure the footprint
\v+-------------+---------------+----------------+---------+
| Name | Type | Shape / Length | Memory |
+-------------+---------------+----------------+---------+
| trades | Table | 5000000 x 4 | 120.0 MB|
| quotes | Table | 10000000 x 4 | 320.0 MB|
| m | Table | 5000000 x 8 | 280.0 MB|
| b | Table | 390 x 6 | 18.7 KB |
| ... | ... | ... | ... |
+-------------+---------------+----------------+---------+\v walks nested structures recursively, so a table's number is real rather than a
shallow guess. It also lists the REPL library's own internals (repl.*, PAL,
GB) — scan for the names you defined, or \d yourns first to narrow the
namespace.
Profile a single query
\trace aj[`sym`time; 100000#trades; quotes]+-------------------------------------------------------+
| Parse 454ns [ ] 0.1% |
| Arena 38ns [ ] 0.0% |
| Execute 14.1ms [■■■■■■■■■■■■■■■■■■ ] 91.2% |
| Format 1.4ms [■ ] 8.7% |
+-------------------------------------------------------+
| Total: 15.5ms Arena peak: 32 B |
+-------------------------------------------------------+Only expressions that reach an arena-backed kernel (aj, wj,
`csvr, \ast) report a non-zero arena peak. Arena peak is a
true high-water mark taken from arena_peak().
Persist it
Two options, depending on whether you want it human-readable or byte-exact.
/ portable Amber text — readable, version-independent, not memory-mapped
splay["/tmp/store/trades"; trades]
t2:dload "/tmp/store/trades"
/ compact binary — byte-exact including attributes, nulls and infinities
`:/tmp/trades.bin 1: -8!trades
t3:-9! read1 `:/tmp/trades.bin
t3~trades / 1bThe binary form is what amber-tick's partitioned
store writes, one file per column, so a query for px and sz reads two files
and never opens the other seven.
Where the 60 seconds went
| Step | ms | Notes |
|---|---|---|
gentq 5000000 | 2382 | 15M rows of realistic microstructure |
xasc × 2 | ~1900 | radix sort; sets `p and `s |
aj | 704 | 5M branch-free binary searches |
| 1-minute VWAP | 151 | group + weighted average on raw column vectors |
ema (50-period, tacit) | 0.1 | straight into the C kernel |
Scale it up
Nothing above changes at ten times the size except the numbers. For a store that outlives the session — 38 million prints, 89 million quotes, ten-level book snapshots, LULD halts, and twenty-one stylized facts checked on every generated store — use amber-tick:
python -m amber_tick.generate --out store --symbols 500 --sessions 5 --rows 100M
python tests/validate.py --store store # 21 stylized facts