wickra_core/indicators/
funding_implied_apr.rs1use crate::derivatives::DerivativesTick;
4use crate::error::{Error, Result};
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
37pub struct FundingImpliedApr {
38 intervals_per_year: f64,
39 ready: bool,
40}
41
42impl FundingImpliedApr {
43 pub fn new(intervals_per_year: f64) -> Result<Self> {
51 if !intervals_per_year.is_finite() || intervals_per_year <= 0.0 {
52 return Err(Error::InvalidParameter {
53 message: "intervals_per_year must be finite and positive",
54 });
55 }
56 Ok(Self {
57 intervals_per_year,
58 ready: false,
59 })
60 }
61
62 pub const fn intervals_per_year(&self) -> f64 {
64 self.intervals_per_year
65 }
66}
67
68impl Indicator for FundingImpliedApr {
69 type Input = DerivativesTick;
70 type Output = f64;
71
72 #[inline]
73 fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
74 self.ready = true;
75 Some(tick.funding_rate * self.intervals_per_year)
76 }
77
78 fn reset(&mut self) {
79 self.ready = false;
80 }
81
82 #[inline]
83 fn warmup_period(&self) -> usize {
84 1
85 }
86
87 #[inline]
88 fn is_ready(&self) -> bool {
89 self.ready
90 }
91
92 #[inline]
93 fn name(&self) -> &'static str {
94 "FundingImpliedApr"
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::traits::BatchExt;
102 use approx::assert_relative_eq;
103
104 fn tick(funding: f64) -> DerivativesTick {
105 DerivativesTick::new_unchecked(
106 funding, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
107 )
108 }
109
110 #[test]
111 fn rejects_invalid_intervals() {
112 assert!(matches!(
113 FundingImpliedApr::new(0.0),
114 Err(Error::InvalidParameter { .. })
115 ));
116 assert!(matches!(
117 FundingImpliedApr::new(-1.0),
118 Err(Error::InvalidParameter { .. })
119 ));
120 }
121
122 #[test]
123 fn accessors_and_metadata() {
124 let f = FundingImpliedApr::new(1095.0).unwrap();
125 assert_relative_eq!(f.intervals_per_year(), 1095.0, epsilon = 1e-12);
126 assert_eq!(f.warmup_period(), 1);
127 assert_eq!(f.name(), "FundingImpliedApr");
128 assert!(!f.is_ready());
129 }
130
131 #[test]
132 fn apr_reference_value() {
133 let mut f = FundingImpliedApr::new(1095.0).unwrap();
134 assert_relative_eq!(f.update(tick(0.0001)).unwrap(), 0.1095, epsilon = 1e-9);
135 }
136
137 #[test]
138 fn negative_funding_is_negative_apr() {
139 let mut f = FundingImpliedApr::new(1095.0).unwrap();
140 assert!(f.update(tick(-0.0001)).unwrap() < 0.0);
141 }
142
143 #[test]
144 fn zero_funding_is_zero() {
145 let mut f = FundingImpliedApr::new(365.0).unwrap();
146 assert_relative_eq!(f.update(tick(0.0)).unwrap(), 0.0, epsilon = 1e-12);
147 }
148
149 #[test]
150 fn reset_clears_state() {
151 let mut f = FundingImpliedApr::new(1095.0).unwrap();
152 f.update(tick(0.0001));
153 assert!(f.is_ready());
154 f.reset();
155 assert!(!f.is_ready());
156 }
157
158 #[test]
159 fn batch_equals_streaming() {
160 let ticks: Vec<DerivativesTick> = (0..40)
161 .map(|i| tick(0.0001 * (f64::from(i) * 0.3).sin()))
162 .collect();
163 let batch = FundingImpliedApr::new(1095.0).unwrap().batch(&ticks);
164 let mut b = FundingImpliedApr::new(1095.0).unwrap();
165 let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
166 assert_eq!(batch, streamed);
167 }
168}