Skip to main content

wickra_core/indicators/
estimated_leverage_ratio.rs

1//! Estimated Leverage Ratio — open interest per unit of exchange reserve.
2
3use crate::traits::Indicator;
4
5/// Estimated Leverage Ratio (ELR) — a derivatives exchange's open interest
6/// divided by its coin reserve, `CryptoQuant`'s measure of how much leverage
7/// traders use on average.
8///
9/// ```text
10/// ELR = open_interest / exchange_reserve
11/// ```
12///
13/// Each update takes one `(open_interest, exchange_reserve)` pair: the open
14/// interest of the exchange's derivatives and the amount of the coin it holds
15/// in reserve, in the same unit. A rising ELR means a given reserve backs more
16/// outstanding contracts — traders are taking on more leverage, and the market
17/// is more fragile to liquidation cascades; a falling ELR marks deleveraging.
18///
19/// The ratio is non-negative for non-negative inputs; a non-positive reserve
20/// reports `0` rather than dividing by zero. It is stateless — each pair yields
21/// one value (no warmup). Each `update` is O(1).
22///
23/// # Example
24///
25/// ```
26/// use wickra_core::{EstimatedLeverageRatio, Indicator};
27///
28/// let mut indicator = EstimatedLeverageRatio::new();
29/// // Open interest 25,000 coins against a 100,000-coin reserve.
30/// let elr = indicator.update((25_000.0, 100_000.0)).unwrap();
31/// assert!((elr - 0.25).abs() < 1e-12);
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct EstimatedLeverageRatio {
35    ready: bool,
36}
37
38impl EstimatedLeverageRatio {
39    /// Construct a new Estimated Leverage Ratio. The indicator is parameter-free.
40    #[must_use]
41    pub const fn new() -> Self {
42        Self { ready: false }
43    }
44}
45
46impl Indicator for EstimatedLeverageRatio {
47    type Input = (f64, f64);
48    type Output = f64;
49
50    #[inline]
51    fn update(&mut self, input: (f64, f64)) -> Option<f64> {
52        let (open_interest, reserve) = input;
53        if !open_interest.is_finite() || !reserve.is_finite() {
54            return None;
55        }
56        let elr = if reserve > 0.0 {
57            open_interest / reserve
58        } else {
59            0.0
60        };
61        self.ready = true;
62        Some(elr)
63    }
64
65    fn reset(&mut self) {
66        self.ready = false;
67    }
68
69    #[inline]
70    fn warmup_period(&self) -> usize {
71        1
72    }
73
74    #[inline]
75    fn is_ready(&self) -> bool {
76        self.ready
77    }
78
79    #[inline]
80    fn name(&self) -> &'static str {
81        "EstimatedLeverageRatio"
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::traits::BatchExt;
89    use approx::assert_relative_eq;
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 / 4000 = 0.25.
103        assert_relative_eq!(e.update((1_000.0, 4_000.0)).unwrap(), 0.25, epsilon = 1e-12);
104    }
105
106    #[test]
107    fn higher_oi_raises_ratio() {
108        let mut e = EstimatedLeverageRatio::new();
109        let low = e.update((1_000.0, 10_000.0)).unwrap();
110        let high = e.update((3_000.0, 10_000.0)).unwrap();
111        assert!(high > low);
112    }
113
114    #[test]
115    fn zero_reserve_is_zero() {
116        let mut e = EstimatedLeverageRatio::new();
117        assert_relative_eq!(e.update((1_000.0, 0.0)).unwrap(), 0.0, epsilon = 1e-12);
118    }
119
120    #[test]
121    fn non_finite_input_returns_none() {
122        let mut e = EstimatedLeverageRatio::new();
123        assert_eq!(e.update((f64::NAN, 1.0)), None);
124        assert_eq!(e.update((1.0, f64::INFINITY)), None);
125        assert!(!e.is_ready());
126    }
127
128    #[test]
129    fn ready_after_first_update() {
130        let mut e = EstimatedLeverageRatio::new();
131        assert!(!e.is_ready());
132        e.update((1_000.0, 10_000.0));
133        assert!(e.is_ready());
134    }
135
136    #[test]
137    fn reset_clears_state() {
138        let mut e = EstimatedLeverageRatio::new();
139        e.update((1_000.0, 10_000.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 pairs: Vec<(f64, f64)> = (0..40)
148            .map(|i| (1_000.0 + f64::from(i) * 10.0, 10_000.0 - f64::from(i)))
149            .collect();
150        let batch = EstimatedLeverageRatio::new().batch(&pairs);
151        let mut b = EstimatedLeverageRatio::new();
152        let streamed: Vec<_> = pairs.iter().map(|x| b.update(*x)).collect();
153        assert_eq!(batch, streamed);
154    }
155
156    #[test]
157    fn warmup_first_value_at_index_zero() {
158        let mut e = EstimatedLeverageRatio::new();
159        // warmup_period() - 1 == 0: the first finite pair already emits.
160        assert_eq!(e.update((50.0, 200.0)), Some(0.25));
161    }
162
163    #[test]
164    fn hand_computed_series() {
165        let mut e = EstimatedLeverageRatio::new();
166        // 30,000 / 120,000 = 0.25; 45,000 / 90,000 = 0.5; 90,000 / 60,000 = 1.5.
167        assert_eq!(e.update((30_000.0, 120_000.0)), Some(0.25));
168        assert_eq!(e.update((45_000.0, 90_000.0)), Some(0.5));
169        assert_eq!(e.update((90_000.0, 60_000.0)), Some(1.5));
170        // Zero open interest against a positive reserve is a genuine 0.
171        assert_eq!(e.update((0.0, 60_000.0)), Some(0.0));
172    }
173
174    #[test]
175    fn negative_reserve_is_zero() {
176        let mut e = EstimatedLeverageRatio::new();
177        assert_eq!(
178            e.update((1_000.0, -5.0)).unwrap().to_bits(),
179            0.0f64.to_bits()
180        );
181        assert_eq!(
182            e.update((1_000.0, -0.0)).unwrap().to_bits(),
183            0.0f64.to_bits()
184        );
185        assert!(e.is_ready());
186    }
187
188    #[test]
189    fn every_non_finite_combination_returns_none() {
190        let mut e = EstimatedLeverageRatio::new();
191        assert_eq!(e.update((f64::INFINITY, 1.0)), None);
192        assert_eq!(e.update((f64::NEG_INFINITY, 1.0)), None);
193        assert_eq!(e.update((1.0, f64::NAN)), None);
194        assert_eq!(e.update((1.0, f64::NEG_INFINITY)), None);
195        assert_eq!(e.update((f64::NAN, f64::NAN)), None);
196        // A non-finite pair must not flip a ready indicator back, nor emit.
197        e.update((1.0, 2.0));
198        assert!(e.is_ready());
199        assert_eq!(e.update((f64::NAN, 2.0)), None);
200        assert!(e.is_ready());
201    }
202
203    #[test]
204    fn reset_replays_identically_to_fresh_instance() {
205        let pairs = [
206            (1_000.0, 4_000.0),
207            (f64::NAN, 1.0),
208            (2_000.0, 0.0),
209            (500.0, 250.0),
210        ];
211        let mut e = EstimatedLeverageRatio::new();
212        let first = e.batch(&pairs);
213        e.reset();
214        let second = e.batch(&pairs);
215        let fresh = EstimatedLeverageRatio::default().batch(&pairs);
216        assert_eq!(first, second);
217        assert_eq!(second, fresh);
218        assert_eq!(first, vec![Some(0.25), None, Some(0.0), Some(2.0)]);
219    }
220
221    #[test]
222    fn batch_nan_into_is_bit_identical_to_streaming() {
223        let pairs: Vec<(f64, f64)> = (0..30)
224            .map(|i| match i % 5 {
225                0 => (f64::NAN, 10.0),
226                1 => (100.0, 0.0),
227                _ => (100.0 + f64::from(i) * 3.3, 7.0 + f64::from(i) * 0.7),
228            })
229            .collect();
230        let mut out = vec![0.0; pairs.len()];
231        EstimatedLeverageRatio::new().batch_nan_into(&pairs, &mut out);
232        let mut s = EstimatedLeverageRatio::new();
233        assert!(pairs
234            .iter()
235            .zip(&out)
236            .all(|(p, o)| s.update(*p).unwrap_or(f64::NAN).to_bits() == o.to_bits()));
237    }
238}