本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。
//@version=6
// ══════════════════════════════════════════════════════════
// SMART BREAKOUT TARGETS [WillyAlgoTrader]
// ══════════════════════════════════════════════════════════
// Author: Willy | WillyAlgoTrader
// Version: 1.2.0
indicator(
title = "Smart Breakout Targets [WillyAlgoTrader]",
shorttitle = "Smart Breakout",
overlay = true,
max_lines_count = 500,
max_labels_count = 500,
max_boxes_count = 500,
max_bars_back = 5000)
// ══════════════════════════════════════
// CONSTANTS
// ══════════════════════════════════════
GRP_MAIN = "⚙️ Main Settings"
GRP_TARGETS = "🎯 Targets"
GRP_FILTER = "🔍 Filters"
GRP_VISUAL = "🎨 Visual Settings"
GRP_DASH = "📊 Dashboard"
GRP_ALERT = "🔔 Alerts"
GRP_COLORS = "🎨 Colors"
INDICATOR_VERSION = "v1.2.0"
// ══════════════════════════════════════
// INPUTS
// ══════════════════════════════════════
// ── Main Settings ───────────────────────────────────────
squeezeLengthInput = input.int(20, "Squeeze Detection Length", minval = 10, maxval = 100, group = GRP_MAIN, tooltip = "Lookback period for Bollinger Band Width and ATR compression detection.\n• Higher = detects longer consolidations, fewer signals\n• Lower = catches short squeezes, more signals\n• Recommended: 15–30")
bbMultInput = input.float(2.0, "BB Multiplier", minval = 1.0, maxval = 4.0, step = 0.1, group = GRP_MAIN, tooltip = "Bollinger Band standard deviation multiplier.\n• Higher = wider bands, stricter squeeze detection\n• Lower = tighter bands, more squeezes detected\n• Recommended: 1.5–2.5")
squeezeThreshInput = input.float(0.6, "Squeeze Threshold", minval = 0.1, maxval = 1.0, step = 0.05, group = GRP_MAIN, tooltip = "BB Width percentile threshold for squeeze detection.\nWhen BB Width falls below this fraction of its SMA, a squeeze is active.\n• Lower = stricter, catches only tight squeezes\n• Higher = looser, more ranges detected\n• Recommended: 0.4–0.7")
atrCompressInput = input.float(0.75, "ATR Compression Ratio", minval = 0.3, maxval = 1.0, step = 0.05, group = GRP_MAIN, tooltip = "ATR must be below this fraction of its SMA for squeeze confirmation.\nDual-engine: both BB squeeze AND ATR compression must agree.\n• Lower = stricter compression filter\n• Higher = allows moderate volatility squeezes\n• Recommended: 0.6–0.85")
minSqueezeInput = input.int(5, "Min Squeeze Bars", minval = 2, maxval = 50, group = GRP_MAIN, tooltip = "Minimum consecutive bars in squeeze to form a valid range.\nFilters out momentary dips in volatility.\n• Higher = only well-established ranges\n• Lower = catches quick consolidations\n• Recommended: 4–10")
impulseMultInput = input.float(0.8, "Impulse Body Threshold", minval = 0.2, maxval = 3.0, step = 0.1, group = GRP_MAIN, tooltip = "Breakout candle body must exceed ATR × this multiplier.\nConfirms the breakout has genuine momentum.\n• Higher = stricter, fewer but stronger breakouts\n• Lower = more breakouts, includes weaker ones\n• Recommended: 0.5–1.2")
preventOverlapInput = input.bool(true, "Prevent Overlap", group = GRP_MAIN, tooltip = "If enabled, prevents new range boxes from overlapping with existing ones.")
// ── Targets ─────────────────────────────────────────────
showTargetsInput = input.bool(true, "Show Entry/SL/TP Levels", group = GRP_TARGETS, tooltip = "Toggle visibility of entry, stop loss, and take profit levels after breakout.")
slBufferInput = input.float(0.5, "SL ATR Buffer", minval = 0.0, maxval = 3.0, step = 0.1, group = GRP_TARGETS, tooltip = "Extra ATR-based buffer added beyond the opposite range boundary for SL.\n• Higher = wider stop, more room to breathe\n• Lower = tighter stop, better R:R but more stops\n• Recommended: 0.3–1.0")
tp1RRInput = input.float(1.0, "TP1 Risk:Reward", minval = 0.1, maxval = 5.0, step = 0.1, group = GRP_TARGETS, tooltip = "TP1 = Entry + Risk × this multiplier.\n• 1.0 = 1:1 R:R\n• Recommended: 0.75–1.5")
tp2RRInput = input.float(2.0, "TP2 Risk:Reward", minval = 0.5, maxval = 10.0, step = 0.1, group = GRP_TARGETS, tooltip = "TP2 = Entry + Risk × this multiplier.\n• 2.0 = 1:2 R:R\n• Recommended: 1.5–3.0")
tp3RRInput = input.float(3.0, "TP3 Risk:Reward", minval = 1.0, maxval = 15.0, step = 0.1, group = GRP_TARGETS, tooltip = "TP3 = Entry + Risk × this multiplier.\n• 3.0 = 1:3 R:R\n• Recommended: 2.5–5.0")
// ── Filters ─────────────────────────────────────────────
useVolFilterInput = input.bool(false, "Volume Breakout Filter", group = GRP_FILTER, tooltip = "Require above-average volume on breakout candle.\nDisabled by default (works on forex where volume = 0).\nEnable for stocks/crypto for cleaner signals.")
volMultInput = input.float(1.5, "Volume Surge Multiplier", minval = 1.0, maxval = 5.0, step = 0.1, group = GRP_FILTER, tooltip = "Breakout bar volume must exceed SMA(volume,20) × this multiplier.\n• Higher = stricter volume confirmation\n• Recommended: 1.2–2.0")
useHtfInput = input.bool(false, "HTF Trend Filter", group = GRP_FILTER, tooltip = "Only take breakouts aligned with the higher timeframe trend.\nBullish breakout = HTF uptrend, Bearish breakout = HTF downtrend.")
htfInput = input.timeframe("D", "Higher Timeframe", group = GRP_FILTER, tooltip = "Timeframe for trend bias filter.\nLeave at 'D' for daily trend on intraday charts.\n• Use 4H on 15M chart, D on 1H chart, W on D chart")
htfMaLenInput = input.int(50, "HTF MA Length", minval = 10, maxval = 200, group = GRP_FILTER, tooltip = "Moving average length on the HTF for trend determination.\n• Recommended: 50 (standard)")
// ── Visual Settings ─────────────────────────────────────
showBoxesInput = input.bool(true, "Show Range Boxes", group = GRP_VISUAL, tooltip = "Show consolidation range boxes on chart.")
showSignalsInput = input.bool(true, "Show Breakout Signals", group = GRP_VISUAL, tooltip = "Show breakout arrow labels.")
showCenterInput = input.bool(true, "Show Range Centerline", group = GRP_VISUAL, tooltip = "Show dashed centerline inside range boxes.")
showBgInput = input.bool(false, "Show Squeeze Background", group = GRP_VISUAL, tooltip = "Highlight background when squeeze is active.")
showCloseLabelsInput = input.bool(true, "Show Close Labels", group = GRP_VISUAL, tooltip = "Show ✔/✘ labels when trade closes at TP3 or SL.")
signalSizeInput = input.string("Small", "Signal Label Size", options = ["Auto", "Tiny", "Small", "Normal", "Large", "Huge"], group = GRP_VISUAL, tooltip = "Size of Long/Short breakout labels on chart.\n• Small: compact, good for lower TFs with many signals\n• Normal: balanced visibility\n• Large: easy to spot on higher TFs")
targetSizeInput = input.string("Small", "Target Label Size", options = ["Auto", "Tiny", "Small", "Normal", "Large", "Huge"], group = GRP_VISUAL, tooltip = "Size of Entry/SL/TP labels on target lines.\n• Small: minimal, keeps chart clean\n• Normal: easy to read prices\n• Tiny: maximum compactness")
closeSizeInput = input.string("Small", "Close Label Size", options = ["Auto", "Tiny", "Small", "Normal", "Large", "Huge"], group = GRP_VISUAL, tooltip = "Size of ✔ TP3 / ✘ SL close labels.\n• Matches signal labels by default")
showWatermarkInput = input.bool(true, "Show Watermark", group = GRP_VISUAL, tooltip = "Display 'WillyAlgoTrader - by Willy' watermark on chart.")
themeInput = input.string("Auto", "Theme", options = ["Auto", "Dark", "Light"], group = GRP_VISUAL, tooltip = "Chart color theme.\n• Auto: detects from chart background\n• Dark: optimized for dark backgrounds\n• Light: optimized for light/white backgrounds")
// ── Dashboard ───────────────────────────────────────────
showDashInput = input.bool(true, "Show Dashboard", group = GRP_DASH, tooltip = "Show info dashboard on chart.")
dashPosStr = input.string("Top Right", "Dashboard Position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = GRP_DASH, tooltip = "Position of the dashboard panel on chart.")
// ── Alerts ──────────────────────────────────────────────
webhookInput = input.bool(false, "Webhook JSON Format", group = GRP_ALERT, tooltip = "Send alerts in JSON format for webhook integrations (3Commas, Autoview, etc.)")
// ── Colors ──────────────────────────────────────────────
bullColorInput = input.color(#00E676, "Bull", inline = "colors", group = GRP_COLORS, tooltip = "Color for bullish breakouts and targets.")
bearColorInput = input.color(#FF5252, "Bear", inline = "colors", group = GRP_COLORS, tooltip = "Color for bearish breakouts and targets.")
neutralColorInput = input.color(#FFEB3B, "Neutral", inline = "colors", group = GRP_COLORS, tooltip = "Color for neutral/squeeze state.")
// ══════════════════════════════════════
// THEME DETECTION & COLORS
// ══════════════════════════════════════
isDark = switch themeInput
"Dark" => true
"Light" => false
=> color.r(chart.bg_color) < 128
TEXT_COLOR = isDark ? #E0E0E0 : #1A1A1A
TEXT_MUTED = isDark ? color.new(#9E9E9E, 0) : color.new(#757575, 0)
TABLE_BG = isDark ? color.new(#131722, 5) : color.new(#FFFFFF, 5)
TABLE_BORDER = isDark ? color.new(#2A2E39, 0) : color.new(#D0D0D0, 0)
TABLE_ROW_ALT = isDark ? color.new(#1C2030, 0) : color.new(#F0F4F8, 0)
HEADER_BG = color.new(#2962FF, 0)
LINE_SUBTLE = isDark ? color.new(#555555, 0) : color.new(#AAAAAA, 0)
RANGE_BG = isDark ? color.new(#FFFFFF, 92) : color.new(#000000, 92)
WM_COLOR = isDark ? color.new(#FFFFFF, 80) : color.new(#000000, 80)
LABEL_TEXT = #FFFFFF
// ══════════════════════════════════════
// FUNCTIONS
// ══════════════════════════════════════
safeDiv(float num, float den, float fallback = 0.0) =>
den != 0 and not na(num) and not na(den) ? num / den : fallback
// Convert string input to size constant
toSize(string s) =>
switch s
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
"Huge" => size.huge
=> size.auto
// Resolved label sizes
SIGNAL_SIZE = toSize(signalSizeInput)
TARGET_SIZE = toSize(targetSizeInput)
CLOSE_SIZE = toSize(closeSizeInput)
// ══════════════════════════════════════
// DASHBOARD POSITION
// ══════════════════════════════════════
dashPos = switch dashPosStr
"Top Left" => position.top_left
"Top Right" => position.top_right
"Bottom Left" => position.bottom_left
"Bottom Right" => position.bottom_right
=> position.top_right
// ══════════════════════════════════════
// CALCULATIONS
// ══════════════════════════════════════
int WARMUP_BARS = math.max(squeezeLengthInput, 50)
bool isWarmedUp = bar_index >= WARMUP_BARS
// ── ATR ─────────────────────────────────────────────────
int atrPeriod = math.max(int(squeezeLengthInput / 2), 7)
float rawAtr = ta.atr(atrPeriod)
float atrVal = nz(rawAtr, 0.0)
float atrSma = nz(ta.sma(rawAtr, squeezeLengthInput), atrVal)
// ── Bollinger Band Width ────────────────────────────────
float bbBasis = nz(ta.sma(close, squeezeLengthInput), close)
float bbStdev = nz(ta.stdev(close, squeezeLengthInput), 0.0)
float bbUpper = bbBasis + bbMultInput * bbStdev
float bbLower = bbBasis - bbMultInput * bbStdev
float bbWidth = safeDiv(bbUpper - bbLower, bbBasis)
float bbWidthSma = nz(ta.sma(bbWidth, squeezeLengthInput), bbWidth)
// ── Squeeze Detection (dual engine) ─────────────────────
bool bbSqueeze = bbWidth < bbWidthSma * squeezeThreshInput
bool atrCompress = atrSma != 0 ? atrVal < atrSma * atrCompressInput : false
bool isSqueeze = bbSqueeze and atrCompress and isWarmedUp
// ── Squeeze duration counter ────────────────────────────
var int squeezeBars = 0
squeezeBars := isSqueeze ? squeezeBars + 1 : 0
bool validSqueeze = squeezeBars >= minSqueezeInput
// ── Adaptive Donchian boundaries during squeeze ─────────
var float squeezeHigh = na
var float squeezeLow = na
var int squeezeStartBar = na
if isSqueeze and not isSqueeze[1]
squeezeHigh := high
squeezeLow := low
squeezeStartBar := bar_index
if isSqueeze
squeezeHigh := math.max(nz(squeezeHigh, high), high)
squeezeLow := math.min(nz(squeezeLow, low), low)
if not isSqueeze and isSqueeze[1]
squeezeHigh := na
squeezeLow := na
// ── Volume filter ───────────────────────────────────────
bool hasVolume = nz(volume, 0) > 0
float volSma = hasVolume ? nz(ta.sma(volume, 20), volume) : 0.0
bool volConfirm = useVolFilterInput ? (hasVolume ? volume > volSma * volMultInput : true) : true
// ── HTF Trend Filter (non-repainting) ───────────────────
float htfMa = request.security(syminfo.tickerid, htfInput, ta.sma(close, htfMaLenInput)[1], barmerge.gaps_off, barmerge.lookahead_on)
float safeHtfMa = nz(htfMa, close)
float htfClose = request.security(syminfo.tickerid, htfInput, close[1], barmerge.gaps_off, barmerge.lookahead_on)
float safeHtfClose = nz(htfClose, close)
bool htfBullish = safeHtfClose > safeHtfMa
bool htfBearish = safeHtfClose < safeHtfMa
bool htfBullOk = useHtfInput ? htfBullish : true
bool htfBearOk = useHtfInput ? htfBearish : true
// ── Impulse candle detection ────────────────────────────
float bodySize = math.abs(close - open)
bool isBullCandle = close > open
bool isBearCandle = close < open
bool impulseUp = isBullCandle and bodySize > atrVal * impulseMultInput
bool impulseDown = isBearCandle and bodySize > atrVal * impulseMultInput
// ══════════════════════════════════════
// RANGE BOX MANAGEMENT
// ══════════════════════════════════════
var array<box> rangeBoxes = array.new_box()
var array<box> resistBoxes = array.new_box()
var array<box> supportBoxes = array.new_box()
var array<line> centerLines = array.new_line()
var int lastBoxRight = 0
if rangeBoxes.size() > 0
lastBoxRight := rangeBoxes.first().get_right()
// ── Create new range box when squeeze ends ──────────────
bool squeezeEnd = not isSqueeze and isSqueeze[1] and validSqueeze[1] and isWarmedUp and barstate.isconfirmed
bool canCreate = preventOverlapInput ? nz(squeezeStartBar, 0) > lastBoxRight : true
float sqzHighSnap = squeezeHigh[1]
float sqzLowSnap = squeezeLow[1]
if squeezeEnd and canCreate and not na(sqzHighSnap) and not na(sqzLowSnap) and sqzHighSnap > sqzLowSnap
int startBar = nz(squeezeStartBar[1], bar_index - minSqueezeInput)
float rangeSpan = sqzHighSnap - sqzLowSnap
float rangeCenterV = (sqzHighSnap + sqzLowSnap) / 2.0
float maxRangeSpan = atrVal * 6.0
float fHigh = rangeSpan > maxRangeSpan and atrVal > 0 ? rangeCenterV + atrVal * 3.0 : sqzHighSnap
float fLow = rangeSpan > maxRangeSpan and atrVal > 0 ? rangeCenterV - atrVal * 3.0 : sqzLowSnap
rangeBoxes.unshift(box.new(startBar, fHigh, bar_index, fLow, bgcolor = showBoxesInput ? RANGE_BG : na, border_color = showBoxesInput ? LINE_SUBTLE : na, border_width = showBoxesInput ? 1 : 0))
float rZoneBot = fHigh - atrVal * 0.5
resistBoxes.unshift(box.new(startBar, fHigh, bar_index, rZoneBot, bgcolor = showBoxesInput ? color.new(bearColorInput, 85) : na, border_color = na))
float sZoneTop = fLow + atrVal * 0.5
supportBoxes.unshift(box.new(startBar, sZoneTop, bar_index, fLow, bgcolor = showBoxesInput ? color.new(bullColorInput, 85) : na, border_color = na))
float mid = (fHigh + fLow) / 2.0
centerLines.unshift(line.new(startBar, mid, bar_index, mid, color = showBoxesInput and showCenterInput ? LINE_SUBTLE : na, width = 1, style = line.style_dashed))
// ══════════════════════════════════════
// TRADE STATE
// ══════════════════════════════════════
var line entryLine = na
var line slLine = na
var line tp1Line = na
var line tp2Line = na
var line tp3Line = na
var label entryLbl = na
var label slLbl = na
var label tp1Lbl = na
var label tp2Lbl = na
var label tp3Lbl = na
var linefill lfRisk = na
var linefill lfReward = na
var int tradeDir = 0 // 0 = no trade, 1 = long, -1 = short
var bool tradeActive = false // true while trade is open
var float lastEntry = na
var float lastSL = na
var float lastTP1 = na
var float lastTP2 = na
var float lastTP3 = na
// ── Trade outcome tracking ──────────────────────────────
var string lastResult = "—" // "Win (TP3)" / "Loss (SL)" / "—"
var color lastResultClr = color.gray
var float lastPnl = na // P&L in price units
bool bullBreak = false
bool bearBreak = false
// ══════════════════════════════════════
// BREAKOUT DETECTION
// ══════════════════════════════════════
if rangeBoxes.size() > 0 and barstate.isconfirmed and isWarmedUp
for i = rangeBoxes.size() - 1 to 0
box b = rangeBoxes.get(i)
float bTop = b.get_top()
float bBot = b.get_bottom()
if close > bTop and impulseUp and volConfirm and htfBullOk
// ── Bullish Breakout ────────────────────────────
bullBreak := true
tradeDir := 1
tradeActive := true
float entryPrice = close
float slPrice = bBot - atrVal * slBufferInput
float risk = math.abs(entryPrice - slPrice)
float tp1Price = entryPrice + risk * tp1RRInput
float tp2Price = entryPrice + risk * tp2RRInput
float tp3Price = entryPrice + risk * tp3RRInput
lastEntry := entryPrice
lastSL := slPrice
lastTP1 := tp1Price
lastTP2 := tp2Price
lastTP3 := tp3Price
lastResult := "Active"
lastResultClr := bullColorInput
lastPnl := na
rangeBoxes.remove(i)
resistBoxes.remove(i)
supportBoxes.remove(i)
centerLines.remove(i)
if showTargetsInput
line.delete(entryLine)
line.delete(slLine)
line.delete(tp1Line)
line.delete(tp2Line)
line.delete(tp3Line)
label.delete(entryLbl)
label.delete(slLbl)
label.delete(tp1Lbl)
label.delete(tp2Lbl)
label.delete(tp3Lbl)
linefill.delete(lfRisk)
linefill.delete(lfReward)
entryLine := line.new(bar_index, entryPrice, bar_index, entryPrice, color = bullColorInput, width = 2)
entryLbl := label.new(bar_index, entryPrice, "Entry " + str.tostring(entryPrice, format.mintick), style = label.style_label_left, color = bullColorInput, textcolor = LABEL_TEXT, size = TARGET_SIZE)
slLine := line.new(bar_index, slPrice, bar_index, slPrice, color = color.new(bearColorInput, 50), width = 2, style = line.style_dashed)
slLbl := label.new(bar_index, slPrice, "SL " + str.tostring(slPrice, format.mintick), style = label.style_label_left, color = color.new(bearColorInput, 50), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp1Line := line.new(bar_index, tp1Price, bar_index, tp1Price, color = color.new(bullColorInput, 60), width = 1, style = line.style_dotted)
tp1Lbl := label.new(bar_index, tp1Price, "TP1 " + str.tostring(tp1Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 60), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp2Line := line.new(bar_index, tp2Price, bar_index, tp2Price, color = color.new(bullColorInput, 50), width = 1, style = line.style_dotted)
tp2Lbl := label.new(bar_index, tp2Price, "TP2 " + str.tostring(tp2Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 50), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp3Line := line.new(bar_index, tp3Price, bar_index, tp3Price, color = color.new(bullColorInput, 40), width = 1, style = line.style_dotted)
tp3Lbl := label.new(bar_index, tp3Price, "TP3 " + str.tostring(tp3Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 40), textcolor = LABEL_TEXT, size = TARGET_SIZE)
lfRisk := linefill.new(entryLine, slLine, color.new(bearColorInput, 93))
lfReward := linefill.new(entryLine, tp3Line, color.new(bullColorInput, 93))
break
else if close < bBot and impulseDown and volConfirm and htfBearOk
// ── Bearish Breakout ────────────────────────────
bearBreak := true
tradeDir := -1
tradeActive := true
float entryPrice = close
float slPrice = bTop + atrVal * slBufferInput
float risk = math.abs(slPrice - entryPrice)
float tp1Price = entryPrice - risk * tp1RRInput
float tp2Price = entryPrice - risk * tp2RRInput
float tp3Price = entryPrice - risk * tp3RRInput
lastEntry := entryPrice
lastSL := slPrice
lastTP1 := tp1Price
lastTP2 := tp2Price
lastTP3 := tp3Price
lastResult := "Active"
lastResultClr := bearColorInput
lastPnl := na
rangeBoxes.remove(i)
resistBoxes.remove(i)
supportBoxes.remove(i)
centerLines.remove(i)
if showTargetsInput
line.delete(entryLine)
line.delete(slLine)
line.delete(tp1Line)
line.delete(tp2Line)
line.delete(tp3Line)
label.delete(entryLbl)
label.delete(slLbl)
label.delete(tp1Lbl)
label.delete(tp2Lbl)
label.delete(tp3Lbl)
linefill.delete(lfRisk)
linefill.delete(lfReward)
entryLine := line.new(bar_index, entryPrice, bar_index, entryPrice, color = bearColorInput, width = 2)
entryLbl := label.new(bar_index, entryPrice, "Entry " + str.tostring(entryPrice, format.mintick), style = label.style_label_left, color = bearColorInput, textcolor = LABEL_TEXT, size = TARGET_SIZE)
slLine := line.new(bar_index, slPrice, bar_index, slPrice, color = color.new(bearColorInput, 50), width = 2, style = line.style_dashed)
slLbl := label.new(bar_index, slPrice, "SL " + str.tostring(slPrice, format.mintick), style = label.style_label_left, color = color.new(bearColorInput, 50), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp1Line := line.new(bar_index, tp1Price, bar_index, tp1Price, color = color.new(bullColorInput, 60), width = 1, style = line.style_dotted)
tp1Lbl := label.new(bar_index, tp1Price, "TP1 " + str.tostring(tp1Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 60), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp2Line := line.new(bar_index, tp2Price, bar_index, tp2Price, color = color.new(bullColorInput, 50), width = 1, style = line.style_dotted)
tp2Lbl := label.new(bar_index, tp2Price, "TP2 " + str.tostring(tp2Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 50), textcolor = LABEL_TEXT, size = TARGET_SIZE)
tp3Line := line.new(bar_index, tp3Price, bar_index, tp3Price, color = color.new(bullColorInput, 40), width = 1, style = line.style_dotted)
tp3Lbl := label.new(bar_index, tp3Price, "TP3 " + str.tostring(tp3Price, format.mintick), style = label.style_label_left, color = color.new(bullColorInput, 40), textcolor = LABEL_TEXT, size = TARGET_SIZE)
lfRisk := linefill.new(entryLine, slLine, color.new(bearColorInput, 93))
lfReward := linefill.new(entryLine, tp3Line, color.new(bullColorInput, 93))
break
else
b.set_right(bar_index)
resistBoxes.get(i).set_right(bar_index)
supportBoxes.get(i).set_right(bar_index)
centerLines.get(i).set_x2(bar_index)
// Extend remaining boxes after breakout
if (bullBreak or bearBreak) and rangeBoxes.size() > 0
for j = 0 to rangeBoxes.size() - 1
rangeBoxes.get(j).set_right(bar_index)
resistBoxes.get(j).set_right(bar_index)
supportBoxes.get(j).set_right(bar_index)
centerLines.get(j).set_x2(bar_index)
// ══════════════════════════════════════
// TP/SL HIT DETECTION & TRADE CLOSE
// ══════════════════════════════════════
bool tp1HitCond = false
bool tp2HitCond = false
bool tp3HitCond = false
bool slHitCond = false
bool tradeClosed = false
// Only check hits if trade is active and no new breakout on this bar
if tradeActive and not bullBreak and not bearBreak and barstate.isconfirmed
if tradeDir == 1 and not na(lastTP1)
// ── Long trade hit checks ───────────────────────
if high >= lastTP1 and high[1] < lastTP1
tp1HitCond := true
if not na(lastTP2) and high >= lastTP2 and high[1] < lastTP2
tp2HitCond := true
// ── TP3 HIT → CLOSE TRADE (WIN) ────────────────
if not na(lastTP3) and high >= lastTP3 and high[1] < lastTP3
tp3HitCond := true
tradeClosed := true
lastResult := "Win (TP3)"
lastResultClr := bullColorInput
lastPnl := lastTP3 - lastEntry
// ── SL HIT → CLOSE TRADE (LOSS) ────────────────
if not na(lastSL) and low <= lastSL and low[1] > lastSL
slHitCond := true
tradeClosed := true
lastResult := "Loss (SL)"
lastResultClr := bearColorInput
lastPnl := lastSL - lastEntry
if tradeDir == -1 and not na(lastTP1)
// ── Short trade hit checks ──────────────────────
if low <= lastTP1 and low[1] > lastTP1
tp1HitCond := true
if not na(lastTP2) and low <= lastTP2 and low[1] > lastTP2
tp2HitCond := true
// ── TP3 HIT → CLOSE TRADE (WIN) ────────────────
if not na(lastTP3) and low <= lastTP3 and low[1] > lastTP3
tp3HitCond := true
tradeClosed := true
lastResult := "Win (TP3)"
lastResultClr := bullColorInput
lastPnl := lastEntry - lastTP3
// ── SL HIT → CLOSE TRADE (LOSS) ────────────────
if not na(lastSL) and high >= lastSL and high[1] < lastSL
slHitCond := true
tradeClosed := true
lastResult := "Loss (SL)"
lastResultClr := bearColorInput
lastPnl := lastEntry - lastSL
// ── Execute trade close: delete visuals, reset state ────
if tradeClosed
// Delete all target lines and labels
if showTargetsInput
line.delete(entryLine)
line.delete(slLine)
line.delete(tp1Line)
line.delete(tp2Line)
line.delete(tp3Line)
label.delete(entryLbl)
label.delete(slLbl)
label.delete(tp1Lbl)
label.delete(tp2Lbl)
label.delete(tp3Lbl)
linefill.delete(lfRisk)
linefill.delete(lfReward)
entryLine := na
slLine := na
tp1Line := na
tp2Line := na
tp3Line := na
entryLbl := na
slLbl := na
tp1Lbl := na
tp2Lbl := na
tp3Lbl := na
lfRisk := na
lfReward := na
// Place close label on chart
if showCloseLabelsInput
if tp3HitCond
label.new(bar_index, tradeDir == 1 ? high : low, "✔ TP3",
color = bullColorInput, textcolor = LABEL_TEXT,
style = tradeDir == 1 ? label.style_label_down : label.style_label_up,
size = CLOSE_SIZE,
tooltip = "Trade closed at TP3\nEntry: " + str.tostring(lastEntry, format.mintick) + "\nTP3: " + str.tostring(lastTP3, format.mintick) + "\nP&L: " + str.tostring(nz(lastPnl, 0.0), format.mintick))
if slHitCond
label.new(bar_index, tradeDir == 1 ? low : high, "✘ SL",
color = bearColorInput, textcolor = LABEL_TEXT,
style = tradeDir == 1 ? label.style_label_up : label.style_label_down,
size = CLOSE_SIZE,
tooltip = "Trade stopped out\nEntry: " + str.tostring(lastEntry, format.mintick) + "\nSL: " + str.tostring(lastSL, format.mintick) + "\nP&L: " + str.tostring(nz(lastPnl, 0.0), format.mintick))
// Reset trade state
tradeActive := false
tradeDir := 0
// ── Extend target lines while trade is active ───────────
if tradeActive and not bullBreak and not bearBreak and showTargetsInput
line.set_x2(entryLine, bar_index)
label.set_x(entryLbl, bar_index)
line.set_x2(slLine, bar_index)
label.set_x(slLbl, bar_index)
line.set_x2(tp1Line, bar_index)
label.set_x(tp1Lbl, bar_index)
line.set_x2(tp2Line, bar_index)
label.set_x(tp2Lbl, bar_index)
line.set_x2(tp3Line, bar_index)
label.set_x(tp3Lbl, bar_index)
// ── Clean up old objects ────────────────────────────────
if rangeBoxes.size() > 50
box.delete(rangeBoxes.pop())
if resistBoxes.size() > 50
box.delete(resistBoxes.pop())
if supportBoxes.size() > 50
box.delete(supportBoxes.pop())
if centerLines.size() > 50
line.delete(centerLines.pop())
// ══════════════════════════════════════
// SIGNAL LOGIC
// ══════════════════════════════════════
bool confirmedBullBreak = bullBreak
bool confirmedBearBreak = bearBreak
// ══════════════════════════════════════
// PLOTS & VISUALS
// ══════════════════════════════════════
bgcolor(showBgInput and isSqueeze ? color.new(neutralColorInput, 92) : na, title = "Squeeze Background")
// ══════════════════════════════════════
// SIGNAL LABELS
// ══════════════════════════════════════
if showSignalsInput and confirmedBullBreak
label.new(bar_index, low, "Long",
color = bullColorInput,
textcolor = LABEL_TEXT,
style = label.style_label_up,
size = SIGNAL_SIZE,
tooltip = "Bullish Breakout\nEntry: " + str.tostring(close, format.mintick))
if showSignalsInput and confirmedBearBreak
label.new(bar_index, high, "Short",
color = bearColorInput,
textcolor = LABEL_TEXT,
style = label.style_label_down,
size = SIGNAL_SIZE,
tooltip = "Bearish Breakout\nEntry: " + str.tostring(close, format.mintick))
// ══════════════════════════════════════
// DASHBOARD STATUS TRACKING
// ══════════════════════════════════════
var string lastSignalStr = "Wait"
var color lastSignalClr = color.gray
var int barsSinceSignal = 0
if confirmedBullBreak
lastSignalStr := "Buy"
lastSignalClr := bullColorInput
barsSinceSignal := 0
else if confirmedBearBreak
lastSignalStr := "Sell"
lastSignalClr := bearColorInput
barsSinceSignal := 0
else
barsSinceSignal += 1
var int signalStrength = 0
if confirmedBullBreak or confirmedBearBreak
signalStrength := 0
if impulseUp or impulseDown
signalStrength += 1
if volConfirm and useVolFilterInput
signalStrength += 1
if (confirmedBullBreak and htfBullOk and useHtfInput) or (confirmedBearBreak and htfBearOk and useHtfInput)
signalStrength += 1
if squeezeBars[1] >= minSqueezeInput * 2
signalStrength += 1
string strengthStr = signalStrength >= 3 ? "Strong" : signalStrength >= 2 ? "Medium" : "Normal"
color strengthClr = signalStrength >= 3 ? bullColorInput : signalStrength >= 2 ? neutralColorInput : TEXT_MUTED
// ══════════════════════════════════════
// DASHBOARD
// ══════════════════════════════════════
if showDashInput and barstate.islast
var dashTable = table.new(dashPos, 2, 11, TABLE_BG, TABLE_BORDER, 1, TABLE_BORDER, 1)
// Row 0: Header
table.cell(dashTable, 0, 0, "Smart Breakout", text_color = #FFFFFF, bgcolor = HEADER_BG, text_size = size.small, text_halign = text.align_center)
table.merge_cells(dashTable, 0, 0, 1, 0)
// Row 1: Trade Status (NEW — key row)
string statusStr = tradeActive ? "Active" : lastResult
color statusClr = tradeActive ? neutralColorInput : lastResultClr
table.cell(dashTable, 0, 1, "Status", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
table.cell(dashTable, 1, 1, statusStr, text_color = statusClr, text_size = size.small, bgcolor = TABLE_ROW_ALT)
// Row 2: Signal
string sigDashStr = lastSignalStr + (barsSinceSignal > 0 ? " (" + str.tostring(barsSinceSignal) + " ago)" : "")
table.cell(dashTable, 0, 2, "Signal", text_color = TEXT_MUTED, text_size = size.small)
table.cell(dashTable, 1, 2, sigDashStr, text_color = lastSignalClr, text_size = size.small)
// Row 3: Strength
table.cell(dashTable, 0, 3, "Strength", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
table.cell(dashTable, 1, 3, strengthStr, text_color = strengthClr, text_size = size.small, bgcolor = TABLE_ROW_ALT)
// Row 4: Trend
string trendStr = tradeActive ? (tradeDir == 1 ? "Bullish" : "Bearish") : "Neutral"
color trendClr = tradeActive ? (tradeDir == 1 ? bullColorInput : bearColorInput) : TEXT_MUTED
table.cell(dashTable, 0, 4, "Trend", text_color = TEXT_MUTED, text_size = size.small)
table.cell(dashTable, 1, 4, trendStr, text_color = trendClr, text_size = size.small)
// Row 5: Squeeze
string sqzStr = isSqueeze ? "ACTIVE (" + str.tostring(squeezeBars) + ")" : "None"
color sqzColor = isSqueeze ? neutralColorInput : TEXT_MUTED
table.cell(dashTable, 0, 5, "Squeeze", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
table.cell(dashTable, 1, 5, sqzStr, text_color = sqzColor, text_size = size.small, bgcolor = TABLE_ROW_ALT)
// Row 6: Entry
string entryDashStr = na(lastEntry) ? "—" : str.tostring(lastEntry, format.mintick)
table.cell(dashTable, 0, 6, "Entry", text_color = TEXT_MUTED, text_size = size.small)
table.cell(dashTable, 1, 6, tradeActive ? entryDashStr : "—", text_color = TEXT_COLOR, text_size = size.small)
// Row 7: SL
string slDashStr = na(lastSL) ? "—" : str.tostring(lastSL, format.mintick)
table.cell(dashTable, 0, 7, "Stop Loss", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
table.cell(dashTable, 1, 7, tradeActive ? slDashStr : "—", text_color = bearColorInput, text_size = size.small, bgcolor = TABLE_ROW_ALT)
// Row 8: Last P&L
string pnlStr = na(lastPnl) ? "—" : (lastPnl >= 0 ? "+" : "") + str.tostring(lastPnl, format.mintick)
color pnlClr = na(lastPnl) ? TEXT_MUTED : (lastPnl >= 0 ? bullColorInput : bearColorInput)
table.cell(dashTable, 0, 8, "Last P&L", text_color = TEXT_MUTED, text_size = size.small)
table.cell(dashTable, 1, 8, pnlStr, text_color = pnlClr, text_size = size.small)
// Row 9: HTF bias
string htfStr = not useHtfInput ? "Off" : (htfBullish ? "Bullish" : "Bearish")
color htfColor = not useHtfInput ? TEXT_MUTED : (htfBullish ? bullColorInput : bearColorInput)
table.cell(dashTable, 0, 9, "HTF Bias", text_color = TEXT_MUTED, text_size = size.small, bgcolor = TABLE_ROW_ALT)
table.cell(dashTable, 1, 9, htfStr, text_color = htfColor, text_size = size.small, bgcolor = TABLE_ROW_ALT)
// Row 10: Version + TF
string versionTfStr = INDICATOR_VERSION + " | " + timeframe.period
table.cell(dashTable, 0, 10, "Version", text_color = TEXT_MUTED, text_size = size.small)
table.cell(dashTable, 1, 10, versionTfStr, text_color = TEXT_MUTED, text_size = size.small)
// ══════════════════════════════════════
// WATERMARK — WillyAlgoTrader
// ══════════════════════════════════════
if barstate.islast and showWatermarkInput
var wmTable = table.new(position = position.bottom_center, columns = 1, rows = 1, bgcolor = color.new(color.black, 100), border_color = color.new(color.black, 100), border_width = 0, frame_color = color.new(color.black, 100), frame_width = 0)
table.cell(wmTable, 0, 0, "WillyAlgoTrader", text_color = WM_COLOR, text_size = size.normal, text_halign = text.align_center, bgcolor = color.new(color.black, 100))
// ══════════════════════════════════════
// ALERTS
// ══════════════════════════════════════
string tickerStr = syminfo.tickerid
string tfStr = timeframe.period
string priceStr = str.tostring(close, format.mintick)
string slAlertStr = na(lastSL) ? "0" : str.tostring(lastSL, format.mintick)
string tp1AlertStr = na(lastTP1) ? "0" : str.tostring(lastTP1, format.mintick)
if confirmedBullBreak
string jsonMsg = '{"action":"buy","ticker":"' + tickerStr + '","price":' + priceStr + ',"sl":' + slAlertStr + ',"tp1":' + tp1AlertStr + ',"tf":"' + tfStr + '"}'
string textMsg = "🟢 BULL BREAKOUT | " + tickerStr + " | TF: " + tfStr + " | Price: " + priceStr + " | SL: " + slAlertStr + " | TP1: " + tp1AlertStr
alert(webhookInput ? jsonMsg : textMsg, alert.freq_once_per_bar_close)
if confirmedBearBreak
string jsonMsg = '{"action":"sell","ticker":"' + tickerStr + '","price":' + priceStr + ',"sl":' + slAlertStr + ',"tp1":' + tp1AlertStr + ',"tf":"' + tfStr + '"}'
string textMsg = "🔴 BEAR BREAKOUT | " + tickerStr + " | TF: " + tfStr + " | Price: " + priceStr + " | SL: " + slAlertStr + " | TP1: " + tp1AlertStr
alert(webhookInput ? jsonMsg : textMsg, alert.freq_once_per_bar_close)
if tp1HitCond
alert("✅ TP1 HIT | " + tickerStr + " | TF: " + tfStr + " | Price: " + priceStr, alert.freq_once_per_bar_close)
if tp2HitCond
alert("✅ TP2 HIT | " + tickerStr + " | TF: " + tfStr + " | Price: " + priceStr, alert.freq_once_per_bar_close)
if tp3HitCond
string jsonMsg = '{"action":"close","ticker":"' + tickerStr + '","reason":"tp3","price":' + priceStr + ',"pnl":' + str.tostring(nz(lastPnl, 0.0), format.mintick) + ',"tf":"' + tfStr + '"}'
string textMsg = "✅ TP3 HIT — TRADE CLOSED | " + tickerStr + " | TF: " + tfStr + " | P&L: " + str.tostring(nz(lastPnl, 0.0), format.mintick)
alert(webhookInput ? jsonMsg : textMsg, alert.freq_once_per_bar_close)
if slHitCond
string jsonMsg = '{"action":"close","ticker":"' + tickerStr + '","reason":"sl","price":' + priceStr + ',"pnl":' + str.tostring(nz(lastPnl, 0.0), format.mintick) + ',"tf":"' + tfStr + '"}'
string textMsg = "❌ SL HIT — TRADE CLOSED | " + tickerStr + " | TF: " + tfStr + " | P&L: " + str.tostring(nz(lastPnl, 0.0), format.mintick)
alert(webhookInput ? jsonMsg : textMsg, alert.freq_once_per_bar_close)