Skip to main content

quantwave_core/indicators/
reversion_index.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::super_smoother::SuperSmoother;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// The Reversion Index
7///
8/// Based on John Ehlers' "The Reversion Index" (TASC January 2026).
9/// This indicator identifies peaks and valleys in ranging markets by summing
10/// bar-to-bar price changes and normalizing them by their absolute values.
11/// It uses two SuperSmoother filters (Length 4 and 8) as trigger and signal lines.
12#[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); // (Smooth, Trigger)
41
42    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            // Reference implementation
132            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}