Files
foxhunt/crates/ml-backtesting/tests/parallel_sim_correctness.rs
jgrusewski c03cf9aa38 fix(ml-backtesting): parameterized price-range sanitization replaces hardcoded 1e8
Audit (fxt-data-audit on ES.FUT_2024-Q1) revealed real source-data
outliers that the hardcoded < 1e8 threshold didn't catch:
- bid_min = -$4.85 (negative bid)
- bid_p1 = $64.15 (1% of bids in sub-$100 range, far below ES)
- ask_max = $53,012 (10x above any plausible ES price)

The < 1e8 threshold = $100M was useless: never triggered on real ES.

Fix: parameterize the range. min_reasonable_px / max_reasonable_px as
per-backtest fields in UniformSimParams, ResolvedSimVariant, and
BatchedSimConfig (defaults 0.0 / f32::INFINITY = no-check, preserves
existing test fixtures). Sweep_smoke.yaml sets 1000/20000 for ES
futures — catches all observed outliers without rejecting any plausible
price. BacktestHarness calls upload_price_range() once after creating
the sim so the bounds are active before the first apply_snapshot.

CUDA kernel book_update_apply_snapshot gains two new args
(min_reasonable_px[n_backtests], max_reasonable_px[n_backtests]) that
replace the hardcoded > 0.0f && < 1.0e8f checks at both the top-of-book
gate and the per-level sanitization pass.

Test price_range_rejection_skips_snapshot validates: sub-$1000
snapshot, super-$20000 snapshot, and negative bid all skip with
snapshots_skipped counter increment; valid ES snapshot passes through.
17/17 stop_controller tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:31:43 +02:00

57 lines
2.1 KiB
Rust

//! P1 regression test: per-backtest sim parameter arrays. With uniform
//! config across N backtests, all N must produce the same market_target
//! decision (proves the per-backtest indexing reduces correctly to the
//! pre-refactor scalar-broadcast semantics).
//!
//! Pair: the independence test (proving DIFFERENT per-backtest configs
//! produce different outputs) lands alongside the P2 inflight-limits
//! kernel where the read_first_inflight_arrival_ts helper exists.
use anyhow::Result;
use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams};
use ml_core::device::MlDevice;
#[test]
#[ignore = "requires CUDA"]
fn parallel_sim_equivalence_with_uniform_config() -> Result<()> {
let dev = match MlDevice::cuda(0) {
Ok(d) => d,
Err(e) => {
eprintln!("skipping: cuda device unavailable ({e})");
return Ok(());
}
};
let mut sim = LobSimCuda::new(8, &dev)?;
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
let cfg = BatchedSimConfig::from_uniform(
8,
&UniformSimParams {
target_annual_vol_units: 50.0,
annualisation_factor: 825.0,
max_lots: 5,
latency_ns: 0,
kelly_frac_floor: 0.20,
sharpe_weight_floor: 0.10,
threshold: 0.0,
cost_per_lot_per_side: 0.0,
max_hold_ns: 0,
min_reasonable_px: 0.0,
max_reasonable_px: f32::INFINITY,
},
);
sim.step_decision_with_latency(0, &cfg)?;
let first = sim.read_market_target(0)?;
for b in 1..8 {
let got = sim.read_market_target(b)?;
assert_eq!(
got, first,
"backtest {b} differs from backtest 0 under uniform config: {got:?} vs {first:?}"
);
}
// And the uniform-config result must equal the result we'd get from
// a single-backtest sim (preserves pre-refactor behaviour).
assert_eq!(first.0, 0, "uniform config with p_h=0.8 should produce a buy; got side={}", first.0);
assert!(first.1 >= 1, "uniform-config size {} < 1", first.1);
Ok(())
}