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.

Open Pine Editor
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.