quantwave_core/indicators/
oc_price_rsi.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::momentum::RSI;
4
5#[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 usage: "Use to measure momentum on the open-to-close price differential rather than close-to-close, capturing intraday directional strength more directly.",
43 keywords: &["oscillator", "rsi", "ehlers", "momentum"],
44 ehlers_summary: "Ehlers computes this RSI variant on the difference between the open and close price of each bar rather than on the closing price series. The open-close differential captures the net directional pressure within each bar, producing a momentum oscillator more sensitive to intraday commitment than standard RSI.",
45 params: &[
46 ParamDef {
47 name: "period",
48 default: "14",
49 description: "RSI period",
50 },
51 ],
52 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/EveryLittleBitHelps.pdf",
53 formula_latex: r#"
54\[
55Input = \frac{Open + Close}{2}
56\]
57\[
58RSI = \text{Wilder's RSI}(Input, Period)
59\]
60"#,
61 gold_standard_file: "oc_price_rsi.json",
62 category: "Ehlers DSP",
63};
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::traits::Next;
69 use crate::test_utils::{load_gold_standard_oc, assert_indicator_parity_oc};
70 use proptest::prelude::*;
71
72 #[test]
73 fn test_oc_price_rsi_gold_standard() {
74 let case = load_gold_standard_oc("oc_price_rsi");
75 let ocrsi = OCPriceRSI::new(14);
76 assert_indicator_parity_oc(ocrsi, &case.input, &case.expected);
77 }
78
79 #[test]
80 fn test_oc_price_rsi_basic() {
81 let mut ocrsi = OCPriceRSI::new(14);
82 for i in 0..50 {
83 let val = ocrsi.next((100.0 + i as f64, 101.0 + i as f64));
84 if i >= 14 {
85 assert!(!val.is_nan());
86 }
87 }
88 }
89
90 proptest! {
91 #[test]
92 fn test_oc_price_rsi_parity(
93 opens in prop::collection::vec(1.0..100.0, 50..100),
94 closes in prop::collection::vec(1.0..100.0, 50..100),
95 ) {
96 let period = 14;
97 let mut ocrsi = OCPriceRSI::new(period);
98
99 let min_len = opens.len().min(closes.len());
100 let inputs: Vec<(f64, f64)> = opens[..min_len].iter().cloned().zip(closes[..min_len].iter().cloned()).collect();
101 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ocrsi.next(x)).collect();
102
103 let mut batch_results = Vec::with_capacity(min_len);
105 let mut rsi = RSI::new(period);
106 for &(o, c) in &inputs {
107 batch_results.push(rsi.next((o + c) / 2.0));
108 }
109
110 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
111 if s.is_nan() {
112 assert!(b.is_nan());
113 } else {
114 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
115 }
116 }
117 }
118 }
119}