1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::ema::Ema;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
39pub struct Stc {
40 fast_period: usize,
41 slow_period: usize,
42 schaff_period: usize,
43 factor: f64,
44 fast_ema: Ema,
45 slow_ema: Ema,
46 macd_window: VecDeque<f64>,
47 d_window: VecDeque<f64>,
48 last_d: Option<f64>,
49 last_value: Option<f64>,
50}
51
52impl Stc {
53 pub fn new(fast: usize, slow: usize, schaff_period: usize, factor: f64) -> Result<Self> {
57 if fast == 0 || slow == 0 || schaff_period == 0 {
58 return Err(Error::PeriodZero);
59 }
60 if fast >= slow {
61 return Err(Error::InvalidPeriod {
62 message: "STC fast period must be strictly less than slow",
63 });
64 }
65 if !factor.is_finite() || factor <= 0.0 || factor > 1.0 {
66 return Err(Error::InvalidPeriod {
67 message: "STC factor must be a finite value in (0, 1]",
68 });
69 }
70 Ok(Self {
71 fast_period: fast,
72 slow_period: slow,
73 schaff_period,
74 factor,
75 fast_ema: Ema::new(fast)?,
76 slow_ema: Ema::new(slow)?,
77 macd_window: VecDeque::with_capacity(schaff_period),
78 d_window: VecDeque::with_capacity(schaff_period),
79 last_d: None,
80 last_value: None,
81 })
82 }
83
84 pub fn classic() -> Self {
86 Self::new(23, 50, 10, 0.5).expect("classic STC parameters are valid")
87 }
88
89 pub const fn params(&self) -> (usize, usize, usize, f64) {
91 (
92 self.fast_period,
93 self.slow_period,
94 self.schaff_period,
95 self.factor,
96 )
97 }
98}
99
100fn rolling_minmax(window: &VecDeque<f64>) -> (f64, f64) {
101 let mut lo = f64::INFINITY;
102 let mut hi = f64::NEG_INFINITY;
103 for &v in window {
104 if v < lo {
105 lo = v;
106 }
107 if v > hi {
108 hi = v;
109 }
110 }
111 (lo, hi)
112}
113
114impl Indicator for Stc {
115 type Input = f64;
116 type Output = f64;
117
118 fn update(&mut self, input: f64) -> Option<f64> {
119 let f = self.fast_ema.update(input);
120 let s = self.slow_ema.update(input);
121 let (f, s) = (f?, s?);
122 let macd = f - s;
123
124 if self.macd_window.len() == self.schaff_period {
125 self.macd_window.pop_front();
126 }
127 self.macd_window.push_back(macd);
128 if self.macd_window.len() < self.schaff_period {
129 return None;
130 }
131
132 let (lo, hi) = rolling_minmax(&self.macd_window);
133 let k = if hi > lo {
134 100.0 * (macd - lo) / (hi - lo)
135 } else {
136 0.0
137 };
138
139 let d = match self.last_d {
140 Some(prev) => prev + self.factor * (k - prev),
141 None => k,
142 };
143 self.last_d = Some(d);
144
145 if self.d_window.len() == self.schaff_period {
146 self.d_window.pop_front();
147 }
148 self.d_window.push_back(d);
149 if self.d_window.len() < self.schaff_period {
150 return None;
151 }
152
153 let (lo_d, hi_d) = rolling_minmax(&self.d_window);
154 let k2 = if hi_d > lo_d {
155 100.0 * (d - lo_d) / (hi_d - lo_d)
156 } else {
157 0.0
158 };
159
160 let stc = match self.last_value {
161 Some(prev) => prev + self.factor * (k2 - prev),
162 None => k2,
163 };
164 self.last_value = Some(stc);
165 Some(stc.clamp(0.0, 100.0))
166 }
167
168 fn reset(&mut self) {
169 self.fast_ema.reset();
170 self.slow_ema.reset();
171 self.macd_window.clear();
172 self.d_window.clear();
173 self.last_d = None;
174 self.last_value = None;
175 }
176
177 fn warmup_period(&self) -> usize {
178 self.slow_period + 2 * (self.schaff_period - 1)
182 }
183
184 fn is_ready(&self) -> bool {
185 self.last_value.is_some() && self.d_window.len() == self.schaff_period
186 }
187
188 fn name(&self) -> &'static str {
189 "STC"
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::traits::BatchExt;
197
198 #[test]
199 fn rejects_zero_period() {
200 assert!(matches!(Stc::new(0, 50, 10, 0.5), Err(Error::PeriodZero)));
201 assert!(matches!(Stc::new(23, 0, 10, 0.5), Err(Error::PeriodZero)));
202 assert!(matches!(Stc::new(23, 50, 0, 0.5), Err(Error::PeriodZero)));
203 }
204
205 #[test]
206 fn rejects_invalid_params() {
207 assert!(matches!(
208 Stc::new(50, 23, 10, 0.5),
209 Err(Error::InvalidPeriod { .. })
210 ));
211 assert!(matches!(
212 Stc::new(23, 50, 10, 0.0),
213 Err(Error::InvalidPeriod { .. })
214 ));
215 assert!(matches!(
216 Stc::new(23, 50, 10, 1.5),
217 Err(Error::InvalidPeriod { .. })
218 ));
219 assert!(matches!(
220 Stc::new(23, 50, 10, f64::NAN),
221 Err(Error::InvalidPeriod { .. })
222 ));
223 }
224
225 #[test]
226 fn accessors_and_metadata() {
227 let stc = Stc::classic();
228 let (f, s, p, k) = stc.params();
229 assert_eq!((f, s, p), (23, 50, 10));
230 assert!((k - 0.5).abs() < 1e-12);
231 assert_eq!(stc.warmup_period(), 50 + 18);
232 assert_eq!(stc.name(), "STC");
233 }
234
235 #[test]
236 fn classic_factory() {
237 let (f, s, p, k) = Stc::classic().params();
238 assert_eq!((f, s, p), (23, 50, 10));
239 assert!((k - 0.5).abs() < 1e-12);
240 }
241
242 #[test]
243 fn constant_series_yields_zero() {
244 let mut stc = Stc::new(3, 5, 4, 0.5).unwrap();
247 let out = stc.batch(&[42.0_f64; 80]);
248 for v in out.iter().rev().take(5).flatten() {
249 assert_eq!(*v, 0.0);
250 }
251 }
252
253 #[test]
254 fn warmup_emits_first_value_at_warmup_period() {
255 let mut stc = Stc::new(2, 4, 3, 0.5).unwrap();
256 assert_eq!(stc.warmup_period(), 8);
258 let prices: Vec<f64> = (1..=10).map(f64::from).collect();
259 let out = stc.batch(&prices);
260 for v in out.iter().take(7) {
261 assert!(v.is_none());
262 }
263 assert!(out[7].is_some());
264 }
265
266 #[test]
267 fn output_is_bounded() {
268 let mut stc = Stc::classic();
269 let prices: Vec<f64> = (0..400)
270 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 25.0)
271 .collect();
272 for v in stc.batch(&prices).iter().flatten() {
273 assert!((0.0..=100.0).contains(v), "STC out of [0, 100]: {v}");
274 }
275 }
276
277 #[test]
278 fn oscillating_series_visits_full_range() {
279 let mut stc = Stc::classic();
286 let prices: Vec<f64> = (0..400)
287 .map(|i| 100.0 + (f64::from(i) * 0.15).sin() * 30.0)
288 .collect();
289 let out = stc.batch(&prices);
290 let mut saw_high = false;
291 let mut saw_low = false;
292 for v in out.iter().flatten() {
293 if *v > 80.0 {
294 saw_high = true;
295 }
296 if *v < 20.0 {
297 saw_low = true;
298 }
299 }
300 assert!(
301 saw_high,
302 "STC should reach above 80 on a strong oscillation"
303 );
304 assert!(saw_low, "STC should reach below 20 on a strong oscillation");
305 }
306
307 #[test]
308 fn batch_equals_streaming() {
309 let prices: Vec<f64> = (1..=200)
310 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
311 .collect();
312 let mut a = Stc::classic();
313 let mut b = Stc::classic();
314 assert_eq!(
315 a.batch(&prices),
316 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
317 );
318 }
319
320 #[test]
321 fn reset_clears_state() {
322 let mut stc = Stc::classic();
323 stc.batch(&(1..=200).map(f64::from).collect::<Vec<_>>());
324 assert!(stc.is_ready());
325 stc.reset();
326 assert!(!stc.is_ready());
327 assert!(stc.last_value.is_none());
328 }
329}