Skip to main content

finance_solution/stocks/ta/
advanced_ma.rs

1//! # Advanced moving averages: DEMA, TEMA, RMA (Wilder), KAMA
2//!
3//! Smoothers beyond SMA/EMA/WMA/HMA. Same API layers: free batch + `*State` +
4//! (for KAMA) [`KamaParams`].
5//!
6//! | Name | Formula sketch | vs SMA/EMA | Typical use |
7//! |------|----------------|------------|-------------|
8//! | **RMA / Wilder** | α = 1/n; seed SMA | **Slower** than EMA(α=2/(n+1)); same family as RSI/ATR smoothers | Match Wilder indicators; “SMMA” on some platforms |
9//! | **DEMA** | `2·EMA − EMA(EMA)` | **Faster** / less lag than EMA of same period | Trend follow when EMA feels late |
10//! | **TEMA** | `3·e1 − 3·e2 + e3` | Still more lag reduction than DEMA | Aggressive smooth; longer warm-up |
11//! | **KAMA** | ER scales SC between fast/slow EMA constants | **Adaptive**: quiet → slow; trending → fast | Choppy markets where fixed EMA whipsaws |
12//!
13//! ## When to pick which
14//!
15//! | Goal | Prefer | Avoid / caution |
16//! |------|--------|-----------------|
17//! | Classic chart MA | SMA / EMA | TEMA until you need speed |
18//! | Less lag than EMA | DEMA → TEMA | TEMA needs ~3× period warm-up bars |
19//! | Same math as RSI/ATR | **RMA** | Using EMA(14) and calling it Wilder |
20//! | Regime-adaptive | **KAMA** | Expecting a fixed “period feel” |
21//!
22//! ## Pairing for stock signals (illustrative, not advice)
23//!
24//! - **DEMA/TEMA** + **ADX**: only take MA crossovers when ADX shows trend strength.
25//! - **KAMA** + **ATR/Supertrend**: adaptive midline + volatility stop.
26//! - **RMA** + **RSI**: both Wilder-family; consistent smoothing philosophy.
27//! - Fast/slow **EMA** still the default for MACD-style spreads; DEMA/TEMA are optional legs.
28//!
29//! ## Engineering
30//!
31//! Batch free functions use the matching `*State` end-to-end.  
32//! KAMA first value when the ER window fills is **seeded to price** (common convention).
33//!
34//! Defaults: RMA/DEMA/TEMA period often **20** in call sites; KAMA **(10, 2, 30)**.
35
36use crate::stocks::ta::common::validate_series;
37use crate::stocks::ta::moving_average::EmaState;
38use crate::stocks::ta::ring::RingF64;
39use crate::util::error::{require_finite, FinanceError, FinanceResult};
40use crate::util::primitives::PeriodLength;
41
42// ---------------------------------------------------------------------------
43// RMA (Wilder / SMMA)
44// ---------------------------------------------------------------------------
45
46/// Incremental Wilder RMA (also called SMMA). α = 1/`period`.
47///
48/// Use when you want the same recursive smoother as Wilder RSI/ATR, not EMA’s 2/(n+1).
49#[derive(Clone, Debug)]
50pub struct RmaState {
51    period: usize,
52    alpha: f64,
53    seed: RingF64,
54    value: Option<f64>,
55}
56
57impl RmaState {
58    pub fn new(period: usize) -> FinanceResult<Self> {
59        let period = PeriodLength::new(period)?.get();
60        Ok(Self {
61            period,
62            alpha: 1.0 / period as f64,
63            seed: RingF64::with_capacity(period),
64            value: None,
65        })
66    }
67
68    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
69        let mut s = Self::new(period)?;
70        s.push_bars(closes)?;
71        Ok(s)
72    }
73
74    pub fn period(&self) -> usize {
75        self.period
76    }
77
78    pub fn reset(&mut self) {
79        self.seed.clear();
80        self.value = None;
81    }
82
83    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
84        require_finite("close", close)?;
85        if let Some(prev) = self.value {
86            let next = self.alpha * close + (1.0 - self.alpha) * prev;
87            self.value = Some(next);
88            return Ok(Some(next));
89        }
90        let _ = self.seed.push(close);
91        if self.seed.is_full() {
92            let seed = self.seed.sum() / self.period as f64;
93            self.value = Some(seed);
94            Ok(Some(seed))
95        } else {
96            Ok(None)
97        }
98    }
99
100    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
101        let mut out = Vec::with_capacity(closes.len());
102        for &c in closes {
103            out.push(self.push(c)?);
104        }
105        Ok(out)
106    }
107
108    pub fn last(&self) -> Option<f64> {
109        self.value
110    }
111}
112
113/// Wilder RMA / SMMA of `period` closes.
114pub fn rma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
115    validate_series("close", closes)?;
116    let mut st = RmaState::new(period)?;
117    st.push_bars(closes)
118}
119
120pub fn rma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
121    let mut st = RmaState::new(period)?;
122    for &c in closes {
123        st.push(c)?;
124    }
125    Ok(st.last())
126}
127
128// ---------------------------------------------------------------------------
129// DEMA
130// ---------------------------------------------------------------------------
131
132/// Double exponential moving average: `2·EMA − EMA(EMA)`.
133///
134/// Reduces lag vs a single EMA of the same period (Mulloy). Warm-up is longer (nested EMA).
135#[derive(Clone, Debug)]
136pub struct DemaState {
137    e1: EmaState,
138    e2: EmaState,
139}
140
141impl DemaState {
142    pub fn new(period: usize) -> FinanceResult<Self> {
143        Ok(Self {
144            e1: EmaState::new(period)?,
145            e2: EmaState::new(period)?,
146        })
147    }
148
149    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
150        let mut s = Self::new(period)?;
151        s.push_bars(closes)?;
152        Ok(s)
153    }
154
155    pub fn period(&self) -> usize {
156        self.e1.period()
157    }
158
159    pub fn reset(&mut self) {
160        self.e1.reset();
161        self.e2.reset();
162    }
163
164    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
165        let e1 = match self.e1.push(close)? {
166            Some(v) => v,
167            None => return Ok(None),
168        };
169        match self.e2.push(e1)? {
170            Some(e2) => Ok(Some(2.0 * e1 - e2)),
171            None => Ok(None),
172        }
173    }
174
175    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
176        let mut out = Vec::with_capacity(closes.len());
177        for &c in closes {
178            out.push(self.push(c)?);
179        }
180        Ok(out)
181    }
182
183    pub fn last(&self) -> Option<f64> {
184        match (self.e1.last(), self.e2.last()) {
185            (Some(e1), Some(e2)) => Some(2.0 * e1 - e2),
186            _ => None,
187        }
188    }
189}
190
191/// DEMA of `period` closes.
192pub fn dema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
193    validate_series("close", closes)?;
194    let mut st = DemaState::new(period)?;
195    st.push_bars(closes)
196}
197
198pub fn dema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
199    let mut st = DemaState::new(period)?;
200    for &c in closes {
201        st.push(c)?;
202    }
203    Ok(st.last())
204}
205
206// ---------------------------------------------------------------------------
207// TEMA
208// ---------------------------------------------------------------------------
209
210/// Triple exponential moving average: `3·e1 − 3·e2 + e3`.
211///
212/// Further lag reduction vs DEMA; longest warm-up of the EMA-family stack here.
213#[derive(Clone, Debug)]
214pub struct TemaState {
215    e1: EmaState,
216    e2: EmaState,
217    e3: EmaState,
218}
219
220impl TemaState {
221    pub fn new(period: usize) -> FinanceResult<Self> {
222        Ok(Self {
223            e1: EmaState::new(period)?,
224            e2: EmaState::new(period)?,
225            e3: EmaState::new(period)?,
226        })
227    }
228
229    pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
230        let mut s = Self::new(period)?;
231        s.push_bars(closes)?;
232        Ok(s)
233    }
234
235    pub fn period(&self) -> usize {
236        self.e1.period()
237    }
238
239    pub fn reset(&mut self) {
240        self.e1.reset();
241        self.e2.reset();
242        self.e3.reset();
243    }
244
245    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
246        let e1 = match self.e1.push(close)? {
247            Some(v) => v,
248            None => return Ok(None),
249        };
250        let e2 = match self.e2.push(e1)? {
251            Some(v) => v,
252            None => return Ok(None),
253        };
254        match self.e3.push(e2)? {
255            Some(e3) => Ok(Some(3.0 * e1 - 3.0 * e2 + e3)),
256            None => Ok(None),
257        }
258    }
259
260    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
261        let mut out = Vec::with_capacity(closes.len());
262        for &c in closes {
263            out.push(self.push(c)?);
264        }
265        Ok(out)
266    }
267
268    pub fn last(&self) -> Option<f64> {
269        match (self.e1.last(), self.e2.last(), self.e3.last()) {
270            (Some(e1), Some(e2), Some(e3)) => Some(3.0 * e1 - 3.0 * e2 + e3),
271            _ => None,
272        }
273    }
274}
275
276/// TEMA of `period` closes.
277pub fn tema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
278    validate_series("close", closes)?;
279    let mut st = TemaState::new(period)?;
280    st.push_bars(closes)
281}
282
283pub fn tema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
284    let mut st = TemaState::new(period)?;
285    for &c in closes {
286        st.push(c)?;
287    }
288    Ok(st.last())
289}
290
291// ---------------------------------------------------------------------------
292// KAMA
293// ---------------------------------------------------------------------------
294
295/// Kaufman Adaptive Moving Average parameters.
296///
297/// Efficiency ratio over `period` maps between `fast` and `slow` EMA-style smoothing
298/// constants. Classic desk pack: [`KamaParams::standard`] `(10, 2, 30)`.
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
300pub struct KamaParams {
301    /// Efficiency-ratio lookback (typically 10).
302    pub period: usize,
303    /// Fast EMA-equivalent period (typically 2).
304    pub fast: usize,
305    /// Slow EMA-equivalent period (typically 30).
306    pub slow: usize,
307}
308
309impl KamaParams {
310    pub const fn new(period: usize, fast: usize, slow: usize) -> Self {
311        Self { period, fast, slow }
312    }
313
314    /// Classic `(10, 2, 30)`.
315    pub const fn standard() -> Self {
316        Self {
317            period: 10,
318            fast: 2,
319            slow: 30,
320        }
321    }
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
325pub struct ValidatedKama {
326    params: KamaParams,
327}
328
329impl ValidatedKama {
330    pub fn new(params: KamaParams) -> FinanceResult<Self> {
331        PeriodLength::new(params.period)?;
332        PeriodLength::new(params.fast)?;
333        PeriodLength::new(params.slow)?;
334        if params.fast >= params.slow {
335            return Err(FinanceError::Unsolvable {
336                message: "KAMA requires fast < slow",
337            });
338        }
339        Ok(Self { params })
340    }
341
342    pub fn params(self) -> KamaParams {
343        self.params
344    }
345
346    pub fn compute(self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
347        kama(closes, self.params)
348    }
349}
350
351/// Incremental KAMA.
352///
353/// First defined bar seeds KAMA to the current price; thereafter  
354/// `KAMA += SC²-scaled step toward price` with SC from the efficiency ratio.
355#[derive(Clone, Debug)]
356pub struct KamaState {
357    params: KamaParams,
358    fast_sc: f64,
359    slow_sc: f64,
360    /// Rolling window of `period + 1` closes for ER (change over `period` steps).
361    closes: RingF64,
362    kama: Option<f64>,
363    last: Option<f64>,
364}
365
366impl KamaState {
367    pub fn new(params: KamaParams) -> FinanceResult<Self> {
368        let _ = ValidatedKama::new(params)?;
369        let fast_sc = 2.0 / (params.fast as f64 + 1.0);
370        let slow_sc = 2.0 / (params.slow as f64 + 1.0);
371        Ok(Self {
372            params,
373            fast_sc,
374            slow_sc,
375            closes: RingF64::with_capacity(params.period + 1),
376            kama: None,
377            last: None,
378        })
379    }
380
381    pub fn from_history(params: KamaParams, closes: &[f64]) -> FinanceResult<Self> {
382        let mut s = Self::new(params)?;
383        s.push_bars(closes)?;
384        Ok(s)
385    }
386
387    pub fn params(&self) -> KamaParams {
388        self.params
389    }
390
391    pub fn reset(&mut self) {
392        self.closes.clear();
393        self.kama = None;
394        self.last = None;
395    }
396
397    pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
398        require_finite("close", close)?;
399        let _ = self.closes.push(close);
400        // Need period+1 samples for ER over `period` steps
401        if self.closes.len() < self.params.period + 1 {
402            self.last = None;
403            return Ok(None);
404        }
405        let mut ordered = Vec::with_capacity(self.params.period + 1);
406        self.closes.copy_ordered(&mut ordered);
407        let n = ordered.len();
408        let change = (ordered[n - 1] - ordered[0]).abs();
409        let mut volatility = 0.0;
410        for i in 1..n {
411            volatility += (ordered[i] - ordered[i - 1]).abs();
412        }
413        let er = if volatility > 0.0 {
414            change / volatility
415        } else {
416            0.0
417        };
418        let sc = (er * (self.fast_sc - self.slow_sc) + self.slow_sc).powi(2);
419        let kama = match self.kama {
420            None => close, // first KAMA = price when ER window first fills
421            Some(prev) => prev + sc * (close - prev),
422        };
423        self.kama = Some(kama);
424        self.last = Some(kama);
425        Ok(Some(kama))
426    }
427
428    pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
429        validate_series("close", closes)?;
430        let mut out = Vec::with_capacity(closes.len());
431        for &c in closes {
432            out.push(self.push(c)?);
433        }
434        Ok(out)
435    }
436
437    pub fn last(&self) -> Option<f64> {
438        self.last
439    }
440}
441
442/// Kaufman adaptive moving average.
443pub fn kama(closes: &[f64], params: KamaParams) -> FinanceResult<Vec<Option<f64>>> {
444    let mut st = KamaState::new(params)?;
445    st.push_bars(closes)
446}
447
448pub fn kama_last(closes: &[f64], params: KamaParams) -> FinanceResult<Option<f64>> {
449    let mut st = KamaState::new(params)?;
450    for &c in closes {
451        st.push(c)?;
452    }
453    Ok(st.last())
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::stocks::ta::moving_average::ema;
460
461    #[test]
462    fn rma_matches_wilder_seed() {
463        let c: Vec<_> = (1..=20).map(|x| x as f64).collect();
464        let r = rma(&c, 5).unwrap();
465        // First RMA at index 4 = SMA(1..5)=3
466        assert!((r[4].unwrap() - 3.0).abs() < 1e-12);
467        // Next: (3*4 + 6)/5 = 3.6
468        assert!((r[5].unwrap() - 3.6).abs() < 1e-12);
469    }
470
471    #[test]
472    fn dema_related_to_ema() {
473        let c: Vec<_> = (1..=40).map(|x| x as f64).collect();
474        let d = dema(&c, 5).unwrap();
475        let e1 = ema(&c, 5).unwrap();
476        // DEMA should be defined later than EMA
477        let first_e = e1.iter().position(|x| x.is_some()).unwrap();
478        let first_d = d.iter().position(|x| x.is_some()).unwrap();
479        assert!(first_d >= first_e);
480        assert!(d.last().unwrap().is_some());
481    }
482
483    #[test]
484    fn tema_runs() {
485        let c: Vec<_> = (1..=50).map(|x| 100.0 + x as f64 * 0.1).collect();
486        let t = tema(&c, 5).unwrap();
487        assert!(t.iter().filter(|x| x.is_some()).count() > 10);
488    }
489
490    #[test]
491    fn kama_standard_runs() {
492        let c: Vec<_> = (0..40).map(|i| 100.0 + (i as f64).sin() * 2.0).collect();
493        let k = kama(&c, KamaParams::standard()).unwrap();
494        assert!(k[9].is_none() || k[10].is_some()); // first at period (index period)
495        assert!(k.last().unwrap().is_some());
496    }
497
498    #[test]
499    fn dema_tema_state_parity() {
500        let c: Vec<_> = (0..45).map(|i| 50.0 + i as f64 * 0.25).collect();
501        for (batch, mut st) in [(dema(&c, 8).unwrap(), DemaState::new(8).unwrap())] {
502            for i in 0..c.len() {
503                let o = st.push(c[i]).unwrap();
504                match (o, batch[i]) {
505                    (None, None) => {}
506                    (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
507                    other => panic!("{other:?}"),
508                }
509            }
510        }
511        let batch = tema(&c, 6).unwrap();
512        let mut st = TemaState::new(6).unwrap();
513        for i in 0..c.len() {
514            let o = st.push(c[i]).unwrap();
515            match (o, batch[i]) {
516                (None, None) => {}
517                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
518                other => panic!("{other:?}"),
519            }
520        }
521        let batch = rma(&c, 7).unwrap();
522        let mut st = RmaState::new(7).unwrap();
523        for i in 0..c.len() {
524            let o = st.push(c[i]).unwrap();
525            match (o, batch[i]) {
526                (None, None) => {}
527                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
528                other => panic!("{other:?}"),
529            }
530        }
531        let p = KamaParams::standard();
532        let batch = kama(&c, p).unwrap();
533        let mut st = KamaState::new(p).unwrap();
534        for i in 0..c.len() {
535            let o = st.push(c[i]).unwrap();
536            match (o, batch[i]) {
537                (None, None) => {}
538                (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
539                other => panic!("{other:?}"),
540            }
541        }
542    }
543
544    #[test]
545    fn kama_fast_ge_slow_err() {
546        assert!(ValidatedKama::new(KamaParams::new(10, 30, 2)).is_err());
547    }
548}