From 852c2324ec8b315637294310a3783277edc6c38b Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Fri, 24 Apr 2026 14:13:46 +0900 Subject: [PATCH] Remove dense EMA and RSI adapters --- core/src/indicators/cv.rs | 32 +++++++++++++++++---- core/src/indicators/ema.rs | 5 ---- core/src/indicators/erbear.rs | 38 +++++++++++++++++++------ core/src/indicators/erbull.rs | 38 +++++++++++++++++++------ core/src/indicators/rsi.rs | 5 ---- core/src/indicators/stochrsi.rs | 50 ++++++++++++++++++++++++++++++--- 6 files changed, 132 insertions(+), 36 deletions(-) diff --git a/core/src/indicators/cv.rs b/core/src/indicators/cv.rs index a06fd25..22fe11b 100644 --- a/core/src/indicators/cv.rs +++ b/core/src/indicators/cv.rs @@ -1,6 +1,6 @@ -use crate::indicators::ema::ema_dense; +use crate::indicators::ema::ema; -pub fn cv(highs: &[f64], lows: &[f64], period: usize) -> Vec> { +pub fn cv(highs: &[Option], lows: &[Option], period: usize) -> Vec> { let mut cv = vec![None; highs.len()]; let len = highs.len(); @@ -8,8 +8,15 @@ pub fn cv(highs: &[f64], lows: &[f64], period: usize) -> Vec> { return cv; } - let high_low_diffs: Vec = highs.iter().zip(lows.iter()).map(|(h, l)| h - l).collect(); - let ema_high_low_diffs = ema_dense(&high_low_diffs, period); + 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 ema_high_low_diffs = ema(&high_low_diffs, period); for i in period * 2 - 1..len { if let (Some(current_ema), Some(previous_ema)) = @@ -33,8 +40,8 @@ mod tests { fn test_cv() { let test_cases = vec!["005930", "TSLA"]; for symbol in test_cases { - let high = testutils::load_data(&format!("../data/{}.json", symbol), "h"); - let low = testutils::load_data(&format!("../data/{}.json", symbol), "l"); + let high = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h"); + let low = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l"); let result = cv(&high, &low, 10); let expected = testutils::load_expected::>(&format!( "../data/expected/cv_{}.json", @@ -49,4 +56,17 @@ mod tests { ); } } + + #[test] + fn test_cv_gap_invalidates_until_shifted_ema_is_available() { + let highs = vec![Some(3.0), Some(5.0), None, Some(9.0), Some(11.0)]; + let lows = vec![Some(1.0), Some(1.0), None, Some(3.0), Some(5.0)]; + + let result = round_vec(cv(&highs, &lows, 2), 8); + + assert_eq!( + result, + round_vec(vec![None, None, None, Some(66.66666666666667), None], 8) + ); + } } diff --git a/core/src/indicators/ema.rs b/core/src/indicators/ema.rs index 875a81d..517d3e3 100644 --- a/core/src/indicators/ema.rs +++ b/core/src/indicators/ema.rs @@ -1,8 +1,3 @@ -pub(crate) fn ema_dense(data: &[f64], period: usize) -> Vec> { - let nullable = data.iter().copied().map(Some).collect::>(); - ema(&nullable, period) -} - /// Computes an exponential moving average over an aligned nullable series. /// /// The returned vector keeps the same length as the input and emits `None` diff --git a/core/src/indicators/erbear.rs b/core/src/indicators/erbear.rs index f5ef92a..ed426be 100644 --- a/core/src/indicators/erbear.rs +++ b/core/src/indicators/erbear.rs @@ -1,17 +1,17 @@ -use crate::indicators::ema::ema_dense; +use crate::indicators::ema::ema; -pub fn erbear(lows: &[f64], closes: &[f64], period: usize) -> Vec> { +pub fn erbear(lows: &[Option], closes: &[Option], period: usize) -> Vec> { let mut erbear = vec![None; lows.len()]; - if lows.len() < period { + if lows.len() != closes.len() || lows.len() < period || period == 0 { return erbear; } - let ema_values = ema_dense(closes, period); + let ema_values = ema(closes, period); for i in (period - 1)..lows.len() { - if let Some(ema_value) = ema_values[i] { - let bear_power = lows[i] - ema_value; + if let (Some(low), Some(ema_value)) = (lows[i], ema_values[i]) { + let bear_power = low - ema_value; erbear[i] = Some(bear_power); } } @@ -29,8 +29,8 @@ mod tests { fn test_erbear() { let test_cases = vec!["005930", "TSLA"]; for symbol in test_cases { - let lows = testutils::load_data(&format!("../data/{}.json", symbol), "l"); - let closes = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let lows = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l"); + let closes = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c"); let result = erbear(&lows, &closes, 13); let expected = testutils::load_expected::>(&format!( "../data/expected/erbear_{}.json", @@ -45,4 +45,26 @@ mod tests { ); } } + + #[test] + fn test_erbear_gap_propagates_low_nulls_and_resumes_ema_state() { + let lows = vec![Some(0.0), Some(1.0), None, Some(3.0), Some(4.0)]; + let closes = vec![Some(1.0), Some(2.0), None, Some(4.0), Some(5.0)]; + + let result = round_vec(erbear(&lows, &closes, 2), 8); + + assert_eq!( + result, + round_vec( + vec![ + None, + Some(-0.5), + None, + Some(-0.16666666666666652), + Some(-0.3888888888888884), + ], + 8 + ) + ); + } } diff --git a/core/src/indicators/erbull.rs b/core/src/indicators/erbull.rs index cda1125..9cf5cc7 100644 --- a/core/src/indicators/erbull.rs +++ b/core/src/indicators/erbull.rs @@ -1,17 +1,17 @@ -use crate::indicators::ema::ema_dense; +use crate::indicators::ema::ema; -pub fn erbull(highs: &[f64], closes: &[f64], period: usize) -> Vec> { +pub fn erbull(highs: &[Option], closes: &[Option], period: usize) -> Vec> { let mut erbull = vec![None; highs.len()]; - if highs.len() < period { + if highs.len() != closes.len() || highs.len() < period || period == 0 { return erbull; } - let ema_values = ema_dense(closes, period); + let ema_values = ema(closes, period); for i in (period - 1)..highs.len() { - if let Some(ema_value) = ema_values[i] { - let bull_power = highs[i] - ema_value; + if let (Some(high), Some(ema_value)) = (highs[i], ema_values[i]) { + let bull_power = high - ema_value; erbull[i] = Some(bull_power); } } @@ -29,8 +29,8 @@ mod tests { fn test_erbull() { let test_cases = vec!["005930", "TSLA"]; for symbol in test_cases { - let highs = testutils::load_data(&format!("../data/{}.json", symbol), "h"); - let closes = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let highs = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h"); + let closes = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c"); let result = erbull(&highs, &closes, 13); let expected = testutils::load_expected::>(&format!( "../data/expected/erbull_{}.json", @@ -45,4 +45,26 @@ mod tests { ); } } + + #[test] + fn test_erbull_gap_propagates_high_nulls_and_resumes_ema_state() { + let highs = vec![Some(3.0), Some(4.0), None, Some(6.0), Some(7.0)]; + let closes = vec![Some(1.0), Some(2.0), None, Some(4.0), Some(5.0)]; + + let result = round_vec(erbull(&highs, &closes, 2), 8); + + assert_eq!( + result, + round_vec( + vec![ + None, + Some(2.5), + None, + Some(2.8333333333333335), + Some(2.6111111111111116), + ], + 8 + ) + ); + } } diff --git a/core/src/indicators/rsi.rs b/core/src/indicators/rsi.rs index 84a5ab6..d1ae07b 100644 --- a/core/src/indicators/rsi.rs +++ b/core/src/indicators/rsi.rs @@ -1,8 +1,3 @@ -pub(crate) fn rsi_dense(data: &[f64], period: usize) -> Vec> { - let nullable = data.iter().copied().map(Some).collect::>(); - rsi(&nullable, period) -} - pub fn rsi(data: &[Option], period: usize) -> Vec> { let mut rsi = vec![None; data.len()]; diff --git a/core/src/indicators/stochrsi.rs b/core/src/indicators/stochrsi.rs index 4fda02f..5008629 100644 --- a/core/src/indicators/stochrsi.rs +++ b/core/src/indicators/stochrsi.rs @@ -1,8 +1,8 @@ -use crate::indicators::rsi::rsi_dense; +use crate::indicators::rsi::rsi; use crate::utils::{rolling_max_min, rolling_mean_strict}; pub fn stochrsi( - closes: &[f64], + closes: &[Option], period_rsi: usize, period_k: usize, period_d: usize, @@ -14,7 +14,7 @@ pub fn stochrsi( return (percent_k, vec![None; len]); } - let rsi_values = rsi_dense(closes, period_rsi); + let rsi_values = rsi(closes, period_rsi); let rsi_values_with_nan: Vec = rsi_values .iter() .map(|value| value.unwrap_or(f64::NAN)) @@ -64,7 +64,7 @@ mod tests { fn test_stochrsi() { let test_cases = vec!["005930", "TSLA"]; for symbol in test_cases { - let closes = testutils::load_data(&format!("../data/{}.json", symbol), "c"); + let closes = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c"); let (percent_k, percent_d) = stochrsi(&closes, 14, 14, 3); @@ -91,4 +91,46 @@ mod tests { ); } } + + #[test] + fn test_stochrsi_gap_requires_full_valid_rsi_windows_to_resume() { + let closes = vec![ + Some(1.0), + Some(2.0), + Some(3.0), + Some(2.0), + None, + Some(4.0), + Some(5.0), + Some(6.0), + Some(7.0), + ]; + + let (percent_k, percent_d) = stochrsi(&closes, 3, 2, 2); + + assert_eq!( + round_vec(percent_k, 8), + round_vec( + vec![ + None, + None, + None, + None, + None, + None, + None, + Some(100.0), + Some(100.0) + ], + 8 + ) + ); + assert_eq!( + round_vec(percent_d, 8), + round_vec( + vec![None, None, None, None, None, None, None, None, Some(100.0)], + 8 + ) + ); + } }