wickra_core/indicators/
center_of_gravity.rs1#![allow(clippy::manual_midpoint)]
3
4use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
37pub struct CenterOfGravity {
38 period: usize,
39 window: VecDeque<f64>,
40 last_value: Option<f64>,
41}
42
43impl CenterOfGravity {
44 pub fn new(period: usize) -> Result<Self> {
50 if period == 0 {
51 return Err(Error::PeriodZero);
52 }
53 if period > crate::error::MAX_PERIOD {
54 return Err(Error::InvalidPeriod {
55 message: crate::error::PERIOD_ABOVE_MAX,
56 });
57 }
58 Ok(Self {
59 period,
60 window: VecDeque::with_capacity(period),
61 last_value: None,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69
70 pub const fn value(&self) -> Option<f64> {
72 self.last_value
73 }
74}
75
76impl Indicator for CenterOfGravity {
77 type Input = f64;
78 type Output = f64;
79
80 #[inline]
81 fn update(&mut self, input: f64) -> Option<f64> {
82 if !input.is_finite() {
83 return None;
84 }
85 if self.window.len() == self.period {
86 self.window.pop_front();
87 }
88 self.window.push_back(input);
89 if self.window.len() < self.period {
90 return None;
91 }
92 let mut num = 0.0;
94 let mut den = 0.0;
95 for (k, p) in self.window.iter().rev().enumerate() {
96 let w = 1.0 + k as f64;
97 num += w * p;
98 den += p;
99 }
100 let v = if den.abs() > f64::EPSILON {
101 -num / den + (self.period as f64 + 1.0) / 2.0
102 } else {
103 0.0
104 };
105 self.last_value = Some(v);
106 Some(v)
107 }
108
109 fn reset(&mut self) {
110 self.window.clear();
111 self.last_value = None;
112 }
113
114 #[inline]
115 fn warmup_period(&self) -> usize {
116 self.period
117 }
118
119 #[inline]
120 fn is_ready(&self) -> bool {
121 self.last_value.is_some()
122 }
123
124 #[inline]
125 fn name(&self) -> &'static str {
126 "CenterOfGravity"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134 use approx::assert_relative_eq;
135
136 #[test]
137 fn new_rejects_zero_period() {
138 assert!(matches!(CenterOfGravity::new(0), Err(Error::PeriodZero)));
139 }
140
141 #[test]
142 fn accessors_and_metadata() {
143 let mut cg = CenterOfGravity::new(10).unwrap();
144 assert_eq!(cg.period(), 10);
145 assert_eq!(cg.warmup_period(), 10);
146 assert_eq!(cg.name(), "CenterOfGravity");
147 assert!(!cg.is_ready());
148 for i in 1..=10 {
149 cg.update(f64::from(i));
150 }
151 assert!(cg.is_ready());
152 assert!(cg.value().is_some());
153 }
154
155 #[test]
156 fn constant_series_yields_zero() {
157 let mut cg = CenterOfGravity::new(5).unwrap();
160 let out = cg.batch(&[7.0_f64; 30]);
161 for x in out.iter().skip(5).flatten() {
162 assert_relative_eq!(*x, 0.0, epsilon = 1e-12);
163 }
164 }
165
166 #[test]
167 fn batch_equals_streaming() {
168 let prices: Vec<f64> = (1..=50).map(f64::from).collect();
169 let mut a = CenterOfGravity::new(10).unwrap();
170 let mut b = CenterOfGravity::new(10).unwrap();
171 let batch = a.batch(&prices);
172 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
173 assert_eq!(batch, streamed);
174 }
175
176 #[test]
177 fn ignores_non_finite_input() {
178 let mut cg = CenterOfGravity::new(5).unwrap();
179 cg.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
180 let before = cg.value();
181 assert!(before.is_some());
182 assert_eq!(cg.update(f64::NAN), None);
183 }
184
185 #[test]
186 fn reset_clears_state() {
187 let mut cg = CenterOfGravity::new(5).unwrap();
188 cg.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
189 assert!(cg.is_ready());
190 cg.reset();
191 assert!(!cg.is_ready());
192 }
193
194 #[test]
195 fn warmup_returns_none_until_seed() {
196 let mut cg = CenterOfGravity::new(4).unwrap();
197 assert_eq!(cg.update(1.0), None);
198 assert_eq!(cg.update(2.0), None);
199 assert_eq!(cg.update(3.0), None);
200 assert!(cg.update(4.0).is_some());
201 }
202
203 #[test]
204 fn zero_window_uses_zero_fallback() {
205 let mut cg = CenterOfGravity::new(5).unwrap();
208 let out = cg.batch(&[0.0_f64; 10]);
209 for x in out.iter().skip(5).flatten() {
210 assert_relative_eq!(*x, 0.0, epsilon = 1e-12);
211 }
212 }
213}