Source migration: - crates/ml-backtesting/src/lob/mod.rs:5 + policy/mod.rs:18: local const N_HORIZONS 5→3 via re-export from ml_alpha::heads::N_HORIZONS (single source of truth across crates) - crates/ml-backtesting/src/sim/mod.rs:1751,1773,1784: [IsvKellyStateHost; 5] array literals → ; N_HORIZONS] - crates/ml-backtesting/cuda/lob_state.cuh:9: #define N_HORIZONS 5→3 (this triggers cubin rebuild of decision_policy + 4 other ml-backtesting kernels that #include this header) Test migration (8 test files + 2 JSON fixtures): - threshold_and_cost.rs, decision_floor_coldstart.rs, parallel_sim_ correctness.rs, stop_controller.rs (37+10+1 broadcast_alpha calls), lob_sim_integrated_fuzz.rs, lob_sim_fixtures.rs: hardcoded [f32; 5] alpha-probs and [IsvKellyStateHost; 5] arrays → N_HORIZONS-sized via std::array::from_fn or [v; N_HORIZONS] literals - trainer_parity.rs:34 + ring3_replay.rs:47: horizons literal [30,100,300,1000,6000] → ml_alpha::heads::HORIZONS - fixtures/decision_alpha_buy_close.json + decision_program_h4_only.json: 5-element warm_start_isv_kelly / alpha_probs / expected_isv_kelly_after trimmed to 3 elements; active horizon relocated to N_HORIZONS-1 Library lib-test rewrite (per pearl_tests_must_prove_not_lock_observations): - crates/ml-backtesting/src/policy/mod.rs:197-233: lib tests default_strategy_has_5_horizon_leaves + ..._flattens_to_5_emits... renamed to N_HORIZONS-parametric form (observed-value 5 and 7 were bug-locks). cargo check -p ml-backtesting --all-targets: clean. cargo test -p ml-backtesting --lib: 33 passed. cargo test -p ml-backtesting --tests (non-CUDA, non-fixture-data): 2 passed, 53 ignored (CUDA-gated or FOXHUNT_TEST_DATA-gated). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.6 KiB
Rust
87 lines
3.6 KiB
Rust
//! P4 regression: threshold gate + per-fill cost integration.
|
|
//!
|
|
//! Tight tests focused on the GATE's pre-Kelly skip path and the cost
|
|
//! plumbing in apply_fill_to_pos. The full kelly_state_sees_net_return
|
|
//! end-to-end test requires a full submit_market → fill → close sequence
|
|
//! which is exercised by the production smoke; here we test the kernel
|
|
//! contract in isolation.
|
|
|
|
use anyhow::Result;
|
|
use ml_backtesting::policy::N_HORIZONS;
|
|
use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams};
|
|
use ml_core::device::MlDevice;
|
|
|
|
fn cfg_with_threshold(n: usize, threshold: f32, cost: f32) -> BatchedSimConfig {
|
|
BatchedSimConfig::from_uniform(n, &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,
|
|
cost_per_lot_per_side: cost,
|
|
max_hold_ns: 0,
|
|
min_reasonable_px: 0.0,
|
|
max_reasonable_px: f32::INFINITY,
|
|
delta_floor: 0.0,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires CUDA"]
|
|
fn threshold_gate_skips_low_conviction() -> 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(1, &dev)?;
|
|
// CRT.1 C1.2: at cold-start ISV all weights are eps_edge / cost² and
|
|
// identical across horizons. With uniform alpha=0.51 across all 5
|
|
// horizons (all bullish), magnitude_h = 0.02 and conviction_signed =
|
|
// 0.02 — well below threshold = 0.10. Kernel writes side=2 (no-op).
|
|
sim.broadcast_alpha(&[0.51; N_HORIZONS])?;
|
|
sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.10, 1.0))?;
|
|
let (side, size) = sim.read_market_target(0)?;
|
|
assert_eq!(side, 2, "side should be noop under threshold gate; got side={side}");
|
|
assert_eq!(size, 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires CUDA"]
|
|
fn threshold_gate_allows_high_conviction() -> 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(1, &dev)?;
|
|
// CRT.1 C1.2: uniform alpha=0.8 across horizons → magnitude_h = 0.6,
|
|
// direction_h = +1 for all → conviction_signed = 0.6, above threshold
|
|
// = 0.10. target_lots = round(1 * 0.6 * 5) = 3.
|
|
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
|
sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.10, 1.0))?;
|
|
let (side, size) = sim.read_market_target(0)?;
|
|
assert_eq!(side, 0, "side should be buy with strong alpha; got side={side}");
|
|
assert!(size >= 1, "size {size} < 1 — threshold gate may be over-restricting");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires CUDA"]
|
|
fn threshold_zero_is_passthrough() -> Result<()> {
|
|
// Sanity check: threshold = 0.0 should pass any non-zero conviction
|
|
// through to a non-zero target. conviction_abs >= 0 always.
|
|
let dev = match MlDevice::cuda(0) {
|
|
Ok(d) => d,
|
|
Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); }
|
|
};
|
|
let mut sim = LobSimCuda::new(1, &dev)?;
|
|
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
|
sim.step_decision_with_latency(0, &cfg_with_threshold(1, 0.0, 1.0))?;
|
|
let (side, size) = sim.read_market_target(0)?;
|
|
assert_eq!(side, 0, "threshold=0 with strong alpha should pass through; got side={side}");
|
|
assert!(size >= 1, "size {size} < 1 — passthrough behaviour broken");
|
|
Ok(())
|
|
}
|