Skip to main content

finance_query/indicators/
stochastic.rs

1//! Stochastic Oscillator indicator.
2
3use super::{IndicatorError, Result, sma::sma};
4use serde::{Deserialize, Serialize};
5
6/// Result of Stochastic Oscillator calculation
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct StochasticResult {
9    /// %K line (optionally slow-smoothed)
10    pub k: Vec<Option<f64>>,
11    /// %D line (Signal line — SMA of %K)
12    pub d: Vec<Option<f64>>,
13}
14
15/// Calculate Stochastic Oscillator.
16///
17/// Returns (%K, %D) where:
18/// - Raw %K = (Close − Lowest Low) / (Highest High − Lowest Low) × 100
19/// - Slow %K = SMA(Raw %K, k_slow) — set `k_slow = 1` for no smoothing
20/// - %D = SMA(Slow %K, d_period)
21///
22/// # Arguments
23///
24/// * `highs` - High prices
25/// * `lows` - Low prices
26/// * `closes` - Close prices
27/// * `k_period` - Lookback period for raw %K (number of bars)
28/// * `k_slow` - Smoothing period applied to raw %K before computing %D; `1` = no smoothing
29/// * `d_period` - Period for %D signal line (SMA of slow %K)
30///
31/// # Example
32///
33/// ```
34/// use finance_query::indicators::stochastic;
35///
36/// let highs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
37/// let lows = vec![8.0, 9.0, 10.0, 11.0, 12.0];
38/// let closes = vec![9.0, 10.0, 11.0, 12.0, 13.0];
39/// let result = stochastic(&highs, &lows, &closes, 3, 1, 2).unwrap();
40/// ```
41pub fn stochastic(
42    highs: &[f64],
43    lows: &[f64],
44    closes: &[f64],
45    k_period: usize,
46    k_slow: usize,
47    d_period: usize,
48) -> Result<StochasticResult> {
49    if k_period == 0 || k_slow == 0 || d_period == 0 {
50        return Err(IndicatorError::InvalidPeriod(
51            "Periods must be greater than 0".to_string(),
52        ));
53    }
54    let len = highs.len();
55    if lows.len() != len || closes.len() != len {
56        return Err(IndicatorError::InvalidPeriod(
57            "Data lengths must match".to_string(),
58        ));
59    }
60    if len < k_period {
61        return Err(IndicatorError::InsufficientData {
62            need: k_period,
63            got: len,
64        });
65    }
66
67    // Step 1: compute raw %K
68    let mut raw_k = vec![None; len];
69    let mut raw_k_for_sma = vec![0.0; len];
70
71    for i in (k_period - 1)..len {
72        let start = i + 1 - k_period;
73        let highest = highs[start..=i]
74            .iter()
75            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
76        let lowest = lows[start..=i].iter().fold(f64::INFINITY, |a, &b| a.min(b));
77        let k = if (highest - lowest).abs() < f64::EPSILON {
78            50.0 // Neutral when no range
79        } else {
80            ((closes[i] - lowest) / (highest - lowest)) * 100.0
81        };
82        raw_k[i] = Some(k);
83        raw_k_for_sma[i] = k;
84    }
85
86    // Step 2: apply k_slow smoothing to raw %K
87    let raw_k_valid_start = k_period - 1;
88    let (slow_k, slow_k_valid_start) = if k_slow == 1 {
89        // No smoothing: slow %K = raw %K
90        (raw_k.clone(), raw_k_valid_start)
91    } else {
92        let raw_k_slice = &raw_k_for_sma[raw_k_valid_start..];
93        let smoothed = sma(raw_k_slice, k_slow);
94        let slow_valid_start = raw_k_valid_start + k_slow - 1;
95
96        let mut slow_k = vec![None; len];
97        for (j, val) in smoothed.into_iter().enumerate() {
98            let idx = j + raw_k_valid_start;
99            if idx < len {
100                slow_k[idx] = val;
101            }
102        }
103        (slow_k, slow_valid_start)
104    };
105
106    // Step 3: compute %D as SMA of slow %K
107    let slow_k_values: Vec<f64> = slow_k.iter().map(|v| v.unwrap_or(0.0)).collect();
108    let slow_k_slice = &slow_k_values[slow_k_valid_start..];
109    let d_smoothed = sma(slow_k_slice, d_period);
110
111    let mut d_values = vec![None; len];
112    for (j, val) in d_smoothed.into_iter().enumerate() {
113        let idx = j + slow_k_valid_start;
114        if idx < len {
115            d_values[idx] = val;
116        }
117    }
118
119    Ok(StochasticResult {
120        k: slow_k,
121        d: d_values,
122    })
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_stochastic_no_k_slow() {
131        let highs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
132        let lows = vec![8.0, 9.0, 10.0, 11.0, 12.0];
133        let closes = vec![9.0, 10.0, 11.0, 12.0, 13.0];
134        let result = stochastic(&highs, &lows, &closes, 3, 1, 2).unwrap();
135
136        assert_eq!(result.k.len(), 5);
137        assert_eq!(result.d.len(), 5);
138
139        // raw %K valid from index 2
140        assert!(result.k[0].is_none());
141        assert!(result.k[1].is_none());
142        assert!(result.k[2].is_some());
143
144        // %D valid from index 2 + (2-1) = 3 (k_slow=1 means no additional delay)
145        assert!(result.d[0].is_none());
146        assert!(result.d[1].is_none());
147        assert!(result.d[2].is_none());
148        assert!(result.d[3].is_some());
149    }
150
151    #[test]
152    fn test_stochastic_with_k_slow() {
153        let highs = vec![10.0; 10];
154        let lows = vec![8.0; 10];
155        let closes = vec![9.0; 10];
156        // k_period=3, k_slow=3, d_period=3: slow k valid from idx 4, d from idx 6
157        let result = stochastic(&highs, &lows, &closes, 3, 3, 3).unwrap();
158        // raw k valid from 2; slow k starts 2+2=4; d starts 4+2=6
159        assert!(result.k[3].is_none());
160        assert!(result.k[4].is_some());
161        assert!(result.d[5].is_none());
162        assert!(result.d[6].is_some());
163    }
164
165    #[test]
166    fn test_stochastic_k_slow_produces_different_k_than_no_slow() {
167        // Alternating high/low closes make raw %K oscillate, so SMA smoothing produces
168        // a noticeably different value than the unsmoothed raw %K.
169        let closes: Vec<f64> = (0..20)
170            .map(|i| if i % 2 == 0 { 10.0 } else { 20.0 })
171            .collect();
172        let highs: Vec<f64> = closes.iter().map(|&c| c + 0.5).collect();
173        let lows: Vec<f64> = closes.iter().map(|&c| c - 0.5).collect();
174
175        // fast: no k_slow smoothing — reads raw %K at each bar
176        let fast = stochastic(&highs, &lows, &closes, 5, 1, 3).unwrap();
177        // slow: SMA(3) over raw %K — averages three oscillating values
178        let slow = stochastic(&highs, &lows, &closes, 5, 3, 3).unwrap();
179
180        // Both must be valid at index 10; slow starts at 4 + (3-1) = 6
181        let idx = 10;
182        assert!(fast.k[idx].is_some());
183        assert!(slow.k[idx].is_some());
184        // raw %K oscillates ~4.5 / ~95.5; SMA-3 of those three values ≈ 34.8 ≠ raw value
185        assert_ne!(fast.k[idx], slow.k[idx]);
186    }
187}