indexes_rs/v1/stochastic/
main.rs

1//! # Stochastic Oscillator Module
2//!
3//! This module implements a Stochastic Oscillator indicator. It computes %K and %D values based
4//! on a sliding window of prices. The oscillator can be smoothed using a `%K smoothing` parameter.
5//!
6//! The indicator also generates a trading signal, a condition, a crossover type, and a strength value.
7//!
8//! # Example
9//!
10//! ```rust
11//! use indexes_rs::v1::stochastic::main::StochasticOscillator;
12//! use indexes_rs::v1::stochastic::types::{StochResult, StochSignal, StochCondition, StochCrossover};
13//!
14//! let mut stoch = StochasticOscillator::new(
15//!     StochasticOscillator::DEFAULT_PERIOD,
16//!     StochasticOscillator::DEFAULT_K_SMOOTH,
17//!     StochasticOscillator::DEFAULT_D_PERIOD
18//! );
19//!
20//! // Feed in a series of prices (for example, closing prices).
21//! let prices = vec![100.0, 102.0, 101.5, 103.0, 104.0, 102.5, 101.0, 100.5, 99.5, 98.0, 97.5, 98.5];
22//! let mut result: Option<StochResult> = None;
23//! for price in prices {
24//!     result = stoch.calculate(price);
25//! }
26//!
27//! if let Some(res) = result {
28//!     println!("K value: {:.2}", res.k_value);
29//!     println!("D value: {:.2}", res.d_value);
30//!     println!("Signal: {:?}", res.signal);
31//!     println!("Condition: {:?}", res.condition);
32//!     println!("Crossover: {:?}", res.crossover);
33//!     println!("Strength: {:.2}%", res.strength);
34//! }
35//! ```
36
37use super::types::*; // This module should define StochResult, StochSignal, StochCondition, and StochCrossover.
38
39/// A Stochastic Oscillator indicator.
40pub struct StochasticOscillator {
41    period: usize,   // %K period (typically 14)
42    k_smooth: usize, // %K smoothing period (typically 3)
43    d_period: usize, // %D period (typically 3)
44    prices: Vec<f64>,
45    highs: Vec<f64>,
46    lows: Vec<f64>,
47    /// Stores the smoothed %K values (used for %D calculation).
48    k_values: Vec<f64>,
49    /// Stores raw %K values for smoothing calculation.
50    raw_k_values: Vec<f64>,
51}
52
53impl StochasticOscillator {
54    /// Default %K period (typically 14).
55    pub const DEFAULT_PERIOD: usize = 14;
56    /// Default %K smoothing period (typically 3).
57    pub const DEFAULT_K_SMOOTH: usize = 3;
58    /// Default %D period (typically 3).
59    pub const DEFAULT_D_PERIOD: usize = 3;
60
61    const OVERBOUGHT_THRESHOLD: f64 = 80.0;
62    const OVERSOLD_THRESHOLD: f64 = 20.0;
63
64    /// Creates a new StochasticOscillator with the given parameters.
65    ///
66    /// # Arguments
67    ///
68    /// * `period` - The period for %K calculation.
69    /// * `k_smooth` - The smoothing period for %K.
70    /// * `d_period` - The period for %D calculation.
71    pub fn new(period: usize, k_smooth: usize, d_period: usize) -> Self {
72        StochasticOscillator {
73            period,
74            k_smooth,
75            d_period,
76            prices: Vec::new(),
77            highs: Vec::new(),
78            lows: Vec::new(),
79            k_values: Vec::new(),
80            raw_k_values: Vec::new(),
81        }
82    }
83
84    /// Updates the oscillator with a new price and returns the current oscillator result.
85    ///
86    /// The oscillator calculates the raw %K value based on the current window of prices,
87    /// applies smoothing if enough values exist, and then computes the %D value. It also
88    /// generates a trading signal, a condition, and detects a crossover.
89    ///
90    /// # Arguments
91    ///
92    /// * `price` - The latest price.
93    ///
94    /// # Returns
95    ///
96    /// * `Some(StochResult)` if there is sufficient data, else `None`.
97    pub fn calculate(&mut self, price: f64) -> Option<StochResult> {
98        // Update price history (for simplicity, highs and lows are the same as price).
99        self.prices.push(price);
100        self.highs.push(price);
101        self.lows.push(price);
102
103        // Keep sliding window for price/high/low arrays.
104        if self.prices.len() > self.period {
105            self.prices.remove(0);
106            self.highs.remove(0);
107            self.lows.remove(0);
108        }
109
110        if self.prices.len() < self.period {
111            return None;
112        }
113
114        // Determine the highest high and lowest low in the window.
115        let highest_high = self.highs.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
116        let lowest_low = self.lows.iter().fold(f64::INFINITY, |a, &b| a.min(b));
117
118        // Compute raw %K value. If highest equals lowest, use 50.
119        let raw_k = if highest_high == lowest_low {
120            50.0
121        } else {
122            ((price - lowest_low) / (highest_high - lowest_low)) * 100.0
123        };
124
125        // Store raw %K for smoothing.
126        self.raw_k_values.push(raw_k);
127        if self.raw_k_values.len() > self.k_smooth {
128            self.raw_k_values.remove(0);
129        }
130
131        // Compute the smoothed %K value if we have enough raw values; otherwise, use raw_k.
132        let k = if self.raw_k_values.len() < self.k_smooth {
133            raw_k
134        } else {
135            self.raw_k_values.iter().sum::<f64>() / self.k_smooth as f64
136        };
137
138        // Store the smoothed %K value for %D calculation.
139        self.k_values.push(k);
140        if self.k_values.len() > self.d_period {
141            self.k_values.remove(0);
142        }
143
144        let d = self.calculate_d();
145        let signal = self.generate_signal(k, d);
146        let condition = self.determine_condition(k);
147        let crossover = self.detect_crossover(k, d);
148        let strength = self.calculate_strength(k, d);
149
150        Some(StochResult {
151            k_value: k,
152            d_value: d,
153            signal,
154            condition,
155            crossover,
156            strength,
157        })
158    }
159
160    /// Calculates the %D value as the average of the last `d_period` %K values.
161    fn calculate_d(&self) -> f64 {
162        if self.k_values.len() < self.d_period {
163            self.k_values.last().copied().unwrap_or(50.0)
164        } else {
165            self.k_values.iter().sum::<f64>() / self.d_period as f64
166        }
167    }
168
169    /// Generates a trading signal based on %K and %D.
170    fn generate_signal(&self, k: f64, d: f64) -> StochSignal {
171        match (k, d) {
172            (k, d) if k > d && k < Self::OVERBOUGHT_THRESHOLD => StochSignal::Buy,
173            (k, d) if k < d && k > Self::OVERSOLD_THRESHOLD => StochSignal::Sell,
174            (k, _) if k >= Self::OVERBOUGHT_THRESHOLD => StochSignal::Overbought,
175            (k, _) if k <= Self::OVERSOLD_THRESHOLD => StochSignal::Oversold,
176            _ => StochSignal::Neutral,
177        }
178    }
179
180    /// Determines the current condition of the oscillator based on %K.
181    fn determine_condition(&self, k: f64) -> StochCondition {
182        match k {
183            k if k >= Self::OVERBOUGHT_THRESHOLD => StochCondition::Overbought,
184            k if k <= Self::OVERSOLD_THRESHOLD => StochCondition::Oversold,
185            k if k > 50.0 => StochCondition::Strong,
186            k if k < 50.0 => StochCondition::Weak,
187            _ => StochCondition::Neutral,
188        }
189    }
190
191    /// Detects if a crossover occurred between %K and %D.
192    ///
193    /// A bullish crossover is detected when %K crosses above %D, and bearish when %K crosses below %D.
194    fn detect_crossover(&self, k: f64, d: f64) -> StochCrossover {
195        if self.k_values.len() < 2 {
196            return StochCrossover::None;
197        }
198        let prev_k = self.k_values[self.k_values.len() - 2];
199        if k > d && prev_k <= d {
200            StochCrossover::Bullish
201        } else if k < d && prev_k >= d {
202            StochCrossover::Bearish
203        } else {
204            StochCrossover::None
205        }
206    }
207
208    /// Calculates an overall strength value (0-100) for the oscillator.
209    ///
210    /// Combines the deviation of %K from 50 and the absolute difference between %K and %D.
211    fn calculate_strength(&self, k: f64, d: f64) -> f64 {
212        let trend_strength = (k - 50.0).abs() / 50.0;
213        let momentum = (k - d).abs() / 20.0; // Normalized difference
214        ((trend_strength + momentum) / 2.0 * 100.0).min(100.0)
215    }
216}