mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
176 lines
6.4 KiB
Plaintext
176 lines
6.4 KiB
Plaintext
// Licensed under the Apache License, Version 2.0
|
||
// © mihakralj
|
||
//@version=6
|
||
indicator("Squeeze Momentum (SQUEEZE)", "SQUEEZE", overlay=false)
|
||
|
||
//@function Calculates Squeeze Momentum — BB vs KC squeeze detection with LinReg momentum
|
||
//@param source Series to evaluate (typically close)
|
||
//@param period Lookback period for BB, KC, Donchian midline, and LinReg
|
||
//@param bbMult Bollinger Band standard deviation multiplier
|
||
//@param kcMult Keltner Channel ATR multiplier
|
||
//@returns [momentum, squeezeOn] momentum histogram and squeeze state (1=on, 0=off)
|
||
//@description Squeeze Momentum detects low-volatility compressions (BB inside KC)
|
||
// and measures directional momentum via linear regression:
|
||
// Bollinger Bands: SMA ± bbMult × StdDev(period)
|
||
// Keltner Channel: EMA ± kcMult × ATR(period) using Wilder RMA
|
||
// Squeeze On: BB_upper < KC_upper and BB_lower > KC_lower
|
||
// Midline: (Highest(high, period) + Lowest(low, period)) / 2
|
||
// Delta: close - (midline + SMA) / 2
|
||
// Momentum: LinReg(delta, period) evaluated at endpoint
|
||
// SMA and variance use O(1) circular buffer (§3 warmup).
|
||
// EMA and RMA use §2 exponential warmup compensators.
|
||
// Donchian highest/lowest use circular buffers with running max/min scan.
|
||
// LinReg uses incremental O(1) running sums (ΣY, ΣXY).
|
||
// Positive momentum = bullish, negative = bearish.
|
||
// Squeeze transitions (on→off) signal potential breakout moves.
|
||
squeeze(series float source, simple int period, simple float bbMult, simple float kcMult) =>
|
||
if period <= 0
|
||
runtime.error("Period must be greater than 0")
|
||
if bbMult <= 0.0
|
||
runtime.error("BB multiplier must be greater than 0")
|
||
if kcMult <= 0.0
|
||
runtime.error("KC multiplier must be greater than 0")
|
||
|
||
float EPSILON = 1e-10
|
||
|
||
// ===== SMA + Variance (circular buffer, §3 warmup) =====
|
||
var array<float> smaBuf = array.new_float(period, na)
|
||
var int smaHead = 0
|
||
var float smaSum = 0.0
|
||
var float smaSumSq = 0.0
|
||
var int smaCount = 0
|
||
|
||
float srcVal = nz(source)
|
||
float oldSma = array.get(smaBuf, smaHead)
|
||
if not na(oldSma)
|
||
smaSum -= oldSma
|
||
smaSumSq -= oldSma * oldSma
|
||
else
|
||
smaCount += 1
|
||
smaSum += srcVal
|
||
smaSumSq += srcVal * srcVal
|
||
array.set(smaBuf, smaHead, srcVal)
|
||
smaHead := (smaHead + 1) % period
|
||
|
||
int n = math.max(1, smaCount)
|
||
float smaVal = smaSum / float(n)
|
||
float variance = math.max(0.0, smaSumSq / float(n) - smaVal * smaVal)
|
||
float stddev = math.sqrt(variance)
|
||
|
||
float bbUpper = smaVal + bbMult * stddev
|
||
float bbLower = smaVal - bbMult * stddev
|
||
|
||
// ===== EMA for KC midline (§2 warmup) =====
|
||
var float rawEma = 0.0
|
||
var float eEma = 1.0
|
||
float emaAlpha = 2.0 / (float(period) + 1.0)
|
||
float emaBeta = 1.0 - emaAlpha
|
||
|
||
rawEma := rawEma * emaBeta + srcVal * emaAlpha
|
||
eEma *= emaBeta
|
||
float cEma = eEma > EPSILON ? 1.0 / (1.0 - eEma) : 1.0
|
||
float emaVal = rawEma * cEma
|
||
|
||
// ===== True Range + RMA for ATR (§2 warmup) =====
|
||
var float prevClose = na
|
||
float trueRange = high - low
|
||
if not na(prevClose)
|
||
trueRange := math.max(trueRange, math.max(math.abs(high - prevClose), math.abs(low - prevClose)))
|
||
prevClose := close
|
||
|
||
var float rawRma = 0.0
|
||
var float eRma = 1.0
|
||
float rmaAlpha = 1.0 / float(period)
|
||
float rmaBeta = 1.0 - rmaAlpha
|
||
|
||
rawRma := rawRma * rmaBeta + trueRange * rmaAlpha
|
||
eRma *= rmaBeta
|
||
float cRma = eRma > EPSILON ? 1.0 / (1.0 - eRma) : 1.0
|
||
float atr = rawRma * cRma
|
||
|
||
float kcUpper = emaVal + kcMult * atr
|
||
float kcLower = emaVal - kcMult * atr
|
||
|
||
// ===== Squeeze detection =====
|
||
float sqOn = bbUpper < kcUpper and bbLower > kcLower ? 1.0 : 0.0
|
||
|
||
// ===== Donchian midline: (highest + lowest) / 2 over period =====
|
||
var array<float> hiBuf = array.new_float(period, na)
|
||
var array<float> loBuf = array.new_float(period, na)
|
||
var int donHead = 0
|
||
|
||
array.set(hiBuf, donHead, high)
|
||
array.set(loBuf, donHead, low)
|
||
donHead := (donHead + 1) % period
|
||
|
||
float highest = high
|
||
float lowest = low
|
||
for i = 0 to period - 1
|
||
float h = array.get(hiBuf, i)
|
||
float l = array.get(loBuf, i)
|
||
if not na(h)
|
||
highest := math.max(highest, h)
|
||
if not na(l)
|
||
lowest := math.min(lowest, l)
|
||
|
||
float donMid = (highest + lowest) / 2.0
|
||
|
||
// ===== Delta: close - average of donchian midline and SMA =====
|
||
float delta = srcVal - (donMid + smaVal) / 2.0
|
||
|
||
// ===== LinReg of delta over period (O(1) incremental) =====
|
||
var array<float> lrBuf = array.new_float(period, na)
|
||
var int lrHead = 0
|
||
var float sumY = 0.0
|
||
var float sumXY = 0.0
|
||
var int lrCount = 0
|
||
|
||
float oldLr = array.get(lrBuf, lrHead)
|
||
if not na(oldLr)
|
||
int oldIdx = lrCount - period
|
||
sumY -= oldLr
|
||
sumXY -= float(oldIdx) * oldLr
|
||
else
|
||
// noop — count handles warmup
|
||
nop = 0
|
||
|
||
sumY += delta
|
||
sumXY += float(lrCount) * delta
|
||
array.set(lrBuf, lrHead, delta)
|
||
lrHead := (lrHead + 1) % period
|
||
|
||
lrCount += 1
|
||
|
||
int pn = math.min(lrCount, period)
|
||
int startIdx = lrCount - pn
|
||
float sumX = float(pn) * float(startIdx + startIdx + pn - 1) / 2.0
|
||
float sumX2 = 0.0
|
||
for i = 0 to pn - 1
|
||
float xi = float(startIdx + i)
|
||
sumX2 += xi * xi
|
||
|
||
float denomX = float(pn) * sumX2 - sumX * sumX
|
||
float slope = denomX == 0.0 ? 0.0 : (float(pn) * sumXY - sumX * sumY) / denomX
|
||
float intercept = (sumY - slope * sumX) / float(pn)
|
||
float momentum = slope * float(lrCount - 1) + intercept
|
||
|
||
[momentum, sqOn]
|
||
|
||
// ---------- Main loop ----------
|
||
|
||
// Inputs
|
||
i_source = input.source(close, "Source")
|
||
i_period = input.int(20, "Period", minval=1)
|
||
i_bbMult = input.float(2.0, "BB Multiplier", minval=0.001, step=0.1)
|
||
i_kcMult = input.float(1.5, "KC Multiplier", minval=0.001, step=0.1)
|
||
|
||
// Calculation
|
||
[mom, sqOn] = squeeze(i_source, i_period, i_bbMult, i_kcMult)
|
||
|
||
// Plot
|
||
momColor = mom > 0 ? (mom > nz(mom[1]) ? color.lime : color.green) : (mom < nz(mom[1]) ? color.red : color.maroon)
|
||
plot(mom, "Momentum", color=momColor, linewidth=2, style=plot.style_histogram)
|
||
plot(sqOn == 1.0 ? 0 : na, "Squeeze On", color=color.red, linewidth=4, style=plot.style_circles)
|
||
plot(sqOn == 0.0 ? 0 : na, "Squeeze Off", color=color.lime, linewidth=4, style=plot.style_circles)
|
||
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)
|