Skip to main content

wickra_core/indicators/
perpetual_premium_index.rs

1//! Perpetual Premium Index — the perp mark price relative to spot.
2
3use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6/// Perpetual Premium Index — the perpetual's mark price relative to the spot index
7/// it tracks, as a fraction.
8///
9/// ```text
10/// premium = (mark_price − index_price) / index_price
11/// ```
12///
13/// A perpetual swap is pegged to spot by the funding mechanism, but it can still
14/// trade at a premium (above spot) or discount (below). A positive premium signals
15/// net long demand willing to pay up to hold the perp — bullish positioning, and
16/// the proximate driver of positive funding; a negative premium signals the
17/// reverse. Sustained extremes flag crowded positioning ripe for a funding-driven
18/// mean reversion.
19///
20/// The output is centred on zero and dimensionless (a fraction; multiply by `100`
21/// for percent). `index_price` is validated strictly positive on the tick, so the
22/// division is always defined. It is stateless — each tick yields one value (no
23/// warmup). Each `update` is O(1).
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{DerivativesTick, Indicator, PerpetualPremiumIndex};
29///
30/// let mut indicator = PerpetualPremiumIndex::new();
31/// // Mark 101 vs index 100 -> +1% premium.
32/// let tick = DerivativesTick::new(0.0, 101.0, 100.0, 101.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap();
33/// let premium = indicator.update(tick).unwrap();
34/// assert!((premium - 0.01).abs() < 1e-12);
35/// ```
36#[derive(Debug, Clone, Default)]
37pub struct PerpetualPremiumIndex {
38    ready: bool,
39}
40
41impl PerpetualPremiumIndex {
42    /// Construct a new Perpetual Premium Index. The indicator is parameter-free.
43    #[must_use]
44    pub const fn new() -> Self {
45        Self { ready: false }
46    }
47}
48
49impl Indicator for PerpetualPremiumIndex {
50    type Input = DerivativesTick;
51    type Output = f64;
52
53    #[inline]
54    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
55        let premium = (tick.mark_price - tick.index_price) / tick.index_price;
56        self.ready = true;
57        Some(premium)
58    }
59
60    fn reset(&mut self) {
61        self.ready = false;
62    }
63
64    #[inline]
65    fn warmup_period(&self) -> usize {
66        1
67    }
68
69    #[inline]
70    fn is_ready(&self) -> bool {
71        self.ready
72    }
73
74    #[inline]
75    fn name(&self) -> &'static str {
76        "PerpetualPremiumIndex"
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::traits::BatchExt;
84    use approx::assert_relative_eq;
85
86    fn tick(mark: f64, index: f64) -> DerivativesTick {
87        DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
88    }
89
90    #[test]
91    fn accessors_and_metadata() {
92        let p = PerpetualPremiumIndex::new();
93        assert_eq!(p.warmup_period(), 1);
94        assert_eq!(p.name(), "PerpetualPremiumIndex");
95        assert!(!p.is_ready());
96    }
97
98    #[test]
99    fn premium_reference_value() {
100        let mut p = PerpetualPremiumIndex::new();
101        assert_relative_eq!(p.update(tick(101.0, 100.0)).unwrap(), 0.01, epsilon = 1e-12);
102    }
103
104    #[test]
105    fn discount_is_negative() {
106        let mut p = PerpetualPremiumIndex::new();
107        assert!(p.update(tick(99.0, 100.0)).unwrap() < 0.0);
108    }
109
110    #[test]
111    fn at_par_is_zero() {
112        let mut p = PerpetualPremiumIndex::new();
113        assert_relative_eq!(p.update(tick(100.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
114    }
115
116    #[test]
117    fn ready_after_first_update() {
118        let mut p = PerpetualPremiumIndex::new();
119        assert!(!p.is_ready());
120        p.update(tick(100.0, 100.0));
121        assert!(p.is_ready());
122    }
123
124    #[test]
125    fn reset_clears_state() {
126        let mut p = PerpetualPremiumIndex::new();
127        p.update(tick(101.0, 100.0));
128        assert!(p.is_ready());
129        p.reset();
130        assert!(!p.is_ready());
131    }
132
133    #[test]
134    fn batch_equals_streaming() {
135        let ticks: Vec<DerivativesTick> = (0..40)
136            .map(|i| tick(100.0 + (f64::from(i) * 0.3).sin(), 100.0))
137            .collect();
138        let batch = PerpetualPremiumIndex::new().batch(&ticks);
139        let mut b = PerpetualPremiumIndex::new();
140        let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
141        assert_eq!(batch, streamed);
142    }
143}