// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Savitzky-Golay Filter (SGF)", "SGF", overlay=true)
//@function Applies Savitzky-Golay filter to input series
//@param src Input series to filter
//@param window_size Window size (rounded down to nearest odd number)
//@param poly_order Polynomial order (2 or 4 recommended)
//@returns Filtered series
//@optimized Uses polynomial convolution with O(n) complexity per bar
sgf(series float src, simple int window_size, simple int poly_order) =>
int adj_size = math.max(1, window_size % 2 == 0 ? window_size - 1 : window_size)
if poly_order >= adj_size
runtime.error("Polynomial order must be less than window size")
var array<float> coeffs = array.new_float(0)
array.clear(coeffs)
int half_window = int(adj_size / 2)
float sum_denom = 0.0
if poly_order == 2
for i = -half_window to half_window
float weight = 3.0 * (3.0 * adj_size * adj_size - 7.0 - 20.0 * i * i)
array.push(coeffs, weight)
sum_denom += weight
else if poly_order == 4
for i = -half_window to half_window
float i2 = i * i
float weight = 15.0 + i2 * (-20.0 + i2 * 6.0)
array.push(coeffs, weight)
sum_denom += weight
else
for i = -half_window to half_window
float weight = 1.0 - math.abs(float(i)) / float(half_window)
array.push(coeffs, weight)
sum_denom += weight
if sum_denom != 0.0
for i = 0 to adj_size - 1
array.set(coeffs, i, array.get(coeffs, i) / sum_denom)
float sum = 0.0
float weight_sum = 0.0
for i = 0 to adj_size - 1
float price = src[i]
if not na(price)
float w = array.get(coeffs, i)
sum += price * w
weight_sum += w
nz(sum / weight_sum, src)
// ---------- Main loop ----------
// Inputs
i_window = input.int(21, "Window Size", minval=1, step=2)
i_order = input.int(2, "Polynomial Order", minval=2, maxval=4, step=2)
i_source = input.source(close, "Source")
// Calculation
sgf_val = sgf(i_source, i_window, i_order)
// Plot
plot(sgf_val, "SGF", color=color.yellow, linewidth=2)