mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
feat: add ADF (Augmented Dickey-Fuller) indicator
- Core implementation with Cholesky OLS, MacKinnon p-value, AIC lag selection - Three regression models: NoConstant, Constant, ConstantAndTrend - NormCdf via Abramowitz & Stegun 7.1.26 erf approximation - Quantower adapter, Python bridge (NativeAOT export + ctypes + wrapper) - 69 tests (41 unit + 12 validation + 14 Quantower + 2 consistency) - Documentation with Schwert table, MacKinnon coefficients, PineScript ref - All 19,095 tests pass, zero warnings
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0
|
||||
// https://mozilla.org/MPL/2.0/
|
||||
// © QuanTAlib
|
||||
|
||||
//@version=6
|
||||
indicator("ADF - Augmented Dickey-Fuller Test", shorttitle="ADF", overlay=false, precision=4)
|
||||
|
||||
// ─── Inputs ────────────────────────────────────────────────────────────
|
||||
int period = input.int(50, "Period", minval=20, maxval=500)
|
||||
int maxLag = input.int(0, "Max Lag (0=auto)", minval=0, maxval=10)
|
||||
string regModel = input.string("Constant", "Regression", options=["NoConstant", "Constant", "ConstantAndTrend"])
|
||||
|
||||
// ─── Helper: OLS solve 2×2 (constant + gamma, no lags for simplicity) ──
|
||||
// Full ADF with Cholesky is too complex for Pine; this is a reference only.
|
||||
// For production use, the C# implementation is authoritative.
|
||||
|
||||
adf_pvalue(float[] data, int p, int mLag, string reg) =>
|
||||
int n = array.size(data)
|
||||
if n < 20
|
||||
1.0
|
||||
else
|
||||
// Compute first differences
|
||||
int nd = n - 1
|
||||
float[] dy = array.new_float(nd, 0.0)
|
||||
for i = 0 to nd - 1
|
||||
array.set(dy, i, array.get(data, i + 1) - array.get(data, i))
|
||||
|
||||
// Simple ADF: regress dy on y_{t-1} (constant model, no lags)
|
||||
// X = [1, y_{t-1}], Y = dy_t
|
||||
float sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0, syy = 0.0
|
||||
int startIdx = math.max(mLag, 0)
|
||||
int nObs = nd - startIdx
|
||||
if nObs < 3
|
||||
1.0
|
||||
else
|
||||
for t = startIdx to nd - 1
|
||||
float x = array.get(data, t) // y_{t-1} in level
|
||||
float y = array.get(dy, t)
|
||||
sx += x
|
||||
sy += y
|
||||
sxx += x * x
|
||||
sxy += x * y
|
||||
syy += y * y
|
||||
|
||||
// OLS for constant model: [alpha, gamma]
|
||||
float xbar = sx / nObs
|
||||
float ybar = sy / nObs
|
||||
float denom = sxx - sx * sx / nObs
|
||||
if math.abs(denom) < 1e-15
|
||||
1.0
|
||||
else
|
||||
float gamma = (sxy - sx * sy / nObs) / denom
|
||||
float alpha = ybar - gamma * xbar
|
||||
|
||||
// RSS
|
||||
float rss = 0.0
|
||||
for t = startIdx to nd - 1
|
||||
float x = array.get(data, t)
|
||||
float y = array.get(dy, t)
|
||||
float resid = y - alpha - gamma * x
|
||||
rss += resid * resid
|
||||
|
||||
float s2 = rss / (nObs - 2)
|
||||
if s2 <= 0
|
||||
1.0
|
||||
else
|
||||
float se = math.sqrt(s2 / denom)
|
||||
if se <= 1e-15
|
||||
1.0
|
||||
else
|
||||
float tStat = gamma / se
|
||||
|
||||
// MacKinnon (1994) p-value for N=1, regression-dependent
|
||||
float tauStar = reg == "NoConstant" ? -1.04 : reg == "ConstantAndTrend" ? -2.89 : -1.61
|
||||
float tauMin = reg == "NoConstant" ? -19.04 : reg == "ConstantAndTrend" ? -16.18 : -18.83
|
||||
float tauMax = reg == "NoConstant" ? 1e308 : reg == "ConstantAndTrend" ? 0.70 : 2.74
|
||||
|
||||
if tStat < tauMin
|
||||
0.0
|
||||
else if tStat > tauMax
|
||||
1.0
|
||||
else
|
||||
float poly = 0.0
|
||||
if tStat <= tauStar
|
||||
// Small-p coefficients (regression-dependent)
|
||||
if reg == "NoConstant"
|
||||
poly := 0.6344 + 1.2378 * tStat + 0.032496 * tStat * tStat
|
||||
else if reg == "ConstantAndTrend"
|
||||
poly := 3.2512 + 1.6047 * tStat + 0.049588 * tStat * tStat
|
||||
else
|
||||
poly := 2.1659 + 1.4412 * tStat + 0.038269 * tStat * tStat
|
||||
else
|
||||
// Large-p coefficients (regression-dependent)
|
||||
float t2 = tStat * tStat
|
||||
float t3 = t2 * tStat
|
||||
if reg == "NoConstant"
|
||||
poly := 0.4797 + 0.93557 * tStat - 0.06999 * t2 + 0.033066 * t3
|
||||
else if reg == "ConstantAndTrend"
|
||||
poly := 2.5261 + 0.61654 * tStat - 0.37956 * t2 - 0.060285 * t3
|
||||
else
|
||||
poly := 1.7339 + 0.93202 * tStat - 0.12745 * t2 - 0.010368 * t3
|
||||
|
||||
// Normal CDF via A&S 7.1.26 erf → Φ(x) = 0.5·(1 + erf(x/√2))
|
||||
int sgn = poly < 0 ? -1 : 1
|
||||
float z = math.abs(poly) * 0.70710678118654752
|
||||
float tt = 1.0 / (1.0 + 0.3275911 * z)
|
||||
float tt2 = tt * tt
|
||||
float tt3 = tt2 * tt
|
||||
float tt4 = tt3 * tt
|
||||
float tt5 = tt4 * tt
|
||||
float erfcZ = (0.254829592 * tt - 0.284496736 * tt2 + 1.421413741 * tt3 - 1.453152027 * tt4 + 1.061405429 * tt5) * math.exp(-z * z)
|
||||
float erfZ = 1.0 - erfcZ
|
||||
float result = 0.5 * (1.0 + sgn * erfZ)
|
||||
result
|
||||
|
||||
// ─── Main ──────────────────────────────────────────────────────────────
|
||||
float[] window = array.new_float(0)
|
||||
for i = 0 to period - 1
|
||||
array.push(window, close[period - 1 - i])
|
||||
|
||||
float pval = adf_pvalue(window, period, maxLag, regModel)
|
||||
|
||||
// ─── Plot ──────────────────────────────────────────────────────────────
|
||||
plot(pval, "ADF p-value", color=color.blue, linewidth=2)
|
||||
hline(0.01, "1% significance", color=color.red, linestyle=hline.style_dotted)
|
||||
hline(0.05, "5% significance", color=color.orange, linestyle=hline.style_dashed)
|
||||
hline(0.10, "10% significance", color=color.gray, linestyle=hline.style_dotted)
|
||||
|
||||
bgcolor(pval < 0.05 ? color.new(color.green, 90) : na)
|
||||
Reference in New Issue
Block a user