TradingView indicator guides

QQE / RSI: Double Smoothing and Column States

This QQE / RSI variant applies two EMA passes to changes in smoothed RSI before constructing trailing bands. Its columns are centered secondary RSI values, filtered by a threshold and a Bollinger envelope on the primary trailing line. The separate line is a secondary trailing reference, not a raw RSI plot.

Inspect the Pine Script

The published variant and this copy

NodeX4 published QQE / RSI on March 18, 2022, crediting inspiration from Mihkel00. The public source's first revision uses two volatility-smoothing passes. This page provides the Pineify v6 helper implementing that structure, with fixed arguments rather than the original author's configurable inputs.

The QQE MOD guide covers a different, later single-pass version. A matching 6/5/3 label does not make those variants equivalent. The original publication and this helper also contain counters that do not affect the displayed signal conditions.

Two smoothing passes change the distance

F = EMA(RSI(source, n), smoothing) D = abs(F − F[1]) Wilder length = 2 × n − 1 A = EMA(D, Wilder length) B = EMA(A, Wilder length) Distance = factor × B

For n = 6, both volatility EMAs have length 11 and coefficient 1/6. This is not price ATR. Each distance forms both an upper and a lower candidate around its own smoothed RSI. The labels “Upper QQE” and “Bottom QQE” refer to the two calculations, not independently adjustable bullish and bearish sides of one band.

Consider assumed prior A = B = 1. If D jumps to 7, A becomes 2 and B becomes 1.166667. With factor 3, the distance is 3.5. A single-pass calculation would instead use 3 × A = 6. If the next D is 1, A becomes 1.833333 and B becomes 1.277778, so the double-pass distance rises to 3.833333 even though current D fell. These are synthetic recurrence inputs, not market results.

The trailing bands retain state under the conditions printed in the code. Trend selection uses bidirectional crosses against shifted bands and defaults a missing prior trend to 1. Initialization and loaded history therefore matter; substituting another smoothing or seeding rule changes the series.

Follow the correct series into the Bollinger filter

The envelope uses FastAtrRsiTL − 50, the primary trailing line. It does not use primary smoothed RSI as its basis. Upper and lower are the 50-bar SMA of that trailing series plus or minus 0.35 times its population standard deviation. The test against those bands then uses primary smoothed RSI minus 50.

The default primary and secondary RSI calls both use close, length 6 and smoothing 5, so their smoothed RSI streams are equal. Factors 3 and 1.61 change trailing distances; they do not make the RSI streams independent. The secondary factor affects the returned trailing line, while the colored-column conditions do not read that line.

The first ThreshHold argument is unused. Only ThreshHold2 sets the symmetric positive and negative secondary threshold. Changing the unused argument alone cannot change a column, and changing the secondary factor alone cannot change the three column-state booleans.

Cyan, pink, gray, or no column

R1 = primary smoothed RSI − 50 R2 = secondary smoothed RSI − 50 Long = R2 > 3 AND R1 > Upper Short = R2 < −3 AND R1 < Lower Gray = (R2 > 3 OR R2 < −3) AND NOT Long AND NOT Short

At the defaults, a cyan column is above zero because R2 must exceed 3. A pink column is below zero. Gray requires the secondary threshold to pass and both full-color conditions to fail. It is not a generic “exactly one of two tests passed” rule: a primary band breach by itself does not produce gray if the secondary threshold fails.

For an isolated condition check, assume Upper = 5, Lower = −5 and equal R1/R2. Values 6, 4, 3, 0, −3, −4 and −6 produce cyan, gray, no column, no column, no column, gray and pink respectively. Equality at ±3 fails the strict comparisons. These assumed test values demonstrate branches rather than reconstructing a price series.

The tests run on every bar. Several adjacent cyan columns mean a persistent state, not several fresh crossover events. No column can also occur while data is initializing or unavailable. Gray is not a calibrated probability that a full signal will follow.

What the copyable script actually displays

The code returns the secondary trailing line minus 50 first, then centered secondary RSI. The blue line below has been labeled accordingly. The earlier page called the first output “RSI Line” and made it and the zero reference fully transparent. This revision changes their display names and colors without changing the underlying recurrence or signal rules.

Gray columns show the defined partial state; cyan and pink show the full states. The zero line is a visible gray reference. These levels are oscillator units, not stop-loss prices. The script has no configurable inputs, alerts, orders, higher-timeframe request or volume filter. Its unused time helpers do not impose a trading-session filter.

Current-bar prices can change the plots before close. A closed-bar-only alert would need additional code. Compare intermediate values after sufficient history, including the primary trailing series, its envelope and both centered RSI readings. Syntax success alone cannot establish parity with another implementation.

Use the defaults to check arithmetic, not select a market

RSI itself is available in TradingView and Pine; fixed 70/30 reference levels do not exhaust the ways it can be used. This variant adds a specific double-smoothing and state-filter construction. Neither the extra stages nor a smaller signal count establishes better returns or a universal delay in bars.

The Bollinger multiplier describes a distance measured in standard deviations of the primary trailing series. It is not a promised coverage percentage for the different RSI series being tested. Larger windows also change the observations and initialization; there is no fixed conversion from a parameter change to a win-rate change.

To test an entry rule, define whether only the first transition into a color counts, whether the bar must close, and how positions exit. Specify costs and evaluate separate data. No market return, optimal timeframe, annual signal count or performance comparison is established here.

Pine Script v6: implementation and limits

The complete helper retains the double-smoothing algorithm. The visible blue trailing line and gray zero reference correct the earlier transparent display; all column calculations are unchanged.

Pine Script v6
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify

//@version=6
indicator(title="QQE / RSI", overlay=false, max_labels_count=500)

//#region --- Custom Code

//#endregion

//#region --- Common Dependence

p_comm_time_range_to_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
    int start_unix_time = na
    int end_unix_time = na
    int start_time_hour = na
    int start_time_minute = na
    int end_time_hour = na
    int end_time_minute = na
    if str.length(time_range) == 11
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
    else if str.length(time_range) == 9
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
    start_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
    end_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
    [start_unix_time, end_unix_time]

p_comm_time_range_to_start_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
    int start_time_hour = na
    int start_time_minute = na
    if str.length(time_range) == 11
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
    else if str.length(time_range) == 9
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
    timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)

p_comm_time_range_to_end_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
    int end_time_hour = na
    int end_time_minute = na
    if str.length(time_range) == 11
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
    else if str.length(time_range) == 9
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
    timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)

p_comm_timeframe_to_seconds(simple string tf) =>
    float seconds = 0
    tf_lower = str.lower(tf)
    value = str.tonumber(str.substring(tf_lower, 0, str.length(tf_lower) - 1))
    if str.endswith(tf_lower, 's')
        seconds := value
    else if str.endswith(tf_lower, 'd')
        seconds := value * 86400
    else if str.endswith(tf_lower, 'w')
        seconds := value * 604800
    else if str.endswith(tf_lower, 'm')
        seconds := value * 2592000
    else
        seconds := str.tonumber(tf_lower) * 60
    seconds

p_custom_sources() =>
    [open, high, low, close, volume]

//#endregion

//#region --- Ta Dependence

p_ta_qqe_rsi(simple int RSI_Period, simple int SF, series float src, simple int RSI_Period2, simple int SF2, series float src2, simple float QQE, simple int ThreshHold, simple float QQE2, simple int ThreshHold2, simple int bb_length, simple float bb_mult) =>
    Wilders_Period = RSI_Period * 2 - 1

    Rsi = ta.rsi(src, RSI_Period)
    RsiMa = ta.ema(Rsi, SF)
    AtrRsi = math.abs(RsiMa[1] - RsiMa)
    MaAtrRsi = ta.ema(AtrRsi, Wilders_Period)
    dar = ta.ema(MaAtrRsi, Wilders_Period) * QQE

    longband = 0.0
    shortband = 0.0
    trend = 0

    DeltaFastAtrRsi = dar
    RSIndex = RsiMa
    newshortband = RSIndex + DeltaFastAtrRsi
    newlongband = RSIndex - DeltaFastAtrRsi
    longband := RSIndex[1] > longband[1] and RSIndex > longband[1] ? math.max(longband[1], newlongband) : newlongband
    shortband := RSIndex[1] < shortband[1] and RSIndex < shortband[1] ? math.min(shortband[1], newshortband) : newshortband
    cross_1 = ta.cross(longband[1], RSIndex)
    trend := ta.cross(RSIndex, shortband[1]) ? 1 : cross_1 ? -1 : nz(trend[1], 1)
    FastAtrRsiTL = trend == 1 ? longband : shortband

    basis = ta.sma(FastAtrRsiTL - 50, bb_length)
    dev = bb_mult * ta.stdev(FastAtrRsiTL - 50, bb_length)
    upper = basis + dev
    lower = basis - dev

    QQEzlong = 0
    QQEzlong := nz(QQEzlong[1])
    QQEzshort = 0
    QQEzshort := nz(QQEzshort[1])
    QQEzlong := RSIndex >= 50 ? QQEzlong + 1 : 0
    QQEzshort := RSIndex < 50 ? QQEzshort + 1 : 0

    Wilders_Period2 = RSI_Period2 * 2 - 1

    Rsi2 = ta.rsi(src2, RSI_Period2)
    RsiMa2 = ta.ema(Rsi2, SF2)
    AtrRsi2 = math.abs(RsiMa2[1] - RsiMa2)
    MaAtrRsi2 = ta.ema(AtrRsi2, Wilders_Period2)
    dar2 = ta.ema(MaAtrRsi2, Wilders_Period2) * QQE2
    longband2 = 0.0
    shortband2 = 0.0
    trend2 = 0

    DeltaFastAtrRsi2 = dar2
    RSIndex2 = RsiMa2
    newshortband2 = RSIndex2 + DeltaFastAtrRsi2
    newlongband2 = RSIndex2 - DeltaFastAtrRsi2
    longband2 := RSIndex2[1] > longband2[1] and RSIndex2 > longband2[1] ? math.max(longband2[1], newlongband2) : newlongband2
    shortband2 := RSIndex2[1] < shortband2[1] and RSIndex2 < shortband2[1] ? math.min(shortband2[1], newshortband2) : newshortband2
    cross_2 = ta.cross(longband2[1], RSIndex2)
    trend2 := ta.cross(RSIndex2, shortband2[1]) ? 1 : cross_2 ? -1 : nz(trend2[1], 1)
    FastAtrRsi2TL = trend2 == 1 ? longband2 : shortband2

    QQE2zlong = 0
    QQE2zlong := nz(QQE2zlong[1])
    QQE2zshort = 0
    QQE2zshort := nz(QQE2zshort[1])
    QQE2zlong := RSIndex2 >= 50 ? QQE2zlong + 1 : 0
    QQE2zshort := RSIndex2 < 50 ? QQE2zshort + 1 : 0

    Greenbar1 = RsiMa2 - 50 > ThreshHold2
    Greenbar2 = RsiMa - 50 > upper

    Redbar1 = RsiMa2 - 50 < 0 - ThreshHold2
    Redbar2 = RsiMa - 50 < lower

    long = Greenbar1 and Greenbar2
    short = Redbar1 and Redbar2
    none = ((RsiMa2 - 50 > ThreshHold2) or (RsiMa2 - 50 < 0 - ThreshHold2)) and (not long and not short)

    [FastAtrRsi2TL - 50, RsiMa2 - 50, long, short, none]

//#endregion

//#region --- Constants

string P_GP_1 = ""

//#endregion

//#region --- Inputs

//#endregion

//#region --- Price Data

//#endregion

//#region --- Indicators

[p_ind_1_trailing, p_ind_1_centeredRsi, p_ind_1_longSignal, p_ind_1_shortSignal, p_ind_1_noneSignal] = p_ta_qqe_rsi(6, 5, close, 6, 5, close, 3, 3, 1.61, 3, 50, 0.35)

//#endregion

//#region --- Conditions

//#endregion

//#region --- Indicator Plots

hline(0, color=color.gray, title="QQE / RSI - Zero Line", linestyle=hline.style_dotted, linewidth=1)
plot(p_ind_1_trailing, title="QQE / RSI - Secondary Trailing Line", color=color.blue, linewidth=2)
plot(p_ind_1_centeredRsi, title="QQE / RSI - No Signal", style=plot.style_columns, color=p_ind_1_noneSignal ? color.rgb(93, 96, 107, 0) : na)
plot(p_ind_1_longSignal ? p_ind_1_centeredRsi : na, title="QQE / RSI - Long Signal", style=plot.style_columns, color=color.rgb(0, 188, 212, 0))
plot(p_ind_1_shortSignal ? p_ind_1_centeredRsi : na, title="QQE / RSI - Short Signal", style=plot.style_columns, color=color.rgb(233, 30, 99, 0))

//#endregion

//#region --- Custom Plots

//#endregion

//#region --- Alert

//#endregion

Specify a checkable adaptation in Pineify

Give the Pine Script AI Coding Agent this exact variant and specify which outputs or inputs to add. For an alert, distinguish a continuing color from the first transition and specify bar-close confirmation. Check the expected state sequence before adding separate strategy entries, exits and costs.

Open Pine Script AI Coding Agent

Educational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.

Implementation questions

No. The first returned value is the secondary QQE trailing line minus 50. The centered smoothed secondary RSI supplies the columns. The plot labels now distinguish those outputs.

Gray also requires the secondary RSI to be strictly outside its ±3 threshold and both full signal conditions to fail. A primary condition alone is insufficient.

No. The first threshold argument is unused. The second applies symmetrically to both positive and negative secondary-RSI tests.

Not necessarily. This helper uses two EMA passes on absolute changes in smoothed RSI; the December 2024 QQE MOD calculation uses one. Initialization and display also differ.

The copy has fixed function arguments and no input declarations. The author’s original publication has inputs. Adding them here requires a code change and another validation.

No. It records two true comparisons on the current bar. The script has no trade execution, exits or measured performance, and the current bar can change before close.