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
23 changes: 19 additions & 4 deletions core/src/indicators/aroon.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use crate::utils::rolling_argmax_argmin;

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

if highs.len() < period {
if highs.len() != lows.len() || period == 0 || highs.len() < period + 1 {
return (aroon_up, aroon_down);
}

Expand Down Expand Up @@ -39,8 +43,8 @@ mod tests {
fn test_aroon() {
let test_cases = vec!["005930", "TSLA"];
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 = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h");
let lows = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l");
let (aroon_up, aroon_down) = aroon(&highs, &lows, 25);

let expected_up = testutils::load_expected::<Option<f64>>(&format!(
Expand All @@ -66,4 +70,15 @@ mod tests {
);
}
}

#[test]
fn test_aroon_gap_invalidates_full_extrema_window() {
let highs = vec![Some(1.0), Some(5.0), None, Some(4.0), Some(3.0), Some(6.0)];
let lows = vec![Some(6.0), Some(2.0), None, Some(3.0), Some(1.0), Some(2.0)];

let (up, down) = aroon(&highs, &lows, 2);

assert_eq!(up, vec![None, None, None, None, None, Some(100.0)]);
assert_eq!(down, vec![None, None, None, None, None, Some(50.0)]);
}
}
8 changes: 4 additions & 4 deletions core/src/indicators/aroonosc.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::indicators::aroon;

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

if highs.len() < period {
if highs.len() != lows.len() || period == 0 || highs.len() < period + 1 {
return aroonosc;
}

Expand All @@ -28,8 +28,8 @@ mod tests {
fn test_aroonosc() {
let test_cases = vec!["005930", "TSLA"];
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 = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h");
let lows = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l");
let result = aroonosc(&highs, &lows, 25);

let expected = testutils::load_expected::<Option<f64>>(&format!(
Expand Down
97 changes: 76 additions & 21 deletions core/src/indicators/cci.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
pub fn cci(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn cci(
highs: &[Option<f64>],
lows: &[Option<f64>],
closes: &[Option<f64>],
period: usize,
) -> Vec<Option<f64>> {
let len = highs.len();
let mut result = vec![None; len];

if len != lows.len() || len != closes.len() || len < period || period <= 1 {
return result;
}

let typical_prices: Vec<f64> = highs
.iter()
.zip(lows.iter())
.zip(closes.iter())
.map(|((h, l), c)| (h + l + c) / 3.0)
.collect();

for i in period - 1..len {
let slice = &typical_prices[i + 1 - period..=i];
let sma_tp: f64 = slice.iter().sum::<f64>() / period as f64;
let mean_deviation = slice.iter().map(|&x| (x - sma_tp).abs()).sum::<f64>() / period as f64;

result[i] = if mean_deviation == 0.0 {
None
} else {
Some((typical_prices[i] - sma_tp) / (0.015 * mean_deviation))
};
let mut typical_prices = Vec::with_capacity(len);
let mut valid_typical_prices = Vec::with_capacity(len);
for i in 0..len {
match (highs[i], lows[i], closes[i]) {
(Some(high), Some(low), Some(close)) => {
typical_prices.push((high + low + close) / 3.0);
valid_typical_prices.push(true);
}
_ => {
typical_prices.push(0.0);
valid_typical_prices.push(false);
}
}
}

let mut sum = 0.0;
let mut valid_count = 0usize;

for i in 0..len {
if valid_typical_prices[i] {
sum += typical_prices[i];
valid_count += 1;
}

if i >= period && valid_typical_prices[i - period] {
sum -= typical_prices[i - period];
valid_count -= 1;
}

if i < period - 1 || valid_count != period {
continue;
}

let sma_tp = sum / period as f64;
let window = &typical_prices[i + 1 - period..=i];
let mean_deviation = window
.iter()
.map(|&value| (value - sma_tp).abs())
.sum::<f64>()
/ period as f64;

if mean_deviation != 0.0 {
result[i] = Some((typical_prices[i] - sma_tp) / (0.015 * mean_deviation));
}
}

result
Expand All @@ -38,9 +70,9 @@ mod tests {
fn test_cci() {
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 close = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let high = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h");
let low = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l");
let close = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c");
let result = cci(&high, &low, &close, 20);
let expected = testutils::load_expected::<Option<f64>>(&format!(
"../data/expected/cci_{}.json",
Expand All @@ -55,4 +87,27 @@ mod tests {
);
}
}

#[test]
fn test_cci_gap_invalidates_full_typical_price_window() {
let highs = vec![Some(4.0), Some(6.0), None, Some(10.0), Some(12.0)];
let lows = vec![Some(2.0), Some(2.0), None, Some(6.0), Some(8.0)];
let closes = vec![Some(3.0), Some(4.0), None, Some(9.0), Some(11.0)];

let result = round_vec(cci(&highs, &lows, &closes, 2), 8);

assert_eq!(
result,
round_vec(
vec![
None,
Some(66.66666666666667),
None,
None,
Some(66.66666666666667)
],
8
)
);
}
}
71 changes: 56 additions & 15 deletions core/src/indicators/ichimoku.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,40 @@ fn leading_span_a_from_lines(
forward_shift(span, base_line_period)
}

pub fn ichimoku_conversion_line(highs: &[f64], lows: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn ichimoku_conversion_line(
highs: &[Option<f64>],
lows: &[Option<f64>],
period: usize,
) -> Vec<Option<f64>> {
rolling_midpoint(highs, lows, period)
}

pub fn ichimoku_base_line(highs: &[f64], lows: &[f64], period: usize) -> Vec<Option<f64>> {
pub fn ichimoku_base_line(
highs: &[Option<f64>],
lows: &[Option<f64>],
period: usize,
) -> Vec<Option<f64>> {
rolling_midpoint(highs, lows, period)
}

pub fn ichimoku_lagging_span(closes: &[f64], base_line_period: usize) -> Vec<Option<f64>> {
pub fn ichimoku_lagging_span(closes: &[Option<f64>], base_line_period: usize) -> Vec<Option<f64>> {
let len = closes.len();
let mut lagging_span = vec![None; len];

if len < base_line_period {
if base_line_period == 0 || len < base_line_period {
return lagging_span;
}

for i in (base_line_period - 1)..len {
lagging_span[i + 1 - base_line_period] = Some(closes[i]);
lagging_span[i + 1 - base_line_period] = closes[i];
}

lagging_span
}

pub fn ichimoku_leading_span_a(
highs: &[f64],
lows: &[f64],
highs: &[Option<f64>],
lows: &[Option<f64>],
conversion_line_period: usize,
base_line_period: usize,
) -> Vec<Option<f64>> {
Expand All @@ -52,18 +60,18 @@ pub fn ichimoku_leading_span_a(
}

pub fn ichimoku_leading_span_b(
highs: &[f64],
lows: &[f64],
highs: &[Option<f64>],
lows: &[Option<f64>],
period: usize,
base_line_period: usize,
) -> Vec<Option<f64>> {
forward_shift(rolling_midpoint(highs, lows, period), base_line_period)
}

pub fn ichimoku(
highs: &[f64],
lows: &[f64],
closes: &[f64],
highs: &[Option<f64>],
lows: &[Option<f64>],
closes: &[Option<f64>],
conversion_line_period: usize,
base_line_period: usize,
leading_span_b_period: usize,
Expand All @@ -74,6 +82,17 @@ pub fn ichimoku(
Vec<Option<f64>>, // Leading span A
Vec<Option<f64>>, // Leading span B
) {
let len = highs.len();
if len != lows.len() || len != closes.len() {
return (
vec![None; len],
vec![None; len],
vec![None; len],
vec![None; len],
vec![None; len],
);
}

let conversion_line = ichimoku_conversion_line(highs, lows, conversion_line_period);
let base_line = ichimoku_base_line(highs, lows, base_line_period);
let lagging_span = ichimoku_lagging_span(closes, base_line_period);
Expand All @@ -99,9 +118,9 @@ mod tests {
fn test_ichimoku() {
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 close = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let high = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "h");
let low = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "l");
let close = testutils::load_data_nullable(&format!("../data/{}.json", symbol), "c");

let (conversion_line, base_line, lagging_span, leading_span_a, leading_span_b) =
ichimoku(&high, &low, &close, 9, 26, 52);
Expand Down Expand Up @@ -159,4 +178,26 @@ mod tests {
);
}
}

#[test]
fn test_ichimoku_gap_invalidates_extrema_windows_and_preserves_lagging_alignment() {
let highs = vec![Some(5.0), Some(7.0), None, Some(10.0), Some(12.0)];
let lows = vec![Some(1.0), Some(3.0), None, Some(6.0), Some(8.0)];
let closes = vec![Some(4.0), Some(5.0), None, Some(8.0), Some(11.0)];

let (conversion, base, lagging, leading_a, leading_b) =
ichimoku(&highs, &lows, &closes, 2, 2, 2);

assert_eq!(conversion, vec![None, Some(4.0), None, None, Some(9.0)]);
assert_eq!(base, vec![None, Some(4.0), None, None, Some(9.0)]);
assert_eq!(lagging, vec![Some(5.0), None, Some(8.0), Some(11.0), None]);
assert_eq!(
leading_a,
vec![None, None, Some(4.0), None, None, Some(9.0)]
);
assert_eq!(
leading_b,
vec![None, None, Some(4.0), None, None, Some(9.0)]
);
}
}
Loading