Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions core/src/indicators/cv.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use crate::indicators::ema::ema_dense;
use crate::indicators::ema::ema;

pub fn cv(highs: &[f64], lows: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn cv(highs: &[Option<f64>], lows: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
let mut cv = vec![None; highs.len()];
let len = highs.len();

if len != lows.len() || len < period * 2 || period <= 1 {
return cv;
}

let high_low_diffs: Vec<f64> = 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::<Vec<_>>();
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)) =
Expand All @@ -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::<Option<f64>>(&format!(
"../data/expected/cv_{}.json",
Expand All @@ -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)
);
}
}
5 changes: 0 additions & 5 deletions core/src/indicators/ema.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
pub(crate) fn ema_dense(data: &[f64], period: usize) -> Vec<Option<f64>> {
let nullable = data.iter().copied().map(Some).collect::<Vec<_>>();
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`
Expand Down
38 changes: 30 additions & 8 deletions core/src/indicators/erbear.rs
Original file line number Diff line number Diff line change
@@ -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<Option<f64>> {
pub fn erbear(lows: &[Option<f64>], closes: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
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);
}
}
Expand All @@ -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::<Option<f64>>(&format!(
"../data/expected/erbear_{}.json",
Expand All @@ -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
)
);
}
}
38 changes: 30 additions & 8 deletions core/src/indicators/erbull.rs
Original file line number Diff line number Diff line change
@@ -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<Option<f64>> {
pub fn erbull(highs: &[Option<f64>], closes: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
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);
}
}
Expand All @@ -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::<Option<f64>>(&format!(
"../data/expected/erbull_{}.json",
Expand All @@ -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
)
);
}
}
5 changes: 0 additions & 5 deletions core/src/indicators/rsi.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
pub(crate) fn rsi_dense(data: &[f64], period: usize) -> Vec<Option<f64>> {
let nullable = data.iter().copied().map(Some).collect::<Vec<_>>();
rsi(&nullable, period)
}

pub fn rsi(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
let mut rsi = vec![None; data.len()];

Expand Down
50 changes: 46 additions & 4 deletions core/src/indicators/stochrsi.rs
Original file line number Diff line number Diff line change
@@ -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<f64>],
period_rsi: usize,
period_k: usize,
period_d: usize,
Expand All @@ -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<f64> = rsi_values
.iter()
.map(|value| value.unwrap_or(f64::NAN))
Expand Down Expand Up @@ -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);

Expand All @@ -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
)
);
}
}