//! Integration tests for RaptorBT indicators. use raptorbt::indicators::ferro_bridge::{ chandelier_exit, choppiness_index, detect_breaks_cusum, donchian, drawdown_series, ht_dcperiod, ht_dcphase, ht_trendline, hull_ma, ichimoku, pivot_points, regime_adx, relative_strength, rolling_beta, rolling_variance_break, ratio, spread, vwma, zscore_series, }; use raptorbt::indicators::momentum::{macd, rsi, stochastic}; use raptorbt::indicators::strength::adx; use raptorbt::indicators::trend::{ema, sma, supertrend}; use raptorbt::indicators::volatility::{atr, bollinger_bands}; use raptorbt::indicators::volume::vwap; fn sample_ohlcv() -> (Vec, Vec, Vec, Vec, Vec) { // Create sample OHLCV data with 50 bars let n = 50; let mut close: Vec = vec![100.0]; let mut high: Vec = vec![101.0]; let mut low: Vec = vec![99.0]; let mut open: Vec = vec![100.0]; let volume: Vec = vec![1000.0; n]; // Generate trending data for i in 1..n { let prev_close = close[i - 1]; let change = ((i as f64 * 0.2).sin() * 2.0) + 0.5; // Slight uptrend with oscillation let new_close = prev_close + change; close.push(new_close); open.push(prev_close); high.push(new_close.max(prev_close) + 0.5); low.push(new_close.min(prev_close) - 0.5); } (open, high, low, close, volume) } #[test] fn test_sma_correctness() { let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; let result = sma(&data, 3).unwrap(); // First 2 values should be NaN assert!(result[0].is_nan()); assert!(result[1].is_nan()); // SMA(3) for [1,2,3] = 2.0 assert!((result[2] - 2.0).abs() < 1e-10); // SMA(3) for [2,3,4] = 3.0 assert!((result[3] - 3.0).abs() < 1e-10); // SMA(3) for [8,9,10] = 9.0 assert!((result[9] - 9.0).abs() < 1e-10); } #[test] fn test_ema_correctness() { let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; let result = ema(&data, 3).unwrap(); // First 2 values should be NaN assert!(result[0].is_nan()); assert!(result[1].is_nan()); // EMA should be valid from index 2 assert!(!result[2].is_nan()); assert!(!result[9].is_nan()); // EMA should be between min and max assert!(result[9] >= 1.0 && result[9] <= 10.0); } #[test] fn test_rsi_range() { let (_, _, _, close, _) = sample_ohlcv(); let result = rsi(&close, 14).unwrap(); // Check RSI is in valid range [0, 100] for (i, &value) in result.iter().enumerate() { if !value.is_nan() { assert!( value >= 0.0 && value <= 100.0, "RSI at index {} is out of range: {}", i, value ); } } } #[test] fn test_macd_structure() { let (_, _, _, close, _) = sample_ohlcv(); let result = macd(&close, 12, 26, 9).unwrap(); assert_eq!(result.macd_line.len(), close.len()); assert_eq!(result.signal_line.len(), close.len()); assert_eq!(result.histogram.len(), close.len()); // MACD line should be valid from index 25 (slow_period - 1) assert!(result.macd_line[24].is_nan()); assert!(!result.macd_line[25].is_nan()); } #[test] fn test_stochastic_range() { let (_, high, low, close, _) = sample_ohlcv(); let result = stochastic(&high, &low, &close, 14, 3).unwrap(); // %K and %D should be in [0, 100] for (i, &k) in result.k.iter().enumerate() { if !k.is_nan() { assert!(k >= 0.0 && k <= 100.0, "%K at index {} is out of range: {}", i, k); } } for (i, &d) in result.d.iter().enumerate() { if !d.is_nan() { assert!(d >= 0.0 && d <= 100.0, "%D at index {} is out of range: {}", i, d); } } } #[test] fn test_atr_positive() { let (_, high, low, close, _) = sample_ohlcv(); let result = atr(&high, &low, &close, 14).unwrap(); // ATR should always be non-negative for (i, &value) in result.iter().enumerate() { if !value.is_nan() { assert!(value >= 0.0, "ATR at index {} is negative: {}", i, value); } } } #[test] fn test_bollinger_bands_ordering() { let (_, _, _, close, _) = sample_ohlcv(); let result = bollinger_bands(&close, 20, 2.0).unwrap(); // Upper > Middle > Lower for i in 19..close.len() { if !result.upper[i].is_nan() { assert!( result.upper[i] >= result.middle[i], "Upper band should be >= middle at index {}", i ); assert!( result.middle[i] >= result.lower[i], "Middle band should be >= lower at index {}", i ); } } } #[test] fn test_adx_range() { let (_, high, low, close, _) = sample_ohlcv(); let result = adx(&high, &low, &close, 14).unwrap(); // ADX should be in [0, 100] for (i, &value) in result.iter().enumerate() { if !value.is_nan() { assert!( value >= 0.0 && value <= 100.0, "ADX at index {} is out of range: {}", i, value ); } } } #[test] fn test_vwap_bounds() { let (_, high, low, close, volume) = sample_ohlcv(); let result = vwap(&high, &low, &close, &volume).unwrap(); // VWAP should be between the overall min low and max high let min_low = low.iter().cloned().fold(f64::INFINITY, f64::min); let max_high = high.iter().cloned().fold(f64::NEG_INFINITY, f64::max); for (i, &value) in result.iter().enumerate() { if !value.is_nan() { assert!( value >= min_low && value <= max_high, "VWAP at index {} is out of bounds: {} (should be between {} and {})", i, value, min_low, max_high ); } } } #[test] fn test_supertrend_direction() { let (_, high, low, close, _) = sample_ohlcv(); let result = supertrend(&high, &low, &close, 10, 3.0).unwrap(); // Direction should be either 1 or -1 for (i, &dir) in result.direction.iter().enumerate() { if dir != 0 { assert!( dir == 1 || dir == -1, "Supertrend direction at index {} is invalid: {}", i, dir ); } } } #[test] fn test_invalid_period() { let data = vec![1.0, 2.0, 3.0]; // Period of 0 should error assert!(sma(&data, 0).is_err()); assert!(ema(&data, 0).is_err()); assert!(rsi(&data, 0).is_err()); } #[test] fn test_empty_data() { let empty: Vec = vec![]; let result = sma(&empty, 10).unwrap(); assert!(result.is_empty()); let result = ema(&empty, 10).unwrap(); assert!(result.is_empty()); } // ========================================================================= // P0 batch — Extended / Cycle / Regime / Portfolio // ========================================================================= #[test] fn test_vwma_basic() { let (open, high, low, close, volume) = sample_ohlcv(); let _ = open; let _ = high; let _ = low; let r = vwma(&close, &volume, 5).unwrap(); assert_eq!(r.len(), close.len()); assert!(r[0].is_nan()); assert!(r[3].is_nan()); assert!(!r[4].is_nan()); } #[test] fn test_vwma_invalid_period() { let (_, _, _, close, volume) = sample_ohlcv(); assert!(vwma(&close, &volume, 0).is_err()); } #[test] fn test_donchian_basic() { let (_, high, low, _, _) = sample_ohlcv(); let r = donchian(&high, &low, 10).unwrap(); assert_eq!(r.upper.len(), high.len()); assert_eq!(r.middle.len(), high.len()); assert_eq!(r.lower.len(), high.len()); for i in 9..high.len() { assert!(r.upper[i] >= r.middle[i]); assert!(r.middle[i] >= r.lower[i]); } } #[test] fn test_donchian_invalid_period() { let (_, high, low, _, _) = sample_ohlcv(); assert!(donchian(&high, &low, 0).is_err()); } #[test] fn test_choppiness_range() { let (_, high, low, close, _) = sample_ohlcv(); let r = choppiness_index(&high, &low, &close, 14).unwrap(); for (i, &v) in r.iter().enumerate() { if !v.is_nan() { assert!(v >= 0.0 && v <= 100.0, "CI at {} out of range: {}", i, v); } } } #[test] fn test_hull_ma_basic() { let (_, _, _, close, _) = sample_ohlcv(); let r = hull_ma(&close, 20).unwrap(); assert_eq!(r.len(), close.len()); let valid = r.iter().filter(|v| !v.is_nan()).count(); assert!(valid > 0); } #[test] fn test_chandelier_exit_ordering() { let (_, high, low, close, _) = sample_ohlcv(); let r = chandelier_exit(&high, &low, &close, 10, 2.0).unwrap(); assert_eq!(r.long_exit.len(), high.len()); assert_eq!(r.short_exit.len(), high.len()); } #[test] fn test_chandelier_invalid() { let (_, high, low, close, _) = sample_ohlcv(); assert!(chandelier_exit(&high, &low, &close, 0, 2.0).is_err()); assert!(chandelier_exit(&high, &low, &close, 10, -1.0).is_err()); } #[test] fn test_ichimoku_structure() { let (_, high, low, close, _) = sample_ohlcv(); let r = ichimoku(&high, &low, &close, 9, 26, 52, 26).unwrap(); assert_eq!(r.tenkan.len(), close.len()); assert_eq!(r.kijun.len(), close.len()); assert_eq!(r.senkou_a.len(), close.len()); assert_eq!(r.senkou_b.len(), close.len()); assert_eq!(r.chikou.len(), close.len()); // tenkan period 9 -> first valid at index 8 assert!(r.tenkan[7].is_nan()); assert!(!r.tenkan[8].is_nan()); } #[test] fn test_pivot_points_classic() { let (_, high, low, close, _) = sample_ohlcv(); let r = pivot_points(&high, &low, &close, "classic").unwrap(); assert_eq!(r.pivot.len(), close.len()); assert!(r.pivot[0].is_nan()); assert!(!r.pivot[1].is_nan()); } #[test] fn test_pivot_points_unknown_method() { let (_, high, low, close, _) = sample_ohlcv(); assert!(pivot_points(&high, &low, &close, "bogus").is_err()); } #[test] fn test_ht_trendline_min_length() { let (_, _, _, close, _) = sample_ohlcv(); assert!(ht_trendline(&close).is_ok()); let short = vec![1.0; 10]; assert!(ht_trendline(&short).is_err()); } #[test] fn test_ht_dcperiod_dcphase_basic() { let (_, _, _, close, _) = sample_ohlcv(); let p = ht_dcperiod(&close).unwrap(); let ph = ht_dcphase(&close).unwrap(); assert_eq!(p.len(), close.len()); assert_eq!(ph.len(), close.len()); } #[test] fn test_regime_adx_labels() { // Synthesize ADX series let adx_input: Vec = (0..50).map(|i| if i < 25 { 10.0 } else { 30.0 }).collect(); let r = regime_adx(&adx_input, 20.0).unwrap(); // First 25 bars -> range (0), last 25 -> trend (1) for i in 0..25 { assert_eq!(r[i], 0, "idx {} expected 0 got {}", i, r[i]); } for i in 25..50 { assert_eq!(r[i], 1, "idx {} expected 1 got {}", i, r[i]); } } #[test] fn test_detect_breaks_cusum_basic() { let series: Vec = (0..100) .map(|i| if i < 50 { 0.0 } else { 5.0 }) .collect(); let r = detect_breaks_cusum(&series, 10, 5.0, 0.5).unwrap(); assert_eq!(r.len(), series.len()); assert_eq!(r[0], 0); } #[test] fn test_detect_breaks_cusum_invalid_window() { let v = vec![1.0; 10]; assert!(detect_breaks_cusum(&v, 1, 1.0, 0.5).is_err()); } #[test] fn test_rolling_variance_break_basic() { let series: Vec = (0..60) .map(|i| if i < 40 { 0.01 } else { 1.0 }) .collect(); let r = rolling_variance_break(&series, 5, 20, 2.0).unwrap(); assert_eq!(r.len(), series.len()); } #[test] fn test_rolling_variance_break_invalid() { let v = vec![1.0; 30]; assert!(rolling_variance_break(&v, 1, 10, 1.0).is_err()); assert!(rolling_variance_break(&v, 5, 5, 1.0).is_err()); } #[test] fn test_rolling_beta_basic() { let (_, _, _, close, _) = sample_ohlcv(); let bench: Vec = close.iter().map(|x| x * 0.5 + 1.0).collect(); let r = rolling_beta(&close, &bench, 14).unwrap(); assert_eq!(r.len(), close.len()); } #[test] fn test_rolling_beta_invalid_window() { let v = vec![1.0; 10]; assert!(rolling_beta(&v, &v, 1).is_err()); } #[test] fn test_drawdown_series_basic() { let equity = vec![100.0, 110.0, 105.0, 120.0, 90.0, 95.0, 130.0]; let r = drawdown_series(&equity).unwrap(); assert_eq!(r.series.len(), equity.len()); assert!(r.max_drawdown <= 0.0); // Max drawdown: 90 / 120 - 1 = -0.25 assert!((r.max_drawdown - (-0.25)).abs() < 1e-9); // Per-bar dd is non-positive at all valid points for &v in &r.series { assert!(v <= 0.0); } } #[test] fn test_zscore_series_basic() { let x: Vec = (0..30).map(|i| i as f64).collect(); let r = zscore_series(&x, 10).unwrap(); assert_eq!(r.len(), x.len()); } #[test] fn test_zscore_invalid_window() { let v = vec![1.0; 10]; assert!(zscore_series(&v, 1).is_err()); } #[test] fn test_relative_strength_basic() { let a: Vec = (0..50).map(|i| (i as f64) * 0.01).collect(); let b: Vec = (0..50).map(|i| (i as f64) * 0.005).collect(); let r = relative_strength(&a, &b).unwrap(); assert_eq!(r.len(), a.len()); // RS should be positive: a - beta*b > 0 since a > b for i in 10..r.len() { assert!(r[i] > 0.0); } } #[test] fn test_relative_strength_length_mismatch() { let a = vec![1.0; 10]; let b = vec![2.0; 5]; assert!(relative_strength(&a, &b).is_err()); } #[test] fn test_spread_and_ratio() { let a = vec![10.0, 20.0, 30.0]; let b = vec![1.0, 2.0, 3.0]; let s = spread(&a, &b, 2.0).unwrap(); let r = ratio(&a, &b).unwrap(); assert_eq!(s.len(), a.len()); assert_eq!(r.len(), a.len()); assert!((s[0] - 8.0).abs() < 1e-9); // 10 - 2*1 assert!((r[0] - 10.0).abs() < 1e-9); // 10/1 } #[test] fn test_spread_length_mismatch() { let a = vec![1.0; 5]; let b = vec![2.0; 3]; assert!(spread(&a, &b, 1.0).is_err()); assert!(ratio(&a, &b).is_err()); }