//! Ring 1b — trainer/backtest parity check. See spec §8 Ring 1b. //! //! Verifies that the data-loading path used by the backtest harness //! (`inference_only=true`) produces an Mbp10RawInput that is byte-equal //! to what the trainer's loader produces from the same source. //! Since both paths share the same loader code and same Mbp10Snapshot //! → Mbp10RawInput conversion, the equality is structurally guaranteed — //! but the test guards against any future refactor that breaks the claim. //! //! The full inference-output parity (asserting [probs; N_HORIZONS] from //! the trunk's captured graph is bit-equal between training-time loader //! and backtest-time loader paths) requires a real ml-alpha checkpoint, //! which is deferred until a checkpoint-format is pinned in ml-alpha. //! When a checkpoint is available, FOXHUNT_TEST_CKPT will gate the //! gpu-inference assertion below. use anyhow::Result; use ml_alpha::data::loader::{ discover_mbp10_files_sorted, InstrumentFilter, MultiHorizonLoader, MultiHorizonLoaderConfig, }; use std::path::PathBuf; fn try_loader(inference_only: bool) -> Option { let root = std::env::var("FOXHUNT_TEST_DATA").ok()?; let mbp10 = PathBuf::from(&root).join("ES.FUT"); if !mbp10.exists() { return None; } let files = discover_mbp10_files_sorted(&mbp10).ok()?; let cfg = MultiHorizonLoaderConfig { files, predecoded_dir: mbp10.clone(), multi_resolution: ml_alpha::data::aggregation::MultiResolutionConfig::default_three_scale(), horizons: ml_alpha::heads::HORIZONS, n_max_sequences: 1, seed: 0xCAFEF00D, inference_only, outcome_label_cost: ml_alpha::data::loader::DEFAULT_OUTCOME_LABEL_COST_ES, instrument_filter: InstrumentFilter::All, }; match MultiHorizonLoader::new(&cfg) { Ok(l) => Some(l), Err(e) => { eprintln!("skipping: fixture data not usable ({e})"); None } } } #[test] #[ignore = "requires populated FOXHUNT_TEST_DATA"] fn peek_first_byte_equal_across_modes() -> Result<()> { let Some(loader_train) = try_loader(false) else { return Ok(()); }; let Some(loader_bt) = try_loader(true) else { return Ok(()); }; let first_train = loader_train.peek_first()?; let first_bt = loader_bt.peek_first()?; // Inputs must be byte-equal (Mbp10RawInput is Pod via #[derive(Default)] // — bid_px/sz/ask_px/sz arrays of f32, plus a handful of scalars). assert_eq!(first_train.ts_ns, first_bt.ts_ns); assert_eq!(first_train.prev_ts_ns, first_bt.prev_ts_ns); for k in 0..10 { assert_eq!( first_train.bid_px[k].to_bits(), first_bt.bid_px[k].to_bits(), "bid_px[{k}] bit-mismatch between training and inference loader" ); assert_eq!(first_train.bid_sz[k].to_bits(), first_bt.bid_sz[k].to_bits()); assert_eq!(first_train.ask_px[k].to_bits(), first_bt.ask_px[k].to_bits()); assert_eq!(first_train.ask_sz[k].to_bits(), first_bt.ask_sz[k].to_bits()); } for k in 0..6 { assert_eq!( first_train.regime[k].to_bits(), first_bt.regime[k].to_bits(), "regime[{k}] bit-mismatch" ); } assert_eq!(first_train.prev_mid.to_bits(), first_bt.prev_mid.to_bits()); assert_eq!(first_train.trade_signed_vol.to_bits(), first_bt.trade_signed_vol.to_bits()); assert_eq!(first_train.trade_count, first_bt.trade_count); Ok(()) } #[test] #[ignore = "requires populated FOXHUNT_TEST_DATA"] fn inference_iteration_matches_chronological_snapshots() -> Result<()> { let Some(mut loader) = try_loader(true) else { return Ok(()); }; // Pull 5 inference inputs; verify timestamps are monotone non-decreasing // and each input's prev_ts_ns equals the previous input's ts_ns // (after the first, which uses cur=prev semantics). let mut prev_ts_ns = 0u64; let mut first_iter = true; for i in 0..5 { let snap = loader.next_inference_input()?.expect("stream not exhausted"); if first_iter { // peek_first/next-first uses cur==prev → prev_ts == ts. assert_eq!(snap.prev_ts_ns, snap.ts_ns, "first snap has prev_ts == ts"); first_iter = false; } else { assert_eq!( snap.prev_ts_ns, prev_ts_ns, "snap {i}: prev_ts_ns should equal previous snap's ts_ns" ); } assert!(snap.ts_ns >= prev_ts_ns, "snap {i}: monotonic ts violated"); prev_ts_ns = snap.ts_ns; } Ok(()) }