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    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
62        let oi = tick.open_interest;
63        let delta = self.prev.map(|prev| oi - prev);
64        self.prev = Some(oi);
65        if delta.is_some() {
66            self.has_emitted = true;
67        }
68        delta
69    }
70
71    fn reset(&mut self) {
72        self.prev = None;
73        self.has_emitted = false;
74    }
75
76    fn warmup_period(&self) -> usize {
77        2
78    }
79
80    fn is_ready(&self) -> bool {
81        self.has_emitted
82    }
83
84    fn name(&self) -> &'static str {
85        "OpenInterestDelta"
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::traits::BatchExt;
93
94    fn tick(oi: f64) -> DerivativesTick {
95        DerivativesTick::new_unchecked(
96            0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
97        )
98    }
99
100    #[test]
101    fn accessors_and_metadata() {
102        let oid = OpenInterestDelta::new();
103        assert_eq!(oid.name(), "OpenInterestDelta");
104        assert_eq!(oid.warmup_period(), 2);
105        assert!(!oid.is_ready());
106    }
107
108    #[test]
109    fn seeds_then_emits_delta() {
110        let mut oid = OpenInterestDelta::new();
111        assert_eq!(oid.update(tick(1_000.0)), None);
112        assert!(!oid.is_ready());
113        assert_eq!(oid.update(tick(1_250.0)), Some(250.0));
114        assert!(oid.is_ready());
115        assert_eq!(oid.update(tick(1_100.0)), Some(-150.0));
116    }
117
118    #[test]
119    fn batch_equals_streaming() {
120        let ticks: Vec<DerivativesTick> = (0..20)
121            .map(|i| tick(1_000.0 + f64::from(i * i % 13) * 10.0))
122            .collect();
123        let mut a = OpenInterestDelta::new();
124        let mut b = OpenInterestDelta::new();
125        assert_eq!(
126            a.batch(&ticks),
127            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
128        );
129    }
130
131    #[test]
132    fn reset_clears_state() {
133        let mut oid = OpenInterestDelta::new();
134        oid.update(tick(1_000.0));
135        oid.update(tick(1_250.0));
136        assert!(oid.is_ready());
137        oid.reset();
138        assert!(!oid.is_ready());
139        // After reset the next tick only re-seeds, returning None.
140        assert_eq!(oid.update(tick(2_000.0)), None);
141    }
142}