Skip to main content

quantwave_core/indicators/
oc_price_rsi.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::momentum::RSI;
4
5/// OCPrice RSI
6///
7/// Based on John Ehlers' "Every Little Bit Helps".
8/// Uses the average of Open and Close as the input to a standard RSI
9/// to reduce Nyquist frequency noise and provide a half-bar lead.
10#[derive(Debug, Clone)]
11pub struct OCPriceRSI {
12    rsi: RSI,
13}
14
15impl OCPriceRSI {
16    pub fn new(period: usize) -> Self {
17        Self {
18            rsi: RSI::new(period),
19        }
20    }
21}
22
23impl Default for OCPriceRSI {
24    fn default() -> Self {
25        Self::new(14)
26    }
27}
28
29impl Next<(f64, f64)> for OCPriceRSI {
30    type Output = f64;
31
32    fn next(&mut self, input: (f64, f64)) -> Self::Output {
33        let (open, close) = input;
34        let oc_avg = (open + close) / 2.0;
35        self.rsi.next(oc_avg)
36    }
37}
38
39pub const OC_PRICE_RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
40    name: "OCPriceRSI",
41    description: "RSI calculated using the average of Open and Close prices to reduce noise.",
42    params: &[
43        ParamDef {
44            name: "period",
45            default: "14",
46            description: "RSI period",
47        },
48    ],
49    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/EveryLittleBitHelps.pdf",
50    formula_latex: r#"
51\[
52Input = \frac{Open + Close}{2}
53\]
54\[
55RSI = \text{Wilder's RSI}(Input, Period)
56\]
57"#,
58    gold_standard_file: "oc_price_rsi.json",
59    category: "Ehlers DSP",
60};
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::traits::Next;
66    use crate::test_utils::{load_gold_standard_oc, assert_indicator_parity_oc};
67    use proptest::prelude::*;
68
69    #[test]
70    fn test_oc_price_rsi_gold_standard() {
71        let case = load_gold_standard_oc("oc_price_rsi");
72        let ocrsi = OCPriceRSI::new(14);
73        assert_indicator_parity_oc(ocrsi, &case.input, &case.expected);
74    }
75
76    #[test]
77    fn test_oc_price_rsi_basic() {
78        let mut ocrsi = OCPriceRSI::new(14);
79        for i in 0..50 {
80            let val = ocrsi.next((100.0 + i as f64, 101.0 + i as f64));
81            if i >= 14 {
82                assert!(!val.is_nan());
83            }
84        }
85    }
86
87    proptest! {
88        #[test]
89        fn test_oc_price_rsi_parity(
90            opens in prop::collection::vec(1.0..100.0, 50..100),
91            closes in prop::collection::vec(1.0..100.0, 50..100),
92        ) {
93            let period = 14;
94            let mut ocrsi = OCPriceRSI::new(period);
95            
96            let min_len = opens.len().min(closes.len());
97            let inputs: Vec<(f64, f64)> = opens[..min_len].iter().cloned().zip(closes[..min_len].iter().cloned()).collect();
98            let streaming_results: Vec<f64> = inputs.iter().map(|&x| ocrsi.next(x)).collect();
99
100            // Batch implementation
101            let mut batch_results = Vec::with_capacity(min_len);
102            let mut rsi = RSI::new(period);
103            for &(o, c) in &inputs {
104                batch_results.push(rsi.next((o + c) / 2.0));
105            }
106
107            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
108                if s.is_nan() {
109                    assert!(b.is_nan());
110                } else {
111                    approx::assert_relative_eq!(s, b, epsilon = 1e-10);
112                }
113            }
114        }
115    }
116}