Skip to main content

quantwave_core/indicators/
continuation_index.rs

1use crate::indicators::generalized_laguerre::GeneralizedLaguerre;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::indicators::smoothing::SMA;
4use crate::indicators::ultimate_smoother::UltimateSmoother;
5use crate::traits::Next;
6
7/// Continuation Index
8///
9/// Based on John Ehlers' "The Continuation Index" (TASC September 2025).
10/// The indicator is designed to signal both the early onset and potential exhaustion of a trend.
11/// It uses a Laguerre filter and UltimateSmoother to reduce lag, then compresses the result
12/// using an Inverse Fisher Transform (tanh).
13#[derive(Debug, Clone)]
14pub struct ContinuationIndex {
15    us: UltimateSmoother,
16    lg: GeneralizedLaguerre,
17    variance_sma: SMA,
18}
19
20impl ContinuationIndex {
21    pub fn new(gamma: f64, order: usize, length: usize) -> Self {
22        Self {
23            us: UltimateSmoother::new(length / 2),
24            lg: GeneralizedLaguerre::new(length, gamma, order),
25            variance_sma: SMA::new(length),
26        }
27    }
28}
29
30impl Next<f64> for ContinuationIndex {
31    type Output = f64;
32
33    fn next(&mut self, input: f64) -> Self::Output {
34        let us_val = self.us.next(input);
35        let lg_val = self.lg.next(input);
36
37        let diff = us_val - lg_val;
38        let variance = self.variance_sma.next(diff.abs());
39
40        let ref_val = if variance != 0.0 {
41            2.0 * diff / variance
42        } else {
43            0.0
44        };
45
46        // CI = (exp(2 * ref) - 1) / (exp(2 * ref) + 1) which is tanh(ref)
47        ref_val.tanh()
48    }
49}
50
51pub const CONTINUATION_INDEX_METADATA: IndicatorMetadata = IndicatorMetadata {
52    name: "Continuation Index",
53    description: "An oscillator that identifies trend onset and exhaustion by comparing a fast UltimateSmoother with a Generalized Laguerre filter.",
54    params: &[
55        ParamDef {
56            name: "gamma",
57            default: "0.8",
58            description: "Laguerre gamma parameter",
59        },
60        ParamDef {
61            name: "order",
62            default: "8",
63            description: "Laguerre filter order",
64        },
65        ParamDef {
66            name: "length",
67            default: "40",
68            description: "Base smoothing length",
69        },
70    ],
71    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202025.html",
72    formula_latex: r#"
73\[
74US = UltimateSmoother(Close, Length/2)
75\]
76\[
77LG = Laguerre(Close, \gamma, Order, Length)
78\]
79\[
80Variance = SMA(|US - LG|, Length)
81\]
82\[
83Ref = 2 \times (US - LG) / Variance
84\]
85\[
86CI = \tanh(Ref)
87\]
88"#,
89    gold_standard_file: "continuation_index.json",
90    category: "Ehlers DSP",
91};
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::traits::Next;
97    use proptest::prelude::*;
98
99    #[test]
100    fn test_continuation_index_basic() {
101        let mut ci = ContinuationIndex::new(0.8, 8, 40);
102        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
103        for input in inputs {
104            let res = ci.next(input);
105            assert!(!res.is_nan());
106            assert!(res >= -1.0 && res <= 1.0);
107        }
108    }
109
110    proptest! {
111        #[test]
112        fn test_continuation_index_parity(
113            inputs in prop::collection::vec(1.0..100.0, 50..100),
114        ) {
115            let gamma = 0.8;
116            let order = 8;
117            let length = 40;
118            let mut ci = ContinuationIndex::new(gamma, order, length);
119            let streaming_results: Vec<f64> = inputs.iter().map(|&x| ci.next(x)).collect();
120
121            // Reference implementation
122            let mut us = UltimateSmoother::new(length / 2);
123            let mut lg = GeneralizedLaguerre::new(length, gamma, order);
124            let mut diffs = Vec::new();
125            let mut batch_results = Vec::with_capacity(inputs.len());
126
127            for &input in &inputs {
128                let u = us.next(input);
129                let l = lg.next(input);
130                let d = u - l;
131                diffs.push(d.abs());
132
133                let start = if diffs.len() > length { diffs.len() - length } else { 0 };
134                let window = &diffs[start..];
135                let variance = window.iter().sum::<f64>() / window.len() as f64;
136
137                let ref_val = if variance != 0.0 { 2.0 * d / variance } else { 0.0 };
138                batch_results.push(ref_val.tanh());
139            }
140
141            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
142                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
143            }
144        }
145    }
146}