quantwave_core/indicators/
cycle_trend_analytics.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
10pub struct CycleTrendAnalytics {
11 smas: Vec<SMA>,
12}
13
14impl CycleTrendAnalytics {
15 pub fn new(min_length: usize, max_length: usize) -> Self {
16 let smas = (min_length..=max_length).map(SMA::new).collect();
17 Self { smas }
18 }
19}
20
21impl Next<f64> for CycleTrendAnalytics {
22 type Output = Vec<f64>; fn next(&mut self, input: f64) -> Self::Output {
25 self.smas
26 .iter_mut()
27 .map(|sma| input - sma.next(input))
28 .collect()
29 }
30}
31
32pub const CYCLE_TREND_ANALYTICS_METADATA: IndicatorMetadata = IndicatorMetadata {
33 name: "Cycle/Trend Analytics",
34 description: "A set of oscillators (Price - SMA) with lengths from 5 to 30 used to visualize cycles and trends.",
35 usage: "Use to classify the current market mode as trending or cycling before selecting your strategy. Apply trend-following systems in trend mode and mean-reversion systems in cycle mode.",
36 keywords: &["cycle", "trend", "ehlers", "classification", "adaptive"],
37 ehlers_summary: "Ehlers presents Cycle/Trend Analytics in Cycle Analytics for Traders as a framework for determining the dominant market mode. By measuring the correlation between price and the best-fit dominant cycle, the indicator classifies market behavior, enabling traders to switch between trend and cycle trading strategies dynamically.",
38 params: &[
39 ParamDef {
40 name: "min_length",
41 default: "5",
42 description: "Minimum SMA length",
43 },
44 ParamDef {
45 name: "max_length",
46 default: "30",
47 description: "Maximum SMA length",
48 },
49 ],
50 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
51 formula_latex: r#"
52\[
53Osc(L) = Price - SMA(Price, L) \quad \text{for } L \in [min, max]
54\]
55"#,
56 gold_standard_file: "cycle_trend_analytics.json",
57 category: "Ehlers DSP",
58};
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use crate::test_utils::{assert_indicator_parity_vec, load_gold_standard_vec};
64 use crate::traits::Next;
65 use proptest::prelude::*;
66
67 #[test]
68 fn test_cycle_trend_analytics_gold_standard() {
69 let case = load_gold_standard_vec("cycle_trend_analytics");
70 let cta = CycleTrendAnalytics::new(5, 15);
71 assert_indicator_parity_vec(cta, &case.input, &case.expected);
72 }
73
74 #[test]
75 fn test_cycle_trend_analytics_basic() {
76 let mut cta = CycleTrendAnalytics::new(5, 10);
77 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
78 for input in inputs {
79 let res = cta.next(input);
80 assert_eq!(res.len(), 6);
81 }
82 }
83
84 proptest! {
85 #[test]
86 fn test_cycle_trend_analytics_parity(
87 inputs in prop::collection::vec(1.0..100.0, 30..100),
88 ) {
89 let min = 5;
90 let max = 15;
91 let mut cta = CycleTrendAnalytics::new(min, max);
92 let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| cta.next(x)).collect();
93
94 let mut batch_results = Vec::with_capacity(inputs.len());
96 for i in 0..inputs.len() {
97 let mut bar_results = Vec::with_capacity(max - min + 1);
98 for length in min..=max {
99 let sum: f64 = inputs[(i.saturating_sub(length - 1))..=i].iter().sum();
100 let count = (i + 1).min(length);
101 let sma = sum / count as f64;
102 bar_results.push(inputs[i] - sma);
103 }
104 batch_results.push(bar_results);
105 }
106
107 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
108 for (sv, bv) in s.iter().zip(b.iter()) {
109 approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
110 }
111 }
112 }
113 }
114}