// The MIT License (MIT) // © 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 with Gaussian 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 4×4 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 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 --- // Normal equations for degree-3 polynomial: M * a = rhs // M[i][j] = Σ x^(i+j), rhs[i] = Σ x^i * y for i,j = 0..3 // x = 0 (newest) to p-1 (oldest), so a0 = fitted value at newest bar float s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0 float s4 = 0.0, s5 = 0.0, s6 = 0.0 float r0 = 0.0, r1 = 0.0, r2 = 0.0, 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 // Σ x^0 = count s1 += x // Σ x^1 s2 += x2 // Σ x^2 s3 += x3 // Σ x^3 s4 += x2 * x2 // Σ x^4 s5 += x2 * x3 // Σ x^5 s6 += x3 * x3 // Σ x^6 r0 += v // Σ y r1 += x * v // Σ x*y r2 += x2 * v // Σ x²*y r3 += x3 * v // Σ x³*y idx := (idx - 1 + period) % period // --- Build 4×4 augmented matrix (row-major, 4 rows × 5 cols) --- var matrix m = matrix.new(4, 5, 0.0) // Row 0: [s0, s1, s2, s3 | r0] matrix.set(m, 0, 0, s0), matrix.set(m, 0, 1, s1), matrix.set(m, 0, 2, s2), matrix.set(m, 0, 3, s3), matrix.set(m, 0, 4, r0) // Row 1: [s1, s2, s3, s4 | r1] matrix.set(m, 1, 0, s1), matrix.set(m, 1, 1, s2), matrix.set(m, 1, 2, s3), matrix.set(m, 1, 3, s4), matrix.set(m, 1, 4, r1) // Row 2: [s2, s3, s4, s5 | r2] matrix.set(m, 2, 0, s2), matrix.set(m, 2, 1, s3), matrix.set(m, 2, 2, s4), matrix.set(m, 2, 3, s5), matrix.set(m, 2, 4, r2) // Row 3: [s3, s4, s5, s6 | r3] matrix.set(m, 3, 0, s3), matrix.set(m, 3, 1, s4), matrix.set(m, 3, 2, s5), matrix.set(m, 3, 3, s6), matrix.set(m, 3, 4, r3) // --- Gaussian elimination with partial pivoting --- bool singular = false for col = 0 to 3 // Find pivot row int pivot_row = col float pivot_max = math.abs(matrix.get(m, col, col)) for row = col + 1 to 3 float absval = math.abs(matrix.get(m, row, col)) if absval > pivot_max pivot_max := absval pivot_row := row if pivot_max < 1e-12 singular := true break // Swap rows if needed if pivot_row != col for k = col to 4 float tmp = matrix.get(m, col, k) matrix.set(m, col, k, matrix.get(m, pivot_row, k)) matrix.set(m, pivot_row, k, tmp) // Eliminate below float diag = matrix.get(m, col, col) for row = col + 1 to 3 float factor = matrix.get(m, row, col) / diag for k = col to 4 matrix.set(m, row, k, matrix.get(m, row, k) - factor * matrix.get(m, col, k)) float result = price if not singular // Back-substitution var array a = array.new_float(4, 0.0) for row = 3 to 0 float val = matrix.get(m, row, 4) for k = row + 1 to 3 val -= matrix.get(m, row, k) * array.get(a, k) array.set(a, row, val / matrix.get(m, row, row)) // a[0] is the fitted value at x = 0 (most recent bar) result := array.get(a, 0) 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)