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 fn update(&mut self, tick: DerivativesTick) -> Option<f64> {
73 self.ready = true;
74 Some(tick.funding_rate * self.intervals_per_year)
75 }
76
77 fn reset(&mut self) {
78 self.ready = false;
79 }
80
81 fn warmup_period(&self) -> usize {
82 1
83 }
84
85 fn is_ready(&self) -> bool {
86 self.ready
87 }
88
89 fn name(&self) -> &'static str {
90 "FundingImpliedApr"
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::traits::BatchExt;
98 use approx::assert_relative_eq;
99
100 fn tick(funding: f64) -> DerivativesTick {
101 DerivativesTick::new_unchecked(
102 funding, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0,
103 )
104 }
105
106 #[test]
107 fn rejects_invalid_intervals() {
108 assert!(matches!(
109 FundingImpliedApr::new(0.0),
110 Err(Error::InvalidParameter { .. })
111 ));
112 assert!(matches!(
113 FundingImpliedApr::new(-1.0),
114 Err(Error::InvalidParameter { .. })
115 ));
116 }
117
118 #[test]
119 fn accessors_and_metadata() {
120 let f = FundingImpliedApr::new(1095.0).unwrap();
121 assert_relative_eq!(f.intervals_per_year(), 1095.0, epsilon = 1e-12);
122 assert_eq!(f.warmup_period(), 1);
123 assert_eq!(f.name(), "FundingImpliedApr");
124 assert!(!f.is_ready());
125 }
126
127 #[test]
128 fn apr_reference_value() {
129 let mut f = FundingImpliedApr::new(1095.0).unwrap();
130 assert_relative_eq!(f.update(tick(0.0001)).unwrap(), 0.1095, epsilon = 1e-9);
131 }
132
133 #[test]
134 fn negative_funding_is_negative_apr() {
135 let mut f = FundingImpliedApr::new(1095.0).unwrap();
136 assert!(f.update(tick(-0.0001)).unwrap() < 0.0);
137 }
138
139 #[test]
140 fn zero_funding_is_zero() {
141 let mut f = FundingImpliedApr::new(365.0).unwrap();
142 assert_relative_eq!(f.update(tick(0.0)).unwrap(), 0.0, epsilon = 1e-12);
143 }
144
145 #[test]
146 fn reset_clears_state() {
147 let mut f = FundingImpliedApr::new(1095.0).unwrap();
148 f.update(tick(0.0001));
149 assert!(f.is_ready());
150 f.reset();
151 assert!(!f.is_ready());
152 }
153
154 #[test]
155 fn batch_equals_streaming() {
156 let ticks: Vec<DerivativesTick> = (0..40)
157 .map(|i| tick(0.0001 * (f64::from(i) * 0.3).sin()))
158 .collect();
159 let batch = FundingImpliedApr::new(1095.0).unwrap().batch(&ticks);
160 let mut b = FundingImpliedApr::new(1095.0).unwrap();
161 let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect();
162 assert_eq!(batch, streamed);
163 }
164}