//@version=6
// ============================================================================
// Signal Quality Engine [WavesUnchained]
// Version: 3.4
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// One thing done well: a range-fade signal engine for commodities, futures,
// stocks and indices. It fades the range edges — long the exhausted lows, short
// the exhausted highs — when price is stretched, structure says "edge", and a
// candle rejects. Built for ranging markets: a regime gate (ADX + EMA slope)
// stands the tool down in strong trends, so it never fades a trend pullback.
// No regime switching, no MTF, no divergence, no pivot signals.
//
// The one logic (mirrored for both sides):
// Range regime ok → price stretched to an edge (distance + structure) →
// momentum turns back → candle rejects → score high enough → Trigger (fade).
//
// Features:
// ✓ Range regime gate (ADX + EMA slope) — only fades when actually ranging
// ✓ Edge bias (which side is exhausted) → Setup → Watch → Trigger, one logic
// ✓ Exhaustion score (0–100): Distance + Structure + Momentum
// ✓ Candle-rejection confirmation (sweep of the range edge & reclaim); confirmation window
// ✓ Market profiles tune band width / edge sensitivity
// ✓ Strictness presets (score threshold + cooldown)
// ✓ ATR risk levels (SL beyond the edge / TP toward the mean)
// ✓ Range channel fill + outer-edge zone shading (where a fade can trigger)
// ✓ Pivot reference markers (control overlay, never a signal input)
// ✓ Light-theme dashboard with a "why no trigger" status line + alerts
//
// ============================================================================
indicator("Signal Quality Engine [WavesUnchained]", "SQE", overlay=true, max_labels_count=500)
// ─────────────────────────────────────────────────────────────────────────────
// Inputs
// ─────────────────────────────────────────────────────────────────────────────
string profile = input.string(
"Natural Gas",
"Market Profile",
options=[
"Natural Gas",
"Oil",
"Gold / Silver",
"Broad Commodities",
"Stocks / Indices",
"Generic Futures"
],
group="Core"
)
string strictness = input.string(
"Strict",
"Signal Strictness",
options=["Very Strict", "Strict", "Balanced"],
group="Core"
)
int confirmWindow = input.int(3, "Confirmation Window (bars)", minval=1, maxval=8, group="Core", tooltip="A candle rejection within this many bars after an exhaustion extreme still triggers — so a one-bar timing miss doesn't void the setup. Momentum is already inside the exhaustion score, so there is no separate momentum gate.")
bool showStates = input.bool(true, "Show Setup / Watch Markers", group="Visuals", tooltip="Faint gray triangle = exhaustion building at an edge, bright yellow triangle = extreme reached, rejection pending. Both point the pending fade direction.")
bool onlyActionable = input.bool(false, "Only Watch / Trigger (hide Setup)", group="Visuals", tooltip="Hides the gray Setup markers so a choppy range doesn't clutter the chart — Watch and Trigger still show.")
bool showBands = input.bool(true, "Show Mean & Bands", group="Visuals")
bool showEdgeZones = input.bool(true, "Show Range Fill & Edge Zones", group="Visuals", tooltip="Shades the band channel and tints the outer 20% of the 80-bar range where a fade can trigger.")
bool showTable = input.bool(false, "Show Table", group="Visuals")
bool showRiskLevels = input.bool(true, "Show ATR Risk Levels", group="Visuals")
bool showPivots = input.bool(false, "Show Pivot Reference", group="Visuals", tooltip="Confirmed swing high/low markers. Control overlay only — never feeds the signals.")
bool colorBars = input.bool(false, "Color Bars", group="Visuals")
// ─────────────────────────────────────────────────────────────────────────────
// Profile presets — what actually differs by asset class for a range-fade tool:
// bbMult band width — how stretched before an "edge" (spiky commodities
// overshoot more, so they need wider bands than indices)
// nearAtrMult edge proximity tolerance
// adxMax regime tolerance — how trendy the tape can be and still be faded.
// Commodities mean-revert through choppy trends (fade readily);
// indices trend persistently, so they need a stricter range filter.
// slopeMax EMA50-slope ceiling (ATR/bar) for the regime gate, same logic.
// ─────────────────────────────────────────────────────────────────────────────
float bbMult = switch profile
"Natural Gas" => 2.5
"Oil" => 2.3
"Gold / Silver" => 2.0
"Broad Commodities" => 2.2
"Stocks / Indices" => 1.9
=> 2.1
float nearAtrMult = switch profile
"Natural Gas" => 0.95
"Oil" => 0.85
"Gold / Silver" => 0.75
"Broad Commodities" => 0.82
"Stocks / Indices" => 0.68
=> 0.80
float adxMax = switch profile
"Natural Gas" => 28.0
"Oil" => 26.0
"Gold / Silver" => 23.0
"Broad Commodities" => 25.0
"Stocks / Indices" => 20.0
=> 24.0
float slopeMax = switch profile
"Natural Gas" => 0.55
"Oil" => 0.50
"Gold / Silver" => 0.42
"Broad Commodities" => 0.48
"Stocks / Indices" => 0.35
=> 0.45
// Exhaustion thresholds — note: weighted exhaustion scores top out lower than a
// trend score, so these are deliberately below the old 76–88 band.
float scoreMin = switch strictness
"Very Strict" => 56.0
"Strict" => 48.0
=> 38.0
float setupMin = scoreMin - 12.0
int cooldownBars = switch strictness
"Very Strict" => 18
"Strict" => 12
=> 8
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
f_clamp(float x, float lo, float hi) =>
math.max(lo, math.min(hi, x))
f_safeDiv(float a, float b) =>
b == 0.0 ? 0.0 : a / b
f_score01(float x) =>
f_clamp(x, 0.0, 1.0) * 100.0
// Grade is relative to the active trigger threshold: B = triggerable (>= base),
// A/A+ progressively above it, C just below. Keeps the dashboard consistent with
// whatever strictness preset (and thus scoreMin) is selected.
f_grade(float score, float base) =>
string g = "—"
if score >= base + 24
g := "A+"
else if score >= base + 12
g := "A"
else if score >= base
g := "B"
else if score >= base - 10
g := "C"
g
// ─────────────────────────────────────────────────────────────────────────────
// Core data — mean, bands, distance
// ─────────────────────────────────────────────────────────────────────────────
float atr = ta.atr(14)
float ema = ta.ema(close, 50)
float basis = ta.sma(close, 20)
float dev = ta.stdev(close, 20)
float upper = basis + dev * bbMult
float lower = basis - dev * bbMult
float distAtr = f_safeDiv(close - ema, atr)
float bbZ = f_clamp(f_safeDiv(close - basis, dev), -4.0, 4.0) // clamp so a tiny dev can't spike the score
// ─────────────────────────────────────────────────────────────────────────────
// Regime gate — the tool only fades when the market is actually ranging.
// A strong trend turns every pullback into a "false edge"; this stands it down.
// ─────────────────────────────────────────────────────────────────────────────
[diPlus, diMinus, adx] = ta.dmi(14, 14)
float emaSlope = f_safeDiv(ema - ema[10], atr)
bool rangeOk = adx < adxMax and math.abs(emaSlope) < slopeMax
// ─────────────────────────────────────────────────────────────────────────────
// Momentum — overbought/oversold readings (feed the exhaustion score, not a gate)
// ─────────────────────────────────────────────────────────────────────────────
float rsi = ta.rsi(close, 14)
float stochRsiRaw = ta.stoch(rsi, rsi, rsi, 14)
float k = ta.sma(stochRsiRaw, 3)
float ap = hlc3
float esa = ta.ema(ap, 10)
float de = ta.ema(math.abs(ap - esa), 10)
float ci = f_safeDiv(ap - esa, 0.015 * de)
float wt1 = ta.ema(ci, 21)
// ─────────────────────────────────────────────────────────────────────────────
// Structure — recent high/low proximity and wick rejection
// ─────────────────────────────────────────────────────────────────────────────
float recentHigh = ta.highest(high, 80)
float recentLow = ta.lowest(low, 80)
float rangeSize = recentHigh - recentLow
float rangePos = f_safeDiv(close - recentLow, rangeSize) // 0 = at low, 1 = at high
// Edge = near the extreme AND in the outer fifth of the 80-bar range (not just
// "close to a trending high" — that is true constantly in a trend).
bool nearHigh = rangePos > 0.80 and recentHigh - close <= atr * nearAtrMult
bool nearLow = rangePos < 0.20 and close - recentLow <= atr * nearAtrMult
float upperWick = f_safeDiv(high - math.max(open, close), high - low)
float lowerWick = f_safeDiv(math.min(open, close) - low, high - low)
// Candle rejection — sweep the actual range edge (not just the prior candle)
// and close back inside it (liquidity-grab style), or a strong opposing wick.
bool sweptHigh = high > recentHigh[1] and close < recentHigh[1]
bool sweptLow = low < recentLow[1] and close > recentLow[1]
bool topCandleReject = sweptHigh or (close < open and upperWick >= 0.40 and close < hl2)
bool botCandleReject = sweptLow or (close > open and lowerWick >= 0.40 and close > hl2)
// Pivot reference — control overlay only. NOT used in any signal/grade/gate.
int pivotLen = 5
bool pivotLowConfirmed = not na(ta.pivotlow(low, pivotLen, pivotLen))
bool pivotHighConfirmed = not na(ta.pivothigh(high, pivotLen, pivotLen))
// ─────────────────────────────────────────────────────────────────────────────
// Exhaustion score (0–100) per edge — Distance + Structure + Momentum
// ─────────────────────────────────────────────────────────────────────────────
// Distance — how far price is stretched past the mean (top = high, bot = low)
float distTop = f_score01(math.max(f_safeDiv(distAtr, 3.0), f_safeDiv(bbZ, 2.5)))
float distBot = f_score01(math.max(f_safeDiv(-distAtr, 3.0), f_safeDiv(-bbZ, 2.5)))
// Structure — at the edge of the 80-bar range. Close beyond the band is already
// measured by Distance, and wick rejection belongs to the candle trigger, so
// neither is counted here a second time.
float structTop = f_score01(nearHigh ? 1.0 : 0.0)
float structBot = f_score01(nearLow ? 1.0 : 0.0)
// Momentum — overbought/oversold extreme
float momTop = f_score01(0.40 * f_safeDiv(rsi - 60, 20) + 0.30 * f_safeDiv(k - 75, 25) + 0.30 * f_safeDiv(wt1 - 45, 25))
float momBot = f_score01(0.40 * f_safeDiv(40 - rsi, 20) + 0.30 * f_safeDiv(25 - k, 25) + 0.30 * f_safeDiv(-45 - wt1, 25))
// Weighted exhaustion (Distance 40%, Structure 30%, Momentum 30%)
float topScore = distTop * 0.40 + structTop * 0.30 + momTop * 0.30
float botScore = distBot * 0.40 + structBot * 0.30 + momBot * 0.30
// Edge bias — the dominant exhausted side (situation-driven, not a trend guess)
int edgeDir = botScore >= setupMin and botScore >= topScore ? 1 : topScore >= setupMin and topScore > botScore ? -1 : 0
float edgeScore = edgeDir == 1 ? botScore : edgeDir == -1 ? topScore : math.max(topScore, botScore)
// ─────────────────────────────────────────────────────────────────────────────
// Three tiers: Setup → Watch → Trigger (fade the dominant edge)
// One logic, mirroring the Exhaustion Scanner: the exhaustion score (which already
// includes momentum) reaching its threshold is the "extreme"; the candle rejection
// confirms it. No separate momentum gate — that was what starved the triggers.
// ─────────────────────────────────────────────────────────────────────────────
// Extreme: exhaustion score clears the threshold on the dominant edge
bool botExtreme = botScore >= scoreMin and botScore >= topScore
bool topExtreme = topScore >= scoreMin and topScore > botScore
// An extreme stays "armed" for confirmWindow bars so the rejection need not be same-bar.
// barssince() is na until the first occurrence — guard it explicitly.
int barsSinceBotExtreme = ta.barssince(botExtreme)
int barsSinceTopExtreme = ta.barssince(topExtreme)
bool botArmed = not na(barsSinceBotExtreme) and barsSinceBotExtreme <= confirmWindow
bool topArmed = not na(barsSinceTopExtreme) and barsSinceTopExtreme <= confirmWindow
// Setup (gray): exhaustion building, not yet at the extreme threshold
bool longSetup = edgeDir == 1 and botScore >= setupMin and not botExtreme
bool shortSetup = edgeDir == -1 and topScore >= setupMin and not topExtreme
// Trigger: armed extreme + candle rejects back off the edge + cooldown
var int lastLongBar = na
var int lastShortBar = na
bool longCooldownOk = na(lastLongBar) or bar_index - lastLongBar > cooldownBars
bool shortCooldownOk = na(lastShortBar) or bar_index - lastShortBar > cooldownBars
// Trigger requires the structure side to still hold (price still in the lower/upper
// portion of the range), so a stale armed extreme alone can't fire off the edge.
bool finalLong = rangeOk and botArmed and rangePos < 0.35 and botCandleReject and longCooldownOk
bool finalShort = rangeOk and topArmed and rangePos > 0.65 and topCandleReject and shortCooldownOk
if finalLong
lastLongBar := bar_index
if finalShort
lastShortBar := bar_index
// Watch (yellow): extreme reached / armed, rejection still pending
bool longWatch = rangeOk and botArmed and not finalLong
bool shortWatch = rangeOk and topArmed and not finalShort
// Markers — only the run-up that has NOT yet triggered
bool showLongSetup = showStates and not onlyActionable and rangeOk and longSetup and not longWatch and not finalLong
bool showLongWatch = showStates and rangeOk and longWatch
bool showShortSetup = showStates and not onlyActionable and rangeOk and shortSetup and not shortWatch and not finalShort
bool showShortWatch = showStates and rangeOk and shortWatch
// ─────────────────────────────────────────────────────────────────────────────
// Risk levels — SL beyond the edge, TP toward the mean
// ─────────────────────────────────────────────────────────────────────────────
float longSL = low - atr * 1.2
float longTP1 = basis
float longTP2 = upper
float shortSL = high + atr * 1.2
float shortTP1 = basis
float shortTP2 = lower
// Latch the last trigger's levels so SL/TP stay visible for a window of bars,
// not just on the single trigger candle.
var float activeSL = na
var float activeTP1 = na
var float activeTP2 = na
var int activeUntil = na
if finalLong
activeSL := longSL
activeTP1 := longTP1
activeTP2 := longTP2
activeUntil := bar_index + 20
if finalShort
activeSL := shortSL
activeTP1 := shortTP1
activeTP2 := shortTP2
activeUntil := bar_index + 20
bool showActiveRisk = showRiskLevels and not na(activeUntil) and bar_index <= activeUntil
// ─────────────────────────────────────────────────────────────────────────────
// Visuals
// ─────────────────────────────────────────────────────────────────────────────
plot(showBands ? ema : na, "Mean (EMA50)", color=color.new(color.gray, 10), linewidth=2)
p_upper = plot(showBands ? upper : na, "Upper Band", color=color.new(color.gray, 60))
p_lower = plot(showBands ? lower : na, "Lower Band", color=color.new(color.gray, 60))
fill(p_upper, p_lower, color=showBands and showEdgeZones ? color.new(color.gray, 96) : na, title="Range Channel")
// Outer-20% edge zones (the actual fade area) get a light tint while the regime is a range.
bgcolor(showEdgeZones and rangeOk and (rangePos > 0.80 or rangePos < 0.20) ? color.new(color.gray, 94) : na, title="Edge Zone")
// Signal labels — compact (tiny) so neighbouring signals don't overlap. Full score on hover.
if finalLong
label.new(bar_index, low, "L " + str.tostring(math.round(botScore)), style=label.style_label_up, yloc=yloc.belowbar, color=color.new(color.lime, 0), textcolor=color.black, size=size.tiny, tooltip="LONG fade (exhausted low) · score " + str.tostring(math.round(botScore)) + "/100")
if finalShort
label.new(bar_index, high, "S " + str.tostring(math.round(topScore)), style=label.style_label_down, yloc=yloc.abovebar, color=color.new(color.red, 0), textcolor=color.white, size=size.tiny, tooltip="SHORT fade (exhausted high) · score " + str.tostring(math.round(topScore)) + "/100")
// Setup (faint gray triangle, "forming") and Watch (bright yellow triangle, "armed") —
// one shape language, same direction as the eventual Trigger, escalating in weight
// (faint/tiny -> bright/small -> label) so the buildup reads at a glance.
plotshape(showLongSetup, title="Long Setup", style=shape.triangleup, location=location.belowbar, color=color.new(color.gray, 55), size=size.tiny)
plotshape(showLongWatch, title="Long Watch", style=shape.triangleup, location=location.belowbar, color=color.new(color.yellow, 0), size=size.small)
plotshape(showShortSetup, title="Short Setup", style=shape.triangledown, location=location.abovebar, color=color.new(color.gray, 55), size=size.tiny)
plotshape(showShortWatch, title="Short Watch", style=shape.triangledown, location=location.abovebar, color=color.new(color.yellow, 0), size=size.small)
// Pivot reference markers — plotted at the confirmed pivot bar (control overlay)
plotshape(showPivots and pivotLowConfirmed, title="Pivot Low", style=shape.triangleup, location=location.belowbar, offset=-pivotLen, color=color.new(color.gray, 40), size=size.tiny)
plotshape(showPivots and pivotHighConfirmed, title="Pivot High", style=shape.triangledown, location=location.abovebar, offset=-pivotLen, color=color.new(color.gray, 40), size=size.tiny)
barcolor(colorBars ? finalLong ? color.lime : finalShort ? color.red : na : na)
plot(showActiveRisk ? activeSL : na, "Active SL", color=color.new(color.red, 0), style=plot.style_linebr)
plot(showActiveRisk ? activeTP1 : na, "Active TP1", color=color.new(color.green, 0), style=plot.style_linebr)
plot(showActiveRisk ? activeTP2 : na, "Active TP2", color=color.new(color.green, 30), style=plot.style_linebr)
// ─────────────────────────────────────────────────────────────────────────────
// Table (light theme — repo standard)
// ─────────────────────────────────────────────────────────────────────────────
color tc = color.new(color.gray, 20) // standard text color
color hd = color.new(color.gray, 90) // header row background
var table t = table.new(position.top_right, 2, 9,
bgcolor=color.new(color.white, 5),
border_color=color.new(color.gray, 60),
border_width=1,
frame_color=color.new(color.gray, 40),
frame_width=1)
if showTable and barstate.islast
// Status — the "why no trigger" line, top tier first
string status = "Mid-range"
string action = "Wait"
if not rangeOk
status := "Trending — fade off"
action := "Stand aside"
else if finalLong or finalShort
status := "Trigger active"
action := "Fade it"
else if longWatch or shortWatch
status := "Watch — rejection pending"
action := "Prepare"
else if longSetup or shortSetup
status := "Exhaustion building"
action := "Watch"
string grade = f_grade(edgeScore, scoreMin)
bool pass = edgeScore >= scoreMin
string edgeTxt = edgeDir == 1 ? "Low -> Long" : edgeDir == -1 ? "High -> Short" : "Mid-range"
color edgeColor = edgeDir == 1 ? #00c853 : edgeDir == -1 ? #d50000 : tc
color statusColor = (finalLong or finalShort) ? #00c853 : (longWatch or shortWatch) ? #ffd600 : (longSetup or shortSetup) ? tc : #ff6f00
table.cell(t, 0, 0, "SQE", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 1, 0, grade, text_color=pass ? color.white : tc, bgcolor=pass ? #00c853 : color.new(color.gray, 80), text_size=size.small)
table.cell(t, 0, 1, "Profile", text_color=tc, text_size=size.small)
table.cell(t, 1, 1, profile, text_color=tc, text_size=size.small)
table.cell(t, 0, 2, "Mode", text_color=tc, text_size=size.small)
table.cell(t, 1, 2, strictness, text_color=tc, text_size=size.small)
table.cell(t, 0, 3, "Edge", text_color=tc, text_size=size.small)
table.cell(t, 1, 3, edgeTxt + " " + str.tostring(math.round(edgeScore)), text_color=edgeColor, text_size=size.small)
table.cell(t, 0, 4, "Status", text_color=tc, text_size=size.small)
table.cell(t, 1, 4, status, text_color=statusColor, text_size=size.small)
table.cell(t, 0, 5, "Action", text_color=tc, text_size=size.small)
table.cell(t, 1, 5, action, text_color=tc, text_size=size.small)
table.cell(t, 0, 6, "Top / Bot", text_color=tc, text_size=size.small)
table.cell(t, 1, 6, str.tostring(math.round(topScore)) + " / " + str.tostring(math.round(botScore)), text_color=tc, text_size=size.small)
table.cell(t, 0, 7, "Dist (ATR)", text_color=tc, text_size=size.small)
table.cell(t, 1, 7, str.tostring(distAtr, "#.##"), text_color=tc, text_size=size.small)
table.cell(t, 0, 8, "TF Hint", text_color=tc, text_size=size.small)
table.cell(t, 1, 8, timeframe.isintraday and timeframe.multiplier < 60 ? "Low TF noisy" : timeframe.period == "240" ? "4H OK" : "Check context", text_color=tc, text_size=size.small)
// ─────────────────────────────────────────────────────────────────────────────
// Alerts
// ─────────────────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (finalLong), "SQE Long Signal", "SQE · LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (finalShort), "SQE Short Signal", "SQE · SHORT · {{ticker}} {{interval}}")
alertcondition(alertOk and (finalLong or finalShort), "SQE Any Signal", "SQE · SIGNAL · {{ticker}} {{interval}}")
alertcondition(alertOk and (rangeOk and (longWatch or shortWatch)), "SQE Watch", "SQE · WATCH · {{ticker}} {{interval}}")
Overview
Signal Quality Engine [WavesUnchained] applies an ADX and EMA slope regime gate to stand down during strong trends. Using distance, structure, and momentum, it computes an exhaustion score confirmed by candle rejections within a selectable window. It plots mean bands, channel fills, edge zones, ATR risk levels, and progressive state markers directly on the price chart, with an optional status table. Settings adjust market profiles, strictness tiers, and visual layers.
Range regime gate using ADX and EMA slope
Multi-factor exhaustion score combining distance, structure, and momentum
Candle-rejection confirmation window
Asset-specific market profiles and strictness presets
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Market Profile
string
Natural Gas
profile
Signal Strictness
string
Strict
strictness
Confirmation Window (bars)
int
3
confirmWindow
Show Setup / Watch Markers
bool
true
showStates
Only Watch / Trigger (hide Setup)
bool
false
onlyActionable
Show Mean & Bands
bool
true
showBands
Show Range Fill & Edge Zones
bool
true
showEdgeZones
Show Table
bool
false
showTable
Show ATR Risk Levels
bool
true
showRiskLevels
Show Pivot Reference
bool
false
showPivots
Color Bars
bool
false
colorBars
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This indicator plots optional candlestick pattern signals on the price chart. It detects engulfing bars, inside bars, HOLP and LOHP, kicker patterns, body-mass extremes, higher highs and lower lows, ATR warnings, and four-bar fractals. Optional pivot marks use
This Pine Script indicator overlays institutional liquidity levels on the price chart. It identifies order block levels using pivot highs and lows, then draws mean reversion lines from the 20 and 50 period simple moving averages, a volatility range midpoint pr
VWAP Trader NXi calculates a daily session-anchored Volume Weighted Average Price along with two custom standard deviation bands on the main price chart. It plots the core VWAP, Value Area bands, outer extension zones with background shading, and the previous
This overlay indicator marks ICT liquidity void fills on the price chart and can show long and short signals. It places a Fibonacci level inside a displacement candle, defaulting to the midpoint, and draws lines in a chosen style. Optional filters limit the fi
This indicator plots trade entry and exit signals on the price chart by combining Bollinger Bands with optional filter conditions. It calculates standard Bollinger Bands along with RSI, Aroon, and moving average filters. Reversal signals trigger when price bre
Mean Reversion Study is an indicator that identifies trade setups combining exponential moving averages, RSI extremes, and engulfing candlestick patterns. It highlights bullish setups when a bullish engulfing candle forms below the EMA while RSI is oversold, a