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-20 21:40:32 -08:00
|
|
|
|
// © mihakralj
|
|
|
|
|
|
//@version=6
|
|
|
|
|
|
indicator("Normal Distribution CDF (NORMDIST)", "NORMDIST", overlay=false, precision=6)
|
|
|
|
|
|
|
|
|
|
|
|
//@function Computes Normal Distribution CDF for a normalized price series
|
|
|
|
|
|
//@param source Series to transform
|
|
|
|
|
|
//@param period Lookback period for z-score normalization
|
|
|
|
|
|
//@param mu Mean parameter (0.0 for standard normal after z-score)
|
|
|
|
|
|
//@param sigma Standard deviation parameter (1.0 for standard normal after z-score)
|
|
|
|
|
|
//@returns CDF value in [0,1]: Φ(z) = 0.5 × (1 + erf(z / √2))
|
|
|
|
|
|
//@optimized O(period) per bar for mean/variance scan; CDF itself is O(1)
|
|
|
|
|
|
normdist(series float source, simple int period, simple float mu, simple float sigma) =>
|
|
|
|
|
|
if period <= 0
|
|
|
|
|
|
runtime.error("Period must be greater than 0")
|
|
|
|
|
|
if sigma <= 0.0
|
|
|
|
|
|
runtime.error("Sigma must be greater than 0")
|
|
|
|
|
|
|
|
|
|
|
|
// Compute rolling mean and standard deviation over lookback
|
|
|
|
|
|
float sum = 0.0
|
|
|
|
|
|
float sumSq = 0.0
|
|
|
|
|
|
int count = 0
|
|
|
|
|
|
for i = 0 to period - 1
|
|
|
|
|
|
float v = source[i]
|
|
|
|
|
|
if not na(v)
|
|
|
|
|
|
sum += v
|
|
|
|
|
|
sumSq += v * v
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
|
|
float result = 0.5
|
|
|
|
|
|
if count >= 2
|
|
|
|
|
|
float mean = sum / count
|
|
|
|
|
|
float variance = (sumSq / count) - (mean * mean)
|
|
|
|
|
|
float stddev = variance > 0.0 ? math.sqrt(variance) : 0.0
|
|
|
|
|
|
|
|
|
|
|
|
// Z-score: normalize source relative to its own rolling distribution
|
|
|
|
|
|
float z = stddev > 0.0 ? (source - mean) / stddev : 0.0
|
|
|
|
|
|
|
|
|
|
|
|
// Apply user-specified mu/sigma shift: z_final = (z - mu) / sigma
|
|
|
|
|
|
float z_final = (z - mu) / sigma
|
|
|
|
|
|
|
|
|
|
|
|
// Approximate erf via Abramowitz & Stegun (max error < 1.5e-7)
|
|
|
|
|
|
// erf(x) = 1 - (a1*t + a2*t^2 + a3*t^3) * exp(-x^2)
|
|
|
|
|
|
// where t = 1 / (1 + 0.47047 * |x|)
|
|
|
|
|
|
float x = z_final / math.sqrt(2.0)
|
|
|
|
|
|
float ax = math.abs(x)
|
|
|
|
|
|
float t = 1.0 / (1.0 + 0.47047 * ax)
|
|
|
|
|
|
float t2 = t * t
|
|
|
|
|
|
float t3 = t2 * t
|
|
|
|
|
|
|
|
|
|
|
|
float a1 = 0.3480242
|
|
|
|
|
|
float a2 = -0.0958798
|
|
|
|
|
|
float a3 = 0.7478556
|
|
|
|
|
|
|
|
|
|
|
|
float erfApprox = 1.0 - (a1 * t + a2 * t2 + a3 * t3) * math.exp(-(ax * ax))
|
|
|
|
|
|
float erf = x >= 0.0 ? erfApprox : -erfApprox
|
|
|
|
|
|
|
|
|
|
|
|
// CDF: Φ(z) = 0.5 * (1 + erf(z / sqrt(2)))
|
|
|
|
|
|
result := 0.5 * (1.0 + erf)
|
|
|
|
|
|
|
|
|
|
|
|
result
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Main loop ----------
|
|
|
|
|
|
|
|
|
|
|
|
// Inputs
|
|
|
|
|
|
i_source = input.source(close, "Source")
|
|
|
|
|
|
i_period = input.int(50, "Lookback Period", minval=2, maxval=5000, tooltip="Rolling window for z-score normalization")
|
|
|
|
|
|
i_mu = input.float(0.0, "Mu (μ)", step=0.1, tooltip="Mean shift parameter (0 = standard normal)")
|
|
|
|
|
|
i_sigma = input.float(1.0, "Sigma (σ)", minval=0.01, step=0.1, tooltip="Scale parameter (1 = standard normal)")
|
|
|
|
|
|
|
|
|
|
|
|
// Calculation
|
|
|
|
|
|
float result = normdist(i_source, i_period, i_mu, i_sigma)
|
|
|
|
|
|
|
|
|
|
|
|
// Plot
|
|
|
|
|
|
plot(result, "NORMDIST", color=color.yellow, linewidth=2)
|
|
|
|
|
|
hline(0.5, "Midline", color=color.gray, linestyle=hline.style_dotted)
|
|
|
|
|
|
hline(0.975, "Upper 2σ", color=color.red, linestyle=hline.style_dashed)
|
|
|
|
|
|
hline(0.025, "Lower 2σ", color=color.green, linestyle=hline.style_dashed)
|
|
|
|
|
|
hline(0.841, "Upper 1σ", color=color.orange, linestyle=hline.style_dashed)
|
|
|
|
|
|
hline(0.159, "Lower 1σ", color=color.teal, linestyle=hline.style_dashed)
|