1Copy the scriptPress Copy script above, or the copy button on the code below.
2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.
Source Code
583 lines of Pine Script v6 from TrendSniperMini.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.
TrendSniperMini.pine
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// Includes the Moving Average Trend Sniper (MATS) calculation by ChartPrime (MPL 2.0), ported to Pine v6
// with minor changes. Range Breakout is an independent implementation of range-detection ideas popularised
// by QuantAlgo's Auto Range Detector (no code copied).
//@version=6
// Trend Sniper Mini [kasutufx]
// Two independent engines: SNIPER (trend / news fade / news reclaim, one trade at a time) and WYCKOFF
// (range breakout, its own trade slot), so both can be in a position at once. every past execution kept on the chart. Blue palette (red-green safe).
//
// BIAS : direction bias = price vs the slow EMA (200): longs only above it, shorts only below it.
// TREND : price on the bias side of EMA 200 (+ HTF agrees), price closes back across the fast EMA,
// volume above average. SL = ATR x mult, targets 1R / 2R / 3R. Win = TP1 or better.
// NEWS FADE : from the News Candle Research findings (news candles close outside the Bollinger Bands on high
// volume, about half to 70% get fully reclaimed, the reclaim is always led by candles closing against
// the news, and the tip usually forms within the first candles).
// Entry = first candle closing against a news candle within Fade Window candles.
// SL = beyond the tip (extreme since the news candle) + ATR buffer. TP = other end of the news candle.
// NEWS RECLAIM: after a news candle, price makes its tip and consolidates there (>= Min Candles inside a
// box no taller than Max Box x ATR). Entry when a candle closes out of the box toward the news candle
// (reclaim direction) as a triple close (3 closes in that direction) or a candle bigger than the
// average consolidation candle x Big Candle. SL beyond the box (tip side) + buffer. TP = other end
// of the news candle. A new tip restarts the consolidation.
// MATS : ChartPrime's Moving Average Trend Sniper line, drawn for visual context only (no entries use it).
// RANGE BREAKOUT: a range is a window (Range Length) whose height is compressed vs its own history
// (percentile rank), rotates around its midline, tests both edges and has little drift. Entry on a
// close beyond the range + buffer, either direction. SL = range midpoint
// +/- buffer, TP = breakout edge + range height (measured move).
// RESEARCH : every entry records its conditions (volume, body, RSI, BB %B, risk, RR, range, news size, tip
// extension). The bottom table shows the winners' range and average vs the losers' average per setup;
// the Dial-in Filters restrict entries to the range you choose.
indicator("Trend Sniper Mini [kasutufx]", shorttitle="Sniper Mini", overlay=true, max_boxes_count=300, max_labels_count=400)
// ===== Inputs =====
useTrend = input.bool(true, "Trend Entries", group="Trend")
biasMode = input.string("Trend only", "EMA200 Direction Bias", options=["All setups", "Trend + Wyckoff", "Trend only"], group="Trend", tooltip="Longs only when price is above the slow EMA (200), shorts only below it. Choose which setups follow the bias; news fades / reclaims trade against the news candle and may fire counter-bias.")
matsLen = input.int(30, "MATS Length", minval=2, group="MATS (ChartPrime, visual only)")
showMats = input.bool(true, "Show MATS Line", group="MATS (ChartPrime, visual only)")
useRange = input.bool(true, "Range Breakout Entries", group="Wyckoff Range Breakout")
rgLen = input.int(20, "Range Length", minval=8, maxval=100, group="Wyckoff Range Breakout")
rgComp = input.float(40, "Compression Percentile (max)", minval=5, maxval=95, step=5, group="Wyckoff Range Breakout", tooltip="Range height vs ATR, ranked against its own history. 40 = tighter than 60% of past windows.")
rgCal = input.int(500, "Calibration Lookback", minval=100, maxval=2000, group="Wyckoff Range Breakout")
rgRot = input.float(0.18, "Min Midline Crossings per Bar", minval=0.05, maxval=0.6, step=0.01, group="Wyckoff Range Breakout")
rgTouch = input.int(2, "Min Touches per Edge", minval=1, maxval=10, group="Wyckoff Range Breakout")
rgTol = input.float(0.10, "Touch Tolerance (ATR)", minval=0.02, maxval=1, step=0.02, group="Wyckoff Range Breakout")
rgDrift = input.float(0.45, "Max Drift", minval=0.05, maxval=2, step=0.05, group="Wyckoff Range Breakout", tooltip="Regression slope over the window as a share of range height.")
rgCont = input.float(0.70, "Min Closes Inside", minval=0.4, maxval=1, step=0.05, group="Wyckoff Range Breakout")
rgBuf = input.float(0.15, "Breakout Buffer (ATR)", minval=0, maxval=2, step=0.05, group="Wyckoff Range Breakout")
rgMaxAge = input.int(200, "Max Range Age (candles)", minval=10, maxval=2000, group="Wyckoff Range Breakout")
fastLen = input.int(50, "Fast EMA", group="Trend")
slowLen = input.int(200, "Slow EMA", group="Trend")
useHtfBias = input.bool(true, "Use HTF Bias", group="Trend", tooltip="Longs only when the HTF fast EMA is above its slow EMA, shorts only when below (closed HTF candles). Applies to the same setups as the EMA200 bias.")
htfTF = input.timeframe("15", "HTF Bias Timeframe (empty = off)", group="Trend")
volMult = input.float(1.2, "Volume x Average (0 = off)", minval=0, step=0.1, group="Trend")
atrMult = input.float(1.5, "SL ATR Multiplier", minval=0.1, step=0.1, group="Trend")
useNews = input.bool(true, "News Fade Entries", group="News Fade")
newsMult = input.float(1.6, "News Candle Size (x largest prior range)", minval=1, step=0.1, group="News Fade")
newsLen = input.int(20, "News Lookback", minval=2, group="News Fade")
newsBody = input.float(40, "News Min Body %", minval=0, maxval=100, group="News Fade")
needBB = input.bool(true, "News Must Close Outside Bollinger", group="News Fade", tooltip="86-93% of news candles in the research closed outside the bands.")
fadeWait = input.int(5, "Fade Window (candles)", minval=1, maxval=20, group="News Fade", tooltip="The first candle closing against the news candle must come within this many candles.")
fadeBuf = input.float(0.2, "SL Buffer beyond Tip (x ATR)", minval=0, step=0.05, group="News Fade")
useRecl = input.bool(true, "News Reclaim Entries", group="News Reclaim")
reclWindow = input.int(60, "Look for Reclaim Within (candles)", minval=5, maxval=200, group="News Reclaim", tooltip="Research: reclaims took about 14 candles on average.")
consMin = input.int(3, "Consolidation Min Candles", minval=2, maxval=50, group="News Reclaim")
consMaxAtr = input.float(3.0, "Consolidation Max Box (x ATR)", minval=0.2, step=0.1, group="News Reclaim")
bigMult = input.float(1.3, "Big Candle (x avg consolidation candle)", minval=1, step=0.1, group="News Reclaim")
useTriple = input.bool(true, "Allow Triple Close Breakout", group="News Reclaim")
reclBuf = input.float(0.2, "SL Buffer beyond Box (x ATR)", minval=0, step=0.05, group="News Reclaim")
showCons = input.bool(true, "Draw Consolidation Box", group="News Reclaim")
showSkips = input.bool(true, "Show Skipped Wyckoff Breakouts", group="Display", tooltip="A grey x on range breakouts that did not become a trade; hover it to see which rule blocked it.")
swingTp = input.bool(true, "ATR-based TP", group="Swing", tooltip="Final TP = entry +/- ATR x multiplier for both setups (replaces 3R for trend and the news other end for fades). Trend TP1 / TP2 sit at 1/3 and 2/3 of the way.")
swingMult = input.float(3.0, "TP ATR Multiplier", minval=0.1, step=0.1, group="Swing")
swingNews = input.bool(false, "Also Use ATR TP for News Setups", group="Swing", tooltip="Off: news fade / reclaim keep the research target (other end of the news candle) even when ATR-based TP is on.")
swingLen = input.int(14, "Swing ATR Length", minval=1, group="Swing")
swingTF = input.timeframe("", "Swing ATR Timeframe (empty = chart)", group="Swing", tooltip="Eg. 60 or 240 to size swing targets from a higher timeframe ATR (closed candles only).")
keep = input.int(50, "Trades Shown", minval=1, maxval=150, group="Display")
winsOnly = input.bool(false, "Show Winning Trades Only", group="Display", tooltip="Losing trades are removed from the chart once they close (still counted in the dashboard and research table).")
showStats = input.bool(true, "Show Entry Conditions Table", group="Display", tooltip="For every setup: range (min-max) and average of each entry condition among winners, and the average among losers. Use it to set the dial-in filters.")
fVolMin = input.float(0, "Min Volume x Avg (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
fBodyMin = input.float(0, "Min Entry Body % (0 = off)", minval=0, maxval=100, group="Dial-in Filters (both setups)")
fRsiMin = input.float(0, "RSI Min (trade direction)", minval=0, maxval=100, group="Dial-in Filters (both setups)", tooltip="RSI in the trade's direction: longs use RSI, shorts use 100 - RSI. Higher = more stretched with the trade.")
fRsiMax = input.float(100, "RSI Max (trade direction)", minval=0, maxval=100, group="Dial-in Filters (both setups)")
fPctBMin = input.float(-1, "BB %B Min (trade direction)", step=0.05, group="Dial-in Filters (both setups)", tooltip="%B in the trade's direction: longs use %B, shorts use 1 - %B. 0 = band against the trade, 1 = band with the trade.")
fPctBMax = input.float(2, "BB %B Max (trade direction)", step=0.05, group="Dial-in Filters (both setups)")
fRiskMin = input.float(0, "Min Risk in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
fRiskMax = input.float(0, "Max Risk in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
fRngMin = input.float(0, "Min Entry Range in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
fRngMax = input.float(0, "Max Entry Range in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
fRRMin = input.float(0, "Min RR to Final TP (0 = off)", minval=0, step=0.1, group="Dial-in Filters (both setups)")
// defaults tuned from the 15m XAUUSD results (winners vs losers); set to 0 to switch a filter off
fSizeMin = input.float(0, "Min News Size x Prior (0 = off)", minval=0, step=0.1, group="Dial-in Filters (news fade)", tooltip="15m results: winners avg 3.86x, losers 2.73x.")
fTipMax = input.float(2.0, "Max Tip Extension in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (news fade)", tooltip="15m results: winners avg 0.64 ATR, losers 0.87 ATR.")
fFadeRiskMin = input.float(0, "Min Risk in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (news fade)", tooltip="15m results: winners avg 1.97 ATR, losers 1.37 ATR. Off by default: together with the other fade filters it left only 3 trades.")
fFadeRngMin = input.float(0, "Min Entry Range in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (news fade)", tooltip="15m results: winners avg 1.95 ATR, losers 1.47 ATR. Off by default: together with the other fade filters it left only 3 trades.")
fTrendRngMax = input.float(1.6, "Max Entry Range in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (trend)", tooltip="15m results: winners avg 1.48 ATR, losers 1.75 ATR (stretched entry candles failed more).")
fTrendRsiMin = input.float(0, "Min RSI, trade direction (0 = off)", minval=0, maxval=100, group="Dial-in Filters (trend)", tooltip="Off by default: in the run after it was enabled, winners and losers had the same RSI (55 vs 54) and trend Net R fell from 8.4R to 2.8R. Longs use RSI, shorts 100 - RSI.")
fTrendPctBMin = input.float(-1, "Min BB %B, trade direction (-1 = off)", minval=-1, step=0.05, group="Dial-in Filters (trend)", tooltip="Off by default: no edge in the latest run (0.80 vs 0.78). Longs use %B, shorts 1 - %B.")
fWyRiskMax = input.float(0, "Max Risk in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (wyckoff)", tooltip="Off by default so explosive breakouts trade. 15m results: winners avg 2.27 ATR, losers 2.74 ATR; try 2.5 to filter wide ranges.")
fWyRngMax = input.float(0, "Max Breakout Candle Range in ATR (0 = off)", minval=0, step=0.1, group="Dial-in Filters (wyckoff)", tooltip="Off by default so explosive breakouts trade. 15m results: winners avg 1.77 ATR, losers 2.03 ATR; try 2.0 to filter climax candles.")
// ===== Blue palette =====
cLight = #64B5F6
cMid = #1E88E5
cDeep = #0D47A1
cPale = #BBDEFB
cWin = color.new(#4FC3F7, 80)
cLoss = color.new(#283593, 70)
// ===== Common =====
atr = ta.atr(14)
f_swingAtr() => ta.atr(swingLen)[1]
swingAtrHtf = request.security(syminfo.tickerid, swingTF == "" ? timeframe.period : swingTF, f_swingAtr(), lookahead=barmerge.lookahead_on)
swingAtrChart = ta.atr(swingLen)
swingAtr = swingTF == "" ? swingAtrChart : swingAtrHtf
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
plot(fast, "Fast EMA", color.new(cLight, 20))
plot(slow, "Slow EMA", color.new(cDeep, 0), 2)
// ===== MATS (ChartPrime, MPL 2.0) =====
f_cpEma(float source) =>
var float e = 0.0
var int count = 0
count := nz(count[1]) + 1
e := (1.0 - 2.0 / (count + 1.0)) * nz(e[1]) + 2.0 / (count + 1.0) * source
e
f_atan2(float y, float x) =>
var float angle = 0.0
if x > 0
angle := math.atan(y / x)
else if x < 0 and y >= 0
angle := math.atan(y / x) + math.pi
else if x < 0 and y < 0
angle := math.atan(y / x) - math.pi
else if x == 0 and y > 0
angle := math.pi / 2
else if x == 0 and y < 0
angle := -math.pi / 2
angle
f_tra() =>
a = f_cpEma(ta.tr)
slope = (close - close[10]) / (a * 10)
deg = f_atan2(slope, 1) * 180 / math.pi
ta.sma(deg > 0 ? high : low, 2)
f_mats(float source, int length) =>
smooth = 0.0
hh = math.max(math.sign(ta.change(ta.highest(length))), 0)
ll = math.max(math.sign(ta.change(ta.lowest(length)) * -1), 0)
tc = math.pow(ta.sma(hh > 0 or ll > 0 ? 1.0 : 0.0, length), 2)
smooth := nz(smooth[1] + tc * (source - smooth[1]), source)
wp = length * 4 - 1
a = math.abs(nz(smooth[1]) - smooth)
maAtr = ta.ema(a, wp)
delta = ta.ema(maAtr, wp) * length * 0.4
result = 0.0
if smooth > nz(result[1])
result := smooth - delta < result[1] ? result[1] : smooth - delta
else
result := smooth + delta > result[1] ? result[1] : smooth + delta
result
matsLine = f_mats(f_tra(), matsLen)
matsUp = ta.sma(close, 2) > matsLine
plot(showMats ? matsLine : na, "MATS", matsUp ? cLight : #5C6BC0, 2)
// ===== Range detection (independent implementation) =====
atrRg = ta.atr(200)
rgTop = ta.highest(high, rgLen)
rgBot = ta.lowest(low, rgLen)
rgH = rgTop - rgBot
rgCompVal = atrRg > 0 and rgH > 0 ? rgH / (atrRg * math.sqrt(rgLen)) : 99.0
rgRank = ta.percentrank(rgCompVal, rgCal)
rgSlope = ta.linreg(close, rgLen, 0) - ta.linreg(close, rgLen, 1)
f_rangeShape(float top, float bot, int len, float tol) =>
mid = (top + bot) / 2
int cross = 0
int tTop = 0
int tBot = 0
int inside = 0
for i = 0 to len - 1
if (close[i] > mid) != (close[i + 1] > mid)
cross += 1
if high[i] >= top - tol
tTop += 1
if low[i] <= bot + tol
tBot += 1
if close[i] <= top and close[i] >= bot
inside += 1
[cross, tTop, tBot, inside]
[rgCross, rgTT, rgTB, rgIn] = f_rangeShape(rgTop, rgBot, rgLen, rgTol * nz(atrRg))
rgQualified = useRange and not na(rgRank) and atrRg > 0 and bar_index > rgCal and rgRank <= rgComp and rgCross >= math.max(3, math.round(rgRot * rgLen)) and rgTT >= rgTouch and rgTB >= rgTouch and rgH > 0 and math.abs(rgSlope) * rgLen / rgH <= rgDrift and rgIn * 1.0 / rgLen >= rgCont
var bool gActive = false
var float gTop = na
var float gBot = na
var int gLeft = na
var box gBox = na
bool rangeSig = false
int rangeDir = 0
float rangeSl = na
float rangeTp = na
if barstate.isconfirmed
if gActive
box.set_right(gBox, bar_index)
upBrk = close > gTop + rgBuf * atrRg
dnBrk = close < gBot - rgBuf * atrRg
if upBrk or dnBrk or bar_index - gLeft >= rgMaxAge
gActive := false
if upBrk or dnBrk
bd = upBrk ? 1 : -1
box.set_border_color(gBox, color.new(bd == 1 ? cLight : #5C6BC0, 20))
rangeSig := true
rangeDir := bd
gMid = (gTop + gBot) / 2
rangeSl := gMid - bd * rgBuf * atrRg
rangeTp := bd == 1 ? gTop + (gTop - gBot) : gBot - (gTop - gBot)
else if rgQualified
gActive := true
gTop := rgTop
gBot := rgBot
gLeft := bar_index - rgLen + 1
gBox := box.new(gLeft, gTop, bar_index, gBot, border_color=color.new(cPale, 50), bgcolor=color.new(cPale, 92), border_style=line.style_dashed)
// ===== Trend signal =====
f_htf() => ta.ema(close, fastLen)[1] - ta.ema(close, slowLen)[1]
htfRaw = request.security(syminfo.tickerid, htfTF == "" ? timeframe.period : htfTF, f_htf(), lookahead=barmerge.lookahead_on)
htfDiff = htfTF == "" or not useHtfBias ? 0.0 : htfRaw
volAvg = ta.sma(volume, 20)
volOk = volMult == 0 or volume > volAvg * volMult
emaBuy = close > slow and ta.crossover(close, fast)
emaSell = close < slow and ta.crossunder(close, fast)
trendBuy = useTrend and barstate.isconfirmed and emaBuy and volOk
trendSell = useTrend and barstate.isconfirmed and emaSell and volOk
// ===== News fade signal =====
rng = high - low
bbBasis = ta.sma(close, 20)
bbDev = 2 * ta.stdev(close, 20)
priorMaxRng = ta.highest(rng, newsLen)[1]
isNews = useNews and barstate.isconfirmed and rng > 0 and rng >= newsMult * priorMaxRng and math.abs(close - open) >= rng * newsBody / 100 and (not needBB or close > bbBasis + bbDev or close < bbBasis - bbDev)
var int nDir = 0
var int nBar = na
var float nOther = na
var float nTip = na
var float nClose = na
var float nVolR = na
var float nSize = na
bool fadeSig = false
int fadeDir = 0
if nDir != 0 and barstate.isconfirmed and bar_index > nBar
nTip := nDir == 1 ? math.max(nTip, high) : math.min(nTip, low)
if nDir == 1 ? close < open : close > open
fadeSig := true
fadeDir := -nDir
nDir := 0
else if bar_index - nBar >= fadeWait
nDir := 0
// ===== News reclaim signal (tip consolidation -> breakout toward the news candle) =====
var int rDir = 0
var int rBar = na
var float rOther = na
var float rTip = na
var float rNClose = na
var float rNVol = na
var float rNSize = na
var float cHi = na
var float cLo = na
var float cSum = 0.0
var int cN = 0
var int cStart = na
bool reclSig = false
int reclDir = 0
float reclSl = na
if rDir != 0 and barstate.isconfirmed and bar_index > rBar
newTip = rDir == 1 ? high > rTip : low < rTip
if newTip
// a new extreme restarts the consolidation from this candle
rTip := rDir == 1 ? high : low
cHi := high
cLo := low
cSum := rng
cN := 1
cStart := bar_index
else
boxOk = cN >= consMin and cHi - cLo <= consMaxAtr * atr
breakout = rDir == 1 ? close < cLo : close > cHi
triple = rDir == 1 ? close < open and close[1] < open[1] and close[2] < open[2] : close > open and close[1] > open[1] and close[2] > open[2]
big = cN > 0 and rng > cSum / cN * bigMult
if boxOk and breakout and ((useTriple and triple) or big)
reclSig := true
reclDir := -rDir
reclSl := rDir == 1 ? cHi + atr * reclBuf : cLo - atr * reclBuf
if showCons
box.new(cStart, cHi, bar_index - 1, cLo, border_color=color.new(cLight, 30), bgcolor=color.new(cLight, 90))
rDir := 0
else
cHi := math.max(cHi, high)
cLo := math.min(cLo, low)
cSum += rng
cN += 1
if rDir != 0 and bar_index - rBar >= reclWindow
rDir := 0
if isNews and useRecl
rDir := close > open ? 1 : -1
rBar := bar_index
rOther := rDir == 1 ? low : high
rTip := rDir == 1 ? high : low
rNClose := close
rNVol := volume / volAvg
rNSize := rng / priorMaxRng
// the news candle itself is the first candle of the tip area
cHi := high
cLo := low
cSum := rng
cN := 1
cStart := bar_index
if isNews
nDir := close > open ? 1 : -1
nBar := bar_index
nOther := nDir == 1 ? low : high
nTip := nDir == 1 ? high : low
nClose := close
nVolR := volume / volAvg
nSize := rng / priorMaxRng
label.new(bar_index, nDir == 1 ? low : high, "N", color=color.new(cPale, 10), textcolor=cDeep, style=nDir == 1 ? label.style_label_up : label.style_label_down, size=size.tiny, tooltip="News candle")
// ===== Entry conditions =====
rsi = ta.rsi(close, 14)
pctB = bbDev > 0 ? (close - (bbBasis - bbDev)) / (2 * bbDev) : 0.5
NF = 9
var names = array.from("Volume x avg", "Entry body %", "RSI (trade dir)", "BB %B (trade dir)", "Risk (ATR)", "RR to final TP", "Entry range (ATR)", "News size (x prior)", "Tip ext. (ATR)")
// winners: sum / min / max, losers: sum, per condition
type Agg
array<float> wSum
array<float> wMin
array<float> wMax
array<float> lSum
array<int> wN
array<int> lN
f_newAgg() => Agg.new(array.new<float>(NF, 0.0), array.new<float>(NF, na), array.new<float>(NF, na), array.new<float>(NF, 0.0), array.new<int>(NF, 0), array.new<int>(NF, 0))
var Agg aggT = f_newAgg()
var Agg aggN = f_newAgg()
var Agg aggR = f_newAgg()
var Agg aggG = f_newAgg()
var array<float> cond = array.new<float>(NF, na)
f_record(Agg a, array<float> c, bool won) =>
for i = 0 to NF - 1
v = c.get(i)
if not na(v)
if won
a.wSum.set(i, a.wSum.get(i) + v)
a.wMin.set(i, na(a.wMin.get(i)) ? v : math.min(a.wMin.get(i), v))
a.wMax.set(i, na(a.wMax.get(i)) ? v : math.max(a.wMax.get(i), v))
a.wN.set(i, a.wN.get(i) + 1)
else
a.lSum.set(i, a.lSum.get(i) + v)
a.lN.set(i, a.lN.get(i) + 1)
// ===== Trades =====
// Two independent position slots: SNIPER (trend / news fade / news reclaim) and WYCKOFF (range breakout).
// kind: 0 = trend, 1 = news fade, 2 = news reclaim, 3 = wyckoff range breakout
type Pos
int dir = 0
int kind = 0
float entry = na
float sl = na
float tp = na
float r = na
float step = na
int hits = 0
box win = na
box loss = na
label tag = na
array<float> cond
var Pos pS = Pos.new(cond=array.new<float>(NF, na))
var Pos pW = Pos.new(cond=array.new<float>(NF, na))
var array<box> boxes = array.new<box>()
var array<label> tags = array.new<label>()
// per kind: trades, wins, sum of R
var array<int> stTrades = array.from(0, 0, 0, 0)
var array<int> stWins = array.from(0, 0, 0, 0)
var array<float> stR = array.from(0.0, 0.0, 0.0, 0.0)
f_aggOf(int k) => k == 3 ? aggG : k == 2 ? aggR : k == 1 ? aggN : aggT
// manage an open position on a confirmed candle (SL checked first); returns true when it closed
f_manage(Pos p) =>
bool closed = false
if p.dir != 0 and bar_index > box.get_left(p.win)
stopped = p.dir == 1 ? low <= p.sl : high >= p.sl
bool done = stopped
if not stopped
if p.kind != 0
if p.dir == 1 ? high >= p.tp : low <= p.tp
p.hits := 1
done := true
else
for i = p.hits + 1 to 3
if p.dir == 1 ? high >= p.entry + i * p.step : low <= p.entry - i * p.step
p.hits := i
done := p.hits == 3
box.set_right(p.win, bar_index)
box.set_right(p.loss, bar_index)
if done
won = p.hits > 0
label.set_text(p.tag, label.get_text(p.tag) + (not won ? " SL" : p.kind != 0 ? " TP" : " TP" + str.tostring(p.hits)))
label.set_color(p.tag, won ? cLight : cDeep)
label.set_textcolor(p.tag, won ? cDeep : color.white)
f_record(f_aggOf(p.kind), p.cond, won)
if won
// profit box ends at the TP actually reached (ATR target when ATR-based TP is on)
reached = p.kind != 0 ? p.tp : p.entry + p.dir * p.hits * p.step
box.set_top(p.win, math.max(p.entry, reached))
box.set_bottom(p.win, math.min(p.entry, reached))
else if winsOnly
iw = boxes.indexof(p.win)
if iw >= 0
boxes.remove(iw)
il = boxes.indexof(p.loss)
if il >= 0
boxes.remove(il)
it = tags.indexof(p.tag)
if it >= 0
tags.remove(it)
box.delete(p.win)
box.delete(p.loss)
label.delete(p.tag)
// R: single-target setups win the TP distance; trend scales out 1/3 at each TP, the rest at the SL
float tradeR = 0.0
if p.kind != 0
tradeR := won ? math.abs(p.tp - p.entry) / p.r : -1.0
else
if p.hits > 0
for i = 1 to p.hits
tradeR += i * p.step / p.r / 3
tradeR += stopped ? -(3 - p.hits) / 3.0 : 0.0
stTrades.set(p.kind, stTrades.get(p.kind) + 1)
stWins.set(p.kind, stWins.get(p.kind) + (won ? 1 : 0))
stR.set(p.kind, stR.get(p.kind) + tradeR)
p.dir := 0
closed := true
closed
// check the dial-in filters and open position p; returns true when opened
f_try(Pos p, int k, int d, float slPx, float tpPx, float volR, float tipExt, float sizeX) =>
string why = ""
fade = k == 1
biasOn = biasMode == "All setups" or (biasMode == "Trend + Wyckoff" and (k == 0 or k == 3)) or k == 0
biasOk = not biasOn or ((d == 1 ? close > slow : close < slow) and (d == 1 ? htfDiff >= 0 : htfDiff <= 0))
rngE = high - low
riskA = math.abs(close - slPx) / atr
rr = math.abs(tpPx - close) / math.max(math.abs(close - slPx), syminfo.mintick)
bodyE = rngE > 0 ? math.abs(close - open) / rngE * 100 : 0
rsiD = d == 1 ? rsi : 100 - rsi
pctBD = d == 1 ? pctB : 1 - pctB
rngA = rngE / atr
setupOk = k == 3 ? (fWyRiskMax == 0 or riskA <= fWyRiskMax) and (fWyRngMax == 0 or rngA <= fWyRngMax) : k == 2 ? true : fade ? (fFadeRiskMin == 0 or riskA >= fFadeRiskMin) and (fFadeRngMin == 0 or rngA >= fFadeRngMin) : (fTrendRngMax == 0 or rngA <= fTrendRngMax) and (fTrendRsiMin == 0 or rsiD >= fTrendRsiMin) and (fTrendPctBMin == -1 or pctBD >= fTrendPctBMin)
passF = biasOk and setupOk and (fVolMin == 0 or volR >= fVolMin) and (fBodyMin == 0 or bodyE >= fBodyMin) and rsiD >= fRsiMin and rsiD <= fRsiMax and pctBD >= fPctBMin and pctBD <= fPctBMax and (fRiskMin == 0 or riskA >= fRiskMin) and (fRiskMax == 0 or riskA <= fRiskMax) and (fRngMin == 0 or rngA >= fRngMin) and (fRngMax == 0 or rngA <= fRngMax) and (fRRMin == 0 or rr >= fRRMin) and (not fade or fSizeMin == 0 or sizeX >= fSizeMin) and (not fade or fTipMax == 0 or tipExt <= fTipMax)
room = d == 1 ? slPx < close and tpPx > close : slPx > close and tpPx < close
why := not biasOk ? ((d == 1 ? close > slow : close < slow) ? "against the HTF bias" : "against the EMA200 bias") : not setupOk ? (k == 3 ? (fWyRiskMax > 0 and riskA > fWyRiskMax ? "risk " + str.tostring(riskA, "#.##") + " ATR > max " + str.tostring(fWyRiskMax) : "breakout candle " + str.tostring(rngA, "#.##") + " ATR > max " + str.tostring(fWyRngMax)) : "setup filter") : not passF ? "dial-in filter" : not room ? "no room between SL and TP" : ""
if why == ""
p.cond.set(0, volR)
p.cond.set(1, bodyE)
p.cond.set(2, rsiD)
p.cond.set(3, pctBD)
p.cond.set(4, riskA)
p.cond.set(5, rr)
p.cond.set(6, rngA)
p.cond.set(7, sizeX)
p.cond.set(8, tipExt)
p.dir := d
p.kind := k
p.entry := close
p.sl := slPx
p.tp := tpPx
p.r := math.abs(close - slPx)
p.step := swingTp and k == 0 ? math.abs(tpPx - close) / 3 : p.r
p.hits := 0
p.win := box.new(bar_index, math.max(close, tpPx), bar_index, math.min(close, tpPx), border_color=na, bgcolor=cWin)
p.loss := box.new(bar_index, math.max(close, slPx), bar_index, math.min(close, slPx), border_color=na, bgcolor=cLoss)
tgtTxt = swingTp and (k == 0 or k == 3 or swingNews) ? " (" + str.tostring(swingMult) + " ATR)" : k == 3 ? " (measured move)" : " (news other end)"
tipTxt = (k == 3 ? "Wyckoff range breakout " : k == 2 ? "News reclaim " : fade ? "News fade " : "Trend ") + (d == 1 ? "BUY" : "SELL") + "\nEntry " + str.tostring(close, format.mintick) + "\nSL " + str.tostring(slPx, format.mintick) + (k != 0 ? "\nTP " + str.tostring(tpPx, format.mintick) + tgtTxt : "\nTP1 " + str.tostring(close + d * p.step, format.mintick) + "\nTP2 " + str.tostring(close + d * 2 * p.step, format.mintick) + "\nTP3 " + str.tostring(tpPx, format.mintick) + (swingTp ? " (" + str.tostring(swingMult) + " ATR)" : ""))
p.tag := label.new(bar_index, close, (k == 3 ? "W" : k == 2 ? "R" : fade ? "N" : "") + (d == 1 ? "B" : "S"), color=k == 3 ? cDeep : cMid, textcolor=color.white, style=label.style_label_right, size=size.tiny, tooltip=tipTxt)
boxes.push(p.win)
boxes.push(p.loss)
tags.push(p.tag)
if tags.size() > keep
box.delete(boxes.shift())
box.delete(boxes.shift())
label.delete(tags.shift())
why
// manage both slots, then open new positions; each slot runs independently
if barstate.isconfirmed
f_manage(pS)
f_manage(pW)
bool openedS = false
bool openedW = false
// SNIPER slot: news reclaim first, then news fade, then trend
if pS.dir == 0 and (reclSig or fadeSig or trendBuy or trendSell) and not na(atr)
k = reclSig ? 2 : fadeSig ? 1 : 0
d = reclSig ? reclDir : fadeSig ? fadeDir : trendBuy ? 1 : -1
slPx = k == 2 ? reclSl : k == 1 ? nTip - d * atr * fadeBuf : close - d * atr * atrMult
tpPx = swingTp and not na(swingAtr) and (k == 0 or swingNews) ? close + d * swingAtr * swingMult : k == 2 ? rOther : k == 1 ? nOther : close + d * 3 * atr * atrMult
volR = k == 2 ? rNVol : k == 1 ? nVolR : volume / volAvg
tipExt = k == 2 ? math.abs(rTip - rNClose) / atr : k == 1 ? math.abs(nTip - nClose) / atr : na
sizeX = k == 2 ? rNSize : k == 1 ? nSize : na
openedS := f_try(pS, k, d, slPx, tpPx, volR, tipExt, sizeX) == ""
// WYCKOFF slot: range breakout
if rangeSig and pW.dir != 0 and showSkips
label.new(bar_index, rangeDir == 1 ? low : high, "x", color=color(na), textcolor=color.gray, style=rangeDir == 1 ? label.style_label_up : label.style_label_down, size=size.small, tooltip="Wyckoff breakout skipped: a Wyckoff trade is already open")
if pW.dir == 0 and rangeSig and not na(atr)
tpW = swingTp and not na(swingAtr) ? close + rangeDir * swingAtr * swingMult : rangeTp
whyW = f_try(pW, 3, rangeDir, rangeSl, tpW, volume / volAvg, na, na)
openedW := whyW == ""
if not openedW and showSkips
label.new(bar_index, rangeDir == 1 ? low : high, "x", color=color(na), textcolor=color.gray, style=rangeDir == 1 ? label.style_label_up : label.style_label_down, size=size.small, tooltip="Wyckoff " + (rangeDir == 1 ? "long" : "short") + " breakout skipped: " + whyW)
alertcondition((openedS and pS.dir == 1) or (openedW and pW.dir == 1), "Buy", "Sniper Mini: BUY")
alertcondition((openedS and pS.dir == -1) or (openedW and pW.dir == -1), "Sell", "Sniper Mini: SELL")
f_state(Pos p) => p.dir == 0 ? "flat" : (p.kind == 3 ? "range " : p.kind == 2 ? "reclaim " : p.kind == 1 ? "fade " : "trend ") + (p.dir == 1 ? "long" : "short")
// ===== Dashboard: win rate per setup =====
var table dash = table.new(position.top_right, 6, 8, bgcolor=color.new(#0B1E3A, 10), border_width=1, border_color=color.new(cMid, 50))
f_row(int r0, string k, int n, int w, float rSum) =>
table.cell(dash, 0, r0, k, text_color=cPale, text_size=size.small, text_halign=text.align_left)
table.cell(dash, 1, r0, str.tostring(n), text_color=color.white, text_size=size.small)
table.cell(dash, 2, r0, str.tostring(w), text_color=color.white, text_size=size.small)
table.cell(dash, 3, r0, n > 0 ? str.tostring(w * 100.0 / n, "#") + "%" : "-", text_color=n > 0 and w * 2 >= n ? cLight : cPale, text_size=size.small)
table.cell(dash, 4, r0, n > 0 ? str.tostring(rSum / n, "#.##") : "-", text_color=rSum > 0 ? cLight : cPale, text_size=size.small)
table.cell(dash, 5, r0, str.tostring(rSum, "#.#") + "R", text_color=rSum > 0 ? cLight : cPale, text_size=size.small)
if barstate.islast
table.cell(dash, 0, 0, "Setup", text_color=cLight, text_size=size.small)
table.cell(dash, 1, 0, "Trades", text_color=cLight, text_size=size.small)
table.cell(dash, 2, 0, "Wins", text_color=cLight, text_size=size.small)
table.cell(dash, 3, 0, "Win rate", text_color=cLight, text_size=size.small)
table.cell(dash, 4, 0, "Avg R", text_color=cLight, text_size=size.small)
table.cell(dash, 5, 0, "Net R", text_color=cLight, text_size=size.small)
f_row(1, "Sniper: Trend (TP1+)", stTrades.get(0), stWins.get(0), stR.get(0))
f_row(2, "Sniper: News fade", stTrades.get(1), stWins.get(1), stR.get(1))
f_row(3, "Sniper: News reclaim", stTrades.get(2), stWins.get(2), stR.get(2))
f_row(4, "Wyckoff: Range breakout", stTrades.get(3), stWins.get(3), stR.get(3))
f_row(5, "All", stTrades.get(0) + stTrades.get(1) + stTrades.get(2) + stTrades.get(3), stWins.get(0) + stWins.get(1) + stWins.get(2) + stWins.get(3), stR.get(0) + stR.get(1) + stR.get(2) + stR.get(3))
table.cell(dash, 0, 6, "Sniper " + f_state(pS) + " | Wyckoff " + f_state(pW) + " | bias " + (close > slow ? "long (above EMA200)" : "short (below EMA200)") + (useHtfBias and htfTF != "" ? " | HTF " + (htfDiff >= 0 ? "up" : "down") : " | HTF off"), text_color=cPale, text_size=size.small, text_halign=text.align_left)
table.merge_cells(dash, 0, 6, 5, 6)
table.cell(dash, 0, 7, "R: loss = -1R. Trend scales out 1/3 at each TP, rest at SL.", text_color=cPale, text_size=size.tiny, text_halign=text.align_left)
table.merge_cells(dash, 0, 7, 5, 7)
// ===== Entry conditions table: winners range (avg) vs losers avg =====
var table st = table.new(position.bottom_right, 9, NF + 2, bgcolor=color.new(#0B1E3A, 10), border_width=1, border_color=color.new(cMid, 50))
f_win(Agg a, int i) => a.wN.get(i) > 0 ? str.tostring(a.wMin.get(i), "#.##") + " - " + str.tostring(a.wMax.get(i), "#.##") + " (" + str.tostring(a.wSum.get(i) / a.wN.get(i), "#.##") + ")" : "-"
f_loss(Agg a, int i) => a.lN.get(i) > 0 ? str.tostring(a.lSum.get(i) / a.lN.get(i), "#.##") : "-"
f_sc(int c, int r0, string t, color col) =>
table.cell(st, c, r0, t, text_color=col, text_size=size.small, text_halign=c == 0 ? text.align_left : text.align_right)
if showStats and barstate.islast
f_sc(0, 0, "Entry condition", cLight)
f_sc(1, 0, "Trend win range (avg)", cLight)
f_sc(2, 0, "Trend loss avg", cLight)
f_sc(3, 0, "Fade win range (avg)", cLight)
f_sc(4, 0, "Fade loss avg", cLight)
f_sc(5, 0, "Reclaim win range (avg)", cLight)
f_sc(6, 0, "Reclaim loss avg", cLight)
f_sc(7, 0, "Wyckoff win range (avg)", cLight)
f_sc(8, 0, "Wyckoff loss avg", cLight)
for i = 0 to NF - 1
f_sc(0, i + 1, names.get(i), cPale)
f_sc(1, i + 1, f_win(aggT, i), color.white)
f_sc(2, i + 1, f_loss(aggT, i), cPale)
f_sc(3, i + 1, f_win(aggN, i), color.white)
f_sc(4, i + 1, f_loss(aggN, i), cPale)
f_sc(5, i + 1, f_win(aggR, i), color.white)
f_sc(6, i + 1, f_loss(aggR, i), cPale)
f_sc(7, i + 1, f_win(aggG, i), color.white)
f_sc(8, i + 1, f_loss(aggG, i), cPale)
f_sc(0, NF + 1, "Set the Dial-in Filters inside the winners' range, then re-check win rate and trade count.", cPale)
table.merge_cells(st, 0, NF + 1, 8, NF + 1)
Overview
Trend Sniper Mini combines trend entries filtered by a 200 EMA bias and higher timeframe, range breakouts from compressed ranges, and fades or reclaims after large news-style candles. Stops use ATR or the setup extreme, with targets at set multiples or the news candle end. A MATS trend line from ChartPrime is drawn for context only, plus a statistics table.
Trend, range breakout and news fade engines
EMA 200 direction bias with higher timeframe filter
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Trend Entries
bool
true
useTrend
EMA200 Direction Bias
string
Trend only
biasMode
MATS Length
int
30
matsLen
Show MATS Line
bool
true
showMats
Range Breakout Entries
bool
true
useRange
Range Length
int
20
rgLen
Compression Percentile (max)
float
40
rgComp
Calibration Lookback
int
500
rgCal
Min Midline Crossings per Bar
float
0.18
rgRot
Min Touches per Edge
int
2
rgTouch
Touch Tolerance (ATR)
float
0.10
rgTol
Max Drift
float
0.45
rgDrift
Show all 40 settings
Setting
Type
Default
Variable
Min Closes Inside
float
0.70
rgCont
Breakout Buffer (ATR)
float
0.15
rgBuf
Max Range Age (candles)
int
200
rgMaxAge
Fast EMA
int
50
fastLen
Slow EMA
int
200
slowLen
Use HTF Bias
bool
true
useHtfBias
HTF Bias Timeframe (empty = off)
timeframe
15
htfTF
Volume x Average (0 = off)
float
1.2
volMult
SL ATR Multiplier
float
1.5
atrMult
News Fade Entries
bool
true
useNews
News Candle Size (x largest prior range)
float
1.6
newsMult
News Lookback
int
20
newsLen
News Min Body %
float
40
newsBody
News Must Close Outside Bollinger
bool
true
needBB
Fade Window (candles)
int
5
fadeWait
SL Buffer beyond Tip (x ATR)
float
0.2
fadeBuf
News Reclaim Entries
bool
true
useRecl
Look for Reclaim Within (candles)
int
60
reclWindow
Consolidation Min Candles
int
3
consMin
Consolidation Max Box (x ATR)
float
3.0
consMaxAtr
Big Candle (x avg consolidation candle)
float
1.3
bigMult
Allow Triple Close Breakout
bool
true
useTriple
SL Buffer beyond Box (x ATR)
float
0.2
reclBuf
Draw Consolidation Box
bool
true
showCons
Show Skipped Wyckoff Breakouts
bool
true
showSkips
ATR-based TP
bool
true
swingTp
TP ATR Multiplier
float
3.0
swingMult
Also Use ATR TP for News Setups
bool
false
swingNews
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 adapts Michael Chalek's Dual Thrust trading concept to plot breakout levels and overlay bands directly on the price chart. It calculates lookback ranges based on historical price action and provides two algorithmic variations along with multiple
The Market Tradability Engine calculates multi-factor tradability, balance, compression, and direction scores to evaluate market regime quality. It classifies market conditions into eight distinct states, including Balance, Compression, Ready, Breakout, Trend,
This overlay indicator scores breakouts of recent highs and lows against failed-breakout checks, EMA trend alignment, RSI momentum, and an ATR volatility band. It plots green buy and red sell labels when conditions pass a minimum score, and it prints ATR-based
Greeffer's Long Study calculates Donchian Channels alongside a volume moving average filter to generate trade signals. When price touches the lower or upper channel boundaries combined with volume exceeding a custom moving average multiplier, the indicator tri
This overlay indicator plots customized asymmetrical Bollinger Bands on the price chart to detect breakout trade signals. It calculates a middle simple moving average and standard deviation bands using separate standard deviation multipliers for upper and lowe
This indicator plots a four-pole Gaussian channel on the price chart. It filters close prices into a yellow midline and filters true range to place equal green and red bands. A long marker prints when price crosses above the upper band and the daily Bitcoin cl