Skip to main content

finance_query/indicators/
stochastic.rs

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