feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
// Licensed under the Apache License, Version 2.0
|
2026-02-18 19:08:15 -08:00
|
|
|
|
// © mihakralj
|
|
|
|
|
|
//@version=6
|
|
|
|
|
|
// Indicator algorithm (C) 2013 John F. Ehlers
|
fix(docs): correct .md documentation across errors, dynamics, filters, forecasts, momentum, numerics, oscillators, reversals, statistics, trends, volatility, volume
2026-03-10 18:38:23 -07:00
|
|
|
|
indicator("Ehlers Trendflex (TRENDFLEX)", "TRENDFLEX", overlay=false)
|
2026-02-18 19:08:15 -08:00
|
|
|
|
|
|
|
|
|
|
//@function Calculates Ehlers Trendflex using SuperSmoother pre-filtering and cumulative slope with RMS normalization
|
|
|
|
|
|
//@param source Series to calculate Trendflex from
|
|
|
|
|
|
//@param period Lookback period for trend measurement (>= 1)
|
|
|
|
|
|
//@returns Normalized Trendflex value centered around zero
|
|
|
|
|
|
//@optimized Uses O(1) running sum for cumulative slope instead of O(N) loop, with RMS normalization
|
|
|
|
|
|
trendflex(series float source, simple int period) =>
|
|
|
|
|
|
if period <= 0
|
|
|
|
|
|
runtime.error("Period must be positive")
|
|
|
|
|
|
|
|
|
|
|
|
float src = nz(source)
|
|
|
|
|
|
|
|
|
|
|
|
// SuperSmoother (2-pole Butterworth lowpass) coefficients
|
|
|
|
|
|
float halfPeriod = period * 0.5
|
|
|
|
|
|
float a1 = math.exp(-1.414 * math.pi / halfPeriod)
|
|
|
|
|
|
float b1 = 2.0 * a1 * math.cos(1.414 * math.pi / halfPeriod)
|
|
|
|
|
|
float c2 = b1
|
|
|
|
|
|
float c3 = -(a1 * a1)
|
|
|
|
|
|
float c1 = 1.0 - c2 - c3
|
|
|
|
|
|
|
|
|
|
|
|
// SuperSmoother filter state
|
|
|
|
|
|
var float filt = 0.0
|
|
|
|
|
|
var float filt1 = 0.0
|
|
|
|
|
|
float new_filt = bar_index < 2 ? src : c1 * (src + nz(src[1])) * 0.5 + c2 * filt + c3 * filt1
|
|
|
|
|
|
filt1 := filt
|
|
|
|
|
|
filt := new_filt
|
|
|
|
|
|
|
|
|
|
|
|
// O(1) cumulative slope via circular buffer and running sum
|
|
|
|
|
|
// Sum = Σ(Filt - Filt[i]) for i=1..N = N × Filt - Σ(Filt[i])
|
|
|
|
|
|
var array<float> buf = array.new_float(period, 0.0)
|
|
|
|
|
|
var int head = 0
|
|
|
|
|
|
var float running_sum = 0.0
|
|
|
|
|
|
var int count = 0
|
|
|
|
|
|
|
|
|
|
|
|
int n = math.min(count, period)
|
|
|
|
|
|
float slope_sum = n > 0 ? (n * new_filt - running_sum) / period : 0.0
|
|
|
|
|
|
|
|
|
|
|
|
float oldest = array.get(buf, head)
|
|
|
|
|
|
running_sum -= oldest
|
|
|
|
|
|
running_sum += new_filt
|
|
|
|
|
|
array.set(buf, head, new_filt)
|
|
|
|
|
|
head := (head + 1) % period
|
|
|
|
|
|
if count < period
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
|
|
// RMS normalization via exponential mean-square
|
|
|
|
|
|
var float ms = 0.0
|
|
|
|
|
|
ms := 0.04 * slope_sum * slope_sum + 0.96 * ms
|
|
|
|
|
|
|
|
|
|
|
|
float result = ms > 0 ? slope_sum / math.sqrt(ms) : 0.0
|
|
|
|
|
|
na(source) ? na : result
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Main loop ----------
|
|
|
|
|
|
|
|
|
|
|
|
// Inputs
|
|
|
|
|
|
i_period = input.int(20, "Period", minval=1, tooltip="Lookback period for trend measurement")
|
|
|
|
|
|
i_source = input.source(close, "Source")
|
|
|
|
|
|
|
|
|
|
|
|
// Calculation
|
|
|
|
|
|
trendflex_value = trendflex(i_source, i_period)
|
|
|
|
|
|
|
|
|
|
|
|
// Plot
|
|
|
|
|
|
plot(trendflex_value, "TRENDFLEX", color=color.yellow, linewidth=2)
|
|
|
|
|
|
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)
|