213 lines
No EOL
6.4 KiB
Rust
213 lines
No EOL
6.4 KiB
Rust
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
|
||
|
||
/// Exponential Moving Average (EMA) Indicator
|
||
///
|
||
/// Calculates the exponentially weighted moving average
|
||
/// giving more weight to recent prices
|
||
pub struct EMA {
|
||
period: usize,
|
||
alpha: f64,
|
||
value: Option<f64>,
|
||
initialized: bool,
|
||
}
|
||
|
||
impl EMA {
|
||
pub fn new(period: usize) -> Result<Self, IndicatorError> {
|
||
if period == 0 {
|
||
return Err(IndicatorError::InvalidParameter(
|
||
"Period must be greater than 0".to_string()
|
||
));
|
||
}
|
||
|
||
// Calculate smoothing factor (alpha)
|
||
// Common formula: 2 / (period + 1)
|
||
let alpha = 2.0 / (period as f64 + 1.0);
|
||
|
||
Ok(Self {
|
||
period,
|
||
alpha,
|
||
value: None,
|
||
initialized: false,
|
||
})
|
||
}
|
||
|
||
/// Create EMA with custom smoothing factor
|
||
pub fn with_alpha(alpha: f64) -> Result<Self, IndicatorError> {
|
||
if alpha <= 0.0 || alpha > 1.0 {
|
||
return Err(IndicatorError::InvalidParameter(
|
||
"Alpha must be between 0 and 1".to_string()
|
||
));
|
||
}
|
||
|
||
// Calculate equivalent period for reference
|
||
let period = ((2.0 / alpha) - 1.0) as usize;
|
||
|
||
Ok(Self {
|
||
period,
|
||
alpha,
|
||
value: None,
|
||
initialized: false,
|
||
})
|
||
}
|
||
|
||
/// Calculate EMA for a series of values
|
||
pub fn calculate_series(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
|
||
if period == 0 {
|
||
return Err(IndicatorError::InvalidParameter(
|
||
"Period must be greater than 0".to_string()
|
||
));
|
||
}
|
||
|
||
if values.is_empty() {
|
||
return Ok(vec![]);
|
||
}
|
||
|
||
let alpha = 2.0 / (period as f64 + 1.0);
|
||
let mut result = Vec::with_capacity(values.len());
|
||
|
||
// Start with first value as initial EMA
|
||
let mut ema = values[0];
|
||
result.push(ema);
|
||
|
||
// Calculate EMA for remaining values
|
||
for i in 1..values.len() {
|
||
ema = alpha * values[i] + (1.0 - alpha) * ema;
|
||
result.push(ema);
|
||
}
|
||
|
||
Ok(result)
|
||
}
|
||
|
||
/// Alternative initialization using SMA of first N values
|
||
pub fn calculate_series_sma_init(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
|
||
if period == 0 {
|
||
return Err(IndicatorError::InvalidParameter(
|
||
"Period must be greater than 0".to_string()
|
||
));
|
||
}
|
||
|
||
if values.len() < period {
|
||
return Err(IndicatorError::InsufficientData {
|
||
required: period,
|
||
actual: values.len(),
|
||
});
|
||
}
|
||
|
||
let alpha = 2.0 / (period as f64 + 1.0);
|
||
let mut result = Vec::with_capacity(values.len() - period + 1);
|
||
|
||
// Calculate initial SMA
|
||
let initial_sma: f64 = values[0..period].iter().sum::<f64>() / period as f64;
|
||
let mut ema = initial_sma;
|
||
result.push(ema);
|
||
|
||
// Calculate EMA for remaining values
|
||
for i in period..values.len() {
|
||
ema = alpha * values[i] + (1.0 - alpha) * ema;
|
||
result.push(ema);
|
||
}
|
||
|
||
Ok(result)
|
||
}
|
||
}
|
||
|
||
impl Indicator for EMA {
|
||
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
|
||
let values = &data.close;
|
||
|
||
if values.is_empty() {
|
||
return Err(IndicatorError::InsufficientData {
|
||
required: 1,
|
||
actual: 0,
|
||
});
|
||
}
|
||
|
||
// Reset and calculate from scratch
|
||
self.reset();
|
||
let ema_values = Self::calculate_series(values, self.period)?;
|
||
|
||
// Update internal state with last value
|
||
if let Some(&last) = ema_values.last() {
|
||
self.value = Some(last);
|
||
self.initialized = true;
|
||
}
|
||
|
||
Ok(IndicatorResult::Series(ema_values))
|
||
}
|
||
|
||
fn reset(&mut self) {
|
||
self.value = None;
|
||
self.initialized = false;
|
||
}
|
||
|
||
fn is_ready(&self) -> bool {
|
||
self.initialized && self.value.is_some()
|
||
}
|
||
}
|
||
|
||
impl IncrementalIndicator for EMA {
|
||
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
|
||
match self.value {
|
||
Some(prev_ema) => {
|
||
// Update EMA: EMA = α × Price + (1 - α) × Previous EMA
|
||
let new_ema = self.alpha * value + (1.0 - self.alpha) * prev_ema;
|
||
self.value = Some(new_ema);
|
||
self.initialized = true;
|
||
Ok(Some(new_ema))
|
||
}
|
||
None => {
|
||
// First value becomes the initial EMA
|
||
self.value = Some(value);
|
||
self.initialized = true;
|
||
Ok(Some(value))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn current(&self) -> Option<f64> {
|
||
self.value
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_ema_calculation() {
|
||
let values = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
|
||
let result = EMA::calculate_series(&values, 3).unwrap();
|
||
|
||
assert_eq!(result.len(), 6);
|
||
assert!((result[0] - 10.0).abs() < 1e-10); // First value
|
||
|
||
// Verify EMA calculation
|
||
let alpha = 2.0 / 4.0; // 0.5
|
||
let expected_ema2 = alpha * 11.0 + (1.0 - alpha) * 10.0; // 10.5
|
||
assert!((result[1] - expected_ema2).abs() < 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_incremental_ema() {
|
||
let mut ema = EMA::new(3).unwrap();
|
||
|
||
// First value
|
||
assert_eq!(ema.update(10.0).unwrap(), Some(10.0));
|
||
|
||
// Second value: EMA = 0.5 * 12 + 0.5 * 10 = 11
|
||
assert_eq!(ema.update(12.0).unwrap(), Some(11.0));
|
||
|
||
// Third value: EMA = 0.5 * 14 + 0.5 * 11 = 12.5
|
||
assert_eq!(ema.update(14.0).unwrap(), Some(12.5));
|
||
}
|
||
|
||
#[test]
|
||
fn test_ema_with_sma_init() {
|
||
let values = vec![2.0, 4.0, 6.0, 8.0, 10.0, 12.0];
|
||
let result = EMA::calculate_series_sma_init(&values, 3).unwrap();
|
||
|
||
// Initial SMA = (2 + 4 + 6) / 3 = 4
|
||
assert_eq!(result.len(), 4);
|
||
assert!((result[0] - 4.0).abs() < 1e-10);
|
||
}
|
||
} |