wickra_core/indicators/
oi_price_divergence.rs1use std::collections::VecDeque;
5
6use crate::derivatives::DerivativesTick;
7use crate::error::{Error, Result};
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
47pub struct OIPriceDivergence {
48 window: usize,
49 history: VecDeque<(f64, f64)>,
50}
51
52impl OIPriceDivergence {
53 pub fn new(window: usize) -> Result<Self> {
59 if window == 0 {
60 return Err(Error::PeriodZero);
61 }
62 if window > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 Ok(Self {
68 window,
69 history: VecDeque::with_capacity(window + 1),
70 })
71 }
72
73 #[must_use]
75 pub fn window(&self) -> usize {
76 self.window
77 }
78}
79
80impl Indicator for OIPriceDivergence {
81 type Input = DerivativesTick;
82 type Output = f64;
83
84 #[inline]
85 fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
86 self.history
87 .push_back((tick.open_interest, tick.mark_price));
88 if self.history.len() > self.window + 1 {
89 self.history.pop_front();
90 }
91 if self.history.len() < self.window + 1 {
92 return None;
93 }
94 let (old_oi, old_mark) = *self.history.front().expect("len == window + 1");
95 let (cur_oi, cur_mark) = *self.history.back().expect("len == window + 1");
96 let oi_change = if old_oi == 0.0 {
99 0.0
100 } else {
101 (cur_oi - old_oi) / old_oi
102 };
103 let price_change = (cur_mark - old_mark) / old_mark;
106 Some(oi_change - price_change)
107 }
108
109 fn reset(&mut self) {
110 self.history.clear();
111 }
112
113 #[inline]
114 fn warmup_period(&self) -> usize {
115 self.window + 1
116 }
117
118 #[inline]
119 fn is_ready(&self) -> bool {
120 self.history.len() == self.window + 1
121 }
122
123 #[inline]
124 fn name(&self) -> &'static str {
125 "OIPriceDivergence"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::traits::BatchExt;
133
134 fn tick(oi: f64, mark: f64) -> DerivativesTick {
135 DerivativesTick::new_unchecked(0.0, mark, mark, mark, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
136 }
137
138 #[test]
139 fn rejects_zero_window() {
140 assert!(matches!(OIPriceDivergence::new(0), Err(Error::PeriodZero)));
141 }
142
143 #[test]
144 fn accessors_and_metadata() {
145 let div = OIPriceDivergence::new(5).unwrap();
146 assert_eq!(div.name(), "OIPriceDivergence");
147 assert_eq!(div.warmup_period(), 6);
148 assert_eq!(div.window(), 5);
149 assert!(!div.is_ready());
150 }
151
152 #[test]
153 fn oi_up_price_flat_is_positive() {
154 let mut div = OIPriceDivergence::new(1).unwrap();
155 assert_eq!(div.update(tick(1_000.0, 100.0)), None);
156 let out = div.update(tick(1_100.0, 100.0)).unwrap();
157 assert!((out - 0.1).abs() < 1e-12);
158 assert!(div.is_ready());
159 }
160
161 #[test]
162 fn oi_flat_price_up_is_negative() {
163 let mut div = OIPriceDivergence::new(1).unwrap();
164 div.update(tick(1_000.0, 100.0));
165 let out = div.update(tick(1_000.0, 110.0)).unwrap();
167 assert!((out + 0.1).abs() < 1e-12);
168 }
169
170 #[test]
171 fn zero_reference_oi_drops_oi_term() {
172 let mut div = OIPriceDivergence::new(1).unwrap();
173 div.update(tick(0.0, 100.0));
174 let out = div.update(tick(500.0, 110.0)).unwrap();
176 assert!((out + 0.1).abs() < 1e-12);
177 }
178
179 #[test]
180 fn batch_equals_streaming() {
181 let ticks: Vec<DerivativesTick> = (0..30)
182 .map(|i| tick(1_000.0 + f64::from(i % 7) * 10.0, 100.0 + f64::from(i % 5)))
183 .collect();
184 let mut a = OIPriceDivergence::new(4).unwrap();
185 let mut b = OIPriceDivergence::new(4).unwrap();
186 assert_eq!(
187 a.batch(&ticks),
188 ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
189 );
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut div = OIPriceDivergence::new(1).unwrap();
195 div.update(tick(1_000.0, 100.0));
196 div.update(tick(1_100.0, 100.0));
197 assert!(div.is_ready());
198 div.reset();
199 assert!(!div.is_ready());
200 assert_eq!(div.update(tick(1_000.0, 100.0)), None);
201 }
202}