The max_hold force-close used event-timestamp nanoseconds, but MBP-10 events fire at variable rates. At dense periods (~100 events/sec), a 60s cap = 6000 events = 6000 training steps — effectively never fires. Observed: alpha-rl-final-f0 step 1500+ → dones=0 (model "hold forever" attractor), pnl growing from unrealized drift, not real trades. Fix: switch to step-based max_hold. - Added entry_step: u32 to PosFlat at offset 28 (struct 28 → 32 bytes) - order_match.cu + resting_orders.cu record entry_step on open/flip - Replaced ns-based check with step-based: current_step - entry_step - max_hold_steps_d replaces max_hold_ns_d in LobSimCuda - alpha_rl_train.rs sets max_hold_steps=100 (matches γ-derived min_hold) - New fill_u32.cu kernel; deleted fill_u64.cu (no remaining consumers) The mega-graph captures self.isv_dev_ptr + 548*4 as current_step pointer so rl_increment_step's in-graph ISV[548] mutation drives the lobsim clock live during fast-path replay. Local smoke 1000 steps (b=32, per=4096): - 750 trades, dones fire consistently every 100-step window - avg_hold stabilizes at 35 (well within 100-step cap) - No "never close" collapse - All architectural fixes preserved Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
62 lines
2.4 KiB
Rust
62 lines
2.4 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::policy::N_HORIZONS;
|
|
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; N_HORIZONS])?;
|
|
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,
|
|
// CRT.1 C1.2: cost > 0 is required for the §4.4 conviction
|
|
// formula's eps_edge floor; 1.0 is a typical futures
|
|
// round-trip cost.
|
|
cost_per_lot_per_side: 1.0,
|
|
max_hold_steps: 0,
|
|
delta_floor: 0.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(())
|
|
}
|