kestrel_chartkit/indicator/
smi.rs1use std::collections::{HashMap, VecDeque};
2
3use crate::model::Bar;
4
5use super::smoothing::Ema;
6use super::{Indicator, IndicatorOutput};
7
8#[derive(Debug, Clone)]
46pub struct StochasticMomentumIndex {
47 len: usize,
48 smooth_1: usize,
49 smooth_2: usize,
50 signal_len: usize,
51 highs: VecDeque<f64>,
52 lows: VecDeque<f64>,
53 distance_1: Ema,
54 distance_2: Ema,
55 range_1: Ema,
56 range_2: Ema,
57 signal_ema: Ema,
58 observations: usize,
59 lines_published: usize,
60}
61
62impl StochasticMomentumIndex {
63 pub fn new(len: usize, smooth_1: usize, smooth_2: usize, signal_len: usize) -> Self {
64 let len = len.max(1);
65 let smooth_1 = smooth_1.max(1);
66 let smooth_2 = smooth_2.max(1);
67 let signal_len = signal_len.max(1);
68 Self {
69 len,
70 smooth_1,
71 smooth_2,
72 signal_len,
73 highs: VecDeque::with_capacity(len),
74 lows: VecDeque::with_capacity(len),
75 distance_1: Ema::new(smooth_1),
76 distance_2: Ema::new(smooth_2),
77 range_1: Ema::new(smooth_1),
78 range_2: Ema::new(smooth_2),
79 signal_ema: Ema::new(signal_len),
80 observations: 0,
81 lines_published: 0,
82 }
83 }
84
85 pub fn with_defaults() -> Self {
86 Self::new(10, 3, 3, 3)
87 }
88}
89
90impl Indicator for StochasticMomentumIndex {
91 fn name(&self) -> &str {
92 "smi"
93 }
94
95 fn warmup_period(&self) -> usize {
96 self.len + self.smooth_1 + self.smooth_2 - 2
97 }
98
99 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
100 self.highs.push_back(bar.high);
101 self.lows.push_back(bar.low);
102 if self.highs.len() > self.len {
103 self.highs.pop_front();
104 self.lows.pop_front();
105 }
106 if self.highs.len() < self.len {
107 return None;
108 }
109
110 let highest = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
111 let lowest = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
112 let range = highest - lowest;
113 let distance = bar.close - (highest + lowest) / 2.0;
114
115 let smoothed_distance = self.distance_2.update(self.distance_1.update(distance)?)?;
116 let smoothed_range = self.range_2.update(self.range_1.update(range)?)?;
117
118 self.observations += 1;
119 if self.observations < self.smooth_1 + self.smooth_2 - 1 {
120 return None;
121 }
122
123 let line = if smoothed_range.abs() > 0.0 {
124 200.0 * smoothed_distance / smoothed_range
125 } else {
126 0.0
127 };
128 self.lines_published += 1;
129
130 let mut extra = HashMap::new();
131 let signal = self.signal_ema.update(line)?;
132 if self.lines_published >= self.signal_len {
133 extra.insert("signal".to_string(), signal);
134 }
135
136 Some(IndicatorOutput::with_extra(line, extra))
137 }
138
139 fn reset(&mut self) {
140 self.highs.clear();
141 self.lows.clear();
142 self.distance_1.reset();
143 self.distance_2.reset();
144 self.range_1.reset();
145 self.range_2.reset();
146 self.signal_ema.reset();
147 self.observations = 0;
148 self.lines_published = 0;
149 }
150}