Property-based fuzz tests with random-walk MBP-10 sequences applied to
the LOB simulator at three backtest counts; assert per-snapshot
invariants that must hold regardless of input or block scheduling:
fuzz_n1_book_only (200 events, no orders)
Pure book-update kernel — verifies mid-drift random walks preserve
book monotonicity and never produce a crossed book.
fuzz_n16_with_orders (200 events, market orders every 8th event)
16 backtests in parallel, each submitting random buy/sell market
orders 1-3 lots at every 8th event. Asserts book invariants on
each backtest's state PLUS:
- position_lots stays within ±30 (plausible given fixture book depth)
- realized_pnl + vwap_entry finite (no NaN/Inf leaks)
fuzz_n256_with_orders (100 events, market orders)
Production-scale parallelism. Same invariants as N=16. Each block
has its own per-backtest Pos + OpenTradeState + TradeLog, exercising
the per-block isolation discipline established in C5-C7.
All 3 pass on RTX 3050. Spec §8 Ring 2 confidence gate hit.
Adds rand + rand_chacha to ml-backtesting dev-dependencies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
149 lines
5.2 KiB
Rust
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::sim::LobSimCuda;
|
|
use ml_core::device::MlDevice;
|
|
use rand::{Rng, SeedableRng};
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
const BOOK_LEVELS: usize = 10;
|
|
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)
|
|
}
|