diff --git a/crates/ml/tests/behavioral/main.rs b/crates/ml/tests/behavioral/main.rs index 76a59fd4a..f8a348877 100644 --- a/crates/ml/tests/behavioral/main.rs +++ b/crates/ml/tests/behavioral/main.rs @@ -4,4 +4,31 @@ mod synthetic_markets; mod oracle; mod harness; -// Phase 2B will add: mod test_2_1_*; ... mod test_2_22_*; + +// Group 1: trading behavior (7 tests) +mod test_2_1_flat_market_holds; +mod test_2_2_trend_market_directional; +mod test_2_3_mean_revert_reverses; +mod test_2_4_cost_sensitivity; +mod test_2_6_regime_silences; +mod test_2_7_stop_discipline; +mod test_2_18_action_latency; + +// Group 2: state/arch grounding (10 tests) +mod test_2_8_hold_vs_flat_semantics; +mod test_2_9_eval_fold_isolation; +mod test_2_13_eval_train_consistency; +mod test_2_14_magnitude_uses_all_buckets; +mod test_2_15_fold_transition_stability; +mod test_2_16_q_value_bounded; +mod test_2_17_gradient_flow_to_all_heads; +mod test_2_19_kelly_calibration; +mod test_2_20_state_input_grounding; +mod test_2_21_egf_gate_opens; + +// Group 3 (Phase 2C) lands paired with Phase 3.5 commits: +// mod test_2_5_drawdown_de_risks; +// mod test_2_10_recovery_after_streak; +// mod test_2_11_dd_state_aware_sizing; +// mod test_2_12_cooldown_engagement; +// mod test_2_22_plasticity_cooldown_interlock; diff --git a/crates/ml/tests/behavioral/test_2_13_eval_train_consistency.rs b/crates/ml/tests/behavioral/test_2_13_eval_train_consistency.rs new file mode 100644 index 000000000..24783edeb --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_13_eval_train_consistency.rs @@ -0,0 +1,6 @@ +//! KL(train-mode, eval-mode action dists) < 0.5. +#[test] +#[ignore = "Phase 2B contract — requires train/eval mode toggle harness"] +fn test_2_13_eval_train_consistency() { + // Phase 2B.b: trainer.set_eval_mode(bool) + paired evaluation. +} diff --git a/crates/ml/tests/behavioral/test_2_14_magnitude_uses_all_buckets.rs b/crates/ml/tests/behavioral/test_2_14_magnitude_uses_all_buckets.rs new file mode 100644 index 000000000..25df914b2 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_14_magnitude_uses_all_buckets.rs @@ -0,0 +1,15 @@ +//! Each magnitude bucket > 5% of trade events. +use crate::synthetic_markets::drift_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 3.5 entropy-aware Hold + magnitude lands"] +fn test_2_14_magnitude_uses_all_buckets() { + let bars = drift_market(20_000, 0.5, 0.5, 0.25, 42); + let mut trainer = (); + let result: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars[5_000..], 0); + for key in ["long_quarter", "long_half", "long_full"] { + let pct = result.action_dist.get(key).copied().unwrap_or(0.0); + assert!(pct >= 0.05, "test 2.14 FAIL: mag bucket {} only {:.2}%", key, pct * 100.0); + } +} diff --git a/crates/ml/tests/behavioral/test_2_15_fold_transition_stability.rs b/crates/ml/tests/behavioral/test_2_15_fold_transition_stability.rs new file mode 100644 index 000000000..0f875d715 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_15_fold_transition_stability.rs @@ -0,0 +1,9 @@ +//! Sharpe collapse at fold boundary < 50%. +#[test] +#[ignore = "Phase 2B contract — green via existing StateResetRegistry"] +fn test_2_15_fold_transition_stability() { + // Train on synthetic fold 1; capture sharpe at end. Reset state. Train fold 2; + // capture sharpe at start. assert!((end - start).abs() / end.max(0.01) < 0.50); + // + // Phase 2B.b: requires fold-boundary scaffolding the harness doesn't yet expose. +} diff --git a/crates/ml/tests/behavioral/test_2_16_q_value_bounded.rs b/crates/ml/tests/behavioral/test_2_16_q_value_bounded.rs new file mode 100644 index 000000000..624f1b32a --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_16_q_value_bounded.rs @@ -0,0 +1,6 @@ +//! Q-values stay in [V_MIN, V_MAX] under K=1000 replay updates. +#[test] +#[ignore = "Phase 2B contract — green via existing C51 V-range; requires Q snapshot harness"] +fn test_2_16_q_value_bounded() { + // Phase 2B.b: trainer.snapshot_q_values() + replay-update loop. +} diff --git a/crates/ml/tests/behavioral/test_2_17_gradient_flow_to_all_heads.rs b/crates/ml/tests/behavioral/test_2_17_gradient_flow_to_all_heads.rs new file mode 100644 index 000000000..ea944ef3f --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_17_gradient_flow_to_all_heads.rs @@ -0,0 +1,6 @@ +//! Non-zero grad reaches every head (dir/mag/ord/urg/aux/CQL/IQN/C51/ens). +#[test] +#[ignore = "Phase 2B contract — green via architecture; requires per-head grad-norm harness"] +fn test_2_17_gradient_flow_to_all_heads() { + // Phase 2B.b: trainer.snapshot_per_head_grad_norms() after one backward pass. +} diff --git a/crates/ml/tests/behavioral/test_2_18_action_latency.rs b/crates/ml/tests/behavioral/test_2_18_action_latency.rs new file mode 100644 index 000000000..a08184735 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_18_action_latency.rs @@ -0,0 +1,27 @@ +//! SP15 Phase 2B Group 1 — action latency: Hold→Long within 5 bars of +5σ signal. +//! Anchor for: Phase 3 (responsiveness via teaching). + +use crate::synthetic_markets::flat_market; +use crate::harness::BehavioralResult; +use ml::cuda_pipeline::lob_bar::LobBar; + +#[test] +#[ignore = "Phase 2B contract — requires per-bar action access from harness (Phase 2B.b)"] +fn test_2_18_action_latency() { + // Flat for 5000 bars, then step-function +drift starts at bar 5000 + let mut bars = flat_market(5_000, 0.1, 0.25, 42); + let drift_start_price = bars.last().unwrap().price; + for i in 0..5_000 { + let p = drift_start_price + 0.5 * (i as f32); + bars.push(LobBar::new(p, 0.25, 1.0)); + } + + // Once harness exposes per-bar actions: + // let switch_bar = result.actions.iter().skip(5_000) + // .position(|a| matches!(a, ActionEnum::Long(_))) + // .map(|p| p + 5_000); + // assert!(switch_bar.unwrap_or(usize::MAX) < 5_005, ...); + + let _ = bars; // contract document + let _ = BehavioralResult::default(); +} diff --git a/crates/ml/tests/behavioral/test_2_19_kelly_calibration.rs b/crates/ml/tests/behavioral/test_2_19_kelly_calibration.rs new file mode 100644 index 000000000..32b6c1b60 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_19_kelly_calibration.rs @@ -0,0 +1,7 @@ +//! kelly_f monotonically decreasing in Q-variance. +#[test] +#[ignore = "Phase 2B contract — green via existing Kelly; requires Q-variance sweep harness"] +fn test_2_19_kelly_calibration() { + // Phase 2B.b: trainer.set_q_variance_for_test(v) + read kelly_f from ISV. + // Sweep v ∈ {0.01, 0.1, 1.0}, assert kelly_f[0] > kelly_f[1] > kelly_f[2]. +} diff --git a/crates/ml/tests/behavioral/test_2_1_flat_market_holds.rs b/crates/ml/tests/behavioral/test_2_1_flat_market_holds.rs new file mode 100644 index 000000000..61853ef16 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_1_flat_market_holds.rs @@ -0,0 +1,18 @@ +//! SP15 Phase 2B Group 1 — flat market should produce ≥80% Hold. +//! Anchor for: Phase 3.5 confidence-aware Hold floor (sigmoid). + +use crate::synthetic_markets::flat_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 3.5 Hold floor lands"] +fn test_2_1_flat_market_holds() { + let bars = flat_market(10_000, 0.1, 0.25, 42); + // Phase 2B.b will replace this with minimal_trainer_for_tests() once that helper exists. + let mut trainer = (); + let result: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars[5_000..], 0); + + let hold_pct = result.action_dist.get("hold").copied().unwrap_or(0.0); + assert!(hold_pct >= 0.80, + "test 2.1 FAIL: flat market Hold rate = {:.2}, expected >= 0.80", hold_pct); +} diff --git a/crates/ml/tests/behavioral/test_2_20_state_input_grounding.rs b/crates/ml/tests/behavioral/test_2_20_state_input_grounding.rs new file mode 100644 index 000000000..3ea562675 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_20_state_input_grounding.rs @@ -0,0 +1,7 @@ +//! KL(uptrend-context, downtrend-context action dists) > 0.5. +#[test] +#[ignore = "Phase 2B contract — green via Phase 1.5 dd_pct trunk concat (already landed) + Phase 1.5.b state_dim bump"] +fn test_2_20_state_input_grounding() { + // Run policy on uptrend then downtrend (same dd_pct=0); compute KL. + // Phase 2B.b: action-dist KL helper. +} diff --git a/crates/ml/tests/behavioral/test_2_21_egf_gate_opens.rs b/crates/ml/tests/behavioral/test_2_21_egf_gate_opens.rs new file mode 100644 index 000000000..9f79e5f42 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_21_egf_gate_opens.rs @@ -0,0 +1,15 @@ +//! Phase 0 anchor: aux/Q deliberate disagreement → gate1 opens within 100 steps. +//! ALREADY IMPLEMENTED in crates/ml/tests/sp14_oracle_tests.rs (Task 0.C will land it +//! once Phase 0.B retune completes). This file is a thin re-export hook so the +//! behavioral_suite test target picks it up. + +#[test] +#[ignore = "Phase 2B contract — green via Phase 0.B (test lives in sp14_oracle_tests.rs)"] +fn test_2_21_egf_gate_opens() { + // The actual implementation lives in: + // crates/ml/tests/sp14_oracle_tests.rs::gpu::egf_gate_opens_under_disagreement + // + // This re-export hook documents that test 2.21 EXISTS in the canonical + // behavioral_suite contract. The implementation is reachable via the GPU + // oracle test path; this stub is the catalog entry. +} diff --git a/crates/ml/tests/behavioral/test_2_2_trend_market_directional.rs b/crates/ml/tests/behavioral/test_2_2_trend_market_directional.rs new file mode 100644 index 000000000..e861fe9fa --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_2_trend_market_directional.rs @@ -0,0 +1,20 @@ +//! SP15 Phase 2B Group 1 — trend market should produce ≥70% direction-of-trend. +//! Anchor for: Phase 1+3 (cost + trader teachings). + +use crate::synthetic_markets::drift_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 1+3 land"] +fn test_2_2_trend_market_directional() { + let bars = drift_market(10_000, 0.5, 0.1, 0.25, 42); + let mut trainer = (); + let result: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars[5_000..], 0); + + let long_pct: f32 = result.action_dist.iter() + .filter(|(k, _)| k.starts_with("long_")) + .map(|(_, v)| *v) + .sum(); + assert!(long_pct >= 0.70, + "test 2.2 FAIL: trend market Long rate = {:.2}, expected >= 0.70 on +drift", long_pct); +} diff --git a/crates/ml/tests/behavioral/test_2_3_mean_revert_reverses.rs b/crates/ml/tests/behavioral/test_2_3_mean_revert_reverses.rs new file mode 100644 index 000000000..b4512cd48 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_3_mean_revert_reverses.rs @@ -0,0 +1,32 @@ +//! SP15 Phase 2B Group 1 — mean-revert market should reverse direction at ±2σ within 5 bars. +//! Anchor for: Phase 1+3. + +use crate::synthetic_markets::ou_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 1+3 land + harness exposes per-bar actions"] +fn test_2_3_mean_revert_reverses() { + let mu: f32 = 4500.0; + let sigma: f32 = 1.0; + let bars = ou_market(10_000, 0.05, sigma, mu, 0.25, 42); + let mut trainer = (); + let _result: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars[5_000..], 0); + + // The full assertion requires per-bar action access from BehavioralResult, + // which the Phase 2A harness stub does not yet expose. Phase 2B.b will + // extend BehavioralResult with `actions: Vec`. Until then, + // this test serves as the contract specification. + // + // Once per-bar actions are exposed: + // for (i, bar) in bars.iter().enumerate() { + // let z = (bar.price - mu) / sigma; + // if z.abs() > 2.0 { + // let next_5 = &result.actions[i+1..(i+6).min(result.actions.len())]; + // let expected_dir = if z > 0.0 { /* Short */ } else { /* Long */ }; + // assert!(next_5.iter().any(|a| matches_direction(a, expected_dir)), + // "no reverse at z={:.2}", z); + // } + // } + // For now the test compiles + runs (no-op past the smoke). +} diff --git a/crates/ml/tests/behavioral/test_2_4_cost_sensitivity.rs b/crates/ml/tests/behavioral/test_2_4_cost_sensitivity.rs new file mode 100644 index 000000000..5ecc004f8 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_4_cost_sensitivity.rs @@ -0,0 +1,24 @@ +//! SP15 Phase 2B Group 1 — cost sensitivity: trade freq drops ≥40% under high spread. +//! Anchor for: Phase 3.2 explicit cost in r_quality. + +use crate::synthetic_markets::drift_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 3.2 lands"] +fn test_2_4_cost_sensitivity() { + // Same +drift series at different mean spreads + let bars_low = drift_market(10_000, 0.5, 0.1, /*mean_spread*/ 0.1, 42); + let bars_high = drift_market(10_000, 0.5, 0.1, /*mean_spread*/ 2.0, 42); + + let mut trainer = (); + let result_low: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars_low[5_000..], 0); + let result_high: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars_high[5_000..], 0); + + let low = result_low.trade_count as f32; + let high = result_high.trade_count as f32; + assert!(low > 0.0, "low-spread trade_count = 0; cannot compute sensitivity"); + let drop = (low - high) / low; + assert!(drop >= 0.40, + "test 2.4 FAIL: trade-freq drop = {:.2}, expected >= 0.40 (high-spread)", drop); +} diff --git a/crates/ml/tests/behavioral/test_2_6_regime_silences.rs b/crates/ml/tests/behavioral/test_2_6_regime_silences.rs new file mode 100644 index 000000000..f1a6bee0b --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_6_regime_silences.rs @@ -0,0 +1,18 @@ +//! SP15 Phase 2B Group 1 — high-noise no-edge regime: trade freq ≤20%. +//! Anchor for: Phase 3.4 regret + Phase 3.5 confidence-aware Hold floor. + +use crate::synthetic_markets::flat_market; +use crate::harness::{evaluate_policy_on_market, BehavioralResult}; + +#[test] +#[ignore = "Phase 2B contract — green when Phase 3.4 + 3.5 land"] +fn test_2_6_regime_silences() { + // High-noise flat market: no edge to extract + let bars = flat_market(10_000, /*noise_sigma*/ 5.0, 0.25, 42); + let mut trainer = (); + let result: BehavioralResult = evaluate_policy_on_market(&mut trainer, &bars[5_000..], 0); + + let trade_freq = result.trade_count as f32 / 5_000.0; + assert!(trade_freq <= 0.20, + "test 2.6 FAIL: trade freq = {:.2}, expected <= 0.20 in no-edge regime", trade_freq); +} diff --git a/crates/ml/tests/behavioral/test_2_7_stop_discipline.rs b/crates/ml/tests/behavioral/test_2_7_stop_discipline.rs new file mode 100644 index 000000000..1903f2bf5 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_7_stop_discipline.rs @@ -0,0 +1,22 @@ +//! SP15 Phase 2B Group 1 — adverse excursion past trail dist → close within 1 bar. +//! Anchor for: existing trail-stop logic. + +use crate::harness::BehavioralResult; + +#[test] +#[ignore = "Phase 2B contract — requires forced-action harness extension (Phase 2B.b)"] +fn test_2_7_stop_discipline() { + // This test requires driving a forced Long entry, then injecting an + // adverse-excursion price move past trail distance, then verifying the + // policy emits Flat within 1 bar of the breach. + // + // Harness extensions needed (Phase 2B.b): + // - trainer.step_with_forced_action(action: ActionEnum) -> Result + // - bar-by-bar action observation + // + // Contract: after a trail breach detected at bar T, action[T+1] == Flat. + + // Until the harness exposes forced-action stepping, this test is a + // contract document. Phase 2B.b will populate the assertion body. + let _ = BehavioralResult::default(); +} diff --git a/crates/ml/tests/behavioral/test_2_8_hold_vs_flat_semantics.rs b/crates/ml/tests/behavioral/test_2_8_hold_vs_flat_semantics.rs new file mode 100644 index 000000000..3d7d4d4d3 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_8_hold_vs_flat_semantics.rs @@ -0,0 +1,8 @@ +//! Hold preserves pre_pos; Flat zeros it. Anchor for: existing semantics. +#[test] +#[ignore = "Phase 2B contract — requires forced-action + position-state inspection harness"] +fn test_2_8_hold_vs_flat_semantics() { + // Drive entry → Hold → assert pre_pos != 0 + // Drive entry → Flat → assert pre_pos == 0 + // Phase 2B.b harness extension required. +} diff --git a/crates/ml/tests/behavioral/test_2_9_eval_fold_isolation.rs b/crates/ml/tests/behavioral/test_2_9_eval_fold_isolation.rs new file mode 100644 index 000000000..fbf744f23 --- /dev/null +++ b/crates/ml/tests/behavioral/test_2_9_eval_fold_isolation.rs @@ -0,0 +1,6 @@ +//! KL(shuffled-future, unshuffled-future) < 0.01 — proves no temporal leak. +#[test] +#[ignore = "Phase 2B contract — green via StateResetRegistry coverage; requires KL helper"] +fn test_2_9_eval_fold_isolation() { + // Phase 2B.b: harness exposes action_distribution_kl(actions_a, actions_b). +}