Indicator · Pine Script v6 · On the chart

Trend Sniper Mini

by kasutu

A multi-engine indicator that marks historical trade setups on the price chart.

View on GitHub

Repaints

GitHub stars1on the repository
Pine versionv6current
Lines of code583
Settings40inputs
LicenceNone statedfile: MPL-2.0
Last update2 weeks ago12 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 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.

Details

Published by

kasutu on GitHub

Draws

On the price chart

Markets

Any symbol

Timeframes

M15-H1

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Trend EntriesbooltrueuseTrend
EMA200 Direction BiasstringTrend onlybiasMode
MATS Lengthint30matsLen
Show MATS LinebooltrueshowMats
Range Breakout EntriesbooltrueuseRange
Range Lengthint20rgLen
Compression Percentile (max)float40rgComp
Calibration Lookbackint500rgCal
Min Midline Crossings per Barfloat0.18rgRot
Min Touches per Edgeint2rgTouch
Touch Tolerance (ATR)float0.10rgTol
Max Driftfloat0.45rgDrift
Show all 40 settings
SettingTypeDefaultVariable
Min Closes Insidefloat0.70rgCont
Breakout Buffer (ATR)float0.15rgBuf
Max Range Age (candles)int200rgMaxAge
Fast EMAint50fastLen
Slow EMAint200slowLen
Use HTF BiasbooltrueuseHtfBias
HTF Bias Timeframe (empty = off)timeframe15htfTF
Volume x Average (0 = off)float1.2volMult
SL ATR Multiplierfloat1.5atrMult
News Fade EntriesbooltrueuseNews
News Candle Size (x largest prior range)float1.6newsMult
News Lookbackint20newsLen
News Min Body %float40newsBody
News Must Close Outside BollingerbooltrueneedBB
Fade Window (candles)int5fadeWait
SL Buffer beyond Tip (x ATR)float0.2fadeBuf
News Reclaim EntriesbooltrueuseRecl
Look for Reclaim Within (candles)int60reclWindow
Consolidation Min Candlesint3consMin
Consolidation Max Box (x ATR)float3.0consMaxAtr
Big Candle (x avg consolidation candle)float1.3bigMult
Allow Triple Close BreakoutbooltrueuseTriple
SL Buffer beyond Box (x ATR)float0.2reclBuf
Draw Consolidation BoxbooltrueshowCons
Show Skipped Wyckoff BreakoutsbooltrueshowSkips
ATR-based TPbooltrueswingTp
TP ATR Multiplierfloat3.0swingMult
Also Use ATR TP for News SetupsboolfalseswingNews
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.

Similar Pine Scripts

114 stars

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

Indicator v4 TrendBreakout Repaints
Updated 1 year ago · None stated GitHub repository of Dual Thrust Trading Algorithm

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,

Indicator v6 TrendVolatility & bands
Updated 2 days ago · MIT GitHub repository of Market Tradability Engine [WavesUnchained]

GainzAlgo V2 Alpha

by Bakomebandias
8 stars

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

Indicator v6 TrendOscillators & momentum
Updated 6 months ago · None stated GitHub repository of GainzAlgo V2 Alpha
3 stars

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

Indicator v4 TrendVolatility & bands
Updated 4 years ago · CC0-1.0 GitHub repository of Greeffer's Long Study

Bollinger Bands

by Web3Degenerate
2 stars

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

Indicator v5 Volatility & bandsBreakout
Updated 2 years ago · None stated GitHub repository of Bollinger Bands

More Trend scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers