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}