Files
foxhunt/crates/ml-backtesting/tests/lob_sim_fuzz.rs
jgrusewski cc4c47f471 audit(rust-consts): catch literal-vs-const drift + cleanup BOOK_LEVELS=10
Audit script (audit-rust-consts.sh) scans Rust src/examples for numeric
literals mirroring structural kernel-side consts (N_ACTIONS, Q_N_ATOMS,
HIDDEN_DIM, MAX_UNITS, BOOK_LEVELS). Closes the layer-3 gap noted in
feedback_use_consts_not_literals_for_structural_dims:

  Layer 1: kernel `#define` allowlist  → audit-isv
  Layer 2: Rust `pub const` canonical  → exists (e.g. N_ACTIONS in rl/common.rs)
  Layer 3: Rust literals mirroring (2) → audit-rust-consts (this commit)

Honors `// audit-ignore: <SYMBOL>` per-line markers and skips `[u8; N]`
byte-buffer patterns (high false-positive class — almost always I/O
scratch, not structural dims).

Cleanup driven by first run (19 real flags, no grandfathering):
* New canonical: `BOOK_LEVELS` in `ml-alpha/src/cfc/snap_features.rs`
  (10 book levels = same place as `Mbp10RawInput` struct)
* `ml-backtesting/src/lob/mod.rs`: redefine as `pub use` re-export from
  ml-alpha (single source of truth; ml-backtesting depends on ml-alpha
  via `Mbp10RawInput` already)
* 19 sites switched literal `10` → `BOOK_LEVELS`:
  - snap_features.rs:44-47 (struct fields)
  - data/loader.rs:872-876, 960 (Mbp10Snapshot → Mbp10RawInput convert)
  - data/aggregation.rs:161 (level-wise aggregation loop)
  - trainer/perception.rs:2750-2756, 6272-6278, 6686-6690, 7247-7253
    (snapshot → batch staging loops)
  - tests/lob_sim_fuzz.rs:21, lob_sim_integrated_fuzz.rs:22 (duplicate
    const → use ml_backtesting::lob::BOOK_LEVELS)
* 5 sites marked `// audit-ignore: BOOK_LEVELS — <reason>`:
  - harness.rs:572,574,594 (conviction-bucket histograms, 10 ≠ depth)
  - multi_horizon_labels.rs:489,557,564 (10-element test price vecs)

Re-run after fixes: 0 suspect literals flagged. PASS.
2026-05-24 17:39:40 +02:00

149 lines
5.2 KiB
Rust

//! Ring 2 — property-based fuzz at N ∈ {1, 16, 256}.
//!
//! Random-walk MBP-10 sequences applied to the LOB simulator; assert
//! invariants that must hold regardless of input or scheduling:
//!
//! * bid_px monotonically non-increasing across levels
//! * ask_px monotonically non-decreasing across levels
//! * ask_px[0] > bid_px[0] (no crossed book)
//! * Position lots stays within plausible bounds even under
//! random market orders
//! * Realised P&L finite (no NaN/Inf leaks)
//!
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §8 Ring 2.
use anyhow::Result;
use ml_backtesting::lob::BOOK_LEVELS;
use ml_backtesting::sim::LobSimCuda;
use ml_core::device::MlDevice;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
const TICK: f32 = 0.25;
fn make_initial_book() -> ([f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LEVELS]) {
let mut bid_px = [0.0; BOOK_LEVELS];
let mut bid_sz = [0.0; BOOK_LEVELS];
let mut ask_px = [0.0; BOOK_LEVELS];
let mut ask_sz = [0.0; BOOK_LEVELS];
let mid = 5500.00_f32;
for k in 0..BOOK_LEVELS {
bid_px[k] = mid - TICK * (k as f32 + 1.0);
ask_px[k] = mid + TICK * (k as f32 + 1.0);
bid_sz[k] = 5.0 + 5.0 * k as f32;
ask_sz[k] = 5.0 + 5.0 * k as f32;
}
(bid_px, bid_sz, ask_px, ask_sz)
}
fn step_random_walk(
rng: &mut ChaCha8Rng,
bid_px: &mut [f32; BOOK_LEVELS],
bid_sz: &mut [f32; BOOK_LEVELS],
ask_px: &mut [f32; BOOK_LEVELS],
ask_sz: &mut [f32; BOOK_LEVELS],
) {
// Random mid drift in {-TICK, 0, +TICK} with mild bias.
let drift_choices = [-TICK, 0.0, 0.0, 0.0, TICK];
let drift = drift_choices[rng.gen_range(0..drift_choices.len())];
// Shift the whole book by drift, preserving level structure.
for k in 0..BOOK_LEVELS {
bid_px[k] += drift;
ask_px[k] += drift;
// Jitter sizes; clamp positive.
let dbz: f32 = rng.gen_range(-3.0..3.0);
let daz: f32 = rng.gen_range(-3.0..3.0);
bid_sz[k] = (bid_sz[k] + dbz).max(1.0);
ask_sz[k] = (ask_sz[k] + daz).max(1.0);
}
}
fn assert_book_invariants(b_idx: usize, bid_px: &[f32], bid_sz: &[f32], ask_px: &[f32], ask_sz: &[f32]) {
for k in 1..BOOK_LEVELS {
assert!(
bid_px[k] <= bid_px[k - 1] + 1e-4,
"backtest {b_idx}: bid_px non-monotone at level {k}: {} > {}",
bid_px[k], bid_px[k - 1]
);
assert!(
ask_px[k] >= ask_px[k - 1] - 1e-4,
"backtest {b_idx}: ask_px non-monotone at level {k}: {} < {}",
ask_px[k], ask_px[k - 1]
);
}
assert!(
ask_px[0] > bid_px[0],
"backtest {b_idx}: crossed book — ask[0]={}, bid[0]={}",
ask_px[0], bid_px[0]
);
for k in 0..BOOK_LEVELS {
assert!(bid_sz[k] > 0.0, "backtest {b_idx}: bid_sz[{k}] non-positive");
assert!(ask_sz[k] > 0.0, "backtest {b_idx}: ask_sz[{k}] non-positive");
assert!(bid_px[k].is_finite() && ask_px[k].is_finite());
}
}
fn run_fuzz_n(n_backtests: usize, n_events: usize, with_orders: bool, seed: u64) -> Result<()> {
let dev = MlDevice::cuda(0).map_err(|e| anyhow::anyhow!("cuda dev: {e}"))?;
let mut sim = LobSimCuda::new(n_backtests, &dev)?;
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let (mut bid_px, mut bid_sz, mut ask_px, mut ask_sz) = make_initial_book();
let mut ts_ns: u64 = 1_000_000_000;
for event_idx in 0..n_events {
step_random_walk(&mut rng, &mut bid_px, &mut bid_sz, &mut ask_px, &mut ask_sz);
sim.apply_snapshot(&bid_px, &bid_sz, &ask_px, &ask_sz)?;
// 1 in 8 events: each backtest submits a random market order.
if with_orders && event_idx % 8 == 0 {
for b in 0..n_backtests {
let side: u8 = if rng.gen_bool(0.5) { 0 } else { 1 };
let size: u16 = rng.gen_range(1..=3);
sim.submit_market(b, side, size, ts_ns)?;
}
}
// Invariants on book state.
let books = sim.read_books()?;
for (b, book) in books.iter().enumerate() {
assert_book_invariants(b, &book.bid_px, &book.bid_sz, &book.ask_px, &book.ask_sz);
}
if with_orders {
// Position + P&L sanity per backtest.
for b in 0..n_backtests {
let pos = sim.read_pos(b)?;
assert!(
pos.position_lots.abs() <= 30,
"backtest {b}: position_lots out of plausible range: {}",
pos.position_lots
);
assert!(pos.realized_pnl.is_finite(), "backtest {b}: realized_pnl NaN/Inf");
assert!(pos.vwap_entry.is_finite(), "backtest {b}: vwap_entry NaN/Inf");
}
}
ts_ns += 1_000_000;
}
Ok(())
}
#[test]
#[ignore = "requires CUDA"]
fn fuzz_n1_book_only() -> Result<()> {
run_fuzz_n(1, 200, /*with_orders=*/ false, 0xCAFE_F00D)
}
#[test]
#[ignore = "requires CUDA"]
fn fuzz_n16_with_orders() -> Result<()> {
run_fuzz_n(16, 200, /*with_orders=*/ true, 0xDEAD_BEEF)
}
#[test]
#[ignore = "requires CUDA"]
fn fuzz_n256_with_orders() -> Result<()> {
run_fuzz_n(256, 100, /*with_orders=*/ true, 0x1234_5678)
}