1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::ema::Ema;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
44pub struct Smi {
45 period: usize,
46 d_period: usize,
47 d2_period: usize,
48 highs: VecDeque<f64>,
49 lows: VecDeque<f64>,
50 ema_d1: Ema,
51 ema_d2: Ema,
52 ema_r1: Ema,
53 ema_r2: Ema,
54 current: Option<f64>,
55}
56
57impl Smi {
58 pub fn new(period: usize, d_period: usize, d2_period: usize) -> Result<Self> {
61 if period == 0 || d_period == 0 || d2_period == 0 {
62 return Err(Error::PeriodZero);
63 }
64 Ok(Self {
65 period,
66 d_period,
67 d2_period,
68 highs: VecDeque::with_capacity(period),
69 lows: VecDeque::with_capacity(period),
70 ema_d1: Ema::new(d_period)?,
71 ema_d2: Ema::new(d2_period)?,
72 ema_r1: Ema::new(d_period)?,
73 ema_r2: Ema::new(d2_period)?,
74 current: None,
75 })
76 }
77
78 pub fn classic() -> Self {
80 Self::new(5, 3, 3).expect("classic SMI parameters are valid")
81 }
82
83 pub const fn periods(&self) -> (usize, usize, usize) {
85 (self.period, self.d_period, self.d2_period)
86 }
87}
88
89impl Indicator for Smi {
90 type Input = Candle;
91 type Output = f64;
92
93 #[inline]
94 fn update(&mut self, candle: Candle) -> Option<f64> {
95 if self.highs.len() == self.period {
96 self.highs.pop_front();
97 self.lows.pop_front();
98 }
99 self.highs.push_back(candle.high);
100 self.lows.push_back(candle.low);
101 if self.highs.len() < self.period {
102 return None;
103 }
104 let hh = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
105 let ll = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
106 let center = f64::midpoint(hh, ll);
107 let displacement = candle.close - center;
108 let range = hh - ll;
109
110 let d1 = self.ema_d1.update(displacement);
114 let r1 = self.ema_r1.update(range);
115 let d2 = d1.and_then(|x| self.ema_d2.update(x));
116 let r2 = r1.and_then(|x| self.ema_r2.update(x));
117 let (d2, r2) = (d2?, r2?);
118
119 if r2 <= 0.0 {
120 return self.current;
123 }
124 let value = 100.0 * d2 / (r2 / 2.0);
125 self.current = Some(value);
126 Some(value)
127 }
128
129 fn reset(&mut self) {
130 self.highs.clear();
131 self.lows.clear();
132 self.ema_d1.reset();
133 self.ema_d2.reset();
134 self.ema_r1.reset();
135 self.ema_r2.reset();
136 self.current = None;
137 }
138
139 #[inline]
140 fn warmup_period(&self) -> usize {
141 self.period + self.d_period + self.d2_period - 2
144 }
145
146 #[inline]
147 fn is_ready(&self) -> bool {
148 self.current.is_some()
149 }
150
151 #[inline]
152 fn name(&self) -> &'static str {
153 "SMI"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::traits::BatchExt;
161 use approx::assert_relative_eq;
162
163 fn candle(high: f64, low: f64, close: f64, ts: i64) -> Candle {
164 Candle::new(close, high, low, close, 1.0, ts).unwrap()
165 }
166
167 #[test]
168 fn rejects_zero_period() {
169 assert!(matches!(Smi::new(0, 3, 3), Err(Error::PeriodZero)));
170 assert!(matches!(Smi::new(5, 0, 3), Err(Error::PeriodZero)));
171 assert!(matches!(Smi::new(5, 3, 0), Err(Error::PeriodZero)));
172 }
173
174 #[test]
175 fn accessors_and_metadata() {
176 let smi = Smi::new(5, 3, 3).unwrap();
177 assert_eq!(smi.periods(), (5, 3, 3));
178 assert_eq!(smi.warmup_period(), 9);
179 assert_eq!(smi.name(), "SMI");
180 }
181
182 #[test]
183 fn classic_factory() {
184 let smi = Smi::classic();
185 assert_eq!(smi.periods(), (5, 3, 3));
186 }
187
188 #[test]
189 fn close_at_high_pushes_toward_plus_100() {
190 let mut smi = Smi::classic();
195 let mut last = None;
196 for i in 0..80 {
197 let h = 100.0 + f64::from(i);
198 let l = h - 2.0;
199 last = smi.update(candle(h, l, h, i64::from(i)));
200 }
201 let v = last.expect("SMI is warm");
202 assert!(
203 v > 50.0,
204 "close-at-high series should drive SMI well above 0: {v}"
205 );
206 }
207
208 #[test]
209 fn close_at_low_pushes_toward_minus_100() {
210 let mut smi = Smi::classic();
211 let mut last = None;
212 for i in 0..80 {
213 let h = 100.0 - f64::from(i);
214 let l = h - 2.0;
215 last = smi.update(candle(h, l, l, i64::from(i)));
216 }
217 let v = last.expect("SMI is warm");
218 assert!(
219 v < -50.0,
220 "close-at-low series should drive SMI well below 0: {v}"
221 );
222 }
223
224 #[test]
225 fn warmup_emits_first_value_at_warmup_period() {
226 let mut smi = Smi::new(3, 2, 2).unwrap();
227 assert_eq!(smi.warmup_period(), 5);
229 let mut got = None;
230 for i in 0..5 {
231 got = smi.update(candle(11.0, 9.0, 10.0, i));
232 }
233 assert!(got.is_some());
234 }
235
236 #[test]
237 fn flat_close_yields_zero_displacement() {
238 let mut smi = Smi::classic();
241 let mut last = None;
242 for i in 0..60 {
243 last = smi.update(candle(11.0, 9.0, 10.0, i));
245 }
246 let v = last.unwrap();
247 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
248 }
249
250 #[test]
251 fn batch_equals_streaming() {
252 let candles: Vec<Candle> = (0..80_i64)
253 .map(|i| {
254 let c = 100.0 + (i as f64 * 0.3).sin() * 8.0;
255 candle(c + 1.0, c - 1.0, c, i)
256 })
257 .collect();
258 let batch = Smi::classic().batch(&candles);
259 let mut b = Smi::classic();
260 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
261 assert_eq!(batch, streamed);
262 }
263
264 #[test]
265 fn reset_clears_state() {
266 let mut smi = Smi::classic();
267 for i in 0..40 {
268 smi.update(candle(11.0, 9.0, 10.0, i));
269 }
270 assert!(smi.is_ready());
271 smi.reset();
272 assert!(!smi.is_ready());
273 }
274
275 #[test]
276 fn zero_range_holds_previous_value() {
277 let mut smi = Smi::new(3, 2, 2).unwrap();
283 for i in 0..7 {
285 let v = smi.update(candle(10.0, 10.0, 10.0, i));
286 assert_eq!(v, None, "zero-range SMI must hold None, got {v:?}");
287 }
288 }
289}