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