Skip to main content

finance_solution/stocks/ta/
moving_average.rs

1//! # Simple & exponential moving averages (SMA / EMA)
2//!
3//! Teaching + production building blocks for price smoothers. **Batch** APIs
4//! ([`sma`], [`ema`]) and **incremental** APIs ([`SmaState`], [`EmaState`]) share one
5//! implementation path: batch is “create state → [`push_bars`](SmaState::push_bars)”.
6//!
7//! ## Word problem
8//!
9//! > A stock closed at 10, 11, 12, 13, 14 over five days. What is the 3-day SMA on
10//! > day 5?
11//!
12//! Expect: `(12 + 13 + 14) / 3 = 13`.
13//!
14//! ```
15//! use finance_solution::stocks::ta::sma;
16//! let closes = [10.0, 11.0, 12.0, 13.0, 14.0];
17//! let s = sma(&closes, 3).unwrap();
18//! // period index:     0     1     2     3     4
19//! // warm-up:        None  None  Some  Some  Some
20//! assert_eq!(s[0], None);
21//! assert_eq!(s[1], None);
22//! assert!((s[2].unwrap() - 11.0).abs() < 1e-12); // (10+11+12)/3
23//! assert!((s[4].unwrap() - 13.0).abs() < 1e-12); // (12+13+14)/3
24//! ```
25//!
26//! ## Quant pattern — one pack, many symbols
27//!
28//! ```
29//! use finance_solution::stocks::ta::{SmaState, EmaState};
30//!
31//! // Live: hold state per symbol (your engine's HashMap)
32//! let mut sma20 = SmaState::new(20).unwrap();
33//! let mut ema20 = EmaState::new(20).unwrap();
34//! # let payload = [100.0, 100.5, 101.0];
35//! // One streaming payload with several bars:
36//! let _ = sma20.push_bars(&payload).unwrap();
37//! let _ = ema20.push_bars(&payload).unwrap();
38//! // Or single bar:
39//! let last_sma = sma20.push(101.2).unwrap(); // Option after warm-up
40//! ```
41//!
42//! ## Formulas
43//!
44//! **SMA** over window of length `n`:
45//!
46//! ```text
47//! SMA_t = (P_{t-n+1} + … + P_t) / n
48//! ```
49//!
50//! **EMA** with span `n` (α = 2/(n+1)), seed = SMA of first `n` closes:
51//!
52//! ```text
53//! EMA_seed = SMA(P_0..P_{n-1})
54//! EMA_t    = α * P_t + (1-α) * EMA_{t-1}
55//! ```
56//!
57//! ## Warm-up
58//!
59//! Output length = input length. Indices `0 .. n-2` are `None` until the window is full.
60//!
61//! ## Also here
62//!
63//! - **WMA** — linear weighted MA (newest bar has highest weight)
64//! - **HMA** — [Hull](https://alanhull.com/) moving average: `WMA(2·WMA(n/2) − WMA(n), √n)`
65//!
66//! ## Related
67//!
68//! - Incremental: [`SmaState`], [`EmaState`], [`WmaState`], [`HmaState`]
69//! - Used by: Bollinger (SMA mid), Keltner/MACD (EMA)
70
71use crate::stocks::ta::ring::RingF64;
72use crate::util::error::{require_finite, FinanceError, FinanceResult};
73use crate::util::primitives::PeriodLength;
74
75// ---------------------------------------------------------------------------
76// Incremental state (canonical math path for SMA/EMA)
77// ---------------------------------------------------------------------------
78
79/// Incremental SMA. After warm-up, each [`SmaState::push`] is O(1).
80///
81/// Batch [`sma`] is implemented as `SmaState::new` + [`SmaState::push_bars`].
82///
83/// # Examples
84/// ```
85/// use finance_solution::stocks::ta::SmaState;
86/// let mut s = SmaState::new(3).unwrap();
87/// assert_eq!(s.push(1.0).unwrap(), None);
88/// assert_eq!(s.push(2.0).unwrap(), None);
89/// assert!((s.push(3.0).unwrap().unwrap() - 2.0).abs() < 1e-12);
90/// assert!((s.push(6.0).unwrap().unwrap() - 3.666666666666).abs() < 1e-9);
91/// ```
92#[derive(Clone, Debug)]
93pub struct SmaState {
94    period: usize,
95    ring: RingF64,
96}
97
98impl SmaState {
99    /// Fallible constructor (`period >= 1`). Named `new` → [`FinanceResult`] (not `try_new`).
100    pub fn new(period: usize) -> FinanceResult<Self> {
101        let period = PeriodLength::new(period)?.get();
102        Ok(Self {
103            period,
104            ring: RingF64::with_capacity(period),
105        })
106    }
107
108    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
109        let mut s = Self::new(period)?;
110        s.push_bars(closes)?;
111        Ok(s)
112    }
113
114    pub fn period(&self) -> usize {
115        self.period
116    }
117
118    pub fn reset(&mut self) {
119        self.ring.clear();
120    }
121
122    /// Push one close. `None` until `period` samples seen.
123    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
124        require_finite("close", close)?;
125        let _ = self.ring.push(close);
126        if self.ring.is_full() {
127            Ok(Some(self.ring.sum() / self.period as f64))
128        } else {
129            Ok(None)
130        }
131    }
132
133    /// Push many closes (one streaming payload). One output per input.
134    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
135        let mut out = Vec::with_capacity(closes.len());
136        for &c in closes {
137            out.push(self.push(c)?);
138        }
139        Ok(out)
140    }
141
142    pub fn last(&self) -> Option<f64> {
143        if self.ring.is_full() {
144            Some(self.ring.sum() / self.period as f64)
145        } else {
146            None
147        }
148    }
149}
150
151/// Incremental EMA (α = 2/(period+1), seed = SMA of first `period` closes).
152///
153/// Batch [`ema`] uses this state end-to-end.
154///
155/// # Examples
156/// ```
157/// use finance_solution::stocks::ta::{EmaState, ema};
158/// let closes: Vec<f64> = (1..=20).map(|x| x as f64).collect();
159/// let batch = ema(&closes, 5).unwrap();
160/// let mut st = EmaState::new(5).unwrap();
161/// let mut last = None;
162/// for &c in &closes {
163///     last = st.push(c).unwrap();
164/// }
165/// assert!((last.unwrap() - batch[19].unwrap()).abs() < 1e-9);
166/// ```
167#[derive(Clone, Debug)]
168pub struct EmaState {
169    period: usize,
170    alpha: f64,
171    seed: RingF64,
172    value: Option<f64>,
173}
174
175impl EmaState {
176    pub fn new(period: usize) -> FinanceResult<Self> {
177        let period = PeriodLength::new(period)?.get();
178        Ok(Self {
179            period,
180            alpha: 2.0 / (period as f64 + 1.0),
181            seed: RingF64::with_capacity(period),
182            value: None,
183        })
184    }
185
186    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
187        let mut s = Self::new(period)?;
188        s.push_bars(closes)?;
189        Ok(s)
190    }
191
192    pub fn period(&self) -> usize {
193        self.period
194    }
195
196    pub fn reset(&mut self) {
197        self.seed.clear();
198        self.value = None;
199    }
200
201    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
202        require_finite("close", close)?;
203        if let Some(prev) = self.value {
204            let next = self.alpha * close + (1.0 - self.alpha) * prev;
205            self.value = Some(next);
206            return Ok(Some(next));
207        }
208        let _ = self.seed.push(close);
209        if self.seed.is_full() {
210            let seed = self.seed.sum() / self.period as f64;
211            self.value = Some(seed);
212            Ok(Some(seed))
213        } else {
214            Ok(None)
215        }
216    }
217
218    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
219        let mut out = Vec::with_capacity(closes.len());
220        for &c in closes {
221            out.push(self.push(c)?);
222        }
223        Ok(out)
224    }
225
226    pub fn last(&self) -> Option<f64> {
227        self.value
228    }
229}
230
231/// SMA of `period` closes. Leading `period - 1` values are `None`.
232///
233/// Implemented via [`SmaState::push_bars`] so batch and streaming stay bit-identical.
234///
235/// # Errors
236/// Empty input, non-finite values, or `period == 0`.
237///
238/// # Examples
239/// ```
240/// use finance_solution::stocks::ta::sma;
241/// let c = [1.0, 2.0, 3.0, 4.0, 5.0];
242/// let s = sma(&c, 3).unwrap();
243/// assert_eq!(s[0], None);
244/// assert_eq!(s[1], None);
245/// assert!((s[2].unwrap() - 2.0).abs() < 1e-12); // (1+2+3)/3
246/// assert!((s[4].unwrap() - 4.0).abs() < 1e-12); // (3+4+5)/3
247/// ```
248///
249/// Short series (shorter than period) — all `None`, still `Ok`:
250/// ```
251/// use finance_solution::stocks::ta::sma;
252/// let s = sma(&[1.0, 2.0], 5).unwrap();
253/// assert!(s.iter().all(|x| x.is_none()));
254/// ```
255pub fn sma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
256    validate_closes(closes)?;
257    let mut st = SmaState::new(period)?;
258    st.push_bars(closes)
259}
260
261/// EMA with span `period` (α = 2 / (period + 1)). Seed = SMA of the first `period` closes.
262///
263/// Implemented via [`EmaState::push_bars`].
264///
265/// # Errors
266/// Same domain as [`sma`].
267///
268/// # Examples
269/// ```
270/// use finance_solution::stocks::ta::ema;
271/// let c: Vec<f64> = (1..=20).map(|x| x as f64).collect();
272/// let e = ema(&c, 5).unwrap();
273/// assert!(e[3].is_none());
274/// assert!(e[4].is_some());
275/// assert!(e[19].unwrap().is_finite());
276/// ```
277pub fn ema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
278    validate_closes(closes)?;
279    let mut st = EmaState::new(period)?;
280    st.push_bars(closes)
281}
282
283/// Last defined SMA value, if any.
284///
285/// Uses [`SmaState`] end-to-end (no intermediate full `Vec` of options beyond the push loop).
286/// Prefer holding an [`SmaState`] across live bars instead of calling this on growing history.
287///
288/// # Examples
289/// ```
290/// use finance_solution::stocks::ta::sma_last;
291/// assert_eq!(sma_last(&[1.0, 2.0, 3.0], 3).unwrap(), Some(2.0));
292/// assert_eq!(sma_last(&[1.0, 2.0], 3).unwrap(), None);
293/// ```
294#[inline]
295pub fn sma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
296    validate_closes(closes)?;
297    let mut st = SmaState::new(period)?;
298    for &c in closes {
299        st.push(c)?;
300    }
301    Ok(st.last())
302}
303
304/// Last defined EMA value, if any (via [`EmaState`]).
305///
306/// # Examples
307/// ```
308/// use finance_solution::stocks::ta::{ema, ema_last};
309/// let c: Vec<f64> = (1..=15).map(|x| x as f64).collect();
310/// let series = ema(&c, 5).unwrap();
311/// let last = ema_last(&c, 5).unwrap();
312/// assert_eq!(last, series[14]);
313/// ```
314#[inline]
315pub fn ema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
316    validate_closes(closes)?;
317    let mut st = EmaState::new(period)?;
318    for &c in closes {
319        st.push(c)?;
320    }
321    Ok(st.last())
322}
323
324// ---------------------------------------------------------------------------
325// WMA (weighted moving average)
326// ---------------------------------------------------------------------------
327
328/// Incremental WMA: newest sample weight = `period`, oldest weight = 1.
329///
330/// \[
331/// \mathrm{WMA} = \frac{\sum_{i=1}^{n} i\, P_{t-n+i}}{\sum_{i=1}^{n} i}
332/// \]
333///
334/// After warm-up each [`push`](Self::push) is **O(1)** via the sliding identity  
335/// `W' = W − Σ + n·p_new` (with ring sum for `Σ`).
336#[derive(Clone, Debug)]
337pub struct WmaState {
338    period: usize,
339    ring: RingF64,
340    weight_sum: f64,
341    /// Numerator `Σ i·P_i` once the window is full.
342    weighted: Option<f64>,
343}
344
345impl WmaState {
346    pub fn new(period: usize) -> FinanceResult<Self> {
347        let period = PeriodLength::new(period)?.get();
348        let weight_sum = (period * (period + 1)) as f64 / 2.0;
349        Ok(Self {
350            period,
351            ring: RingF64::with_capacity(period),
352            weight_sum,
353            weighted: None,
354        })
355    }
356
357    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
358        let mut s = Self::new(period)?;
359        s.push_bars(closes)?;
360        Ok(s)
361    }
362
363    pub fn period(&self) -> usize {
364        self.period
365    }
366
367    pub fn reset(&mut self) {
368        self.ring.clear();
369        self.weighted = None;
370    }
371
372    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
373        require_finite("close", close)?;
374        let n = self.period as f64;
375        if let Some(w) = self.weighted {
376            // Full window: O(1) slide.
377            let sum_before = self.ring.sum();
378            let _old = self.ring.push(close);
379            // W' = W - sum_old + n * p_new  (sum_old is sum before push)
380            let w_new = w - sum_before + n * close;
381            self.weighted = Some(w_new);
382            Ok(Some(w_new / self.weight_sum))
383        } else {
384            let _ = self.ring.push(close);
385            if !self.ring.is_full() {
386                return Ok(None);
387            }
388            // First full window: O(n) seed of weighted sum.
389            let mut ordered = Vec::with_capacity(self.period);
390            self.ring.copy_ordered(&mut ordered);
391            let mut num = 0.0;
392            for (i, &p) in ordered.iter().enumerate() {
393                num += (i + 1) as f64 * p;
394            }
395            self.weighted = Some(num);
396            Ok(Some(num / self.weight_sum))
397        }
398    }
399
400    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
401        let mut out = Vec::with_capacity(closes.len());
402        for &c in closes {
403            out.push(self.push(c)?);
404        }
405        Ok(out)
406    }
407
408    pub fn last(&self) -> Option<f64> {
409        self.weighted.map(|w| w / self.weight_sum)
410    }
411}
412
413/// Weighted moving average (newest weight = `period`).
414pub fn wma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
415    validate_closes(closes)?;
416    let mut st = WmaState::new(period)?;
417    st.push_bars(closes)
418}
419
420pub fn wma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
421    validate_closes(closes)?;
422    let mut st = WmaState::new(period)?;
423    for &c in closes {
424        st.push(c)?;
425    }
426    Ok(st.last())
427}
428
429// ---------------------------------------------------------------------------
430// Hull moving average (HMA)
431// ---------------------------------------------------------------------------
432
433/// Hull moving average state.
434///
435/// ```text
436/// raw = 2 * WMA(price, n/2) - WMA(price, n)
437/// HMA = WMA(raw, floor(sqrt(n)))
438/// ```
439///
440/// Requires `period >= 2`.
441#[derive(Clone, Debug)]
442pub struct HmaState {
443    period: usize,
444    half: WmaState,
445    full: WmaState,
446    sqrt_wma: WmaState,
447    last: Option<f64>,
448}
449
450impl HmaState {
451    pub fn new(period: usize) -> FinanceResult<Self> {
452        let period = PeriodLength::new(period)?.get();
453        if period < 2 {
454            return Err(FinanceError::Unsolvable {
455                message: "HMA period must be >= 2",
456            });
457        }
458        let half_n = (period / 2).max(1);
459        let sqrt_n = ((period as f64).sqrt().floor() as usize).max(1);
460        Ok(Self {
461            period,
462            half: WmaState::new(half_n)?,
463            full: WmaState::new(period)?,
464            sqrt_wma: WmaState::new(sqrt_n)?,
465            last: None,
466        })
467    }
468
469    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
470        let mut s = Self::new(period)?;
471        s.push_bars(closes)?;
472        Ok(s)
473    }
474
475    pub fn period(&self) -> usize {
476        self.period
477    }
478
479    pub fn reset(&mut self) {
480        self.half.reset();
481        self.full.reset();
482        self.sqrt_wma.reset();
483        self.last = None;
484    }
485
486    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
487        let wh = self.half.push(close)?;
488        let wf = self.full.push(close)?;
489        let out = match (wh, wf) {
490            (Some(h), Some(f)) => {
491                let raw = 2.0 * h - f;
492                self.sqrt_wma.push(raw)?
493            }
494            _ => None,
495        };
496        self.last = out;
497        Ok(out)
498    }
499
500    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
501        let mut out = Vec::with_capacity(closes.len());
502        for &c in closes {
503            out.push(self.push(c)?);
504        }
505        Ok(out)
506    }
507
508    pub fn last(&self) -> Option<f64> {
509        // Prefer cached last; fall back to sqrt WMA after clone without re-push.
510        self.last.or_else(|| self.sqrt_wma.last())
511    }
512}
513
514/// Hull moving average of `period` (must be ≥ 2).
515///
516/// # Examples
517/// ```
518/// use finance_solution::stocks::ta::hma;
519/// let c: Vec<f64> = (1..=40).map(|x| x as f64).collect();
520/// let h = hma(&c, 9).unwrap();
521/// assert!(h.iter().any(|x| x.is_some()));
522/// assert!(h.last().unwrap().unwrap().is_finite());
523/// ```
524pub fn hma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
525    validate_closes(closes)?;
526    let mut st = HmaState::new(period)?;
527    st.push_bars(closes)
528}
529
530pub fn hma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
531    validate_closes(closes)?;
532    let mut st = HmaState::new(period)?;
533    for &c in closes {
534        st.push(c)?;
535    }
536    Ok(st.last())
537}
538
539fn validate_closes(closes: &[f64]) -> FinanceResult<()> {
540    if closes.is_empty() {
541        return Err(FinanceError::EmptyInput { what: "closes" });
542    }
543    for &c in closes {
544        require_finite("close", c)?;
545    }
546    Ok(())
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn sma_constant() {
555        let c = [10.0; 5];
556        let s = sma(&c, 3).unwrap();
557        assert_eq!(s[2], Some(10.0));
558        assert_eq!(s[4], Some(10.0));
559    }
560
561    #[test]
562    fn ema_runs() {
563        let c: Vec<_> = (1..=30).map(|x| x as f64).collect();
564        let e = ema(&c, 10).unwrap();
565        assert!(e[8].is_none());
566        assert!(e[9].is_some());
567    }
568
569    #[test]
570    fn rejects_zero_period() {
571        assert!(sma(&[1.0, 2.0], 0).is_err());
572    }
573
574    #[test]
575    fn sma_period_one_is_identity() {
576        let c = [1.0, 2.0, 3.0];
577        let s = sma(&c, 1).unwrap();
578        assert_eq!(s[0], Some(1.0));
579        assert_eq!(s[2], Some(3.0));
580    }
581
582    #[test]
583    fn ema_seed_is_sma() {
584        let c = [1.0, 2.0, 3.0, 4.0, 5.0];
585        let e = ema(&c, 3).unwrap();
586        // First EMA value at index 2 = SMA(1,2,3) = 2
587        assert!((e[2].unwrap() - 2.0).abs() < 1e-12);
588    }
589
590    #[test]
591    fn empty_series_err() {
592        assert!(sma(&[], 3).is_err());
593        assert!(ema(&[], 3).is_err());
594    }
595
596    #[test]
597    fn nan_close_err() {
598        assert!(sma(&[1.0, f64::NAN], 2).is_err());
599    }
600
601    #[test]
602    fn last_matches_series_tail() {
603        let c: Vec<_> = (1..=25).map(|x| x as f64 * 0.5).collect();
604        let s = sma(&c, 7).unwrap();
605        assert_eq!(sma_last(&c, 7).unwrap(), s[24]);
606        let e = ema(&c, 7).unwrap();
607        assert_eq!(ema_last(&c, 7).unwrap(), e[24]);
608    }
609
610    #[test]
611    fn wma_weights_newest_heavier() {
612        // window [1,2,3]: WMA = (1*1+2*2+3*3)/(1+2+3) = 14/6
613        let s = wma(&[1.0, 2.0, 3.0], 3).unwrap();
614        assert!((s[2].unwrap() - 14.0 / 6.0).abs() < 1e-12);
615    }
616
617    #[test]
618    fn hma_state_parity() {
619        let c: Vec<f64> = (1..=50).map(|x| 100.0 + x as f64 * 0.1).collect();
620        let batch = hma(&c, 16).unwrap();
621        let st = HmaState::from_history(16, &c).unwrap();
622        assert!((batch.last().unwrap().unwrap() - st.last().unwrap()).abs() < 1e-9);
623    }
624
625    #[test]
626    fn hma_rejects_period_one() {
627        assert!(HmaState::new(1).is_err());
628    }
629
630    #[test]
631    fn hma_tracks_rising_path() {
632        let c: Vec<f64> = (1..=60).map(|x| x as f64).collect();
633        let h = hma(&c, 9).unwrap();
634        let last = h.iter().rev().find_map(|x| *x).unwrap();
635        // Rising line: HMA should sit near the recent levels (well above early prices).
636        assert!(last > 50.0, "hma last={last}");
637    }
638
639    #[test]
640    fn wma_last_matches_series() {
641        let c: Vec<f64> = (1..=20).map(|x| x as f64).collect();
642        let s = wma(&c, 5).unwrap();
643        assert_eq!(wma_last(&c, 5).unwrap(), s[19]);
644    }
645
646    #[test]
647    fn hma_reset_clears() {
648        let c: Vec<f64> = (1..=30).map(|x| x as f64).collect();
649        let mut st = HmaState::from_history(9, &c).unwrap();
650        assert!(st.last().is_some());
651        st.reset();
652        assert!(st.last().is_none());
653    }
654}