quantwave_core/indicators/
reversion_index.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::super_smoother::SuperSmoother;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone)]
13pub struct ReversionIndex {
14 length: usize,
15 prev_close: Option<f64>,
16 deltas: VecDeque<f64>,
17 abs_deltas: VecDeque<f64>,
18 delta_sum: f64,
19 abs_delta_sum: f64,
20 smooth: SuperSmoother,
21 trigger: SuperSmoother,
22}
23
24impl ReversionIndex {
25 pub fn new(length: usize) -> Self {
26 Self {
27 length,
28 prev_close: None,
29 deltas: VecDeque::with_capacity(length),
30 abs_deltas: VecDeque::with_capacity(length),
31 delta_sum: 0.0,
32 abs_delta_sum: 0.0,
33 smooth: SuperSmoother::new(8),
34 trigger: SuperSmoother::new(4),
35 }
36 }
37}
38
39impl Next<f64> for ReversionIndex {
40 type Output = (f64, f64); fn next(&mut self, input: f64) -> Self::Output {
43 let delta = match self.prev_close {
44 Some(prev) => input - prev,
45 None => 0.0,
46 };
47 self.prev_close = Some(input);
48
49 self.deltas.push_back(delta);
50 self.abs_deltas.push_back(delta.abs());
51 self.delta_sum += delta;
52 self.abs_delta_sum += delta.abs();
53
54 if self.deltas.len() > self.length {
55 if let Some(old_delta) = self.deltas.pop_front() {
56 self.delta_sum -= old_delta;
57 }
58 if let Some(old_abs_delta) = self.abs_deltas.pop_front() {
59 self.abs_delta_sum -= old_abs_delta;
60 }
61 }
62
63 let ratio = if self.abs_delta_sum != 0.0 {
64 self.delta_sum / self.abs_delta_sum
65 } else {
66 0.0
67 };
68
69 let sm_val = self.smooth.next(ratio);
70 let tr_val = self.trigger.next(ratio);
71
72 (sm_val, tr_val)
73 }
74}
75
76pub const REVERSION_INDEX_METADATA: IndicatorMetadata = IndicatorMetadata {
77 name: "Reversion Index",
78 description: "A mean-reversion oscillator that normalizes price changes by their absolute magnitude and applies SuperSmoother filtering.",
79 params: &[ParamDef {
80 name: "length",
81 default: "20",
82 description: "Summation period (approx. half dominant cycle)",
83 }],
84 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20JANUARY%202026.html",
85 formula_latex: r#"
86\[
87\Delta_t = \text{Close}_t - \text{Close}_{t-1}
88\]
89\[
90\text{Ratio} = \frac{\sum_{i=0}^{L-1} \Delta_{t-i}}{\sum_{i=0}^{L-1} |\Delta_{t-i}|}
91\]
92\[
93\text{Smooth} = SuperSmoother(\text{Ratio}, 8)
94\]
95\[
96\text{Trigger} = SuperSmoother(\text{Ratio}, 4)
97\]
98"#,
99 gold_standard_file: "reversion_index.json",
100 category: "Ehlers DSP",
101};
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::traits::Next;
107 use proptest::prelude::*;
108
109 #[test]
110 fn test_reversion_index_basic() {
111 let mut ri = ReversionIndex::new(20);
112 let inputs = vec![100.0, 101.0, 102.0, 101.0, 100.0];
113 for input in inputs {
114 let (sm, tr) = ri.next(input);
115 assert!(!sm.is_nan());
116 assert!(!tr.is_nan());
117 assert!(sm >= -1.0 && sm <= 1.0);
118 assert!(tr >= -1.0 && tr <= 1.0);
119 }
120 }
121
122 proptest! {
123 #[test]
124 fn test_reversion_index_parity(
125 inputs in prop::collection::vec(10.0..110.0, 50..100),
126 ) {
127 let length = 20;
128 let mut ri = ReversionIndex::new(length);
129 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| ri.next(x)).collect();
130
131 let mut deltas = Vec::new();
133 let mut smooth = SuperSmoother::new(8);
134 let mut trigger = SuperSmoother::new(4);
135 let mut batch_results = Vec::with_capacity(inputs.len());
136
137 for i in 0..inputs.len() {
138 let d = if i == 0 { 0.0 } else { inputs[i] - inputs[i-1] };
139 deltas.push(d);
140
141 let start = if deltas.len() > length { deltas.len() - length } else { 0 };
142 let window = &deltas[start..];
143
144 let d_sum: f64 = window.iter().sum();
145 let ad_sum: f64 = window.iter().map(|x| x.abs()).sum();
146
147 let ratio = if ad_sum != 0.0 { d_sum / ad_sum } else { 0.0 };
148
149 let sm = smooth.next(ratio);
150 let tr = trigger.next(ratio);
151 batch_results.push((sm, tr));
152 }
153
154 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
155 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
156 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
157 }
158 }
159 }
160}