feat(tick): precompute pipeline wires MicrostructureState — 20 OFI features

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-17 00:29:03 +02:00
parent b2d7af0763
commit 8b68954ded

View File

@@ -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(&micro_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;