Skip to main content

wickra_core/indicators/
awesome_oscillator.rs

1//! Awesome Oscillator (Bill Williams).
2
3use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// Awesome Oscillator: `SMA(median_price, 5) - SMA(median_price, 34)`.
9///
10/// # Example
11///
12/// ```
13/// use wickra_core::{Candle, Indicator, AwesomeOscillator};
14///
15/// let mut indicator = AwesomeOscillator::new(3, 10).unwrap();
16/// let mut last = None;
17/// for i in 0..80 {
18///     let base = 100.0 + f64::from(i);
19///     let candle =
20///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
21///     last = indicator.update(candle);
22/// }
23/// assert!(last.is_some());
24/// ```
25#[derive(Debug, Clone)]
26pub struct AwesomeOscillator {
27    fast: Sma,
28    slow: Sma,
29    fast_period: usize,
30    slow_period: usize,
31}
32
33impl AwesomeOscillator {
34    /// # Errors
35    /// Returns [`Error::PeriodZero`] for zero periods or [`Error::InvalidPeriod`] when fast >= slow.
36    pub fn new(fast: usize, slow: usize) -> Result<Self> {
37        if fast == 0 || slow == 0 {
38            return Err(Error::PeriodZero);
39        }
40        if fast >= slow {
41            return Err(Error::InvalidPeriod {
42                message: "AO fast period must be strictly less than slow",
43            });
44        }
45        Ok(Self {
46            fast: Sma::new(fast)?,
47            slow: Sma::new(slow)?,
48            fast_period: fast,
49            slow_period: slow,
50        })
51    }
52
53    /// Classic Bill Williams configuration: (5, 34).
54    pub fn classic() -> Self {
55        Self::new(5, 34).expect("classic AO periods are valid")
56    }
57
58    /// Configured `(fast, slow)` periods.
59    pub const fn periods(&self) -> (usize, usize) {
60        (self.fast_period, self.slow_period)
61    }
62}
63
64impl Indicator for AwesomeOscillator {
65    type Input = Candle;
66    type Output = f64;
67
68    fn update(&mut self, candle: Candle) -> Option<f64> {
69        let median = candle.median_price();
70        let f = self.fast.update(median);
71        let s = self.slow.update(median);
72        match (f, s) {
73            (Some(a), Some(b)) => Some(a - b),
74            _ => None,
75        }
76    }
77
78    fn reset(&mut self) {
79        self.fast.reset();
80        self.slow.reset();
81    }
82
83    fn warmup_period(&self) -> usize {
84        self.slow_period
85    }
86
87    fn is_ready(&self) -> bool {
88        self.slow.is_ready()
89    }
90
91    fn name(&self) -> &'static str {
92        "AwesomeOscillator"
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::traits::BatchExt;
100    use approx::assert_relative_eq;
101
102    fn c(h: f64, l: f64, cl: f64) -> Candle {
103        Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
104    }
105
106    #[test]
107    fn constant_series_yields_zero() {
108        let candles: Vec<Candle> = (0..80).map(|_| c(11.0, 9.0, 10.0)).collect();
109        let mut ao = AwesomeOscillator::classic();
110        let last = ao.batch(&candles).into_iter().flatten().last().unwrap();
111        assert_relative_eq!(last, 0.0, epsilon = 1e-9);
112    }
113
114    #[test]
115    fn rejects_fast_geq_slow() {
116        assert!(AwesomeOscillator::new(34, 5).is_err());
117        assert!(AwesomeOscillator::new(5, 5).is_err());
118        assert!(AwesomeOscillator::new(0, 5).is_err());
119    }
120
121    /// Cover the const accessor `periods` (59-61) and the Indicator-impl
122    /// `warmup_period` (83-85) + `name` (91-93). Existing tests never
123    /// inspect these metadata methods.
124    #[test]
125    fn accessors_and_metadata() {
126        let ao = AwesomeOscillator::classic();
127        assert_eq!(ao.periods(), (5, 34));
128        assert_eq!(ao.warmup_period(), 34);
129        assert_eq!(ao.name(), "AwesomeOscillator");
130    }
131
132    #[test]
133    fn batch_equals_streaming() {
134        let candles: Vec<Candle> = (0..50)
135            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
136            .collect();
137        let mut a = AwesomeOscillator::classic();
138        let mut b = AwesomeOscillator::classic();
139        assert_eq!(
140            a.batch(&candles),
141            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
142        );
143    }
144
145    #[test]
146    fn reset_clears_state() {
147        let candles: Vec<Candle> = (0..50)
148            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
149            .collect();
150        let mut ao = AwesomeOscillator::classic();
151        ao.batch(&candles);
152        assert!(ao.is_ready());
153        ao.reset();
154        assert!(!ao.is_ready());
155        assert_eq!(ao.update(candles[0]), None);
156    }
157}