test(v8): 9 explicit unit tests for all v8 reward components
This commit is contained in:
@@ -16,3 +16,5 @@ mod walk_forward;
|
||||
mod hyperopt;
|
||||
#[cfg(test)]
|
||||
mod regression;
|
||||
#[cfg(test)]
|
||||
pub mod reward_v8;
|
||||
|
||||
121
crates/ml/src/trainers/dqn/smoke_tests/reward_v8.rs
Normal file
121
crates/ml/src/trainers/dqn/smoke_tests/reward_v8.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! Reward v8 unit tests — verify mathematical properties of each component.
|
||||
//! CPU-side tests that validate the CUDA kernel logic produces correct results.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
fn asymmetric_soft_clamp(x: f32) -> f32 {
|
||||
if x >= 0.0 { x.min(10.0) } else { -10.0 * (1.0 - (x / 10.0).exp()) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asymmetric_soft_clamp_properties() {
|
||||
assert!((asymmetric_soft_clamp(0.0)).abs() < 1e-6, "f(0) = 0");
|
||||
assert!((asymmetric_soft_clamp(5.0) - 5.0).abs() < 1e-6, "f(5) = 5 (linear)");
|
||||
assert!((asymmetric_soft_clamp(15.0) - 10.0).abs() < 1e-6, "f(15) = 10 (capped)");
|
||||
assert!((asymmetric_soft_clamp(-5.0) - (-3.935)).abs() < 0.01, "f(-5) ≈ -3.93");
|
||||
assert!((asymmetric_soft_clamp(-15.0) - (-7.769)).abs() < 0.01, "f(-15) ≈ -7.77");
|
||||
assert!(asymmetric_soft_clamp(3.0) > asymmetric_soft_clamp(2.0), "monotonic positive");
|
||||
assert!(asymmetric_soft_clamp(-2.0) > asymmetric_soft_clamp(-3.0), "monotonic negative");
|
||||
assert!(asymmetric_soft_clamp(-5.0).abs() < asymmetric_soft_clamp(5.0).abs(), "asymmetric");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kelly_prior_neutral() {
|
||||
let prior_wins = 2.0_f64;
|
||||
let prior_losses = 2.0;
|
||||
let prior_sum_wins = 0.01;
|
||||
let prior_sum_losses = 0.01;
|
||||
let eff_total = prior_wins + prior_losses;
|
||||
let win_rate = prior_wins / eff_total;
|
||||
let avg_win = prior_sum_wins / prior_wins;
|
||||
let avg_loss = prior_sum_losses / prior_losses;
|
||||
let payoff = avg_win / avg_loss;
|
||||
let kelly_f = (payoff * win_rate - (1.0 - win_rate)) / payoff;
|
||||
assert!(kelly_f.abs() < 0.01, "Kelly with prior only should be ~0, got {kelly_f}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cosine_epsilon_schedule() {
|
||||
let eps_start = 0.3_f64;
|
||||
let eps_end = 0.02;
|
||||
let total = 20.0;
|
||||
let e0 = eps_end + 0.5 * (eps_start - eps_end) * (1.0 + (PI * 0.0 / total).cos());
|
||||
assert!((e0 - eps_start).abs() < 0.001, "Epoch 0 should be eps_start, got {e0}");
|
||||
let ef = eps_end + 0.5 * (eps_start - eps_end) * (1.0 + PI.cos());
|
||||
assert!((ef - eps_end).abs() < 0.001, "Final epoch should be eps_end, got {ef}");
|
||||
let em = eps_end + 0.5 * (eps_start - eps_end) * (1.0 + (PI * 0.5).cos());
|
||||
assert!(em > eps_end && em < eps_start, "Mid epsilon {em} between bounds");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_td_lambda_degenerates() {
|
||||
let rewards = [1.0_f64, 2.0, 3.0];
|
||||
let q_next = [10.0, 20.0, 30.0];
|
||||
let gamma = 0.95;
|
||||
let g1 = rewards[0] + gamma * q_next[0];
|
||||
assert!((g1 - 10.5).abs() < 0.01, "1-step return should be 10.5, got {g1}");
|
||||
let g_mc = rewards[0] + gamma * rewards[1] + gamma * gamma * rewards[2]
|
||||
+ gamma * gamma * gamma * q_next[2];
|
||||
let expected = 1.0 + 0.95 * 2.0 + 0.9025 * 3.0 + 0.857375 * 30.0;
|
||||
assert!((g_mc - expected).abs() < 0.01, "MC return should be {expected}, got {g_mc}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_micro_reward_adaptive_scale() {
|
||||
let base = 0.001_f64;
|
||||
let vol_calm = 0.0025_f64;
|
||||
let adaptive_calm = base / (vol_calm / 0.005_f64).sqrt().max(0.5);
|
||||
assert!(adaptive_calm > base, "Calm scale ({adaptive_calm}) > base ({base})");
|
||||
let vol_volatile = 0.02_f64;
|
||||
let adaptive_volatile = base / (vol_volatile / 0.005_f64).sqrt().max(0.5);
|
||||
assert!(adaptive_volatile < base, "Volatile scale ({adaptive_volatile}) < base ({base})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exposure_aux_gradient_unique() {
|
||||
let b0 = 9;
|
||||
let target = 3;
|
||||
let prob = 1.0 / b0 as f64;
|
||||
let grads: Vec<f64> = (0..b0).map(|a| prob - if a == target { 1.0 } else { 0.0 }).collect();
|
||||
assert!(grads[target] < 0.0, "Target gets negative gradient");
|
||||
for a in 0..b0 {
|
||||
if a != target { assert!(grads[a] > 0.0, "Non-target {a} gets positive gradient"); }
|
||||
}
|
||||
assert!((grads[target] - grads[0]).abs() > 0.1, "Target differs from non-target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hindsight_relabel_improves_reward() {
|
||||
let entry = 100.0_f64;
|
||||
let futures = [101.0, 103.0, 102.0];
|
||||
let position = 1.0;
|
||||
let actual_pnl = position * (futures[0] - entry);
|
||||
let best_pnl = futures.iter().map(|&p| position * (p - entry)).fold(0.0_f64, f64::max);
|
||||
assert!((best_pnl - 3.0).abs() < 0.01, "Best PnL should be 3.0");
|
||||
assert!(best_pnl >= actual_pnl, "Hindsight >= actual");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_curriculum_ordering() {
|
||||
let trending = 0.02_f64;
|
||||
let choppy = 0.001;
|
||||
let score_trending = 1.0 / (trending + 0.001);
|
||||
let score_choppy = 1.0 / (choppy + 0.001);
|
||||
assert!(score_trending < score_choppy, "Trending (easy) < choppy (hard)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_popart_normalization() {
|
||||
let rewards = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
|
||||
let mean = rewards.iter().sum::<f64>() / rewards.len() as f64;
|
||||
let var = rewards.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / rewards.len() as f64;
|
||||
let std = var.sqrt();
|
||||
let normalized: Vec<f64> = rewards.iter().map(|r| (r - mean) / std).collect();
|
||||
let norm_mean = normalized.iter().sum::<f64>() / normalized.len() as f64;
|
||||
let norm_var = normalized.iter().map(|r| (r - norm_mean).powi(2)).sum::<f64>() / normalized.len() as f64;
|
||||
assert!(norm_mean.abs() < 1e-10, "Mean should be 0, got {norm_mean}");
|
||||
assert!((norm_var - 1.0).abs() < 0.01, "Variance should be 1, got {norm_var}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user