Files
2026-02-23 17:27:35 -08:00

114 lines
4.3 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0
// https://mozilla.org/MPL/2.0/
// © QuanTAlib
//@version=6
indicator("Quadratic Regression Moving Average (QRMA)", "QRMA", overlay = true)
//@function Quadratic Regression Moving Average — fits a second-degree polynomial
// y = a + b·x + c·x² to the lookback window via ordinary least squares and
// returns the fitted value at the most recent bar (the endpoint). Captures
// both linear trends and curvature, providing better tracking of parabolic
// price movements than linear regression (LSMA). Uses x = 0..N-1 convention
// where x=0 is the oldest bar and the endpoint is evaluated at x=N-1.
//@param source Series to smooth
//@param period Lookback window (>= 3, need at least 3 points for quadratic fit)
//@returns Quadratic regression endpoint value
//@reference Savitzky, A. & Golay, M.J.E. (1964). Analytical Chemistry, 36(8), 16271639.
// (Quadratic regression is a special case of Savitzky-Golay degree=2 smoothing.)
//@optimized O(period) per bar via circular buffer; sums precomputed for normal equations
qrma(series float source, simple int period) =>
if period < 3
runtime.error("Period must be at least 3")
float price = nz(source)
// --- Circular buffer ---
var array<float> buffer = array.new_float(period, 0.0)
var int head = 0
array.set(buffer, head, price)
head := (head + 1) % period
int count = math.min(bar_index + 1, period)
if count < period
price
else
// --- Precompute constant sums for x = 0, 1, ..., N-1 ---
// These only depend on period (N) and could be precomputed once,
// but Pine Script handles them efficiently inline.
float n = period
// Σx = N(N-1)/2
float sx = n * (n - 1.0) / 2.0
// Σx² = N(N-1)(2N-1)/6
float sx2 = n * (n - 1.0) * (2.0 * n - 1.0) / 6.0
// Σx³ = [N(N-1)/2]²
float sx3 = sx * sx
// Σx⁴ = N(N-1)(2N-1)(3N²-3N-1)/30
float sx4 = n * (n - 1.0) * (2.0 * n - 1.0) * (3.0 * n * n - 3.0 * n - 1.0) / 30.0
// --- Compute data-dependent sums ---
// Buffer: head points to next-write = oldest entry
float sy = 0.0
float sxy = 0.0
float sx2y = 0.0
for j = 0 to period - 1
int idx = (head + j) % period
float val = array.get(buffer, idx)
float x = j // x=0 is oldest, x=N-1 is newest
sy += val
sxy += x * val
sx2y += x * x * val
// --- Solve 3x3 normal equations via Cramer's rule ---
// [n Σx Σx²] [a] [Σy ]
// [Σx Σx² Σx³] [b] = [Σxy ]
// [Σx² Σx³ Σx⁴] [c] [Σx²y]
float d00 = n
float d01 = sx
float d02 = sx2
float d10 = sx
float d11 = sx2
float d12 = sx3
float d20 = sx2
float d21 = sx3
float d22 = sx4
// Determinant of coefficient matrix
float det = d00 * (d11 * d22 - d12 * d21) -
d01 * (d10 * d22 - d12 * d20) +
d02 * (d10 * d21 - d11 * d20)
if math.abs(det) < 1e-20
price
else
// Solve for a, b, c using Cramer's rule
float det_a = sy * (d11 * d22 - d12 * d21) -
d01 * (sxy * d22 - sx2y * d21) +
d02 * (sxy * d21 - sx2y * d11)
float det_b = d00 * (sxy * d22 - sx2y * d21) -
sy * (d10 * d22 - d12 * d20) +
d02 * (d10 * sx2y - sxy * d20)
float det_c = d00 * (d11 * sx2y - sxy * d21) -
d01 * (d10 * sx2y - sxy * d20) +
sy * (d10 * d21 - d11 * d20)
float a = det_a / det
float b = det_b / det
float c = det_c / det
// Evaluate at x = N-1 (newest bar = endpoint)
float x_end = n - 1.0
a + b * x_end + c * x_end * x_end
// ── Inputs ──
int p_period = input.int(14, "Period", minval = 3)
float p_src = input.source(close, "Source")
// ── Calculation ──
float out = qrma(p_src, p_period)
// ── Plot ──
plot(out, "QRMA", color.yellow, 2)