Skip to main content

wickra_core/indicators/
oi_delta.rs

1//! Open-Interest Delta — the tick-over-tick change in open interest.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Open-Interest Delta — the change in open interest from the previous tick.
7///
8/// ```text
9/// delta = openInterestₜ − openInterestₜ₋₁
10/// ```
11///
12/// Open interest is the count of outstanding contracts; its change separates new
13/// positioning from mere turnover. Read together with price, rising OI confirms
14/// a trend (fresh money entering) while falling OI flags an unwind (positions
15/// closing) — the raw input to the [OI / price divergence] signal. A positive
16/// delta is net position-building, a negative delta net liquidation/closing.
17///
18/// The first tick only seeds the previous value and returns `None`; from the
19/// second tick on the indicator emits the delta.
20///
21/// `Input = DerivativesTick`, `Output = f64`.
22///
23/// [OI / price divergence]: crate::OIPriceDivergence
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{DerivativesTick, Indicator, OpenInterestDelta};
29///
30/// fn tick(oi: f64) -> DerivativesTick {
31///     DerivativesTick::new(0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
32///         .unwrap()
33/// }
34///
35/// let mut oid = OpenInterestDelta::new();
36/// assert_eq!(oid.update(tick(1_000.0)), None); // seeds the previous OI
37/// assert_eq!(oid.update(tick(1_250.0)), Some(250.0));
38/// assert_eq!(oid.update(tick(1_100.0)), Some(-150.0));
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct OpenInterestDelta {
42    prev: Option<f64>,
43    has_emitted: bool,
44}
45
46impl OpenInterestDelta {
47    /// Construct a new open-interest delta indicator.
48    #[must_use]
49    pub const fn new() -> Self {
50        Self {
51            prev: None,
52            has_emitted: false,
53        }
54    }
55}
56
57impl Indicator for OpenInterestDelta {
58    type Input = DerivativesTick;
59    type Output = f64;
60
61    #[inline]
62    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
63        let oi = tick.open_interest;
64        let delta = self.prev.map(|prev| oi - prev);
65        self.prev = Some(oi);
66        if delta.is_some() {
67            self.has_emitted = true;
68        }
69        delta
70    }
71
72    fn reset(&mut self) {
73        self.prev = None;
74        self.has_emitted = false;
75    }
76
77    #[inline]
78    fn warmup_period(&self) -> usize {
79        2
80    }
81
82    #[inline]
83    fn is_ready(&self) -> bool {
84        self.has_emitted
85    }
86
87    #[inline]
88    fn name(&self) -> &'static str {
89        "OpenInterestDelta"
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::traits::BatchExt;
97
98    fn tick(oi: f64) -> DerivativesTick {
99        DerivativesTick::new_unchecked(
100            0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
101        )
102    }
103
104    #[test]
105    fn accessors_and_metadata() {
106        let oid = OpenInterestDelta::new();
107        assert_eq!(oid.name(), "OpenInterestDelta");
108        assert_eq!(oid.warmup_period(), 2);
109        assert!(!oid.is_ready());
110    }
111
112    #[test]
113    fn seeds_then_emits_delta() {
114        let mut oid = OpenInterestDelta::new();
115        assert_eq!(oid.update(tick(1_000.0)), None);
116        assert!(!oid.is_ready());
117        assert_eq!(oid.update(tick(1_250.0)), Some(250.0));
118        assert!(oid.is_ready());
119        assert_eq!(oid.update(tick(1_100.0)), Some(-150.0));
120    }
121
122    #[test]
123    fn batch_equals_streaming() {
124        let ticks: Vec<DerivativesTick> = (0..20)
125            .map(|i| tick(1_000.0 + f64::from(i * i % 13) * 10.0))
126            .collect();
127        let mut a = OpenInterestDelta::new();
128        let mut b = OpenInterestDelta::new();
129        assert_eq!(
130            a.batch(&ticks),
131            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
132        );
133    }
134
135    #[test]
136    fn reset_clears_state() {
137        let mut oid = OpenInterestDelta::new();
138        oid.update(tick(1_000.0));
139        oid.update(tick(1_250.0));
140        assert!(oid.is_ready());
141        oid.reset();
142        assert!(!oid.is_ready());
143        // After reset the next tick only re-seeds, returning None.
144        assert_eq!(oid.update(tick(2_000.0)), None);
145    }
146}