Skip to main content

quantwave_core/indicators/incremental/
apo.rs

1//! Native O(1) APO and PPO — TA-Lib parity.
2
3use crate::indicators::incremental::ma_stream::MaStream;
4use crate::indicators::ma_type::MaType;
5use crate::traits::Next;
6
7/// Absolute Price Oscillator.
8#[derive(Debug, Clone)]
9#[allow(non_camel_case_types)]
10pub struct APO {
11    pub fastperiod: usize,
12    pub slowperiod: usize,
13    pub matype: MaType,
14    fast_ma: MaStream,
15    slow_ma: MaStream,
16}
17
18impl APO {
19    pub fn new(fastperiod: usize, slowperiod: usize, matype: MaType) -> Self {
20        Self {
21            fastperiod,
22            slowperiod,
23            matype,
24            fast_ma: MaStream::new(fastperiod, matype),
25            slow_ma: MaStream::new(slowperiod, matype),
26        }
27    }
28}
29
30impl Next<f64> for APO {
31    type Output = f64;
32
33    fn next(&mut self, input: f64) -> Self::Output {
34        let fast = self.fast_ma.next(input);
35        let slow = self.slow_ma.next(input);
36        if fast.is_nan() || slow.is_nan() {
37            f64::NAN
38        } else {
39            fast - slow
40        }
41    }
42}
43
44/// Percentage Price Oscillator.
45#[derive(Debug, Clone)]
46#[allow(non_camel_case_types)]
47pub struct PPO {
48    pub fastperiod: usize,
49    pub slowperiod: usize,
50    pub matype: MaType,
51    fast_ma: MaStream,
52    slow_ma: MaStream,
53}
54
55impl PPO {
56    pub fn new(fastperiod: usize, slowperiod: usize, matype: MaType) -> Self {
57        Self {
58            fastperiod,
59            slowperiod,
60            matype,
61            fast_ma: MaStream::new(fastperiod, matype),
62            slow_ma: MaStream::new(slowperiod, matype),
63        }
64    }
65}
66
67impl Next<f64> for PPO {
68    type Output = f64;
69
70    fn next(&mut self, input: f64) -> Self::Output {
71        let fast = self.fast_ma.next(input);
72        let slow = self.slow_ma.next(input);
73        if fast.is_nan() || slow.is_nan() || slow == 0.0 {
74            f64::NAN
75        } else {
76            (fast - slow) / slow * 100.0
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use proptest::prelude::*;
85
86    fn assert_parity(streaming: &[f64], batch: &[f64], label: &str) {
87        for (i, (s, b)) in streaming.iter().zip(batch.iter()).enumerate() {
88            if s.is_nan() {
89                assert!(b.is_nan(), "{label} bar {i}: streaming NaN, batch {b}");
90            } else {
91                assert!(!b.is_nan(), "{label} bar {i}: streaming {s}, batch NaN");
92                approx::assert_relative_eq!(s, b, epsilon = 1e-6);
93            }
94        }
95    }
96
97    proptest! {
98        #[test]
99        fn test_apo_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
100            let fast = 12;
101            let slow = 26;
102            let matype = MaType::Ema;
103            let mut apo = APO::new(fast, slow, matype);
104            let streaming: Vec<f64> = input.iter().map(|&x| apo.next(x)).collect();
105            let batch = talib_rs::momentum::apo(&input, fast, slow, matype.into())
106                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
107            assert_parity(&streaming, &batch, "apo/ema");
108        }
109
110        #[test]
111        fn test_ppo_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
112            let fast = 12;
113            let slow = 26;
114            let matype = MaType::Ema;
115            let mut ppo = PPO::new(fast, slow, matype);
116            let streaming: Vec<f64> = input.iter().map(|&x| ppo.next(x)).collect();
117            let batch = talib_rs::momentum::ppo(&input, fast, slow, matype.into())
118                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
119            assert_parity(&streaming, &batch, "ppo/ema");
120        }
121
122        /// APO/PPO route both legs through `MaStream`, so they inherited the
123        /// silent SMA substitution for every matype except EMA. Cover all nine.
124        #[test]
125        fn test_apo_ppo_parity_all_matypes(
126            input in prop::collection::vec(0.1..100.0, 120..200),
127            idx in 0usize..9,
128        ) {
129            let matype = MaType::ALL[idx];
130            let (fast, slow) = (12, 26);
131
132            let mut apo = APO::new(fast, slow, matype);
133            let s_apo: Vec<f64> = input.iter().map(|&x| apo.next(x)).collect();
134            let b_apo = talib_rs::momentum::apo(&input, fast, slow, matype.into())
135                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
136            assert_parity(&s_apo, &b_apo, &format!("apo/{matype}"));
137
138            let mut ppo = PPO::new(fast, slow, matype);
139            let s_ppo: Vec<f64> = input.iter().map(|&x| ppo.next(x)).collect();
140            let b_ppo = talib_rs::momentum::ppo(&input, fast, slow, matype.into())
141                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
142            assert_parity(&s_ppo, &b_ppo, &format!("ppo/{matype}"));
143        }
144    }
145}