Correlation Cycle (Ehlers)

Parameters: period = 20 | threshold = 9

Overview

The Correlation Cycle, developed by John Ehlers, determines market phase and distinguishes between trending and cycling states using digital signal processing techniques. The indicator correlates price data with cosine and sine reference waves to extract phase information, similar to how radar systems track targets. It calculates the real component through cosine correlation and the imaginary component through negative sine correlation. These components are then converted to a phase angle in degrees, providing a continuous measure of where price sits within its dominant cycle.

The indicator outputs four distinct series that work together to characterize market behavior. The real and imaginary components represent the correlation strengths with their respective reference waves. The phase angle tracks cycle position from 0 to 360 degrees, with smooth progression indicating cycling behavior and erratic jumps suggesting trending conditions. The market state flag uses angle stability to classify periods as either trending or cycling. When consecutive angle changes exceed the threshold (typically 9 degrees), the market is considered trending. Smaller, consistent angle changes indicate cycling behavior where traditional oscillators work best.

Traders use Correlation Cycle to adapt their strategies to current market conditions. During cycling states, mean reversion and oscillator based strategies typically perform well as prices rotate predictably through their phase. In trending states, momentum and breakout strategies become more effective as the normal cycle structure breaks down. The phase angle itself provides timing signals, with certain degree ranges historically coinciding with cycle tops and bottoms. Many systematic traders use the state flag to switch between different trading algorithms, employing cycle strategies when the market is ranging and trend following approaches when directional moves emerge.

Implementation Examples

Compute Correlation Cycle from a slice or candles:

use vectorta::indicators::correlation_cycle::{correlation_cycle, CorrelationCycleInput, CorrelationCycleParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 101.0, 102.5, 101.8, 103.2];
let params = CorrelationCycleParams { period: Some(20), threshold: Some(9.0) };
let input = CorrelationCycleInput::from_slice(&prices, params);
let out = correlation_cycle(&input)?;
// Access outputs
for ((r, i), (a, s)) in out.real.iter().zip(&out.imag).zip(out.angle.iter().zip(&out.state)) {
    println!("real={r:.3}, imag={i:.3}, angle={a:.1}°, state={s}");
}

// From Candles with defaults (period=20, threshold=9.0; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = CorrelationCycleInput::with_default_candles(&candles);
let out = correlation_cycle(&input)?;

API Reference

Input Methods
// From price slice
CorrelationCycleInput::from_slice(&[f64], CorrelationCycleParams) -> CorrelationCycleInput

// From candles with custom source
CorrelationCycleInput::from_candles(&Candles, &str, CorrelationCycleParams) -> CorrelationCycleInput

// From candles with defaults (close, period=20, threshold=9.0)
CorrelationCycleInput::with_default_candles(&Candles) -> CorrelationCycleInput
Parameters Structure
pub struct CorrelationCycleParams {
    pub period: Option<usize>,   // Default: 20
    pub threshold: Option<f64>,  // Default: 9.0 (degrees)
}
Output Structure
pub struct CorrelationCycleOutput {
    pub real: Vec<f64>,   // Cosine correlation component
    pub imag: Vec<f64>,   // Negative-sine correlation component
    pub angle: Vec<f64>,  // Phase angle (degrees), with quadrant adjustment
    pub state: Vec<f64>,  // Trend state: +1.0 up, -1.0 down, 0.0 cycling/indeterminate
}
Validation, Warmup & NaNs
  • period > 0 and period ≤ len(data); else CorrelationCycleError::InvalidPeriod.
  • Rejects all-NaN inputs: CorrelationCycleError::AllValuesNaN.
  • Requires at least period valid points after the first finite value; otherwise CorrelationCycleError::NotEnoughValidData.
  • Warmup: real/imag/angle first finite index is at first_valid + period; preceding entries are NaN. state starts at first_valid + period + 1.
  • Interior NaNs inside a window are treated as 0.0 in correlations; leading prefix determines warmup.
  • state: if |Δangle| < threshold, emits +1.0 when angle ≥ 0, −1.0 when angle < 0, else 0.0.
Error Handling
use vectorta::indicators::correlation_cycle::CorrelationCycleError;

match correlation_cycle(&input) {
    Ok(output) => consume_all(output),
    Err(CorrelationCycleError::EmptyData) =>
        eprintln!("Input data is empty"),
    Err(CorrelationCycleError::InvalidPeriod { period, data_len }) =>
        eprintln!("Invalid period {} for length {}", period, data_len),
    Err(CorrelationCycleError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, only {}", needed, valid),
    Err(CorrelationCycleError::AllValuesNaN) =>
        eprintln!("All input values are NaN"),
}

Python Bindings

Basic Usage
import numpy as np
from vectorta import correlation_cycle

prices = np.asarray([100.0, 101.0, 102.5, 101.8, 103.2], dtype=np.float64)

# Returns a dict with 'real', 'imag', 'angle', 'state'
out = correlation_cycle(prices, period=20, threshold=9.0, kernel="auto")
print(out["real"], out["imag"], out["angle"], out["state"])
Streaming
from vectorta import CorrelationCycleStream

stream = CorrelationCycleStream(period=20, threshold=9.0)
for px in feed_prices():
    tup = stream.update(px)  # (real, imag, angle, state) or None during warmup
    if tup is not None:
        r, i, a, s = tup
        handle(r, i, a, s)
Batch Processing
import numpy as np
from vectorta import correlation_cycle_batch

prices = np.asarray(load_prices(), dtype=np.float64)
res = correlation_cycle_batch(
    prices,
    period_range=(10, 40, 10),
    threshold_range=(5.0, 15.0, 5.0),
    kernel="auto",
)

print(res["real"].shape)   # (rows, len(prices))
print(res["imag"].shape)   # (rows, len(prices))
print(res["angle"].shape)  # (rows, len(prices))
print(res["state"].shape)  # (rows, len(prices))
print(res["periods"])      # vector of period per row
print(res["thresholds"])   # vector of threshold per row
CUDA Acceleration

CUDA support for Correlation Cycle is coming soon.

# Coming soon: CUDA-accelerated Correlation Cycle variants
# (parameter sweeps, multi-series processing, zero-copy into)

JavaScript/WASM Bindings

Basic Usage
import { correlation_cycle_js } from 'vectorta-wasm';

const prices = new Float64Array([100.0, 101.0, 102.5, 101.8, 103.2]);
// Returns an object with Float64Array fields { real, imag, angle, state }
const { real, imag, angle, state } = correlation_cycle_js(prices, 20, 9.0);
console.log('angle[100]=', angle[100]);
Memory-Efficient Operations
import { correlation_cycle_alloc, correlation_cycle_free, correlation_cycle_into, memory } from 'vectorta-wasm';

const data = new Float64Array(loadPrices());
const len = data.length;

const inPtr = correlation_cycle_alloc(len);
const rPtr  = correlation_cycle_alloc(len);
const iPtr  = correlation_cycle_alloc(len);
const aPtr  = correlation_cycle_alloc(len);
const sPtr  = correlation_cycle_alloc(len);

new Float64Array(memory.buffer, inPtr, len).set(data);

// in_ptr, real_ptr, imag_ptr, angle_ptr, state_ptr, len, period, threshold
correlation_cycle_into(inPtr, rPtr, iPtr, aPtr, sPtr, len, 20, 9.0);

const real  = new Float64Array(memory.buffer, rPtr, len).slice();
const imag  = new Float64Array(memory.buffer, iPtr, len).slice();
const angle = new Float64Array(memory.buffer, aPtr, len).slice();
const state = new Float64Array(memory.buffer, sPtr, len).slice();

correlation_cycle_free(inPtr, len);
correlation_cycle_free(rPtr, len);
correlation_cycle_free(iPtr, len);
correlation_cycle_free(aPtr, len);
correlation_cycle_free(sPtr, len);
Batch Processing
import { correlation_cycle_batch_js } from 'vectorta-wasm';

const prices = new Float64Array(loadPrices());
// Define ranges: (start, end, step)
const out = correlation_cycle_batch_js(
  prices,
  10, 40, 10,   // period: 10, 20, 30, 40
  5.0, 15.0, 5.0 // threshold: 5.0, 10.0, 15.0
);

// out has fields { real, imag, angle, state, combos, rows, cols }
// Each of real/imag/angle/state is flat [rows * cols]
const row0_real = out.real.slice(0, out.cols);
const { period, threshold } = out.combos[0];
console.log('row0 params:', period, threshold);

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators