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