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    #[inline]
69    fn update(&mut self, candle: Candle) -> Option<f64> {
70        let median = candle.median_price();
71        let f = self.fast.update(median);
72        let s = self.slow.update(median);
73        match (f, s) {
74            (Some(a), Some(b)) => Some(a - b),
75            _ => None,
76        }
77    }
78
79    fn reset(&mut self) {
80        self.fast.reset();
81        self.slow.reset();
82    }
83
84    #[inline]
85    fn warmup_period(&self) -> usize {
86        self.slow_period
87    }
88
89    #[inline]
90    fn is_ready(&self) -> bool {
91        self.slow.is_ready()
92    }
93
94    #[inline]
95    fn name(&self) -> &'static str {
96        "AwesomeOscillator"
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::traits::BatchExt;
104    use approx::assert_relative_eq;
105
106    fn c(h: f64, l: f64, cl: f64) -> Candle {
107        Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
108    }
109
110    #[test]
111    fn constant_series_yields_zero() {
112        let candles: Vec<Candle> = (0..80).map(|_| c(11.0, 9.0, 10.0)).collect();
113        let mut ao = AwesomeOscillator::classic();
114        let last = ao.batch(&candles).into_iter().flatten().last().unwrap();
115        assert_relative_eq!(last, 0.0, epsilon = 1e-9);
116    }
117
118    #[test]
119    fn rejects_fast_geq_slow() {
120        assert!(AwesomeOscillator::new(34, 5).is_err());
121        assert!(AwesomeOscillator::new(5, 5).is_err());
122        assert!(AwesomeOscillator::new(0, 5).is_err());
123    }
124
125    /// Cover the const accessor `periods` (59-61) and the Indicator-impl
126    /// `warmup_period` (83-85) + `name` (91-93). Existing tests never
127    /// inspect these metadata methods.
128    #[test]
129    fn accessors_and_metadata() {
130        let ao = AwesomeOscillator::classic();
131        assert_eq!(ao.periods(), (5, 34));
132        assert_eq!(ao.warmup_period(), 34);
133        assert_eq!(ao.name(), "AwesomeOscillator");
134    }
135
136    #[test]
137    fn batch_equals_streaming() {
138        let candles: Vec<Candle> = (0..50)
139            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
140            .collect();
141        let mut a = AwesomeOscillator::classic();
142        let mut b = AwesomeOscillator::classic();
143        assert_eq!(
144            a.batch(&candles),
145            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
146        );
147    }
148
149    #[test]
150    fn reset_clears_state() {
151        let candles: Vec<Candle> = (0..50)
152            .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
153            .collect();
154        let mut ao = AwesomeOscillator::classic();
155        ao.batch(&candles);
156        assert!(ao.is_ready());
157        ao.reset();
158        assert!(!ao.is_ready());
159        assert_eq!(ao.update(candles[0]), None);
160    }
161}