Skip to main content

egui_charts/studies/builtin/
stochastic.rs

1//! Stochastic Oscillator Indicator
2//!
3//! The Stochastic Oscillator is a momentum indicator comparing the closing price
4//! to the range of prices over a specified period. It generates overbought/oversold
5//! signals when %K crosses above 80 or below 20.
6//!
7//! # Components
8//! - %K (Fast): Main oscillator line
9//! - %D (Slow): Signal line (SMA of %K)
10//!
11//! # Calculation
12//! %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)
13//! %D = SMA(%K, d_period)
14//!
15//! # Example
16//! ```
17//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
18//! use egui_charts::studies::{Stochastic, Indicator};
19//!
20//! let mut stoch = Stochastic::new(14, 3, 3);
21//! stoch.calculate(&bars);
22//! ```
23
24use super::over_finite_bars;
25use crate::model::Bar;
26use crate::studies::helpers;
27use crate::studies::palette;
28use crate::studies::stepper::{rolling_max, rolling_min, update_window, usable_behind};
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32/// Stochastic Oscillator indicator
33#[derive(Clone)]
34pub struct Stochastic {
35    k_period: usize,
36    k_smooth: usize,
37    d_period: usize,
38    values: IndicatorValues,
39    k_color: Color32,
40    d_color: Color32,
41    visible: bool,
42}
43
44impl Stochastic {
45    /// Create a new Stochastic Oscillator
46    ///
47    /// # Arguments
48    /// * `k_period` - Lookback period for %K (typically 14)
49    /// * `k_smooth` - Smoothing period for %K (typically 3)
50    /// * `d_period` - Period for %D signal line (typically 3)
51    pub fn new(k_period: usize, k_smooth: usize, d_period: usize) -> Self {
52        Self {
53            k_period: k_period.max(1),
54            k_smooth: k_smooth.max(1),
55            d_period: d_period.max(1),
56            values: IndicatorValues::new(),
57            k_color: palette::BLUE,
58            d_color: palette::ORANGE,
59            visible: true,
60        }
61    }
62
63    /// Create with default params (14, 3, 3)
64    pub fn default_params() -> Self {
65        Self::new(14, 3, 3)
66    }
67
68    /// Set colors for %K and %D lines
69    pub fn with_colors(mut self, k_color: Color32, d_color: Color32) -> Self {
70        self.k_color = k_color;
71        self.d_color = d_color;
72        self
73    }
74}
75
76/// Construct with the conventional default parameters.
77impl Default for Stochastic {
78    fn default() -> Self {
79        Self::new(14, 3, 3)
80    }
81}
82
83impl Stochastic {
84    /// The values over `data`, every bar of which has finite inputs.
85    fn compute(&self, data: &[Bar]) -> IndicatorValues {
86        let mut values = IndicatorValues::new();
87        let min_period = self.k_period.saturating_add(self.k_smooth) - 1;
88        if data.len() < min_period {
89            for _ in 0..data.len() {
90                values.push(IndicatorValue::None);
91            }
92            return values;
93        }
94
95        // Calculate raw %K (Fast Stochastic)
96        let highest = rolling_max(data.iter().map(|b| b.high), self.k_period);
97        let lowest = rolling_min(data.iter().map(|b| b.low), self.k_period);
98        let mut raw_k = Vec::with_capacity(data.len());
99        for i in 0..data.len() {
100            if i < self.k_period - 1 {
101                raw_k.push(f64::NAN);
102            } else {
103                let (highest, lowest) = (highest[i], lowest[i]);
104                let range = highest - lowest;
105
106                if range == 0.0 {
107                    raw_k.push(50.0); // Middle value when no range
108                } else {
109                    let k = 100.0 * (data[i].close - lowest) / range;
110                    raw_k.push(k.clamp(0.0, 100.0));
111                }
112            }
113        }
114
115        let smooth_k = helpers::sma(&raw_k, self.k_smooth);
116        let d_values = helpers::sma(&smooth_k, self.d_period);
117
118        // Build output values
119        for i in 0..data.len() {
120            if smooth_k[i].is_nan() {
121                values.push(IndicatorValue::None);
122            } else {
123                values.push_lines(&[smooth_k[i], d_values[i]]);
124            }
125        }
126
127        values
128    }
129}
130
131impl Indicator for Stochastic {
132    crate::studies::inputs::study_inputs!(self;
133        k_period: length("K period", 1, usize::MAX),
134        k_smooth: length("K smooth", 1, usize::MAX),
135        d_period: length("D period", 1, usize::MAX)
136    ; {});
137
138    fn name(&self) -> &str {
139        "Stochastic"
140    }
141
142    fn precision(&self) -> Option<usize> {
143        Some(2)
144    }
145
146    fn desc(&self) -> &str {
147        "Stochastic Oscillator - Momentum oscillator (0-100)"
148    }
149
150    fn calculate(&mut self, data: &[Bar]) {
151        self.update(data, 0);
152    }
153
154    fn update(&mut self, data: &[Bar], from: usize) {
155        let mut values = std::mem::take(&mut self.values);
156        let behind = (self.k_period - 1)
157            .saturating_add(self.k_smooth - 1)
158            .saturating_add(self.d_period - 1);
159        let usable = |b: &Bar| b.high.is_finite() && b.low.is_finite() && b.close.is_finite();
160        update_window(
161            &mut values,
162            data,
163            from,
164            usable_behind(behind, usable),
165            |data| over_finite_bars(data, usable, |data| self.compute(data)),
166        );
167        self.values = values;
168    }
169
170    fn is_incremental(&self) -> bool {
171        true
172    }
173
174    fn values(&self) -> &IndicatorValues {
175        &self.values
176    }
177
178    fn colors(&self) -> Vec<Color32> {
179        vec![self.k_color, self.d_color]
180    }
181
182    fn set_colors(&mut self, colors: Vec<Color32>) {
183        if !colors.is_empty() {
184            self.k_color = colors[0];
185        }
186        if colors.len() > 1 {
187            self.d_color = colors[1];
188        }
189    }
190
191    fn pane(&self) -> crate::studies::StudyPane {
192        crate::studies::StudyPane {
193            height: None,
194            range: Some((0.0, 100.0)),
195            levels: vec![
196                (80.0, palette::MUTED, "Overbought"),
197                (20.0, palette::MUTED, "Oversold"),
198            ],
199            fill_between_levels: false,
200        }
201    }
202
203    fn is_overlay(&self) -> bool {
204        false // Stochastic is drawn in separate pane
205    }
206
207    fn line_count(&self) -> usize {
208        2 // %K and %D
209    }
210
211    fn is_visible(&self) -> bool {
212        self.visible
213    }
214
215    fn set_visible(&mut self, visible: bool) {
216        self.visible = visible;
217    }
218
219    fn clone_box(&self) -> Box<dyn Indicator> {
220        Box::new(self.clone())
221    }
222
223    fn line_names(&self) -> Vec<String> {
224        vec![
225            format!("%K({},{},{})", self.k_period, self.k_smooth, self.d_period),
226            "%D".to_string(),
227        ]
228    }
229}