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 18:44:56 -08:00
|
|
|
|
// © mihakralj
|
|
|
|
|
|
//@version=6
|
|
|
|
|
|
indicator("Student's t-Distribution CDF (TDIST)", "TDIST", overlay=false, precision=6)
|
|
|
|
|
|
|
|
|
|
|
|
//@function Natural log of the Gamma function via Lanczos approximation (g=7, 9 coefficients)
|
|
|
|
|
|
//@param z Argument (must be > 0)
|
|
|
|
|
|
//@returns ln(Γ(z))
|
|
|
|
|
|
lnGamma(float z) =>
|
|
|
|
|
|
float g = 7.0
|
|
|
|
|
|
array<float> c = array.from(
|
|
|
|
|
|
0.99999999999980993,
|
|
|
|
|
|
676.5203681218851,
|
|
|
|
|
|
-1259.1392167224028,
|
|
|
|
|
|
771.32342877765313,
|
|
|
|
|
|
-176.61502916214059,
|
|
|
|
|
|
12.507343278686905,
|
|
|
|
|
|
-0.13857109526572012,
|
|
|
|
|
|
9.9843695780195716e-6,
|
|
|
|
|
|
1.5056327351493116e-7)
|
|
|
|
|
|
float zz = z < 0.5 ? 1.0 - z : z - 1.0
|
|
|
|
|
|
float x = array.get(c, 0)
|
|
|
|
|
|
for i = 1 to 8
|
|
|
|
|
|
x += array.get(c, i) / (zz + float(i))
|
|
|
|
|
|
float t = zz + g + 0.5
|
|
|
|
|
|
float logSqrt2Pi = 0.9189385332046727
|
|
|
|
|
|
float lnG = logSqrt2Pi + math.log(t) * (zz + 0.5) - t + math.log(x)
|
|
|
|
|
|
z < 0.5 ? math.log(math.pi / math.sin(math.pi * z)) - lnG : lnG
|
|
|
|
|
|
|
|
|
|
|
|
//@function Regularized incomplete beta function I_x(a, b) via Lentz continued fraction
|
|
|
|
|
|
//@param x Upper integration limit in [0, 1]
|
|
|
|
|
|
//@param a First shape parameter (> 0)
|
|
|
|
|
|
//@param b Second shape parameter (> 0)
|
|
|
|
|
|
//@returns I_x(a, b) in [0, 1]
|
|
|
|
|
|
betaReg(float x, float a, float b) =>
|
|
|
|
|
|
int MAXITER = 200
|
|
|
|
|
|
float EPS = 1e-10
|
|
|
|
|
|
float TINY = 1e-30
|
|
|
|
|
|
|
|
|
|
|
|
float result = 0.0
|
|
|
|
|
|
if x <= 0.0
|
|
|
|
|
|
result := 0.0
|
|
|
|
|
|
else if x >= 1.0
|
|
|
|
|
|
result := 1.0
|
|
|
|
|
|
else
|
|
|
|
|
|
bool flipped = x > (a + 1.0) / (a + b + 2.0)
|
|
|
|
|
|
float xx = flipped ? 1.0 - x : x
|
|
|
|
|
|
float aa = flipped ? b : a
|
|
|
|
|
|
float bb = flipped ? a : b
|
|
|
|
|
|
|
|
|
|
|
|
float logPfx = aa * math.log(xx) + bb * math.log(1.0 - xx)
|
|
|
|
|
|
- math.log(aa)
|
|
|
|
|
|
- lnGamma(aa) - lnGamma(bb) + lnGamma(aa + bb)
|
|
|
|
|
|
float pfx = math.exp(logPfx)
|
|
|
|
|
|
|
|
|
|
|
|
float f = 1.0 + TINY
|
|
|
|
|
|
float C = f
|
|
|
|
|
|
float D = 0.0
|
|
|
|
|
|
for m = 1 to MAXITER
|
|
|
|
|
|
float m2 = 2.0 * float(m)
|
|
|
|
|
|
float numEven = float(m) * (bb - float(m)) * xx /
|
|
|
|
|
|
((aa + m2 - 1.0) * (aa + m2))
|
|
|
|
|
|
D := 1.0 + numEven * D
|
|
|
|
|
|
D := math.abs(D) < TINY ? TINY : D
|
|
|
|
|
|
D := 1.0 / D
|
|
|
|
|
|
C := 1.0 + numEven / C
|
|
|
|
|
|
C := math.abs(C) < TINY ? TINY : C
|
|
|
|
|
|
f *= C * D
|
|
|
|
|
|
float numOdd = -(aa + float(m)) * (aa + bb + float(m)) * xx /
|
|
|
|
|
|
((aa + m2) * (aa + m2 + 1.0))
|
|
|
|
|
|
D := 1.0 + numOdd * D
|
|
|
|
|
|
D := math.abs(D) < TINY ? TINY : D
|
|
|
|
|
|
D := 1.0 / D
|
|
|
|
|
|
C := 1.0 + numOdd / C
|
|
|
|
|
|
C := math.abs(C) < TINY ? TINY : C
|
|
|
|
|
|
float delta = C * D
|
|
|
|
|
|
f *= delta
|
|
|
|
|
|
if math.abs(delta - 1.0) < EPS
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
float raw = pfx * f
|
|
|
|
|
|
result := flipped ? 1.0 - raw : raw
|
|
|
|
|
|
result
|
|
|
|
|
|
|
|
|
|
|
|
//@function Calculates Student's t-Distribution CDF
|
|
|
|
|
|
//@param source Series to evaluate (typically close)
|
|
|
|
|
|
//@param period Lookback period for min-max normalization
|
|
|
|
|
|
//@param df Degrees of freedom (ν > 0)
|
|
|
|
|
|
//@returns CDF value P(T ≤ t) in [0, 1]
|
|
|
|
|
|
//@description The Student's t-distribution CDF is computed via the relation:
|
|
|
|
|
|
// CDF(t; ν) = 1 − 0.5 × I(ν/(ν+t²), ν/2, 1/2) if t ≥ 0
|
|
|
|
|
|
// CDF(t; ν) = 0.5 × I(ν/(ν+t²), ν/2, 1/2) if t < 0
|
|
|
|
|
|
// where I is the regularized incomplete beta function (Lentz CF).
|
|
|
|
|
|
// The source is min-max normalized over the lookback period, then mapped
|
|
|
|
|
|
// to a t-statistic via linear transform: t = (x − 0.5) × tScale where
|
|
|
|
|
|
// tScale = 6.0 maps the [0,1] range to approximately [−3, +3].
|
|
|
|
|
|
// Reuses lnGamma (Lanczos 9-coeff) and betaReg (Lentz CF with symmetry flip)
|
|
|
|
|
|
// from BETADIST/FDIST. Stateless pure function — no var state.
|
|
|
|
|
|
// df=1 → Cauchy (heavy tails), df=5 → moderate tails, df→∞ → normal.
|
|
|
|
|
|
// Trading interpretation: CDF near 1.0 = price at top of recent range
|
|
|
|
|
|
// (assuming large df, approaches normal behavior). Heavy tails (low df) make
|
|
|
|
|
|
// the CDF less extreme, reflecting uncertainty about outlier moves.
|
|
|
|
|
|
tdist(series float source, simple int period, simple float df) =>
|
|
|
|
|
|
if period <= 0
|
|
|
|
|
|
runtime.error("Period must be greater than 0")
|
|
|
|
|
|
if df <= 0.0
|
|
|
|
|
|
runtime.error("Degrees of freedom must be greater than 0")
|
|
|
|
|
|
|
|
|
|
|
|
float src = nz(source)
|
|
|
|
|
|
float hi = src
|
|
|
|
|
|
float lo = src
|
|
|
|
|
|
for i = 1 to period - 1
|
|
|
|
|
|
float v = nz(source[i])
|
|
|
|
|
|
hi := math.max(hi, v)
|
|
|
|
|
|
lo := math.min(lo, v)
|
|
|
|
|
|
float range = hi - lo
|
|
|
|
|
|
float x = range == 0.0 ? 0.5 : (src - lo) / range
|
|
|
|
|
|
|
|
|
|
|
|
float tScale = 6.0
|
|
|
|
|
|
float t = (x - 0.5) * tScale
|
|
|
|
|
|
|
|
|
|
|
|
float t2 = t * t
|
|
|
|
|
|
float bx = df / (df + t2)
|
|
|
|
|
|
float ibeta = betaReg(bx, df / 2.0, 0.5)
|
|
|
|
|
|
|
|
|
|
|
|
t >= 0.0 ? 1.0 - 0.5 * ibeta : 0.5 * ibeta
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Main loop ----------
|
|
|
|
|
|
|
|
|
|
|
|
// Inputs
|
|
|
|
|
|
i_source = input.source(close, "Source")
|
|
|
|
|
|
i_period = input.int(50, "Period", minval=1)
|
|
|
|
|
|
i_df = input.float(5.0, "Degrees of Freedom", minval=0.1, step=0.1)
|
|
|
|
|
|
|
|
|
|
|
|
// Calculation
|
|
|
|
|
|
tdist_value = tdist(i_source, i_period, i_df)
|
|
|
|
|
|
|
|
|
|
|
|
// Plot
|
|
|
|
|
|
plot(tdist_value, "TDIST", color=color.yellow, linewidth=2)
|
|
|
|
|
|
hline(0.5, "Midline", color=color.gray, linestyle=hline.style_dotted)
|
|
|
|
|
|
hline(0.95, "Upper", color=color.red, linestyle=hline.style_dashed)
|
|
|
|
|
|
hline(0.05, "Lower", color=color.green, linestyle=hline.style_dashed)
|