Files

278 lines
7.3 KiB
Plaintext

// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Cubic Regression Moving Average (CRMA)", "CRMA", overlay=true)
//@function Computes Cubic Regression Moving Average — fits a degree-3 polynomial
// y = a0 + a1*x + a2*x² + a3*x³ to the most recent `period` bars via
// normal equations solved by closed-form elimination, returns the fitted endpoint.
//@param source Series to analyze
//@param period Lookback window for the cubic regression
//@returns Fitted value at the most recent bar (x = 0)
//@reference Polynomial least-squares regression (degree 3), evaluated at endpoint
//@optimized O(period) per bar for accumulating sums; O(1) for solve
crma(series float source, simple int period) =>
if period < 4
runtime.error("Period must be at least 4 for cubic regression")
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
int p = math.min(bar_index + 1, period)
if p < 4
price
else
// --- Accumulate power sums and cross-products ---
float s0 = 0.0
float s1 = 0.0
float s2 = 0.0
float s3 = 0.0
float s4 = 0.0
float s5 = 0.0
float s6 = 0.0
float r0 = 0.0
float r1 = 0.0
float r2 = 0.0
float r3 = 0.0
int idx = (head - 1 + period) % period
for i = 0 to p - 1
float val = array.get(buffer, idx)
float v = na(val) ? price : val
float x = float(i)
float x2 = x * x
float x3 = x2 * x
s0 += 1.0
s1 += x
s2 += x2
s3 += x3
s4 += x2 * x2
s5 += x2 * x3
s6 += x3 * x3
r0 += v
r1 += x * v
r2 += x2 * v
r3 += x3 * v
idx := (idx - 1 + period) % period
// --- Solve 4x4 via scalar Gaussian elimination with partial pivoting ---
// Row 0
float a00 = s0
float a01 = s1
float a02 = s2
float a03 = s3
float b0 = r0
// Row 1
float a10 = s1
float a11 = s2
float a12 = s3
float a13 = s4
float b1 = r1
// Row 2
float a20 = s2
float a21 = s3
float a22 = s4
float a23 = s5
float b2 = r2
// Row 3
float a30 = s3
float a31 = s4
float a32 = s5
float a33 = s6
float b3 = r3
// --- Column 0: partial pivoting among rows 0-3 ---
float best0 = math.abs(a00)
int piv0 = 0
if math.abs(a10) > best0
best0 := math.abs(a10)
piv0 := 1
if math.abs(a20) > best0
best0 := math.abs(a20)
piv0 := 2
if math.abs(a30) > best0
best0 := math.abs(a30)
piv0 := 3
// Swap pivot row with row 0
if piv0 == 1
float t0 = a00
float t1 = a01
float t2 = a02
float t3 = a03
float t4 = b0
a00 := a10
a01 := a11
a02 := a12
a03 := a13
b0 := b1
a10 := t0
a11 := t1
a12 := t2
a13 := t3
b1 := t4
else if piv0 == 2
float t0 = a00
float t1 = a01
float t2 = a02
float t3 = a03
float t4 = b0
a00 := a20
a01 := a21
a02 := a22
a03 := a23
b0 := b2
a20 := t0
a21 := t1
a22 := t2
a23 := t3
b2 := t4
else if piv0 == 3
float t0 = a00
float t1 = a01
float t2 = a02
float t3 = a03
float t4 = b0
a00 := a30
a01 := a31
a02 := a32
a03 := a33
b0 := b3
a30 := t0
a31 := t1
a32 := t2
a33 := t3
b3 := t4
bool singular = best0 < 1e-12
if not singular
// Eliminate column 0 from rows 1,2,3
float f1 = a10 / a00
a11 -= f1 * a01
a12 -= f1 * a02
a13 -= f1 * a03
b1 -= f1 * b0
a10 := 0.0
float f2 = a20 / a00
a21 -= f2 * a01
a22 -= f2 * a02
a23 -= f2 * a03
b2 -= f2 * b0
a20 := 0.0
float f3 = a30 / a00
a31 -= f3 * a01
a32 -= f3 * a02
a33 -= f3 * a03
b3 -= f3 * b0
a30 := 0.0
// --- Column 1: partial pivoting among rows 1-3 ---
float best1 = math.abs(a11)
int piv1 = 1
if math.abs(a21) > best1
best1 := math.abs(a21)
piv1 := 2
if math.abs(a31) > best1
best1 := math.abs(a31)
piv1 := 3
if piv1 == 2
float t1 = a11
float t2 = a12
float t3 = a13
float t4 = b1
a11 := a21
a12 := a22
a13 := a23
b1 := b2
a21 := t1
a22 := t2
a23 := t3
b2 := t4
else if piv1 == 3
float t1 = a11
float t2 = a12
float t3 = a13
float t4 = b1
a11 := a31
a12 := a32
a13 := a33
b1 := b3
a31 := t1
a32 := t2
a33 := t3
b3 := t4
singular := best1 < 1e-12
if not singular
// Eliminate column 1 from rows 2,3
float g2 = a21 / a11
a22 -= g2 * a12
a23 -= g2 * a13
b2 -= g2 * b1
a21 := 0.0
float g3 = a31 / a11
a32 -= g3 * a12
a33 -= g3 * a13
b3 -= g3 * b1
a31 := 0.0
// --- Column 2: partial pivoting among rows 2-3 ---
if math.abs(a32) > math.abs(a22)
float t2 = a22
float t3 = a23
float t4 = b2
a22 := a32
a23 := a33
b2 := b3
a32 := t2
a33 := t3
b3 := t4
singular := math.abs(a22) < 1e-12
if not singular
// Eliminate column 2 from row 3
float h3 = a32 / a22
a33 -= h3 * a23
b3 -= h3 * b2
a32 := 0.0
singular := math.abs(a33) < 1e-12
float result = price
if not singular
// Back-substitution
float c3 = b3 / a33
float c2 = (b2 - a23 * c3) / a22
float c1 = (b1 - a12 * c2 - a13 * c3) / a11
float c0 = (b0 - a01 * c1 - a02 * c2 - a03 * c3) / a00
result := c0
result
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=4)
i_source = input.source(close, "Source")
// Calculation
crma_value = crma(i_source, i_period)
// Plot
plot(crma_value, "CRMA", color=color.yellow, linewidth=2)