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