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