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 {
18 smas,
19 }
20 }
21}
22
23impl Next<f64> for CycleTrendAnalytics {
24 type Output = Vec<f64>; fn next(&mut self, input: f64) -> Self::Output {
27 self.smas.iter_mut().map(|sma| input - sma.next(input)).collect()
28 }
29}
30
31pub const CYCLE_TREND_ANALYTICS_METADATA: IndicatorMetadata = IndicatorMetadata {
32 name: "Cycle/Trend Analytics",
33 description: "A set of oscillators (Price - SMA) with lengths from 5 to 30 used to visualize cycles and trends.",
34 params: &[
35 ParamDef {
36 name: "min_length",
37 default: "5",
38 description: "Minimum SMA length",
39 },
40 ParamDef {
41 name: "max_length",
42 default: "30",
43 description: "Maximum SMA length",
44 },
45 ],
46 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
47 formula_latex: r#"
48\[
49Osc(L) = Price - SMA(Price, L) \quad \text{for } L \in [min, max]
50\]
51"#,
52 gold_standard_file: "cycle_trend_analytics.json",
53 category: "Ehlers DSP",
54};
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::traits::Next;
60 use crate::test_utils::{load_gold_standard_vec, assert_indicator_parity_vec};
61 use proptest::prelude::*;
62
63 #[test]
64 fn test_cycle_trend_analytics_gold_standard() {
65 let case = load_gold_standard_vec("cycle_trend_analytics");
66 let cta = CycleTrendAnalytics::new(5, 15);
67 assert_indicator_parity_vec(cta, &case.input, &case.expected);
68 }
69
70 #[test]
71 fn test_cycle_trend_analytics_basic() {
72 let mut cta = CycleTrendAnalytics::new(5, 10);
73 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
74 for input in inputs {
75 let res = cta.next(input);
76 assert_eq!(res.len(), 6);
77 }
78 }
79
80 proptest! {
81 #[test]
82 fn test_cycle_trend_analytics_parity(
83 inputs in prop::collection::vec(1.0..100.0, 30..100),
84 ) {
85 let min = 5;
86 let max = 15;
87 let mut cta = CycleTrendAnalytics::new(min, max);
88 let streaming_results: Vec<Vec<f64>> = inputs.iter().map(|&x| cta.next(x)).collect();
89
90 let mut batch_results = Vec::with_capacity(inputs.len());
92 for i in 0..inputs.len() {
93 let mut bar_results = Vec::with_capacity(max - min + 1);
94 for length in min..=max {
95 let sum: f64 = inputs[(i.saturating_sub(length - 1))..=i].iter().sum();
96 let count = (i + 1).min(length);
97 let sma = sum / count as f64;
98 bar_results.push(inputs[i] - sma);
99 }
100 batch_results.push(bar_results);
101 }
102
103 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
104 for (sv, bv) in s.iter().zip(b.iter()) {
105 approx::assert_relative_eq!(sv, bv, epsilon = 1e-10);
106 }
107 }
108 }
109 }
110}