Skip to main content

egui_charts/studies/builtin/
smi.rs

1use super::smoothing::Smoothing;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::Stepper;
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Stochastic Momentum Index (SMI)
9/// Enhanced stochastic that measures close relative to HL range midpoint
10/// More responsive than standard stochastic
11#[derive(Clone)]
12pub struct StochasticMomentumIndex {
13    /// Lookback period (typically 10)
14    period: usize,
15    /// First EMA smoothing (typically 3)
16    smooth1: usize,
17    /// Second EMA smoothing (typically 3)
18    smooth2: usize,
19    /// Signal line period (typically 10)
20    signal_period: usize,
21    values: IndicatorValues,
22    stepper: Stepper<(std::collections::VecDeque<[f64; 2]>, [Smoothing; 5])>,
23    /// Colors: [SMI, Signal]
24    colors: Vec<Color32>,
25    visible: bool,
26}
27
28impl StochasticMomentumIndex {
29    /// Create with `period`, `smooth1`, `smooth2`, `signal_period` in bars, each clamped to at
30    /// least one.
31    pub fn new(period: usize, smooth1: usize, smooth2: usize, signal_period: usize) -> Self {
32        Self {
33            period: period.max(1),
34            smooth1: smooth1.max(1),
35            smooth2: smooth2.max(1),
36            signal_period: signal_period.max(1),
37            values: IndicatorValues::new(),
38            stepper: Stepper::default(),
39            colors: vec![
40                palette::BLUE,        // SMI
41                palette::WARM_ORANGE, // Signal
42            ],
43            visible: true,
44        }
45    }
46
47    /// Set plot colors in the order returned by [`Indicator::line_names`].
48    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
49        self.colors = colors;
50        self
51    }
52}
53
54impl Default for StochasticMomentumIndex {
55    fn default() -> Self {
56        Self::new(10, 3, 3, 10)
57    }
58}
59
60impl Indicator for StochasticMomentumIndex {
61    crate::studies::inputs::study_inputs!(self;
62        period: length("Period", 1, usize::MAX),
63        smooth1: length("Smooth1", 1, usize::MAX),
64        smooth2: length("Smooth2", 1, usize::MAX),
65        signal_period: length("Signal period", 1, usize::MAX)
66    ; {});
67
68    fn name(&self) -> &str {
69        "SMI"
70    }
71
72    fn desc(&self) -> &str {
73        "Stochastic Momentum Index - Enhanced stochastic"
74    }
75
76    fn calculate(&mut self, data: &[Bar]) {
77        self.update(data, 0);
78    }
79
80    fn update(&mut self, data: &[Bar], from: usize) {
81        self.stepper
82            .run(data, from, &mut self.values, |state, bar| {
83                if ![bar.high, bar.low, bar.close].iter().all(|x| x.is_finite()) {
84                    return IndicatorValue::None;
85                }
86                state.0.push_back([bar.high, bar.low]);
87                if state.0.len() > self.period {
88                    state.0.pop_front();
89                }
90                if state.0.len() < self.period {
91                    return IndicatorValue::None;
92                }
93                let high = state
94                    .0
95                    .iter()
96                    .map(|v| v[0])
97                    .fold(f64::NEG_INFINITY, f64::max);
98                let low = state.0.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min);
99                let distance = bar.close - (high + low) / 2.0;
100                let range = high - low;
101                let a1 = 2.0 / (self.smooth1 as f64 + 1.0);
102                let a2 = 2.0 / (self.smooth2 as f64 + 1.0);
103                let d1 = state.1[0].next(distance, 1, a1);
104                let d2 = state.1[1].next(d1, 1, a2);
105                let r1 = state.1[2].next(range, 1, a1);
106                let r2 = state.1[3].next(r1, 1, a2);
107                let smi = if d2.is_nan() || r2.is_nan() || r2 == 0.0 {
108                    f64::NAN
109                } else {
110                    100.0 * d2 / (r2 / 2.0)
111                };
112                let signal = state.1[4].next(smi, 1, 2.0 / (self.signal_period as f64 + 1.0));
113                if smi.is_nan() {
114                    IndicatorValue::None
115                } else {
116                    IndicatorValue::Multiple(vec![smi, signal])
117                }
118            });
119    }
120
121    fn is_incremental(&self) -> bool {
122        true
123    }
124
125    fn values(&self) -> &IndicatorValues {
126        &self.values
127    }
128
129    fn colors(&self) -> Vec<Color32> {
130        self.colors.clone()
131    }
132
133    fn set_colors(&mut self, colors: Vec<Color32>) {
134        if colors.len() >= 2 {
135            self.colors = colors;
136        }
137    }
138
139    fn is_overlay(&self) -> bool {
140        false
141    }
142
143    fn is_visible(&self) -> bool {
144        self.visible
145    }
146
147    fn set_visible(&mut self, visible: bool) {
148        self.visible = visible;
149    }
150
151    fn clone_box(&self) -> Box<dyn Indicator> {
152        Box::new(self.clone())
153    }
154
155    fn line_names(&self) -> Vec<String> {
156        vec![
157            format!("SMI({},{},{})", self.period, self.smooth1, self.smooth2),
158            format!("Signal({})", self.signal_period),
159        ]
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::studies::test_support::make_range_bar as make_bar;
167
168    #[test]
169    fn test_smi_calculation() {
170        let mut smi = StochasticMomentumIndex::new(5, 2, 2, 3);
171
172        let data: Vec<Bar> = (0..20)
173            .map(|i| make_bar(102.0 + i as f64, 98.0 + i as f64, 101.0 + i as f64))
174            .collect();
175
176        smi.calculate(&data);
177
178        assert_eq!(smi.values.len(), 20);
179
180        // Should have valid values
181        let valid_cnt = smi
182            .values
183            .to_rows()
184            .iter()
185            .filter(|v| !matches!(v, IndicatorValue::None))
186            .count();
187
188        assert!(valid_cnt > 0);
189    }
190}