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/// EMA that handles leading NaN values, matching Python's `ewm(adjust=False)` behaviour.
39///
40/// Unlike [`ema`], which seeds from the arithmetic mean of the first `period`
41/// values, this function seeds from the **first non-NaN value** and applies
42/// the recursive formula from that point on.  All positions before the first
43/// non-NaN value are left as `NaN`.
44///
45/// This is needed wherever EMA is applied to a derived series (e.g. the MACD
46/// line) that already has a leading NaN warm-up period.  Using the standard
47/// [`ema`] on such a series would propagate NaN through the SMA seed and
48/// produce an all-NaN output.
49pub fn ema_nan_aware(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
50    if period == 0 {
51        return Err(IndicatorError::InvalidParameter {
52            name: "period".into(),
53            value: 0.0,
54        });
55    }
56    let mut result = vec![f64::NAN; prices.len()];
57    let alpha = 2.0 / (period as f64 + 1.0);
58
59    // Seed from the first non-NaN value (adjust=False, no SMA warm-up).
60    let Some(start) = prices.iter().position(|v| !v.is_nan()) else {
61        return Ok(result); // all NaN — nothing to compute
62    };
63
64    result[start] = prices[start];
65    for i in (start + 1)..prices.len() {
66        result[i] = if prices[i].is_nan() {
67            f64::NAN
68        } else {
69            prices[i] * alpha + result[i - 1] * (1.0 - alpha)
70        };
71    }
72    Ok(result)
73}
74
75/// Simple Moving Average over a price slice.
76pub fn sma(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
77    if period == 0 {
78        return Err(IndicatorError::InvalidParameter {
79            name: "period".into(),
80            value: 0.0,
81        });
82    }
83    if prices.len() < period {
84        return Err(IndicatorError::InsufficientData {
85            required: period,
86            available: prices.len(),
87        });
88    }
89    let mut result = vec![f64::NAN; prices.len()];
90    for i in (period - 1)..prices.len() {
91        let sum: f64 = prices[(i + 1 - period)..=i].iter().sum();
92        result[i] = sum / period as f64;
93    }
94    Ok(result)
95}
96
97/// True Range = max(H-L, |H-prevC|, |L-prevC|).
98pub fn true_range(high: &[f64], low: &[f64], close: &[f64]) -> Result<Vec<f64>, IndicatorError> {
99    if high.len() != low.len() || high.len() != close.len() {
100        return Err(IndicatorError::InsufficientData {
101            required: high.len(),
102            available: low.len().min(close.len()),
103        });
104    }
105    let mut result = vec![f64::NAN; high.len()];
106    if !high.is_empty() {
107        result[0] = high[0] - low[0];
108    }
109    for i in 1..high.len() {
110        let tr1 = high[i] - low[i];
111        let tr2 = (high[i] - close[i - 1]).abs();
112        let tr3 = (low[i] - close[i - 1]).abs();
113        result[i] = tr1.max(tr2).max(tr3);
114    }
115    Ok(result)
116}
117
118/// Average True Range (EMA-smoothed).
119pub fn atr(
120    high: &[f64],
121    low: &[f64],
122    close: &[f64],
123    period: usize,
124) -> Result<Vec<f64>, IndicatorError> {
125    let tr = true_range(high, low, close)?;
126    ema(&tr, period)
127}
128
129/// Relative Strength Index.
130pub fn rsi(prices: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
131    if prices.len() < period + 1 {
132        return Err(IndicatorError::InsufficientData {
133            required: period + 1,
134            available: prices.len(),
135        });
136    }
137    let mut result = vec![f64::NAN; prices.len()];
138    let mut gains = vec![0.0; prices.len()];
139    let mut losses = vec![0.0; prices.len()];
140    for i in 1..prices.len() {
141        let change = prices[i] - prices[i - 1];
142        if change > 0.0 {
143            gains[i] = change;
144        } else {
145            losses[i] = -change;
146        }
147    }
148    let avg_gains = ema(&gains, period)?;
149    let avg_losses = ema(&losses, period)?;
150    for i in period..prices.len() {
151        if avg_losses[i] == 0.0 {
152            result[i] = 100.0;
153        } else {
154            let rs = avg_gains[i] / avg_losses[i];
155            result[i] = 100.0 - (100.0 / (1.0 + rs));
156        }
157    }
158    Ok(result)
159}
160
161/// MACD — returns (macd_line, signal_line, histogram).
162pub fn macd(
163    prices: &[f64],
164    fast_period: usize,
165    slow_period: usize,
166    signal_period: usize,
167) -> MacdResult {
168    // Use ema_nan_aware to match Python's ewm(span=X, adjust=False), which
169    // seeds from the first value rather than an SMA of the first `period` bars.
170    let fast_ema = ema_nan_aware(prices, fast_period)?;
171    let slow_ema = ema_nan_aware(prices, slow_period)?;
172    let mut macd_line = vec![f64::NAN; prices.len()];
173    for i in 0..prices.len() {
174        if !fast_ema[i].is_nan() && !slow_ema[i].is_nan() {
175            macd_line[i] = fast_ema[i] - slow_ema[i];
176        }
177    }
178    // The macd_line has leading NaN (warm-up from the slow EMA); use the
179    // NaN-aware variant so the signal seeds from the first valid MACD value
180    // rather than an all-NaN SMA, matching Python's ewm(adjust=False).
181    let signal_line = ema_nan_aware(&macd_line, signal_period)?;
182    let mut histogram = vec![f64::NAN; prices.len()];
183    for i in 0..prices.len() {
184        if !macd_line[i].is_nan() && !signal_line[i].is_nan() {
185            histogram[i] = macd_line[i] - signal_line[i];
186        }
187    }
188    Ok((macd_line, signal_line, histogram))
189}
190
191// ── Incremental structs ───────────────────────────────────────────────────────
192
193/// Incremental EMA — O(1) update, SMA warm-up.
194///
195/// Unlike the batch [`ema`] function (which initialises from an SMA over the
196/// first `period` prices), this struct emits its first value *after* it has
197/// accumulated exactly `period` samples and seeds itself from their average.
198/// Both approaches are correct; this one is more natural for streaming use.
199#[derive(Debug, Clone)]
200pub struct EMA {
201    period: usize,
202    alpha: f64,
203    value: f64,
204    initialized: bool,
205    warmup: VecDeque<f64>,
206}
207
208impl EMA {
209    pub fn new(period: usize) -> Self {
210        Self {
211            period,
212            alpha: 2.0 / (period as f64 + 1.0),
213            value: 0.0,
214            initialized: false,
215            warmup: VecDeque::with_capacity(period),
216        }
217    }
218
219    pub fn update(&mut self, price: f64) {
220        if !self.initialized {
221            self.warmup.push_back(price);
222            if self.warmup.len() >= self.period {
223                self.value = self.warmup.iter().sum::<f64>() / self.period as f64;
224                self.initialized = true;
225                self.warmup.clear();
226            }
227        } else {
228            self.value = price * self.alpha + self.value * (1.0 - self.alpha);
229        }
230    }
231
232    pub fn value(&self) -> f64 {
233        if self.initialized {
234            self.value
235        } else {
236            f64::NAN
237        }
238    }
239
240    pub fn is_ready(&self) -> bool {
241        self.initialized
242    }
243
244    pub fn reset(&mut self) {
245        self.value = 0.0;
246        self.initialized = false;
247        self.warmup.clear();
248    }
249}
250
251/// Incremental Wilder ATR.
252#[derive(Debug, Clone)]
253pub struct ATR {
254    #[allow(dead_code)]
255    period: usize,
256    ema: EMA,
257    prev_close: Option<f64>,
258}
259
260impl ATR {
261    pub fn new(period: usize) -> Self {
262        Self {
263            period,
264            ema: EMA::new(period),
265            prev_close: None,
266        }
267    }
268
269    pub fn update(&mut self, high: f64, low: f64, close: f64) {
270        let tr = if let Some(prev) = self.prev_close {
271            (high - low)
272                .max((high - prev).abs())
273                .max((low - prev).abs())
274        } else {
275            high - low
276        };
277        self.ema.update(tr);
278        self.prev_close = Some(close);
279    }
280
281    pub fn value(&self) -> f64 {
282        self.ema.value()
283    }
284
285    pub fn is_ready(&self) -> bool {
286        self.ema.is_ready()
287    }
288}
289
290/// Bundle of per-strategy indicator series.
291#[derive(Debug, Clone)]
292pub struct StrategyIndicators {
293    pub ema_fast: Vec<f64>,
294    pub ema_slow: Vec<f64>,
295    pub atr: Vec<f64>,
296}
297
298/// Multi-period indicator calculator (batch mode).
299#[derive(Debug, Clone)]
300pub struct IndicatorCalculator {
301    pub fast_ema_period: usize,
302    pub slow_ema_period: usize,
303    pub atr_period: usize,
304}
305
306impl Default for IndicatorCalculator {
307    fn default() -> Self {
308        Self {
309            fast_ema_period: 8,
310            slow_ema_period: 21,
311            atr_period: 14,
312        }
313    }
314}
315
316impl IndicatorCalculator {
317    pub fn new(fast_ema: usize, slow_ema: usize, atr_period: usize) -> Self {
318        Self {
319            fast_ema_period: fast_ema,
320            slow_ema_period: slow_ema,
321            atr_period,
322        }
323    }
324
325    pub fn calculate_all(
326        &self,
327        close: &[f64],
328        high: &[f64],
329        low: &[f64],
330    ) -> Result<StrategyIndicators, IndicatorError> {
331        Ok(StrategyIndicators {
332            ema_fast: ema(close, self.fast_ema_period)?,
333            ema_slow: ema(close, self.slow_ema_period)?,
334            atr: atr(high, low, close, self.atr_period)?,
335        })
336    }
337}
338
339/// Incremental EMA — O(1) update per tick that returns the new value each call.
340///
341/// Unlike [`EMA`] (which separates `update` from `value`/`is_ready`), this
342/// seeds from the first sample and returns the EMA on every `update`, which
343/// suits streaming pipelines that consume the value inline.
344pub struct IncrementalEma {
345    alpha: f64,
346    state: f64,
347    initialized: bool,
348}
349
350impl IncrementalEma {
351    /// Create an incremental EMA for the given period.
352    pub fn new(period: usize) -> Self {
353        Self {
354            alpha: 2.0 / (period as f64 + 1.0),
355            state: 0.0,
356            initialized: false,
357        }
358    }
359
360    /// Feed the next price; returns the updated EMA (seeds from the first price).
361    pub fn update(&mut self, price: f64) -> f64 {
362        if !self.initialized {
363            self.state = price;
364            self.initialized = true;
365        } else {
366            self.state = self.alpha * price + (1.0 - self.alpha) * self.state;
367        }
368        self.state
369    }
370
371    /// Current EMA value, or `None` before the first `update`.
372    pub fn current(&self) -> Option<f64> {
373        if self.initialized {
374            Some(self.state)
375        } else {
376            None
377        }
378    }
379}
380
381/// Incremental ATR — O(1) per-tick true-range EMA.
382///
383/// Wraps an [`IncrementalEma`] over the true range and returns the smoothed
384/// ATR on each `update`. The first sample's true range is `high - low`.
385pub struct IncrementalAtr {
386    ema: IncrementalEma,
387    prev_close: Option<f64>,
388}
389
390impl IncrementalAtr {
391    /// Create an incremental ATR for the given period.
392    pub fn new(period: usize) -> Self {
393        Self {
394            ema: IncrementalEma::new(period),
395            prev_close: None,
396        }
397    }
398
399    /// Feed the next high/low/close; returns the updated ATR.
400    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
401        let tr = if let Some(prev) = self.prev_close {
402            let tr1 = high - low;
403            let tr2 = (high - prev).abs();
404            let tr3 = (low - prev).abs();
405            tr1.max(tr2).max(tr3)
406        } else {
407            high - low
408        };
409
410        self.prev_close = Some(close);
411        Some(self.ema.update(tr))
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_ema_sma_seed() {
421        let prices = vec![22.27, 22.19, 22.08, 22.17, 22.18];
422        let result = ema(&prices, 5).unwrap();
423        let expected = (22.27 + 22.19 + 22.08 + 22.17 + 22.18) / 5.0;
424        assert!((result[4] - expected).abs() < 1e-9);
425    }
426
427    #[test]
428    fn test_true_range_first() {
429        let h = vec![50.0, 52.0];
430        let l = vec![48.0, 49.0];
431        let c = vec![49.0, 51.0];
432        let tr = true_range(&h, &l, &c).unwrap();
433        assert_eq!(tr[0], 2.0);
434        assert_eq!(tr[1], 3.0);
435    }
436
437    #[test]
438    fn test_ema_incremental() {
439        let mut e = EMA::new(3);
440        e.update(10.0);
441        assert!(!e.is_ready());
442        e.update(20.0);
443        assert!(!e.is_ready());
444        e.update(30.0);
445        assert!(e.is_ready());
446        assert!((e.value() - 20.0).abs() < 1e-9);
447    }
448
449    #[test]
450    fn test_incremental_ema_returns_value() {
451        let mut e = IncrementalEma::new(3); // alpha = 0.5
452        assert_eq!(e.current(), None);
453        assert_eq!(e.update(10.0), 10.0); // seeds from first sample
454        assert_eq!(e.current(), Some(10.0));
455        let v = e.update(20.0); // 0.5*20 + 0.5*10
456        assert!((v - 15.0).abs() < 1e-9);
457    }
458
459    #[test]
460    fn test_incremental_atr_first_is_range() {
461        let mut a = IncrementalAtr::new(3);
462        // First sample: TR = high - low, EMA seeds to it.
463        assert_eq!(a.update(12.0, 10.0, 11.0), Some(2.0));
464    }
465}