A three-tier, low-latency personal quant lab for crypto-derivatives, built in C++23 with the engineering discipline of a proprietary trading desk: deterministic, replayable, and ruthlessly tiered by latency budget. Deribit is the default venue; the feed and control adapters live behind a venue-neutral interface so another exchange can be swapped in without touching the hot loop.
Status: Phase 8 complete. The full build order is green: 127/127 offline tests (126 Catch2 + 1 Python bridge) plus a live testnet smoke that runs network → parse → tee → hot-path market-maker loop → binary event log → Prometheus metrics, with built-in
--smokegates (parse_errors=0,dropped=0, events processed, log written). See End-to-end smoke test below.
Every decision is justifiable in terms of latency, correctness, or maintainability, and when in doubt we keep the hot path simpler and purer. The system must be deterministic and replayable: the same event sequence produces identical output in live trading and in backtest. The IStrategy interface is therefore byte-for-byte identical across live and replay — no if (backtesting) branches, ever.
Source directories use hot, warm, and cold instead of the older tier1/tier2/tier3 names. The latency rules are unchanged.
| Path | Thread model | Latency budget | Rules |
|---|---|---|---|
hot (src/hot/) |
single thread, pinned to an isolated core | nanoseconds | zero heap alloc, zero mutex, zero syscall, zero exceptions (-fno-exceptions), __rdtsc() timing only, every struct alignas(64) |
warm (src/warm/) |
thread pool | milliseconds | reads hot path via SPSCRing; writes back only via atomic pointer swap (vol surface) or SPSCRing<Event> (risk events); may allocate/STL; never blocks the hot loop |
cold (src/cold/) |
dedicated threads | best-effort | logging, config hot-reload, backtest, metrics; reads position via Seqlock; never touches hot-path state directly |
All cross-path data flow is via SPSCRing or an atomic pointer swap. No shared mutable state, no locks across paths.
Exchange-specific code is isolated under src/venues/<name>/. Everything else — hot/, warm/, cold/, core/, strategies/ — is venue-neutral and consumes the shared Event type.
Venue-specific (swappable) Venue-neutral (unchanged)
venues/deribit/feed_parser hot/ (pinned event loop)
venues/deribit/control_api warm/ (SVI, risk, positions)
venues/<future>/... cold/ (log, config, backtest, metrics)
core/ (Event, SPSCRing, types)
strategies/
| Interface | Role |
|---|---|
venues::IFeedParser |
parse raw wire frames → Event, push into the hot inbound ring |
venues::IControlApi |
auth, heartbeat, subscribe (JSON-RPC for Deribit) |
venues::factory |
create_feed_parser(venue, …) / create_control_api(venue) from config.venue |
Set venue = "deribit" in config.toml (or add a new enum value + adapter package). main.cpp never includes Deribit headers — only the factory does.
To add a new venue: implement IFeedParser + IControlApi under venues/<name>/, register both in venues/factory.cpp, and add a VenueId case in venues/venue.hpp. The hot loop, strategies, and backtester stay untouched.
Credentials: DERIBIT_CLIENT_ID / DERIBIT_CLIENT_SECRET, or the generic QL_VENUE_CLIENT_ID / QL_VENUE_CLIENT_SECRET.
The hot path runs on a single thread pinned via pthread_setaffinity_np. To get deterministic latency, isolate that core from the Linux scheduler at boot.
-
Edit
/etc/default/gruband add the core(s) to the kernel command line (here we reserve CPU 3, matchingconfig.toml[hot].pinned_cpu):GRUB_CMDLINE_LINUX="isolcpus=3 nohz_full=3 rcu_nocbs=3"isolcpus=3— keep the general scheduler off core 3.nohz_full=3— disable the timer tick on core 3 (no periodic interrupts).rcu_nocbs=3— offload RCU callbacks away from core 3.
-
sudo update-grub && sudo reboot. -
Verify:
cat /sys/devices/system/cpu/isolated→3.
The hot loop pins itself to this core at startup and spins with _mm_pause(); nothing else should be scheduled there.
- Compiler: C++23 (GCC 11+/13+ or Clang 16+),
-O3 -march=native -Wall -Wextra. - Build: CMake ≥ 3.26.
- Packages (vcpkg manifest,
vcpkg.json):openssl,simdjson,nlohmann-json(config only),tomlplusplus,spdlog,eigen3,lbfgspp,prometheus-cpp,sqlite3(portfolio risk store),boost-beast,boost-asio,catch2,benchmark,pybind11.
JSON policy: simdjson (on-demand, zero-copy) everywhere market data is parsed; nlohmann/json only for config. Never nlohmann on a hot path.
Recommended (safe on 16 GiB laptops — avoids ninja -j24 OOM hangs):
export VCPKG_ROOT=~/vcpkg # or your vcpkg clone
./scripts/build.sh # deps + quant_lab in build-risk/
./scripts/build.sh --target risk_cli
BUILD_JOBS=2 ./scripts/build.sh # even more conservativescripts/build.sh installs vcpkg ports one at a time (classic mode, not manifest)
and caps compile parallelism via CMake job pools (QL_MAX_COMPILE_JOBS=4 default).
Manual configure (only if you know what you're doing):
export VCPKG_ROOT=/path/to/vcpkg
./scripts/install_runtime_deps.sh # once — sequential vcpkg installs
cmake -S . -B build-risk -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake \
-DVCPKG_MANIFEST_INSTALL=OFF -DQL_BUILD_BRIDGE=OFF \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-risk -j4 --target quant_lab # never -j$(nproc) on 16 GiB
ctest --test-dir build-risk --output-on-failureDo not run bare
vcpkg installin the repo root on a 16 GiB machine — manifest mode builds openssl, boost, duckdb, pybind11, etc. in parallel and will swap-thrash. Use./scripts/install_runtime_deps.shinstead.
| File | Purpose |
|---|---|
src/core/types.hpp |
fixed-width scalars (OrderId/Price/Qty/Nanos), Side/EventType enums, and SymbolArena — interns instrument symbols once into a pre-allocated buffer so the hot path only ever holds a std::string_view (Instrument). |
src/core/event.hpp |
alignas(64) tagged-union Event (sizeof == 128, trivially copyable) — the single currency of cross-tier communication. Compile-time enforced layout contract. |
src/core/spsc_ring.hpp |
power-of-two lock-free SPSC ring (requires (N&(N-1))==0); cache-line-separated head/tail; relaxed-own / acquire-other / release-publish ordering; noexcept, alloc-free try_push/try_pop. |
Verified: sizeof(Event) == 128, alignof(Event) == 64, FIFO + wrap correctness, full/empty edge cases, and a 2M-item threaded producer/consumer transfer with exactly-once delivery.
| File | Purpose |
|---|---|
src/network/ws_client.{hpp,cpp} |
Async WebSocket-over-TLS (boost-beast/asio) on its own io thread. Full async resolve → connect → TLS → WS handshake, real cert + SNI verification, exponential-backoff reconnect, keep-alive pings, and a thread-safe outbound queue. Never touches hot-path state. |
src/venues/venue.hpp |
VenueId, Endpoint, parse_venue_id() — venue identity from config. |
src/venues/factory.{hpp,cpp} |
create_feed_parser / create_control_api — the only place that knows about Deribit (or future venues). |
src/venues/deribit/control_api.hpp |
Deribit JSON-RPC 2.0 builders (authenticate, set_heartbeat, subscribe/unsubscribe, send_order, cancel_order, edit_order, test) + channel helpers. Implements IControlApi. |
src/venues/deribit/feed_parser.{hpp,cpp} |
Deribit simdjson on-demand parse of subscription frames → Event. Implements IFeedParser. |
The Phase 8 driver src/main.cpp (deribit_feed) loads venue from config, creates adapters via the factory, connects (testnet by default, --mainnet), subscribes to book/trades/ticker/index, parses on the network thread, and the hot loop drains the ring. Set DERIBIT_CLIENT_ID/DERIBIT_CLIENT_SECRET (or QL_VENUE_*) to authenticate and use .raw subscriptions; otherwise it runs a public 100ms feed. QL_DUMP=1 echoes raw frames.
Verified live on testnet: clean WSS/TLS handshake, successful JSON-RPC subscribe, Events flowing network→ring with parse_errors=0, dropped=0, and the instrument symbol interned exactly once (zero-copy string_view). Hot-path audit: parse() allocation-free / lock-free in steady state; the only cross-tier crossing is the lock-free SPSC ring.
Every file under src/hot/ opens with the hard-rule comment block and is compiled into ql_hot with -fno-exceptions -mavx2 -mfma.
| File | Purpose |
|---|---|
core/clock.hpp |
rdtsc()/rdtscp() and RdtscHistogram — a fixed pre-allocated bucket array; record() is branch+increment (no alloc/lock/syscall), percentiles are cold-path. |
core/cacheline.hpp |
CacheAligned<T> + compile-time false-sharing assertions. |
hot/order_book.hpp |
array half-books (std::array<Price,10>), apply/mid/spread/microprice/imbalance/ofi — all noexcept; OFI = d(bidQ)·sign(d(bidP)) − d(askQ)·sign(d(askP)); change-id gap detection. |
hot/greeks_avx2.hpp |
Black-76 greeks (Δ Γ Vega Θ Vanna Volga) vectorised 4 strikes/pass with FMA; vectorised Cephes exp/log + A&S erf; scalar libm reference for the tail. |
hot/grisu2.hpp + hot/order_sender.hpp |
Grisu2 fast_dtoa (zero-alloc shortest round-trip) stamps prices/qtys into a PrebuiltOrder 512-byte buffer; send_limit/cancel/edit build Deribit JSON-RPC with no heap at order time. |
hot/order_state_machine.hpp |
mandatory per-order lifecycle Pending→Acked→PartiallyFilled→Filled/Cancelled/Rejected in a fixed open-addressed table; client↔exchange id mapping; idempotent cancel/edit. |
hot/strategy_interface.hpp |
IStrategy (identical in live + backtest) + a single pending OrderRequest the loop drains. |
hot/event_loop.{hpp,cpp} |
single pinned thread, _mm_pause() spin, switch(EventType) dispatch, routes pending orders through the sender into an outbound SPSCRing<PrebuiltOrder>, records per-event rdtsc latency. Reads the vol-surface pointer fresh per event (hazard-pointer-lite invariant, wired in Phase 4). |
Verified: 24 hot-path unit tests (AVX2 greeks vs libm to ~1e-6; Grisu2 round-trips 400k random values exactly; OSM lifecycle; event-loop dispatch/order-routing/latency) plus a live run draining real testnet data through the pinned loop (p50≈1k, p99≈3.3k cycles/event). Hot-path audit: zero alloc / lock / syscall / std::chrono / exceptions in hot/ (only startup-time cached-powers init and a one-shot core pin); the only cross-path hops are the two lock-free SPSC rings (network→loop, loop→network).
The warm path reads hot-path snapshots via SPSCRing, does the heavy numerical work off the hot loop (it may allocate / use the STL / throw), and writes back to the hot path only via an atomic pointer swap (the vol surface) or an SPSCRing<Event> push (risk breaches). Two new core primitives back it.
| File | Purpose |
|---|---|
core/seqlock.hpp |
single-writer / multi-reader sequence lock. Writer bumps seq odd→write→even; readers retry on odd-or-changed. Wait-free writer, lock-free readers, memcpy snapshot of a trivially-copyable T. Backs the position keeper's cold-path reads. |
core/memory_pool.hpp |
fixed, pre-allocated slot pool (placement-new, no malloc after construction). Backs the vol-surface double buffer; Slots≥3 guarantees a free slot (current + one-generation grace + the slot being filled). |
warm/svi_calibrator.{hpp,cpp} |
raw SVI per-expiry fit w(k)=a+b(ρ(k−m)+√((k−m)²+σ²)) by box-constrained L-BFGS-B (LBFGS++/Eigen) with analytic gradients, warm-started from the previous slice. Arbitrage diagnostics: is_butterfly_arb_free (Durrleman g(k)≥0) and is_calendar_arb_free (total variance non-decreasing in maturity). |
warm/vol_surface.hpp |
global SSVI surface w(k,θ)=θ/2·(1+ρφk+√((φk+ρ)²+1−ρ²)), φ(θ)=η·θ^−γ, θ(T) the interpolated ATM-variance term structure. Published to the hot path by std::atomic pointer swap into a MemoryPool slot; the replaced slot is retired one generation later (hazard-pointer-lite grace). A SurfaceUpdate event is optionally pushed to the warm/cold path. get()/queries are noexcept, alloc-free. |
warm/risk_engine.hpp |
signed portfolio Δ/Γ/Vega + gross-notional aggregation; pluggable IMonteCarloEngine (default Gaussian delta-gamma-vega VaR and ETL); pushes one RiskBreach Event per breached, hot-reloadable limit into the hot loop's inbound ring (best-effort try_push). |
warm/position_keeper.hpp |
real-time per-instrument P&L by average-cost accounting (realized on close, unrealized from mark) plus perpetual funding accrual; publishes a whole-book PositionSnapshot through a Seqlock so the cold path reads coherently without blocking the writer. |
Dependencies:
eigen3via vcpkg; LBFGS++ is header-only and not in the vcpkg baseline, so it is vendored underthird_party/LBFGSpp/and exposed as thelbfgsppINTERFACE target.
Verified: 33 warm-path unit tests — seqlock survives a 2M-read torn-snapshot stress with 3 concurrent readers; the pool exhausts/recovers with zero allocation; L-BFGS-B recovers a known smile to RMSE < 1e-5 (and warm-starts); both arbitrage checks are direction-aware; the publisher swaps pointers without overflowing the pool under 5k publishes against a live reader thread; risk breaches fire per-limit and hot-reload; MC VaR/ETL satisfy ETL ≥ VaR; and the position keeper's averaging/close/flip/funding maths are exact.
The cold path is best-effort and off the critical path: it may block on I/O, allocate, and lock. It never touches hot-path state directly — it reads the loop's atomic counters / RdtscHistogram and the position keeper's Seqlock snapshot. Crucially, the backtester drives a strategy through the exact same dispatch as the live loop (hot/dispatch.hpp, now shared by both), so live and replay are equal by construction.
| File | Purpose |
|---|---|
cold/event_log.{hpp,cpp} |
append-only binary event log. A dedicated thread drains a lock-free SPSCRing<Event> and appends fixed-size raw Event records behind a LogHeader that pins magic/version/sizeof(Event). EventLogReader reads it back and rejects logs from a mismatched build. This is the backbone of deterministic replay. |
cold/config.{hpp,cpp} |
TOML config (tomlplusplus) → typed Config struct, with inotify hot-reload. The watcher watches the parent directory (robust to editors' atomic-rename saves), re-parses on change, atomically swaps a shared_ptr<const Config>, and fires callbacks (e.g. to RiskEngine::set_limits). A bad edit keeps the last good snapshot. Secrets stay in the environment, never the file. |
cold/backtester.{hpp,cpp} |
deterministic replay of a recorded Event span through an IStrategy via the shared dispatch; collects emitted OrderRequests (optionally to a sink). Pure function of (events, initial state) — no threads, no clock, no RNG. |
cold/metrics.{hpp,cpp} |
Prometheus (prometheus-cpp): a Registry with counters (events/orders/parse-errors), gauges (ring occupancy, net delta, total P&L), and a latency histogram (TSC cycles). start_http() exposes /metrics; serialize() renders the text exposition without a live scrape. |
Refactor: the live event loop's
switch(EventType)dispatch moved intohot/dispatch.hppso the backtester and the hot loop share one code path — a divergent live/replay branch is now impossible, not merely discouraged.
Verified: 15 cold-path unit tests — 500 events round-trip through the writer thread and back bit-exactly (and bad headers/missing files are rejected); TOML parses all sections with default fallbacks, throws on garbage, hot-reloads on manual trigger and via a real inotify file-change; replay is proven deterministic (identical input → identical orders) and the order sink fires per emission; Prometheus exposition contains every metric with monotone counters, latest-value gauges, and histogram buckets.
Concrete IStrategy implementations. They run inside the pinned hot loop, so they obey the hot-path hard rules and compile under -fno-exceptions -mavx2 -mfma. Each emits at most one OrderRequest per callback (the interface contract) and is driven by the shared dispatch — so every strategy is backtestable through cold::Backtester from day one, with zero code divergence from live.
| File | Purpose |
|---|---|
strategies/black76.hpp |
scalar Black-76 call price + implied-vol inversion (Newton on vega with a guaranteed-progress bisection fallback; fixed iteration budget, no allocation). Round-trips IV to ~1e-5 across vols and moneyness. |
strategies/quoting.hpp |
pure Avellaneda–Stoikov math: inventory-skewed reservation price r = s − q·γ·σ²·τ and optimal spread δ = γ·σ²·τ + (2/γ)·ln(1+γ/κ). |
strategies/market_maker.hpp |
A-S market-making skeleton: recompute reservation/spread each tick, snap quotes to the tick, track inventory from fills, and skew toward flat (inventory caps force the reducing side under the single-pending-order contract). |
strategies/delta_hedger.hpp |
keep net delta in a band by trading the perp. Option delta is fed from the warm-path risk aggregation (set_option_delta); perp fills are tracked internally; out-of-band → marketable flatten clipped to max_clip and snapped to lot. |
strategies/vol_arb.hpp |
trade when the warm-path SSVI surface disagrees with market IV by more than a cost threshold. Reads the surface through the atomic pointer (fresh each event, never cached), inverts Black-76 on the coin-quoted option mid (× forward → USD), and buys cheap / sells rich vol. |
Verified: 23 strategy tests — Black-76 IV round-trips; A-S reservation price skews correctly and the spread grows with vol/horizon; the MM emits tick-aligned quotes, updates inventory on fills, and respects inventory caps; the hedger flattens long/short deltas and stops once back in band; vol-arb recovers market IV from the mid and fires buy/sell against both an override fair-vol and a live SSVI surface.
The quantlab pybind11 extension module exposes the exact same C++ types the live system runs — no Python re-implementation. Calibrate a surface in Jupyter and the hot loop reads bit-identical numbers.
| Python API | C++ source |
|---|---|
OrderBook |
hot/order_book.hpp — apply, mid, spread, microprice, imbalance, ofi |
black76_call_price/vega/implied_vol, black76_call_greeks |
strategies/black76.hpp, hot/greeks_avx2.hpp |
reservation_price, optimal_spread, as_quotes |
strategies/quoting.hpp |
SVICalibrator, SVIParams, SVIQuote |
warm/svi_calibrator.hpp — L-BFGS-B fit + arb checks |
VolSurface, SSVIParams, ThetaNode |
warm/vol_surface.hpp |
PositionKeeper |
warm/position_keeper.hpp — fills, marks, funding, snapshot |
cmake --build build -j --target quantlab
PYTHONPATH=build python3 tests/test_bridge.py # smoke test
PYTHONPATH=build python3 -c "import quantlab as ql; print(ql.black76_call_price(100,100,0.2,1))"In Jupyter, add build/ to sys.path (or install the .so into your venv). PositionKeeper accepts Python str instrument names — they are interned into a SymbolArena inside the wrapper so the underlying string_view handles stay valid.
pybind11: listed in
vcpkg.json; if the vcpkg install fails (e.g.libb2on a minimal system), pip'spybind11works: pass-Dpybind11_DIR=$(python3 -c "import pybind11; print(pybind11.get_cmake_dir())")at configure time.
Verified: Python smoke test drives order-book BBO maths, Black-76 IV round-trip, A-S inventory skew, SVI L-BFGS-B recovery, SSVI ATM vol, and position-keeper P&L accounting — all matching the C++ unit-test invariants.
A user-facing risk service for a multi-position options portfolio. A user creates a portfolio (equities and/or European options), persists it in an embedded SQLite database, and runs 95% / 99% Value-at-Risk and Expected Tail Loss under a correlated Monte-Carlo simulation of spot, implied-vol, rate and time-decay shocks. The engine runs six P&L models on the same scenario draws and reports how far each greek-based approximation sits from a full repricing — the honest measure of nonlinear option risk.
This is cold-path analytics (ql_risk library + risk_cli executable); it is fully
decoupled from the hot loop and is the C++ backend a React frontend calls (today by
shelling out / a thin HTTP wrapper — the JSON shapes are already stable).
| File | Purpose |
|---|---|
risk/instrument.hpp |
portfolio data model — Portfolio / Position / Instrument (equity or European option), asset-class agnostic (listed equity options and crypto). |
risk/bsm.hpp |
Black-Scholes-Merton price + full analytic greeks (Δ Γ Vega Θ ρ) for calls and puts, with a carry/dividend yield; implied-vol inversion. Greeks are in per-unit terms so the Taylor expansion is dimensionally consistent with the shocks. |
risk/scenario.hpp |
correlated multi-factor MC generator: log-normal spot, additive vol (correlated with spot — the leverage effect, via a 2×2 Cholesky), a market-wide rate factor, and deterministic time decay; antithetic variates; fully seed-deterministic (reproducible risk reports). |
risk/var_engine.{hpp,cpp} |
drives every scenario through full revaluation + Delta, Delta-Gamma, Delta-Gamma-Vega, +Theta, +Rho, computes VaR/ETL per confidence, and the per-method approximation error (P&L RMSE, tail RMSE, VaR/ETL error vs full). |
risk/portfolio_store.{hpp,cpp} |
SQLite persistence — portfolio / position CRUD + stored market assumptions, schema auto-created, WAL mode. |
risk/market_data.{hpp,cpp} |
IMarketDataProvider → ManualMarketData (offline / stored, deterministic) and DeribitMarketData (a live crypto snapshot: spot + mark_iv, reusing the production WebSocket client). |
risk/json_io.{hpp,cpp} |
the wire format — emit a VaRReport / portfolio as JSON; parse a frontend run-spec with simdjson. |
src/risk_main.cpp |
the risk_cli front door. |
# Seed the classic long-vol / short-vol structure (long ATM call, short OTM call,
# long OTM put, short ATM put) on a $100 underlying:
risk_cli init-demo --db risk.db
# Run 50k-scenario VaR/ETL at 95% and 99% (uses the stored market assumptions):
risk_cli run --portfolio demo-equity-vol-book --db risk.db \
--scenarios 50000 --confidence 0.95,0.99Build your own book:
risk_cli create --name my-book --db risk.db
risk_cli add-position --portfolio my-book --db risk.db --type option --underlying AAPL \
--symbol AAPL-ATM-C --option-type call --strike 180 --expiry-years 0.0833 --quantity 10
risk_cli set-market --portfolio my-book --db risk.db --underlying AAPL \
--spot 180 --vol 0.28 --rate 0.045
risk_cli run --portfolio my-book --db risk.db --scenarios 50000--live overlays a live spot (and implied vol where available) from Deribit onto the
stored assumptions — so a crypto book is revalued against the real market:
risk_cli run --portfolio my-btc-book --db risk.db --live --mainnet \
--spot-instrument BTC-PERPETUALLive equity data needs an external market-data vendor (not bundled); the manual / stored snapshot path covers equities deterministically in the meantime.
run-spec takes a self-describing JSON payload (portfolio + market + scenario) and
returns the full VaRReport as JSON — no database needed. This is the endpoint a "Run
VaR" button calls. See config/sample_var_spec.json:
risk_cli run-spec --file config/sample_var_spec.json # or: ... < payload.jsonThe report contains, per method: VaR/ETL at each confidence, the error vs full revaluation, and per-scenario / tail RMSE — i.e. exactly how much each higher-order greek matters for tail risk.
A correctness note baked into the tests: delta-gamma alone can be a worse VaR approximation than delta-only, because the Black-Scholes PDE links gamma and theta (
theta ≈ -½·gamma·S²·σ²). Adding gamma without theta breaks that balance — therisk_testssuite asserts the balanced delta-gamma-theta beats delta, and that vega dominates once implied vol moves. This is the kind of nonlinearity the project is built to expose.
Verified: 16 risk unit tests — BSM put-call parity, analytic greeks vs central finite differences, implied-vol round-trip; VaR>0 / ETL≥VaR / 99%≥95%; seed-determinism; gamma-theta balance and vega dominance; equity-only books are approximation-exact; and SQLite CRUD / market upsert / constraints round-trip.
Live Deribit implied-volatility surfaces calibrated with Horvath et al. (2019)
forward rBergomi network (params → 9×6 IV grid, ELU, L-BFGS-B through the frozen net)
plus on-line SVI slice L-BFGS refinement for the published warm::VolSurface.
Training uses synthetic rBergomi surfaces (Fukasawa short-time approximation) on the Deribit log-moneyness grid. Production loads exported weights from JSON.
| Component | Path |
|---|---|
| Quote filter + IV grid | src/risk/vol_quote_filter.*, vol_surface_grid.* |
| Forward MLP + L-BFGS calibrator | src/risk/nn_mlp.*, dl_vol_calibrator.* |
| Background calibration loop | src/risk/vol_surface_service.* |
| Atomic surface publish (vol_arb hot read) | src/risk/vol_surface_hub.* |
| Registry → hot ring bridge + vol_arb loop | src/risk/registry_hot_bridge.*, trading_service.* |
| SSE push stream | src/risk/vol_surface_stream.*, GET /api/vol-surface/:u/stream |
| Training notebook | notebooks/train_vol_calibration_nn.ipynb |
| Weight export | scripts/export_vol_weights.py → models/vol_calibrator_weights.json |
Configure in config/config.toml under [vol_surface] (calibration_interval_sec,
model_weights, spread/IV filters) and [strategy] (active = "vol_arb" wires the
paper-trading hot loop inside risk_api). Dashboard sidebar → Vol surface for the 3D view,
calibration method toggle, and auto-calibrate interval (30s / 1m / 5m / manual).
risk_api is the unified runtime: one process owns live quotes → vol calibration →
atomic surface publish → vol_arb hot loop (paper mode) → REST/SSE dashboard. Use
--no-hot-loop to disable strategy dispatch while keeping analytics.
REST:
curl http://localhost:8080/api/vol-surface/status
curl http://localhost:8080/api/trading/status
curl http://localhost:8080/api/vol-surface/BTC
curl -N http://localhost:8080/api/vol-surface/BTC/stream
curl -X POST 'http://localhost:8080/api/vol-surface/BTC/calibrate?method=rbergomi_horvath'
curl -X PATCH http://localhost:8080/api/vol-surface/config \
-H 'Content-Type: application/json' \
-d '{"calibration_interval_sec":60}'quant_lab --smoke (symlinked as deribit_feed) is the live MM orchestrator. The default
mode (no flags) starts risk_api — the unified runtime with vol surface + vol_arb.
config.venue → factory → IFeedParser + IControlApi
│
Deribit WSS → simdjson parse → SPSCRing<Event>
│ tee (best-effort)
▼
EventLogWriter → logs/events.bin
│
▼
hot EventLoop (pinned) + MarketMakerStrategy
│
▼
Metrics sampler → Prometheus /metrics
| Flag | Purpose |
|---|---|
--config PATH |
load config.toml (venue, instruments, risk limits, log path, metrics port) |
--duration N |
auto-stop after N seconds (for CI / smoke scripts) |
--smoke |
exit non-zero if any gate fails (events, parse_errors, drops, hot-loop processed, log records) |
--core N |
pin hot loop to CPU N (isolcpus in production) |
--no-log / --no-metrics |
disable cold-path components |
--trade |
wire outbound order ring (requires DERIBIT_CLIENT_ID / DERIBIT_CLIENT_SECRET or QL_VENUE_*) |
cmake --build build-risk -j --target quant_lab
./scripts/smoke_testnet.sh # 20 s on testnet, checks --smoke gates
QL_SMOKE_SECONDS=30 ./scripts/smoke_testnet.sh
# Via ctest (opt-in — needs network):
QL_SMOKE_LIVE=1 ctest --test-dir build-risk -R smoke_testnetVerified live on testnet: WSS/TLS handshake, subscribe, 20 events / 0 parse_errors / 0 dropped, hot loop processed all events with p50≈4.5k cycles, market-maker emitted 19 quotes, 20 records written to logs/events.bin (replayable by cold::EventLogReader / Backtester).
- core/ primitives + tests ✅ (done)
- network/ + venues/ — WS/TLS client, venue factory, Deribit feed parser + control API →
SPSCRing<Event>✅ (done) - hot/ — array order book, AVX2 Black-76 greeks, order sender (pre-serialised templates + Grisu2), order state machine, event loop ✅ (done)
- warm/ — SVI calibrator (L-BFGS-B), SSVI vol surface (atomic pointer swap), risk engine, position keeper (seqlock + funding) ✅ (done)
- cold/ — async logger + binary event log, TOML config hot-reload (inotify), backtester, Prometheus metrics ✅ (done)
- strategies/ — vol arb, delta hedger, Avellaneda-Stoikov MM skeleton ✅ (done)
- pybind11 research bridge (OrderBook / VolSurface / PositionKeeper → Jupyter) ✅ (done)
- end-to-end testnet smoke test ✅ (done)
Higher phases are intentionally not scaffolded until the phase below has green tests — a bug in an SPSC ring is trivial to fix in a Phase 1 test and catastrophic in live testnet trading.
MIT.