1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! Moving Average Convergence Divergence (MACD) indicator.
use quant_primitives::Candle;
use crate::ema::Ema;
use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;
/// MACD indicator.
///
/// Measures the relationship between two EMAs. Produces three outputs:
/// - MACD line: fast EMA - slow EMA
/// - Signal line: EMA of MACD line
/// - Histogram: MACD - Signal
///
/// This implementation returns the MACD line. Use `MacdSignal` for signal line
/// or `MacdHistogram` for histogram.
///
/// # Standard Parameters
///
/// - Fast: 12
/// - Slow: 26
/// - Signal: 9
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, Macd};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..30).map(|i| {
/// Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
/// }).collect();
/// let macd = Macd::new(12, 26).unwrap();
/// let series = macd.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Macd {
fast_period: usize,
slow_period: usize,
name: String,
}
impl Macd {
/// Create a new MACD indicator.
///
/// # Arguments
///
/// * `fast_period` - Period for fast EMA (typically 12)
/// * `slow_period` - Period for slow EMA (typically 26)
///
/// # Errors
///
/// Returns `InvalidParameter` if fast >= slow or periods are 0.
pub fn new(fast_period: usize, slow_period: usize) -> Result<Self, IndicatorError> {
if fast_period == 0 || slow_period == 0 {
return Err(IndicatorError::InvalidParameter {
message: "MACD periods must be > 0".to_string(),
});
}
if fast_period >= slow_period {
return Err(IndicatorError::InvalidParameter {
message: format!(
"MACD fast period ({}) must be < slow period ({})",
fast_period, slow_period
),
});
}
Ok(Self {
fast_period,
slow_period,
name: format!("MACD({},{})", fast_period, slow_period),
})
}
/// Create MACD with standard parameters (12, 26).
pub fn standard() -> Result<Self, IndicatorError> {
Self::new(12, 26)
}
}
impl Indicator for Macd {
fn name(&self) -> &str {
&self.name
}
fn warmup_period(&self) -> usize {
self.slow_period
}
fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
if candles.len() < self.slow_period {
return Err(IndicatorError::InsufficientData {
required: self.slow_period,
actual: candles.len(),
});
}
let fast_ema = Ema::new(self.fast_period)?;
let slow_ema = Ema::new(self.slow_period)?;
let fast_series = fast_ema.compute(candles)?;
let slow_series = slow_ema.compute(candles)?;
// Align: slow EMA starts later
let offset = self.slow_period - self.fast_period;
let fast_values = fast_series.values();
let slow_values = slow_series.values();
let mut values = Vec::with_capacity(slow_values.len());
for (i, (ts, slow_val)) in slow_values.iter().enumerate() {
let fast_val = fast_values[i + offset].1;
let macd = fast_val - *slow_val;
values.push((*ts, macd));
}
Ok(Series::new(values))
}
}
#[cfg(test)]
#[path = "macd_tests.rs"]
mod tests;