The strategy script
Pine Script v6. Open TradingView, switch to the 4 hour chart, paste this into the Pine Editor, add it to the chart, then open the Strategy Tester to see the backtest.
sweep_bos_fib71_strategy.pine234 lines
//@version=6
// ─────────────────────────────────────────────────────────────────────────────
// Liquidity Sweep + Break of Structure + Fib 0.71 entry (4H execution)
// Daily premium / discount filter for direction confirmation
//
// How the setup works (BUY example – SELL is the mirror):
// 1. SWEEP : a 4H candle wicks below the last swing low and closes back above it.
// 2. BOS : after the sweep, a 4H candle CLOSES above the last swing high.
// 3. FIB : Fibonacci is dragged from the sweep low (1.0) to the top of the
// impulse (0). The 0 level keeps updating while price is still pushing up.
// 4. ENTRY : limit order at the 0.71 retracement.
// 5. STOP : at the 1.0 level (sweep low) + optional buffer.
// 6. TARGET : Risk:Reward multiple (default +2R).
// 7. FILTER : daily chart must be in DISCOUNT (below the daily range mid) for buys,
// and in PREMIUM (above the mid) for sells.
// 8. RETRY : if stopped out the same 0.71 level is re-armed, up to "Max entries per setup".
// The setup is cancelled if price closes beyond the sweep extreme,
// a target is hit, or too many bars pass without a fill.
//
// Use on the 4H timeframe. Run Strategy Tester for the backtest.
// ─────────────────────────────────────────────────────────────────────────────
strategy("Sweep + BOS + Fib 0.71 (4H · Daily P/D)", shorttitle = "SBF-71",
overlay = true, initial_capital = 10000,
default_qty_type = strategy.percent_of_equity, default_qty_value = 2,
commission_type = strategy.commission.percent, commission_value = 0.0,
calc_on_every_tick = false, process_orders_on_close = false,
max_lines_count = 500, max_boxes_count = 500, max_labels_count = 500)
// ── Inputs ───────────────────────────────────────────────────────────────────
grpS = "Structure"
pivLen = input.int(5, "Swing pivot length (bars each side)", minval = 2, group = grpS)
maxWaitBars = input.int(60, "Cancel setup after N bars without fill", minval = 5, group = grpS)
grpF = "Fibonacci / Risk"
fibEntry = input.float(0.71, "Entry retracement", step = 0.01, minval = 0.1, maxval = 0.99, group = grpF)
fibSL = input.float(1.0, "Stop-loss retracement", step = 0.01, minval = 0.5, maxval = 1.5, group = grpF)
slBufTicks = input.int(0, "Extra stop buffer (ticks)", minval = 0, group = grpF)
rr = input.float(2.0, "Risk : Reward", step = 0.1, minval = 0.5, group = grpF)
maxAttempts = input.int(3, "Max entries per setup", minval = 1, maxval = 10, group = grpF)
grpD = "Daily premium / discount filter"
useDaily = input.bool(true, "Enable daily filter", group = grpD)
dailyLen = input.int(20, "Daily range lookback (days)", minval = 3, group = grpD)
grpV = "Visuals"
showFib = input.bool(true, "Draw fib levels", group = grpV)
showZones = input.bool(true, "Draw TP / SL zones", group = grpV)
showLabels = input.bool(true, "Label sweep / BOS", group = grpV)
showDailyEQ = input.bool(true, "Show daily equilibrium line", group = grpV)
// ── Daily premium / discount ─────────────────────────────────────────────────
dHi = request.security(syminfo.tickerid, "D", ta.highest(high, dailyLen), lookahead = barmerge.lookahead_off)
dLo = request.security(syminfo.tickerid, "D", ta.lowest(low, dailyLen), lookahead = barmerge.lookahead_off)
dMid = (dHi + dLo) / 2
inDiscount = close < dMid
inPremium = close > dMid
buyAllowed = not useDaily or inDiscount
sellAllowed = not useDaily or inPremium
plot(showDailyEQ ? dMid : na, "Daily equilibrium", color = color.new(color.gray, 30), style = plot.style_linebr, linewidth = 1)
bgcolor(useDaily ? (inDiscount ? color.new(color.green, 96) : color.new(color.red, 96)) : na)
// ── Swing points ─────────────────────────────────────────────────────────────
ph = ta.pivothigh(high, pivLen, pivLen)
pl = ta.pivotlow(low, pivLen, pivLen)
var float lastPH = na
var float lastPL = na
float prevPH = lastPH // swing levels known BEFORE this bar's update
float prevPL = lastPL
if not na(ph)
lastPH := ph
if not na(pl)
lastPL := pl
// ── Setup state machine ──────────────────────────────────────────────────────
// stage: 0 = idle · 1 = swept, waiting for BOS · 2 = armed (fib drawn, order working / managing)
var int dir = 0
var int stage = 0
var float sweepExt = na // fib 1.0
var float bosLevel = na
var float impExt = na // fib 0
var int armedBar = na
var int attempts = 0
var bool frozen = false // fib stops updating after first fill
var float entryPx = na
var float slPx = na
var float tpPx = na
var int closedSeen = 0
var line lnZero = na, var line lnHalf = na, var line lnEntry = na, var line ln75 = na, var line lnOne = na
var label lbSweep = na, var label lbBos = na
f_clearFib() =>
line.delete(lnZero), line.delete(lnHalf), line.delete(lnEntry), line.delete(ln75), line.delete(lnOne)
f_reset() =>
strategy.cancel_all()
stage := 0, dir := 0, attempts := 0, frozen := false
sweepExt := na, bosLevel := na, impExt := na, armedBar := na
entryPx := na, slPx := na, tpPx := na
bullSweep = not na(prevPL) and low < prevPL and close > prevPL
bearSweep = not na(prevPH) and high > prevPH and close < prevPH
// 1) SWEEP detection (a new sweep overrides an unfilled / un-broken setup)
if stage <= 1 or (stage == 2 and strategy.position_size == 0 and not frozen)
if bullSweep and buyAllowed
if stage == 2
f_reset()
dir := 1, stage := 1, sweepExt := low, bosLevel := prevPH, impExt := high
if showLabels
lbSweep := label.new(bar_index, low, "lq sweep", style = label.style_label_up, color = color.new(color.red, 20), textcolor = color.white, size = size.small)
else if bearSweep and sellAllowed
if stage == 2
f_reset()
dir := -1, stage := 1, sweepExt := high, bosLevel := prevPL, impExt := low
if showLabels
lbSweep := label.new(bar_index, high, "lq sweep", style = label.style_label_down, color = color.new(color.red, 20), textcolor = color.white, size = size.small)
// 2) Waiting for BOS
if stage == 1
if dir == 1
sweepExt := math.min(sweepExt, low)
if not na(bosLevel) and close > bosLevel
stage := 2, impExt := high, armedBar := bar_index, attempts := 0, frozen := false
if showLabels
lbBos := label.new(bar_index, bosLevel, "bos", style = label.style_label_down, color = color.new(color.green, 20), textcolor = color.white, size = size.small)
else if close < sweepExt - (prevPH - prevPL) // ran far away → abandon
f_reset()
else
sweepExt := math.max(sweepExt, high)
if not na(bosLevel) and close < bosLevel
stage := 2, impExt := low, armedBar := bar_index, attempts := 0, frozen := false
if showLabels
lbBos := label.new(bar_index, bosLevel, "bos", style = label.style_label_up, color = color.new(color.green, 20), textcolor = color.white, size = size.small)
else if close > sweepExt + (prevPH - prevPL)
f_reset()
// 3) Armed: fib + orders
inPos = strategy.position_size != 0
if stage == 2
// update fib 0 while impulse still extends and nothing has filled yet
if not frozen and not inPos
impExt := dir == 1 ? math.max(impExt, high) : math.min(impExt, low)
rng = math.abs(impExt - sweepExt)
tick = syminfo.mintick
if dir == 1
entryPx := impExt - rng * fibEntry
slPx := impExt - rng * fibSL - slBufTicks * tick
tpPx := entryPx + (entryPx - slPx) * rr
else
entryPx := impExt + rng * fibEntry
slPx := impExt + rng * fibSL + slBufTicks * tick
tpPx := entryPx - (slPx - entryPx) * rr
// count finished trades of this setup
if strategy.closedtrades > closedSeen
closedSeen := strategy.closedtrades
attempts += 1
lastProfit = strategy.closedtrades.profit(strategy.closedtrades - 1)
if lastProfit > 0
f_reset()
// invalidation rules
invalid = dir == 1 ? close < sweepExt : close > sweepExt
tooLate = not inPos and not frozen and (bar_index - armedBar > maxWaitBars)
if stage == 2 and (invalid or tooLate or attempts >= maxAttempts)
f_reset()
// working order
if stage == 2 and not inPos and attempts < maxAttempts and rng > 0
if dir == 1
strategy.entry("Long", strategy.long, limit = entryPx, comment = "Buy 0.71")
else
strategy.entry("Short", strategy.short, limit = entryPx, comment = "Sell 0.71")
// exits (always attached while in a position)
if inPos
if not frozen
frozen := true
if showZones
box.new(bar_index, entryPx, bar_index + 6, tpPx, border_color = color.new(color.teal, 40), bgcolor = color.new(color.teal, 80))
box.new(bar_index, entryPx, bar_index + 6, slPx, border_color = color.new(color.red, 40), bgcolor = color.new(color.red, 80))
label.new(bar_index + 3, tpPx, "+" + str.tostring(rr, "#.#") + "R", style = strategy.position_size > 0 ? label.style_label_down : label.style_label_up, color = color.new(color.teal, 100), textcolor = color.teal, size = size.small)
if strategy.position_size > 0
strategy.exit("Long X", "Long", stop = slPx, limit = tpPx)
else
strategy.exit("Short X", "Short", stop = slPx, limit = tpPx)
// ── Fib drawing ──────────────────────────────────────────────────────────────
if showFib
if stage == 2 and not na(impExt) and not na(sweepExt)
rngD = math.abs(impExt - sweepExt)
sgn = dir == 1 ? -1 : 1
y0 = impExt
y5 = impExt + sgn * rngD * 0.5
yE = impExt + sgn * rngD * fibEntry
y75 = impExt + sgn * rngD * 0.75
y1 = sweepExt
x1 = armedBar, x2 = bar_index + 10
if na(lnZero)
lnZero := line.new(x1, y0, x2, y0, color = color.black, width = 1)
lnHalf := line.new(x1, y5, x2, y5, color = color.black, width = 1)
lnEntry := line.new(x1, yE, x2, yE, color = color.new(color.black, 0), width = 2)
ln75 := line.new(x1, y75, x2, y75, color = color.new(color.gray, 20), width = 1)
lnOne := line.new(x1, y1, x2, y1, color = color.black, width = 1)
else
line.set_xy1(lnZero, x1, y0), line.set_xy2(lnZero, x2, y0)
line.set_xy1(lnHalf, x1, y5), line.set_xy2(lnHalf, x2, y5)
line.set_xy1(lnEntry, x1, yE), line.set_xy2(lnEntry, x2, yE)
line.set_xy1(ln75, x1, y75), line.set_xy2(ln75, x2, y75)
line.set_xy1(lnOne, x1, y1), line.set_xy2(lnOne, x2, y1)
else if not na(lnZero)
// keep the finished fib on chart but stop extending it
lnZero := na, lnHalf := na, lnEntry := na, ln75 := na, lnOne := na
// ── Info panel ───────────────────────────────────────────────────────────────
var table info = table.new(position.top_right, 2, 6, bgcolor = color.new(color.white, 10), border_width = 1)
if barstate.islast
table.cell(info, 0, 0, "Daily bias", text_color = color.black), table.cell(info, 1, 0, useDaily ? (inDiscount ? "DISCOUNT (buys)" : "PREMIUM (sells)") : "off", text_color = inDiscount ? color.green : color.red)
table.cell(info, 0, 1, "Stage", text_color = color.black), table.cell(info, 1, 1, stage == 0 ? "idle" : stage == 1 ? "swept → wait BOS" : "armed @ 0.71", text_color = color.black)
table.cell(info, 0, 2, "Direction", text_color = color.black), table.cell(info, 1, 2, dir == 1 ? "BUY" : dir == -1 ? "SELL" : "-", text_color = dir == 1 ? color.green : dir == -1 ? color.red : color.black)
table.cell(info, 0, 3, "Entry / SL / TP", text_color = color.black), table.cell(info, 1, 3, stage == 2 ? str.tostring(entryPx, format.mintick) + " / " + str.tostring(slPx, format.mintick) + " / " + str.tostring(tpPx, format.mintick) : "-", text_color = color.black)
table.cell(info, 0, 4, "Attempts", text_color = color.black), table.cell(info, 1, 4, str.tostring(attempts) + " / " + str.tostring(maxAttempts), text_color = color.black)
table.cell(info, 0, 5, "Win rate", text_color = color.black), table.cell(info, 1, 5, strategy.closedtrades > 0 ? str.tostring(100 * strategy.wintrades / strategy.closedtrades, "#.#") + "% (" + str.tostring(strategy.closedtrades) + " trades)" : "-", text_color = color.black)
// ── Alerts ───────────────────────────────────────────────────────────────────
alertcondition(stage == 2 and stage[1] != 2, "Setup armed", "SBF-71: BOS confirmed, limit order placed at 0.71")
alertcondition(bullSweep and buyAllowed, "Bullish sweep", "SBF-71: bullish liquidity sweep in daily discount")
alertcondition(bearSweep and sellAllowed, "Bearish sweep", "SBF-71: bearish liquidity sweep in daily premium")
Settings that matter
- Swing pivot length — larger values only count bigger swings, so fewer but cleaner sweeps.
- Entry retracement — 0.71 by default; test 0.618 and 0.79 against it.
- Risk : Reward — 2.0 default target multiple.
- Max entries per setup — 3, matching the three re-arms on the level.
- Enable daily filter — turn it off to measure how much the premium/discount rule is actually adding.