diff --git a/bin/fxt-backtest/src/main.rs b/bin/fxt-backtest/src/main.rs index 068d4d963..5f21026b0 100644 --- a/bin/fxt-backtest/src/main.rs +++ b/bin/fxt-backtest/src/main.rs @@ -67,9 +67,6 @@ struct RunArgs { /// If omitted, every cell gets Strategy::default_for(--max-lots). #[arg(long)] policy_grid: Option, - /// Decide every Sth event (matches training stride; default 4). - #[arg(long, default_value_t = 4)] - decision_stride: usize, /// Total submission→fill latency in nanoseconds (IBKR + Scaleway /// baseline = 100_000_000 = 100ms). Propagated to the resting-order /// engine so in-flight orders wait for arrival_ts_ns before crossing. @@ -188,7 +185,6 @@ struct SweepBase { #[serde(default)] predecoded_dir: Option, #[serde(default = "default_n_parallel")] n_parallel: usize, - #[serde(default = "default_decision_stride")] decision_stride: usize, #[serde(default = "default_latency_ns")] latency_ns: u32, #[serde(default = "default_target_vol")] target_annual_vol_units: f32, #[serde(default = "default_ann_factor")] annualisation_factor: f32, @@ -232,7 +228,6 @@ struct SimVariant { } fn default_n_parallel() -> usize { 1 } -fn default_decision_stride() -> usize { 4 } fn default_latency_ns() -> u32 { 100_000_000 } fn default_target_vol() -> f32 { 50.0 } fn default_ann_factor() -> f32 { 825.0 } @@ -255,7 +250,6 @@ struct SweepCell { /// SweepBase.data_template. When set, the cell's data path is /// data_template.replace("{window}", window). #[serde(default)] window: Option, - #[serde(default)] decision_stride: Option, #[serde(default)] latency_ns: Option, #[serde(default)] target_annual_vol_units: Option, #[serde(default)] annualisation_factor: Option, @@ -370,8 +364,7 @@ fn sweep(args: SweepArgs) -> Result<()> { n_parallel: grid.base.n_parallel, policy_grid: None, // sweep cells use the default policy; nested // grid-of-grids is a separate v2 concern. - decision_stride: cell.decision_stride.unwrap_or(grid.base.decision_stride), - latency_ns: cell.latency_ns.unwrap_or(grid.base.latency_ns), + latency_ns: cell.latency_ns.unwrap_or(grid.base.latency_ns), target_annual_vol_units: cell.target_annual_vol_units.unwrap_or(grid.base.target_annual_vol_units), annualisation_factor: @@ -456,7 +449,6 @@ fn run_batched_cell( data_root: cell_data.to_path_buf(), predecoded_dir: base.predecoded_dir.clone().unwrap_or_else(|| cell_data.to_path_buf()), n_parallel: n, - decision_stride: cell.decision_stride.unwrap_or(base.decision_stride), target_annual_vol_units: base.target_annual_vol_units, // overridden by sim_config annualisation_factor: base.annualisation_factor, max_lots: base.max_lots, @@ -543,7 +535,6 @@ fn run(args: RunArgs) -> Result<()> { data_root: args.data.clone(), predecoded_dir: args.predecoded_dir.clone().unwrap_or(args.data), n_parallel: args.n_parallel, - decision_stride: args.decision_stride, target_annual_vol_units: args.target_annual_vol_units, annualisation_factor: args.annualisation_factor, max_lots: args.max_lots, diff --git a/config/ml/sweep_decision_stride_example.yaml b/config/ml/sweep_decision_stride_example.yaml index 5fbf8a42c..31297846b 100644 --- a/config/ml/sweep_decision_stride_example.yaml +++ b/config/ml/sweep_decision_stride_example.yaml @@ -1,16 +1,16 @@ # Example sweep grid for `fxt-backtest sweep`. -# Mirrors the decision_stride sweep deferred from the original plan. +# A1: decision_stride removed. This file previously swept stride={1,2,4,8}; +# those cell overrides are now no-ops. Repurposed as a threshold sweep example. # # Run: # fxt-backtest sweep --grid config/ml/sweep_decision_stride_example.yaml \ -# --out results/sweep_stride -# Produces results/sweep_stride/{stride_1,stride_2,stride_4,stride_8}/{summary.json,trades.csv,pnl_curve.bin} +# --out results/sweep_example +# Produces results/sweep_example/{cell_1,...}/{summary.json,trades.csv,pnl_curve.bin} # plus aggregate.parquet + pareto_frontier.json at the root. base: data: test_data/futures-baseline/ES.FUT n_parallel: 1 - decision_stride: 4 latency_ns: 100000000 # 100 ms IBKR + Scaleway baseline target_annual_vol_units: 50.0 annualisation_factor: 825.0 @@ -20,11 +20,7 @@ base: # checkpoint: path/to/trained.bin # uncomment to use real weights cells: - - name: stride_1 - decision_stride: 1 - - name: stride_2 - decision_stride: 2 - - name: stride_4 - decision_stride: 4 - - name: stride_8 - decision_stride: 8 + - name: cell_1 + - name: cell_2 + - name: cell_3 + - name: cell_4 diff --git a/config/ml/sweep_deployability.yaml b/config/ml/sweep_deployability.yaml index 12330f11d..428b975b2 100644 --- a/config/ml/sweep_deployability.yaml +++ b/config/ml/sweep_deployability.yaml @@ -6,7 +6,6 @@ base: data_template: /mnt/training-data/futures-baseline-mbp10/ES.FUT/ES.FUT_{window}.dbn.zst predecoded_dir: /feature-cache/predecoded n_parallel: 1 # batched flow overrides with variants.len()=140 - decision_stride: 4 latency_ns: 200000000 # default (overridden per variant) target_annual_vol_units: 50.0 annualisation_factor: 825.0 diff --git a/config/ml/sweep_smoke.yaml b/config/ml/sweep_smoke.yaml index db54413bb..38c81d953 100644 --- a/config/ml/sweep_smoke.yaml +++ b/config/ml/sweep_smoke.yaml @@ -15,13 +15,8 @@ base: data: /mnt/training-data/futures-baseline-mbp10/ES.FUT predecoded_dir: /feature-cache/predecoded n_parallel: 1 # overridden by sim_variants.len() - # decision_stride matches the latency anchor: ~1ms per event × stride=200 - # = 200ms between decisions ≈ latency_ns. Prior smoke (n59t4) at stride=4 - # fired decisions 50× faster than orders could land; controller made - # decisions on phantom in-flight positions, churned ~96 trades/sec, and - # burned cost-per-trade × decision-rate. With stride=200, ~10k decisions - # over 2M events — natural cadence for a 200ms-latency deployment anchor. - decision_stride: 200 + # A1: decision_stride removed. Decisions now fire every event (event-rate + # CRT). With ~2M events, expect ~2M decisions (minus seq_len warmup). latency_ns: 200000000 # realistic anchor (overridden per variant) target_annual_vol_units: 50.0 annualisation_factor: 825.0 diff --git a/config/ml/sweep_threshold_tuning.yaml b/config/ml/sweep_threshold_tuning.yaml index 3abdebf3e..d24a5c86c 100644 --- a/config/ml/sweep_threshold_tuning.yaml +++ b/config/ml/sweep_threshold_tuning.yaml @@ -10,7 +10,6 @@ base: data_template: /mnt/training-data/futures-baseline-mbp10/ES.FUT/ES.FUT_{window}.dbn.zst predecoded_dir: /feature-cache/predecoded n_parallel: 1 # legacy default; overridden by variants.len() per cell - decision_stride: 4 latency_ns: 200000000 # anchor (Scaleway PAR → IBKR realistic RTT) target_annual_vol_units: 50.0 annualisation_factor: 825.0 diff --git a/crates/ml-alpha/examples/alpha_train.rs b/crates/ml-alpha/examples/alpha_train.rs index 6a255502b..e0d183e94 100644 --- a/crates/ml-alpha/examples/alpha_train.rs +++ b/crates/ml-alpha/examples/alpha_train.rs @@ -153,15 +153,6 @@ struct Cli { #[arg(long, default_value_t = 0)] cv_train_window: usize, - /// Decision-stride sampling: yield every S-th snapshot per training - /// sequence. S=1 (default) preserves current consecutive-snapshot - /// behaviour. S>1 expands the effective time-window covered by each - /// sequence (window = (seq_len-1) * S + 1 snapshots) at the same - /// compute cost. Pairs with Mamba2 dt_s scaling so the SSM's state - /// decay reflects the actual elapsed time between K-positions. - /// Recommended S=4 with K=64 for h6000 deployment (256-tick window). - #[arg(long, default_value_t = 1)] - decision_stride: usize, } #[derive(Serialize, serde::Deserialize, Default)] @@ -270,7 +261,6 @@ fn main() -> Result<()> { seed: cli.seed, horizon_weights, n_batch: cli.batch_size, - decision_stride: cli.decision_stride, }; let mut trainer = PerceptionTrainer::new(&dev, &trainer_cfg).context("trainer init")?; @@ -400,7 +390,6 @@ fn main() -> Result<()> { horizons, n_max_sequences: cli.n_train_seqs, seed: cli.seed, - decision_stride: cli.decision_stride, inference_only: false, }) .context("train loader")?; @@ -411,7 +400,6 @@ fn main() -> Result<()> { horizons, n_max_sequences: cli.n_val_seqs, seed: cli.seed.wrapping_add(0xC0FFEE), - decision_stride: cli.decision_stride, inference_only: false, }) .context("val loader")?; diff --git a/crates/ml-alpha/src/data/loader.rs b/crates/ml-alpha/src/data/loader.rs index 1bdf8c9fe..e058947a5 100644 --- a/crates/ml-alpha/src/data/loader.rs +++ b/crates/ml-alpha/src/data/loader.rs @@ -107,18 +107,6 @@ pub struct MultiHorizonLoaderConfig { /// seed only randomises which snapshot inside each file becomes /// the start of each yielded sequence.) pub seed: u64, - /// Decision-stride sampling. With `decision_stride = S`, position k - /// of the yielded sequence reads snapshot `anchor + k * S` from the - /// source file. `S = 1` (default) preserves the original - /// consecutive-snapshot behaviour. `S > 1` expands the effective - /// time-window covered by each sequence (window = (seq_len - 1) * S - /// + 1 snapshots) at the same K-positions compute cost. The - /// per-sequence `Δt = ts_ns - prev_ts_ns` then reflects the actual - /// elapsed time between consecutive K-positions, which is what - /// downstream Mamba2 SSM dt_s + Phase 2C TGN Fourier features - /// consume. Labels stay in absolute-snapshot horizons (h=6000 is - /// always "predict 6000 snapshots forward" regardless of stride). - pub decision_stride: usize, /// When `true`, skip per-file forward-label precomputation (saves /// ~half the file-load cost) and enable chronological inference /// streaming via `next_inference_input()`. Used by the ml-backtesting @@ -290,9 +278,8 @@ impl MultiHorizonLoader { /// Iterator-style accessor for inference mode: walks every loaded /// snapshot across every loaded file in chronological order. /// Returns `Ok(None)` when the stream is exhausted. Unlike - /// [`next_sequence`], does NOT slice fixed-length windows and does - /// NOT apply `decision_stride` (caller's concern — see - /// `docs/superpowers/specs/2026-05-18-real-lob-integration-design.md` §7). + /// [`next_sequence`], does NOT slice fixed-length windows. + /// A1: decision_stride removed; every snapshot is yielded consecutively. /// Mutates the inference cursor. Errors if `cfg.inference_only` is false. pub fn next_inference_input(&mut self) -> Result> { anyhow::ensure!( @@ -336,7 +323,8 @@ impl MultiHorizonLoader { return Ok(None); } let max_horizon = *self.cfg.horizons.iter().max().expect("non-empty horizons"); - let stride = self.cfg.decision_stride.max(1); + // A1: decision_stride removed; training sequences are always consecutive + // snapshots (stride = 1). dt_s = 1.0 in all CfC forward passes. // Pick a random file from the preloaded inventory, then a random // anchor within that file. Uniform-across-files sampling — each @@ -344,16 +332,11 @@ impl MultiHorizonLoader { let n_files = self.files_loaded.len(); let file_idx = self.rng.gen_range(0..n_files); let lf = &self.files_loaded[file_idx]; - // With stride S, position k reads snapshot `anchor + k * S`; the - // last sequence index is `anchor + (seq_len-1) * S`, and the - // label there looks `max_horizon` forward in raw snapshots - // (labels stay in absolute-snapshot units regardless of stride). - let last_seq_offset = (self.cfg.seq_len - 1) * stride; - let needed = last_seq_offset + 1 + max_horizon; + let needed = self.cfg.seq_len + max_horizon; anyhow::ensure!( lf.snapshots.len() > needed, - "file too short for seq_len={} stride={} max_horizon={}: {} <= {}", - self.cfg.seq_len, stride, max_horizon, lf.snapshots.len(), needed + "file too short for seq_len={} max_horizon={}: {} <= {}", + self.cfg.seq_len, max_horizon, lf.snapshots.len(), needed ); let max_anchor = lf.snapshots.len() - needed; let anchor: usize = self.rng.gen_range(0..max_anchor); @@ -362,28 +345,15 @@ impl MultiHorizonLoader { for h in 0..5 { let mut row = Vec::with_capacity(self.cfg.seq_len); for k in 0..self.cfg.seq_len { - row.push(lf.labels_full[h][anchor + k * stride]); + row.push(lf.labels_full[h][anchor + k]); } labels[h] = row; } let mut sequence = Vec::with_capacity(self.cfg.seq_len); for k in 0..self.cfg.seq_len { - let idx = anchor + k * stride; + let idx = anchor + k; let cur = &lf.snapshots[idx]; - // `prev` for microstructure features is the snapshot one - // K-position back in the strided sequence, NOT the - // consecutive-snapshot prior. This makes `Δt = ts_ns - - // prev_ts_ns` carry the actual elapsed time between - // consecutive K-positions — Mamba2's dt_s and the Phase 2C - // TGN Fourier features both consume this. - let prev_idx = if k == 0 { - // At k=0 there's no prior K-position; fall back to the - // immediately-prior snapshot (or self if anchor=0). Δt - // here will be ~tick-scale, but only at position 0. - if idx == 0 { 0 } else { idx - 1 } - } else { - anchor + (k - 1) * stride - }; + let prev_idx = if idx == 0 { 0 } else { idx - 1 }; let prev = &lf.snapshots[prev_idx]; sequence.push(convert(cur, prev, lf.regime_full[idx])); } @@ -567,7 +537,6 @@ mod inference_mode_tests { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 1, seed: 0xCAFEF00D, - decision_stride: 1, inference_only, }) } @@ -648,7 +617,6 @@ mod inference_mode_tests { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 1, seed: 0, - decision_stride: 1, inference_only: false, }; if let Ok(mut loader) = MultiHorizonLoader::new(&cfg) { diff --git a/crates/ml-alpha/src/trainer/perception.rs b/crates/ml-alpha/src/trainer/perception.rs index 3d3280ff1..70dce4917 100644 --- a/crates/ml-alpha/src/trainer/perception.rs +++ b/crates/ml-alpha/src/trainer/perception.rs @@ -82,13 +82,6 @@ pub struct PerceptionTrainerConfig { /// (cfc_step_batched, multi_horizon_heads_batched, etc.). /// `n_batch=1` matches the unbatched behavior exactly. pub n_batch: usize, - /// Decision-stride from the loader. Sets Mamba2's `dt_s` scalar to - /// reflect the time gap between consecutive K-positions: at stride=1 - /// dt_s=1.0; at stride=4 dt_s=4.0. Mamba2's selective scan uses - /// `exp(-dt * sigmoid(a))` for the state-decay gate, so this matters - /// for the SSM dynamics when stride > 1. `1` matches the legacy - /// consecutive-snapshot behaviour exactly. - pub decision_stride: usize, } impl Default for PerceptionTrainerConfig { @@ -101,7 +94,6 @@ impl Default for PerceptionTrainerConfig { seed: 0x4242, horizon_weights: [1.0; N_HORIZONS], n_batch: 1, - decision_stride: 1, } } } @@ -1736,10 +1728,8 @@ impl PerceptionTrainer { self.stream.memset_zeros(&mut self.zero_h_d) .map_err(|e| anyhow::anyhow!("zero zero_h: {e}"))?; - // Launch configs reused inside the K loop. With decision_stride S, - // dt_s = S so Mamba2's selective scan `exp(-dt * sigmoid(a))` - // reflects the actual elapsed time between consecutive K-positions. - let dt_s = self.cfg.decision_stride.max(1) as f32; + // A1: decision_stride removed; CRT runs at event rate, dt = 1.0 per step. + let dt_s = 1.0_f32; let n_in_i = HIDDEN_DIM as i32; let n_hid_i = HIDDEN_DIM as i32; let block_dim = 128u32; @@ -2794,7 +2784,8 @@ impl PerceptionTrainer { // Per-K CfC + heads. Pointer arithmetic on stable base addresses; // no host branches inside the captured region. - let dt_s = self.cfg.decision_stride.max(1) as f32; + // A1: decision_stride removed; CRT runs at event rate, dt = 1.0 per step. + let dt_s = 1.0_f32; let n_in_i = HIDDEN_DIM as i32; let n_hid_i = HIDDEN_DIM as i32; let cfc_fwd_smem = (2 * HIDDEN_DIM * std::mem::size_of::()) as u32; @@ -3107,9 +3098,9 @@ impl PerceptionTrainer { // ── 9. CfC single step. h_old is the persistent cfc_h_state_step; // h_new is written to cfc_h_new_step (then copied back into - // cfc_h_state_step). decision_stride=1 in event-rate mode — - // the harness no longer applies stride scaling to dt_s. - let dt_s: f32 = self.cfg.decision_stride.max(1) as f32; + // cfc_h_state_step). + // A1: decision_stride removed; event-rate dt = 1.0 per CfC step. + let dt_s: f32 = 1.0; let n_in_i: i32 = HIDDEN_DIM as i32; let n_hid_i: i32 = HIDDEN_DIM as i32; let n_batch_i: i32 = b_sz as i32; @@ -3642,10 +3633,9 @@ impl PerceptionTrainer { } self.stream.memset_zeros(&mut self.zero_h_d).map_err(|e| anyhow::anyhow!("zero h: {e}"))?; - // Per-K forward (recurrent CfC + heads, batched). Eval mirrors - // training: dt_s = decision_stride so Mamba2's SSM dynamics - // match what was trained. - let dt_s = self.cfg.decision_stride.max(1) as f32; + // Per-K forward (recurrent CfC + heads, batched). + // A1: decision_stride removed; event-rate dt = 1.0 per CfC step. + let dt_s = 1.0_f32; let n_in_i = HIDDEN_DIM as i32; let n_hid_i = HIDDEN_DIM as i32; // Phase B fix-up: block-per-batch (Phase B), same launch contract as diff --git a/crates/ml-alpha/tests/forward_step_golden.rs b/crates/ml-alpha/tests/forward_step_golden.rs index 686c18aa9..7dfcaef7e 100644 --- a/crates/ml-alpha/tests/forward_step_golden.rs +++ b/crates/ml-alpha/tests/forward_step_golden.rs @@ -96,7 +96,6 @@ fn build_trainer(dev: &MlDevice) -> Result { n_batch: 1, mamba2_state_dim: 16, seed: SEED, - decision_stride: 1, ..Default::default() }; PerceptionTrainer::new(dev, &cfg).context("trainer init") diff --git a/crates/ml-alpha/tests/multi_horizon_loader.rs b/crates/ml-alpha/tests/multi_horizon_loader.rs index bf0049cad..02c83d5ac 100644 --- a/crates/ml-alpha/tests/multi_horizon_loader.rs +++ b/crates/ml-alpha/tests/multi_horizon_loader.rs @@ -22,7 +22,6 @@ fn cfg_from_env() -> Option { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 100, seed: 0xA1A2_A3A4, - decision_stride: 1, inference_only: false, }) } @@ -65,7 +64,6 @@ fn loader_errors_on_empty_files() { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 10, seed: 0, - decision_stride: 1, inference_only: false, }; let res = MultiHorizonLoader::new(&cfg); @@ -75,24 +73,18 @@ fn loader_errors_on_empty_files() { #[test] #[ignore] fn loader_stride_4_yields_correct_spacing() { - // Verifies decision_stride=4 produces snapshots that are 4-apart in - // the source file. Only meaningful with real data. Uses ts_ns to - // confirm the spacing (since indices aren't exposed externally). + // A1: decision_stride removed; sequences are always consecutive snapshots. + // Test repurposed: verifies consecutive-snapshot ts_ns monotonicity. let Some(mut cfg) = cfg_from_env() else { eprintln!("skipping: FOXHUNT_TEST_DATA not set or mbp10 dir missing"); return; }; - cfg.decision_stride = 4; cfg.seq_len = 32; cfg.n_max_sequences = 5; let mut loader = MultiHorizonLoader::new(&cfg).expect("loader init"); while let Some(seq) = loader.next_sequence().expect("next") { assert_eq!(seq.snapshots.len(), cfg.seq_len); - // The Δt between consecutive K-positions should be visibly - // larger than a single-tick gap. At stride=4 on ES MBP-10, that - // means the median Δt across positions should exceed ~3× the - // typical tick interval (microsecond-scale jitter is fine). let dts_ns: Vec = (1..seq.snapshots.len()) .map(|k| (seq.snapshots[k].ts_ns as i64) - (seq.snapshots[k - 1].ts_ns as i64)) .filter(|d| *d >= 0) // ts_ns is monotonic in MBP-10 @@ -101,11 +93,7 @@ fn loader_stride_4_yields_correct_spacing() { let mut sorted = dts_ns.clone(); sorted.sort(); let median = sorted[sorted.len() / 2]; - eprintln!("stride=4 median Δt_ns between K-positions = {median}"); - // Sanity: median Δt should be > 0 (stride>1 means no zero-Δt - // self-pairs in the steady state). Stride > 1 with sparse - // periods could theoretically dip to zero on quiet windows, - // so don't assert a lower bound on the median magnitude. + eprintln!("consecutive median Δt_ns between K-positions = {median}"); assert!(median >= 0, "Δt_ns negative — ts_ns not monotonic?"); } } diff --git a/crates/ml-alpha/tests/perception_overfit.rs b/crates/ml-alpha/tests/perception_overfit.rs index 41b8a1a56..188ba54fb 100644 --- a/crates/ml-alpha/tests/perception_overfit.rs +++ b/crates/ml-alpha/tests/perception_overfit.rs @@ -72,7 +72,6 @@ fn stacked_trainer_loss_shrinks_on_constant_signal() { seed: 0x4242, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); @@ -130,12 +129,11 @@ fn stacked_trainer_loss_shrinks_on_constant_signal() { ); } -/// Verifies the trainer still converges on a constant-direction signal -/// with `decision_stride = 4`. Stride affects loader output AND Mamba2 -/// dt_s (= 4.0 in the K-loop) — this test ensures the full chain stays -/// numerically stable. Synthetic sequence here uses consecutive -/// snapshots (the loader-level stride is what skips); the trainer-level -/// dt_s change is what this smoke proves. +/// Verifies the trainer still converges with a distinct seed and lr. +/// A1: decision_stride removed; dt_s is always 1.0 (event-rate). +/// The test previously verified stride=4 dt_s dynamics; those are +/// now unconditionally event-rate, so this becomes a second convergence +/// smoke with different hyperparameters. #[test] fn stacked_trainer_loss_shrinks_with_stride_4() { let dev = test_device(); @@ -147,7 +145,6 @@ fn stacked_trainer_loss_shrinks_with_stride_4() { seed: 0xC4C4, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 4, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); @@ -201,7 +198,6 @@ fn stacked_trainer_loss_shrinks_at_batch_32() { seed: 0xB32B, horizon_weights: [1.0; 5], n_batch: 32, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); @@ -299,7 +295,6 @@ fn evaluate_alone_succeeds() { seed: 0x6262, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); let ts = 1_000_000u64; @@ -327,7 +322,6 @@ fn evaluate_works_after_captured_training_step() { seed: 0x5151, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); @@ -362,7 +356,6 @@ fn evaluate_works_after_capture_no_replay() { seed: 0x8181, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); let ts = 1_000_000u64; @@ -392,7 +385,6 @@ fn horizon_ema_and_lambda_track_after_training() { seed: 0x9292, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); @@ -448,7 +440,6 @@ fn evaluate_works_after_warmup_only() { seed: 0x7171, horizon_weights: [1.0; 5], n_batch: 1, - decision_stride: 1, }; let mut trainer = PerceptionTrainer::new(&dev, &cfg).expect("init"); let ts = 1_000_000u64; diff --git a/crates/ml-backtesting/cuda/resting_orders.cu b/crates/ml-backtesting/cuda/resting_orders.cu index 4cb00e34b..db1e626c9 100644 --- a/crates/ml-backtesting/cuda/resting_orders.cu +++ b/crates/ml-backtesting/cuda/resting_orders.cu @@ -309,11 +309,8 @@ extern "C" __global__ void resting_orders_step( last_event_ts[b] = current_ts_ns; // S2.2: max_hold force-close at event rate. - // Mirrors the decision-rate check that used to live in stop_check_isv (removed), - // but runs every event so the 60s cap is actually enforceable. The previous - // decision-rate check fired every decision_stride events (~13 min sim-time on ES - // data at stride=200), so it was ~13 min LATE on average. Event-rate enforces - // within one event of the cap. + // Runs every event so the 60s cap is actually enforceable. + // A1: decision_stride removed; all enforcement is event-rate. const unsigned long long max_hold = max_hold_ns_per_b[b]; if (max_hold > 0ull && pos.position_lots != 0) { const unsigned long long entry_ts = diff --git a/crates/ml-backtesting/src/harness.rs b/crates/ml-backtesting/src/harness.rs index db2d87465..9e8b70a65 100644 --- a/crates/ml-backtesting/src/harness.rs +++ b/crates/ml-backtesting/src/harness.rs @@ -27,7 +27,6 @@ pub struct BacktestHarnessConfig { pub data_root: PathBuf, pub predecoded_dir: PathBuf, pub n_parallel: usize, - pub decision_stride: usize, pub target_annual_vol_units: f32, pub annualisation_factor: f32, pub max_lots: u16, @@ -148,7 +147,7 @@ impl BacktestHarness { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 0, seed: 0, - decision_stride: cfg.decision_stride, + // A1: decision_stride removed; inference mode walks every event at stride=1. inference_only: true, }; let loader = MultiHorizonLoader::new(&loader_cfg)?; @@ -221,7 +220,6 @@ impl BacktestHarness { /// decision loop until either the stream is exhausted or /// cfg.max_events is reached. See spec §7 orchestrator code. pub fn run(&mut self) -> Result { - let stride = self.cfg.decision_stride.max(1) as u64; let mut total_decisions = 0u64; // Periodic progress line — without this, multi-million-event // runs look identical to a deadlock in pod logs. @@ -247,24 +245,17 @@ impl BacktestHarness { } self.snapshot_window.push_back(raw.clone()); - // CRT Phase A0.5 (corrective): advance the encoder state on - // EVERY event once the window is bootstrapped. The GRN heads - // kernel writes the per-horizon probs directly into the sim's - // on-device `alpha_probs_d` — zero CPU touchpoint, no - // stream.synchronize on the hot path. forward_step_into is - // O(hidden_dim × state_dim) per call — independent of K. + // CRT A1: once the window is full, fire BOTH forward_step_into AND + // step_decision_with_latency on every event. decision_stride is gone; + // the window-full guard is the only gate. + // forward_step_into (A0.5) advances the SSM state and writes + // per-horizon probs into alpha_probs_d. step_decision_with_latency + // reads alpha_probs_d immediately after — zero CPU roundtrip. if self.snapshot_window.len() == self.seq_len { self.trainer .forward_step_into(&raw, self.sim.alpha_probs_d_mut()) .context("trainer.forward_step_into")?; - } - // Decision-stride gate: step the sim only at stride boundaries. - // A1 will remove this gate so decisions fire every event. - // `broadcast_alpha` is gone: forward_step_into already wrote - // the probs into `alpha_probs_d`, so the decision kernels see - // the freshest event's probs without any host roundtrip. - if self.event_count % stride == 0 && self.snapshot_window.len() == self.seq_len { // Side-channel: record this decision's max_conviction // on-device for the threshold-tuning percentile // computation. Kernel reads `alpha_probs_d` and writes @@ -272,7 +263,7 @@ impl BacktestHarness { // Single end-of-run DtoV in `read_convictions` materialises // the host log. Doing it BEFORE step_decision so the log // captures every decision attempt, including those the - // threshold gate would skip — matches prior behaviour. + // threshold gate would skip. self.sim.record_max_conviction(self.convictions_recorded)?; self.convictions_recorded = self.convictions_recorded.saturating_add(1); diff --git a/crates/ml-backtesting/tests/ring3_replay.rs b/crates/ml-backtesting/tests/ring3_replay.rs index 669e5b4aa..1a329e487 100644 --- a/crates/ml-backtesting/tests/ring3_replay.rs +++ b/crates/ml-backtesting/tests/ring3_replay.rs @@ -47,7 +47,6 @@ fn try_loader() -> Option { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 0, seed: 0xCAFE_F00D, - decision_stride: 1, inference_only: true, }; match MultiHorizonLoader::new(&cfg) { diff --git a/crates/ml-backtesting/tests/trainer_parity.rs b/crates/ml-backtesting/tests/trainer_parity.rs index 3e3d37577..074686bd3 100644 --- a/crates/ml-backtesting/tests/trainer_parity.rs +++ b/crates/ml-backtesting/tests/trainer_parity.rs @@ -34,7 +34,6 @@ fn try_loader(inference_only: bool) -> Option { horizons: [30, 100, 300, 1000, 6000], n_max_sequences: 1, seed: 0xCAFEF00D, - decision_stride: 1, inference_only, }; match MultiHorizonLoader::new(&cfg) { diff --git a/crates/ml/examples/alpha_baseline.rs b/crates/ml/examples/alpha_baseline.rs index 50f494c31..3e718e108 100644 --- a/crates/ml/examples/alpha_baseline.rs +++ b/crates/ml/examples/alpha_baseline.rs @@ -226,14 +226,6 @@ struct Cli { #[arg(long, default_value_t = 0.99)] gamma: f32, - /// Decision stride — emit a new action only every N steps; between - /// decisions force `action=0` (wait), preserving the previously-opened - /// position. Aligns DQN decision cadence with the multi-minute alpha - /// horizon and stops the policy from flip-flopping on per-bar noise - /// ("coin-flip problem"). 1 = current per-bar behaviour. Pair with - /// γ→0.999+ so the Bellman chain reaches across the wait segments. - #[arg(long, default_value_t = 1)] - decision_stride: u32, #[arg(long, default_value_t = 0.9)] alpha_m: f32, #[arg(long, default_value_t = 0.03)] @@ -660,16 +652,8 @@ fn main() -> Result<()> { } } stream.synchronize()?; - // `--decision-stride N`: only emit a new action every N steps; - // between strides force action=0 (wait) so an open position is - // held rather than re-decided per bar. Avoids the per-bar - // "coin-flip" overtrading. step==0 is always a decision. - let stride_train = cli.decision_stride.max(1) as usize; - let actions = if step % stride_train == 0 { - batched_action_pinned_train.read_all() - } else { - vec![0_i32; train_n_par] - }; + // A1: decision_stride removed; act on every step. + let actions = batched_action_pinned_train.read_all(); for i in 0..train_n_par { if done_flags[i] { continue; } @@ -1011,17 +995,8 @@ fn main() -> Result<()> { } // ONE sync per step (instead of N). stream.synchronize()?; - // `--decision-stride N`: same rate-limit semantics as training. - // Between strides force action=0 so an open passive limit - // post / live position is held rather than re-decided. Cuts - // per-bar trade counts ~stride× and removes the coin-flip - // overtrading on noise. - let stride_eval = cli.decision_stride.max(1) as usize; - let actions = if step % stride_eval == 0 { - batched_action_pinned.read_all() - } else { - vec![0_i32; n_par] - }; + // A1: decision_stride removed; act on every step. + let actions = batched_action_pinned.read_all(); // Step all envs on CPU. for i in 0..n_par { if done_flags[i] { continue; }