Center of Gravity (CG)

Parameters: period = 10 (2–200)

Overview

The Center of Gravity (CG) oscillator, developed by John Ehlers, identifies potential turning points with minimal lag by calculating the balance point of prices within a lookback window. The indicator applies position weighted averaging where each price is multiplied by its position in the window, with recent prices receiving higher weights. This weighted sum is then divided by the sum of prices to produce an oscillator that responds quickly to price inflections while filtering out market noise.

CG oscillates around a zero line, with crossings signaling potential trend changes. Positive values indicate upward price momentum while negative values suggest downward pressure. The indicator excels at identifying cycle extremes and momentum shifts earlier than traditional moving average based oscillators. Because it uses position weighting rather than exponential smoothing, CG maintains better phase characteristics for cycle analysis.

Traders often combine CG with other Ehlers indicators like the Cyber Cycle or bandpass filters to create comprehensive cycle trading systems. The oscillator works particularly well in ranging markets where its low lag properties help identify turning points before price reverses. Divergences between CG and price action frequently precede significant market moves, making it valuable for anticipating trend exhaustion.

Implementation Examples

Compute CG from price slices or candles in a few lines:

use vectorta::indicators::cg::{cg, CgInput, CgParams};
use vectorta::utilities::data_loader::Candles;

let closes = vec![101.2, 102.4, 100.9, 99.8, 101.7, 103.0];
let params = CgParams { period: Some(10) };
let input = CgInput::from_slice(&closes, params);
let output = cg(&input)?;

// Or leverage the candle helper (defaults to close prices, period = 10)
let candles: Candles = load_candles()?;
let input = CgInput::with_default_candles(&candles);
let output = cg(&input)?;

for (idx, value) in output.values.iter().enumerate() {
    println!("Bar {idx}: {:.4}", value);
}

API Reference

Input Methods
// From raw slices
CgInput::from_slice(&[f64], CgParams) -> CgInput

// From Candles with explicit source column
CgInput::from_candles(&Candles, &str, CgParams) -> CgInput

// Convenience helper (close price, default params)
CgInput::with_default_candles(&Candles) -> CgInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct CgParams {
    pub period: Option<usize>, // Default: Some(10)
}

impl Default for CgParams {
    fn default() -> Self {
        Self { period: Some(10) }
    }
}
Output Structures
pub struct CgOutput {
    pub values: Vec<f64>, // NaNs until warm-up completes
}

pub struct CgBatchOutput {
    pub values: Vec<f64>, // Flattened [rows * cols]
    pub combos: Vec<CgParams>,
    pub rows: usize,
    pub cols: usize,
}

impl CgBatchOutput {
    pub fn row_for_params(&self, p: &CgParams) -> Option<usize> { /* ... */ }
    pub fn values_for(&self, p: &CgParams) -> Option<&[f64]> { /* ... */ }
}
Error Handling
use vectorta::indicators::cg::{cg, CgError};

match cg(&input) {
    Ok(output) => process(output.values),
    Err(CgError::EmptyData) => eprintln!("CG: empty data provided"),
    Err(CgError::InvalidPeriod { period, data_len }) =>
        eprintln!("CG: period {period} incompatible with length {data_len}"),
    Err(CgError::AllValuesNaN) => eprintln!("CG: every value was NaN"),
    Err(CgError::NotEnoughValidData { needed, valid }) =>
        eprintln!("CG: need {needed} valid points, only {valid} available"),
}
SIMD & Zero-Copy Helpers
use vectorta::indicators::cg::{cg_into_slice, CgInput, CgParams};
use vectorta::utilities::enums::Kernel;

let params = CgParams { period: Some(14) };
let input = CgInput::from_slice(&closes, params);
let mut buffer = vec![f64::NAN; closes.len()];

cg_into_slice(&mut buffer, &input, Kernel::Auto)?;
assert_eq!(buffer.len(), closes.len());

Python Bindings

Basic Usage

NumPy arrays map directly onto the Rust implementation:

import numpy as np
from vectorta import cg

prices = np.array([101.2, 102.4, 100.9, 99.8, 101.7, 103.0], dtype=np.float64)

# Default period (10)
values = cg(prices)

# Custom period with explicit kernel selection
values = cg(prices, period=14, kernel="avx2")

print(f"CG values: {values}")
Streaming Updates

Maintain intraday state with the bound CgStream class:

import numpy as np
from vectorta import CgStream

stream = CgStream(period=10)
prices = np.array([101.2, 102.4, 100.9, 99.8, 101.7, 103.0, 104.1], dtype=np.float64)

for price in prices:
    value = stream.update(float(price))
    if value is None:
        print("warming up...")
    else:
        print(f"CG: {value:.4f}")
Batch Optimization

Test multiple period settings with a single call:

import numpy as np
from vectorta import cg_batch

prices = np.array([101.2, 102.4, 100.9, 99.8, 101.7, 103.0, 104.1], dtype=np.float64)

result = cg_batch(
    prices,
    period_range=(6, 18, 2),  # 6, 8, ..., 18
    kernel="auto",
)

values = result["values"]  # Shape: (rows, len(prices))
periods = result["periods"]

best_idx = np.argmax(values[:, -1])
print(f"Best period: {periods[best_idx]}")
CUDA Acceleration

CUDA bindings for CG follow the same pattern as other GPU-enabled indicators and are currently under active development.

# Coming soon: CUDA-accelerated Center of Gravity routines
#
# from vectorta import cg_cuda_batch, cg_cuda_many_series_one_param
# import numpy as np
#
# prices = np.array([...], dtype=np.float32)
# results = cg_cuda_batch(
#     data=prices,
#     period_range=(6, 32, 2),
#     device_id=0,
# )
#
# # Multi-series, single parameter configuration
# grid = np.array([[...], [...]], dtype=np.float32)  # Shape: [time, assets]
# output = cg_cuda_many_series_one_param(
#     data_tm_f32=grid,
#     period=10,
#     device_id=0,
# )

JavaScript/WASM Bindings

Basic Usage

Call CG directly from WebAssembly-enabled runtimes:

import { cg_js } from 'vectorta-wasm';

const prices = new Float64Array([101.2, 102.4, 100.9, 99.8, 101.7, 103.0]);
const values = cg_js(prices, 10);

console.log('CG values:', values);
console.log('Last point:', values[values.length - 1]);
Zero-Copy & Memory Helpers

Reuse WASM buffers for high-throughput feeds:

import { cg_alloc, cg_free, cg_into, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* streaming data */]);
const length = prices.length;

const inputPtr = cg_alloc(length);
new Float64Array(memory.buffer, inputPtr, length).set(prices);

const outputPtr = cg_alloc(length);

cg_into(inputPtr, outputPtr, length, 10);

const cgValues = new Float64Array(memory.buffer, outputPtr, length).slice();

cg_free(inputPtr, length);
cg_free(outputPtr, length);
Batch Sweeps

Choose between object-based or raw-pointer APIs:

import { cg_alloc, cg_batch, cg_batch_into, cg_free, memory } from 'vectorta-wasm';

const prices = new Float64Array([101.2, 102.4, 100.9, 99.8, 101.7, 103.0]);

// High-level config API
const config = { period_range: [6, 18, 2] }; // start, end, step
const result = cg_batch(prices, config);
const { values, combos, rows, cols } = result;

const firstRow = values.slice(0, cols);
console.log('Period tested:', combos[0].period);
console.log('First CG series:', firstRow);

// Low-level zero-copy variant (inclusive range)
const combosEstimate = Math.floor((18 - 6) / 2) + 1;
const inputPtr = cg_alloc(prices.length);
new Float64Array(memory.buffer, inputPtr, prices.length).set(prices);
const outputPtr = cg_alloc(prices.length * combosEstimate);

const combos = cg_batch_into(inputPtr, outputPtr, prices.length, 6, 18, 2);
console.log(`Tested ${combos} combinations`);
const flat = new Float64Array(memory.buffer, outputPtr, combos * prices.length);

cg_free(inputPtr, prices.length);
cg_free(outputPtr, combos * prices.length);

Performance Analysis

Comparison:
View:
Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

Related Indicators