egui_charts/studies/builtin/
schaff_trend.rs1use 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#[derive(Clone)]
12pub struct SchaffTrendCycle {
13 macd_short: usize,
15 macd_long: usize,
17 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 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 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 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 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 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 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}