WaveTrend Indicator
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; elseWavetrendError::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
hlc3unless you specify the source string. - Streaming returns
Noneuntil 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
Performance Analysis
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.