Skip to main content

quantwave_core/indicators/
cg.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Center of Gravity (CG) Oscillator
6///
7/// Based on John Ehlers' "The CG Oscillator".
8/// The CG Oscillator is a smoothed oscillator with essentially zero lag.
9/// It identifies turning points by calculating the balance point of prices over a window.
10#[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            // Since prices are increasing, Num/Denom should be small (towards the newest price)
79            // -Num/Denom should be less negative.
80        }
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            // Batch implementation
94            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}