wickra_core/indicators/
awesome_oscillator.rs1use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[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 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 pub fn classic() -> Self {
55 Self::new(5, 34).expect("classic AO periods are valid")
56 }
57
58 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 #[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}