quantwave_core/indicators/
stc.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
10pub struct SchaffTrendCycle {
11 fast_ema: Ema,
12 slow_ema: Ema,
13 st1: StochasticEma,
14 st2: StochasticEma,
15}
16
17#[derive(Debug, Clone)]
18struct Ema {
19 alpha: f64,
20 prev: Option<f64>,
21}
22
23impl Ema {
24 fn new(period: usize) -> Self {
25 Self {
26 alpha: 2.0 / (period as f64 + 1.0),
27 prev: None,
28 }
29 }
30
31 fn next(&mut self, input: f64) -> f64 {
32 let val = match self.prev {
33 None => input,
34 Some(p) => self.alpha * input + (1.0 - self.alpha) * p,
35 };
36 self.prev = Some(val);
37 val
38 }
39}
40
41#[derive(Debug, Clone)]
42struct StochasticEma {
43 period: usize,
44 window: VecDeque<f64>,
45 ema: Ema,
46}
47
48impl StochasticEma {
49 fn new(period: usize, ema_period: usize) -> Self {
50 Self {
51 period,
52 window: VecDeque::with_capacity(period),
53 ema: Ema::new(ema_period),
54 }
55 }
56
57 fn next(&mut self, input: f64) -> f64 {
58 self.window.push_front(input);
59 if self.window.len() > self.period {
60 self.window.pop_back();
61 }
62
63 if self.window.len() < self.period {
64 return self.ema.next(0.0);
65 }
66
67 let mut min = f64::MAX;
68 let mut max = f64::MIN;
69 for &v in &self.window {
70 if v < min { min = v; }
71 if v > max { max = v; }
72 }
73
74 let stoch = if max == min {
75 0.0
76 } else {
77 100.0 * (input - min) / (max - min)
78 };
79
80 self.ema.next(stoch)
81 }
82}
83
84impl SchaffTrendCycle {
85 pub fn new(cycle_period: usize, fast_period: usize, slow_period: usize) -> Self {
86 Self {
87 fast_ema: Ema::new(fast_period),
88 slow_ema: Ema::new(slow_period),
89 st1: StochasticEma::new(cycle_period, 3), st2: StochasticEma::new(cycle_period, 3),
91 }
92 }
93}
94
95impl Default for SchaffTrendCycle {
96 fn default() -> Self {
97 Self::new(10, 23, 50)
98 }
99}
100
101impl Next<f64> for SchaffTrendCycle {
102 type Output = f64;
103
104 fn next(&mut self, input: f64) -> Self::Output {
105 let macd = self.fast_ema.next(input) - self.slow_ema.next(input);
106 let s1 = self.st1.next(macd);
107 self.st2.next(s1)
108 }
109}
110
111pub const STC_METADATA: IndicatorMetadata = IndicatorMetadata {
112 name: "Schaff Trend Cycle",
113 description: "A hybrid indicator that applies a double-smoothed stochastic to MACD for faster trend identification.",
114 usage: "Use as a faster trend-cycle momentum indicator. STC typically reaches overbought/oversold levels sooner than MACD while generating fewer false signals than a raw stochastic.",
115 keywords: &["trend", "momentum", "cycle", "oscillator", "classic"],
116 ehlers_summary: "The Schaff Trend Cycle, developed by Doug Schaff, applies the stochastic oscillator formula twice to MACD values rather than to price. This double stochastic smoothing produces faster, more defined overbought and oversold levels than MACD alone, while the cycle component reduces the lag of a conventional stochastic. — investopedia.com",
117 params: &[
118 ParamDef { name: "cycle_period", default: "10", description: "Stochastic lookback period" },
119 ParamDef { name: "fast_period", default: "23", description: "Fast EMA period for MACD" },
120 ParamDef { name: "slow_period", default: "50", description: "Slow EMA period for MACD" },
121 ],
122 formula_source: "https://www.investopedia.com/articles/forex/10/schaff-trend-cycle-indicator.asp",
123 formula_latex: r#"
124\[
125MACD = EMA(23) - EMA(50)
126\]
127\[
128STC = EMA(Stochastic(EMA(Stochastic(MACD, 10), 3), 10), 3)
129\]
130"#,
131 gold_standard_file: "stc.json",
132 category: "Modern",
133};
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::traits::Next;
139 use proptest::prelude::*;
140
141 #[test]
142 fn test_stc_basic() {
143 let mut stc = SchaffTrendCycle::new(10, 23, 50);
144 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0];
145 for input in inputs {
146 let res = stc.next(input);
147 assert!(res >= 0.0 && res <= 100.0);
148 }
149 }
150
151 proptest! {
152 #[test]
153 fn test_stc_parity(
154 inputs in prop::collection::vec(1.0..100.0, 50..100),
155 ) {
156 let mut stc = SchaffTrendCycle::new(10, 23, 50);
157 let streaming_results: Vec<f64> = inputs.iter().map(|&x| stc.next(x)).collect();
158
159 let mut stc_batch = SchaffTrendCycle::new(10, 23, 50);
160 let batch_results: Vec<f64> = inputs.iter().map(|&x| stc_batch.next(x)).collect();
161
162 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
163 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
164 }
165 }
166 }
167}