diff --git a/core/src/indicators/efi.rs b/core/src/indicators/efi.rs index 3ff3324..a1207ae 100644 --- a/core/src/indicators/efi.rs +++ b/core/src/indicators/efi.rs @@ -1,4 +1,4 @@ -use crate::indicators::ema::ema_aligned; +use crate::indicators::ema::ema; pub fn efi(closes: &[Option], volumes: &[Option], period: usize) -> Vec> { let len = closes.len(); @@ -20,7 +20,7 @@ pub fn efi(closes: &[Option], volumes: &[Option], period: usize) -> Ve if period == 1 { efi = force; } else { - efi = ema_aligned(&force, period); + efi = ema(&force, period); } efi diff --git a/core/src/indicators/ema.rs b/core/src/indicators/ema.rs index 5fdb38d..875a81d 100644 --- a/core/src/indicators/ema.rs +++ b/core/src/indicators/ema.rs @@ -47,9 +47,6 @@ pub fn ema(data: &[Option], period: usize) -> Vec> { result } - -pub(crate) use ema as ema_aligned; - #[cfg(test)] mod tests { use super::*; diff --git a/core/src/indicators/eom.rs b/core/src/indicators/eom.rs index b519f6c..f86996b 100644 --- a/core/src/indicators/eom.rs +++ b/core/src/indicators/eom.rs @@ -1,4 +1,4 @@ -use crate::indicators::sma::sma_aligned; +use crate::indicators::sma::sma; pub fn eom( highs: &[Option], @@ -9,7 +9,7 @@ pub fn eom( scale: f64, ) -> (Vec>, Vec>) { let eom_line = eom_line(highs, lows, volumes, period, scale); - let signal = sma_aligned(&eom_line, signal_period); + let signal = sma(&eom_line, signal_period); (eom_line, signal) } @@ -23,7 +23,7 @@ pub fn eom_signal( scale: f64, ) -> Vec> { let eom_line = eom_line(highs, lows, volumes, period, scale); - sma_aligned(&eom_line, signal_period) + sma(&eom_line, signal_period) } pub fn eom_line( @@ -63,7 +63,7 @@ pub fn eom_line( } } - sma_aligned(&eom_values, period) + sma(&eom_values, period) } #[cfg(test)] diff --git a/core/src/indicators/macd.rs b/core/src/indicators/macd.rs index 1a605e2..d79ace8 100644 --- a/core/src/indicators/macd.rs +++ b/core/src/indicators/macd.rs @@ -1,13 +1,13 @@ -use crate::indicators::ema::{ema_aligned, ema_dense}; +use crate::indicators::ema::ema; pub fn macd( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> (Vec>, Vec>, Vec>) { let macd_line = macd_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&macd_line, signal_period); + let signal_line = ema(&macd_line, signal_period); let histogram = macd_line .iter() .zip(signal_line.iter()) @@ -21,13 +21,13 @@ pub fn macd( } pub fn macd_histogram( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let macd_line = macd_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&macd_line, signal_period); + let signal_line = ema(&macd_line, signal_period); macd_line .iter() @@ -40,24 +40,24 @@ pub fn macd_histogram( } pub fn macd_signal( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let macd_line = macd_line(data, fast_period, slow_period); - ema_aligned(&macd_line, signal_period) + ema(&macd_line, signal_period) } -pub fn macd_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec> { +pub fn macd_line(data: &[Option], fast_period: usize, slow_period: usize) -> Vec> { let mut macd_line = vec![None; data.len()]; if data.len() < slow_period || fast_period >= slow_period { return macd_line; } - let fast_ema = ema_dense(data, fast_period); - let slow_ema = ema_dense(data, slow_period); + let fast_ema = ema(data, fast_period); + let slow_ema = ema(data, slow_period); for i in (slow_period - 1)..data.len() { if let (Some(fast), Some(slow)) = (fast_ema[i], slow_ema[i]) { @@ -82,7 +82,10 @@ mod tests { // When for symbol in test_cases { - let input = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let input = testutils::load_data(&format!("../data/{}.json", symbol), "c") + .into_iter() + .map(Some) + .collect::>(); let (macd_line, signal_line, histogram) = macd(&input, 12, 26, 9); let expected_macd = testutils::load_expected::>(&format!( @@ -119,4 +122,61 @@ mod tests { ); } } + + #[test] + fn test_macd_signal_follows_base_ema_contract_across_gaps() { + let input = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + Some(4.0), + None, + Some(5.0), + Some(6.0), + Some(7.0), + ]; + + let (line, signal, histogram) = macd(&input, 2, 3, 2); + + assert_eq!( + line, + vec![ + None, + None, + Some(0.5), + Some(0.5), + None, + Some(0.5), + Some(0.5), + Some(0.5), + ] + ); + assert_eq!( + signal, + vec![ + None, + None, + None, + Some(0.5), + None, + Some(0.5), + Some(0.5), + Some(0.5), + ] + ); + assert_eq!(signal, ema(&line, 2)); + assert_eq!( + histogram, + vec![ + None, + None, + None, + Some(0.0), + None, + Some(0.0), + Some(0.0), + Some(0.0), + ] + ); + } } diff --git a/core/src/indicators/massi.rs b/core/src/indicators/massi.rs index 6f5cfbf..65e0a34 100644 --- a/core/src/indicators/massi.rs +++ b/core/src/indicators/massi.rs @@ -1,63 +1,67 @@ -use crate::indicators::ema::{ema_aligned, ema_dense}; +use crate::indicators::ema::ema; +use crate::utils::rolling_sum_strict; pub fn massi( - highs: &[f64], - lows: &[f64], + highs: &[Option], + lows: &[Option], period_ema: usize, period_sum: usize, period_signal: usize, ) -> (Vec>, Vec>) { let mass = massi_line(highs, lows, period_ema, period_sum); - let signal = ema_aligned(&mass, period_signal); + let signal = ema(&mass, period_signal); (mass, signal) } pub fn massi_signal( - highs: &[f64], - lows: &[f64], + highs: &[Option], + lows: &[Option], period_ema: usize, period_sum: usize, period_signal: usize, ) -> Vec> { let mass = massi_line(highs, lows, period_ema, period_sum); - ema_aligned(&mass, period_signal) + ema(&mass, period_signal) } pub fn massi_line( - highs: &[f64], - lows: &[f64], + highs: &[Option], + lows: &[Option], period_ema: usize, period_sum: usize, ) -> Vec> { let len = highs.len(); let mut mass = vec![None; len]; - if len != lows.len() || len < 2 * (period_ema - 1) + (period_sum - 1) + 1 { + if len != lows.len() + || period_ema == 0 + || period_sum == 0 + || len < 2 * (period_ema - 1) + (period_sum - 1) + 1 + { return mass; } - let high_low_diffs: Vec = highs.iter().zip(lows.iter()).map(|(h, l)| h - l).collect(); - let s_ema = ema_dense(&high_low_diffs, period_ema); - let offset: usize = period_ema - 1; - let d_ema = ema_aligned(&s_ema, period_ema); - - let mut ema_ratio = Vec::with_capacity(len.saturating_sub(2 * offset)); - for i in 0..len { - if let (Some(s), Some(d)) = (s_ema[i], d_ema[i]) { - ema_ratio.push(s / d); - } - } - - let mut ratio_sum = 0.0; - for i in 0..ema_ratio.len() { - ratio_sum += ema_ratio[i]; - if i >= period_sum - 1 { - mass[i + 2 * offset] = Some(ratio_sum); - ratio_sum -= ema_ratio[i - (period_sum - 1)]; - } - } + let high_low_diffs = highs + .iter() + .zip(lows.iter()) + .map(|(high, low)| match (high, low) { + (Some(high), Some(low)) => Some(high - low), + _ => None, + }) + .collect::>(); + let s_ema = ema(&high_low_diffs, period_ema); + let d_ema = ema(&s_ema, period_ema); + let ema_ratio = s_ema + .iter() + .zip(d_ema.iter()) + .map(|(&single, &double)| match (single, double) { + (Some(single), Some(double)) if double != 0.0 => Some(single / double), + _ => None, + }) + .collect::>(); + mass = rolling_sum_strict(&ema_ratio, period_sum); mass } @@ -77,6 +81,8 @@ mod tests { for symbol in test_cases { let highs = testutils::load_data(&format!("../data/{}.json", symbol), "h"); let lows = testutils::load_data(&format!("../data/{}.json", symbol), "l"); + let highs = highs.into_iter().map(Some).collect::>(); + let lows = lows.into_iter().map(Some).collect::>(); let (mass, signal) = massi(&highs, &lows, 9, 25, 9); @@ -104,4 +110,95 @@ mod tests { ); } } + + #[test] + fn test_massi_with_gap_invalidates_ratio_window() { + let highs = vec![ + Some(5.0), + Some(6.0), + Some(7.0), + None, + Some(8.0), + Some(9.0), + Some(10.0), + ]; + let lows = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + None, + Some(4.0), + Some(5.0), + Some(6.0), + ]; + + let line = massi_line(&highs, &lows, 2, 2); + + assert_eq!( + line, + vec![None, None, None, None, None, Some(2.0), Some(2.0)] + ); + } + + #[test] + fn test_massi_signal_follows_base_ema_contract_across_gaps() { + let highs = vec![ + Some(5.0), + Some(6.0), + Some(7.0), + Some(8.0), + Some(9.0), + None, + Some(10.0), + Some(11.0), + Some(12.0), + Some(13.0), + ]; + let lows = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + Some(4.0), + Some(5.0), + None, + Some(6.0), + Some(7.0), + Some(8.0), + Some(9.0), + ]; + + let (line, signal) = massi(&highs, &lows, 2, 2, 2); + + assert_eq!( + line, + vec![ + None, + None, + None, + Some(2.0), + Some(2.0), + None, + None, + Some(2.0), + Some(2.0), + Some(2.0), + ] + ); + assert_eq!( + signal, + vec![ + None, + None, + None, + None, + Some(2.0), + None, + None, + Some(2.0), + Some(2.0), + Some(2.0), + ] + ); + assert_eq!(signal, ema(&line, 2)); + } } diff --git a/core/src/indicators/nvi.rs b/core/src/indicators/nvi.rs index 3ccecf9..56823bc 100644 --- a/core/src/indicators/nvi.rs +++ b/core/src/indicators/nvi.rs @@ -1,4 +1,4 @@ -use crate::indicators::ema::ema_aligned; +use crate::indicators::ema::ema; pub fn nvi( closes: &[Option], @@ -6,7 +6,7 @@ pub fn nvi( signal_period: usize, ) -> (Vec>, Vec>) { let nvi_line = nvi_line(closes, volumes); - let signal = ema_aligned(&nvi_line, signal_period); + let signal = ema(&nvi_line, signal_period); (nvi_line, signal) } @@ -17,7 +17,7 @@ pub fn nvi_signal( signal_period: usize, ) -> Vec> { let nvi_line = nvi_line(closes, volumes); - ema_aligned(&nvi_line, signal_period) + ema(&nvi_line, signal_period) } pub fn nvi_line(closes: &[Option], volumes: &[Option]) -> Vec> { @@ -151,7 +151,7 @@ mod tests { Some(1090.0), ] ); - assert_eq!(signal, ema_aligned(&line, 2)); + assert_eq!(signal, ema(&line, 2)); assert_eq!( round_vec(signal, 8), round_vec( diff --git a/core/src/indicators/obv.rs b/core/src/indicators/obv.rs index 7b4324d..31944c8 100644 --- a/core/src/indicators/obv.rs +++ b/core/src/indicators/obv.rs @@ -1,4 +1,4 @@ -use crate::indicators::ema::ema_aligned; +use crate::indicators::ema::ema; pub fn obv( data: &[Option], @@ -6,7 +6,7 @@ pub fn obv( signal_period: usize, ) -> (Vec>, Vec>) { let obv_line = obv_line(data, volumes); - let obv_signal = ema_aligned(&obv_line, signal_period); + let obv_signal = ema(&obv_line, signal_period); (obv_line, obv_signal) } @@ -17,7 +17,7 @@ pub fn obv_signal( signal_period: usize, ) -> Vec> { let obv_line = obv_line(data, volumes); - ema_aligned(&obv_line, signal_period) + ema(&obv_line, signal_period) } pub fn obv_line(data: &[Option], volumes: &[Option]) -> Vec> { @@ -154,7 +154,7 @@ mod tests { Some(140.0) ] ); - assert_eq!(signal, ema_aligned(&line, 2)); + assert_eq!(signal, ema(&line, 2)); assert_eq!( round_vec(signal, 8), round_vec( diff --git a/core/src/indicators/ppo.rs b/core/src/indicators/ppo.rs index 63ea40d..ed0e6ed 100644 --- a/core/src/indicators/ppo.rs +++ b/core/src/indicators/ppo.rs @@ -1,13 +1,13 @@ -use crate::indicators::ema::{ema_aligned, ema_dense}; +use crate::indicators::ema::ema; pub fn ppo( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> (Vec>, Vec>, Vec>) { let ppo_line = ppo_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&ppo_line, signal_period); + let signal_line = ema(&ppo_line, signal_period); let histogram = ppo_line .iter() .zip(signal_line.iter()) @@ -21,13 +21,13 @@ pub fn ppo( } pub fn ppo_histogram( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let ppo_line = ppo_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&ppo_line, signal_period); + let signal_line = ema(&ppo_line, signal_period); ppo_line .iter() .zip(signal_line.iter()) @@ -39,24 +39,24 @@ pub fn ppo_histogram( } pub fn ppo_signal( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let ppo_line = ppo_line(data, fast_period, slow_period); - ema_aligned(&ppo_line, signal_period) + ema(&ppo_line, signal_period) } -pub fn ppo_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec> { +pub fn ppo_line(data: &[Option], fast_period: usize, slow_period: usize) -> Vec> { let mut ppo_line = vec![None; data.len()]; if data.len() < slow_period || fast_period >= slow_period { return ppo_line; } - let fast_ema = ema_dense(data, fast_period); - let slow_ema = ema_dense(data, slow_period); + let fast_ema = ema(data, fast_period); + let slow_ema = ema(data, slow_period); for i in (slow_period - 1)..data.len() { if let (Some(fast), Some(slow)) = (fast_ema[i], slow_ema[i]) { @@ -83,7 +83,10 @@ mod tests { // When for symbol in test_cases { - let input = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let input = testutils::load_data(&format!("../data/{}.json", symbol), "c") + .into_iter() + .map(Some) + .collect::>(); let (ppo_line, signal_line, histogram) = ppo(&input, 12, 26, 9); let expected_ppo = testutils::load_expected::>(&format!( @@ -120,4 +123,64 @@ mod tests { ); } } + + #[test] + fn test_ppo_signal_follows_base_ema_contract_across_gaps() { + let input = vec![ + Some(10.0), + Some(11.0), + Some(12.0), + Some(13.0), + None, + Some(14.0), + Some(15.0), + Some(16.0), + ]; + + let (line, signal, histogram) = ppo(&input, 2, 3, 2); + + assert!(line[3].is_some()); + assert_eq!(line[4], None); + assert!(line[5].is_some()); + assert!(line[6].is_some()); + + assert!(signal[3].is_some()); + assert_eq!(signal[4], None); + assert_eq!(signal, ema(&line, 2)); + assert_eq!( + round_vec(signal.clone(), 8), + round_vec( + vec![ + None, + None, + None, + Some(4.356060606060606), + None, + Some(4.016122766122766), + Some(3.7196599696599697), + Some(3.4621088787755454), + ], + 8, + ) + ); + + assert!(histogram[3].is_some()); + assert_eq!(histogram[4], None); + assert_eq!( + round_vec(histogram, 8), + round_vec( + vec![ + None, + None, + None, + Some(-0.189393939393939), + None, + Some(-0.16996891996891972), + Some(-0.14823139823139803), + Some(-0.12877554544221196), + ], + 8, + ) + ); + } } diff --git a/core/src/indicators/pvi.rs b/core/src/indicators/pvi.rs index b9506d3..09f1822 100644 --- a/core/src/indicators/pvi.rs +++ b/core/src/indicators/pvi.rs @@ -1,4 +1,4 @@ -use crate::indicators::ema::ema_aligned; +use crate::indicators::ema::ema; pub fn pvi( closes: &[Option], @@ -6,7 +6,7 @@ pub fn pvi( signal_period: usize, ) -> (Vec>, Vec>) { let pvi_line = pvi_line(closes, volumes); - let signal = ema_aligned(&pvi_line, signal_period); + let signal = ema(&pvi_line, signal_period); (pvi_line, signal) } @@ -17,7 +17,7 @@ pub fn pvi_signal( signal_period: usize, ) -> Vec> { let pvi_line = pvi_line(closes, volumes); - ema_aligned(&pvi_line, signal_period) + ema(&pvi_line, signal_period) } pub fn pvi_line(closes: &[Option], volumes: &[Option]) -> Vec> { @@ -157,7 +157,7 @@ mod tests { Some(1090.0), ] ); - assert_eq!(signal, ema_aligned(&line, 2)); + assert_eq!(signal, ema(&line, 2)); assert_eq!( round_vec(signal, 8), round_vec( diff --git a/core/src/indicators/pvo.rs b/core/src/indicators/pvo.rs index c9ae630..b0d6c23 100644 --- a/core/src/indicators/pvo.rs +++ b/core/src/indicators/pvo.rs @@ -1,13 +1,13 @@ -use crate::indicators::ema::{ema_aligned, ema_dense}; +use crate::indicators::ema::ema; pub fn pvo( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> (Vec>, Vec>, Vec>) { let pvo_line = pvo_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&pvo_line, signal_period); + let signal_line = ema(&pvo_line, signal_period); let histogram = pvo_line .iter() .zip(signal_line.iter()) @@ -21,13 +21,13 @@ pub fn pvo( } pub fn pvo_histogram( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let pvo_line = pvo_line(data, fast_period, slow_period); - let signal_line = ema_aligned(&pvo_line, signal_period); + let signal_line = ema(&pvo_line, signal_period); pvo_line .iter() .zip(signal_line.iter()) @@ -39,24 +39,24 @@ pub fn pvo_histogram( } pub fn pvo_signal( - data: &[f64], + data: &[Option], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec> { let pvo_line = pvo_line(data, fast_period, slow_period); - ema_aligned(&pvo_line, signal_period) + ema(&pvo_line, signal_period) } -pub fn pvo_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec> { +pub fn pvo_line(data: &[Option], fast_period: usize, slow_period: usize) -> Vec> { let mut pvo_line = vec![None; data.len()]; if data.len() < slow_period || fast_period >= slow_period { return pvo_line; } - let fast_ema = ema_dense(data, fast_period); - let slow_ema = ema_dense(data, slow_period); + let fast_ema = ema(data, fast_period); + let slow_ema = ema(data, slow_period); for i in (slow_period - 1)..data.len() { if let (Some(fast), Some(slow)) = (fast_ema[i], slow_ema[i]) { @@ -83,7 +83,10 @@ mod tests { // When for symbol in test_cases { - let input = testutils::load_data(&format!("../data/{}.json", symbol), "v"); + let input = testutils::load_data(&format!("../data/{}.json", symbol), "v") + .into_iter() + .map(Some) + .collect::>(); let (pvo_line, signal_line, histogram) = pvo(&input, 12, 26, 9); let expected_pvo = testutils::load_expected::>(&format!( @@ -120,4 +123,64 @@ mod tests { ); } } + + #[test] + fn test_pvo_signal_follows_base_ema_contract_across_gaps() { + let input = vec![ + Some(10.0), + Some(11.0), + Some(12.0), + Some(13.0), + None, + Some(14.0), + Some(15.0), + Some(16.0), + ]; + + let (line, signal, histogram) = pvo(&input, 2, 3, 2); + + assert!(line[3].is_some()); + assert_eq!(line[4], None); + assert!(line[5].is_some()); + assert!(line[6].is_some()); + + assert!(signal[3].is_some()); + assert_eq!(signal[4], None); + assert_eq!(signal, ema(&line, 2)); + assert_eq!( + round_vec(signal.clone(), 8), + round_vec( + vec![ + None, + None, + None, + Some(4.356060606060606), + None, + Some(4.016122766122766), + Some(3.7196599696599697), + Some(3.4621088787755454), + ], + 8, + ) + ); + + assert!(histogram[3].is_some()); + assert_eq!(histogram[4], None); + assert_eq!( + round_vec(histogram, 8), + round_vec( + vec![ + None, + None, + None, + Some(-0.189393939393939), + None, + Some(-0.16996891996891972), + Some(-0.14823139823139803), + Some(-0.12877554544221196), + ], + 8, + ) + ); + } } diff --git a/core/src/indicators/sma.rs b/core/src/indicators/sma.rs index 519ae1e..f32ffb9 100644 --- a/core/src/indicators/sma.rs +++ b/core/src/indicators/sma.rs @@ -1,5 +1,4 @@ use crate::utils::rolling_mean_strict; - /// Computes a simple moving average over an aligned nullable series. /// /// The returned vector keeps the same length as the input and emits `None` @@ -8,8 +7,6 @@ pub fn sma(data: &[Option], period: usize) -> Vec> { rolling_mean_strict(data, period) } -pub(crate) use sma as sma_aligned; - #[cfg(test)] mod tests { use super::*; diff --git a/core/src/indicators/sonar.rs b/core/src/indicators/sonar.rs index e462d44..76378ac 100644 --- a/core/src/indicators/sonar.rs +++ b/core/src/indicators/sonar.rs @@ -1,35 +1,35 @@ -use crate::indicators::ema::{ema_aligned, ema_dense}; +use crate::indicators::ema::ema; pub fn sonar( - data: &[f64], + data: &[Option], period: usize, step: usize, signal_period: usize, ) -> (Vec>, Vec>) { let sonar_line = sonar_line(data, period, step); - let signal_line = ema_aligned(&sonar_line, signal_period); + let signal_line = ema(&sonar_line, signal_period); (sonar_line, signal_line) } pub fn sonar_signal( - data: &[f64], + data: &[Option], period: usize, step: usize, signal_period: usize, ) -> Vec> { let sonar_line = sonar_line(data, period, step); - ema_aligned(&sonar_line, signal_period) + ema(&sonar_line, signal_period) } -pub fn sonar_line(data: &[f64], period: usize, step: usize) -> Vec> { +pub fn sonar_line(data: &[Option], period: usize, step: usize) -> Vec> { let mut sonar_line = vec![None; data.len()]; if data.len() < period + step { return sonar_line; } - let ema_values = ema_dense(data, period); + let ema_values = ema(data, period); for i in (period + step - 1)..data.len() { if let (Some(current_ema), Some(previous_ema)) = (ema_values[i], ema_values[i - step]) { @@ -54,7 +54,10 @@ mod tests { // When for symbol in test_cases { - let input = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let input = testutils::load_data(&format!("../data/{}.json", symbol), "c") + .into_iter() + .map(Some) + .collect::>(); let (sonar_line, signal_line) = sonar(&input, 9, 6, 5); let expected_sonar = testutils::load_expected::>(&format!( @@ -81,4 +84,68 @@ mod tests { ); } } + + #[test] + fn test_sonar_with_gap_requires_valid_current_and_stepped_prior_state() { + let input = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + None, + Some(4.0), + Some(5.0), + Some(6.0), + ]; + + let line = sonar_line(&input, 2, 2); + + assert_eq!( + line, + vec![None, None, None, None, Some(1.0), None, Some(2.0)] + ); + } + + #[test] + fn test_sonar_signal_follows_base_ema_contract_across_gaps() { + let input = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + Some(4.0), + None, + Some(5.0), + Some(6.0), + Some(7.0), + ]; + + let (line, signal) = sonar(&input, 2, 1, 2); + + assert_eq!( + line, + vec![ + None, + None, + Some(1.0), + Some(1.0), + None, + None, + Some(1.0), + Some(1.0), + ] + ); + assert_eq!( + signal, + vec![ + None, + None, + None, + Some(1.0), + None, + None, + Some(1.0), + Some(1.0), + ] + ); + assert_eq!(signal, ema(&line, 2)); + } }