quantwave_core/indicators/
continuation_index.rs1use 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#[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 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 usage: "Use to measure whether a price move is likely to continue or reverse based on cycle analysis. High index values suggest trend continuation; low values suggest an impending cycle turn.",
55 keywords: &["trend", "momentum", "ehlers", "cycle"],
56 ehlers_summary: "The Continuation Index measures the persistence of directional price movement relative to the dominant cycle. Ehlers derives it from the cycle phase velocity — when phase advances quickly in one direction, momentum is strong and continuation is likely; slow or reversing phase suggests the move is exhausting.",
57 params: &[
58 ParamDef {
59 name: "gamma",
60 default: "0.8",
61 description: "Laguerre gamma parameter",
62 },
63 ParamDef {
64 name: "order",
65 default: "8",
66 description: "Laguerre filter order",
67 },
68 ParamDef {
69 name: "length",
70 default: "40",
71 description: "Base smoothing length",
72 },
73 ],
74 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20SEPTEMBER%202025.html",
75 formula_latex: r#"
76\[
77US = UltimateSmoother(Close, Length/2)
78\]
79\[
80LG = Laguerre(Close, \gamma, Order, Length)
81\]
82\[
83Variance = SMA(|US - LG|, Length)
84\]
85\[
86Ref = 2 \times (US - LG) / Variance
87\]
88\[
89CI = \tanh(Ref)
90\]
91"#,
92 gold_standard_file: "continuation_index.json",
93 category: "Ehlers DSP",
94};
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::traits::Next;
100 use proptest::prelude::*;
101
102 #[test]
103 fn test_continuation_index_basic() {
104 let mut ci = ContinuationIndex::new(0.8, 8, 40);
105 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
106 for input in inputs {
107 let res = ci.next(input);
108 assert!(!res.is_nan());
109 assert!(res >= -1.0 && res <= 1.0);
110 }
111 }
112
113 proptest! {
114 #[test]
115 fn test_continuation_index_parity(
116 inputs in prop::collection::vec(1.0..100.0, 50..100),
117 ) {
118 let gamma = 0.8;
119 let order = 8;
120 let length = 40;
121 let mut ci = ContinuationIndex::new(gamma, order, length);
122 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ci.next(x)).collect();
123
124 let mut us = UltimateSmoother::new(length / 2);
126 let mut lg = GeneralizedLaguerre::new(length, gamma, order);
127 let mut diffs = Vec::new();
128 let mut batch_results = Vec::with_capacity(inputs.len());
129
130 for &input in &inputs {
131 let u = us.next(input);
132 let l = lg.next(input);
133 let d = u - l;
134 diffs.push(d.abs());
135
136 let start = if diffs.len() > length { diffs.len() - length } else { 0 };
137 let window = &diffs[start..];
138 let variance = window.iter().sum::<f64>() / window.len() as f64;
139
140 let ref_val = if variance != 0.0 { 2.0 * d / variance } else { 0.0 };
141 batch_results.push(ref_val.tanh());
142 }
143
144 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
145 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
146 }
147 }
148 }
149}