indexes_rs/v2/stochastic/
main.rs

1use crate::v2::stochastic::types::StochasticSignal;
2
3use super::types::{OHLCData, StochasticError, StochasticResult};
4use std::collections::VecDeque;
5
6/// Stochastic Oscillator Calculator
7///
8/// The Stochastic Oscillator is a momentum indicator that shows the location of the close
9/// relative to the high-low range over a set number of periods.
10///
11/// %K = (Current Close - Lowest Low) / (Highest High - Lowest Low) * 100
12/// %D = SMA of %K over smoothing period
13///
14/// Typical settings: (14, 3, 3) or (5, 3, 3) for faster signals
15pub struct StochasticOscillator {
16    period: usize,               // Lookback period for highest high and lowest low
17    k_smooth: usize,             // Smoothing period for %K (typically 3)
18    d_period: usize,             // Period for %D (SMA of %K, typically 3)
19    highs: VecDeque<f64>,        // Rolling window of highs
20    lows: VecDeque<f64>,         // Rolling window of lows
21    closes: VecDeque<f64>,       // Rolling window of closes
22    k_values: VecDeque<f64>,     // Rolling window of %K values for %D calculation
23    raw_k_values: VecDeque<f64>, // Raw %K values for smoothing
24}
25
26impl StochasticOscillator {
27    /// Creates a new Stochastic Oscillator with specified parameters
28    ///
29    /// # Arguments
30    /// * `period` - Lookback period for high/low (typically 14)
31    /// * `k_smooth` - Smoothing period for %K (typically 3)
32    /// * `d_period` - Period for %D calculation (typically 3)
33    pub fn new(period: usize, k_smooth: usize, d_period: usize) -> Result<Self, StochasticError> {
34        if period == 0 {
35            return Err(StochasticError::InvalidPeriod);
36        }
37        if k_smooth == 0 || d_period == 0 {
38            return Err(StochasticError::InvalidSmoothingPeriod);
39        }
40
41        Ok(Self {
42            period,
43            k_smooth,
44            d_period,
45            highs: VecDeque::with_capacity(period),
46            lows: VecDeque::with_capacity(period),
47            closes: VecDeque::with_capacity(k_smooth),
48            k_values: VecDeque::with_capacity(d_period),
49            raw_k_values: VecDeque::with_capacity(k_smooth),
50        })
51    }
52
53    /// Creates a new Stochastic Oscillator with default parameters (14, 3, 3)
54    pub fn default() -> Self {
55        Self::new(14, 3, 3).unwrap()
56    }
57
58    /// Updates the oscillator with OHLC data
59    pub fn update(&mut self, data: OHLCData) -> Option<StochasticResult> {
60        // Validate input
61        if !self.validate_ohlc(&data) {
62            return None;
63        }
64
65        // Add to rolling windows
66        self.add_to_windows(data.high, data.low, data.close);
67
68        // Calculate if we have enough data
69        if self.highs.len() >= self.period {
70            self.calculate()
71        } else {
72            None
73        }
74    }
75
76    /// Updates with separate high, low, close values
77    pub fn update_hlc(&mut self, high: f64, low: f64, close: f64) -> Option<StochasticResult> {
78        self.update(OHLCData::new(high, low, close))
79    }
80
81    /// Returns the current Stochastic values
82    pub fn value(&self) -> Option<StochasticResult> {
83        if self.k_values.is_empty() {
84            return None;
85        }
86
87        let k = *self.k_values.back()?;
88        let d = if self.k_values.len() >= self.d_period {
89            self.k_values.iter().rev().take(self.d_period).sum::<f64>() / self.d_period as f64
90        } else {
91            self.k_values.iter().sum::<f64>() / self.k_values.len() as f64
92        };
93
94        Some(StochasticResult { k, d })
95    }
96
97    /// Resets the oscillator
98    pub fn reset(&mut self) {
99        self.highs.clear();
100        self.lows.clear();
101        self.closes.clear();
102        self.k_values.clear();
103        self.raw_k_values.clear();
104    }
105
106    /// Checks if the oscillator is ready (has enough data)
107    pub fn is_ready(&self) -> bool {
108        self.highs.len() >= self.period && !self.k_values.is_empty()
109    }
110
111    /// Get the period
112    pub fn period(&self) -> usize {
113        self.period
114    }
115
116    /// Batch calculation for historical data
117    pub fn calculate_batch(period: usize, k_smooth: usize, d_period: usize, data: &[OHLCData]) -> Result<Vec<Option<StochasticResult>>, StochasticError> {
118        let mut stoch = Self::new(period, k_smooth, d_period)?;
119        let mut results = Vec::with_capacity(data.len());
120
121        for ohlc in data {
122            results.push(stoch.update(*ohlc));
123        }
124
125        Ok(results)
126    }
127
128    /// Fast Stochastic (no smoothing)
129    pub fn fast(period: usize) -> Result<Self, StochasticError> {
130        Self::new(period, 1, 3)
131    }
132
133    /// Slow Stochastic (standard smoothing)
134    pub fn slow(period: usize) -> Result<Self, StochasticError> {
135        Self::new(period, 3, 3)
136    }
137
138    /// Full Stochastic (customizable smoothing)
139    pub fn full(period: usize, k_smooth: usize, d_period: usize) -> Result<Self, StochasticError> {
140        Self::new(period, k_smooth, d_period)
141    }
142
143    // === Private methods ===
144
145    fn validate_ohlc(&self, data: &OHLCData) -> bool {
146        // Check for NaN or Infinite
147        if data.high.is_nan() || data.low.is_nan() || data.close.is_nan() {
148            return false;
149        }
150        if data.high.is_infinite() || data.low.is_infinite() || data.close.is_infinite() {
151            return false;
152        }
153        // Check OHLC relationship
154        if data.high < data.low {
155            return false;
156        }
157        // Close should be between high and low (with small tolerance for rounding)
158        let tolerance = 0.0001;
159        if data.close > data.high + tolerance || data.close < data.low - tolerance {
160            return false;
161        }
162        true
163    }
164
165    fn add_to_windows(&mut self, high: f64, low: f64, close: f64) {
166        // Add to highs window
167        if self.highs.len() >= self.period {
168            self.highs.pop_front();
169        }
170        self.highs.push_back(high);
171
172        // Add to lows window
173        if self.lows.len() >= self.period {
174            self.lows.pop_front();
175        }
176        self.lows.push_back(low);
177
178        // Add to closes window (for smoothing)
179        if self.closes.len() >= self.k_smooth {
180            self.closes.pop_front();
181        }
182        self.closes.push_back(close);
183    }
184
185    fn calculate(&mut self) -> Option<StochasticResult> {
186        // Find highest high and lowest low in the period
187        let highest_high = self.highs.iter().fold(f64::MIN, |a, &b| a.max(b));
188        let lowest_low = self.lows.iter().fold(f64::MAX, |a, &b| a.min(b));
189
190        // Calculate raw %K
191        let range = highest_high - lowest_low;
192        let raw_k = if range > 0.0 {
193            let current_close = *self.closes.back()?;
194            ((current_close - lowest_low) / range) * 100.0
195        } else {
196            50.0 // If range is 0 (all prices are the same), use 50%
197        };
198
199        // Add to raw K values for smoothing
200        if self.raw_k_values.len() >= self.k_smooth {
201            self.raw_k_values.pop_front();
202        }
203        self.raw_k_values.push_back(raw_k);
204
205        // Calculate smoothed %K if we have enough values
206        if self.raw_k_values.len() >= self.k_smooth {
207            let smoothed_k = self.raw_k_values.iter().sum::<f64>() / self.k_smooth as f64;
208
209            // Add to K values for %D calculation
210            if self.k_values.len() >= self.d_period {
211                self.k_values.pop_front();
212            }
213            self.k_values.push_back(smoothed_k);
214
215            // Calculate %D
216            let d = if self.k_values.len() >= self.d_period {
217                self.k_values.iter().sum::<f64>() / self.d_period as f64
218            } else {
219                self.k_values.iter().sum::<f64>() / self.k_values.len() as f64
220            };
221
222            Some(StochasticResult { k: smoothed_k, d })
223        } else {
224            None
225        }
226    }
227
228    /// Get crossover signal
229    pub fn signal(&self) -> Option<StochasticSignal> {
230        let result = self.value()?;
231
232        if result.k > 80.0 && result.d > 80.0 {
233            Some(StochasticSignal::Overbought)
234        } else if result.k < 20.0 && result.d < 20.0 {
235            Some(StochasticSignal::Oversold)
236        } else if result.k > result.d {
237            Some(StochasticSignal::Bullish)
238        } else if result.k < result.d {
239            Some(StochasticSignal::Bearish)
240        } else {
241            Some(StochasticSignal::Neutral)
242        }
243    }
244}