Skip to main content

wickra_core/indicators/
fama.rs

1//! Ehlers Following Adaptive Moving Average (FAMA).
2
3use crate::error::Result;
4use crate::indicators::mama::Mama;
5use crate::traits::Indicator;
6
7/// Scalar wrapper that exposes only the FAMA line from a [`Mama`] indicator.
8///
9/// FAMA (Following Adaptive Moving Average) is MAMA's lagging companion in
10/// Ehlers' MESA construction. It uses half MAMA's adaptive alpha, so it
11/// reacts later than MAMA — MAMA crossing above FAMA marks a trend
12/// confirmation, MAMA below FAMA a reversal. See [`Mama`] for the joint
13/// `(mama, fama)` output; this wrapper exposes the slow line as a plain
14/// scalar indicator so it can be chained directly.
15///
16/// # Example
17///
18/// ```
19/// use wickra_core::{Indicator, Fama};
20///
21/// let mut fama = Fama::new(0.5, 0.05).unwrap();
22/// let mut last = None;
23/// for i in 0..80 {
24///     last = fama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
25/// }
26/// assert!(last.is_some());
27/// ```
28#[derive(Debug, Clone)]
29pub struct Fama {
30    inner: Mama,
31    last_value: Option<f64>,
32}
33
34impl Fama {
35    /// Construct with the same `(fast_limit, slow_limit)` semantics as [`Mama`].
36    ///
37    /// # Errors
38    ///
39    /// Forwards [`Mama::new`]'s validation errors.
40    pub fn new(fast_limit: f64, slow_limit: f64) -> Result<Self> {
41        Ok(Self {
42            inner: Mama::new(fast_limit, slow_limit)?,
43            last_value: None,
44        })
45    }
46
47    /// Default `(0.5, 0.05)` parameters.
48    pub fn classic() -> Self {
49        Self {
50            inner: Mama::classic(),
51            last_value: None,
52        }
53    }
54
55    /// Configured `(fast_limit, slow_limit)`.
56    pub const fn limits(&self) -> (f64, f64) {
57        self.inner.limits()
58    }
59
60    /// Current FAMA value if available.
61    pub const fn value(&self) -> Option<f64> {
62        self.last_value
63    }
64}
65
66impl Indicator for Fama {
67    type Input = f64;
68    type Output = f64;
69
70    #[inline]
71    fn update(&mut self, input: f64) -> Option<f64> {
72        let v = self.inner.update(input)?.fama;
73        self.last_value = Some(v);
74        Some(v)
75    }
76
77    fn reset(&mut self) {
78        self.inner.reset();
79        self.last_value = None;
80    }
81
82    #[inline]
83    fn warmup_period(&self) -> usize {
84        self.inner.warmup_period()
85    }
86
87    #[inline]
88    fn is_ready(&self) -> bool {
89        self.last_value.is_some()
90    }
91
92    #[inline]
93    fn name(&self) -> &'static str {
94        "FAMA"
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::error::Error;
102    use crate::traits::BatchExt;
103
104    #[test]
105    fn rejects_invalid_limits() {
106        assert!(matches!(
107            Fama::new(0.0, 0.05),
108            Err(Error::InvalidPeriod { .. })
109        ));
110        assert!(matches!(
111            Fama::new(0.05, 0.5),
112            Err(Error::InvalidPeriod { .. })
113        ));
114    }
115
116    #[test]
117    fn new_with_valid_limits_constructs_via_mama() {
118        // `classic()` bypasses `new` by going through `Mama::classic`; this
119        // test exercises the happy-path `Ok(Self { inner: Mama::new(..)? })`
120        // arm so the `?` doesn't only collapse to the error path.
121        let mut fama = Fama::new(0.5, 0.05).expect("valid Mama limits");
122        assert_eq!(fama.limits(), (0.5, 0.05));
123        for i in 0..60 {
124            fama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
125        }
126        assert!(fama.value().is_some());
127    }
128
129    #[test]
130    fn accessors_and_metadata() {
131        let mut fama = Fama::classic();
132        assert_eq!(fama.limits(), (0.5, 0.05));
133        assert_eq!(fama.warmup_period(), 33);
134        assert_eq!(fama.name(), "FAMA");
135        assert!(!fama.is_ready());
136        for i in 0..60 {
137            fama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
138        }
139        assert!(fama.is_ready());
140        assert!(fama.value().is_some());
141    }
142
143    #[test]
144    fn batch_equals_streaming() {
145        let prices: Vec<f64> = (0..120)
146            .map(|i| 100.0 + (f64::from(i) * 0.25).cos() * 5.0)
147            .collect();
148        let mut a = Fama::classic();
149        let mut b = Fama::classic();
150        let batch = a.batch(&prices);
151        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
152        assert_eq!(batch, streamed);
153    }
154
155    #[test]
156    fn ignores_non_finite_input() {
157        let mut fama = Fama::classic();
158        let prices: Vec<f64> = (0..100)
159            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
160            .collect();
161        fama.batch(&prices);
162        let before = fama.value();
163        assert!(before.is_some());
164        assert_eq!(fama.update(f64::NAN), None);
165    }
166
167    #[test]
168    fn reset_clears_state() {
169        let mut fama = Fama::classic();
170        let prices: Vec<f64> = (0..100)
171            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
172            .collect();
173        fama.batch(&prices);
174        assert!(fama.is_ready());
175        fama.reset();
176        assert!(!fama.is_ready());
177    }
178}