egui_charts/studies/builtin/
smi.rs1use 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#[derive(Clone)]
12pub struct StochasticMomentumIndex {
13 period: usize,
15 smooth1: usize,
17 smooth2: usize,
19 signal_period: usize,
21 values: IndicatorValues,
22 stepper: Stepper<(std::collections::VecDeque<[f64; 2]>, [Smoothing; 5])>,
23 colors: Vec<Color32>,
25 visible: bool,
26}
27
28impl StochasticMomentumIndex {
29 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, palette::WARM_ORANGE, ],
43 visible: true,
44 }
45 }
46
47 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 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}