Files

84 lines
3.7 KiB
Plaintext

// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kaiser Window Moving Average (KAISER)", "KAISER", overlay=true)
//@function I0 approximation (modified Bessel, first kind, order 0) via 25-term power series.
bessel_i0(float x) =>
float sum = 1.0
float term = 1.0
float half_x = x / 2.0
for m = 1 to 25
term *= (half_x / m)
sum += term * term
sum
//@function Computes Kaiser Window Moving Average — a symmetric FIR filter using the
// Kaiser-Bessel window function for optimal sidelobe attenuation. The beta
// parameter controls the trade-off between main lobe width and sidelobe level.
//@param source Series to smooth
//@param period Lookback window (>= 2)
//@param beta Kaiser shape parameter controlling sidelobe attenuation (default 3.0).
// Higher beta = smoother (more attenuation) but wider transition band.
// beta=0 reduces to rectangular (SMA), beta~5.65 approximates Blackman.
//@returns Kaiser-weighted moving average
//@reference Kaiser, J.F. & Schafer, R.W. (1980). "On the Use of the I0-Sinh Window
// for Spectrum Analysis." IEEE Trans. Acoust., Speech, Signal Process.
//@optimized O(period) per bar for convolution; weights precomputed once via I0 series
kaiser(series float source, simple int period, simple float beta = 3.0) =>
if period < 2
runtime.error("Period must be at least 2")
float price = nz(source)
// --- Circular buffer for rolling window ---
var array<float> buffer = array.new_float(period, na)
var int head = 0
array.set(buffer, head, price)
head := (head + 1) % period
// --- Precompute Kaiser window weights once ---
// Kaiser window: w(k) = I0(beta * sqrt(1 - ((2k/(N-1)) - 1)^2)) / I0(beta)
var array<float> weights = array.new_float(0)
if barstate.isfirst
float i0_beta = bessel_i0(beta)
float N = period - 1
float wsum = 0.0
for k = 0 to period - 1
float t = N > 0 ? (2.0 * k / N) - 1.0 : 0.0
float arg_sq = 1.0 - t * t
// Clamp to avoid sqrt of negative due to floating-point
float arg = arg_sq > 0 ? math.sqrt(arg_sq) : 0.0
float w = i0_beta > 0 ? bessel_i0(beta * arg) / i0_beta : 1.0
array.push(weights, w)
wsum += w
// Normalize weights to sum exactly 1.0
if wsum > 0
for j = 0 to period - 1
array.set(weights, j, array.get(weights, j) / wsum)
int count = math.min(bar_index + 1, period)
if count < period
price
else
// --- Apply Kaiser window convolution via circular buffer ---
// Buffer: head points to next-write = oldest entry
// Weight[0] = oldest bar, Weight[period-1] = newest
float result = 0.0
for j = 0 to period - 1
int idx = (head + j) % period
float val = nz(array.get(buffer, idx))
result += val * array.get(weights, j)
result
// ── Inputs ──────────────────────────────────────────────────────────────
src = input.source(close, "Source")
per = input.int(14, "Period", minval=2)
bet = input.float(3.0, "Beta (shape)", minval=0, maxval=20, step=0.1,
tooltip="0=rectangular(SMA), 3=good general, 5.65≈Blackman, 8.6=Hamming-like")
// ── Plot ────────────────────────────────────────────────────────────────
plot(kaiser(src, per, bet), "KAISER", color.new(color.yellow, 0), 2)