wickra_core/indicators/
open_interest_momentum.rs1use std::collections::VecDeque;
4
5use crate::derivatives::DerivativesTick;
6use crate::error::{Error, Result};
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
43pub struct OpenInterestMomentum {
44 period: usize,
45 window: VecDeque<f64>,
46 last: Option<f64>,
47}
48
49impl OpenInterestMomentum {
50 pub fn new(period: usize) -> Result<Self> {
56 if period == 0 {
57 return Err(Error::PeriodZero);
58 }
59 if period > crate::error::MAX_PERIOD {
60 return Err(Error::InvalidPeriod {
61 message: crate::error::PERIOD_ABOVE_MAX,
62 });
63 }
64 Ok(Self {
65 period,
66 window: VecDeque::with_capacity(period + 1),
67 last: None,
68 })
69 }
70
71 pub const fn period(&self) -> usize {
73 self.period
74 }
75
76 pub const fn value(&self) -> Option<f64> {
78 self.last
79 }
80}
81
82impl Indicator for OpenInterestMomentum {
83 type Input = DerivativesTick;
84 type Output = f64;
85
86 #[inline]
87 fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
88 if self.window.len() == self.period + 1 {
89 self.window.pop_front();
90 }
91 self.window.push_back(tick.open_interest);
92 if self.window.len() < self.period + 1 {
93 return None;
94 }
95 let base = *self.window.front().expect("non-empty");
96 let current = tick.open_interest;
97 let oim = if base > 0.0 {
98 100.0 * (current - base) / base
99 } else {
100 0.0
101 };
102 self.last = Some(oim);
103 Some(oim)
104 }
105
106 fn reset(&mut self) {
107 self.window.clear();
108 self.last = None;
109 }
110
111 #[inline]
112 fn warmup_period(&self) -> usize {
113 self.period + 1
114 }
115
116 #[inline]
117 fn is_ready(&self) -> bool {
118 self.last.is_some()
119 }
120
121 #[inline]
122 fn name(&self) -> &'static str {
123 "OpenInterestMomentum"
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::traits::BatchExt;
131 use approx::assert_relative_eq;
132
133 fn tick(oi: f64) -> DerivativesTick {
134 DerivativesTick::new_unchecked(
135 0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
136 )
137 }
138
139 #[test]
140 fn rejects_zero_period() {
141 assert!(matches!(
142 OpenInterestMomentum::new(0),
143 Err(Error::PeriodZero)
144 ));
145 }
146
147 #[test]
148 fn accessors_and_metadata() {
149 let o = OpenInterestMomentum::new(5).unwrap();
150 assert_eq!(o.period(), 5);
151 assert_eq!(o.warmup_period(), 6);
152 assert_eq!(o.name(), "OpenInterestMomentum");
153 assert!(!o.is_ready());
154 assert_eq!(o.value(), None);
155 }
156
157 #[test]
158 fn first_emission_at_warmup_period() {
159 let mut o = OpenInterestMomentum::new(3).unwrap();
160 let ticks: Vec<DerivativesTick> = (0..6)
161 .map(|i| tick(1_000.0 + f64::from(i) * 100.0))
162 .collect();
163 let out = o.batch(&ticks);
164 for v in out.iter().take(3) {
165 assert!(v.is_none());
166 }
167 assert!(out[3].is_some());
168 }
169
170 #[test]
171 fn reference_value() {
172 let mut o = OpenInterestMomentum::new(2).unwrap();
174 let out = o.batch(&[tick(1_000.0), tick(1_100.0), tick(1_200.0)]);
175 assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-9);
176 }
177
178 #[test]
179 fn expanding_oi_is_positive() {
180 let mut o = OpenInterestMomentum::new(5).unwrap();
181 let ticks: Vec<DerivativesTick> = (0..20)
182 .map(|i| tick(1_000.0 + f64::from(i) * 100.0))
183 .collect();
184 let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
185 assert!(last > 0.0);
186 }
187
188 #[test]
189 fn contracting_oi_is_negative() {
190 let mut o = OpenInterestMomentum::new(5).unwrap();
191 let ticks: Vec<DerivativesTick> = (0..20)
192 .map(|i| tick(3_000.0 - f64::from(i) * 100.0))
193 .collect();
194 let last = o.batch(&ticks).into_iter().flatten().last().unwrap();
195 assert!(last < 0.0);
196 }
197
198 #[test]
199 fn zero_base_is_zero() {
200 let mut o = OpenInterestMomentum::new(2).unwrap();
201 let out = o.batch(&[tick(0.0), tick(100.0), tick(200.0)]);
202 assert_relative_eq!(out[2].unwrap(), 0.0, epsilon = 1e-12);
203 }
204
205 #[test]
206 fn reset_clears_state() {
207 let mut o = OpenInterestMomentum::new(3).unwrap();
208 o.batch(
209 &(0..10)
210 .map(|i| tick(1_000.0 + f64::from(i) * 50.0))
211 .collect::<Vec<_>>(),
212 );
213 assert!(o.is_ready());
214 o.reset();
215 assert!(!o.is_ready());
216 assert_eq!(o.value(), None);
217 assert_eq!(o.update(tick(1_000.0)), None);
218 }
219
220 #[test]
221 fn batch_equals_streaming() {
222 let ticks: Vec<DerivativesTick> = (0..80)
223 .map(|i| tick(1_000.0 + (f64::from(i) * 0.25).sin() * 300.0))
224 .collect();
225 let batch = OpenInterestMomentum::new(10).unwrap().batch(&ticks);
226 let mut b = OpenInterestMomentum::new(10).unwrap();
227 let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
228 assert_eq!(batch, streamed);
229 }
230}