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    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
52        let base = tick.long_size + tick.short_size;
53        let elr = if base > 0.0 {
54            tick.open_interest / base
55        } else {
56            0.0
57        };
58        self.ready = true;
59        Some(elr)
60    }
61
62    fn reset(&mut self) {
63        self.ready = false;
64    }
65
66    fn warmup_period(&self) -> usize {
67        1
68    }
69
70    fn is_ready(&self) -> bool {
71        self.ready
72    }
73
74    fn name(&self) -> &'static str {
75        "EstimatedLeverageRatio"
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::traits::BatchExt;
83    use approx::assert_relative_eq;
84
85    fn tick(oi: f64, long: f64, short: f64) -> DerivativesTick {
86        DerivativesTick::new_unchecked(
87            0.0, 100.0, 100.0, 100.0, oi, long, short, 0.0, 0.0, 0.0, 0.0, 0,
88        )
89    }
90
91    #[test]
92    fn accessors_and_metadata() {
93        let e = EstimatedLeverageRatio::new();
94        assert_eq!(e.warmup_period(), 1);
95        assert_eq!(e.name(), "EstimatedLeverageRatio");
96        assert!(!e.is_ready());
97    }
98
99    #[test]
100    fn ratio_reference_value() {
101        let mut e = EstimatedLeverageRatio::new();
102        // 1000 / (400 + 600) = 1.0.
103        assert_relative_eq!(
104            e.update(tick(1_000.0, 400.0, 600.0)).unwrap(),
105            1.0,
106            epsilon = 1e-12
107        );
108    }
109
110    #[test]
111    fn higher_oi_raises_ratio() {
112        let mut e = EstimatedLeverageRatio::new();
113        let low = e.update(tick(1_000.0, 500.0, 500.0)).unwrap();
114        let high = e.update(tick(3_000.0, 500.0, 500.0)).unwrap();
115        assert!(high > low);
116    }
117
118    #[test]
119    fn zero_base_is_zero() {
120        let mut e = EstimatedLeverageRatio::new();
121        assert_relative_eq!(
122            e.update(tick(1_000.0, 0.0, 0.0)).unwrap(),
123            0.0,
124            epsilon = 1e-12
125        );
126    }
127
128    #[test]
129    fn ready_after_first_update() {
130        let mut e = EstimatedLeverageRatio::new();
131        assert!(!e.is_ready());
132        e.update(tick(1_000.0, 500.0, 500.0));
133        assert!(e.is_ready());
134    }
135
136    #[test]
137    fn reset_clears_state() {
138        let mut e = EstimatedLeverageRatio::new();
139        e.update(tick(1_000.0, 500.0, 500.0));
140        assert!(e.is_ready());
141        e.reset();
142        assert!(!e.is_ready());
143    }
144
145    #[test]
146    fn batch_equals_streaming() {
147        let ticks: Vec<DerivativesTick> = (0..40)
148            .map(|i| tick(1_000.0 + f64::from(i) * 10.0, 500.0, 500.0))
149            .collect();
150        let batch = EstimatedLeverageRatio::new().batch(&ticks);
151        let mut b = EstimatedLeverageRatio::new();
152        let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
153        assert_eq!(batch, streamed);
154    }
155}