quantwave_core/indicators/
cg.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
11pub struct CenterOfGravity {
12 period: usize,
13 window: VecDeque<f64>,
14}
15
16impl CenterOfGravity {
17 pub fn new(period: usize) -> Self {
18 Self {
19 period,
20 window: VecDeque::with_capacity(period),
21 }
22 }
23}
24
25impl Next<f64> for CenterOfGravity {
26 type Output = f64;
27
28 fn next(&mut self, input: f64) -> Self::Output {
29 self.window.push_front(input);
30 if self.window.len() > self.period {
31 self.window.pop_back();
32 }
33
34 let mut num = 0.0;
35 let mut denom = 0.0;
36
37 for (i, &price) in self.window.iter().enumerate() {
38 let count = i + 1;
39 num += count as f64 * price;
40 denom += price;
41 }
42
43 if denom == 0.0 { 0.0 } else { -num / denom }
44 }
45}
46
47pub const CG_METADATA: IndicatorMetadata = IndicatorMetadata {
48 name: "Center of Gravity Oscillator",
49 description: "The CG Oscillator identifies price turning points with essentially zero lag by calculating the balance point of prices.",
50 params: &[ParamDef {
51 name: "period",
52 default: "10",
53 description: "Observation window length",
54 }],
55 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/TheCGOscillator.pdf",
56 formula_latex: r#"
57\[
58CG = -\frac{\sum_{i=0}^{N-1} (i+1) \times Price_i}{\sum_{i=0}^{N-1} Price_i}
59\]
60"#,
61 gold_standard_file: "cg.json",
62 category: "Ehlers DSP",
63};
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::traits::Next;
69 use proptest::prelude::*;
70
71 #[test]
72 fn test_cg_basic() {
73 let mut cg = CenterOfGravity::new(10);
74 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
75 for input in inputs {
76 let val = cg.next(input);
77 assert!(!val.is_nan());
78 }
81 }
82
83 proptest! {
84 #[test]
85 fn test_cg_parity(
86 inputs in prop::collection::vec(1.0..100.0, 10..100),
87 ) {
88 let period = 10;
89 let mut cg = CenterOfGravity::new(period);
90
91 let streaming_results: Vec<f64> = inputs.iter().map(|&x| cg.next(x)).collect();
92
93 let mut batch_results = Vec::with_capacity(inputs.len());
95 for i in 0..inputs.len() {
96 let start = if i >= period { i + 1 - period } else { 0 };
97 let window = &inputs[start..=i];
98
99 let mut num = 0.0;
100 let mut denom = 0.0;
101 for (j, &price) in window.iter().rev().enumerate() {
102 let count = j + 1;
103 num += count as f64 * price;
104 denom += price;
105 }
106 batch_results.push(if denom == 0.0 { 0.0 } else { -num / denom });
107 }
108
109 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
110 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
111 }
112 }
113 }
114}