use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct SchaffTrendCycle {
fast_ema: Ema,
slow_ema: Ema,
st1: StochasticEma,
st2: StochasticEma,
}
#[derive(Debug, Clone)]
struct Ema {
alpha: f64,
prev: Option<f64>,
}
impl Ema {
fn new(period: usize) -> Self {
Self {
alpha: 2.0 / (period as f64 + 1.0),
prev: None,
}
}
fn next(&mut self, input: f64) -> f64 {
let val = match self.prev {
None => input,
Some(p) => self.alpha * input + (1.0 - self.alpha) * p,
};
self.prev = Some(val);
val
}
}
#[derive(Debug, Clone)]
struct StochasticEma {
period: usize,
window: VecDeque<f64>,
ema: Ema,
}
impl StochasticEma {
fn new(period: usize, ema_period: usize) -> Self {
Self {
period,
window: VecDeque::with_capacity(period),
ema: Ema::new(ema_period),
}
}
fn next(&mut self, input: f64) -> f64 {
self.window.push_front(input);
if self.window.len() > self.period {
self.window.pop_back();
}
if self.window.len() < self.period {
return self.ema.next(0.0);
}
let mut min = f64::MAX;
let mut max = f64::MIN;
for &v in &self.window {
if v < min { min = v; }
if v > max { max = v; }
}
let stoch = if max == min {
0.0
} else {
100.0 * (input - min) / (max - min)
};
self.ema.next(stoch)
}
}
impl SchaffTrendCycle {
pub fn new(cycle_period: usize, fast_period: usize, slow_period: usize) -> Self {
Self {
fast_ema: Ema::new(fast_period),
slow_ema: Ema::new(slow_period),
st1: StochasticEma::new(cycle_period, 3), st2: StochasticEma::new(cycle_period, 3),
}
}
}
impl Default for SchaffTrendCycle {
fn default() -> Self {
Self::new(10, 23, 50)
}
}
impl Next<f64> for SchaffTrendCycle {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
let macd = self.fast_ema.next(input) - self.slow_ema.next(input);
let s1 = self.st1.next(macd);
self.st2.next(s1)
}
}
pub const STC_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Schaff Trend Cycle",
description: "A hybrid indicator that applies a double-smoothed stochastic to MACD for faster trend identification.",
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.",
keywords: &["trend", "momentum", "cycle", "oscillator", "classic"],
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",
params: &[
ParamDef { name: "cycle_period", default: "10", description: "Stochastic lookback period" },
ParamDef { name: "fast_period", default: "23", description: "Fast EMA period for MACD" },
ParamDef { name: "slow_period", default: "50", description: "Slow EMA period for MACD" },
],
formula_source: "https://www.investopedia.com/articles/forex/10/schaff-trend-cycle-indicator.asp",
formula_latex: r#"
\[
MACD = EMA(23) - EMA(50)
\]
\[
STC = EMA(Stochastic(EMA(Stochastic(MACD, 10), 3), 10), 3)
\]
"#,
gold_standard_file: "stc.json",
category: "Modern",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_stc_basic() {
let mut stc = SchaffTrendCycle::new(10, 23, 50);
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];
for input in inputs {
let res = stc.next(input);
assert!(res >= 0.0 && res <= 100.0);
}
}
proptest! {
#[test]
fn test_stc_parity(
inputs in prop::collection::vec(1.0..100.0, 50..100),
) {
let mut stc = SchaffTrendCycle::new(10, 23, 50);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| stc.next(x)).collect();
let mut stc_batch = SchaffTrendCycle::new(10, 23, 50);
let batch_results: Vec<f64> = inputs.iter().map(|&x| stc_batch.next(x)).collect();
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}