Skip to main content

wickra_core/indicators/
calendar_spread.rs

1//! Calendar Spread — the dated future's relative premium to the perpetual.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Calendar Spread — the relative spread between a dated (e.g. quarterly)
7/// futures price and the perpetual mark price.
8///
9/// ```text
10/// spread = (futuresPrice − markPrice) / markPrice
11/// ```
12///
13/// A calendar (or inter-delivery) spread trades the *near* leg against the
14/// *far* leg — here the perpetual against a dated future. The relative spread is
15/// the roll yield available between the two contracts: positive when the future
16/// trades over the perpetual (contango roll), negative when under
17/// (backwardation). Where [`TermStructureBasis`] measures the future against
18/// spot, this measures it against the perpetual — the leg a perp-vs-future
19/// basis trade actually holds. The output is a fraction; multiply by `10_000`
20/// for basis points.
21///
22/// `Input = DerivativesTick`, `Output = f64`. Stateless; ready after the first
23/// tick.
24///
25/// [`TermStructureBasis`]: crate::TermStructureBasis
26///
27/// # Example
28///
29/// ```
30/// use wickra_core::{CalendarSpread, DerivativesTick, Indicator};
31///
32/// fn tick(futures: f64, mark: f64) -> DerivativesTick {
33///     DerivativesTick::new(0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
34///         .unwrap()
35/// }
36///
37/// let mut cs = CalendarSpread::new();
38/// // futures 101 vs perpetual mark 100 -> 0.01.
39/// assert!((cs.update(tick(101.0, 100.0)).unwrap() - 0.01).abs() < 1e-12);
40/// ```
41#[derive(Debug, Clone, Default)]
42pub struct CalendarSpread {
43    has_emitted: bool,
44}
45
46impl CalendarSpread {
47    /// Construct a new calendar-spread indicator.
48    #[must_use]
49    pub const fn new() -> Self {
50        Self { has_emitted: false }
51    }
52}
53
54impl Indicator for CalendarSpread {
55    type Input = DerivativesTick;
56    type Output = f64;
57
58    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
59        self.has_emitted = true;
60        Some((tick.futures_price - tick.mark_price) / tick.mark_price)
61    }
62
63    fn reset(&mut self) {
64        self.has_emitted = false;
65    }
66
67    fn warmup_period(&self) -> usize {
68        1
69    }
70
71    fn is_ready(&self) -> bool {
72        self.has_emitted
73    }
74
75    fn name(&self) -> &'static str {
76        "CalendarSpread"
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::traits::BatchExt;
84
85    fn tick(futures: f64, mark: f64) -> DerivativesTick {
86        DerivativesTick::new_unchecked(
87            0.0, mark, mark, futures, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
88        )
89    }
90
91    #[test]
92    fn accessors_and_metadata() {
93        let cs = CalendarSpread::new();
94        assert_eq!(cs.name(), "CalendarSpread");
95        assert_eq!(cs.warmup_period(), 1);
96        assert!(!cs.is_ready());
97    }
98
99    #[test]
100    fn future_over_perp_is_positive() {
101        let mut cs = CalendarSpread::new();
102        let out = cs.update(tick(101.0, 100.0)).unwrap();
103        assert!((out - 0.01).abs() < 1e-12);
104        assert!(cs.is_ready());
105    }
106
107    #[test]
108    fn future_under_perp_is_negative() {
109        let mut cs = CalendarSpread::new();
110        let out = cs.update(tick(99.0, 100.0)).unwrap();
111        assert!((out + 0.01).abs() < 1e-12);
112    }
113
114    #[test]
115    fn flat_is_zero() {
116        let mut cs = CalendarSpread::new();
117        assert_eq!(cs.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), 100.0))
124            .collect();
125        let mut a = CalendarSpread::new();
126        let mut b = CalendarSpread::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 cs = CalendarSpread::new();
136        cs.update(tick(101.0, 100.0));
137        assert!(cs.is_ready());
138        cs.reset();
139        assert!(!cs.is_ready());
140    }
141}