Skip to main content

wickra_core/indicators/
oi_weighted.rs

1//! Open-Interest-Weighted Price — cumulative mark price weighted by open
2//! interest.
3
4use crate::derivatives::DerivativesTick;
5use crate::traits::Indicator;
6
7/// Open-Interest-Weighted Price — the running mean mark price, weighting each
8/// tick by its open interest.
9///
10/// ```text
11/// oiWeighted = Σ(markPrice · openInterest) / Σ openInterest
12/// ```
13///
14/// Where a plain mean treats every tick equally, the OI-weighted price pulls
15/// toward the levels at which the most contracts were actually outstanding — the
16/// price the bulk of open positioning sits around, a fair-value anchor for
17/// liquidations and mean-reversion. The accumulation runs from construction;
18/// call [`reset`] at each session boundary to re-anchor. Until any open interest
19/// has accrued the indicator returns the current mark price.
20///
21/// `Input = DerivativesTick`, `Output = f64`. Ready after the first tick.
22///
23/// [`reset`]: crate::Indicator::reset
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{DerivativesTick, Indicator, OIWeighted};
29///
30/// fn tick(mark: f64, oi: f64) -> DerivativesTick {
31///     DerivativesTick::new(0.0, mark, mark, mark, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
32///         .unwrap()
33/// }
34///
35/// let mut oiw = OIWeighted::new();
36/// assert_eq!(oiw.update(tick(100.0, 10.0)), Some(100.0));
37/// // (100·10 + 110·30) / (10 + 30) = 4300 / 40 = 107.5.
38/// assert_eq!(oiw.update(tick(110.0, 30.0)), Some(107.5));
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct OIWeighted {
42    sum_weighted: f64,
43    sum_oi: f64,
44    has_emitted: bool,
45}
46
47impl OIWeighted {
48    /// Construct a new OI-weighted price indicator.
49    #[must_use]
50    pub const fn new() -> Self {
51        Self {
52            sum_weighted: 0.0,
53            sum_oi: 0.0,
54            has_emitted: false,
55        }
56    }
57}
58
59impl Indicator for OIWeighted {
60    type Input = DerivativesTick;
61    type Output = f64;
62
63    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
64        self.has_emitted = true;
65        self.sum_weighted += tick.mark_price * tick.open_interest;
66        self.sum_oi += tick.open_interest;
67        if self.sum_oi == 0.0 {
68            // No open interest has accrued yet: fall back to the mark price.
69            return Some(tick.mark_price);
70        }
71        Some(self.sum_weighted / self.sum_oi)
72    }
73
74    fn reset(&mut self) {
75        self.sum_weighted = 0.0;
76        self.sum_oi = 0.0;
77        self.has_emitted = false;
78    }
79
80    fn warmup_period(&self) -> usize {
81        1
82    }
83
84    fn is_ready(&self) -> bool {
85        self.has_emitted
86    }
87
88    fn name(&self) -> &'static str {
89        "OIWeighted"
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::traits::BatchExt;
97
98    fn tick(mark: f64, oi: f64) -> DerivativesTick {
99        DerivativesTick::new_unchecked(0.0, mark, mark, mark, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
100    }
101
102    #[test]
103    fn accessors_and_metadata() {
104        let oiw = OIWeighted::new();
105        assert_eq!(oiw.name(), "OIWeighted");
106        assert_eq!(oiw.warmup_period(), 1);
107        assert!(!oiw.is_ready());
108    }
109
110    #[test]
111    fn weights_by_open_interest() {
112        let mut oiw = OIWeighted::new();
113        assert_eq!(oiw.update(tick(100.0, 10.0)), Some(100.0));
114        // (100·10 + 110·30) / 40 = 107.5.
115        assert_eq!(oiw.update(tick(110.0, 30.0)), Some(107.5));
116        assert!(oiw.is_ready());
117    }
118
119    #[test]
120    fn zero_open_interest_falls_back_to_mark() {
121        let mut oiw = OIWeighted::new();
122        assert_eq!(oiw.update(tick(123.0, 0.0)), Some(123.0));
123        // Still no OI on the second zero-OI tick.
124        assert_eq!(oiw.update(tick(125.0, 0.0)), Some(125.0));
125    }
126
127    #[test]
128    fn batch_equals_streaming() {
129        let ticks: Vec<DerivativesTick> = (0..20)
130            .map(|i| tick(100.0 + f64::from(i % 5), 1.0 + f64::from(i % 4)))
131            .collect();
132        let mut a = OIWeighted::new();
133        let mut b = OIWeighted::new();
134        assert_eq!(
135            a.batch(&ticks),
136            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
137        );
138    }
139
140    #[test]
141    fn reset_re_anchors() {
142        let mut oiw = OIWeighted::new();
143        oiw.update(tick(100.0, 10.0));
144        oiw.update(tick(110.0, 30.0));
145        assert!(oiw.is_ready());
146        oiw.reset();
147        assert!(!oiw.is_ready());
148        // After reset the accumulation starts again from the next tick.
149        assert_eq!(oiw.update(tick(200.0, 5.0)), Some(200.0));
150    }
151}