Files
foxhunt/docs/superpowers/specs/2026-04-16-tick-microstructure-intelligence-design.md
jgrusewski e6b3fffa6d spec: fix 4 critical issues from review — OFI_DIM cascade, deploy atomicity
1. Enumerate all 6+ files with hardcoded OFI_DIM=8 literal
2. Document atomic deploy requirement (code + fxcache together)
3. Rename Depth Renewal Rate → Order Count Flux (MBP-10 approximation)
4. Fix Intra-Bar Momentum to Welford online covariance (O(1), no Vec)
5. Clarify snapshot() is pure read, non-mutating, idempotent
6. Fix precompute time: 8s CPU + 5min I/O = ~5-10min total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:43:55 +02:00

15 KiB
Raw Blame History

Tick-Level Microstructure Intelligence — Design Spec

Goal: Extract 12 new predictive features from raw MBP-10 tick data (1000-5000 ticks/bar) to give the DQN model sub-bar microstructure awareness. At inference, use H100 idle time between bars for speculative pre-computation and mid-bar regime detection. Feature vector grows from 50 (42+8 OFI) to 62 (42+20 OFI) dimensions.

Core Principle: Bar-level models see WHERE price went. Tick-level models see HOW it got there — the microstructure path within the bar predicts the next bar.


Data Source

Databento — single source for all tick data (historical + live).

  • Dataset: GLBX.MDP3 (CME Globex MDP 3.0)
  • Schema: mbp-10 (10-level order book snapshots)
  • Symbol: ES (E-mini S&P 500 Futures)
  • Historical: 148GB on PVC at /mnt/training-data/futures-baseline
  • Live: Databento Rust crate (databento), 6.1μs median feed latency
  • Format: Mbp10Snapshot — 10 BidAskPair levels (bid_px, bid_sz, bid_ct, ask_px, ask_sz, ask_ct)
  • Trades: from same Databento feed, Lee-Ready classification for buy/sell

IBKR is execution only (max 9 depth levels, subscription caps — unsuitable for tick data).


The 12 New Microstructure Features

OFI_DIM: 8→20. Existing 8 features (OFI_L1, OFI_L5, depth_imbalance, VPIN, Kyle's Lambda, bid_slope, ask_slope, trade_imbalance) stay at indices 0-7. New features at indices 8-19.

All features are incremental — O(1) per tick via running statistics (EMAs, counters, running sums). Same computation for precompute (batch over historical ticks) and inference (streaming from Databento live).

PEARLS (Cont 2014, Easley 2012)

Index Feature Incremental State Formula
8 OFI Trajectory LinearRegressor (online Welford) Slope of OFI_L1 over all ticks in bar. Front-loaded buying → continuation. Back-loaded → reversal.
9 Realized Variance Running sum of squared returns Σ(log(mid_t/mid_{t-1})²). True intra-bar volatility — bar range is a poor proxy.
10 Trade Arrival Intensity Hawkes exponential kernel EMA λ(t) = μ + α·Σexp(-β(t-t_i)). Self-exciting clusters. Rising intensity → momentum.

GEMS (microstructure theory)

Index Feature Incremental State Formula
11 Book Pressure Gradient Weighted running sum Σ_l(exp(-0.3l)·(bid_sz_l - ask_sz_l)) / Σ(bid_sz_l + ask_sz_l). Deep vs surface pressure across 10 levels.
12 Spread Dynamics Running min/max/mean of spread (max_spread - min_spread) / mean_spread. Widening = market makers pulling back = volatility incoming.
13 Aggression Ratio Two counters (ask_trades, total) trades_at_ask / total_trades (Lee-Ready). > 0.6 = strong buy pressure.
14 Queue Depletion Asymmetry Running depletion rates bid/ask (bid_depletion - ask_depletion) / max. Reveals hidden institutional flow direction.
15 Order Count Flux Running delta of bid_ct/ask_ct at top 3 Δbid_ct>0 → new order proxy, Δbid_ct<0 → cancel/fill proxy. Approximation: MBP-10 snapshots can't distinguish cancel from fill. Treat as market maker activity volatility — high flux = active refresh, low flux = about to pull.

NOVELS (new combinations)

Index Feature Incremental State Formula
16 Intra-Bar Momentum Two accumulators (first/second half returns) corr(returns_first_half, returns_second_half). Positive = continuation. Negative = reversal. Detects regime WITHIN the bar.
17 Microstructure Regime Score Composite of aggression, spread, book thickness sigmoid(aggression × spread_widen × thin_book). "Institutional sweep mode" (high) vs "market making mode" (low).
18 Order Flow Acceleration EMA of OFI deltas d(OFI_L1)/dt — is buying accelerating or decelerating? Acceleration → breakout. Deceleration → pullback.
19 Toxicity Gradient EMA of VPIN deltas d(VPIN)/dt. Catches the ONSET of informed flow — earlier signal than VPIN level alone.

IncrementalMicrostructureCalculator

New struct extending the existing OFICalculator in crates/ml-features/src/ofi_calculator.rs.

pub struct IncrementalMicrostructureCalculator {
    /// Existing 8 OFI features
    ofi: OFICalculator,
    
    /// Running state for 12 new features (all O(1) per tick)
    ofi_trajectory: OnlineLinearRegressor,  // slope of OFI_L1 over time
    realized_var_sum: f64,                   // sum of squared log returns
    prev_mid: f64,                           // previous midpoint for returns
    hawkes_intensity: f64,                   // exponential kernel EMA
    last_trade_time_ns: u64,                 // for Hawkes decay
    spread_min: f64,                         // running min/max/mean
    spread_max: f64,
    spread_sum: f64,
    trades_at_ask: u64,                      // aggression counters
    trades_total: u64,
    bid_depletion: f64,                      // queue depletion EMAs
    ask_depletion: f64,
    prev_bid_sz_top3: u64,                   // for depletion rate
    prev_ask_sz_top3: u64,
    new_orders_top3: u64,                    // depth renewal counters
    cancels_top3: u64,
    // Intra-bar momentum: Welford online covariance (O(1) per tick, no allocation)
    half1_sum_x: f64, half1_sum_x2: f64, half1_n: u64,
    half2_sum_x: f64, half2_sum_x2: f64, half2_n: u64,
    cross_sum_xy: f64,  // for correlation between halves
    ofi_prev: f64,                           // for acceleration
    ofi_delta_ema: f64,
    vpin_prev: f64,                          // for toxicity gradient
    vpin_delta_ema: f64,
    tick_count: u64,
    bar_start_ns: u64,
    bar_duration_ns: u64,                    // expected bar duration (60s)
}

Interface:

impl IncrementalMicrostructureCalculator {
    pub fn new(bar_duration_ns: u64) -> Self;
    pub fn update(&mut self, snapshot: &Mbp10Snapshot, trade: Option<&Trade>) -> ();
    pub fn snapshot(&self) -> MicrostructureFeatures;  // [20] f64 — pure read, non-mutating, idempotent
    pub fn reset(&mut self);  // call at bar boundary — clears all running state
}

update() is O(1) — no allocations, no loops over history. Called once per MBP-10 event. snapshot() finalizes the 20 features. Called at bar close. reset() clears running state for the next bar. Called after snapshot.


fxcache Format Change

Single format. OFI_DIM: 8→20. All existing fxcache files must be regenerated.

FxCacheHeader (64 bytes):
  magic       [u8; 8]   = b"FXCACHE\0"
  version     u16       = 1
  feat_dim    u16       = 42
  target_dim  u16       = 4
  ofi_dim     u16       = 20          ← was 8
  bar_count   u64
  cache_key   [u8; 32]
  reserved    [u8; 8]

Record: [i64 ts][66 × f32] = 272 bytes/bar   ← was 224
  66 = 42 features + 4 targets + 20 OFI

Constants change in crates/ml/src/fxcache.rs:

const OFI_DIM: usize = 20;              // was 8
const RECORD_F64_COUNT: usize = 66;      // was 54 (42+4+20)

CRITICAL: OFI_DIM=8 is load-bearing in 6+ files. All must be updated atomically:

File Location What to change
fxcache.rs:124 validate() Reads ofi_dim from header — will reject old files. Code and data deploy atomically.
fxcache.rs:195,377 Vec<[f64; OFI_DIM]> Compile-time array generic — all callers cascade.
precompute_features.rs:329,342,434 [f64; 8] literals Change to [f64; OFI_DIM] or use fxcache::OFI_DIM.
gpu_experience_collector.rs:747 let ofi_dim = if ... { 8 } Hardcoded detection. Must use OFI_DIM constant.
cuda_pipeline/mod.rs:402,477,516 if ofi { 8 } else { 0 } Same hardcoded literal.
constructor.rs:52-53,255 raw_state_dim = 74 (42+8+16+8) Must become 42+8+16+OFI_DIM. Aligned: (86+7)&!7 = 88.
hyperopt/adapters/dqn.rs:1437 Literal 8 Use OFI_DIM.
Test files [0.0; 8], vec![[0.0; 8]; n] All OFI fixtures must use OFI_DIM.

Deploy rule: Code and fxcache data must deploy atomically. New code with OFI_DIM=20 will reject old fxcache (ofi_dim=8 in header). Regenerate fxcache BEFORE deploying new code, or deploy both in the same Argo workflow step.


Precompute Pipeline

Extend crates/ml/examples/precompute_features.rs:

For each minute bar [bar_start, bar_end):
  1. Collect Mbp10Snapshots from DBN files within time range
  2. Collect Trades from trades DBN files within time range
  3. Feed into IncrementalMicrostructureCalculator.update() per tick
  4. calculator.snapshot() → [20] features
  5. Write to fxcache record alongside 42 features + 4 targets

The existing OFI calculation path in precompute_features.rs already loads MBP-10 snapshots per bar and calls OFICalculator. The change: replace OFICalculator with IncrementalMicrostructureCalculator which computes all 20 features (including the existing 8).

Precompute time: Calculator CPU time is ~8 seconds (8B ticks × <1μs). But disk I/O dominates — 148GB MBP-10 at ~500MB/s = ~5 minutes read time. Total precompute: ~5-10 minutes depending on I/O. Set Argo workflow timeout accordingly.


Model Integration

The DQN's feature input grows by 12 dimensions:

Component Change
fxcache.rs OFI_DIM: 8→20, RECORD_F64_COUNT: 54→66
precompute_features.rs Use IncrementalMicrostructureCalculator
Experience collector (gpu_experience_collector.rs) Load 20 OFI features per bar (was 8)
DQN config state_dim or OFI concat dimension grows by 12
Trunk w_s1 Input dimension grows by 12 (via state_dim config)
ISV feature gating (Phase 3) Controls which of 20 microstructure features matter per regime

The additional 12 dimensions are handled automatically by the existing bottleneck/VSN architecture — the trunk learns which features matter. ISV feature gating (when implemented) provides regime-adaptive control.


Inference: H100 Between-Bar Compute

Databento Live → Incremental Calculator

Databento Live (GLBX.MDP3, mbp-10, ES)
    │
    │ databento Rust crate → Mbp10Snapshot per event
    │ 6.1μs median latency
    │
    ▼
IncrementalMicrostructureCalculator.update()
    │ <1μs per tick, O(1), no allocation
    │
    ▼ At bar close:
calculator.snapshot() → microstructure_state [20]
    │
    ▼
DQN forward pass on H100 → action

Between-Bar Speculative Compute (NOVEL N10)

The H100 sits idle 99.9998% of inference time. Use it:

Every 5 seconds (12× per bar):

  1. Snapshot intermediate microstructure_state [20]
  2. ISV signal update → micro-regime detection (regime_velocity, stability)
  3. Speculative trunk forward → pre-compute h_s2 from intermediate features
  4. Cache the speculative h_s2 + ISV state

At bar close:

  • If actual features within 0.5σ of speculative: use cached h_s2, run branch heads only (~0.01ms)
  • If features deviate significantly: discard cache, full forward (~0.1ms)
  • Free option: helps when right, costs nothing when wrong

Mid-Bar Regime Shift Early Warning (NOVEL N11)

With 12 regime assessments per bar (every 5s):

  1. regime_velocity spikes mid-bar → regime shift detected 30s before bar close
  2. ISV automatically reduces risk_budget via risk branch
  3. Speculative "escape action" (direction=Flat) pre-computed and cached
  4. At bar close: if shift confirmed → execute escape instantly. False alarm → proceed normally.

30-second early warning that no bar-level model can match.


Inference Service Architecture

New component in services/trading-agent/ (or extend existing):

struct TickProcessor {
    calculator: IncrementalMicrostructureCalculator,
    databento_client: databento::LiveClient,
    h100_channel: tokio::sync::mpsc::Sender<MicrostructureSnapshot>,
}

impl TickProcessor {
    /// Subscribe to Databento live MBP-10 for ES
    async fn run(&mut self) {
        let subscription = self.databento_client
            .subscribe("GLBX.MDP3", "mbp-10", &["ES"])
            .await;
        
        while let Some(event) = subscription.next().await {
            self.calculator.update(&event.snapshot, event.trade.as_ref());
            
            // Every 5s: send intermediate snapshot to H100 for speculative compute
            if self.calculator.tick_count % SPECULATIVE_INTERVAL == 0 {
                let snapshot = self.calculator.snapshot();
                self.h100_channel.send(snapshot).await;
            }
        }
    }
    
    /// Called at bar close — returns final features
    fn bar_close(&mut self) -> MicrostructureFeatures {
        let features = self.calculator.snapshot();
        self.calculator.reset();
        features
    }
}

Implementation Order

  1. IncrementalMicrostructureCalculator — 12 new features in ofi_calculator.rs
  2. fxcache.rs — OFI_DIM 8→20, record size 224→272
  3. precompute_features.rs — use new calculator, output 20 OFI
  4. Regenerate all fxcache files on PVC
  5. Experience collector — load 20 OFI, widen state buffers
  6. DQN config — state_dim grows by 12
  7. Trunk w_s1 resize — automatic via config
  8. Databento live integration — TickProcessor service
  9. H100 between-bar speculative compute
  10. Smoke test + verification

Risks

Risk Mitigation
Existing fxcache files incompatible Regenerate all (~5-10 min). Code and data deploy atomically — new code rejects old fxcache.
OFI_DIM=8 hardcoded in 6+ files All sites enumerated in spec. Must update atomically in single commit. Use shared constant, not literals.
12 new features → overfitting on training data ISV feature gating suppresses irrelevant features. 12 features / 4M bars = negligible risk.
IncrementalMicrostructureCalculator state drift Reset at every bar boundary. Running statistics are bounded by definition.
Databento live feed drops/gaps Calculator handles missing ticks gracefully (EMA continues with last value). At bar close, features reflect whatever ticks arrived.
Speculative trunk cache miss rate high Free option. Cache hit → 10× faster. Cache miss → normal path. Measure hit rate and tune threshold.
Precompute changes bar alignment Use same bar boundaries as existing pipeline. Ticks are assigned to bars by timestamp range.
Hawkes intensity numerically unstable Clamp intensity to [0, 100]. Exponential decay with β=1.0 prevents unbounded growth.
Lee-Ready classification errors Standard approach in literature. Errors are symmetric (equal chance of misclassifying buy as sell). Net effect averages out over 2000 ticks.