Skip to main content

quantwave_core/indicators/
price_transform.rs

1use crate::traits::Next;
2
3talib_4_in_1_out!(AVGPRICE, talib_rs::price_transform::avgprice);
4impl Default for AVGPRICE {
5    fn default() -> Self {
6        Self::new()
7    }
8}
9talib_2_in_1_out!(MEDPRICE, talib_rs::price_transform::medprice);
10impl Default for MEDPRICE {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15talib_3_in_1_out!(TYPPRICE, talib_rs::price_transform::typprice);
16impl Default for TYPPRICE {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21talib_3_in_1_out!(WCLPRICE, talib_rs::price_transform::wclprice);
22impl Default for WCLPRICE {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28/// (Open + Close) / 2
29/// 
30/// Based on John Ehlers' "Every Little Bit Helps" (2023).
31/// Used to reduce noise in technical indicators by averaging the open and close.
32#[derive(Debug, Clone, Default)]
33pub struct OC2;
34
35impl OC2 {
36    pub fn new() -> Self {
37        Self
38    }
39}
40
41impl Next<(f64, f64)> for OC2 {
42    type Output = f64;
43    fn next(&mut self, input: (f64, f64)) -> Self::Output {
44        (input.0 + input.1) / 2.0
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::traits::Next;
52    use proptest::prelude::*;
53
54    proptest! {
55        #[test]
56        fn test_avgprice_parity(
57            o in prop::collection::vec(0.1..100.0, 1..100),
58            h in prop::collection::vec(0.1..100.0, 1..100),
59            l in prop::collection::vec(0.1..100.0, 1..100),
60            c in prop::collection::vec(0.1..100.0, 1..100)
61        ) {
62            let len = o.len().min(h.len()).min(l.len()).min(c.len());
63            if len == 0 { return Ok(()); }
64
65            let mut avgprice = AVGPRICE::new();
66            let streaming_results: Vec<f64> = (0..len).map(|i| avgprice.next((o[i], h[i], l[i], c[i]))).collect();
67            let batch_results = talib_rs::price_transform::avgprice(&o[..len], &h[..len], &l[..len], &c[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
68
69            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
70                if s.is_nan() {
71                    assert!(b.is_nan());
72                } else {
73                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
74                }
75            }
76        }
77
78        #[test]
79        fn test_medprice_parity(
80            h in prop::collection::vec(0.1..100.0, 1..100),
81            l in prop::collection::vec(0.1..100.0, 1..100)
82        ) {
83            let len = h.len().min(l.len());
84            if len == 0 { return Ok(()); }
85
86            let mut medprice = MEDPRICE::new();
87            let streaming_results: Vec<f64> = (0..len).map(|i| medprice.next((h[i], l[i]))).collect();
88            let batch_results = talib_rs::price_transform::medprice(&h[..len], &l[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
89
90            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
91                if s.is_nan() {
92                    assert!(b.is_nan());
93                } else {
94                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
95                }
96            }
97        }
98
99        #[test]
100        fn test_oc2_parity(
101            o in prop::collection::vec(0.1..100.0, 1..100),
102            c in prop::collection::vec(0.1..100.0, 1..100)
103        ) {
104            let len = o.len().min(c.len());
105            if len == 0 { return Ok(()); }
106
107            let mut oc2 = OC2::new();
108            let streaming_results: Vec<f64> = (0..len).map(|i| oc2.next((o[i], c[i]))).collect();
109            let batch_results: Vec<f64> = (0..len).map(|i| (o[i] + c[i]) / 2.0).collect();
110
111            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
112                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
113            }
114        }
115    }
116}