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
77 changes: 77 additions & 0 deletions core/src/indicators/ema.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/// Computes an exponential moving average over a dense `f64` series.
///
/// The returned vector keeps the same length as the input and emits `None`
/// until the first full `period` window has been observed.
pub fn ema(data: &[f64], period: usize) -> Vec<Option<f64>> {
let mut result = vec![None; data.len()];

Expand All @@ -18,15 +22,59 @@ pub fn ema(data: &[f64], period: usize) -> Vec<Option<f64>> {
result
}

/// Computes an EMA over an optional series while preserving original alignment.
///
/// The EMA state advances only on `Some(f64)` values, so interior `None` holes
/// are skipped safely. This lets callers avoid compacting a sparse aligned
/// series into a temporary dense vector before applying `ema`.
pub(crate) fn ema_aligned(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
let mut result = vec![None; data.len()];
if period == 0 {
return result;
}

let alpha = 2.0 / (period as f64 + 1.0);
let mut seeded_count = 0usize;
let mut seed_sum = 0.0;
let mut ema = None;

for (idx, value) in data.iter().enumerate() {
let Some(value) = value else {
continue;
};

if let Some(current_ema) = ema {
let next_ema = alpha * value + (1.0 - alpha) * current_ema;
ema = Some(next_ema);
result[idx] = Some(next_ema);
continue;
}

seed_sum += value;
seeded_count += 1;
if seeded_count == period {
let initial_ema = seed_sum / period as f64;
ema = Some(initial_ema);
result[idx] = Some(initial_ema);
}
}

result
}

#[cfg(test)]
mod tests {
use super::*;
use crate::testutils;
use crate::utils::round_vec;

/// Verifies the standard EMA output against fixture data.
#[test]
fn test_ema() {
// Given
let test_cases = vec!["005930", "TSLA"];

// When
for symbol in test_cases {
let input = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let result = ema(&input, 20);
Expand All @@ -35,6 +83,7 @@ mod tests {
symbol
));

// Then
assert_eq!(
round_vec(result, 8),
round_vec(expected, 8),
Expand All @@ -43,4 +92,32 @@ mod tests {
);
}
}

/// Verifies that aligned EMA preserves offsets while matching dense EMA values.
#[test]
fn test_ema_aligned() {
// Given
let aligned = vec![None, None, Some(1.0), Some(2.0), Some(3.0), Some(4.0)];
let expected = vec![None, None, None, Some(1.5), Some(2.5), Some(3.5)];

// When
let result = ema_aligned(&aligned, 2);

// Then
assert_eq!(result, expected);
}

/// Verifies that aligned EMA skips interior gaps without panicking.
#[test]
fn test_ema_aligned_with_interior_gaps() {
// Given
let aligned = vec![None, None, Some(1.0), Some(2.0), None, Some(3.0), Some(4.0)];
let expected = vec![None, None, None, Some(1.5), None, Some(2.5), Some(3.5)];

// When
let result = ema_aligned(&aligned, 2);

// Then
assert_eq!(result, expected);
}
}
44 changes: 33 additions & 11 deletions core/src/indicators/eom.rs
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 작업이 적용되는 친구들 중 분리안되어있는 친구들은 다 분리해뒀습니다.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::indicators::sma::sma;
use crate::indicators::sma::{sma, sma_aligned};

pub fn eom(
highs: &[f64],
Expand All @@ -8,9 +8,34 @@ pub fn eom(
signal_period: usize,
scale: f64,
) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
let eom_line = eom_line(highs, lows, volumes, period, scale);
let signal = sma_aligned(&eom_line, signal_period);

(eom_line, signal)
}

pub fn eom_signal(
highs: &[f64],
lows: &[f64],
volumes: &[f64],
period: usize,
signal_period: usize,
scale: f64,
) -> Vec<Option<f64>> {
let eom_line = eom_line(highs, lows, volumes, period, scale);
sma_aligned(&eom_line, signal_period)
}

pub fn eom_line(
highs: &[f64],
lows: &[f64],
volumes: &[f64],
period: usize,
scale: f64,
) -> Vec<Option<f64>> {
let len = highs.len();
if len < 2 || len != lows.len() || len != volumes.len() {
return (vec![None; len], vec![None; len]);
return vec![None; len];
}

let mut eom_values = Vec::with_capacity(len - 1);
Expand All @@ -35,15 +60,7 @@ pub fn eom(
eom_line[i + 1] = value;
}

let eom_values: Vec<f64> = eom_line.iter().filter_map(|&x| x).collect();
let signal_sma = sma(&eom_values, signal_period);
let mut signal = vec![None; eom_line.len()];
let signal_offset = eom_line.len() - signal_sma.len();
for (i, &s) in signal_sma.iter().enumerate() {
signal[i + signal_offset] = s;
}

(eom_line, signal)
eom_line
}

#[cfg(test)]
Expand All @@ -52,9 +69,13 @@ mod tests {
use crate::testutils;
use crate::utils::round_vec;

/// Verifies the standard EOM outputs against fixture data.
#[test]
fn test_eom() {
// Given
let test_cases = vec!["005930", "TSLA"];

// When
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");
Expand All @@ -71,6 +92,7 @@ mod tests {
symbol
));

// Then
assert_eq!(
round_vec(eom, 8),
round_vec(expected_eom, 8),
Expand Down
62 changes: 21 additions & 41 deletions core/src/indicators/macd.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use crate::indicators::ema::ema;
use crate::indicators::ema::{ema, ema_aligned};

pub fn macd(
data: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> (Vec<Option<f64>>, Vec<Option<f64>>, Vec<Option<f64>>) {
let macd_line = calc_macd_line(data, fast_period, slow_period);
let signal_line = calc_macd_signal(&macd_line, signal_period);

// Calculate the histogram
let macd_line = macd_line(data, fast_period, slow_period);
let signal_line = ema_aligned(&macd_line, signal_period);
let histogram = macd_line
.iter()
.zip(signal_line.iter())
Expand All @@ -22,28 +20,14 @@ pub fn macd(
(macd_line, signal_line, histogram)
}

pub fn macd_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec<Option<f64>> {
calc_macd_line(data, fast_period, slow_period)
}

pub fn macd_signal(
data: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> Vec<Option<f64>> {
let macd_line = calc_macd_line(data, fast_period, slow_period);
calc_macd_signal(&macd_line, signal_period)
}

pub fn macd_histogram(
data: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> Vec<Option<f64>> {
let macd_line = calc_macd_line(data, fast_period, slow_period);
let signal_line = calc_macd_signal(&macd_line, signal_period);
let macd_line = macd_line(data, fast_period, slow_period);
let signal_line = ema_aligned(&macd_line, signal_period);

macd_line
.iter()
Expand All @@ -55,7 +39,17 @@ pub fn macd_histogram(
.collect()
}

fn calc_macd_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec<Option<f64>> {
pub fn macd_signal(
data: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> Vec<Option<f64>> {
let macd_line = macd_line(data, fast_period, slow_period);
ema_aligned(&macd_line, signal_period)
}

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

if data.len() < slow_period || fast_period >= slow_period {
Expand All @@ -74,34 +68,19 @@ fn calc_macd_line(data: &[f64], fast_period: usize, slow_period: usize) -> Vec<O
macd_line
}

fn calc_macd_signal(macd_line: &[Option<f64>], signal_period: usize) -> Vec<Option<f64>> {
let mut signal_line: Vec<Option<f64>> = vec![None; macd_line.len()];
let null_count = macd_line.iter().take_while(|&&x| x.is_none()).count();
let macd_values: Vec<f64> = macd_line
.iter()
.skip(null_count)
.filter_map(|&x| x)
.collect();
let ema_values = ema(&macd_values, signal_period);

for i in 0..ema_values.len() {
if let Some(ema_value) = ema_values[i] {
signal_line[i + null_count] = Some(ema_value);
}
}

signal_line
}

#[cfg(test)]
mod tests {
use super::*;
use crate::testutils;
use crate::utils::round_vec;

/// Verifies the standard MACD outputs against fixture data.
#[test]
fn test_macd() {
// Given
let test_cases = vec!["005930", "TSLA"];

// When
for symbol in test_cases {
let input = testutils::load_data(&format!("../data/{}.json", symbol), "c");
let (macd_line, signal_line, histogram) = macd(&input, 12, 26, 9);
Expand All @@ -119,6 +98,7 @@ mod tests {
symbol
));

// Then
assert_eq!(
round_vec(macd_line, 8),
round_vec(expected_macd, 8),
Expand Down
Loading