Skip to main content

egui_charts/studies/builtin/
schaff_trend.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Schaff Trend Cycle (STC)
9/// Combines MACD with stochastic to create a faster, more accurate oscillator
10/// Oscillates between 0 and 100
11#[derive(Clone)]
12pub struct SchaffTrendCycle {
13    /// MACD short period (typically 23)
14    macd_short: usize,
15    /// MACD long period (typically 50)
16    macd_long: usize,
17    /// Stochastic cycle period (typically 10)
18    cycle: usize,
19    values: IndicatorValues,
20    stepper: Stepper<State>,
21    color: Color32,
22    visible: bool,
23}
24
25#[derive(Clone, Default)]
26struct State {
27    index: usize,
28    count: usize,
29    ema: [Smoothing; 2],
30    macd: std::collections::VecDeque<f64>,
31    first: std::collections::VecDeque<f64>,
32    smooth: [f64; 2],
33    pending: Vec<(usize, f64)>,
34}
35
36impl SchaffTrendCycle {
37    /// Create with MACD periods 23 and 50 and cycle length 10.
38    pub fn new() -> Self {
39        Self {
40            macd_short: 23,
41            macd_long: 50,
42            cycle: 10,
43            values: IndicatorValues::new(),
44            stepper: Stepper::default(),
45            color: palette::PURPLE,
46            visible: true,
47        }
48    }
49
50    /// Set the short and long MACD periods and cycle length in bars, each at least one.
51    pub fn with_periods(mut self, short: usize, long: usize, cycle: usize) -> Self {
52        self.macd_short = short.max(1);
53        self.macd_long = long.max(1);
54        self.cycle = cycle.max(1);
55        self
56    }
57
58    /// Set the plot color.
59    pub fn with_color(mut self, color: Color32) -> Self {
60        self.color = color;
61        self
62    }
63
64    fn stochastic(values: &std::collections::VecDeque<f64>, period: usize) -> f64 {
65        let value = *values.back().unwrap();
66        if values.len() < period || value.is_nan() {
67            return f64::NAN;
68        }
69        let high = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
70        let low = values.iter().copied().fold(f64::INFINITY, f64::min);
71        let range = high - low;
72        if range == 0.0 {
73            50.0
74        } else {
75            (value - low) / range * 100.0
76        }
77    }
78}
79
80impl Default for SchaffTrendCycle {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl Indicator for SchaffTrendCycle {
87    fn source(&self) -> Option<crate::studies::StudySource> {
88        Some(crate::studies::StudySource::Price(
89            crate::model::PriceSource::Close,
90        ))
91    }
92
93    crate::studies::inputs::study_inputs!(self;
94        macd_short: length("MACD short", 1, usize::MAX),
95        macd_long: length("MACD long", 1, usize::MAX),
96        cycle: length("Cycle", 1, usize::MAX)
97    ; {});
98
99    fn name(&self) -> &str {
100        "STC"
101    }
102
103    fn desc(&self) -> &str {
104        "Schaff Trend Cycle - Fast trend oscillator"
105    }
106
107    fn calculate(&mut self, data: &[Bar]) {
108        self.update(data, 0);
109    }
110
111    fn update(&mut self, data: &[Bar], from: usize) {
112        self.stepper
113            .run(data, from, &mut self.values, |state, bar| {
114                let index = state.index;
115                state.index += 1;
116                if !bar.close.is_finite() {
117                    return IndicatorValue::None;
118                }
119                state.count += 1;
120                let short = state.ema[0].next(
121                    bar.close,
122                    self.macd_short,
123                    2.0 / (self.macd_short as f64 + 1.0),
124                );
125                let long = state.ema[1].next(
126                    bar.close,
127                    self.macd_long,
128                    2.0 / (self.macd_long as f64 + 1.0),
129                );
130                let macd = if short.is_nan() || long.is_nan() {
131                    f64::NAN
132                } else {
133                    short - long
134                };
135                state.macd.push_back(macd);
136                if state.macd.len() > self.cycle {
137                    state.macd.pop_front();
138                }
139                let first = Self::stochastic(&state.macd, self.cycle);
140                let smooth = if first.is_nan() {
141                    f64::NAN
142                } else {
143                    state.smooth[0] += 0.5 * (first - state.smooth[0]);
144                    state.smooth[0]
145                };
146                state.first.push_back(smooth);
147                if state.first.len() > self.cycle {
148                    state.first.pop_front();
149                }
150                let second = Self::stochastic(&state.first, self.cycle);
151                if second.is_nan() {
152                    return IndicatorValue::None;
153                }
154                state.smooth[1] += 0.5 * (second - state.smooth[1]);
155                let value = state.smooth[1].clamp(0.0, 100.0);
156                if state.count < self.macd_long.saturating_add(self.cycle) {
157                    if value.is_finite() {
158                        state.pending.push((index, value));
159                    }
160                    IndicatorValue::None
161                } else {
162                    IndicatorValue::Single(value)
163                }
164            });
165        let required = self.macd_long.saturating_add(self.cycle);
166        for &(index, value) in &self.stepper.state().pending {
167            self.values.set(
168                index,
169                if self.stepper.state().count < required {
170                    IndicatorValue::None
171                } else {
172                    IndicatorValue::Single(value)
173                },
174            );
175        }
176    }
177
178    fn is_incremental(&self) -> bool {
179        true
180    }
181
182    fn values(&self) -> &IndicatorValues {
183        &self.values
184    }
185
186    fn colors(&self) -> Vec<Color32> {
187        vec![self.color]
188    }
189
190    fn set_colors(&mut self, colors: Vec<Color32>) {
191        if !colors.is_empty() {
192            self.color = colors[0];
193        }
194    }
195
196    fn is_overlay(&self) -> bool {
197        false
198    }
199
200    fn is_visible(&self) -> bool {
201        self.visible
202    }
203
204    fn set_visible(&mut self, visible: bool) {
205        self.visible = visible;
206    }
207
208    fn clone_box(&self) -> Box<dyn Indicator> {
209        Box::new(self.clone())
210    }
211
212    fn line_names(&self) -> Vec<String> {
213        vec![format!(
214            "STC({},{},{})",
215            self.macd_short, self.macd_long, self.cycle
216        )]
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::studies::test_support::make_bar;
224
225    #[test]
226    fn test_stc_calculation() {
227        let mut stc = SchaffTrendCycle::new().with_periods(5, 10, 3);
228
229        let data: Vec<Bar> = (0..50).map(|i| make_bar(100.0 + i as f64)).collect();
230
231        stc.calculate(&data);
232
233        assert_eq!(stc.values.len(), 50);
234
235        // Check that valid values are between 0 and 100
236        let mut checked = 0;
237        for value in &stc.values.to_rows() {
238            if let IndicatorValue::Single(v) = value {
239                assert!(*v >= 0.0 && *v <= 100.0, "STC should be 0-100, got {v}");
240                checked += 1;
241            }
242        }
243        assert!(checked > 0, "no value to check");
244    }
245
246    #[test]
247    fn test_stc_uptrend() {
248        let mut stc = SchaffTrendCycle::new().with_periods(3, 6, 3);
249
250        // Strong uptrend
251        let data: Vec<Bar> = (0..30).map(|i| make_bar(100.0 + i as f64 * 2.0)).collect();
252
253        stc.calculate(&data);
254
255        // In uptrend, STC should be high (close to 100)
256        let rows = stc.values.to_rows();
257        let IndicatorValue::Single(v) = rows.last().unwrap() else {
258            panic!("no value: {:?}", stc.values.to_rows().last().unwrap());
259        };
260        assert!(*v > 50.0, "STC should be high in uptrend, got {v}");
261    }
262}