refactor(per-horizon): N_HORIZONS 5→3 — ml-backtesting full propagation
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>
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
#ifndef LOB_STATE_CUH
|
||||
#define LOB_STATE_CUH
|
||||
|
||||
#define N_HORIZONS 5
|
||||
// Must stay in sync with crates/ml-alpha/src/heads.rs::N_HORIZONS and
|
||||
// crates/ml-backtesting/src/lob/mod.rs::N_HORIZONS.
|
||||
#define N_HORIZONS 3
|
||||
#define MAX_LIMITS 32
|
||||
#define MAX_STOPS 16
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! See spec §2 (CUDA data layout) and §5b (state ownership).
|
||||
|
||||
pub const BOOK_LEVELS: usize = 10;
|
||||
pub const N_HORIZONS: usize = 5;
|
||||
/// Re-export of `ml_alpha::heads::N_HORIZONS` — single source of truth.
|
||||
pub const N_HORIZONS: usize = ml_alpha::heads::N_HORIZONS;
|
||||
pub const MAX_LIMITS: usize = 32;
|
||||
pub const MAX_STOPS: usize = 16;
|
||||
/// Bytes per LimitSlot — matches cuda/lob_state.cuh::LimitSlot.
|
||||
|
||||
@@ -14,8 +14,8 @@ pub use sizing::{IsvKellyStateHost, SizingPolicyId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Mirrors `crates/ml-alpha/src/heads.rs::N_HORIZONS`.
|
||||
pub const N_HORIZONS: usize = 5;
|
||||
/// Re-export of `ml_alpha::heads::N_HORIZONS` — single source of truth.
|
||||
pub const N_HORIZONS: usize = ml_alpha::heads::N_HORIZONS;
|
||||
|
||||
/// One horizon-anchored leaf strategy. See spec §5 (per-horizon ISV-Kelly) + §6.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -194,11 +194,11 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_strategy_has_5_horizon_leaves() {
|
||||
fn default_strategy_has_one_leaf_per_horizon() {
|
||||
let s = Strategy::default_for(3);
|
||||
match s {
|
||||
Strategy::Ensemble { children, aggregator } => {
|
||||
assert_eq!(children.len(), 5);
|
||||
assert_eq!(children.len(), N_HORIZONS);
|
||||
assert!(matches!(
|
||||
aggregator,
|
||||
EnsembleAggregator::WeightedByRealizedSharpe
|
||||
@@ -218,18 +218,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_strategy_flattens_to_5_emits_plus_agg_plus_write() {
|
||||
fn default_strategy_flattens_to_emits_plus_agg_plus_write() {
|
||||
let s = Strategy::default_for(3);
|
||||
let p = s.flatten();
|
||||
assert_eq!(p.instructions.len(), 7);
|
||||
for i in 0..5 {
|
||||
// N_HORIZONS EmitPerHorizonSize + 1 AggWeightedSharpe + 1 WriteOrder.
|
||||
assert_eq!(p.instructions.len(), N_HORIZONS + 2);
|
||||
for i in 0..N_HORIZONS {
|
||||
assert_eq!(p.instructions[i].op, OpCode::EmitPerHorizonSize as u8);
|
||||
assert_eq!(p.instructions[i].arg0, i as u8);
|
||||
assert_eq!(p.instructions[i].arg1, 3);
|
||||
}
|
||||
assert_eq!(p.instructions[5].op, OpCode::AggWeightedSharpe as u8);
|
||||
assert_eq!(p.instructions[5].arg0, 5);
|
||||
assert_eq!(p.instructions[6].op, OpCode::WriteOrder as u8);
|
||||
assert_eq!(p.instructions[N_HORIZONS].op, OpCode::AggWeightedSharpe as u8);
|
||||
assert_eq!(p.instructions[N_HORIZONS].arg0, N_HORIZONS as u8);
|
||||
assert_eq!(p.instructions[N_HORIZONS + 1].op, OpCode::WriteOrder as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1748,7 +1748,7 @@ impl LobSimCuda {
|
||||
pub fn write_isv_kelly(
|
||||
&mut self,
|
||||
backtest_idx: usize,
|
||||
states: &[crate::policy::IsvKellyStateHost; 5],
|
||||
states: &[crate::policy::IsvKellyStateHost; N_HORIZONS],
|
||||
) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
backtest_idx < self.n_backtests,
|
||||
@@ -1770,7 +1770,7 @@ impl LobSimCuda {
|
||||
pub fn read_isv_kelly(
|
||||
&self,
|
||||
backtest_idx: usize,
|
||||
) -> Result<[crate::policy::IsvKellyStateHost; 5]> {
|
||||
) -> Result<[crate::policy::IsvKellyStateHost; N_HORIZONS]> {
|
||||
anyhow::ensure!(
|
||||
backtest_idx < self.n_backtests,
|
||||
"backtest_idx {} >= n_backtests {}",
|
||||
@@ -1781,7 +1781,7 @@ impl LobSimCuda {
|
||||
self.stream.memcpy_dtoh(&self.isv_kelly_d, raw.as_mut_slice())?;
|
||||
let off = backtest_idx * N_HORIZONS * ISV_KELLY_STATE_BYTES;
|
||||
let slice = &raw[off..off + N_HORIZONS * ISV_KELLY_STATE_BYTES];
|
||||
let mut out = [crate::policy::IsvKellyStateHost::default(); 5];
|
||||
let mut out = [crate::policy::IsvKellyStateHost::default(); N_HORIZONS];
|
||||
let bytes_out: &mut [u8] = bytemuck::cast_slice_mut(&mut out);
|
||||
bytes_out.copy_from_slice(slice);
|
||||
Ok(out)
|
||||
|
||||
@@ -64,7 +64,7 @@ fn cold_start_sentinel_state_still_fires_a_trade() -> Result<()> {
|
||||
// Do NOT seed isv_kelly — leave at zeros (alloc_zeros' sentinel).
|
||||
// Strong directional alpha across all horizons → conviction-driven
|
||||
// sig_mag = 0.6 for every horizon, dir = +1.
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?;
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
assert_eq!(side, 0, "cold-start with p_h=0.8 must produce a long; got side={side}");
|
||||
@@ -96,7 +96,7 @@ fn post_first_loss_state_does_not_lock_out_further_trades() -> Result<()> {
|
||||
// Seed isv_kelly_d with the exact state pattern the smoke produced:
|
||||
// one closed-loss trade, large realised_return_var. Pre-fix, the
|
||||
// variance-derived cap collapses to ~0.
|
||||
let post_loss: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let post_loss: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 10.18, // magnitude of the lone loss return
|
||||
win_rate_ema: 0.0,
|
||||
@@ -105,7 +105,7 @@ fn post_first_loss_state_does_not_lock_out_further_trades() -> Result<()> {
|
||||
recent_sharpe: -1.0, // very negative — would have starved the weight side too
|
||||
});
|
||||
sim.write_isv_kelly(0, &post_loss)?;
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?;
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
assert_eq!(side, 0, "post-loss state must still fire a long with strong alpha (got side={side})");
|
||||
@@ -151,7 +151,7 @@ fn cold_start_stopgap_bytecode_vm_fires_a_trade() -> Result<()> {
|
||||
let prog = max_conf.flatten();
|
||||
sim.upload_program(0, &prog)?;
|
||||
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_uniform(1, 0.20, 0.10))?;
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
eprintln!("stopgap cold-start single-step: side={side} size={size}");
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
"n_backtests": 1,
|
||||
"warm_start_isv_kelly": [
|
||||
[
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 2.0, "pnl_ema_loss": 0.5, "win_rate_ema": 0.8, "n_trades_seen": 50, "realised_return_var": 0.25, "recent_sharpe": 1.0 }
|
||||
@@ -20,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"type": "decision",
|
||||
"alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.9],
|
||||
"alpha_probs": [0.5, 0.5, 0.9],
|
||||
"target_annual_vol_units": 50.0,
|
||||
"annualisation_factor": 1.0,
|
||||
"max_lots": 5,
|
||||
@@ -35,7 +33,7 @@
|
||||
},
|
||||
{
|
||||
"type": "decision",
|
||||
"alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.1],
|
||||
"alpha_probs": [0.5, 0.5, 0.1],
|
||||
"target_annual_vol_units": 50.0,
|
||||
"annualisation_factor": 1.0,
|
||||
"max_lots": 5,
|
||||
@@ -51,8 +49,6 @@
|
||||
"expected_min_trade_records": 1,
|
||||
"expected_isv_kelly_after": [
|
||||
[
|
||||
{ "n_trades_seen_min": 0, "n_trades_seen_max": 0 },
|
||||
{ "n_trades_seen_min": 0, "n_trades_seen_max": 0 },
|
||||
{ "n_trades_seen_min": 0, "n_trades_seen_max": 0 },
|
||||
{ "n_trades_seen_min": 0, "n_trades_seen_max": 0 },
|
||||
{ "n_trades_seen_min": 51, "n_trades_seen_max": 51 }
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"name": "decision_program_h4_only",
|
||||
"n_backtests": 1,
|
||||
"comment": "Uploads a custom Strategy bytecode program (single Leaf at horizon 4) that bypasses the hardcoded WeightedByRealizedSharpe default. Should produce equivalent end-state when h4 is the only weighted horizon.",
|
||||
"comment": "Uploads a custom Strategy bytecode program (single Leaf at horizon N_HORIZONS-1) that bypasses the hardcoded WeightedByRealizedSharpe default. Should produce equivalent end-state when only the last horizon is weighted.",
|
||||
"warm_start_isv_kelly": [
|
||||
[
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 0.0, "pnl_ema_loss": 0.0, "win_rate_ema": 0.0, "n_trades_seen": 0, "realised_return_var": 0.0, "recent_sharpe": 0.0 },
|
||||
{ "pnl_ema_win": 2.0, "pnl_ema_loss": 0.5, "win_rate_ema": 0.8, "n_trades_seen": 50, "realised_return_var": 0.25, "recent_sharpe": 1.0 }
|
||||
@@ -22,7 +20,7 @@
|
||||
},
|
||||
{
|
||||
"type": "decision",
|
||||
"alpha_probs": [0.5, 0.5, 0.5, 0.5, 0.9],
|
||||
"alpha_probs": [0.5, 0.5, 0.9],
|
||||
"target_annual_vol_units": 50.0,
|
||||
"annualisation_factor": 1.0,
|
||||
"max_lots": 5,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use ml_backtesting::lob::BOOK_LEVELS;
|
||||
use ml_backtesting::policy::IsvKellyStateHost;
|
||||
use ml_backtesting::policy::{IsvKellyStateHost, N_HORIZONS};
|
||||
use ml_backtesting::sim::LobSimCuda;
|
||||
use ml_core::device::MlDevice;
|
||||
use serde::Deserialize;
|
||||
@@ -28,7 +28,7 @@ enum FixtureEvent {
|
||||
ts_ns: u64,
|
||||
},
|
||||
Decision {
|
||||
alpha_probs: [f32; 5],
|
||||
alpha_probs: [f32; N_HORIZONS],
|
||||
target_annual_vol_units: f32,
|
||||
annualisation_factor: f32,
|
||||
max_lots: u16,
|
||||
@@ -152,11 +152,11 @@ struct Fixture {
|
||||
#[serde(default)]
|
||||
expected_trade_records: Vec<ExpectedTradeRecord>,
|
||||
#[serde(default)]
|
||||
warm_start_isv_kelly: Vec<[IsvKellyFixture; 5]>,
|
||||
warm_start_isv_kelly: Vec<[IsvKellyFixture; N_HORIZONS]>,
|
||||
#[serde(default)]
|
||||
expected_min_trade_records: usize,
|
||||
#[serde(default)]
|
||||
expected_isv_kelly_after: Vec<[ExpectedIsvKelly; 5]>,
|
||||
expected_isv_kelly_after: Vec<[ExpectedIsvKelly; N_HORIZONS]>,
|
||||
#[serde(default)]
|
||||
expected_limit_slot_states: Vec<ExpectedSlotState>,
|
||||
#[serde(default)]
|
||||
@@ -202,11 +202,12 @@ fn run_book_fixture(path: &Path) -> Result<()> {
|
||||
.map_err(|e| anyhow::anyhow!("cuda device: {e}"))?;
|
||||
let mut sim = LobSimCuda::new(fx.n_backtests, &dev)?;
|
||||
|
||||
// Upload a single-leaf h4 Strategy program for backtest 0 if requested.
|
||||
// Upload a single-leaf longest-horizon Strategy program for backtest 0
|
||||
// if requested. Horizon idx = N_HORIZONS-1 (longest available horizon).
|
||||
if fx.upload_h4_leaf_program {
|
||||
use ml_backtesting::policy::{SizingPolicyId, Strategy, StrategyConfig};
|
||||
let leaf = Strategy::Leaf(StrategyConfig {
|
||||
horizon_idx: 4,
|
||||
horizon_idx: (N_HORIZONS - 1) as u8,
|
||||
sizing_policy: SizingPolicyId::IsvKelly,
|
||||
max_concurrent_lots: 5,
|
||||
});
|
||||
@@ -216,13 +217,8 @@ fn run_book_fixture(path: &Path) -> Result<()> {
|
||||
|
||||
// Apply ISV-Kelly warm-start if provided (one row per backtest).
|
||||
for (b, states) in fx.warm_start_isv_kelly.iter().enumerate() {
|
||||
let host_states: [IsvKellyStateHost; 5] = [
|
||||
states[0].to_host(),
|
||||
states[1].to_host(),
|
||||
states[2].to_host(),
|
||||
states[3].to_host(),
|
||||
states[4].to_host(),
|
||||
];
|
||||
let host_states: [IsvKellyStateHost; N_HORIZONS] =
|
||||
std::array::from_fn(|h| states[h].to_host());
|
||||
sim.write_isv_kelly(b, &host_states)?;
|
||||
}
|
||||
|
||||
@@ -345,7 +341,7 @@ fn run_book_fixture(path: &Path) -> Result<()> {
|
||||
if !fx.expected_isv_kelly_after.is_empty() {
|
||||
for (b, expected) in fx.expected_isv_kelly_after.iter().enumerate() {
|
||||
let got = sim.read_isv_kelly(b)?;
|
||||
for h in 0..5 {
|
||||
for h in 0..N_HORIZONS {
|
||||
let n = got[h].n_trades_seen;
|
||||
assert!(
|
||||
n >= expected[h].n_trades_seen_min && n <= expected[h].n_trades_seen_max,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//! resting-order pressure + trade-flow simulation.
|
||||
|
||||
use anyhow::Result;
|
||||
use ml_backtesting::policy::IsvKellyStateHost;
|
||||
use ml_backtesting::policy::{IsvKellyStateHost, N_HORIZONS};
|
||||
use ml_backtesting::sim::LobSimCuda;
|
||||
use ml_core::device::MlDevice;
|
||||
use rand::{Rng, SeedableRng};
|
||||
@@ -37,12 +37,12 @@ fn make_initial_book() -> ([f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LE
|
||||
(bid_px, bid_sz, ask_px, ask_sz)
|
||||
}
|
||||
|
||||
fn random_warm_start_isv(rng: &mut ChaCha8Rng) -> [IsvKellyStateHost; 5] {
|
||||
let mut out: [IsvKellyStateHost; 5] = Default::default();
|
||||
for h in 0..5 {
|
||||
fn random_warm_start_isv(rng: &mut ChaCha8Rng) -> [IsvKellyStateHost; N_HORIZONS] {
|
||||
let mut out: [IsvKellyStateHost; N_HORIZONS] = Default::default();
|
||||
for h in 0..N_HORIZONS {
|
||||
// Make at least one horizon credibly profitable so the decision
|
||||
// kernel actually opens positions; others get random warm-starts.
|
||||
let positive_h = h == 4;
|
||||
let positive_h = h == N_HORIZONS - 1;
|
||||
let win_rate: f32 = if positive_h { 0.7 } else { rng.gen_range(0.3..0.6) };
|
||||
let pnl_win: f32 = if positive_h { 2.0 } else { rng.gen_range(0.5..1.5) };
|
||||
let pnl_loss: f32 = rng.gen_range(0.3..1.0);
|
||||
@@ -113,8 +113,8 @@ fn run_integrated_fuzz(n_backtests: usize, n_events: usize, seed: u64) -> Result
|
||||
|
||||
// Decide every 4th event.
|
||||
if event_idx % 4 == 0 {
|
||||
let mut probs = [0.5_f32; 5];
|
||||
for h in 0..5 {
|
||||
let mut probs = [0.5_f32; N_HORIZONS];
|
||||
for h in 0..N_HORIZONS {
|
||||
probs[h] = rng.gen_range(0.1..0.9);
|
||||
}
|
||||
sim.broadcast_alpha(&probs)?;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//! 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;
|
||||
|
||||
@@ -22,7 +23,7 @@ fn parallel_sim_equivalence_with_uniform_config() -> Result<()> {
|
||||
}
|
||||
};
|
||||
let mut sim = LobSimCuda::new(8, &dev)?;
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
||||
let cfg = BatchedSimConfig::from_uniform(
|
||||
8,
|
||||
&UniformSimParams {
|
||||
|
||||
@@ -44,7 +44,7 @@ fn try_loader() -> Option<MultiHorizonLoader> {
|
||||
files,
|
||||
predecoded_dir: mbp10.clone(),
|
||||
seq_len: 1,
|
||||
horizons: [30, 100, 300, 1000, 6000],
|
||||
horizons: ml_alpha::heads::HORIZONS,
|
||||
n_max_sequences: 0,
|
||||
seed: 0xCAFE_F00D,
|
||||
inference_only: true,
|
||||
|
||||
@@ -92,7 +92,7 @@ fn stop_check_skipped_when_flat() -> Result<()> {
|
||||
|
||||
// Position is flat (just allocated, zeros). Stop check must not fire;
|
||||
// alpha decides the action.
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
sim.broadcast_alpha(&[0.8; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_default(1))?;
|
||||
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
@@ -118,7 +118,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> {
|
||||
// pnl_ema_win=0.0 forces cold-start branch in decision kernel (kelly_frac →
|
||||
// floor=0.20, cap_lots → max_lots=5). With alpha=0.95 this gives
|
||||
// lots=round(0.9*0.20*5)=round(0.9)=1 — position opens.
|
||||
let seeded: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let seeded: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 2.0,
|
||||
win_rate_ema: 0.5,
|
||||
@@ -135,7 +135,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> {
|
||||
sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?;
|
||||
|
||||
// Open a long position via strong-alpha + step_decision + manual fill.
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_default(1))?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
for _ in 0..3 {
|
||||
@@ -149,7 +149,7 @@ fn sl_fires_when_unrealized_breaks_distance() -> Result<()> {
|
||||
// Drive mid down by 3.0 (>= sl_distance=2.0). Stop must fire force-flat.
|
||||
let (bp3, bs3, ap3, az3) = level_book(5497.0, 0.25);
|
||||
sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
@@ -169,7 +169,7 @@ fn trail_arms_then_fires() -> Result<()> {
|
||||
|
||||
// Cold-start ISV (forces kelly_frac_floor + max_lots cap) so position opens.
|
||||
// pnl_ema_loss large enough that SL won't fire during the +5pt walk.
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 10.0, // big SL so trail fires first
|
||||
win_rate_ema: 0.0,
|
||||
@@ -185,7 +185,7 @@ fn trail_arms_then_fires() -> Result<()> {
|
||||
sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?;
|
||||
|
||||
// Open long.
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_default(1))?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; }
|
||||
@@ -193,7 +193,7 @@ fn trail_arms_then_fires() -> Result<()> {
|
||||
|
||||
// After opening, switch ISV to one with pnl_ema_win=1.5 (trail_distance target).
|
||||
// The trail check reads this fresh state on subsequent decision steps.
|
||||
let trail_state: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let trail_state: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 1.5,
|
||||
pnl_ema_loss: 10.0,
|
||||
win_rate_ema: 0.5,
|
||||
@@ -207,7 +207,7 @@ fn trail_arms_then_fires() -> Result<()> {
|
||||
for &m in &[5501.0_f32, 5502.0, 5503.0, 5504.0, 5505.0] {
|
||||
let (b, bs, a, asz) = level_book(m, 0.25);
|
||||
sim.apply_snapshot(&b, &bs, &a, &asz)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
ts += 1_000_000;
|
||||
}
|
||||
@@ -217,7 +217,7 @@ fn trail_arms_then_fires() -> Result<()> {
|
||||
// Drop mid by trail_distance+ε from peak to fire trail.
|
||||
let (b, bs, a, asz) = level_book(5502.0, 0.25); // drop from peak 5505 to 5502 = 3.0
|
||||
sim.apply_snapshot(&b, &bs, &a, &asz)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
@@ -244,7 +244,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> {
|
||||
// Bit-pick-first (h0=2.0) would fire at Δ_entry=2.5 → test catches it.
|
||||
// Bit-pick-max (h1=4.0) would not fire at Δ_entry=4.5 → test catches it.
|
||||
// Correct mean (3.0) → no fire at Δ_entry=2.5, fire at Δ_entry=4.5.
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 10.0,
|
||||
win_rate_ema: 0.0,
|
||||
@@ -259,7 +259,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> {
|
||||
let (bp2, bs2, ap2, az2) = level_book(5500.1, 0.25);
|
||||
sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?;
|
||||
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_default(1))?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; }
|
||||
@@ -274,7 +274,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> {
|
||||
// Seed multi-horizon ISV state. pnl_ema_win=100.0 keeps trail_distance huge
|
||||
// so the trail never fires during the test.
|
||||
// See setup comment above: mean=3.0, regression discriminators at Δ=2.5/4.5.
|
||||
let mut seeded: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let mut seeded: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 100.0,
|
||||
pnl_ema_loss: 3.0,
|
||||
win_rate_ema: 0.5,
|
||||
@@ -299,7 +299,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> {
|
||||
let mid_no_fire = entry_px - 2.25;
|
||||
let (bp3, bs3, ap3, az3) = level_book(mid_no_fire, 0.25);
|
||||
sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
let (side_at_25, _) = sim.read_market_target(0)?;
|
||||
assert_ne!(side_at_25, 3,
|
||||
@@ -317,7 +317,7 @@ fn multi_horizon_mask_averages_emas() -> Result<()> {
|
||||
let mid_fire = entry_px - 4.25;
|
||||
let (bp4, bs4, ap4, az4) = level_book(mid_fire, 0.25);
|
||||
sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
let (side_at_45, size_at_45) = sim.read_market_target(0)?;
|
||||
assert_eq!(side_at_45, 3, "Δ=4.5 > mean_sl=3.0 must fire force-flat; got side={side_at_45}");
|
||||
@@ -358,7 +358,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> {
|
||||
// Cold-start ISV: pnl_ema_win=0.0, n_trades_seen=0, realised_return_var=0.0
|
||||
// forces kelly_frac_floor + max_lots path so position opens.
|
||||
// pnl_ema_loss=10.0 keeps SL far enough that it won't fire during open setup.
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 10.0,
|
||||
win_rate_ema: 0.0,
|
||||
@@ -374,7 +374,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> {
|
||||
sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?;
|
||||
|
||||
// Open long.
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_default(1))?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
for _ in 0..3 {
|
||||
@@ -391,7 +391,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> {
|
||||
|
||||
// Set SL trigger setup: pnl_ema_loss=2.0 → sl_distance=2.0 (assuming ATR < 2.0).
|
||||
// pnl_ema_win=10.0 keeps trail_distance=10.0 so trail won't fire before SL.
|
||||
let triggered: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let triggered: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 10.0,
|
||||
pnl_ema_loss: 2.0,
|
||||
win_rate_ema: 0.5,
|
||||
@@ -407,7 +407,7 @@ fn bytecode_and_default_kernel_agree_on_stops() -> Result<()> {
|
||||
let trigger_mid = pos_open.vwap_entry - 4.0;
|
||||
let (bp3, bs3, ap3, az3) = level_book(trigger_mid, 0.25);
|
||||
sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
sim.read_market_target(0)
|
||||
}
|
||||
@@ -444,7 +444,7 @@ fn position_target_not_additive() -> Result<()> {
|
||||
let cfg = cfg_default(1);
|
||||
let mut ts: u64 = 0;
|
||||
for _ in 0..10 {
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg)?;
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
ts += 1_000_000;
|
||||
@@ -487,7 +487,7 @@ fn position_target_not_additive_with_latency() -> Result<()> {
|
||||
let mut ts: u64 = 0;
|
||||
// 500 events of persistent target — most should produce delta=0.
|
||||
for _ in 0..500 {
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_lat)?;
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
ts += 1_000_000;
|
||||
@@ -508,7 +508,7 @@ fn trail_hwm_reset_on_close() -> Result<()> {
|
||||
};
|
||||
let mut sim = LobSimCuda::new(1, &dev)?;
|
||||
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0, pnl_ema_loss: 10.0,
|
||||
win_rate_ema: 0.0, n_trades_seen: 0,
|
||||
realised_return_var: 0.0, recent_sharpe: 0.0,
|
||||
@@ -521,14 +521,14 @@ fn trail_hwm_reset_on_close() -> Result<()> {
|
||||
sim.apply_snapshot(&bp2, &bs2, &ap2, &az2)?;
|
||||
|
||||
// Open long.
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; }
|
||||
assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opens");
|
||||
|
||||
// Switch ISV to trail-test state (pnl_ema_win=1.5 → trail_distance=1.5).
|
||||
let trail_state: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let trail_state: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 1.5, pnl_ema_loss: 100.0, // big SL → only trail can fire
|
||||
win_rate_ema: 0.5, n_trades_seen: 20,
|
||||
realised_return_var: 0.5, recent_sharpe: 0.5,
|
||||
@@ -539,7 +539,7 @@ fn trail_hwm_reset_on_close() -> Result<()> {
|
||||
for &m in &[5501.0_f32, 5502.0, 5503.0, 5504.0, 5505.0] {
|
||||
let (b, bs, a, asz) = level_book(m, 0.25);
|
||||
sim.apply_snapshot(&b, &bs, &a, &asz)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
sim.step_pnl_track(ts)?;
|
||||
@@ -551,7 +551,7 @@ fn trail_hwm_reset_on_close() -> Result<()> {
|
||||
// Fire trail — drop mid past trail_distance from peak.
|
||||
let (b, bs, a, asz) = level_book(5502.0, 0.25);
|
||||
sim.apply_snapshot(&b, &bs, &a, &asz)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
let (s_fire, _) = sim.read_market_target(0)?;
|
||||
assert_eq!(s_fire, 3, "trail must fire force-flat");
|
||||
@@ -593,7 +593,7 @@ fn max_hold_forces_close() -> Result<()> {
|
||||
let mut sim = LobSimCuda::new(1, &dev)?;
|
||||
|
||||
// Cold-start ISV: large pnl_ema_loss so SL never fires during the test.
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0,
|
||||
pnl_ema_loss: 100.0,
|
||||
win_rate_ema: 0.0,
|
||||
@@ -626,7 +626,7 @@ fn max_hold_forces_close() -> Result<()> {
|
||||
|
||||
// Open long at t=1ms via strong alpha.
|
||||
let mut ts: u64 = 1_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_max_hold)?;
|
||||
for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; sim.step_pnl_track(ts)?; ts += 1_000_000; }
|
||||
assert!(sim.read_pos(0)?.position_lots > 0, "setup: long opens");
|
||||
@@ -693,7 +693,7 @@ fn book_nan_inf_prices_dont_corrupt_realized_pnl() -> Result<()> {
|
||||
|
||||
// Open long via strong alpha. The fill must NOT propagate NaN/Inf into
|
||||
// pos.realized_pnl, even though the book has NaN at ask[3] and Inf at bid[5].
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
for _ in 0..5 {
|
||||
@@ -730,7 +730,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> {
|
||||
};
|
||||
let mut sim = LobSimCuda::new(1, &dev)?;
|
||||
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0, pnl_ema_loss: 100.0, // huge SL so it doesn't fire
|
||||
win_rate_ema: 0.0, n_trades_seen: 0,
|
||||
realised_return_var: 0.0, recent_sharpe: 0.0,
|
||||
@@ -760,7 +760,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> {
|
||||
|
||||
// Trade 1: open long, force-flatten via max_hold.
|
||||
let mut ts: u64 = 1_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_max_hold)?;
|
||||
for _ in 0..3 {
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
@@ -771,7 +771,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> {
|
||||
|
||||
// Force close trade 1 via max_hold: jump ts by 200ms.
|
||||
ts += 200_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_max_hold)?;
|
||||
for _ in 0..5 {
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
@@ -795,7 +795,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> {
|
||||
|
||||
// Trade 2: re-open + close via max_hold.
|
||||
ts += 10_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_max_hold)?;
|
||||
for _ in 0..3 {
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
@@ -806,7 +806,7 @@ fn pnl_track_resets_scratch_on_close() -> Result<()> {
|
||||
|
||||
// Force close trade 2.
|
||||
ts += 200_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_max_hold)?;
|
||||
for _ in 0..5 {
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
@@ -850,7 +850,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> {
|
||||
|
||||
// Zero ema_loss: only ATR and cost floor govern sl_distance.
|
||||
// Large pnl_ema_win to keep trail_distance enormous, preventing trail fires.
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 100.0, pnl_ema_loss: 0.0,
|
||||
win_rate_ema: 0.0, n_trades_seen: 0,
|
||||
realised_return_var: 0.0, recent_sharpe: 0.0,
|
||||
@@ -886,7 +886,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> {
|
||||
delta_floor: 0.0,
|
||||
});
|
||||
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(0, &cfg_cost)?;
|
||||
let mut ts: u64 = 1_000_000;
|
||||
for _ in 0..3 { sim.step_resting_orders(ts, 0.0)?; ts += 1_000_000; }
|
||||
@@ -903,7 +903,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> {
|
||||
let (bp3, bs3, ap3, az3) = level_book(mid_no_fire, 0.25);
|
||||
sim.apply_snapshot(&bp3, &bs3, &ap3, &az3)?;
|
||||
// ATR update: delta ≈ |(entry+0.17) - 5500.1| — pumped slightly but still << 0.125 ✓
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_cost)?;
|
||||
let (side_no_fire, _) = sim.read_market_target(0)?;
|
||||
assert_ne!(side_no_fire, 3,
|
||||
@@ -915,7 +915,7 @@ fn trade_vol_floor_prevents_sub_cost_stops() -> Result<()> {
|
||||
let mid_fire = entry + 0.05;
|
||||
let (bp4, bs4, ap4, az4) = level_book(mid_fire, 0.25);
|
||||
sim.apply_snapshot(&bp4, &bs4, &ap4, &az4)?;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_cost)?;
|
||||
let (side_fire, size_fire) = sim.read_market_target(0)?;
|
||||
assert_eq!(side_fire, 3,
|
||||
@@ -940,7 +940,7 @@ fn session_gap_force_closes_open_positions() -> Result<()> {
|
||||
};
|
||||
let mut sim = LobSimCuda::new(1, &dev)?;
|
||||
|
||||
let cold_start: [IsvKellyStateHost; 5] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
let cold_start: [IsvKellyStateHost; N_HORIZONS] = std::array::from_fn(|_| IsvKellyStateHost {
|
||||
pnl_ema_win: 0.0, pnl_ema_loss: 100.0, // huge SL — won't fire
|
||||
win_rate_ema: 0.0, n_trades_seen: 0,
|
||||
realised_return_var: 0.0, recent_sharpe: 0.0,
|
||||
@@ -954,7 +954,7 @@ fn session_gap_force_closes_open_positions() -> Result<()> {
|
||||
|
||||
// Open long at t=1ms.
|
||||
let mut ts: u64 = 1_000_000;
|
||||
sim.broadcast_alpha(&[0.95, 0.95, 0.95, 0.95, 0.95])?;
|
||||
sim.broadcast_alpha(&[0.95; N_HORIZONS])?;
|
||||
sim.step_decision_with_latency(ts, &cfg_default(1))?;
|
||||
for _ in 0..3 {
|
||||
sim.step_resting_orders(ts, 0.0)?;
|
||||
@@ -1231,15 +1231,14 @@ fn multi_horizon_conviction_cancels_on_disagreement() -> Result<()> {
|
||||
delta_floor: 0.0,
|
||||
});
|
||||
|
||||
// Two horizons bullish (0.7), two bearish (0.3), one neutral (0.5).
|
||||
// Magnitudes: [0.4, 0.4, 0.4, 0.4, 0.0]. Directions: [+1, -1, +1, -1, +1].
|
||||
// One horizon bullish (0.7), one bearish (0.3), one neutral (0.5).
|
||||
// Magnitudes: [0.4, 0.4, 0.0]. Directions: [+1, -1, +1].
|
||||
// At cold start (ISV all zero, cost=1.0), every weight_h is
|
||||
// eps_edge / (0 + cost²) = 0.01 — equal. Weighted signed sum:
|
||||
// 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0
|
||||
// = 0
|
||||
// 0.4*0.01*(+1) + 0.4*0.01*(-1) + 0 = 0
|
||||
// conviction_signed = 0 / total_abs_weight = 0
|
||||
// → conv_ema = 0 → target_lots = 0 → side ∈ {2, 3}.
|
||||
let mixed: [f32; N_HORIZONS] = [0.7, 0.3, 0.7, 0.3, 0.5];
|
||||
let mixed: [f32; N_HORIZONS] = [0.7, 0.3, 0.5];
|
||||
sim.broadcast_alpha(&mixed)?;
|
||||
sim.step_decision_with_latency(1_000_000_000u64, &cfg)?;
|
||||
let (side, size) = sim.read_market_target(0)?;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! contract in isolation.
|
||||
|
||||
use anyhow::Result;
|
||||
use ml_backtesting::policy::N_HORIZONS;
|
||||
use ml_backtesting::sim::{BatchedSimConfig, LobSimCuda, UniformSimParams};
|
||||
use ml_core::device::MlDevice;
|
||||
|
||||
@@ -39,7 +40,7 @@ fn threshold_gate_skips_low_conviction() -> Result<()> {
|
||||
// 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, 0.51, 0.51, 0.51, 0.51])?;
|
||||
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}");
|
||||
@@ -58,7 +59,7 @@ fn threshold_gate_allows_high_conviction() -> Result<()> {
|
||||
// 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, 0.8, 0.8, 0.8, 0.8])?;
|
||||
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}");
|
||||
@@ -76,7 +77,7 @@ fn threshold_zero_is_passthrough() -> Result<()> {
|
||||
Err(e) => { eprintln!("skipping: cuda device unavailable ({e})"); return Ok(()); }
|
||||
};
|
||||
let mut sim = LobSimCuda::new(1, &dev)?;
|
||||
sim.broadcast_alpha(&[0.8, 0.8, 0.8, 0.8, 0.8])?;
|
||||
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}");
|
||||
|
||||
@@ -31,7 +31,7 @@ fn try_loader(inference_only: bool) -> Option<MultiHorizonLoader> {
|
||||
files,
|
||||
predecoded_dir: mbp10.clone(),
|
||||
seq_len: 1,
|
||||
horizons: [30, 100, 300, 1000, 6000],
|
||||
horizons: ml_alpha::heads::HORIZONS,
|
||||
n_max_sequences: 1,
|
||||
seed: 0xCAFEF00D,
|
||||
inference_only,
|
||||
|
||||
Reference in New Issue
Block a user