Correlation Cycle (Ehlers)
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 > 0andperiod ≤ len(data); elseCorrelationCycleError::InvalidPeriod.- Rejects all-
NaNinputs:CorrelationCycleError::AllValuesNaN. - Requires at least
periodvalid points after the first finite value; otherwiseCorrelationCycleError::NotEnoughValidData. - Warmup:
real/imag/anglefirst finite index is atfirst_valid + period; preceding entries areNaN.statestarts atfirst_valid + period + 1. - Interior
NaNs inside a window are treated as0.0in correlations; leading prefix determines warmup. state: if|Δangle| < threshold, emits+1.0whenangle ≥ 0,−1.0whenangle < 0, else0.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
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05