Skip to main content

quantwave_core/indicators/
stc.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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 { 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), // Fixed smoothing 3 in many implementations
90            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}