use approx::assert_relative_eq;
use proptest::prelude::*;
use quantwave_core::features::*;
use quantwave_core::regimes::MarketRegime;
use quantwave_core::traits::Next;
fn generate_synthetic_with_regime_shifts(n: usize) -> (Vec<f64>, Vec<&'static str>) {
let mut prices = Vec::with_capacity(n);
let mut labels = Vec::with_capacity(n);
let mut price = 100.0;
let mut rng = 0.42f64;
let n1 = n / 4;
for i in 0..n1 {
rng = (rng * 6364136223846793005u64 as f64 + 1.0).fract();
let noise = (rng - 0.5) * 0.4;
price += 0.25 + noise;
prices.push(price);
labels.push("trending");
}
let n2 = n / 4;
let mut local_mean = price;
for _ in 0..n2 {
rng = (rng * 6364136223846793005u64 as f64 + 1.0).fract();
let noise = (rng - 0.5) * 1.2;
price = 0.7 * price + 0.3 * local_mean + noise;
prices.push(price);
labels.push("mean_reverting");
if (prices.len() - n1) % 8 == 0 {
local_mean = price; }
}
let n3 = n / 4;
for _ in 0..n3 {
rng = (rng * 6364136223846793005u64 as f64 + 1.0).fract();
let noise = (rng - 0.5) * 4.5;
price += noise * 0.1; prices.push(price);
labels.push("high_vol");
}
let n4 = n - prices.len();
for _ in 0..n4 {
rng = (rng * 6364136223846793005u64 as f64 + 1.0).fract();
let noise = (rng - 0.5) * 0.6;
price += noise * 0.05;
prices.push(price);
labels.push("steady");
}
(prices, labels)
}
fn replay_to<T: Next<f64> + Clone>(mut ext: T, data: &[f64], up_to: usize) -> T::Output {
let mut out = ext.next(data[0]);
for &x in &data[1..=up_to] {
out = ext.next(x);
}
out
}
#[test]
fn test_regime_to_features_basic() {
let f = regime_to_features(MarketRegime::Bull);
assert_eq!(f.regime_vector[0], 1.0);
assert_eq!(f.regime_label, MarketRegime::Bull);
let f2 = regime_to_features(MarketRegime::Crisis);
assert_eq!(f2.regime_vector[2], 1.0);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn test_cyber_cycle_feature_parity_no_lookahead(
inputs in prop::collection::vec(80.0..120.0, 40..120)
) {
let mut extractor = CyberCycleFeatureExtractor::new(14);
let mut streamed: Vec<CyberCycleFeatures> = Vec::new();
for &x in &inputs {
streamed.push(extractor.next(x));
}
let check_points = [8usize, inputs.len() / 3, inputs.len() / 2, inputs.len() - 1];
for &t in &check_points {
if t >= inputs.len() { continue; }
let fresh = CyberCycleFeatureExtractor::new(14);
let replay = replay_to(fresh, &inputs, t);
if !streamed[t].cycle.is_nan() && !replay.cycle.is_nan() {
assert_relative_eq!(streamed[t].cycle, replay.cycle, epsilon = 1e-9);
}
if !streamed[t].trigger.is_nan() && !replay.trigger.is_nan() {
assert_relative_eq!(streamed[t].trigger, replay.trigger, epsilon = 1e-9);
}
assert_relative_eq!(streamed[t].cycle_momentum, replay.cycle_momentum, epsilon = 1e-9);
assert_relative_eq!(streamed[t].trigger_signal, replay.trigger_signal, epsilon = 1e-9);
}
}
#[test]
fn test_hurst_feature_parity_no_lookahead(
inputs in prop::collection::vec(90.0..110.0, 50..150)
) {
let mut extractor = HurstFeatureExtractor::new(20);
let mut streamed: Vec<HurstFeatures> = Vec::new();
for &x in &inputs {
streamed.push(extractor.next(x));
}
let check_points = [19usize, inputs.len() / 2, inputs.len() - 1];
for &t in &check_points {
if t >= inputs.len() { continue; }
let fresh = HurstFeatureExtractor::new(20);
let replay = replay_to(fresh, &inputs, t);
if !streamed[t].persistence.is_nan() && !replay.persistence.is_nan() {
assert_relative_eq!(streamed[t].persistence, replay.persistence, epsilon = 5e-8);
}
assert_eq!(streamed[t].regime_label, replay.regime_label);
}
}
#[test]
fn test_trendflex_feature_parity_no_lookahead(
inputs in prop::collection::vec(95.0..105.0, 40..100)
) {
let mut extractor = TrendflexFeatureExtractor::new(20);
let mut streamed: Vec<TrendflexFeatures> = Vec::new();
for &x in &inputs {
streamed.push(extractor.next(x));
}
let check_points = [10usize, inputs.len() / 2, inputs.len() - 1];
for &t in &check_points {
if t >= inputs.len() { continue; }
let fresh = TrendflexFeatureExtractor::new(20);
let replay = replay_to(fresh, &inputs, t);
if !streamed[t].trendflex.is_nan() && !replay.trendflex.is_nan() {
assert_relative_eq!(streamed[t].trendflex, replay.trendflex, epsilon = 1e-9);
}
}
}
#[test]
fn test_instantaneous_trendline_feature_parity_no_lookahead(
inputs in prop::collection::vec(100.0..110.0, 60..120)
) {
let mut extractor = InstantaneousTrendlineFeatureExtractor::new();
let mut streamed: Vec<InstantaneousTrendlineFeatures> = Vec::new();
for &x in &inputs {
streamed.push(extractor.next(x));
}
let check_points = [20usize, inputs.len() / 2, inputs.len() - 1];
for &t in &check_points {
if t >= inputs.len() { continue; }
let fresh = InstantaneousTrendlineFeatureExtractor::new();
let replay = replay_to(fresh, &inputs, t);
if !streamed[t].trend.is_nan() && !replay.trend.is_nan() {
assert_relative_eq!(streamed[t].trend, replay.trend, epsilon = 1e-8);
}
assert_relative_eq!(streamed[t].strength, replay.strength, epsilon = 1e-12);
}
}
}
#[test]
fn test_feature_regime_conditional_and_stability() {
let (prices, labels) = generate_synthetic_with_regime_shifts(180);
let mut h_ext = HurstFeatureExtractor::new(16);
let mut hurst_trending = Vec::new();
let mut hurst_mr = Vec::new();
let mut hurst_highvol = Vec::new();
for (i, &p) in prices.iter().enumerate() {
let f = h_ext.next(p);
if !f.persistence.is_nan() {
match labels[i] {
"trending" => hurst_trending.push(f.persistence),
"mean_reverting" => hurst_mr.push(f.persistence),
"high_vol" => hurst_highvol.push(f.persistence),
_ => {}
}
}
}
if !hurst_trending.is_empty() && !hurst_mr.is_empty() {
let mean_trend: f64 = hurst_trending.iter().sum::<f64>() / hurst_trending.len() as f64;
let mean_mr: f64 = hurst_mr.iter().sum::<f64>() / hurst_mr.len() as f64;
assert!(mean_trend > mean_mr - 0.08, "Hurst trending mean {} not > mean-rev {}", mean_trend, mean_mr);
}
let mut cc_ext = CyberCycleFeatureExtractor::new(12);
let mut cycle_vals: Vec<f64> = Vec::new();
for (i, &p) in prices.iter().enumerate() {
let f = cc_ext.next(p);
if labels[i] == "mean_reverting" && !f.cycle.is_nan() {
cycle_vals.push(f.cycle.abs());
}
}
if cycle_vals.len() > 10 {
let max_cycle = cycle_vals.iter().cloned().fold(0.0f64, f64::max);
assert!(max_cycle > 0.1, "CyberCycle produced near-zero amplitude in mean-rev regime");
}
let mut tf_ext = TrendflexFeatureExtractor::new(18);
let mut steady_tf: Vec<f64> = Vec::new();
for (i, &p) in prices.iter().enumerate() {
let f = tf_ext.next(p);
if labels[i] == "steady" && !f.trendflex.is_nan() {
steady_tf.push(f.trendflex);
}
}
if steady_tf.len() > 5 {
let mean: f64 = steady_tf.iter().sum::<f64>() / steady_tf.len() as f64;
let var: f64 = steady_tf.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / steady_tf.len() as f64;
assert!(var < 25.0, "Trendflex variance too high ({}) in steady regime - stability failure", var);
}
}
#[test]
fn test_feature_matrix_build_smoke() {
let (prices, _labels) = generate_synthetic_with_regime_shifts(60);
let mut cc = CyberCycleFeatureExtractor::new(10);
let mut hu = HurstFeatureExtractor::new(12);
let mut tf = TrendflexFeatureExtractor::new(10);
let mut it = InstantaneousTrendlineFeatureExtractor::new();
let mut matrix_rows = 0usize;
for &p in &prices {
let _c = cc.next(p);
let _h = hu.next(p);
let _t = tf.next(p);
let _i = it.next(p);
matrix_rows += 1;
}
assert!(matrix_rows == 60);
}
#[cfg(test)]
mod real_data_like_samples {
use super::*;
#[test]
fn test_features_on_realistic_sample() {
let realistic: Vec<f64> = vec![
100.0, 100.8, 99.7, 101.2, 100.5, 102.1, 101.8, 99.9, 103.4, 102.7,
104.1, 103.0, 101.5, 105.2, 104.8, 106.3, 105.9, 104.2, 107.1, 106.5,
];
let mut h = HurstFeatureExtractor::new(8);
let mut last_pers = 0.0;
for &p in &realistic {
let f = h.next(p);
if !f.persistence.is_nan() {
last_pers = f.persistence;
}
}
assert!(last_pers.is_finite() || last_pers.is_nan());
}
}