Skip to main content

wickra_core/indicators/
term_structure_basis.rs

1//! Term-Structure Basis — the dated future's relative premium to spot.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Term-Structure Basis — the relative basis between a dated (e.g. quarterly)
7/// futures price and the spot index.
8///
9/// ```text
10/// basis = (futuresPrice − indexPrice) / indexPrice
11/// ```
12///
13/// Where [`FundingBasis`] measures the *perpetual*'s premium to spot, this
14/// measures a *dated future*'s — the term-structure carry that a calendar or
15/// cash-and-carry trade harvests as the contract converges to spot at expiry. A
16/// positive basis is contango (futures above spot), a negative one backwardation.
17/// The output is a fraction (e.g. `0.02` = 2%); multiply by `10_000` for basis
18/// points.
19///
20/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
21/// tick.
22///
23/// [`FundingBasis`]: crate::FundingBasis
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{DerivativesTick, Indicator, TermStructureBasis};
29///
30/// fn tick(futures: f64, index: f64) -> DerivativesTick {
31///     DerivativesTick::new(0.0, index, index, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
32///         .unwrap()
33/// }
34///
35/// let mut ts = TermStructureBasis::new();
36/// // futures 102 vs index 100 -> 0.02 (2% contango).
37/// assert!((ts.update(tick(102.0, 100.0)).unwrap() - 0.02).abs() < 1e-12);
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct TermStructureBasis {
41    has_emitted: bool,
42}
43
44impl TermStructureBasis {
45    /// Construct a new term-structure basis indicator.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self { has_emitted: false }
49    }
50}
51
52impl Indicator for TermStructureBasis {
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.futures_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        "TermStructureBasis"
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::traits::BatchExt;
86
87    fn tick(futures: f64, index: f64) -> DerivativesTick {
88        DerivativesTick::new_unchecked(
89            0.0, index, index, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
90        )
91    }
92
93    #[test]
94    fn accessors_and_metadata() {
95        let ts = TermStructureBasis::new();
96        assert_eq!(ts.name(), "TermStructureBasis");
97        assert_eq!(ts.warmup_period(), 1);
98        assert!(!ts.is_ready());
99    }
100
101    #[test]
102    fn contango_is_positive() {
103        let mut ts = TermStructureBasis::new();
104        let out = ts.update(tick(102.0, 100.0)).unwrap();
105        assert!((out - 0.02).abs() < 1e-12);
106        assert!(ts.is_ready());
107    }
108
109    #[test]
110    fn backwardation_is_negative() {
111        let mut ts = TermStructureBasis::new();
112        let out = ts.update(tick(98.0, 100.0)).unwrap();
113        assert!((out + 0.02).abs() < 1e-12);
114    }
115
116    #[test]
117    fn at_par_is_zero() {
118        let mut ts = TermStructureBasis::new();
119        assert_eq!(ts.update(tick(100.0, 100.0)), Some(0.0));
120    }
121
122    #[test]
123    fn batch_equals_streaming() {
124        let ticks: Vec<DerivativesTick> = (0..20)
125            .map(|i| tick(100.0 + f64::from(i % 5), 100.0))
126            .collect();
127        let mut a = TermStructureBasis::new();
128        let mut b = TermStructureBasis::new();
129        assert_eq!(
130            a.batch(&ticks),
131            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
132        );
133    }
134
135    #[test]
136    fn reset_clears_state() {
137        let mut ts = TermStructureBasis::new();
138        ts.update(tick(102.0, 100.0));
139        assert!(ts.is_ready());
140        ts.reset();
141        assert!(!ts.is_ready());
142    }
143}