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