From a06abf60df6f2c4dd66b8a5ae07c0d683b850d9b Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 22 May 2026 20:46:55 +0200 Subject: [PATCH] test(ml-alpha): synthetic multi-resolution pipeline test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four pure-CPU tests confirming aggregate_window emits the right ts_ns - prev_ts_ns span per scale, so the existing snap-feature Δt Fourier encoder gets correctly-scaled inputs without any CUDA kernel changes. --- .../tests/multi_resolution_pipeline.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/ml-alpha/tests/multi_resolution_pipeline.rs diff --git a/crates/ml-alpha/tests/multi_resolution_pipeline.rs b/crates/ml-alpha/tests/multi_resolution_pipeline.rs new file mode 100644 index 000000000..e2e00bd69 --- /dev/null +++ b/crates/ml-alpha/tests/multi_resolution_pipeline.rs @@ -0,0 +1,84 @@ +//! End-to-end pipeline test using synthetic Mbp10RawInput data. +//! +//! Pure-CPU test (no GPU, no FOXHUNT_TEST_DATA gate). Confirms that +//! `aggregate_window` emits ts_ns / prev_ts_ns spans matching the design, +//! so the snap-feature Δt Fourier encoder receives the correct per-position +//! dt for each scale. + +use ml_alpha::cfc::snap_features::Mbp10RawInput; +use ml_alpha::data::aggregation::{aggregate_window, MultiResolutionConfig}; + +#[test] +fn default_three_scale_position_layout() { + let cfg = MultiResolutionConfig::default_three_scale(); + assert_eq!(cfg.scales(), &[(1, 10), (30, 10), (100, 12)]); + assert_eq!(cfg.total_positions(), 32); + assert_eq!(cfg.required_lookback_ticks(), 1510); +} + +#[test] +fn aggregate_window_30_tick_dt_spans_window() { + let snaps: Vec = (0..30u64) + .map(|i| { + let mut x = Mbp10RawInput::default(); + x.ts_ns = 1000 + i * 100; + x.prev_ts_ns = x.ts_ns.saturating_sub(100); + x + }) + .collect(); + let agg = aggregate_window(&snaps); + let dt = agg.ts_ns - agg.prev_ts_ns; + assert_eq!(dt, 29 * 100, "30-tick window @ 100ns spacing -> dt = 2900ns"); +} + +#[test] +fn aggregate_window_100_tick_dt_much_larger_than_raw_dt() { + // 100-tick window @ 100ns spacing -> dt = 99 * 100 = 9900ns + let snaps_100: Vec = (0..100u64) + .map(|i| { + let mut x = Mbp10RawInput::default(); + x.ts_ns = 5000 + i * 100; + x.prev_ts_ns = x.ts_ns.saturating_sub(100); + x + }) + .collect(); + let agg_100 = aggregate_window(&snaps_100); + let dt_100 = agg_100.ts_ns - agg_100.prev_ts_ns; + + // 1-tick raw position -> dt = 100ns (the input's own prev_ts_ns offset). + let raw = Mbp10RawInput { + ts_ns: 5000, + prev_ts_ns: 4900, + ..Default::default() + }; + let dt_raw = raw.ts_ns - raw.prev_ts_ns; + + // 9900 vs 100 -> 99x ratio. Confirms the snap-feature Δt encoder will + // see a clearly distinct time-scale signature per position. + assert!( + dt_100 as f32 / dt_raw as f32 > 50.0, + "coarse-scale dt={dt_100} should be >50x raw dt={dt_raw}" + ); +} + +#[test] +fn aggregate_window_prev_mid_is_first_snapshot_mid() { + // Walking returns calc: cur.mid (last) - agg.prev_mid (first) should + // equal the realized return across the window. + let snaps: Vec = (0..30u64) + .map(|i| { + let mut x = Mbp10RawInput::default(); + x.prev_mid = 5000.0 + i as f32; + x.bid_px[0] = x.prev_mid - 0.125; + x.ask_px[0] = x.prev_mid + 0.125; + x.ts_ns = 1000 + i * 100; + x.prev_ts_ns = x.ts_ns.saturating_sub(100); + x + }) + .collect(); + let agg = aggregate_window(&snaps); + assert!( + (agg.prev_mid - 5000.0).abs() < 1e-5, + "agg.prev_mid should equal first snapshot's prev_mid" + ); +}