wickra_core/indicators/
intraday_momentum_index.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
42pub struct IntradayMomentumIndex {
43 period: usize,
44 window: VecDeque<(f64, f64)>,
46 sum_gain: f64,
47 sum_loss: f64,
48}
49
50impl IntradayMomentumIndex {
51 pub fn new(period: usize) -> Result<Self> {
57 if period == 0 {
58 return Err(Error::PeriodZero);
59 }
60 Ok(Self {
61 period,
62 window: VecDeque::with_capacity(period),
63 sum_gain: 0.0,
64 sum_loss: 0.0,
65 })
66 }
67
68 pub const fn period(&self) -> usize {
70 self.period
71 }
72
73 pub fn value(&self) -> Option<f64> {
75 if self.window.len() != self.period {
76 return None;
77 }
78 let denom = self.sum_gain + self.sum_loss;
79 if denom == 0.0 {
80 Some(50.0)
81 } else {
82 Some(100.0 * self.sum_gain / denom)
83 }
84 }
85}
86
87impl Indicator for IntradayMomentumIndex {
88 type Input = Candle;
89 type Output = f64;
90
91 fn update(&mut self, candle: Candle) -> Option<f64> {
92 let body = candle.close - candle.open;
93 let gain = if body > 0.0 { body } else { 0.0 };
94 let loss = if body < 0.0 { -body } else { 0.0 };
95
96 if self.window.len() == self.period {
97 let (old_g, old_l) = self.window.pop_front().expect("window full");
98 self.sum_gain -= old_g;
99 self.sum_loss -= old_l;
100 }
101 self.window.push_back((gain, loss));
102 self.sum_gain += gain;
103 self.sum_loss += loss;
104 self.value()
105 }
106
107 fn reset(&mut self) {
108 self.window.clear();
109 self.sum_gain = 0.0;
110 self.sum_loss = 0.0;
111 }
112
113 fn warmup_period(&self) -> usize {
114 self.period
115 }
116
117 fn is_ready(&self) -> bool {
118 self.window.len() == self.period
119 }
120
121 fn name(&self) -> &'static str {
122 "IMI"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::BatchExt;
130 use approx::assert_relative_eq;
131
132 fn candle(open: f64, close: f64) -> Candle {
133 let hi = open.max(close) + 1.0;
134 let lo = open.min(close) - 1.0;
135 Candle::new(open, hi, lo, close, 1.0, 0).unwrap()
136 }
137
138 #[test]
139 fn rejects_zero_period() {
140 assert!(matches!(
141 IntradayMomentumIndex::new(0),
142 Err(Error::PeriodZero)
143 ));
144 }
145
146 #[test]
149 fn accessors_and_metadata() {
150 let imi = IntradayMomentumIndex::new(14).unwrap();
151 assert_eq!(imi.period(), 14);
152 assert_eq!(imi.warmup_period(), 14);
153 assert_eq!(imi.name(), "IMI");
154 }
155
156 #[test]
157 fn all_up_bodies_is_one_hundred() {
158 let mut imi = IntradayMomentumIndex::new(3).unwrap();
159 let bars = [candle(10.0, 11.0), candle(11.0, 13.0), candle(13.0, 14.0)];
160 let out = imi.batch(&bars);
161 assert!(out[0].is_none());
162 assert!(out[1].is_none());
163 assert_relative_eq!(out[2].unwrap(), 100.0, epsilon = 1e-12);
164 }
165
166 #[test]
167 fn all_down_bodies_is_zero() {
168 let mut imi = IntradayMomentumIndex::new(3).unwrap();
169 let bars = [candle(14.0, 13.0), candle(13.0, 11.0), candle(11.0, 10.0)];
170 assert_relative_eq!(imi.batch(&bars)[2].unwrap(), 0.0, epsilon = 1e-12);
171 }
172
173 #[test]
174 fn known_value_mixed_bodies() {
175 let mut imi = IntradayMomentumIndex::new(3).unwrap();
177 let bars = [candle(10.0, 11.0), candle(11.0, 10.0), candle(10.0, 12.0)];
178 assert_relative_eq!(imi.batch(&bars)[2].unwrap(), 75.0, epsilon = 1e-12);
179 }
180
181 #[test]
182 fn doji_window_is_neutral() {
183 let mut imi = IntradayMomentumIndex::new(3).unwrap();
185 let bars = [candle(10.0, 10.0), candle(11.0, 11.0), candle(12.0, 12.0)];
186 assert_relative_eq!(imi.batch(&bars)[2].unwrap(), 50.0, epsilon = 1e-12);
187 }
188
189 #[test]
190 fn slides_window() {
191 let mut imi = IntradayMomentumIndex::new(3).unwrap();
193 let bars = [
194 candle(10.0, 11.0),
195 candle(11.0, 10.0),
196 candle(10.0, 12.0),
197 candle(12.0, 12.0),
198 ];
199 let out = imi.batch(&bars);
200 assert_relative_eq!(out[3].unwrap(), 100.0 * 2.0 / 3.0, epsilon = 1e-12);
201 }
202
203 #[test]
204 fn reset_clears_state() {
205 let mut imi = IntradayMomentumIndex::new(3).unwrap();
206 imi.batch(&[candle(10.0, 11.0), candle(11.0, 12.0), candle(12.0, 13.0)]);
207 assert!(imi.is_ready());
208 imi.reset();
209 assert!(!imi.is_ready());
210 assert_eq!(imi.update(candle(1.0, 2.0)), None);
211 }
212
213 #[test]
214 fn batch_equals_streaming() {
215 let bars: Vec<Candle> = (0..30)
216 .map(|i| {
217 let base = 100.0 + f64::from(i);
218 candle(base, base + (f64::from(i) * 0.5).sin())
219 })
220 .collect();
221 let mut a = IntradayMomentumIndex::new(7).unwrap();
222 let mut b = IntradayMomentumIndex::new(7).unwrap();
223 assert_eq!(
224 a.batch(&bars),
225 bars.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
226 );
227 }
228}