Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ML Components

Mercury’s strategy crate ships self-contained machine-learning building blocks — no Python, no external service, all pure Rust.


Feature engineering (FeatureComputer)

FeatureComputer maintains running indicator state and produces a 12-element f32 vector on every on_book call.

IndexFeatureRangeNotes
0Order imbalance[−1, 1](bid_qty − ask_qty) / total
1Spread bps / 100[0, ∞)
2RSI-14 / 100[0, 1]
3MACD histogram[−1, 1]sign × clamped magnitude
4Depth ratio top-5[0, 1]bid depth fraction
5EMA-50 deviationuncapped(mid − ema) / ema
6ATR-14 / mid[0, ∞)
7VWAP deviationuncapped100-trade rolling
8Depth slopeuncapped(bid[0] − bid[4]) / mid
9Trade-flow imbalance[0, 1]20-trade rolling buy fraction
10Kyle’s Lambda(−1, 1)tanh(λ × 1000) — price-impact coefficient
11VPIN[0, 1]volume-synchronised toxicity

compute_extended() appends a 13th feature at index 12: bounded Hawkes trade-arrival intensity.


Online normalisation (RunningNormalizer)

Implements Welford’s one-pass algorithm to track per-feature mean and variance without storing history.

#![allow(unused)]
fn main() {
pub struct RunningNormalizer {
    mean: [f64; FEATURE_COUNT],
    m2:   [f64; FEATURE_COUNT],   // Welford M2 accumulator
    count: u64,
    min_count: u64,               // warm-up ticks before normalisation activates
}
}

Features are standardised in-place: z_i = (x_i − μ_i) / max(σ_i, 1e-8). Before min_count observations, features are returned unchanged.

InferenceStrategy uses a 30-tick warm-up by default.


Adam optimizer (OnlineClassifier)

3-class softmax logistic regression trained with the Adam adaptive gradient method.

HyperparameterDefaultDescription
learning_rate0.001Adam α
beta10.9First-moment decay
beta20.999Second-moment decay
epsilon1e-8Numerical stability
l21e-4L2 weight decay

Update rule (per weight):

m_t = β₁·m_{t-1} + (1−β₁)·g
v_t = β₂·v_{t-1} + (1−β₂)·g²
α_t = α · √(1−β₂ᵗ) / (1−β₁ᵗ)
w ← w − α_t · m̂_t / (√v̂_t + ε)

HMM regime filter (HmmFilter)

Two-state online forward filter classifying the market into Trending or MeanReverting.

Observation: sign persistence of consecutive log returns (+1 same direction, −1 alternating).

States:

  • Trending — emission mean +0.75 (returns persist in the same direction).
  • MeanReverting — emission mean −0.75 (returns alternate sign).
#![allow(unused)]
fn main() {
let mut hmm = HmmFilter::default();
hmm.update_price(50_000.0);  // 1st price — no output
hmm.update_price(50_050.0);  // 2nd — computes 1st return, no output
let regime = hmm.update_price(50_100.0);  // 3rd — first regime estimate
// Some(Regime::Trending)

let trend_prob = hmm.trend_probability();  // ∈ [0, 1]
}

A confident regime is declared only when trend_probability ≥ confidence_threshold (default 0.60).

Integration points:

  • MarketMaker — spread multiplier: 2.0× trending, 0.75× mean-reverting.
  • RlQuotePlacementStrategy — trend_probability − 0.5 as state dimension 6.

Microstructure estimators (microstructure.rs)

Kyle’s Lambda

Price-impact coefficient estimated via rolling OLS on signed order flow:

λ = Σ(Q_i · ΔP_i) / Σ(Q_i²)

where Q is signed volume (+buy, −sell) and ΔP is the concurrent mid-price change. Rising λ indicates informed directional flow.

#![allow(unused)]
fn main() {
let mut kl = KyleLambda::new(20);         // 20-trade rolling window
kl.on_trade(1.0, Side::Buy, 0.01);        // qty, side, price_change
let lambda = kl.lambda();                  // Some(f64) once window is full
}

Roll Spread

Estimates the effective half-spread from the serial covariance of price changes (Roll, 1984):

c = √(−Cov(ΔP_t, ΔP_{t−1}))

Valid only when serial covariance is negative (as expected when bid-ask bounce dominates).

#![allow(unused)]
fn main() {
let mut rs = RollSpread::new(20);
rs.on_price(100.0);
let half_spread = rs.half_spread();       // Some(f64) once window full and Cov < 0
}

VPIN

Volume-synchronised probability of informed trading (Easley et al., 2012):

  • Volume is divided into fixed-size buckets.
  • Each bucket: vpin = |buy_vol − sell_vol| / bucket_size.
  • Rolling average over the last window_buckets gives VPIN ∈ [0, 1].
  • Values > 0.5 indicate a toxic order-flow environment.
#![allow(unused)]
fn main() {
let mut vpin = Vpin::new(10.0, 20);       // bucket_size=10, 20-bucket window
vpin.on_trade(1.0, Side::Buy);
let score = vpin.vpin();                   // 0.0 until first bucket completes
}

Model checkpointing (checkpoint.rs)

Online classifier (JSON)

#![allow(unused)]
fn main() {
// Save weights + bias + update count.
clf.save("model.json")?;

// Restore.
let mut clf = OnlineClassifier::new(OnlineClassifierConfig::default());
clf.load("model.json")?;
}

Format: {"updates": u64, "bias": [f32; 3], "weights": [f32; 36]} (3 × 12 features).

DQN network (binary f32 LE)

#![allow(unused)]
fn main() {
// Flatten each weight matrix then save as one binary blob.
save_f32_weights("dqn.bin", &[&w1_flat, &w2_flat, &w3_flat])?;

// Load and split by known sizes.
let weights = load_f32_weights("dqn.bin", total_params)?;
}

Dimension mismatch on load returns io::Error::InvalidData.