WaveTrend Indicator

Parameters: channel_length = 9 | average_length = 12 | ma_length = 3 | factor = 0.015

Overview

The WaveTrend oscillator traces its roots back to AIQ Systems' Trading Channel Index created in 1986, which itself adapted Donald Lambert's Commodity Channel Index, before LazyBear popularized it on TradingView in 2014 where it became one of the platform's top five most used indicators. The algorithm transforms price through multiple layers of exponential smoothing and normalization to produce wave-like momentum oscillations that excel at detecting true reversals while avoiding false signals during choppy sideways markets. WaveTrend calculates an initial channel smoothing (ESA) followed by deviation measurements that get normalized by a scaling factor, then applies additional exponential smoothing to create the primary WT1 line and a simple moving average to generate the WT2 signal line. Traders watch for crossovers between these lines when the oscillator reaches extreme levels, with WT1 crossing above WT2 near oversold territory signaling potential buying opportunities and downward crossovers in overbought zones suggesting sells. The indicator's layered approach filters out market noise while maintaining sensitivity to genuine price movements, making it particularly effective for short-term traders who need quick yet reliable overbought and oversold signals. Its non-repainting nature and ability to stay quiet during consolidation phases have made WaveTrend a trusted tool for identifying high-probability reversal points across various markets and timeframes.

Implementation Examples

Get started with WaveTrend in Rust:

use vectorta::indicators::wavetrend::{wavetrend, WavetrendInput, WavetrendParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with price slice
let prices = vec![100.0, 102.0, 101.5, 103.0, 105.0, 104.5];
let params = WavetrendParams {
    channel_length: Some(9),
    average_length: Some(12),
    ma_length: Some(3),
    factor: Some(0.015),
};
let input = WavetrendInput::from_slice(&prices, params);
let out = wavetrend(&input)?;

// Using with Candles (defaults: source="hlc3", 9/12/3, 0.015)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = WavetrendInput::with_default_candles(&candles);
let out = wavetrend(&input)?;

// Access WT1 / WT2 / Diff (WT2 − WT1)
for (wt1, wt2, d) in itertools::izip!(out.wt1, out.wt2, out.wt_diff) {
    println!("WT1={:.4}, WT2={:.4}, Diff={:.4}", wt1, wt2, d);
}

API Reference

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

// From candles with custom source (e.g., "close", "hlc3")
WavetrendInput::from_candles(&Candles, &str, WavetrendParams) -> WavetrendInput

// From candles with defaults (source = "hlc3", 9/12/3, factor = 0.015)
WavetrendInput::with_default_candles(&Candles) -> WavetrendInput
Parameters Structure
pub struct WavetrendParams {
    pub channel_length: Option<usize>, // Default: 9
    pub average_length: Option<usize>, // Default: 12
    pub ma_length: Option<usize>,      // Default: 3
    pub factor: Option<f64>,           // Default: 0.015
}
Output Structure
pub struct WavetrendOutput {
    pub wt1: Vec<f64>,     // Primary line (WT1)
    pub wt2: Vec<f64>,     // Signal line (SMA of WT1)
    pub wt_diff: Vec<f64>, // Difference (WT2 − WT1)
}
Validation, Warmup & NaNs
  • channel_length > 0, average_length > 0, ma_length > 0; each must be ≤ input length.
  • At least max(channel_length, average_length, ma_length) valid points after the first finite input; else WavetrendError::NotEnoughValidData.
  • Warmup prefix is NaN. Warmup length: first_valid + (channel − 1) + (average − 1) + (ma − 1).
  • Slice inputs use the series as-is; candles default to hlc3 unless you specify the source string.
  • Streaming returns None until all stages have sufficient history; subsequent updates emit (wt1, wt2, wt2 − wt1).
Error Handling
use vectorta::indicators::wavetrend::WavetrendError;

match wavetrend(&input) {
    Ok(output) => process(output),
    Err(WavetrendError::EmptyData) => eprintln!("no data"),
    Err(WavetrendError::AllValuesNaN) => eprintln!("all NaN"),
    Err(WavetrendError::InvalidChannelLen { .. }) => eprintln!("invalid channel length"),
    Err(WavetrendError::InvalidAverageLen { .. }) => eprintln!("invalid average length"),
    Err(WavetrendError::InvalidMaLen { .. }) => eprintln!("invalid ma length"),
    Err(WavetrendError::NotEnoughValidData { needed, valid }) => eprintln!("need {needed}, have {valid}"),
    Err(WavetrendError::OutputSliceLengthMismatch { .. }) => eprintln!("slice size mismatch"),
    Err(WavetrendError::EmaError(e)) => eprintln!("EMA error: {e}"),
    Err(WavetrendError::SmaError(e)) => eprintln!("SMA error: {e}"),
}

Python Bindings

Basic Usage
import numpy as np
from vectorta import wavetrend

prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5], dtype=np.float64)
wt1, wt2, wt_diff = wavetrend(prices, 9, 12, 3, 0.015, kernel='auto')
print(wt1, wt2, wt_diff)
Streaming
from vectorta import WavetrendStream

stream = WavetrendStream(9, 12, 3, 0.015)
for price in [100.0, 101.0, 100.5, 102.0]:
    res = stream.update(price)
    if res is not None:
        wt1, wt2, diff = res
        print(wt1, wt2, diff)
Batch Processing
import numpy as np
from vectorta import wavetrend_batch

prices = np.asarray([...], dtype=np.float64)
result = wavetrend_batch(
    prices,
    (6, 12, 3),   # channel_length range
    (9, 15, 3),   # average_length range
    (2, 4, 1),    # ma_length range
    (0.010, 0.020, 0.005),
    kernel='auto'
)

# result is a dict with keys: 'wt1', 'wt2', 'wt_diff',
# and parameter arrays: 'channel_lengths', 'average_lengths', 'ma_lengths', 'factors'
wt1_matrix = result['wt1']  # shape: (rows, len)
wt2_matrix = result['wt2']
diff_matrix = result['wt_diff']

JavaScript/WASM Bindings

Basic Usage

Compute WaveTrend in JS (flattened arrays):

import { wavetrend_js } from 'vectorta-wasm';

const prices = new Float64Array([/* your data */]);
const flat = wavetrend_js(prices, 9, 12, 3, 0.015);

// Result layout: [wt1..., wt2..., wt_diff...]
const n = prices.length;
const wt1 = flat.slice(0, n);
const wt2 = flat.slice(n, 2*n);
const wt_diff = flat.slice(2*n);
Memory-Efficient Operations

Use zero-copy into pre-allocated buffers:

import { wavetrend_alloc, wavetrend_free, wavetrend_into, memory } from 'vectorta-wasm';

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

const inPtr = wavetrend_alloc(n);
const wt1Ptr = wavetrend_alloc(n);
const wt2Ptr = wavetrend_alloc(n);
const diffPtr = wavetrend_alloc(n);

new Float64Array(memory.buffer, inPtr, n).set(prices);
wavetrend_into(inPtr, wt1Ptr, wt2Ptr, diffPtr, n, 9, 12, 3, 0.015);

const wt1 = new Float64Array(memory.buffer, wt1Ptr, n).slice();
const wt2 = new Float64Array(memory.buffer, wt2Ptr, n).slice();
const wt_diff = new Float64Array(memory.buffer, diffPtr, n).slice();

wavetrend_free(inPtr, n);
wavetrend_free(wt1Ptr, n);
wavetrend_free(wt2Ptr, n);
wavetrend_free(diffPtr, n);
Batch Processing

Run sweeps and split outputs per row:

import { wavetrend_batch_js } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);
const config = {
  channel_length_range: [6, 12, 3],
  average_length_range: [9, 15, 3],
  ma_length_range: [2, 4, 1],
  factor_range: [0.010, 0.020, 0.005],
};

const out = wavetrend_batch_js(prices, config);
// out contains { wt1_values, wt2_values, wt_diff_values, channel_lengths, average_lengths, ma_lengths, factors, rows, cols }
// Each array is flattened row-major: row i occupies [i*cols .. (i+1)*cols)

CUDA

CUDA bindings: Coming soon.

Performance Analysis

Comparison:
View:
Loading chart...

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

CUDA note

In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.

Related Indicators