Skip to main content

wickra_core/indicators/
funding_rate_mean.rs

1//! Funding Rate Rolling Mean — average funding rate over a trailing window.
2
3use std::collections::VecDeque;
4
5use crate::derivatives::DerivativesTick;
6use crate::error::{Error, Result};
7use crate::traits::Indicator;
8
9/// Funding Rate Rolling Mean — the arithmetic mean of the funding rate over the
10/// trailing window of `window` ticks.
11///
12/// ```text
13/// mean = (1 / window) · Σ fundingRate over the last `window` ticks
14/// ```
15///
16/// Smoothing the raw [funding rate] reveals the persistent carry regime — a
17/// sustained positive mean marks a crowded-long market paying to hold the
18/// perpetual, a sustained negative mean a crowded-short one. The indicator warms
19/// up for `window` ticks — `update` returns `None` until the window is full —
20/// then emits the rolling mean, maintained in O(1) per tick via a running sum.
21///
22/// `Input = DerivativesTick`, `Output = f64`.
23///
24/// [funding rate]: crate::FundingRate
25///
26/// # Example
27///
28/// ```
29/// use wickra_core::{DerivativesTick, FundingRateMean, Indicator};
30///
31/// fn tick(rate: f64) -> DerivativesTick {
32///     DerivativesTick::new(rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0)
33///         .unwrap()
34/// }
35///
36/// let mut frm = FundingRateMean::new(2).unwrap();
37/// assert_eq!(frm.update(tick(0.001)), None);
38/// // Window full: (0.001 + 0.003) / 2 = 0.002.
39/// assert_eq!(frm.update(tick(0.003)), Some(0.002));
40/// ```
41#[derive(Debug, Clone)]
42pub struct FundingRateMean {
43    window: usize,
44    history: VecDeque<f64>,
45    sum: f64,
46}
47
48impl FundingRateMean {
49    /// Construct a funding-rate rolling mean over a window of `window` ticks.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`Error::PeriodZero`] if `window` is zero.
54    pub fn new(window: usize) -> Result<Self> {
55        if window == 0 {
56            return Err(Error::PeriodZero);
57        }
58        Ok(Self {
59            window,
60            history: VecDeque::with_capacity(window),
61            sum: 0.0,
62        })
63    }
64
65    /// The configured window length, in ticks.
66    #[must_use]
67    pub fn window(&self) -> usize {
68        self.window
69    }
70}
71
72impl Indicator for FundingRateMean {
73    type Input = DerivativesTick;
74    type Output = f64;
75
76    fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
77        self.history.push_back(tick.funding_rate);
78        self.sum += tick.funding_rate;
79        if self.history.len() > self.window {
80            let old = self.history.pop_front().expect("window >= 1, len > window");
81            self.sum -= old;
82        }
83        if self.history.len() < self.window {
84            return None;
85        }
86        Some(self.sum / self.window as f64)
87    }
88
89    fn reset(&mut self) {
90        self.history.clear();
91        self.sum = 0.0;
92    }
93
94    fn warmup_period(&self) -> usize {
95        self.window
96    }
97
98    fn is_ready(&self) -> bool {
99        self.history.len() >= self.window
100    }
101
102    fn name(&self) -> &'static str {
103        "FundingRateMean"
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::traits::BatchExt;
111
112    fn tick(rate: f64) -> DerivativesTick {
113        DerivativesTick::new_unchecked(
114            rate, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
115        )
116    }
117
118    #[test]
119    fn rejects_zero_window() {
120        assert!(matches!(FundingRateMean::new(0), Err(Error::PeriodZero)));
121    }
122
123    #[test]
124    fn accessors_and_metadata() {
125        let frm = FundingRateMean::new(5).unwrap();
126        assert_eq!(frm.name(), "FundingRateMean");
127        assert_eq!(frm.warmup_period(), 5);
128        assert_eq!(frm.window(), 5);
129        assert!(!frm.is_ready());
130    }
131
132    #[test]
133    fn warms_up_then_emits_mean() {
134        let mut frm = FundingRateMean::new(2).unwrap();
135        assert_eq!(frm.update(tick(0.001)), None);
136        assert!(!frm.is_ready());
137        assert_eq!(frm.update(tick(0.003)), Some(0.002));
138        assert!(frm.is_ready());
139    }
140
141    #[test]
142    fn rolls_off_old_values() {
143        let mut frm = FundingRateMean::new(2).unwrap();
144        frm.update(tick(0.001));
145        frm.update(tick(0.003)); // mean 0.002
146        let out = frm.update(tick(0.005)).unwrap(); // window [0.003, 0.005] -> 0.004
147        assert!((out - 0.004).abs() < 1e-12);
148    }
149
150    #[test]
151    fn handles_negative_rates() {
152        let mut frm = FundingRateMean::new(2).unwrap();
153        frm.update(tick(-0.002));
154        let out = frm.update(tick(0.004)).unwrap();
155        assert!((out - 0.001).abs() < 1e-12);
156    }
157
158    #[test]
159    fn batch_equals_streaming() {
160        let ticks: Vec<DerivativesTick> = (0..30)
161            .map(|i| tick(0.0001 * f64::from(i % 7) - 0.0003))
162            .collect();
163        let mut a = FundingRateMean::new(5).unwrap();
164        let mut b = FundingRateMean::new(5).unwrap();
165        assert_eq!(
166            a.batch(&ticks),
167            ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
168        );
169    }
170
171    #[test]
172    fn reset_clears_state() {
173        let mut frm = FundingRateMean::new(2).unwrap();
174        frm.update(tick(0.001));
175        frm.update(tick(0.003));
176        assert!(frm.is_ready());
177        frm.reset();
178        assert!(!frm.is_ready());
179        assert_eq!(frm.update(tick(0.002)), None);
180    }
181}