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::traits::Next;
5use talib_rs::MaType;
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    proptest! {
87        #[test]
88        fn test_apo_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
89            let fast = 12;
90            let slow = 26;
91            let matype = MaType::Ema;
92            let mut apo = APO::new(fast, slow, matype);
93            let streaming: Vec<f64> = input.iter().map(|&x| apo.next(x)).collect();
94            let batch = talib_rs::momentum::apo(&input, fast, slow, matype)
95                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
96            for (s, b) in streaming.iter().zip(batch.iter()) {
97                if s.is_nan() { assert!(b.is_nan()); }
98                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
99            }
100        }
101
102        #[test]
103        fn test_ppo_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
104            let fast = 12;
105            let slow = 26;
106            let matype = MaType::Ema;
107            let mut ppo = PPO::new(fast, slow, matype);
108            let streaming: Vec<f64> = input.iter().map(|&x| ppo.next(x)).collect();
109            let batch = talib_rs::momentum::ppo(&input, fast, slow, matype)
110                .unwrap_or_else(|_| vec![f64::NAN; input.len()]);
111            for (s, b) in streaming.iter().zip(batch.iter()) {
112                if s.is_nan() { assert!(b.is_nan()); }
113                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
114            }
115        }
116    }
117}