Skip to content

Repository files navigation

deribit-quant-lab

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 --smoke gates (parse_errors=0, dropped=0, events processed, log written). See End-to-end smoke test below.


Design philosophy

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.

Three-path architecture (hot / warm / cold)

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.

Venue abstraction

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.


Pinning the hot loop — isolcpus (required for production latency)

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.

  1. Edit /etc/default/grub and add the core(s) to the kernel command line (here we reserve CPU 3, matching config.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.
  2. sudo update-grub && sudo reboot.

  3. Verify: cat /sys/devices/system/cpu/isolated3.

The hot loop pins itself to this core at startup and spins with _mm_pause(); nothing else should be scheduled there.


Toolchain & dependencies

  • 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.

Build

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 conservative

scripts/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-failure

Do not run bare vcpkg install in 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.sh instead.


Core primitives (Phase 1)

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.


Network layer (Phase 2)

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.


Hot path (Phase 3)

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).


Warm path (Phase 4)

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: eigen3 via vcpkg; LBFGS++ is header-only and not in the vcpkg baseline, so it is vendored under third_party/LBFGSpp/ and exposed as the lbfgspp INTERFACE 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.


Cold path (Phase 5)

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 into hot/dispatch.hpp so 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.


Strategies (Phase 6)

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.


Research bridge (Phase 7)

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.hppapply, 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

Build & use

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. libb2 on a minimal system), pip's pybind11 works: 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.


Portfolio risk: Monte-Carlo VaR / ETL (risk_cli)

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} IMarketDataProviderManualMarketData (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.

Quick start

# 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.99

Build 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 data

--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-PERPETUAL

Live equity data needs an external market-data vendor (not bundled); the manual / stored snapshot path covers equities deterministically in the meantime.

Stateless run (React frontend seam)

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.json

The 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 — the risk_tests suite 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.


Deep learning vol surface (risk_api + dashboard)

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.pymodels/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}'

End-to-end smoke test (Phase 8)

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_*)

Run the smoke test

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_testnet

Verified 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).


Build order (one phase at a time)

  1. core/ primitives + tests ✅ (done)
  2. network/ + venues/ — WS/TLS client, venue factory, Deribit feed parser + control API → SPSCRing<Event>(done)
  3. hot/ — array order book, AVX2 Black-76 greeks, order sender (pre-serialised templates + Grisu2), order state machine, event loop ✅ (done)
  4. warm/ — SVI calibrator (L-BFGS-B), SSVI vol surface (atomic pointer swap), risk engine, position keeper (seqlock + funding) ✅ (done)
  5. cold/ — async logger + binary event log, TOML config hot-reload (inotify), backtester, Prometheus metrics ✅ (done)
  6. strategies/ — vol arb, delta hedger, Avellaneda-Stoikov MM skeleton ✅ (done)
  7. pybind11 research bridge (OrderBook / VolSurface / PositionKeeper → Jupyter) ✅ (done)
  8. 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.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages