#![allow(clippy::manual_midpoint)]
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct CenterOfGravity {
period: usize,
window: VecDeque<f64>,
last_value: Option<f64>,
}
impl CenterOfGravity {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
window: VecDeque::with_capacity(period),
last_value: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for CenterOfGravity {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last_value;
}
if self.window.len() == self.period {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period {
return None;
}
let mut num = 0.0;
let mut den = 0.0;
for (k, p) in self.window.iter().rev().enumerate() {
let w = 1.0 + k as f64;
num += w * p;
den += p;
}
let v = if den.abs() > f64::EPSILON {
-num / den + (self.period as f64 + 1.0) / 2.0
} else {
0.0
};
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.window.clear();
self.last_value = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"CenterOfGravity"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn new_rejects_zero_period() {
assert!(matches!(CenterOfGravity::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let mut cg = CenterOfGravity::new(10).unwrap();
assert_eq!(cg.period(), 10);
assert_eq!(cg.warmup_period(), 10);
assert_eq!(cg.name(), "CenterOfGravity");
assert!(!cg.is_ready());
for i in 1..=10 {
cg.update(f64::from(i));
}
assert!(cg.is_ready());
assert!(cg.value().is_some());
}
#[test]
fn constant_series_yields_zero() {
let mut cg = CenterOfGravity::new(5).unwrap();
let out = cg.batch(&[7.0_f64; 30]);
for x in out.iter().skip(5).flatten() {
assert_relative_eq!(*x, 0.0, epsilon = 1e-12);
}
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=50).map(f64::from).collect();
let mut a = CenterOfGravity::new(10).unwrap();
let mut b = CenterOfGravity::new(10).unwrap();
let batch = a.batch(&prices);
let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn ignores_non_finite_input() {
let mut cg = CenterOfGravity::new(5).unwrap();
cg.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
let before = cg.value();
assert!(before.is_some());
assert_eq!(cg.update(f64::NAN), before);
}
#[test]
fn reset_clears_state() {
let mut cg = CenterOfGravity::new(5).unwrap();
cg.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
assert!(cg.is_ready());
cg.reset();
assert!(!cg.is_ready());
}
#[test]
fn warmup_returns_none_until_seed() {
let mut cg = CenterOfGravity::new(4).unwrap();
assert_eq!(cg.update(1.0), None);
assert_eq!(cg.update(2.0), None);
assert_eq!(cg.update(3.0), None);
assert!(cg.update(4.0).is_some());
}
#[test]
fn zero_window_uses_zero_fallback() {
let mut cg = CenterOfGravity::new(5).unwrap();
let out = cg.batch(&[0.0_f64; 10]);
for x in out.iter().skip(5).flatten() {
assert_relative_eq!(*x, 0.0, epsilon = 1e-12);
}
}
}