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