use crate::error::{Error, Result};
use crate::indicators::awesome_oscillator::AwesomeOscillator;
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct AwesomeOscillatorHistogram {
fast_period: usize,
slow_period: usize,
sma_period: usize,
ao: AwesomeOscillator,
sma: Sma,
}
impl AwesomeOscillatorHistogram {
pub fn new(fast: usize, slow: usize, sma_period: usize) -> Result<Self> {
if fast == 0 || slow == 0 || sma_period == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
return Err(Error::InvalidPeriod {
message: "AwesomeOscillatorHistogram fast must be strictly less than slow",
});
}
Ok(Self {
fast_period: fast,
slow_period: slow,
sma_period,
ao: AwesomeOscillator::new(fast, slow)?,
sma: Sma::new(sma_period)?,
})
}
pub fn classic() -> Self {
Self::new(5, 34, 5).expect("classic Awesome Oscillator Histogram parameters are valid")
}
pub const fn periods(&self) -> (usize, usize, usize) {
(self.fast_period, self.slow_period, self.sma_period)
}
}
impl Indicator for AwesomeOscillatorHistogram {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let ao = self.ao.update(candle)?;
let sma = self.sma.update(ao)?;
Some(ao - sma)
}
fn reset(&mut self) {
self.ao.reset();
self.sma.reset();
}
fn warmup_period(&self) -> usize {
self.slow_period + self.sma_period - 1
}
fn is_ready(&self) -> bool {
self.sma.is_ready()
}
fn name(&self) -> &'static str {
"AwesomeOscillatorHistogram"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn candle(price: f64, ts: i64) -> Candle {
Candle::new(price, price + 0.5, price - 0.5, price, 1.0, ts).unwrap()
}
#[test]
fn rejects_zero_period() {
assert!(matches!(
AwesomeOscillatorHistogram::new(0, 34, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
AwesomeOscillatorHistogram::new(5, 0, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
AwesomeOscillatorHistogram::new(5, 34, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn rejects_fast_geq_slow() {
assert!(matches!(
AwesomeOscillatorHistogram::new(34, 5, 5),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let hist = AwesomeOscillatorHistogram::classic();
assert_eq!(hist.periods(), (5, 34, 5));
assert_eq!(hist.warmup_period(), 38);
assert_eq!(hist.name(), "AwesomeOscillatorHistogram");
}
#[test]
fn constant_series_converges_to_zero() {
let mut hist = AwesomeOscillatorHistogram::new(3, 5, 3).unwrap();
let candles: Vec<Candle> = (0..30).map(|i| candle(42.0, i)).collect();
let out = hist.batch(&candles);
for v in out.iter().skip(hist.warmup_period() - 1).flatten() {
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut hist = AwesomeOscillatorHistogram::new(2, 4, 3).unwrap();
assert_eq!(hist.warmup_period(), 6);
let candles: Vec<Candle> = (0..8)
.map(|i| candle(10.0 + f64::from(i), i64::from(i)))
.collect();
let out = hist.batch(&candles);
for v in out.iter().take(5) {
assert!(v.is_none());
}
assert!(out[5].is_some());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..100_i64)
.map(|i| candle(100.0 + (i as f64 * 0.3).sin() * 5.0, i))
.collect();
let batch = AwesomeOscillatorHistogram::classic().batch(&candles);
let mut b = AwesomeOscillatorHistogram::classic();
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn reset_clears_state() {
let mut hist = AwesomeOscillatorHistogram::classic();
let candles: Vec<Candle> = (0..80)
.map(|i| candle(10.0 + f64::from(i), i64::from(i)))
.collect();
hist.batch(&candles);
assert!(hist.is_ready());
hist.reset();
assert!(!hist.is_ready());
}
}