WaveTrend Oscillator (WTO)

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

Overview

The WaveTrend Oscillator evolved from AIQ Systems' Trading Channel Index created in 1986, which itself adapted Donald Lambert's Commodity Channel Index, before gaining widespread recognition when LazyBear ported it to TradingView in 2014 where it quickly became one of the platform's most popular momentum indicators. WTO transforms price through multiple layers of exponential smoothing and normalization, first calculating an EMA-based price channel (ESA), then measuring absolute deviations (DE) to normalize the distance between price and ESA by a scaling factor, creating a channel index that captures momentum without the noise of raw price movements. The resulting channel index undergoes additional EMA smoothing to generate the primary WT1 line, followed by a simple moving average to produce the WT2 signal line, with their difference creating a histogram that visualizes momentum acceleration and deceleration. Traders value WTO for its ability to identify genuine reversals while remaining quiet during choppy consolidations, as the layered smoothing filters out false signals that plague simpler oscillators. The indicator excels at spotting divergences between price and momentum, particularly when WT1 crosses WT2 in extreme zones above 60 or below -60, signaling potential turning points with remarkable accuracy. Its non-repainting nature and sophisticated mathematical foundation have made WaveTrend a cornerstone indicator for traders seeking reliable momentum signals across all timeframes and markets.

Implementation Examples

Compute WT1, WT2, and their difference from a price slice or candles:

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

// From price slice
let prices = vec![100.0, 101.5, 102.2, 101.0, 103.3];
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)?;
println!("WT1[0..]: {:?}", &out.wt1[..]);
println!("WT2[0..]: {:?}", &out.wt2[..]);
println!("Diff[0..]: {:?}", &out.wt_diff[..]);

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

API Reference

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

// From candles with custom source
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 WaveTrend line
    pub wt2: Vec<f64>,     // Signal (SMA of WT1)
    pub wt_diff: Vec<f64>, // WT2 - WT1
}
Validation, Warmup & NaNs
  • channel_length > 0, average_length > 0, ma_length > 0; else specific WavetrendError::Invalid*.
  • Requires at least max(channel, average, ma) valid points after the first finite value; else WavetrendError::NotEnoughValidData.
  • Outputs are NaN until warmup completes (roughly first_non_nan + channel − 1 + average − 1 + ma − 1).
  • Streaming update returns None until ESA/DE/WT1/WT2 are all seeded; non‑finite inputs yield None.
Error Handling
use vectorta::indicators::wavetrend::{wavetrend, WavetrendError};

match wavetrend(&input) {
    Ok(out) => use_outputs(out.wt1, out.wt2, out.wt_diff),
    Err(WavetrendError::EmptyData) => eprintln!("No input data"),
    Err(WavetrendError::AllValuesNaN) => eprintln!("All values are NaN"),
    Err(WavetrendError::InvalidChannelLen { channel_length, data_len }) =>
        eprintln!("Invalid channel_length {} for data length {}", channel_length, data_len),
    Err(WavetrendError::InvalidAverageLen { average_length, data_len }) =>
        eprintln!("Invalid average_length {} for data length {}", average_length, data_len),
    Err(WavetrendError::InvalidMaLen { ma_length, data_len }) =>
        eprintln!("Invalid ma_length {} for data length {}", ma_length, data_len),
    Err(WavetrendError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, only {}", needed, valid),
    Err(WavetrendError::OutputSliceLengthMismatch { expected, got }) =>
        eprintln!("Output slice size mismatch: expected {}, got {}", expected, got),
    Err(e) => eprintln!("WTO error: {}", e),
}

Python Bindings

Single-Series Computation
from vectorta import wavetrend
import numpy as np

prices = np.array([100.0, 101.5, 102.2, 101.0, 103.3], dtype=np.float64)
wt1, wt2, wt_diff = wavetrend(prices, 9, 12, 3, 0.015)
print(wt1.shape, wt2.shape, wt_diff.shape)
Streaming
from vectorta import WavetrendStream

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

prices = np.array([/* your data */], dtype=np.float64)
results = wavetrend_batch(
    prices,
    (6, 12, 3),    # channel_length: 6, 9, 12
    (9, 15, 3),    # average_length: 9, 12, 15
    (2, 4, 1),     # ma_length: 2, 3, 4
    (0.010, 0.020, 0.005)  # factor sweep
)

# Results dict contains flat arrays and metadata
print(results['wt1'].shape, results['wt2'].shape, results['wt_diff'].shape)
print(results['channel_lengths'])
CUDA Acceleration

CUDA device batch APIs are available via Python when built with CUDA support:

from vectorta import wavetrend_cuda_batch_dev
import numpy as np

# Device batch: one series, many parameters on GPU
prices_f32 = np.array([/* your data */], dtype=np.float32)
res = wavetrend_cuda_batch_dev(
    prices_f32,
    (6, 12, 3),
    (9, 15, 3),
    (2, 4, 1),
    (0.010, 0.020, 0.005),
    device_id=0
)
print(res.keys())  # 'wt1', 'wt2', 'wt_diff', 'channel_lengths', ...

JavaScript/WASM Bindings

Single-Series (Flattened Output)
import { wavetrend_js } from 'vectorta-wasm';

const prices = new Float64Array([/* data */]);
const out = wavetrend_js(prices, 9, 12, 3, 0.015);
// out is [wt1..., wt2..., wt_diff...] length = prices.length * 3
const len = prices.length;
const wt1 = out.slice(0, len);
const wt2 = out.slice(len, 2*len);
const diff = out.slice(2*len);
Memory-Efficient Operations

Allocate and compute directly in WASM memory:

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

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

// Allocate memory for input and three outputs
const inPtr = wavetrend_alloc(n);
const wt1Ptr = wavetrend_alloc(n);
const wt2Ptr = wavetrend_alloc(n);
const diffPtr = wavetrend_alloc(n);

// Copy input into WASM memory
new Float64Array(memory.buffer, inPtr, n).set(prices);

// Compute directly into output buffers
wavetrend_into(inPtr, wt1Ptr, wt2Ptr, diffPtr, n, 9, 12, 3, 0.015);

// Read back results
const wt1 = new Float64Array(memory.buffer, wt1Ptr, n).slice();
const wt2 = new Float64Array(memory.buffer, wt2Ptr, n).slice();
const diff = new Float64Array(memory.buffer, diffPtr, n).slice();

// Free memory
wavetrend_free(inPtr, n);
wavetrend_free(wt1Ptr, n);
wavetrend_free(wt2Ptr, n);
wavetrend_free(diffPtr, n);
Batch Processing
import { wavetrend_batch_js } from 'vectorta-wasm';

const prices = new Float64Array([/* data */]);
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 result = wavetrend_batch_js(prices, config);
// result: { wt1_values, wt2_values, wt_diff_values, channel_lengths, average_lengths, ma_lengths, factors, rows, cols }

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