From 8b68954ded62056eaf78770bb53b146fe2eb87b6 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 17 Apr 2026 00:29:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(tick):=20precompute=20pipeline=20wires=20M?= =?UTF-8?q?icrostructureState=20=E2=80=94=2020=20OFI=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 new tick-level features computed per bar from MBP-10 snapshots: OFI trajectory, realized variance, Hawkes intensity, book pressure, spread dynamics, aggression, queue depletion, order count flux, intra-bar momentum, regime score, OFI acceleration, toxicity gradient. Combined with existing 8 OFI into [f64; 20] per bar in fxcache. Also fixes pre-existing bug: no-MBP-10 fallback was [0.0; 8] not [0.0; 20]. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/ml/examples/precompute_features.rs | 42 ++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/ml/examples/precompute_features.rs b/crates/ml/examples/precompute_features.rs index 45e769338..281174f1b 100644 --- a/crates/ml/examples/precompute_features.rs +++ b/crates/ml/examples/precompute_features.rs @@ -391,32 +391,58 @@ async fn main() -> Result<()> { }; // Compute per-bar OFI - use ml::features::mbp10_loader::get_snapshots_for_timestamp; use ml::features::trades_loader::get_trades_for_bar; + use ml::features::ofi_calculator::MicrostructureState; let mut calculator = OFICalculator::new(); let mut ofi_per_bar = Vec::with_capacity(n); for i in 0..n { let bar = &all_bars[i + WARMUP]; let bar_ts = bar.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64; + let bar_end_ts = all_bars.get(i + WARMUP + 1) + .map(|b| b.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64) + .unwrap_or(bar_ts + 60_000_000_000); + let bar_duration_ns = bar_end_ts - bar_ts; - // Feed trades for this bar window + let mut micro_state = MicrostructureState::new(bar_ts, bar_duration_ns); + + // Feed trades for this bar window (OFICalculator + MicrostructureState) if let Some(ref trades) = all_trades { - let bar_end_ts = all_bars.get(i + WARMUP + 1) - .map(|b| b.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64) - .unwrap_or(bar_ts + 60_000_000_000); for trade in get_trades_for_bar(trades, bar_ts, bar_end_ts) { calculator.feed_trade(trade.price, trade.volume, trade.is_buy); + micro_state.update_trade(trade.price, trade.volume, trade.is_buy, trade.timestamp); } } - let window = get_snapshots_for_timestamp(&all_snapshots, bar_ts, 1); - if let Some(snap) = window.first() { + // Get ALL MBP-10 snapshots within this bar window for tick-level features. + // Binary search for bar range: [bar_ts, bar_end_ts) + let snap_start = all_snapshots.partition_point(|s| s.timestamp < bar_ts); + let snap_end = all_snapshots.partition_point(|s| s.timestamp < bar_end_ts); + let bar_snapshots = &all_snapshots[snap_start..snap_end]; + + // Feed every snapshot to MicrostructureState for tick-level resolution + for snap in bar_snapshots { + micro_state.update_snapshot(snap); + } + + // Use the LAST snapshot in the bar for OFI calculation (matches prior behavior) + let last_snap = bar_snapshots.last() + .or_else(|| { + // Fallback: nearest snapshot at/after bar_ts (prior behavior) + all_snapshots.get(snap_start) + }); + + if let Some(snap) = last_snap { match calculator.calculate(snap) { Ok(f) if f.is_valid() => { + // Feed OFI-derived values to MicrostructureState + micro_state.update_ofi_derived(f.ofi_level1, f.vpin); + let arr8 = f.to_array(); + let micro_12 = micro_state.snapshot(); let mut arr20 = [0.0_f64; 20]; arr20[..8].copy_from_slice(&arr8); + arr20[8..20].copy_from_slice(µ_12); ofi_per_bar.push(arr20); } _ => ofi_per_bar.push([0.0; 20]), @@ -435,7 +461,7 @@ async fn main() -> Result<()> { } } else { info!("No MBP-10 directory, OFI will be zeros"); - vec![[0.0; 8]; n] + vec![[0.0; 20]; n] }; let total_len = n;