Skip to main content

wickra_core/indicators/
estimated_leverage_ratio.rs

1//! Estimated Leverage Ratio — open interest per unit of aggregate position size.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Estimated Leverage Ratio (ELR) — open interest relative to the aggregate
7/// long+short position size, a proxy for how leveraged outstanding positions are.
8///
9/// ```text
10/// ELR = open_interest / (long_size + short_size)
11/// ```
12///
13/// The classic estimated leverage ratio compares open interest (the notional of
14/// outstanding contracts) to the capital backing it. With the size fields of a
15/// [`DerivativesTick`] standing in for the position base, the ratio rises when a
16/// given pool of positions controls more open interest — i.e. when the market is
17/// running hotter leverage. Spikes in ELR mark crowded, fragile conditions where a
18/// move can cascade into liquidations; a falling ELR marks deleveraging.
19///
20/// The ratio is non-negative; a tick with zero aggregate size reports `0` rather
21/// than dividing by zero. It is stateless — each tick yields one value (no warmup).
22/// Each `update` is O(1).
23///
24/// # Example
25///
26/// ```
27/// use wickra_core::{DerivativesTick, Indicator, EstimatedLeverageRatio};
28///
29/// let mut indicator = EstimatedLeverageRatio::new();
30/// let tick = DerivativesTick::new(0.0001, 100.0, 100.0, 100.0, 1_000.0, 400.0, 600.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
31/// let elr = indicator.update(tick).unwrap();
32/// assert!((elr - 1.0).abs() < 1e-12); // 1000 / (400 + 600)
33/// ```
34#[derive(Debug, Clone, Default)]
35pub struct EstimatedLeverageRatio {
36    ready: bool,
37}
38
39impl EstimatedLeverageRatio {
40    /// Construct a new Estimated Leverage Ratio. The indicator is parameter-free.
41    #[must_use]
42    pub const fn new() -> Self {
43        Self { ready: false }
44    }
45}
46
47impl Indicator for EstimatedLeverageRatio {
48    type Input = DerivativesTick;
49    type Output = f64;
50
51    #[inline]
52    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
53        let base = tick.long_size + tick.short_size;
54        let elr = if base > 0.0 {
55            tick.open_interest / base
56        } else {
57            0.0
58        };
59        self.ready = true;
60        Some(elr)
61    }
62
63    fn reset(&mut self) {
64        self.ready = false;
65    }
66
67    #[inline]
68    fn warmup_period(&self) -> usize {
69        1
70    }
71
72    #[inline]
73    fn is_ready(&self) -> bool {
74        self.ready
75    }
76
77    #[inline]
78    fn name(&self) -> &'static str {
79        "EstimatedLeverageRatio"
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::traits::BatchExt;
87    use approx::assert_relative_eq;
88
89    fn tick(oi: f64, long: f64, short: f64) -> DerivativesTick {
90        DerivativesTick::new_unchecked(
91            0.0, 100.0, 100.0, 100.0, oi, long, short, 0.0, 0.0, 0.0, 0.0, 0,
92        )
93    }
94
95    #[test]
96    fn accessors_and_metadata() {
97        let e = EstimatedLeverageRatio::new();
98        assert_eq!(e.warmup_period(), 1);
99        assert_eq!(e.name(), "EstimatedLeverageRatio");
100        assert!(!e.is_ready());
101    }
102
103    #[test]
104    fn ratio_reference_value() {
105        let mut e = EstimatedLeverageRatio::new();
106        // 1000 / (400 + 600) = 1.0.
107        assert_relative_eq!(
108            e.update(tick(1_000.0, 400.0, 600.0)).unwrap(),
109            1.0,
110            epsilon = 1e-12
111        );
112    }
113
114    #[test]
115    fn higher_oi_raises_ratio() {
116        let mut e = EstimatedLeverageRatio::new();
117        let low = e.update(tick(1_000.0, 500.0, 500.0)).unwrap();
118        let high = e.update(tick(3_000.0, 500.0, 500.0)).unwrap();
119        assert!(high > low);
120    }
121
122    #[test]
123    fn zero_base_is_zero() {
124        let mut e = EstimatedLeverageRatio::new();
125        assert_relative_eq!(
126            e.update(tick(1_000.0, 0.0, 0.0)).unwrap(),
127            0.0,
128            epsilon = 1e-12
129        );
130    }
131
132    #[test]
133    fn ready_after_first_update() {
134        let mut e = EstimatedLeverageRatio::new();
135        assert!(!e.is_ready());
136        e.update(tick(1_000.0, 500.0, 500.0));
137        assert!(e.is_ready());
138    }
139
140    #[test]
141    fn reset_clears_state() {
142        let mut e = EstimatedLeverageRatio::new();
143        e.update(tick(1_000.0, 500.0, 500.0));
144        assert!(e.is_ready());
145        e.reset();
146        assert!(!e.is_ready());
147    }
148
149    #[test]
150    fn batch_equals_streaming() {
151        let ticks: Vec<DerivativesTick> = (0..40)
152            .map(|i| tick(1_000.0 + f64::from(i) * 10.0, 500.0, 500.0))
153            .collect();
154        let batch = EstimatedLeverageRatio::new().batch(&ticks);
155        let mut b = EstimatedLeverageRatio::new();
156        let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
157        assert_eq!(batch, streamed);
158    }
159}