quantwave_core/indicators/
stc.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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 {
71 min = v;
72 }
73 if v > max {
74 max = v;
75 }
76 }
77
78 let stoch = if max == min {
79 0.0
80 } else {
81 100.0 * (input - min) / (max - min)
82 };
83
84 self.ema.next(stoch)
85 }
86}
87
88impl SchaffTrendCycle {
89 pub fn new(cycle_period: usize, fast_period: usize, slow_period: usize) -> Self {
90 Self {
91 fast_ema: Ema::new(fast_period),
92 slow_ema: Ema::new(slow_period),
93 st1: StochasticEma::new(cycle_period, 3), st2: StochasticEma::new(cycle_period, 3),
95 }
96 }
97}
98
99impl Default for SchaffTrendCycle {
100 fn default() -> Self {
101 Self::new(10, 23, 50)
102 }
103}
104
105impl Next<f64> for SchaffTrendCycle {
106 type Output = f64;
107
108 fn next(&mut self, input: f64) -> Self::Output {
109 let macd = self.fast_ema.next(input) - self.slow_ema.next(input);
110 let s1 = self.st1.next(macd);
111 self.st2.next(s1)
112 }
113}
114
115pub const STC_METADATA: IndicatorMetadata = IndicatorMetadata {
116 name: "Schaff Trend Cycle",
117 description: "A hybrid indicator that applies a double-smoothed stochastic to MACD for faster trend identification.",
118 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.",
119 keywords: &["trend", "momentum", "cycle", "oscillator", "classic"],
120 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",
121 params: &[
122 ParamDef {
123 name: "cycle_period",
124 default: "10",
125 description: "Stochastic lookback period",
126 },
127 ParamDef {
128 name: "fast_period",
129 default: "23",
130 description: "Fast EMA period for MACD",
131 },
132 ParamDef {
133 name: "slow_period",
134 default: "50",
135 description: "Slow EMA period for MACD",
136 },
137 ],
138 formula_source: "https://www.investopedia.com/articles/forex/10/schaff-trend-cycle-indicator.asp",
139 formula_latex: r#"
140\[
141MACD = EMA(23) - EMA(50)
142\]
143\[
144STC = EMA(Stochastic(EMA(Stochastic(MACD, 10), 3), 10), 3)
145\]
146"#,
147 gold_standard_file: "stc.json",
148 category: "Modern",
149};
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::Next;
155 use proptest::prelude::*;
156
157 #[test]
158 fn test_stc_basic() {
159 let mut stc = SchaffTrendCycle::new(10, 23, 50);
160 let inputs = vec![
161 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0,
162 ];
163 for input in inputs {
164 let res = stc.next(input);
165 assert!(res >= 0.0 && res <= 100.0);
166 }
167 }
168
169 proptest! {
170 #[test]
171 fn test_stc_parity(
172 inputs in prop::collection::vec(1.0..100.0, 50..100),
173 ) {
174 let mut stc = SchaffTrendCycle::new(10, 23, 50);
175 let streaming_results: Vec<f64> = inputs.iter().map(|&x| stc.next(x)).collect();
176
177 let mut stc_batch = SchaffTrendCycle::new(10, 23, 50);
178 let batch_results: Vec<f64> = inputs.iter().map(|&x| stc_batch.next(x)).collect();
179
180 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
181 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
182 }
183 }
184 }
185}