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