wickra_core/indicators/
funding_rate_mean.rs1use 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#[derive(Debug, Clone)]
43pub struct FundingRateMean {
44 window: usize,
45 history: VecDeque<f64>,
46 sum: RollingSum,
47}
48
49impl FundingRateMean {
50 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 #[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)); let out = frm.update(tick(0.005)).unwrap(); 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}