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    usage: "Use as a zero-lag momentum oscillator to detect cycle turning points. Crossovers of the trigger line provide high-accuracy entry and exit signals.",
51    keywords: &["oscillator", "momentum", "ehlers", "dsp", "zero-lag"],
52    ehlers_summary: "Ehlers introduces the Center of Gravity oscillator in Cybernetic Analysis (2004) as a near-zero-lag indicator. It computes the center of mass of a price series over a lookback window, producing an oscillator whose turning points lead price turns — a reversal of the usual indicator lag relationship.",
53    params: &[ParamDef {
54        name: "period",
55        default: "10",
56        description: "Observation window length",
57    }],
58    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/TheCGOscillator.pdf",
59    formula_latex: r#"
60\[
61CG = -\frac{\sum_{i=0}^{N-1} (i+1) \times Price_i}{\sum_{i=0}^{N-1} Price_i}
62\]
63"#,
64    gold_standard_file: "cg.json",
65    category: "Ehlers DSP",
66};
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::traits::Next;
72    use proptest::prelude::*;
73
74    #[test]
75    fn test_cg_basic() {
76        let mut cg = CenterOfGravity::new(10);
77        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0];
78        for input in inputs {
79            let val = cg.next(input);
80            assert!(!val.is_nan());
81            // Since prices are increasing, Num/Denom should be small (towards the newest price)
82            // -Num/Denom should be less negative.
83        }
84    }
85
86    proptest! {
87        #[test]
88        fn test_cg_parity(
89            inputs in prop::collection::vec(1.0..100.0, 10..100),
90        ) {
91            let period = 10;
92            let mut cg = CenterOfGravity::new(period);
93
94            let streaming_results: Vec<f64> = inputs.iter().map(|&x| cg.next(x)).collect();
95
96            // Batch implementation
97            let mut batch_results = Vec::with_capacity(inputs.len());
98            for i in 0..inputs.len() {
99                let start = if i >= period { i + 1 - period } else { 0 };
100                let window = &inputs[start..=i];
101
102                let mut num = 0.0;
103                let mut denom = 0.0;
104                for (j, &price) in window.iter().rev().enumerate() {
105                    let count = j + 1;
106                    num += count as f64 * price;
107                    denom += price;
108                }
109                batch_results.push(if denom == 0.0 { 0.0 } else { -num / denom });
110            }
111
112            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
113                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
114            }
115        }
116    }
117}