Index

Matching and quoting

A deterministic continuous double auction, and the Avellaneda–Stoikov / GLFT closed forms that assume most of it away.

Posted 14 September 2026  ·  tape  ·  glft

SSRN

The problem

Market-making research fails in two opposite ways. One is a matching engine that is fast and wrong: Fill-or-Kill that leaks partials, cancels that leak pool slots, ids that do not round-trip, a sort that is not stable on timestamp ties. The other is a quote formula that is labeled “GLFT” and is not the paper — a reservation price with the wrong units, an inventory skew that does not depend on fill intensity, a simulator whose displayed quotes are not the executable ones.

Both failure modes produce pictures that look like research. Neither is usable as a building block. The question behind Tape and GLFT is narrower than “build a market maker.” It is: can the matching layer be made semantically boring, and can the quoting layer be made to match the papers it cites, including the places where those papers stop being a market.

Tape is a deterministic C++20 continuous double auction: price-time priority, a 1 000 000-slot order pool, an event-driven agent loop, Cont–Stoikov–Talreja synthetic flow, optional Binance WebSocket outside the core. It is not an exchange. GLFT is a Python package that implements the Avellaneda–Stoikov (2008) finite-horizon closed form, the Guéant–Lehalle–Fernandez-Tapia (2013) infinite-horizon approximation (Theorem 3, not the ODE), a censored exponential MLE for \(\lambda(\delta)=A e^{-k\delta}\), and a unit-lot Poisson simulator used only to test those formulas. It is not connected to Tape’s matching engine. The README of the quoting package says that in one sentence, on purpose.

How the two projects sit next to each other

They share a vocabulary — mid, inventory, fills, \(\sigma\), intensity — and almost no state. Tape can emit a print tape. GLFT can read that tape and compute a trade-time quadratic variation. That estimator is not a Brownian coefficient, and the package refuses to drop it into the Python market maker by default. Quote lives, which can identify \((A,k)\), are a different object: distance, exposure, filled-or-censored. Tape’s print fixture is 223 trades over 0.87 s of a CST run. It does not contain quote lives.

TAPE · C++20 CDA GLFT · PYTHON QUOTES agents / CST flow / gateway matching engine · price-time LOB · intrusive FIFO levels pool / reports / L1·L2·tape AS (2008) · GLFT Thm 3 (2013) censored MLE for (A, k) unit-lot Poisson simulator experiments A - F, paper numbers shared objects: print JSONL (σ̂ only) · quote-lifecycle JSONL ((A,k) only) · neither is a live quote path
Figure 1. Adjacent stacks. The dashed line is a file format, not a socket. GLFT does not post into Tape; Tape does not solve an HJB.

The conceptual link is still real. A quote formula is a map \((s,q)\mapsto(\delta^b,\delta^a)\). A matching engine is the thing that decides whether those depths ever trade, in what size, against whom, and after how much queue. AS/GLFT replace that entire object with an independent Poisson clock whose intensity depends only on distance to a Brownian mid. If you want to know what the formula assumes, you need both pictures: the engine you would eventually have to face, and the intensity model you are currently pretending is the engine.

Tape: a CDA you can audit

Public surface is include/tape/*.hpp. Templates live under detail/. tape_core is a static library; the live Binance client is a separate target and is not linked into the core. That split is the whole of Phase 2. Phases are documented in docs/PHASES.md and docs/PHASE1_SUMMARY.md.

The book is a pair of std::map<Price, PriceLevel> — bids ordered low→high so rbegin() is best bid, asks ordered so begin() is best ask — plus an unordered_map<OrderId, Order*> for cancel and modify. Level insert is \(O(\log N_{\text{levels}})\). Best price is an iterator. Cancel is a hash lookup plus an unlink. Price is int64_t ticks. There is no floating-point price on the match path.

A price level is an intrusive doubly-linked list threaded through the Order itself (prev/next). Append at the tail is \(O(1)\) and is how time priority is represented. Unlink from anywhere is \(O(1)\), which is the only acceptable complexity for a mid-queue cancel. Partial fills reduce leaves_qty on the head order and do not move it. That last sentence is the entire content of price-time priority after a partial, and it has a dedicated test.

Order is alignas(64), sizeof(Order) == 128 on the project’s GCC x86-64 build — two cache lines, pinned by a static assert and by test_invariants.cpp. An older comment said 96 bytes. The test kept the measured number.

PriceLevel::push_backC++
// PriceLevel: time priority is the list order.
void push_back(Order* o) noexcept {
    o->prev = tail_; o->next = nullptr;
    if (tail_) tail_->next = o; else head_ = o;
    tail_ = o;
    ++order_count_;
    total_qty_ += o->leaves_qty;
}

The matching engine owns the pool, not the book. That ownership split is easy to get wrong in comments and expensive to get wrong in code. The book unlinks; the engine destroys. Phase 1 found that cancelled resting orders and fully-filled makers never returned their slots. A probe of New/Cancel round-trips exhausted the 1M pool and threw std::bad_alloc. After the fix, the same probe completed 1 000 050 round trips. Both paths are regression tests in test_memory_lifecycle.cpp.

Event model

Three callbacks leave the engine: execution reports, trade prints, market-data updates. An MD update is a discriminated union — L1, L2 delta, trade, heartbeat, status. Sequence numbers are engine-global. The taker’s fill report carries a VWAP of that match as last_px, not the last maker’s price. New acks carry leaves_qty, side, and instrument, which is what lets an agent bind the engine-assigned id instead of the local counter it stamped on the request.

The engine is single-threaded and non-reentrant. Nested process() from inside a callback is pushed onto a pending FIFO and drained after the outer handle_* returns. The flag is RAII-cleared. The comment in the header is the bug report: a nested market can finish and destroy a maker the outer emit_trade loop still holds as a raw pointer. Queueing is the fix, not “don’t call back.”

ONE ORDERREQUEST gateway + risk process() gate pool construct FOK? can_fully_fill() match() walks FIFO levels emit_trade per maker taker VWAP report rest / IOC cancel / destroy publish L1 / L2 / tape drain pending_ FIFO SimClock is set by the event calendar, then the gateway adds fixed profile delays. FullLatencyModel (log-normal + M/M/1) is demonstrated by --exec-model and is not this loop.
Figure 2. One OrderRequest through Tape. FOK is decided on the untouched book. Nested work waits.

Execution semantics that actually matter

Price-time CDA, GTC / IOC / FOK / Post-Only / Market. Post-Only rejects if it would cross, before match. Market that does not exhaust the book is cancelled on the remainder and the taker object is destroyed. Modify is cancel-replace: the resting identity dies, a new order is allocated, priority is lost. That is a choice, not an accident, and test_cancel_modify.cpp treats the loss of priority as the spec.

FOK is the semantic bug that is easy to ship. The first implementation called match() — which consumes makers and emits trades — and cancelled the remainder if the taker was not fully filled. That is IOC. A resting sell of 5 lots and an incoming FOK buy of 10 printed 5 and killed 5. The fix is LimitOrderBook::can_fully_fill, a const walk that sums already-tracked level totals without touching queues, called before match(). The infeasible FOK now emits zero trades and leaves the book untouched. A multi-level feasible FOK still executes. Both are in test_order_types.cpp.

Quantity is conserved through partials. A partial does not reshuffle the remainder. Sweeping two consecutive bid levels in one match() is a known-answer test because that is the reverse-iterator erase path most likely to go off-by-one.

Helpers that look like the engine, and are not:

  • QueuePositionModel in execution.hpp computes \(P(\text{fill})=\max(0,V-Q_{\text{ahead}})/Q_{\text{order}}\). Deterministic. No hidden liquidity, no queue jumping, no priority reset on modify. The matching engine does not call it. The engine fills against resting size exactly.
  • Almgren–Chriss in the same header is square-root temporary impact, \(\Delta s=\sigma\,\mathrm{sign}(q)\sqrt{|q|/V}\). Permanent impact and the AC schedule are not implemented.
  • A Beta partial-fill helper exists and is unused by the engine.
  • Cancellation race is the two-horse closed form \(P(\text{cancel wins})=\lambda_c/(\lambda_c+\lambda_f)\), optionally Monte-Carlo’d with exponential clocks. It is not queue-position-aware.

Those models are documented in docs/MODELS.md with the same warning the quoting package uses: write down what the code computes, then write down what it is not.

Memory, queues, latency, event order

Pool

MemoryPool<T, PoolSize> is a fixed slab with an intrusive free list. Allocate and free are pointer swings. Exhaustion throws std::bad_alloc. Debug builds track in-use bits and abort on double-alloc. The quantitative reason for a pool is not “cache-friendly” as a slogan. It is that a long simulation with a realistic cancel rate has a tiny resting set and a huge lifetime order count. If destroy is missing on the cancel path, lifetime equals capacity, and capacity is 1M. If destroy is present, capacity equals the peak book.

Synthetic flow

Cont–Stoikov–Talreja style Poisson arrivals, pre-generated then stable_sorted by timestamp. Stable, because equal-nanosecond ties under std::sort are a latent determinism break. Intensities, as implemented:

  • limit at level \(k\): \(\lambda_{\text{limit}} e^{-\alpha k}\)
  • market: \(\mu\)
  • cancel: \(\theta \times \text{levels} \times 2\), with target id left at 0

The generator does not know live engine ids. The simulation loop binds a zero-id cancel to oldest_at_best(side) and copies that order’s client id, or drops the event if the touch is empty. A miss is still counted in total_cancels() as an attempt if it reaches the engine; a dropped bind never gets there. Mid used by the generator is not updated from the live book. Intensities do not react to spread. CST here is exogenous flow, not a state-dependent book. The README lists that as a known gap because it is one.

Two latency stories

The agent sim advances SimClock by the constants in GatewayLatencyProfile: 40 µs client→gateway, 2 µs gateway, 5 µs bus, 2 µs engine, 30 µs MD out. FullLatencyModel — log-normal network with mean 40 µs and cv 0.15, M/M/1 queue at 5×106 msg/s, 2 µs constant process — is implemented and demonstrated by --exec-model. It is not wired into --sim. Log-normal parameters are derived from requested mean and cv, not entered as raw \(\mu,\sigma\). Treating the two stories as one model is the mistake the docs exist to prevent.

Agents

Default population: three market makers, ten noise traders, two momentum agents, one latency arb. The MM is a tick-skew rule, not GLFT:

\[ b = m - h - q\cdot\kappa,\qquad a = m + h - q\cdot\kappa \]

with requote on a dirty flag from fills, or when mid moves by at least stale_threshold. Cancels use the engine id captured off the New ack. That binding was the Phase 1 agent-layer defect: local counters and engine ids only coincided in a one-agent vacuum. With two MMs the book filled with zombies. The current agent does not requote from inside on_exec_report; it sets a flag so the MD path can requote without re-entering process from a fill callback.

NoiseTrader samples an exponential interarrival at arrival_rate_per_sec. Momentum is a fast/slow EMA on trades with a 20-trade warm-up. LatencyArb latches L1 and snipes a trade that is through that quote by a threshold. It is a toy stale-quote rule, now present in the default population. Agent PnL is average-cost. Unrealized is position × (mark − average). No fees anywhere in Tape.

Benches

docs/BENCHMARKS.md specifies the command, the timed paths (LOB insert+cancel, engine process, pool construct/destroy, best bid/ask), the clock (Clock::now_mono(), not RDTSC), and the Release flags (-O3 -march=native -mtune=native -ffast-math -funroll-loops -DNDEBUG). It does not publish a number. p50 of ~0 ns on the pool and best-price benches is clock granularity, not a latency claim. The figures are machine-specific by construction. Re-run ./build/tape --bench --json locally; do not quote a foreign box as a regression.

What Tape’s tests pin down

The current tree has 86 Catch2 cases across twelve files, Debug+ASan+UBSan and Release in CI. Phase 1 locked matching and accounting before the header/library split. Determinism is tested at two layers, on purpose: generator-same-seed ⇒ byte-identical event stream; identical stream into two fresh engines ⇒ identical reports, trades, and book. An end-to-end --sim diff (timing lines stripped) is the CI check that the accounting did not drift.

What the suite actually covers, rather than “has tests”:

FileInvariant
test_invariantslayout 128/64, sentinels, order state
test_price_time_priorityFIFO inside a level; partials keep place
test_partial_fillsquantity conservation, level removal
test_order_typesMarket, IOC, FOK-before-match, Post-Only
test_cancel_modifyunknown id, filled id, modify loses priority
test_known_answer_scenariosmulti-level sweeps, demo replay
test_memory_lifecyclecancel and maker-fill return the slot
test_determinismgenerator and engine replay
test_queue_position_and_modelsclosed forms, race MC vs formula
test_sim_defectsCST live-id bind, noise rate, related wiring
test_configsim.cfg load

That is a correctness claim about a single-threaded CDA under its own event model. It is not a claim about FPGA matching, kernel-bypass, fees, hidden orders, or mid-queue priority after a modify-in-place — because modify-in-place does not exist.

GLFT: the quotes and the intensity

Reference price \(s_t=s_0+\sigma W_t\). Exponential intensity \(\lambda(\delta)=A e^{-k\delta}\) on each side, independent of \(W\). CARA parameter \(\gamma\ge 0\). Lot size 1. Units are written down in quotes.py because the first GLFT implementation in this repo added quantities that do not share a dimension.

SymbolRoleUnits
\(s,\delta\)mid, depthsprice
\(\sigma\)diffusionprice / √time
\(k\)intensity decay1 / price
\(A\)baseline intensity1 / time
\(\gamma\)CARA1 / price
\(q\)inventorylots

AS (2008), finite horizon

With \(\tau=T-t\):

\[ r = s - q\gamma\sigma^2\tau, \qquad \delta^a+\delta^b = \gamma\sigma^2\tau + \frac{2}{\gamma}\ln\Bigl(1+\frac{\gamma}{k}\Bigr), \qquad \text{bid, ask}=r\mp\tfrac12(\delta^a+\delta^b). \]

As \(\gamma\to 0\), \(r\to s\) and the spread \(\to 2/k\). The implementation uses log1p and allows \(\gamma=0\), which is the paper’s myopic sanity check and was previously rejected. \(A\) does not enter this approximation. That is the AS approximation, not a missing argument.

GLFT (2013) Theorem 3

The finite-horizon GLFT problem is an ODE system. This package implements the \(T\to\infty\) closed form, not the ODE.

\[ \xi=\sqrt{\frac{\sigma^2\gamma}{2kA}\Bigl(1+\frac{\gamma}{k}\Bigr)^{1+k/\gamma}}, \]

\[ \delta^b_\infty(q)\simeq\frac1\gamma\ln\Bigl(1+\frac\gamma k\Bigr)+\frac{2q+1}{2}\xi, \qquad \delta^a_\infty(q)\simeq\frac1\gamma\ln\Bigl(1+\frac\gamma k\Bigr)-\frac{2q-1}{2}\xi. \]

\(\xi\) has units of price. At \(q=0\) the two depths are \({\rm base}+\xi/2\). Long inventory widens the bid depth and tightens the ask — you want to sell. A depth \(\le 0\) withdraws that side. Flooring a negative depth at \(10^{-12}\) was the previous behaviour: the inventory-increasing side collapsed onto the mid, where \(\lambda\approx A\), which is the opposite of flattening.

The pre-fix formula was

\[ \delta_{\text{bid}}=\frac1\gamma\ln\Bigl(1+\frac\gamma k\Bigr)+\frac{\gamma\sigma^2}{2k}(1+2q) \]

with no \(A\). The second term has units price2/time. Adding it to a depth is legal only if the time unit is silently 1. Inventory skew did not depend on fill intensity, so the simulator was not testing GLFT. The audit is the pre-change record: docs/AUDIT.md.

At the paper point \((\sigma,\gamma,k,A)=(2,0.1,0.3,1.5)\), Experiment A measures \(\xi=1.185185\ldots\). Larger \(A\) tightens the \(q=0\) spread, which is the comparative static the missing-\(A\) formula could not produce.

Censored MLE

Observation \(i\): distance \(\delta_i\), exposure \(t_i\), filled \(f_i\in\{0,1\}\). Density if filled \(\lambda e^{-\lambda t}\); survival if censored \(e^{-\lambda t}\). Log-likelihood

\[ \ell=\sum_i \bigl(f_i\log\lambda_i-\lambda_i t_i\bigr),\qquad \lambda_i=A e^{-k\delta_i}. \]

Parameterised as \((\alpha,k)\) with \(\alpha=\log A\), multi-started L-BFGS-B. Observed information for \((\alpha,k)\)

\[ I=\sum_i \lambda_i t_i\begin{pmatrix}1&-\delta_i\\-\delta_i&\delta_i^2\end{pmatrix}. \]

Standard errors from \(I^{-1}\) when \(I\) is well-conditioned. If every \(\delta\) is equal, \(k\) is not identified: only \(\lambda\) at that depth is. The optimiser will still return a pair. Experiment E’s one-point grid produces \(\hat A=248,\hat k=2\) on the \(\lambda=A e^{-k\delta}\) ridge and flags identifiable=false. That flag is the result. The number is not.

The Poisson simulator

Event order in a step of width \(dt\), state \((s,q,\text{cash})\), documented in sim.py:

  1. Quotes from \((s,q)\) under AS or GLFT.
  2. Withdraw bid if \(q\ge q_{\max}\), ask if \(q\le -q_{\max}\).
  3. Independent Bernoulli fills, \(P=1-e^{-\lambda dt}\), at most one lot per side (truncated Poisson, lot size 1).
  4. Apply fills; drop a side if the pair would breach the cap.
  5. Optional adverse jump (default 0): after a bid fill, \(s\leftarrow s-J\); after an ask fill, \(s\leftarrow s+J\).
  6. \(s\leftarrow s+\sigma\sqrt{dt}\,Z\).
  7. Mark-to-mid \(\mathrm{pnl}=\mathrm{cash}+q s\).

With \(J=0\), \(E[\Delta s\mid\mathrm{fill}]=0\). That is the information structure the quotes were derived under. It is not a bug in the Euler scheme. It is why a positive mean PnL in this simulator is spread capture against uninformed flow, not evidence of a working market maker. Simultaneous two-sided fills occur with probability \(O(dt^2)\). Experiment C measures that term instead of hoping it is small.

Cash identity \(\mathrm{pnl}=\mathrm{cash}+q s\) is a test. Seed determinism is a test. Cap withdrawal — not merely refused fills while still displaying the forbidden quote — is a test. Quotes that are both live do not cross; they need not straddle the end-of-step mid, because the mid is recorded after the Brownian move.

Experiments A-F

All numbers below are from PYTHONPATH=src python -m glft experiment --out results, also written in results/RESULTS.md and summary.json. Seeds are explicit. They are mark-to-mid PnL in the price units of the Brownian motion, not a Sharpe ratio and not a market return.

A - paper identities

AS spread, reservation, and \(\gamma\to 0\) limit match an independent evaluation of the formulas. GLFT Theorem 3 depths match at \(q=0\) and \(q=1\). \(\xi=1.185185\ldots\) at \((2,0.1,0.3,1.5)\). AS half-spread at \(\tau=1\) is 3.0768; GLFT base depth is 2.8768. Raising \(A\) tightens the \(q=0\) GLFT spread.

B - sensitivity (20 seeds, \(T=8\), \(dt=0.05\))

Mean terminal PnL against the parameter being swept, other defaults held:

ParameterValuesMean PnL
\(\gamma\)0, 0.05, 0.1, 0.3, 0.827.92, 27.25, 23.77, 16.30, 4.17
\(\sigma\)0.5, 1, 2, 427.87, 27.02, 23.77, 16.66
\(k\)0.1, 0.3, 0.8, 1.574.44, 23.77, 9.34, 2.95
\(A\)0.3, 0.8, 1.5, 46.28, 13.82, 23.77, 68.45
\(q_{\max}\)2, 5, 10, 2023.39, 23.77, 23.77, 23.77
Mean terminal PnL versus gamma
Figure 3. Experiment B, \(\gamma\) sweep. Risk aversion taxes spread capture under zero adverse selection because it widens quotes and cuts inventory.
Mean terminal PnL versus sigma
Figure 4. Experiment B, \(\sigma\) sweep. Wider \(\sigma\) widens the spread through \(\gamma\sigma^2\tau\), so mean PnL falls the same way it does under the \(\gamma\) sweep.
Mean terminal PnL versus k
Figure 5. Experiment B, \(k\) sweep. Larger \(k\) means intensity dies faster off-touch, so posted depth earns less fill. Mean spread falls with \(k\) (6.94 at 0.3, 1.79 at 1.5) and mean PnL falls with it.
Mean terminal PnL versus A
Figure 6. Experiment B, \(A\) sweep. Higher baseline intensity is more turnover. Under \(J=0\) that is more uninformed spread capture, which is why mean PnL scales almost linearly with \(A\). It is also why this plot is not a capacity result.

Inventory cap, in this parameterisation, barely binds: mean absolute inventory on the default path is about 2 lots, so raising \(q_{\max}\) past 5 does nothing. That is a statement about these intensities, not about caps in general.

C - timestep (\(T=8\), 30 seeds)

dtmean PnLmean double-fillsmean turnover
0.2022.160.237.23
0.1024.020.137.80
0.0523.690.077.93
0.0223.570.007.73
0.0123.950.007.60
Timestep convergence of mean PnL and double fills
Figure 7. Experiment C. Mean PnL is stable for \(dt\le 0.1\). Double-fills vanish as \(dt\) shrinks, as they must for independent Bernoullis. The scheme is Euler; this table is the discretisation error, not a market.

D - Monte Carlo (GLFT, \(T=10\), \(dt=0.05\), 200 seeds)

StatisticValue
mean terminal PnL31.57
median30.69
std12.93
95% CI on the mean[29.78, 33.36]
5–95% range[8.97, 52.75]
mean max drawdown6.79
mean / std2.44
mean turnover (lots)10.15
One GLFT path: mid, quotes, inventory, mark-to-mid PnL
Figure 8. Seed 0, \(T=10\). Inventory stays inside a few lots. PnL steps up on fills and then diffuses with the mid. There is no adverse jump on this path.
Histogram of terminal mark-to-mid PnL across 200 seeds
Figure 9. Experiment D. The BM has no drift. The mass to the right of zero is the spread collected from uninformed Poisson flow.

E - parameter recovery

True \((A,k)=(1.5,0.3)\). One 4000-row fit: \(\hat A=1.43\) (SE 0.07), \(\hat k=0.288\) (SE 0.012), identifiable, 1356 fills. Forty replications at \(n=2000\): \(A\) mean 1.48 (std 0.093), \(k\) mean 0.297 (std 0.015), all identifiable. The one-point \(\delta=4\) grid is flagged. Recovery here is recovery of a simulator that generates from the same parametric family the estimator assumes. It is not recovery from a limit-order book.

F - where the quotes stop being optimal

Same 80 seeds, \(T=8\), quotes remain GLFT, the world changes.

Worldmeanstdmean max DDmean/std
base, jump 024.909.555.382.61
adverse jump 0.522.308.785.372.54
adverse jump 1.019.708.125.412.43
fee 0.5 / fill20.888.735.412.39
quotes \(\sigma=2\), world \(\sigma=8\)25.1324.9223.211.01

Adverse jumps reduce the mean. Fees, which are not in the HJB, reduce the mean by about the fee times turnover. Volatility misspecification — quotes computed as if \(\sigma=2\) while the Brownian uses 8 — barely moves the mean (the mid is still a martingale) and triples drawdown. mean/std falls from 2.61 to 1.01. That is the model breaking on purpose. Scaling both the quote rule and the BM together is a different world, not misspecification; the JSON records that case separately so it is not quoted as one.

What the implementation establishes

Established by tests and experiments.

  • Tape matches price-time CDA semantics for the order types it implements, including FOK-as-FOK, partials that keep queue place, and modify-as-cancel-replace.
  • The order pool does not leak on cancel or on maker fill, past the 1M-slot ceiling.
  • Same seed, same CST parameters ⇒ same generated stream; same stream ⇒ same engine accounting.
  • AS and GLFT Theorem 3 identities hold, including \(\gamma\to 0\) and the comparative static of \(A\) on \(\xi\).
  • The Euler–Bernoulli scheme’s double-fill artefact vanishes as \(dt\to 0\), and mean PnL is stable below \(dt=0.1\) in the default parameterisation.
  • The censored MLE recovers \((A,k)\) when the \(\delta\)-grid identifies them, and refuses to pretend when it does not.
  • Turning on adverse jumps or fees, or quoting the wrong \(\sigma\), moves the statistics in the direction the information structure predicts.

Not proved.

  • That GLFT quotes are a good market maker on a limit-order book. There is no book in the Python simulator.
  • That Tape’s default MM is AS/GLFT. It is a tick-skew rule around a seeded L1, and it often sits behind that seed spread.
  • That Kyle \(\lambda\) estimated on this flow is a usable impact coefficient. The README says it is not.
  • That Tape \(\hat\sigma\) from 223 clustered, integer-tick prints over 0.87 s is a calendar diffusion. It is trade-time quadratic variation.
  • That any throughput number in a README is portable. Tape does not ship one.
  • That \(\gamma\) can be read off PnL. It is not estimated.
  • That the infinite-horizon approximation is close to the finite-horizon ODE at the horizons used here. The ODE is not implemented, so the distance is not measured.