Smart Breakout Targets [WillyAlgoTrader]

WillyAlgoTrader · study · 694 行 · 点赞 6,216 · TradingView 原页

本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。

Pine Script

//@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)

← 返回列表