Files
QuanTAlib/lib/oscillators/squeeze/squeeze.pine
T

176 lines
6.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)