Files

130 lines
5.2 KiB
Plaintext
Raw Permalink Normal View History

// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Quantitative Qualitative Estimation (QQE)", "QQE", overlay=false)
//@function Calculates QQE — smoothed RSI with dynamic volatility bands
//@param source Series to evaluate (typically close)
//@param rsiPeriod RSI lookback period
//@param smoothFactor EMA smoothing factor for RSI (SF)
//@param qqeFactor Multiplier for the ATR-like trailing band (Wilders factor)
//@returns [qqeLine, trailingLevel] QQE smoothed RSI and its trailing signal
//@description QQE applies a multi-stage smoothing pipeline to RSI:
// Stage 1: Compute RSI using Wilder's RMA (alpha=1/rsiPeriod) with §2 warmup.
// Stage 2: Smooth RSI with EMA(smoothFactor) → rsiMA. Also §2 warmup.
// Stage 3: Compute |rsiMA rsiMA[1]|, smooth with double EMA(2*SF-1)
// → "dar" (dynamic average range). Both EMAs use §2 warmup.
// Stage 4: Build trailing level: upper/lower bands at rsiMA ± qqeFactor*dar.
// Trailing level follows price directionally (like Parabolic SAR logic):
// if rsiMA > prevTrail and rsiMA[1] > prevTrail → trail = max(trail, lower)
// if rsiMA < prevTrail and rsiMA[1] < prevTrail → trail = min(trail, upper)
// else → trail flips to upper or lower based on rsiMA side.
// The QQE line is rsiMA; the trailing level acts as signal for crossover triggers.
// Crosses of QQE above/below trailing level indicate momentum shifts.
// Zero-crossing of (QQE - 50) can also be used for trend direction.
qqe(series float source, simple int rsiPeriod, simple int smoothFactor, simple float qqeFactor) =>
if rsiPeriod <= 0
runtime.error("RSI period must be greater than 0")
if smoothFactor <= 0
runtime.error("Smooth factor must be greater than 0")
if qqeFactor <= 0.0
runtime.error("QQE factor must be greater than 0")
float EPSILON = 1e-10
// --- Stage 1: Wilder RSI via RMA (alpha = 1/rsiPeriod) with §2 warmup ---
var float prevSrc = na
var float rmaGain = 0.0
var float rmaLoss = 0.0
var float eRma = 1.0
float rmaAlpha = 1.0 / float(rsiPeriod)
float rmaBeta = 1.0 - rmaAlpha
float chg = not na(prevSrc) ? nz(source) - prevSrc : 0.0
prevSrc := nz(source)
float gain = chg > 0 ? chg : 0.0
float loss = chg < 0 ? -chg : 0.0
rmaGain := rmaGain * rmaBeta + gain * rmaAlpha
rmaLoss := rmaLoss * rmaBeta + loss * rmaAlpha
eRma *= rmaBeta
float cRma = eRma > EPSILON ? 1.0 / (1.0 - eRma) : 1.0
float avgGain = rmaGain * cRma
float avgLoss = rmaLoss * cRma
float rs = avgLoss == 0.0 ? 100.0 : avgGain / avgLoss
float rsiVal = 100.0 - 100.0 / (1.0 + rs)
// --- Stage 2: EMA smooth of RSI (alpha = 2/(SF+1)) with §2 warmup ---
var float rawRsiMa = 0.0
var float eRsiMa = 1.0
float sfAlpha = 2.0 / (float(smoothFactor) + 1.0)
float sfBeta = 1.0 - sfAlpha
rawRsiMa := rawRsiMa * sfBeta + rsiVal * sfAlpha
eRsiMa *= sfBeta
float cRsiMa = eRsiMa > EPSILON ? 1.0 / (1.0 - eRsiMa) : 1.0
float rsiMa = rawRsiMa * cRsiMa
// --- Stage 3: Double EMA of |delta(rsiMA)| → dar ---
// Both EMAs use period = 2*SF - 1 → alpha = 2 / (2*SF)
var float prevRsiMa = na
float darPeriod = float(2 * smoothFactor - 1)
float darAlpha = 2.0 / (darPeriod + 1.0)
float darBeta = 1.0 - darAlpha
float absDelta = not na(prevRsiMa) ? math.abs(rsiMa - prevRsiMa) : 0.0
prevRsiMa := rsiMa
var float rawDar1 = 0.0
var float eDar1 = 1.0
rawDar1 := rawDar1 * darBeta + absDelta * darAlpha
eDar1 *= darBeta
float cDar1 = eDar1 > EPSILON ? 1.0 / (1.0 - eDar1) : 1.0
float dar1 = rawDar1 * cDar1
var float rawDar2 = 0.0
var float eDar2 = 1.0
rawDar2 := rawDar2 * darBeta + dar1 * darAlpha
eDar2 *= darBeta
float cDar2 = eDar2 > EPSILON ? 1.0 / (1.0 - eDar2) : 1.0
float dar = rawDar2 * cDar2
// --- Stage 4: Trailing level (directional flip logic) ---
float band = qqeFactor * dar
float upperBand = rsiMa + band
float lowerBand = rsiMa - band
var float trail = 0.0
var float prevRsiMa2 = 50.0
float newTrail = trail
if rsiMa > trail and prevRsiMa2 > trail
newTrail := math.max(trail, lowerBand)
else if rsiMa < trail and prevRsiMa2 < trail
newTrail := math.min(trail, upperBand)
else
newTrail := rsiMa > trail ? lowerBand : upperBand
prevRsiMa2 := rsiMa
trail := newTrail
[rsiMa, trail]
// ---------- Main loop ----------
// Inputs
i_source = input.source(close, "Source")
i_rsiPeriod = input.int(14, "RSI Period", minval=1)
i_smoothFactor = input.int(5, "Smooth Factor", minval=1)
i_qqeFactor = input.float(4.236, "QQE Factor", minval=0.001, step=0.001)
// Calculation
[qqeLine, trailLine] = qqe(i_source, i_rsiPeriod, i_smoothFactor, i_qqeFactor)
// Plot
plot(qqeLine, "QQE", color=color.yellow, linewidth=2)
plot(trailLine, "Trailing", color=color.aqua, linewidth=1)
hline(50, "Midline", color=color.gray, linestyle=hline.style_dotted)
hline(70, "Overbought", color=color.red, linestyle=hline.style_dashed)
hline(30, "Oversold", color=color.green, linestyle=hline.style_dashed)