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    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
57        self.has_emitted = true;
58        Some((tick.futures_price - tick.index_price) / tick.index_price)
59    }
60
61    fn reset(&mut self) {
62        self.has_emitted = false;
63    }
64
65    fn warmup_period(&self) -> usize {
66        1
67    }
68
69    fn is_ready(&self) -> bool {
70        self.has_emitted
71    }
72
73    fn name(&self) -> &'static str {
74        "TermStructureBasis"
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::traits::BatchExt;
82
83    fn tick(futures: f64, index: f64) -> DerivativesTick {
84        DerivativesTick::new_unchecked(
85            0.0, index, index, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
86        )
87    }
88
89    #[test]
90    fn accessors_and_metadata() {
91        let ts = TermStructureBasis::new();
92        assert_eq!(ts.name(), "TermStructureBasis");
93        assert_eq!(ts.warmup_period(), 1);
94        assert!(!ts.is_ready());
95    }
96
97    #[test]
98    fn contango_is_positive() {
99        let mut ts = TermStructureBasis::new();
100        let out = ts.update(tick(102.0, 100.0)).unwrap();
101        assert!((out - 0.02).abs() < 1e-12);
102        assert!(ts.is_ready());
103    }
104
105    #[test]
106    fn backwardation_is_negative() {
107        let mut ts = TermStructureBasis::new();
108        let out = ts.update(tick(98.0, 100.0)).unwrap();
109        assert!((out + 0.02).abs() < 1e-12);
110    }
111
112    #[test]
113    fn at_par_is_zero() {
114        let mut ts = TermStructureBasis::new();
115        assert_eq!(ts.update(tick(100.0, 100.0)), Some(0.0));
116    }
117
118    #[test]
119    fn batch_equals_streaming() {
120        let ticks: Vec<DerivativesTick> = (0..20)
121            .map(|i| tick(100.0 + f64::from(i % 5), 100.0))
122            .collect();
123        let mut a = TermStructureBasis::new();
124        let mut b = TermStructureBasis::new();
125        assert_eq!(
126            a.batch(&ticks),
127            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
128        );
129    }
130
131    #[test]
132    fn reset_clears_state() {
133        let mut ts = TermStructureBasis::new();
134        ts.update(tick(102.0, 100.0));
135        assert!(ts.is_ready());
136        ts.reset();
137        assert!(!ts.is_ready());
138    }
139}