Skip to main content

wickra_core/indicators/
funding_basis.rs

1//! Funding Basis — the perpetual mark's relative premium to the spot index.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Funding Basis — the relative basis between the perpetual mark price and the
7/// spot index it tracks.
8///
9/// ```text
10/// basis = (markPrice − indexPrice) / indexPrice
11/// ```
12///
13/// The basis is the spread that the funding mechanism continuously pulls toward
14/// zero: a positive basis (perpetual above spot) goes hand in hand with positive
15/// funding (longs pay), a negative basis with negative funding. Reading the
16/// instantaneous basis alongside the [funding rate] separates a genuine premium
17/// from a stale-funding artefact and sizes the carry available to a cash-and-carry
18/// or basis-arbitrage trade. The output is a fraction (e.g. `0.001` = 10 bps);
19/// multiply by `10_000` for basis points.
20///
21/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
22/// tick.
23///
24/// [funding rate]: crate::FundingRate
25///
26/// # Example
27///
28/// ```
29/// use wickra_core::{DerivativesTick, FundingBasis, Indicator};
30///
31/// let mut fb = FundingBasis::new();
32/// // mark 100.5 vs index 100.0 -> (100.5 - 100.0) / 100.0 = 0.005.
33/// let tick = DerivativesTick::new(
34///     0.0, 100.5, 100.0, 100.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
35/// )
36/// .unwrap();
37/// assert!((fb.update(tick).unwrap() - 0.005).abs() < 1e-12);
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct FundingBasis {
41    has_emitted: bool,
42}
43
44impl FundingBasis {
45    /// Construct a new funding-basis indicator.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self { has_emitted: false }
49    }
50}
51
52impl Indicator for FundingBasis {
53    type Input = DerivativesTick;
54    type Output = f64;
55
56    #[inline]
57    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
58        self.has_emitted = true;
59        Some((tick.mark_price - tick.index_price) / tick.index_price)
60    }
61
62    fn reset(&mut self) {
63        self.has_emitted = false;
64    }
65
66    #[inline]
67    fn warmup_period(&self) -> usize {
68        1
69    }
70
71    #[inline]
72    fn is_ready(&self) -> bool {
73        self.has_emitted
74    }
75
76    #[inline]
77    fn name(&self) -> &'static str {
78        "FundingBasis"
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::traits::BatchExt;
86
87    fn tick(mark: f64, index: f64) -> DerivativesTick {
88        DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
89    }
90
91    #[test]
92    fn accessors_and_metadata() {
93        let fb = FundingBasis::new();
94        assert_eq!(fb.name(), "FundingBasis");
95        assert_eq!(fb.warmup_period(), 1);
96        assert!(!fb.is_ready());
97    }
98
99    #[test]
100    fn premium_is_positive() {
101        let mut fb = FundingBasis::new();
102        let out = fb.update(tick(100.5, 100.0)).unwrap();
103        assert!((out - 0.005).abs() < 1e-12);
104        assert!(fb.is_ready());
105    }
106
107    #[test]
108    fn discount_is_negative() {
109        let mut fb = FundingBasis::new();
110        let out = fb.update(tick(99.5, 100.0)).unwrap();
111        assert!((out + 0.005).abs() < 1e-12);
112    }
113
114    #[test]
115    fn at_par_is_zero() {
116        let mut fb = FundingBasis::new();
117        assert_eq!(fb.update(tick(100.0, 100.0)), Some(0.0));
118    }
119
120    #[test]
121    fn batch_equals_streaming() {
122        let ticks: Vec<DerivativesTick> = (0..20)
123            .map(|i| tick(100.0 + f64::from(i % 5) * 0.1, 100.0))
124            .collect();
125        let mut a = FundingBasis::new();
126        let mut b = FundingBasis::new();
127        assert_eq!(
128            a.batch(&ticks),
129            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
130        );
131    }
132
133    #[test]
134    fn reset_clears_state() {
135        let mut fb = FundingBasis::new();
136        fb.update(tick(100.5, 100.0));
137        assert!(fb.is_ready());
138        fb.reset();
139        assert!(!fb.is_ready());
140    }
141}