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
//! Perpetual Premium Index — the perp mark price relative to spot.
use crate::derivatives::DerivativesTick;
use crate::traits::Indicator;
/// Perpetual Premium Index — the perpetual's mark price relative to the spot index
/// it tracks, as a fraction.
///
/// ```text
/// premium = (mark_price − index_price) / index_price
/// ```
///
/// A perpetual swap is pegged to spot by the funding mechanism, but it can still
/// trade at a premium (above spot) or discount (below). A positive premium signals
/// net long demand willing to pay up to hold the perp — bullish positioning, and
/// the proximate driver of positive funding; a negative premium signals the
/// reverse. Sustained extremes flag crowded positioning ripe for a funding-driven
/// mean reversion.
///
/// The output is centred on zero and dimensionless (a fraction; multiply by `100`
/// for percent). `index_price` is validated strictly positive on the tick, so the
/// division is always defined. It is stateless — each tick yields one value (no
/// warmup). Each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{DerivativesTick, Indicator, PerpetualPremiumIndex};
///
/// let mut indicator = PerpetualPremiumIndex::new();
/// // Mark 101 vs index 100 -> +1% premium.
/// 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();
/// let premium = indicator.update(tick).unwrap();
/// assert!((premium - 0.01).abs() < 1e-12);
/// ```
#[derive(Debug, Clone, Default)]
pub struct PerpetualPremiumIndex {
ready: bool,
}
impl PerpetualPremiumIndex {
/// Construct a new Perpetual Premium Index. The indicator is parameter-free.
#[must_use]
pub const fn new() -> Self {
Self { ready: false }
}
}
impl Indicator for PerpetualPremiumIndex {
type Input = DerivativesTick;
type Output = f64;
fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
let premium = (tick.mark_price - tick.index_price) / tick.index_price;
self.ready = true;
Some(premium)
}
fn reset(&mut self) {
self.ready = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.ready
}
fn name(&self) -> &'static str {
"PerpetualPremiumIndex"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn tick(mark: f64, index: f64) -> DerivativesTick {
DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let p = PerpetualPremiumIndex::new();
assert_eq!(p.warmup_period(), 1);
assert_eq!(p.name(), "PerpetualPremiumIndex");
assert!(!p.is_ready());
}
#[test]
fn premium_reference_value() {
let mut p = PerpetualPremiumIndex::new();
assert_relative_eq!(p.update(tick(101.0, 100.0)).unwrap(), 0.01, epsilon = 1e-12);
}
#[test]
fn discount_is_negative() {
let mut p = PerpetualPremiumIndex::new();
assert!(p.update(tick(99.0, 100.0)).unwrap() < 0.0);
}
#[test]
fn at_par_is_zero() {
let mut p = PerpetualPremiumIndex::new();
assert_relative_eq!(p.update(tick(100.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12);
}
#[test]
fn ready_after_first_update() {
let mut p = PerpetualPremiumIndex::new();
assert!(!p.is_ready());
p.update(tick(100.0, 100.0));
assert!(p.is_ready());
}
#[test]
fn reset_clears_state() {
let mut p = PerpetualPremiumIndex::new();
p.update(tick(101.0, 100.0));
assert!(p.is_ready());
p.reset();
assert!(!p.is_ready());
}
#[test]
fn batch_equals_streaming() {
let ticks: Vec<DerivativesTick> = (0..40)
.map(|i| tick(100.0 + (f64::from(i) * 0.3).sin(), 100.0))
.collect();
let batch = PerpetualPremiumIndex::new().batch(&ticks);
let mut b = PerpetualPremiumIndex::new();
let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}