Nadaraya-Watson Envelope (NWE)

Parameters: bandwidth = 8 | multiplier = 3 | lookback = 500

Overview

The Nadaraya Watson Envelope employs kernel regression with Gaussian weighting to create a smooth trend estimate surrounded by adaptive bands that expand and contract based on local price volatility. This non parametric approach assigns exponentially decreasing weights to historical data points based on their distance from the current position, with the bandwidth parameter controlling how quickly this influence decays. The indicator calculates upper and lower bands by measuring the mean absolute error over a rolling window, then multiplying by a user defined factor to create an envelope that contains most price action. Unlike traditional moving average envelopes, NWE adapts its band width to market conditions, tightening during stable periods and widening when volatility increases. Traders leverage this adaptive behavior to identify trend direction while the dynamic bands serve as support and resistance levels, with price breakouts beyond the envelope often signaling significant moves or potential reversals.

Implementation Examples

Compute NWE for prices or candles:

use vectorta::indicators::nadaraya_watson_envelope::{nadaraya_watson_envelope, NweInput, NweParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 102.0, 101.5, 103.0, 105.0, 104.5];
let params = NweParams { bandwidth: Some(8.0), multiplier: Some(3.0), lookback: Some(500) };
let input = NweInput::from_slice(&prices, params);
let out = nadaraya_watson_envelope(&input)?; // NweOutput { upper, lower }

// From candles with defaults (source = "close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = NweInput::with_default_candles(&candles);
let out = nadaraya_watson_envelope(&input)?;

// Access envelope values
for (u, l) in out.upper.iter().zip(out.lower.iter()) {
    println!("upper: {u}, lower: {l}");
}

API Reference

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

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

// From candles with defaults (close, h=8.0, m=3.0, L=500)
NweInput::with_default_candles(&Candles) -> NweInput
Parameters Structure
pub struct NweParams {
    pub bandwidth: Option<f64>, // Default: 8.0
    pub multiplier: Option<f64>, // Default: 3.0
    pub lookback: Option<usize>, // Default: 500
}
Output Structure
pub struct NweOutput {
    pub upper: Vec<f64>, // Upper envelope (y + m * MAE)
    pub lower: Vec<f64>, // Lower envelope (y - m * MAE)
}
Validation, Warmup & NaNs
  • bandwidth > 0 and finite; multiplier ≥ 0 and finite; lookback > 0.
  • Data must contain at least lookback valid points after the first finite value, else NweError::NotEnoughValidData.
  • Warmup: regression warmup ends at warm_out = first_valid + lookback - 1; envelope starts after an additional 499-sample MAE window (warm_total = warm_out + 498).
  • Indices before warm_total are NaN; NaN inputs propagate within the windows.
  • Streaming: NweStream::update returns None until both the regression window and MAE window are filled and free of NaNs.
Error Handling
use vectorta::indicators::nadaraya_watson_envelope::NweError;

match nadaraya_watson_envelope(&input) {
    Ok(output) => handle(output.upper, output.lower),
    Err(NweError::EmptyInputData) => eprintln!("Input data is empty"),
    Err(NweError::AllValuesNaN) => eprintln!("All input values are NaN"),
    Err(NweError::InvalidBandwidth { bandwidth }) => eprintln!("Invalid bandwidth: {}", bandwidth),
    Err(NweError::InvalidMultiplier { multiplier }) => eprintln!("Invalid multiplier: {}", multiplier),
    Err(NweError::InvalidLookback { lookback }) => eprintln!("Invalid lookback: {}", lookback),
    Err(NweError::NotEnoughValidData { needed, valid }) => eprintln!("Need {} valid points, only {}", needed, valid),
    Err(NweError::InvalidPeriod { .. }) => eprintln!("Invalid batch/kernel configuration"),
}

Python Bindings

Basic Usage

Calculate upper/lower envelopes using NumPy arrays (defaults: h=8.0, m=3.0, L=500):

import numpy as np
from vectorta import nadaraya_watson_envelope

prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5])

# Defaults
upper, lower = nadaraya_watson_envelope(prices)

# Custom parameters; optional kernel string is accepted but not used by NWE
upper, lower = nadaraya_watson_envelope(prices, bandwidth=8.0, multiplier=3.0, lookback=500, kernel=None)

print("Upper:", upper)
print("Lower:", lower)
Streaming Real-time Updates

Use the NweStream class for O(1) updates:

from vectorta import NweStream

stream = NweStream(bandwidth=8.0, multiplier=3.0, lookback=500)

for price in price_feed:
    result = stream.update(price)  # returns (upper, lower) or None during warmup
    if result is not None:
        upper, lower = result
        print(upper, lower)
Batch Parameter Optimization

Test multiple combinations and access results as 2D arrays:

import numpy as np
from vectorta import nadaraya_watson_envelope_batch

prices = np.array([...], dtype=np.float64)

out = nadaraya_watson_envelope_batch(
    prices,
    bandwidth_range=(4.0, 10.0, 2.0),
    multiplier_range=(2.0, 4.0, 1.0),
    lookback_range=(300, 600, 100),
    kernel="auto"
)

upper = out["upper"]   # shape: [rows, len(prices)]
lower = out["lower"]   # shape: [rows, len(prices)]
bandwidths = out["bandwidths"]
multipliers = out["multipliers"]
lookbacks = out["lookbacks"]
CUDA Acceleration

CUDA support for NWE is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

JavaScript/WASM Bindings

Basic Usage

Compute NWE and split flattened output into upper/lower:

import { nadaraya_watson_envelope_js } from 'vectorta-wasm';

const prices = new Float64Array([100, 102, 101.5, 103, 105, 104.5]);
const flat = nadaraya_watson_envelope_js(prices, 8.0, 3.0, 500);

const n = prices.length;
const upper = flat.slice(0, n);
const lower = flat.slice(n);
console.log('upper:', upper);
console.log('lower:', lower);
Memory-Efficient Operations

Use zero-copy into_flat and a single output buffer sized 2 × len:

import { nadaraya_watson_envelope_alloc, nadaraya_watson_envelope_free, nadaraya_watson_envelope_into_flat, memory } from 'vectorta-wasm';

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

// Allocate a single output buffer for [upper..., lower...]
const inPtr = nadaraya_watson_envelope_alloc(n);
const outPtr = nadaraya_watson_envelope_alloc(n); // capacity accounts for 2*n values

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

// Compute directly into the output buffer
nadaraya_watson_envelope_into_flat(inPtr, outPtr, n, 8.0, 3.0, 500);

// Read results and split
const flat = new Float64Array(memory.buffer, outPtr, 2 * n).slice();
const upper = flat.slice(0, n);
const lower = flat.slice(n);

// Free buffers
nadaraya_watson_envelope_free(inPtr, n);
nadaraya_watson_envelope_free(outPtr, n);
Batch Processing

Run multiple parameter combinations and reshape results:

import { nadaraya_watson_envelope_batch } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);

const bw = new Float64Array([4.0, 10.0, 2.0]);   // [start, end, step]
const mult = new Float64Array([2.0, 4.0, 1.0]);  // [start, end, step]
const lb = new Uint32Array([300, 600, 100]);     // [start, end, step]

const out = nadaraya_watson_envelope_batch(prices, bw, mult, lb);

// out.upper and out.lower are flattened row-major arrays (rows x len)
// out.bandwidths / out.multipliers / out.lookbacks list parameter combos per row
console.log(out.rows, out.cols);
console.log(out.bandwidths, out.multipliers, out.lookbacks);

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators