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    fn update(&mut self, input: f64) -> Option<f64> {
71        let v = self.inner.update(input)?.fama;
72        self.last_value = Some(v);
73        Some(v)
74    }
75
76    fn reset(&mut self) {
77        self.inner.reset();
78        self.last_value = None;
79    }
80
81    fn warmup_period(&self) -> usize {
82        self.inner.warmup_period()
83    }
84
85    fn is_ready(&self) -> bool {
86        self.last_value.is_some()
87    }
88
89    fn name(&self) -> &'static str {
90        "FAMA"
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::error::Error;
98    use crate::traits::BatchExt;
99
100    #[test]
101    fn rejects_invalid_limits() {
102        assert!(matches!(
103            Fama::new(0.0, 0.05),
104            Err(Error::InvalidPeriod { .. })
105        ));
106        assert!(matches!(
107            Fama::new(0.05, 0.5),
108            Err(Error::InvalidPeriod { .. })
109        ));
110    }
111
112    #[test]
113    fn new_with_valid_limits_constructs_via_mama() {
114        // `classic()` bypasses `new` by going through `Mama::classic`; this
115        // test exercises the happy-path `Ok(Self { inner: Mama::new(..)? })`
116        // arm so the `?` doesn't only collapse to the error path.
117        let mut fama = Fama::new(0.5, 0.05).expect("valid Mama limits");
118        assert_eq!(fama.limits(), (0.5, 0.05));
119        for i in 0..60 {
120            fama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
121        }
122        assert!(fama.value().is_some());
123    }
124
125    #[test]
126    fn accessors_and_metadata() {
127        let mut fama = Fama::classic();
128        assert_eq!(fama.limits(), (0.5, 0.05));
129        assert_eq!(fama.warmup_period(), 33);
130        assert_eq!(fama.name(), "FAMA");
131        assert!(!fama.is_ready());
132        for i in 0..60 {
133            fama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
134        }
135        assert!(fama.is_ready());
136        assert!(fama.value().is_some());
137    }
138
139    #[test]
140    fn batch_equals_streaming() {
141        let prices: Vec<f64> = (0..120)
142            .map(|i| 100.0 + (f64::from(i) * 0.25).cos() * 5.0)
143            .collect();
144        let mut a = Fama::classic();
145        let mut b = Fama::classic();
146        let batch = a.batch(&prices);
147        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
148        assert_eq!(batch, streamed);
149    }
150
151    #[test]
152    fn ignores_non_finite_input() {
153        let mut fama = Fama::classic();
154        let prices: Vec<f64> = (0..100)
155            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
156            .collect();
157        fama.batch(&prices);
158        let before = fama.value();
159        assert!(before.is_some());
160        assert_eq!(fama.update(f64::NAN), before);
161    }
162
163    #[test]
164    fn reset_clears_state() {
165        let mut fama = Fama::classic();
166        let prices: Vec<f64> = (0..100)
167            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
168            .collect();
169        fama.batch(&prices);
170        assert!(fama.is_ready());
171        fama.reset();
172        assert!(!fama.is_ready());
173    }
174}