Skip to main content

quantwave_core/indicators/
stc.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5/// Schaff Trend Cycle (STC)
6///
7/// STC is a trend-following indicator that combines MACD with a double-smoothed
8/// stochastic calculation to provide faster and more accurate signals than MACD alone.
9#[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), // Fixed smoothing 3 in many implementations
94            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}