Skip to main content

indicators/
functions.rs

1//! Standalone batch indicator functions and incremental structs.
2//!
3//! Ported from the original `indicators` crate lib. These work on slices
4//! (batch mode) or as incremental O(1)-per-tick structs.
5
6use std::collections::VecDeque;
7
8use crate::error::IndicatorError;
9use crate::types::MacdResult;
10
11// ── Batch functions ───────────────────────────────────────────────────────────
12
13/// Exponential Moving Average over a price slice.
14/// Returns a Vec of the same length; leading values are `NaN` until warm-up.
15pub fn ema(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
16    if period == 0 {
17        return Err(IndicatorError::InvalidParameter {
18            name: "period".into(),
19            value: 0.0,
20        });
21    }
22    if prices.len() < period {
23        return Err(IndicatorError::InsufficientData {
24            required: period,
25            available: prices.len(),
26        });
27    }
28    let mut result = vec![f64::NAN; prices.len()];
29    let alpha = 2.0 / (period as f64 + 1.0);
30    let first_sma: f64 = prices.iter().take(period).sum::<f64>() / period as f64;
31    result[period - 1] = first_sma;
32    for i in period..prices.len() {
33        result[i] = prices[i] * alpha + result[i - 1] * (1.0 - alpha);
34    }
35    Ok(result)
36}
37
38/// Simple Moving Average over a price slice.
39pub fn sma(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
40    if period == 0 {
41        return Err(IndicatorError::InvalidParameter {
42            name: "period".into(),
43            value: 0.0,
44        });
45    }
46    if prices.len() < period {
47        return Err(IndicatorError::InsufficientData {
48            required: period,
49            available: prices.len(),
50        });
51    }
52    let mut result = vec![f64::NAN; prices.len()];
53    for i in (period - 1)..prices.len() {
54        let sum: f64 = prices[(i + 1 - period)..=i].iter().sum();
55        result[i] = sum / period as f64;
56    }
57    Ok(result)
58}
59
60/// True Range = max(H-L, |H-prevC|, |L-prevC|).
61pub fn true_range(high: &[f64], low: &[f64], close: &[f64]) -> Result<Vec<f64>, IndicatorError> {
62    if high.len() != low.len() || high.len() != close.len() {
63        return Err(IndicatorError::InsufficientData {
64            required: high.len(),
65            available: low.len().min(close.len()),
66        });
67    }
68    let mut result = vec![f64::NAN; high.len()];
69    if !high.is_empty() {
70        result[0] = high[0] - low[0];
71    }
72    for i in 1..high.len() {
73        let tr1 = high[i] - low[i];
74        let tr2 = (high[i] - close[i - 1]).abs();
75        let tr3 = (low[i] - close[i - 1]).abs();
76        result[i] = tr1.max(tr2).max(tr3);
77    }
78    Ok(result)
79}
80
81/// Average True Range (EMA-smoothed).
82pub fn atr(
83    high: &[f64],
84    low: &[f64],
85    close: &[f64],
86    period: usize,
87) -> Result<Vec<f64>, IndicatorError> {
88    let tr = true_range(high, low, close)?;
89    ema(&tr, period)
90}
91
92/// Relative Strength Index.
93pub fn rsi(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
94    if prices.len() < period + 1 {
95        return Err(IndicatorError::InsufficientData {
96            required: period + 1,
97            available: prices.len(),
98        });
99    }
100    let mut result = vec![f64::NAN; prices.len()];
101    let mut gains = vec![0.0; prices.len()];
102    let mut losses = vec![0.0; prices.len()];
103    for i in 1..prices.len() {
104        let change = prices[i] - prices[i - 1];
105        if change > 0.0 {
106            gains[i] = change;
107        } else {
108            losses[i] = -change;
109        }
110    }
111    let avg_gains = ema(&gains, period)?;
112    let avg_losses = ema(&losses, period)?;
113    for i in period..prices.len() {
114        if avg_losses[i] == 0.0 {
115            result[i] = 100.0;
116        } else {
117            let rs = avg_gains[i] / avg_losses[i];
118            result[i] = 100.0 - (100.0 / (1.0 + rs));
119        }
120    }
121    Ok(result)
122}
123
124/// MACD — returns (macd_line, signal_line, histogram).
125pub fn macd(
126    prices: &[f64],
127    fast_period: usize,
128    slow_period: usize,
129    signal_period: usize,
130) -> MacdResult {
131    let fast_ema = ema(prices, fast_period)?;
132    let slow_ema = ema(prices, slow_period)?;
133    let mut macd_line = vec![f64::NAN; prices.len()];
134    for i in 0..prices.len() {
135        if !fast_ema[i].is_nan() && !slow_ema[i].is_nan() {
136            macd_line[i] = fast_ema[i] - slow_ema[i];
137        }
138    }
139    let signal_line = ema(&macd_line, signal_period)?;
140    let mut histogram = vec![f64::NAN; prices.len()];
141    for i in 0..prices.len() {
142        if !macd_line[i].is_nan() && !signal_line[i].is_nan() {
143            histogram[i] = macd_line[i] - signal_line[i];
144        }
145    }
146    Ok((macd_line, signal_line, histogram))
147}
148
149// ── Incremental structs ───────────────────────────────────────────────────────
150
151/// Incremental EMA — O(1) update, SMA warm-up.
152///
153/// Unlike the batch [`ema`] function (which initialises from an SMA over the
154/// first `period` prices), this struct emits its first value *after* it has
155/// accumulated exactly `period` samples and seeds itself from their average.
156/// Both approaches are correct; this one is more natural for streaming use.
157#[derive(Debug, Clone)]
158pub struct EMA {
159    period: usize,
160    alpha: f64,
161    value: f64,
162    initialized: bool,
163    warmup: VecDeque<f64>,
164}
165
166impl EMA {
167    pub fn new(period: usize) -> Self {
168        Self {
169            period,
170            alpha: 2.0 / (period as f64 + 1.0),
171            value: 0.0,
172            initialized: false,
173            warmup: VecDeque::with_capacity(period),
174        }
175    }
176
177    pub fn update(&mut self, price: f64) {
178        if !self.initialized {
179            self.warmup.push_back(price);
180            if self.warmup.len() >= self.period {
181                self.value = self.warmup.iter().sum::<f64>() / self.period as f64;
182                self.initialized = true;
183                self.warmup.clear();
184            }
185        } else {
186            self.value = price * self.alpha + self.value * (1.0 - self.alpha);
187        }
188    }
189
190    pub fn value(&self) -> f64 {
191        if self.initialized {
192            self.value
193        } else {
194            f64::NAN
195        }
196    }
197
198    pub fn is_ready(&self) -> bool {
199        self.initialized
200    }
201
202    pub fn reset(&mut self) {
203        self.value = 0.0;
204        self.initialized = false;
205        self.warmup.clear();
206    }
207}
208
209/// Incremental Wilder ATR.
210#[derive(Debug, Clone)]
211pub struct ATR {
212    #[allow(dead_code)]
213    period: usize,
214    ema: EMA,
215    prev_close: Option<f64>,
216}
217
218impl ATR {
219    pub fn new(period: usize) -> Self {
220        Self {
221            period,
222            ema: EMA::new(period),
223            prev_close: None,
224        }
225    }
226
227    pub fn update(&mut self, high: f64, low: f64, close: f64) {
228        let tr = if let Some(prev) = self.prev_close {
229            (high - low)
230                .max((high - prev).abs())
231                .max((low - prev).abs())
232        } else {
233            high - low
234        };
235        self.ema.update(tr);
236        self.prev_close = Some(close);
237    }
238
239    pub fn value(&self) -> f64 {
240        self.ema.value()
241    }
242
243    pub fn is_ready(&self) -> bool {
244        self.ema.is_ready()
245    }
246}
247
248/// Bundle of per-strategy indicator series.
249#[derive(Debug, Clone)]
250pub struct StrategyIndicators {
251    pub ema_fast: Vec<f64>,
252    pub ema_slow: Vec<f64>,
253    pub atr: Vec<f64>,
254}
255
256/// Multi-period indicator calculator (batch mode).
257#[derive(Debug, Clone)]
258pub struct IndicatorCalculator {
259    pub fast_ema_period: usize,
260    pub slow_ema_period: usize,
261    pub atr_period: usize,
262}
263
264impl Default for IndicatorCalculator {
265    fn default() -> Self {
266        Self {
267            fast_ema_period: 8,
268            slow_ema_period: 21,
269            atr_period: 14,
270        }
271    }
272}
273
274impl IndicatorCalculator {
275    pub fn new(fast_ema: usize, slow_ema: usize, atr_period: usize) -> Self {
276        Self {
277            fast_ema_period: fast_ema,
278            slow_ema_period: slow_ema,
279            atr_period,
280        }
281    }
282
283    pub fn calculate_all(
284        &self,
285        close: &[f64],
286        high: &[f64],
287        low: &[f64],
288    ) -> Result<StrategyIndicators, IndicatorError> {
289        Ok(StrategyIndicators {
290            ema_fast: ema(close, self.fast_ema_period)?,
291            ema_slow: ema(close, self.slow_ema_period)?,
292            atr: atr(high, low, close, self.atr_period)?,
293        })
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_ema_sma_seed() {
303        let prices = vec![22.27, 22.19, 22.08, 22.17, 22.18];
304        let result = ema(&prices, 5).unwrap();
305        let expected = (22.27 + 22.19 + 22.08 + 22.17 + 22.18) / 5.0;
306        assert!((result[4] - expected).abs() < 1e-9);
307    }
308
309    #[test]
310    fn test_true_range_first() {
311        let h = vec![50.0, 52.0];
312        let l = vec![48.0, 49.0];
313        let c = vec![49.0, 51.0];
314        let tr = true_range(&h, &l, &c).unwrap();
315        assert_eq!(tr[0], 2.0);
316        assert_eq!(tr[1], 3.0);
317    }
318
319    #[test]
320    fn test_ema_incremental() {
321        let mut e = EMA::new(3);
322        e.update(10.0);
323        assert!(!e.is_ready());
324        e.update(20.0);
325        assert!(!e.is_ready());
326        e.update(30.0);
327        assert!(e.is_ready());
328        assert!((e.value() - 20.0).abs() < 1e-9);
329    }
330}