Mirage Liquidity Sweep Pro [WillyAlgoTrader]

WillyAlgoTrader · study · 1093 行 · 点赞 8,326 · TradingView 原页

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

Pine Script

//@version=6
// ══════════════════════════════════════════════════════════
// MIRAGE LIQUIDITY SWEEP PRO [WillyAlgoTrader]
// ══════════════════════════════════════════════════════════
// Author:  Willy | WillyAlgoTrader
// Version: 1.3.1
 
indicator(
     title            = "Mirage Liquidity Sweep Pro [WillyAlgoTrader]",
     shorttitle       = "Mirage LSP",
     overlay          = true,
     max_lines_count  = 500,
     max_labels_count = 500,
     max_boxes_count  = 500,
     max_bars_back    = 5000)
 
// ══════════════════════════════════════════════════════════
// 1. CONSTANTS
// ══════════════════════════════════════════════════════════
GRP_MAIN    = "⚙️ Main Settings"
GRP_CONFIRM = "🧩 Confirmation"
GRP_FILTER  = "🔍 Filters"
GRP_RISK    = "🛡️ Risk Management"
GRP_LIQ     = "💧 Liquidity"
GRP_VISUAL  = "🎨 Visual"
GRP_DASH    = "📊 Dashboard"
GRP_ALERT   = "🔔 Alerts"
GRP_COLORS  = "🎨 Colors"
 
INDICATOR_VERSION = "v1.3.1"
MAX_STORED        = 25    // bounded swing memory
MAX_SIG_LABELS    = 50    // bounded score labels
LINE_FORWARD_BARS = 20
LINE_UPDATE_BARS  = 5
LABEL_OFFSET_BARS = 1
BE_DIM_TRANSP     = 70
FORM_LEN          = 10    // recent-form strip length
MAX_EQ_MARKS      = 16    // bounded EQH/EQL drawings
 
// ══════════════════════════════════════════════════════════
// 2. INPUTS
// ══════════════════════════════════════════════════════════
 
// ── Main ────────────────────────────────────────────────────
swingLenInput     = input.int(21, "Swing Length", minval = 3, maxval = 100, group = GRP_MAIN,
     tooltip = "Pivot lookback for major swings to sweep.\n• Higher = bigger/cleaner liquidity pools\n• Lower = more frequent, smaller sweeps\n• Recommended: 10-25")
lookbackBarsInput = input.int(80, "Max Sweep Distance (bars)", minval = 5, maxval = 500, group = GRP_MAIN,
     tooltip = "A swing can only be swept within this many bars of forming.")
minScoreInput     = input.int(50, "Min Sweep Score", minval = 0, maxval = 100, group = GRP_MAIN,
     tooltip = "Minimum 0-100 quality score for a sweep to qualify.\n• Higher = fewer, stronger sweeps\n• ~45-60 balanced, 65+ strict")
 
// ── Confirmation ────────────────────────────────────────────
requireConfirmInput = input.bool(true, "Require CHoCH Confirmation", group = GRP_CONFIRM,
     tooltip = "OFF: signal fires on the sweep bar (responsive).\nON: sweep becomes a setup; signal fires only after price breaks recent market structure (CHoCH).")
minorLenInput       = input.int(8, "Structure Pivot Length", minval = 2, maxval = 30, group = GRP_CONFIRM,
     tooltip = "Pivot length for the minor structure used to confirm CHoCH after a sweep.")
confirmWindowInput  = input.int(13, "Confirmation Window (bars)", minval = 1, maxval = 100, group = GRP_CONFIRM,
     tooltip = "Max bars to wait for a CHoCH after a sweep before the setup expires.")
 
// ── Filters ─────────────────────────────────────────────────
useVolumeInput  = input.bool(true, "Use Volume Filter", group = GRP_FILTER,
     tooltip = "Reward sweeps on above-average volume. Auto-bypassed on volume-less feeds.")
volLenInput     = input.int(21, "Volume MA Length", minval = 2, maxval = 200, group = GRP_FILTER,
     tooltip = "Length of the volume moving average baseline.")
volMultInput    = input.float(1.5, "Volume Spike (×MA)", minval = 1.1, maxval = 5.0, step = 0.1, group = GRP_FILTER,
     tooltip = "Volume multiple of its MA that counts as a full-strength spike.")
useHtfInput     = input.bool(true, "Use HTF Bias (score)", group = GRP_FILTER,
     tooltip = "Reward sweeps aligned with the higher-timeframe EMA trend (soft, +10% of score).")
htfTfInput      = input.timeframe("240", "HTF Timeframe", group = GRP_FILTER,
     tooltip = "Higher timeframe for trend bias. Empty = current TF.")
htfEmaLenInput  = input.int(50, "HTF EMA Length", minval = 5, maxval = 400, group = GRP_FILTER,
     tooltip = "EMA length on the HTF (close above = bullish bias).")
 
// ── Risk Management ─────────────────────────────────────────
riskPresetInput = input.string("Balanced", "Risk Preset",
     options = ["Conservative", "Balanced", "Aggressive", "Scalping", "Custom"], group = GRP_RISK,
     tooltip = "Quick presets (SL buffer ×ATR beyond wick / TP1 / TP2 / TP3 in R):\n• Conservative: 0.5 / 1R / 2R / 4R\n• Balanced: 0.25 / 1R / 2R / 3R\n• Aggressive: 0.15 / 1.5R / 2.5R / 4R\n• Scalping: 0.1 / 0.8R / 1.5R / 2R\n• Custom: manual inputs below")
atrLenRiskInput = input.int(14, "ATR Length", minval = 5, maxval = 50, group = GRP_RISK,
     tooltip = "ATR period for the stop buffer and minimum-distance guard.")
slBufferInput   = input.float(0.25, "SL Buffer ×ATR (Custom)", minval = 0.0, maxval = 3.0, step = 0.05, group = GRP_RISK,
     tooltip = "Extra room beyond the sweep wick for the stop. Used when preset = Custom.")
tp1MultInput    = input.float(1.0, "TP1 ×Risk (Custom)", minval = 0.5, maxval = 5.0,  step = 0.1, group = GRP_RISK,
     tooltip = "TP1 as multiple of risk. 1.0 = 1:1 R:R.")
tp2MultInput    = input.float(2.0, "TP2 ×Risk (Custom)", minval = 1.0, maxval = 10.0, step = 0.1, group = GRP_RISK)
tp3MultInput    = input.float(3.0, "TP3 ×Risk (Custom)", minval = 1.5, maxval = 15.0, step = 0.1, group = GRP_RISK)
 
useBreakEvenInput = input.bool(true, "Break-Even After TP1", group = GRP_RISK,
     tooltip = "When TP1 is touched, move SL to entry. From the next bar, a wick to entry closes at break-even. (TP1 still counts as a WIN.)")
 
showSlTpInput        = input.bool(true, "Show SL/TP Lines",  group = GRP_RISK)
showSlTpLabelsInput  = input.bool(true, "Show SL/TP Labels", group = GRP_RISK)
showPctOnLabelsInput = input.bool(true, "Show % Distance on Labels", group = GRP_RISK,
     tooltip = "Append % distance from entry to SL/TP labels, e.g. 'SL 1.2300 (-0.36%)'.")
entryLineStyleInput  = input.string("Dotted", "Entry Line Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_RISK,
     tooltip = "Entry is a reference, not an action level — keep it subtle.")
slLineStyleInput     = input.string("Solid", "SL Line Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_RISK)
tpLineStyleInput     = input.string("Dashed", "TP Line Style", options = ["Solid", "Dashed", "Dotted"], group = GRP_RISK,
     tooltip = "Style for TP1/TP2/TP3. On hit, a TP line turns solid teal.")
 
// ── Liquidity ───────────────────────────────────────────────
showLiquidityInput = input.bool(true, "Show Resting Liquidity (BSL/SSL)", group = GRP_LIQ,
     tooltip = "Mark un-swept swing highs as Buy-Side Liquidity (BSL) and un-swept swing lows as Sell-Side Liquidity (SSL). A level disappears once it is swept.")
maxLiqInput        = input.int(6, "Max Liquidity Levels / Side", minval = 1, maxval = 25, group = GRP_LIQ,
     tooltip = "Show only the N most recent un-swept levels per side (keeps the chart clean).")
liqExtendInput     = input.int(10, "Liquidity Line Extend (bars)", minval = 0, maxval = 100, group = GRP_LIQ,
     tooltip = "How far past the current bar to project resting-liquidity lines.")
showLiqLabelsInput = input.bool(true, "Show BSL/SSL Labels", group = GRP_LIQ,
     tooltip = "Tag each resting-liquidity line BSL / SSL.")
showEqualsInput    = input.bool(true, "Show Equal Highs/Lows (EQH/EQL)", group = GRP_LIQ,
     tooltip = "Connect consecutive near-equal swing highs/lows — prime liquidity magnets and the most common sweep targets.")
eqTolInput         = input.float(0.15, "Equal H/L Tolerance (×ATR)", minval = 0.02, maxval = 1.0, step = 0.01, group = GRP_LIQ,
     tooltip = "Two swings count as 'equal' if within this fraction of ATR.\nLower = stricter (only very tight equals).")
showTargetInput    = input.bool(true, "Show Liquidity Target (on entry)", group = GRP_LIQ,
     tooltip = "On each entry, project a line to the nearest opposite un-swept liquidity — the logical draw-on-liquidity objective.")
 
// ── Visual ──────────────────────────────────────────────────
themeInput           = input.string("Auto", "Theme", options = ["Auto", "Dark", "Light"], group = GRP_VISUAL,
     tooltip = "Chart color theme.\n• Auto: detect from chart background\n• Dark / Light: force a palette")
showSweepsInput      = input.bool(true,  "Show Raw Sweep Marks",  group = GRP_VISUAL,
     tooltip = "Small × at the wick where liquidity was grabbed (shown even while in a trade).")
showSweepLineInput   = input.bool(true,  "Show Swept Extremum Line", group = GRP_VISUAL,
     tooltip = "Horizontal line at the swing high/low that was swept, drawn from where that swing formed to the sweep bar.")
sweepLineExtendInput = input.int(8, "Swept Line Extend (bars)", minval = 0, maxval = 100, group = GRP_VISUAL,
     tooltip = "How many bars to extend the swept-level line past the sweep bar (swept liquidity often acts as future S/R).")
showSweepPenInput    = input.bool(true,  "Show Sweep Penetration", group = GRP_VISUAL,
     tooltip = "Highlight the wick segment that pierced beyond the level — the depth of the liquidity grab.")
maxSweepVisInput     = input.int(15, "Max Sweep Drawings", minval = 1, maxval = 60, group = GRP_VISUAL,
     tooltip = "Cap on stored swept-level / penetration drawings. Oldest are removed first.")
showSignalsInput     = input.bool(true,  "Show Buy/Sell Signals", group = GRP_VISUAL,
     tooltip = "Print Long ▲ / Short ▼ markers when a trade opens.")
showScoreLabelsInput = input.bool(false, "Show Score Labels", group = GRP_VISUAL,
     tooltip = "Floating 0-100 score next to each qualifying sweep.")
showLevelInput       = input.bool(true,  "Show Swept Level Line", group = GRP_VISUAL,
     tooltip = "Line marking the liquidity level taken by the active trade.")
labelFontSizeInput   = input.string("Small", "SL/TP Label Font Size",
     options = ["Tiny", "Small", "Normal", "Large", "Huge"], group = GRP_VISUAL,
     tooltip = "Font size for SL/TP/Entry labels.")
showWatermarkInput   = input.bool(true,  "Show Watermark", group = GRP_VISUAL,
     tooltip = "Display 'WillyAlgoTrader' watermark on chart.")
 
// ── Dashboard ───────────────────────────────────────────────
showDashInput      = input.bool(true, "Show Dashboard", group = GRP_DASH)
dashPosStr         = input.string("Top Right", "Dashboard Position",
     options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right", "Middle Right"], group = GRP_DASH)
dashFontSizeInput  = input.string("Small", "Dashboard Font Size",
     options = ["Small", "Normal", "Large", "Huge"], group = GRP_DASH,
     tooltip = "Font size for the dashboard table. Section dividers and the\nversion footer render one step smaller to preserve hierarchy.")
showMarketSecInput = input.bool(true, "  ├ Market section", group = GRP_DASH,
     tooltip = "Trend, HTF bias, Signal, Last sweep, Timeframe.")
showTradeSecInput  = input.bool(true, "  ├ Trade section", group = GRP_DASH,
     tooltip = "SL, TP1-3, R:R, SL distance. Collapses to one row when flat.")
showStatsSecInput  = input.bool(true, "  └ Stats section", group = GRP_DASH,
     tooltip = "Trades, W/L, Win Rate (+gauge), recent Form.")
 
// ── Alerts ──────────────────────────────────────────────────
webhookInput        = input.bool(false, "Webhook JSON Format", group = GRP_ALERT,
     tooltip = "Send entry alerts as JSON for webhook / autotrading.")
showSlHitAlertInput = input.bool(true,  "Alert on SL Hit", group = GRP_ALERT,
     tooltip = "Fire an alert when price hits the active stop-loss.")
alertTpHitInput     = input.bool(false, "Alert on TP / Break-Even", group = GRP_ALERT,
     tooltip = "Fire alerts when TP1/TP2/TP3 are touched and on break-even.")
 
// ── Colors ──────────────────────────────────────────────────
bullColorInput = input.color(#00E676, "Bull", inline = "c", group = GRP_COLORS)
bearColorInput = input.color(#FF5252, "Bear", inline = "c", group = GRP_COLORS)
 
// ══════════════════════════════════════════════════════════
// 3. THEME DETECTION & COLORS
// ══════════════════════════════════════════════════════════
isDark = switch themeInput
    "Dark"  => true
    "Light" => false
    =>         color.r(chart.bg_color) < 128
 
BULL_COLOR = bullColorInput
BEAR_COLOR = bearColorInput
 
BULL_TEXT    = isDark ? #00E676 : #00A152
BEAR_TEXT    = isDark ? #FF5252 : #D32F2F
NEUTRAL_TEXT = isDark ? #FFEB3B : #F57F17
 
BULL_LABEL_TEXT = #004D25
BEAR_LABEL_TEXT = #FFFFFF
 
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_ROW_ALT = isDark ? color.new(#1C2030, 0)  : color.new(#F0F4F8, 0)
TABLE_BORDER  = isDark ? color.new(#2A2E39, 0)  : color.new(#D0D0D0, 0)
HEADER_TEXT   = #FFFFFF
WM_COLOR      = isDark ? color.new(#FFFFFF, 80) : color.new(#000000, 80)
 
HEADER_BG_BULL = color.new(#0F6E56, 0)
HEADER_BG_BEAR = color.new(#A32D2D, 0)
HEADER_BG_FLAT = color.new(#2962FF, 0)
 
// Premium, muted, theme-aware risk palette
SL_COLOR       = isDark ? color.new(#E57373, 0)  : color.new(#B71C1C, 0)
TP1_COLOR      = isDark ? color.new(#66BB6A, 30) : color.new(#388E3C, 30)
TP2_COLOR      = isDark ? color.new(#66BB6A, 40) : color.new(#388E3C, 40)
TP3_COLOR      = isDark ? color.new(#66BB6A, 0)  : color.new(#388E3C, 0)
ENTRY_COLOR    = isDark ? color.new(#5C8AAE, 0)  : color.new(#34495E, 0)
TP_LABEL_COLOR = isDark ? color.new(#66BB6A, 0)  : color.new(#388E3C, 0)
TP_HIT_COLOR   = isDark ? color.new(#4DB6AC, 0)  : color.new(#00695C, 0)
BE_COLOR       = isDark ? color.new(#FFA726, 0)  : color.new(#E65100, 0)
LABEL_TEXT     = isDark ? #1A1A1A                : #FFFFFF
FORM_COLOR     = isDark ? #5C8AAE                : #34495E
LEVEL_COLOR    = isDark ? color.new(#B0BEC5, 30) : color.new(#546E7A, 30)
 
// Liquidity family — cohesive steel hue; gold accent for the objective
LIQ_COLOR      = isDark ? color.new(#90A4AE, 40) : color.new(#607D8B, 40)
LIQ_TEXT       = isDark ? #B0BEC5                : #455A64
EQ_COLOR       = isDark ? color.new(#B0BEC5, 0)  : color.new(#455A64, 0)
EQ_TEXT        = isDark ? #ECEFF1                : #263238
TARGET_COLOR   = isDark ? color.new(#FFCA28, 15) : color.new(#F9A825, 15)
TARGET_TEXT    = isDark ? #FFD54F                : #F57F17
 
// ══════════════════════════════════════════════════════════
// 4. FUNCTIONS & HELPERS
// ══════════════════════════════════════════════════════════
// Sweep quality score 0-100. dir: 1 = bullish (low swept), -1 = bearish (high swept).
f_score(int dir, float lvl, float atrv, float volComp, float htfComp) =>
    float rng = high - low
    float sc  = 0.0
    if rng > 0 and atrv > 0 and not na(lvl)
        float wick     = dir == 1 ? math.min(open, close) - low : high - math.max(open, close)
        float reclaim  = dir == 1 ? close - lvl : lvl - close
        float closePos = (close - low) / rng
        float cpComp   = dir == 1 ? closePos : 1.0 - closePos
        float wickComp = math.min(math.max(wick, 0.0) / atrv, 1.0)
        float rclComp  = math.min(math.max(reclaim, 0.0) / atrv, 1.0)
        sc := (wickComp * 0.30 + rclComp * 0.25 + cpComp * 0.20 + volComp * 0.15 + htfComp * 0.10) * 100.0
    sc
 
buildGauge(float value, float maxVal, int width) =>
    string result = ""
    int filled = maxVal <= 0 ? 0 : math.round(math.min(width, math.max(0.0, value / maxVal * width)))
    for i = 0 to width - 1
        result += i < filled ? "▰" : "▱"
    result
 
fmtPrice(float v) =>
    na(v) ? "—" : str.tostring(v, format.mintick)
 
formatPctFromEntry(float level, float entry) =>
    string res = ""
    if showPctOnLabelsInput and not na(level) and not na(entry) and entry != 0
        float pct  = (level - entry) / entry * 100.0
        string sign = pct >= 0 ? "+" : ""
        res := " (" + sign + str.tostring(pct, "#.##") + "%)"
    res
 
lineStyleFromString(string s) =>
    s == "Dashed" ? line.style_dashed : s == "Dotted" ? line.style_dotted : line.style_solid
 
sizeFromString(string s) =>
    s == "Tiny" ? size.tiny : s == "Small" ? size.small : s == "Normal" ? size.normal : s == "Large" ? size.large : s == "Huge" ? size.huge : size.small
 
// One step smaller — used for section dividers and the version footer
sizeDownFromString(string s) =>
    s == "Tiny" ? size.tiny : s == "Small" ? size.tiny : s == "Normal" ? size.small : s == "Large" ? size.normal : s == "Huge" ? size.large : size.tiny
 
dashPos = switch dashPosStr
    "Top Left"     => position.top_left
    "Top Right"    => position.top_right
    "Bottom Left"  => position.bottom_left
    "Bottom Right" => position.bottom_right
    "Middle Right" => position.middle_right
    =>                position.top_right
 
entryLineStyle = lineStyleFromString(entryLineStyleInput)
slLineStyle    = lineStyleFromString(slLineStyleInput)
tpLineStyle    = lineStyleFromString(tpLineStyleInput)
labelSize      = sizeFromString(labelFontSizeInput)
dashFontSize    = sizeFromString(dashFontSizeInput)
dashFontSizeSub = sizeDownFromString(dashFontSizeInput)
 
// ══════════════════════════════════════════════════════════
// 5. WARMUP & BASE CALCULATIONS
// ══════════════════════════════════════════════════════════
int  WARMUP_BARS = math.max(swingLenInput * 2, 50)
bool isWarmedUp  = bar_index >= WARMUP_BARS
 
float riskAtr = nz(ta.atr(atrLenRiskInput), 0.0)
 
bool  hasVolume = nz(volume, 0) > 0
float volSmaRaw = ta.sma(volume, volLenInput)           // called every bar (consistency)
float volSma    = hasVolume ? volSmaRaw : na
float volRatio  = hasVolume and not na(volSma) and volSma > 0 ? volume / volSma : na
float volComp   = useVolumeInput ? (not na(volRatio) ? math.min(math.max((volRatio - 1.0) / math.max(volMultInput - 1.0, 0.1), 0.0), 1.0) : 0.5) : 0.5
 
// HTF bias — request.security UNCONDITIONAL, non-repaint via [1] + lookahead_on
[htfClose, htfEma] = request.security(syminfo.tickerid, htfTfInput, [close[1], ta.ema(close, htfEmaLenInput)[1]], barmerge.gaps_off, barmerge.lookahead_on)
bool  htfValid = not na(htfClose) and not na(htfEma)
bool  htfBull  = htfValid and htfClose > htfEma
bool  htfBear  = htfValid and htfClose < htfEma
float htfCompBull = not useHtfInput ? 0.5 : (htfBull ? 1.0 : 0.0)
float htfCompBear = not useHtfInput ? 0.5 : (htfBear ? 1.0 : 0.0)
 
// ══════════════════════════════════════════════════════════
// 6. SWING MEMORY (bounded) & MINOR STRUCTURE
// ══════════════════════════════════════════════════════════
var array<float> hiLvl  = array.new<float>()
var array<int>   hiBar  = array.new<int>()
var array<bool>  hiUsed = array.new<bool>()
var array<float> loLvl  = array.new<float>()
var array<int>   loBar  = array.new<int>()
var array<bool>  loUsed = array.new<bool>()
 
var array<line>  eqLines  = array.new<line>()
var array<label> eqLabels = array.new<label>()
 
float ph = ta.pivothigh(swingLenInput, swingLenInput)
float pl = ta.pivotlow(swingLenInput, swingLenInput)
 
if not na(ph)
    array.push(hiLvl, ph)
    array.push(hiBar, bar_index - swingLenInput)
    array.push(hiUsed, false)
    if showEqualsInput and riskAtr > 0 and array.size(hiLvl) >= 2
        float prevH  = array.get(hiLvl, array.size(hiLvl) - 2)
        int   prevHB = array.get(hiBar, array.size(hiLvl) - 2)
        if math.abs(ph - prevH) <= riskAtr * eqTolInput
            float eqLvl = math.max(ph, prevH)
            line eqLn = line.new(prevHB, eqLvl, bar_index - swingLenInput, eqLvl, color = EQ_COLOR, style = line.style_solid, width = 1)
            array.push(eqLines, eqLn)
            label eqLb = na
            if showLiqLabelsInput
                eqLb := label.new(bar_index - swingLenInput, eqLvl, "EQH", color = color.new(color.black, 100), textcolor = EQ_TEXT, style = label.style_label_left, size = size.tiny)
            array.push(eqLabels, eqLb)
            if array.size(eqLines) > MAX_EQ_MARKS
                line oldEqL = array.shift(eqLines)
                label oldEqB = array.shift(eqLabels)
                if not na(oldEqL)
                    line.delete(oldEqL)
                if not na(oldEqB)
                    label.delete(oldEqB)
    if array.size(hiLvl) > MAX_STORED
        array.shift(hiLvl)
        array.shift(hiBar)
        array.shift(hiUsed)
 
if not na(pl)
    array.push(loLvl, pl)
    array.push(loBar, bar_index - swingLenInput)
    array.push(loUsed, false)
    if showEqualsInput and riskAtr > 0 and array.size(loLvl) >= 2
        float prevL  = array.get(loLvl, array.size(loLvl) - 2)
        int   prevLB = array.get(loBar, array.size(loLvl) - 2)
        if math.abs(pl - prevL) <= riskAtr * eqTolInput
            float eqLvl = math.min(pl, prevL)
            line eqLn = line.new(prevLB, eqLvl, bar_index - swingLenInput, eqLvl, color = EQ_COLOR, style = line.style_solid, width = 1)
            array.push(eqLines, eqLn)
            label eqLb = na
            if showLiqLabelsInput
                eqLb := label.new(bar_index - swingLenInput, eqLvl, "EQL", color = color.new(color.black, 100), textcolor = EQ_TEXT, style = label.style_label_up, size = size.tiny)
            array.push(eqLabels, eqLb)
            if array.size(eqLines) > MAX_EQ_MARKS
                line oldEqL = array.shift(eqLines)
                label oldEqB = array.shift(eqLabels)
                if not na(oldEqL)
                    line.delete(oldEqL)
                if not na(oldEqB)
                    label.delete(oldEqB)
    if array.size(loLvl) > MAX_STORED
        array.shift(loLvl)
        array.shift(loBar)
        array.shift(loUsed)
 
float mph = ta.pivothigh(minorLenInput, minorLenInput)
float mpl = ta.pivotlow(minorLenInput, minorLenInput)
var float lastMinorHigh = na
var float lastMinorLow  = na
if not na(mph)
    lastMinorHigh := mph
if not na(mpl)
    lastMinorLow := mpl
 
// ══════════════════════════════════════════════════════════
// 7. SWEEP DETECTION (O(N) per bar)
// ══════════════════════════════════════════════════════════
bool  bullSweep = false
float bullLvl   = na
int   bullLvlBar = na
bool  bearSweep = false
float bearLvl   = na
int   bearLvlBar = na
 
int nlo = array.size(loLvl)
if nlo > 0
    for j = 0 to nlo - 1
        int idx = nlo - 1 - j
        if not array.get(loUsed, idx)
            float lvl = array.get(loLvl, idx)
            if bar_index - array.get(loBar, idx) <= lookbackBarsInput
                if close < lvl
                    array.set(loUsed, idx, true)
                else if low < lvl and close > lvl
                    array.set(loUsed, idx, true)
                    if not bullSweep
                        bullSweep := true
                        bullLvl   := lvl
                        bullLvlBar := array.get(loBar, idx)
 
int nhi = array.size(hiLvl)
if nhi > 0
    for j = 0 to nhi - 1
        int idx = nhi - 1 - j
        if not array.get(hiUsed, idx)
            float lvl = array.get(hiLvl, idx)
            if bar_index - array.get(hiBar, idx) <= lookbackBarsInput
                if close > lvl
                    array.set(hiUsed, idx, true)
                else if high > lvl and close < lvl
                    array.set(hiUsed, idx, true)
                    if not bearSweep
                        bearSweep := true
                        bearLvl   := lvl
                        bearLvlBar := array.get(hiBar, idx)
 
float bullScore = bullSweep ? f_score(1, bullLvl, riskAtr, volComp, htfCompBull) : 0.0
float bearScore = bearSweep ? f_score(-1, bearLvl, riskAtr, volComp, htfCompBear) : 0.0
 
bool bullQ = bullSweep and bullScore >= minScoreInput and isWarmedUp and barstate.isconfirmed
bool bearQ = bearSweep and bearScore >= minScoreInput and isWarmedUp and barstate.isconfirmed
 
// ══════════════════════════════════════════════════════════
// 8. SIGNAL RESOLUTION (immediate vs CHoCH-confirmed)
// ══════════════════════════════════════════════════════════
var int   pendingDir      = 0
var int   pendingStartBar = 0
var float pendingLvl      = na
var float pendingWick     = na
var float pendingScore    = na
 
bool  fireBull      = false
bool  fireBear      = false
float sigScore      = na
float anchorExtreme = na   // sweep wick: low for bull, high for bear
float sigLvl        = na
 
if requireConfirmInput
    if pendingDir != 0 and (bar_index - pendingStartBar) > confirmWindowInput
        pendingDir := 0
    if bullQ
        pendingDir      := 1
        pendingStartBar := bar_index
        pendingLvl      := bullLvl
        pendingWick     := low
        pendingScore    := bullScore
    if bearQ
        pendingDir      := -1
        pendingStartBar := bar_index
        pendingLvl      := bearLvl
        pendingWick     := high
        pendingScore    := bearScore
    if pendingDir == 1 and not na(lastMinorHigh) and close > lastMinorHigh and barstate.isconfirmed
        fireBull      := true
        sigScore      := pendingScore
        anchorExtreme := pendingWick
        sigLvl        := pendingLvl
        pendingDir    := 0
    if pendingDir == -1 and not na(lastMinorLow) and close < lastMinorLow and barstate.isconfirmed
        fireBear      := true
        sigScore      := pendingScore
        anchorExtreme := pendingWick
        sigLvl        := pendingLvl
        pendingDir    := 0
else
    if bullQ
        fireBull      := true
        sigScore      := bullScore
        anchorExtreme := low
        sigLvl        := bullLvl
    if bearQ
        fireBear      := true
        sigScore      := bearScore
        anchorExtreme := high
        sigLvl        := bearLvl
 
// ══════════════════════════════════════════════════════════
// 9. RISK MANAGEMENT — single position, wick-based SL, BE, win-rate
// ══════════════════════════════════════════════════════════
[effBuf, effTP1, effTP2, effTP3] = switch riskPresetInput
    "Conservative" => [0.50, 1.0, 2.0, 4.0]
    "Aggressive"   => [0.15, 1.5, 2.5, 4.0]
    "Scalping"     => [0.10, 0.8, 1.5, 2.0]
    "Custom"       => [slBufferInput, tp1MultInput, tp2MultInput, tp3MultInput]
    =>                [0.25, 1.0, 2.0, 3.0]   // Balanced
 
var float activeEntry = na
var float activeSL    = na
var float activeTP1   = na
var float activeTP2   = na
var float activeTP3   = na
var int   activeDir   = 0
var int   entryBar    = na
 
var bool tp1Reached = false
var bool tp2Reached = false
var bool tp3Reached = false
var bool beActive   = false
 
var int    statWins   = 0
var int    statLosses = 0
var string formStr    = ""
 
var int    sweepTrend = 0           // last signal direction (engine read)
 
bool sigConflict = fireBull and fireBear      // bar swept both sides → no clear direction
bool canOpen   = activeDir == 0
bool openLong  = fireBull and not sigConflict and riskAtr > 0 and canOpen
bool openShort = fireBear and not sigConflict and riskAtr > 0 and canOpen
 
if openLong
    activeEntry := close
    float rawSL = anchorExtreme - riskAtr * effBuf
    float dist  = math.abs(activeEntry - rawSL)
    if dist < riskAtr * 0.5
        rawSL := activeEntry - riskAtr * 0.5
        dist  := riskAtr * 0.5
    activeSL    := rawSL
    activeTP1   := activeEntry + dist * effTP1
    activeTP2   := activeEntry + dist * effTP2
    activeTP3   := activeEntry + dist * effTP3
    activeDir   := 1
    entryBar    := bar_index
    tp1Reached  := false
    tp2Reached  := false
    tp3Reached  := false
    beActive    := false
    sweepTrend  := 1
 
if openShort
    activeEntry := close
    float rawSL = anchorExtreme + riskAtr * effBuf
    float dist  = math.abs(rawSL - activeEntry)
    if dist < riskAtr * 0.5
        rawSL := activeEntry + riskAtr * 0.5
        dist  := riskAtr * 0.5
    activeSL    := rawSL
    activeTP1   := activeEntry - dist * effTP1
    activeTP2   := activeEntry - dist * effTP2
    activeTP3   := activeEntry - dist * effTP3
    activeDir   := -1
    entryBar    := bar_index
    tp1Reached  := false
    tp2Reached  := false
    tp3Reached  := false
    beActive    := false
    sweepTrend  := -1
 
bool  beActiveAtBarStart = beActive
float effectiveSL        = activeSL
 
bool canCheckHit = activeDir != 0 and not na(entryBar) and bar_index > entryBar and barstate.isconfirmed
 
bool slHit  = canCheckHit and (activeDir == 1 ? low  <= effectiveSL : high >= effectiveSL)
bool tp1Hit = canCheckHit and (activeDir == 1 ? high >= activeTP1   : low  <= activeTP1)
bool tp2Hit = canCheckHit and (activeDir == 1 ? high >= activeTP2   : low  <= activeTP2)
bool tp3Hit = canCheckHit and (activeDir == 1 ? high >= activeTP3   : low  <= activeTP3)
 
bool tp1FirstTouch = tp1Hit and not tp1Reached and not slHit
bool tp2FirstTouch = tp2Hit and not tp2Reached and not slHit
bool tp3FirstTouch = tp3Hit and not tp3Reached and not slHit
 
if tp1FirstTouch
    tp1Reached := true
if tp2FirstTouch
    tp2Reached := true
if tp3FirstTouch
    tp3Reached := true
 
bool beJustActivated = false
if useBreakEvenInput and tp1FirstTouch and not beActive
    activeSL        := activeEntry
    beActive        := true
    beJustActivated := true
 
// Snapshot BEFORE reset
var float hitSL    = na
var float hitEntry = na
if slHit
    hitSL    := effectiveSL
    hitEntry := activeEntry
 
float exitEntry         = activeEntry
float exitTP1           = activeTP1
float exitTP2           = activeTP2
float exitTP3           = activeTP3
bool  exitBeActive      = beActiveAtBarStart
bool  displayTp1Reached = tp1Reached
bool  displayTp2Reached = tp2Reached
bool  displayTp3Reached = tp3Reached
 
// Closure on SL / BE-stop / TP3
if (slHit or tp3Hit) and activeDir != 0
    bool tradeWin = tp1Reached
    if tradeWin
        statWins += 1
    else
        statLosses += 1
    formStr := formStr + (tradeWin ? "▰" : "▱")
    if str.length(formStr) > FORM_LEN
        formStr := str.substring(formStr, str.length(formStr) - FORM_LEN)
    activeDir   := 0
    activeSL    := na
    activeTP1   := na
    activeTP2   := na
    activeTP3   := na
    activeEntry := na
    entryBar    := na
    tp1Reached  := false
    tp2Reached  := false
    tp3Reached  := false
    beActive    := false
 
// Last sweep memory (dashboard)
var string lastSigType  = "—"
var float  lastSigScore = na
if fireBull
    lastSigType  := "Bullish"
    lastSigScore := sigScore
if fireBear
    lastSigType  := "Bearish"
    lastSigScore := sigScore
 
// ══════════════════════════════════════════════════════════
// 10. PLOTS (sweep marks + entry markers)
// ══════════════════════════════════════════════════════════
plotshape(showSweepsInput and bullQ ? low  : na, "Bull Sweep", shape.xcross, location.belowbar, color.new(BULL_COLOR, 35), size = size.tiny)
plotshape(showSweepsInput and bearQ ? high : na, "Bear Sweep", shape.xcross, location.abovebar, color.new(BEAR_COLOR, 35), size = size.tiny)
 
plotshape(showSignalsInput and openLong  ? low  : na, "Long",  shape.labelup,   location.belowbar, BULL_COLOR, textcolor = BULL_LABEL_TEXT, text = "Long ▲",  size = size.small)
plotshape(showSignalsInput and openShort ? high : na, "Short", shape.labeldown, location.abovebar, BEAR_COLOR, textcolor = BEAR_LABEL_TEXT, text = "Short ▼", size = size.small)
 
var array<label> sigLabels = array.new<label>()
if showScoreLabelsInput and (fireBull or fireBear)
    string lblTxt  = str.tostring(sigScore, "#")
    color  lblTcol = fireBull ? BULL_TEXT : BEAR_TEXT
    float  lblY    = fireBull ? low - riskAtr * 0.6 : high + riskAtr * 0.6
    label  lb = label.new(bar_index, lblY, lblTxt, color = color.new(color.black, 100), textcolor = lblTcol, style = label.style_none, size = size.small, tooltip = "Sweep quality: " + lblTxt + "/100")
    array.push(sigLabels, lb)
    if array.size(sigLabels) > MAX_SIG_LABELS
        label oldLb = array.shift(sigLabels)
        label.delete(oldLb)
 
// ══════════════════════════════════════════════════════════
// 11. SWEPT LEVEL LINE
// ══════════════════════════════════════════════════════════
var line levelLine = na
if (openLong or openShort) and showLevelInput
    line.delete(levelLine)
    if not na(sigLvl)
        levelLine := line.new(bar_index - 1, sigLvl, bar_index + LINE_UPDATE_BARS, sigLvl, color = LEVEL_COLOR, style = line.style_dotted, width = 1)
if activeDir != 0 and showLevelInput and not na(levelLine)
    line.set_x2(levelLine, bar_index + LINE_UPDATE_BARS)
if activeDir == 0 and showLevelInput and not na(levelLine)
    line.delete(levelLine)
    levelLine := na
 
// ══════════════════════════════════════════════════════════
// 11b. SWEEP LIQUIDITY VISUALS (per-sweep, bounded)
//   • level line  : horizontal at the swept extremum, swing origin → sweep
//   • penetration : vertical tick of the wick that pierced beyond the level
// ══════════════════════════════════════════════════════════
var array<line> swpLvlLines = array.new<line>()
var array<line> swpPenLines = array.new<line>()
 
bool drawBullVis = bullQ and (showSweepLineInput or showSweepPenInput)
bool drawBearVis = bearQ and (showSweepLineInput or showSweepPenInput)
 
if drawBullVis
    int   originBar = na(bullLvlBar) ? bar_index : bullLvlBar
    line  lvlLn = na
    line  penLn = na
    if showSweepLineInput
        lvlLn := line.new(originBar, bullLvl, bar_index + sweepLineExtendInput, bullLvl, color = color.new(BULL_COLOR, 45), style = line.style_dashed, width = 1)
    if showSweepPenInput
        penLn := line.new(bar_index, bullLvl, bar_index, low, color = BULL_COLOR, style = line.style_solid, width = 3)
    array.push(swpLvlLines, lvlLn)
    array.push(swpPenLines, penLn)
 
if drawBearVis
    int   originBar = na(bearLvlBar) ? bar_index : bearLvlBar
    line  lvlLn = na
    line  penLn = na
    if showSweepLineInput
        lvlLn := line.new(originBar, bearLvl, bar_index + sweepLineExtendInput, bearLvl, color = color.new(BEAR_COLOR, 45), style = line.style_dashed, width = 1)
    if showSweepPenInput
        penLn := line.new(bar_index, bearLvl, bar_index, high, color = BEAR_COLOR, style = line.style_solid, width = 3)
    array.push(swpLvlLines, lvlLn)
    array.push(swpPenLines, penLn)
 
if array.size(swpLvlLines) > maxSweepVisInput
    line oldLvl = array.shift(swpLvlLines)
    line oldPen = array.shift(swpPenLines)
    if not na(oldLvl)
        line.delete(oldLvl)
    if not na(oldPen)
        line.delete(oldPen)
 
// ══════════════════════════════════════════════════════════
// 11c. RESTING LIQUIDITY (BSL / SSL) + TARGET PROJECTION
// ══════════════════════════════════════════════════════════
var array<line>  liqLines  = array.new<line>()
var array<label> liqLabels = array.new<label>()
 
if showLiquidityInput and barstate.islast
    if array.size(liqLines) > 0
        for ci = 0 to array.size(liqLines) - 1
            line.delete(array.get(liqLines, ci))
    if array.size(liqLabels) > 0
        for ci = 0 to array.size(liqLabels) - 1
            label.delete(array.get(liqLabels, ci))
    array.clear(liqLines)
    array.clear(liqLabels)
 
    int rx = bar_index + liqExtendInput
 
    int drawnH = 0
    int nh = array.size(hiLvl)
    if nh > 0
        for k = 0 to nh - 1
            int idx = nh - 1 - k
            if drawnH < maxLiqInput and not array.get(hiUsed, idx)
                float lvl = array.get(hiLvl, idx)
                int   ob  = array.get(hiBar, idx)
                line ln = line.new(ob, lvl, rx, lvl, color = LIQ_COLOR, style = line.style_dotted, width = 1)
                array.push(liqLines, ln)
                if showLiqLabelsInput
                    label lb = label.new(rx, lvl, "BSL", color = color.new(color.black, 100), textcolor = LIQ_TEXT, style = label.style_label_left, size = size.tiny)
                    array.push(liqLabels, lb)
                drawnH += 1
 
    int drawnL = 0
    int nl = array.size(loLvl)
    if nl > 0
        for k = 0 to nl - 1
            int idx = nl - 1 - k
            if drawnL < maxLiqInput and not array.get(loUsed, idx)
                float lvl = array.get(loLvl, idx)
                int   ob  = array.get(loBar, idx)
                line ln = line.new(ob, lvl, rx, lvl, color = LIQ_COLOR, style = line.style_dotted, width = 1)
                array.push(liqLines, ln)
                if showLiqLabelsInput
                    label lb = label.new(rx, lvl, "SSL", color = color.new(color.black, 100), textcolor = LIQ_TEXT, style = label.style_label_left, size = size.tiny)
                    array.push(liqLabels, lb)
                drawnL += 1
 
// Target = nearest opposite un-swept liquidity, projected on entry
var line  targetLine  = na
var label targetLabel = na
 
if showTargetInput and (openLong or openShort)
    line.delete(targetLine)
    label.delete(targetLabel)
    targetLine  := na
    targetLabel := na
    float tgt = na
    if openLong
        float best = na
        int n = array.size(hiLvl)
        if n > 0
            for i = 0 to n - 1
                if not array.get(hiUsed, i)
                    float lvl = array.get(hiLvl, i)
                    if lvl > activeEntry and (na(best) or lvl < best)
                        best := lvl
        tgt := best
    if openShort
        float best = na
        int n = array.size(loLvl)
        if n > 0
            for i = 0 to n - 1
                if not array.get(loUsed, i)
                    float lvl = array.get(loLvl, i)
                    if lvl < activeEntry and (na(best) or lvl > best)
                        best := lvl
        tgt := best
    if not na(tgt)
        targetLine  := line.new(bar_index, tgt, bar_index + LINE_FORWARD_BARS, tgt, color = TARGET_COLOR, style = line.style_dashed, width = 1)
        targetLabel := label.new(bar_index + LINE_FORWARD_BARS, tgt, "◎ Liquidity Target", color = color.new(color.black, 100), textcolor = TARGET_TEXT, style = label.style_label_left, size = size.small)
 
if showTargetInput and activeDir != 0 and not na(targetLine)
    line.set_x2(targetLine, bar_index + LINE_UPDATE_BARS)
    if not na(targetLabel)
        label.set_x(targetLabel, bar_index + LINE_UPDATE_BARS)
if activeDir == 0 and not na(targetLine)
    line.delete(targetLine)
    label.delete(targetLabel)
    targetLine  := na
    targetLabel := na
 
// ══════════════════════════════════════════════════════════
// 12. SL/TP LINES & LABELS (persist until next entry)
// ══════════════════════════════════════════════════════════
var line slLine    = na
var line tp1Line   = na
var line tp2Line   = na
var line tp3Line   = na
var line entryLine = na
 
var label slLabel    = na
var label tp1Label   = na
var label tp2Label   = na
var label tp3Label   = na
var label entryLabel = na
 
if (openLong or openShort) and showSlTpInput
    line.delete(slLine)
    line.delete(tp1Line)
    line.delete(tp2Line)
    line.delete(tp3Line)
    line.delete(entryLine)
    label.delete(slLabel)
    label.delete(tp1Label)
    label.delete(tp2Label)
    label.delete(tp3Label)
    label.delete(entryLabel)
 
    int x1 = bar_index
    int x2 = bar_index + LINE_FORWARD_BARS
 
    entryLine := line.new(x1, activeEntry, x2, activeEntry, color = ENTRY_COLOR, style = entryLineStyle, width = 1)
    slLine    := line.new(x1, activeSL,    x2, activeSL,    color = SL_COLOR,    style = slLineStyle,    width = 2)
    tp1Line   := line.new(x1, activeTP1,   x2, activeTP1,   color = TP1_COLOR,   style = tpLineStyle,    width = 1)
    tp2Line   := line.new(x1, activeTP2,   x2, activeTP2,   color = TP2_COLOR,   style = tpLineStyle,    width = 1)
    tp3Line   := line.new(x1, activeTP3,   x2, activeTP3,   color = TP3_COLOR,   style = tpLineStyle,    width = 1)
 
    if showSlTpLabelsInput
        int lblX = x2 + LABEL_OFFSET_BARS
        string entryS = str.tostring(activeEntry, format.mintick)
        string slS    = fmtPrice(activeSL)  + formatPctFromEntry(activeSL,  activeEntry)
        string tp1S   = fmtPrice(activeTP1) + formatPctFromEntry(activeTP1, activeEntry)
        string tp2S   = fmtPrice(activeTP2) + formatPctFromEntry(activeTP2, activeEntry)
        string tp3S   = fmtPrice(activeTP3) + formatPctFromEntry(activeTP3, activeEntry)
 
        entryLabel := label.new(lblX, activeEntry, "ENTRY " + entryS, color = ENTRY_COLOR, textcolor = LABEL_TEXT, style = label.style_label_left, size = labelSize)
        slLabel    := label.new(lblX, activeSL,  "SL "  + slS,  color = SL_COLOR,       textcolor = LABEL_TEXT, style = label.style_label_left, size = labelSize)
        tp1Label   := label.new(lblX, activeTP1, "TP1 " + tp1S, color = TP_LABEL_COLOR, textcolor = LABEL_TEXT, style = label.style_label_left, size = labelSize)
        tp2Label   := label.new(lblX, activeTP2, "TP2 " + tp2S, color = TP_LABEL_COLOR, textcolor = LABEL_TEXT, style = label.style_label_left, size = labelSize)
        tp3Label   := label.new(lblX, activeTP3, "TP3 " + tp3S, color = TP_LABEL_COLOR, textcolor = LABEL_TEXT, style = label.style_label_left, size = labelSize)
 
// TP-hit recolor (solid teal + ✓)
if showSlTpInput and not na(tp1Line) and displayTp1Reached
    line.set_color(tp1Line, TP_HIT_COLOR)
    line.set_style(tp1Line, line.style_solid)
    if showSlTpLabelsInput and not na(tp1Label)
        label.set_text(tp1Label, "TP1 ✓ " + fmtPrice(exitTP1) + formatPctFromEntry(exitTP1, exitEntry))
        label.set_color(tp1Label, TP_HIT_COLOR)
if showSlTpInput and not na(tp2Line) and displayTp2Reached
    line.set_color(tp2Line, TP_HIT_COLOR)
    line.set_style(tp2Line, line.style_solid)
    if showSlTpLabelsInput and not na(tp2Label)
        label.set_text(tp2Label, "TP2 ✓ " + fmtPrice(exitTP2) + formatPctFromEntry(exitTP2, exitEntry))
        label.set_color(tp2Label, TP_HIT_COLOR)
if showSlTpInput and not na(tp3Line) and displayTp3Reached
    line.set_color(tp3Line, TP_HIT_COLOR)
    line.set_style(tp3Line, line.style_solid)
    if showSlTpLabelsInput and not na(tp3Label)
        label.set_text(tp3Label, "TP3 ✓ " + fmtPrice(exitTP3) + formatPctFromEntry(exitTP3, exitEntry))
        label.set_color(tp3Label, TP_HIT_COLOR)
 
// Break-even: dim SL line/label, annotate entry
if showSlTpInput and beActive and activeDir != 0
    if not na(slLine)
        line.set_color(slLine, color.new(SL_COLOR, BE_DIM_TRANSP))
    if showSlTpLabelsInput
        if not na(slLabel)
            label.set_color(slLabel, color.new(SL_COLOR, BE_DIM_TRANSP))
        if not na(entryLabel)
            string entryS = str.tostring(activeEntry, format.mintick)
            label.set_text(entryLabel, "ENTRY " + entryS + "  → SL (BE)")
            label.set_color(entryLabel, BE_COLOR)
 
// Extend active lines to current bar
if activeDir != 0 and showSlTpInput and not na(slLine)
    int newX2   = bar_index + LINE_UPDATE_BARS
    int newLblX = newX2 + LABEL_OFFSET_BARS
    line.set_x2(slLine,    newX2)
    line.set_x2(tp1Line,   newX2)
    line.set_x2(tp2Line,   newX2)
    line.set_x2(tp3Line,   newX2)
    line.set_x2(entryLine, newX2)
    if showSlTpLabelsInput and not na(slLabel)
        label.set_x(slLabel,    newLblX)
        label.set_x(tp1Label,   newLblX)
        label.set_x(tp2Label,   newLblX)
        label.set_x(tp3Label,   newLblX)
        label.set_x(entryLabel, newLblX)
 
// ══════════════════════════════════════════════════════════
// 13. DASHBOARD (sectioned, dynamic height, gauge, tooltips)
// ══════════════════════════════════════════════════════════
var table dashTable = na
 
if showDashInput and barstate.islast
    string trendWord = sweepTrend == 1 ? "Bullish" : sweepTrend == -1 ? "Bearish" : "Neutral"
    color  trendCol  = sweepTrend == 1 ? BULL_TEXT : sweepTrend == -1 ? BEAR_TEXT : NEUTRAL_TEXT
    color  headerBg  = sweepTrend == 1 ? HEADER_BG_BULL : sweepTrend == -1 ? HEADER_BG_BEAR : HEADER_BG_FLAT
 
    string biasStr   = not useHtfInput ? "Off" : (htfBull ? "Bullish" : htfBear ? "Bearish" : "—")
    color  biasCol   = not useHtfInput ? TEXT_MUTED : (htfBull ? BULL_TEXT : htfBear ? BEAR_TEXT : TEXT_MUTED)
 
    string signalStr = activeDir == 1 ? "LONG" : activeDir == -1 ? "SHORT" : "Wait"
    color  signalCol = activeDir == 1 ? BULL_TEXT : activeDir == -1 ? BEAR_TEXT : TEXT_MUTED
 
    string lastSweepStr = lastSigType == "—" ? "—" : lastSigType + " " + (na(lastSigScore) ? "" : str.tostring(lastSigScore, "#"))
    color  lastSweepCol = lastSigType == "Bullish" ? BULL_TEXT : lastSigType == "Bearish" ? BEAR_TEXT : TEXT_MUTED
 
    float currentRR = activeDir != 0 ? effTP1 : 0.0
    float slDistPct = activeDir != 0 and not na(activeEntry) and activeEntry != 0 ? math.abs(activeEntry - activeSL) / activeEntry * 100 : 0.0
 
    string dashSL  = activeDir != 0 and not na(activeSL)  ? (beActive ? "BE @ " : "") + str.tostring(activeSL, format.mintick) : "—"
    string dashTP1 = activeDir != 0 and not na(activeTP1) ? (tp1Reached ? "✓ " : "") + str.tostring(activeTP1, format.mintick) : "—"
    string dashTP2 = activeDir != 0 and not na(activeTP2) ? (tp2Reached ? "✓ " : "") + str.tostring(activeTP2, format.mintick) : "—"
    string dashTP3 = activeDir != 0 and not na(activeTP3) ? (tp3Reached ? "✓ " : "") + str.tostring(activeTP3, format.mintick) : "—"
    color  dashSlCol  = beActive   ? BE_COLOR     : SL_COLOR
    color  dashTp1Col = tp1Reached ? TP_HIT_COLOR : TP_LABEL_COLOR
    color  dashTp2Col = tp2Reached ? TP_HIT_COLOR : TP_LABEL_COLOR
    color  dashTp3Col = tp3Reached ? TP_HIT_COLOR : TP_LABEL_COLOR
 
    string dashRR   = str.tostring(currentRR, "#.#") + "R"
    string dashDist = str.tostring(slDistPct, "#.##") + "%"
    color  rrColor   = currentRR >= 1.5 ? BULL_TEXT : currentRR >= 1.0 ? NEUTRAL_TEXT : BEAR_TEXT
    color  distColor = slDistPct > 3.0 ? BEAR_TEXT : slDistPct > 1.0 ? NEUTRAL_TEXT : BULL_TEXT
 
    int    closedTrades = statWins + statLosses
    float  winRate      = closedTrades > 0 ? statWins / float(closedTrades) * 100.0 : 0.0
    string dashWL       = str.tostring(statWins) + " / " + str.tostring(statLosses)
    string dashWR       = closedTrades > 0 ? str.tostring(winRate, "#.#") + "%  " + buildGauge(winRate, 100, 8) : "—"
    color  wrCol        = closedTrades == 0 ? TEXT_MUTED : winRate >= 55.0 ? BULL_TEXT : winRate >= 45.0 ? NEUTRAL_TEXT : BEAR_TEXT
    string dashForm     = formStr == "" ? "—" : formStr
 
    int marketRows = showMarketSecInput ? 6 : 0
    int tradeRows  = showTradeSecInput  ? (activeDir != 0 ? 7 : 2) : 0
    int statsRows  = showStatsSecInput  ? 5 : 0
    int totalRows  = 2 + marketRows + tradeRows + statsRows
 
    if not na(dashTable)
        table.delete(dashTable)
    dashTable := table.new(dashPos, 2, totalRows, bgcolor = TABLE_BG, border_color = TABLE_BORDER, border_width = 1, frame_color = TABLE_BORDER, frame_width = 1)
 
    int  r   = 0
    bool zeb = false
    color rb = TABLE_BG
 
    table.cell(dashTable, 0, r, "◆ Mirage · " + trendWord, text_color = HEADER_TEXT, bgcolor = headerBg, text_size = dashFontSize, text_halign = text.align_center)
    table.merge_cells(dashTable, 0, r, 1, r)
    r += 1
 
    if showMarketSecInput
        table.cell(dashTable, 0, r, "— Market —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = dashFontSizeSub, text_halign = text.align_center)
        table.merge_cells(dashTable, 0, r, 1, r)
        r += 1
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Trend", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Direction of the most recent qualifying sweep signal.")
        table.cell(dashTable, 1, r, trendWord, text_color = trendCol, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "HTF Bias", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Higher-timeframe EMA bias (soft score component).")
        table.cell(dashTable, 1, r, biasStr, text_color = biasCol, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Signal", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Current position. A new trade opens only while flat.")
        table.cell(dashTable, 1, r, signalStr, text_color = signalCol, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Last sweep", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Most recent qualifying sweep + its 0-100 score.")
        table.cell(dashTable, 1, r, lastSweepStr, text_color = lastSweepCol, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Timeframe", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb)
        table.cell(dashTable, 1, r, timeframe.period, text_color = TEXT_COLOR, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
 
    if showTradeSecInput
        table.cell(dashTable, 0, r, "— Trade —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = dashFontSizeSub, text_halign = text.align_center)
        table.merge_cells(dashTable, 0, r, 1, r)
        r += 1
        if activeDir != 0
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "SL", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Active stop. 'BE @' = moved to entry after TP1.")
            table.cell(dashTable, 1, r, dashSL, text_color = dashSlCol, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "TP1", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "✓ = touched (counts as a win).")
            table.cell(dashTable, 1, r, dashTP1, text_color = dashTp1Col, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "TP2", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb)
            table.cell(dashTable, 1, r, dashTP2, text_color = dashTp2Col, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "TP3", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Final target. Touching it closes the trade.")
            table.cell(dashTable, 1, r, dashTP3, text_color = dashTp3Col, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "R:R (TP1)", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Reward-to-risk at TP1.")
            table.cell(dashTable, 1, r, dashRR, text_color = rrColor, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "SL Dist %", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Stop distance as % of entry (not position risk).")
            table.cell(dashTable, 1, r, dashDist, text_color = distColor, text_size = dashFontSize, bgcolor = rb)
            r += 1
            zeb := not zeb
        else
            rb := zeb ? TABLE_ROW_ALT : TABLE_BG
            table.cell(dashTable, 0, r, "Flat · waiting for sweep", text_color = TEXT_MUTED, text_size = dashFontSize, text_halign = text.align_center, bgcolor = rb)
            table.merge_cells(dashTable, 0, r, 1, r)
            r += 1
            zeb := not zeb
 
    if showStatsSecInput
        table.cell(dashTable, 0, r, "— Stats —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = dashFontSizeSub, text_halign = text.align_center)
        table.merge_cells(dashTable, 0, r, 1, r)
        r += 1
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Trades", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Closed trades this session (wins + losses).")
        table.cell(dashTable, 1, r, str.tostring(closedTrades), text_color = TEXT_COLOR, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "W / L", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb)
        table.cell(dashTable, 1, r, dashWL, text_color = TEXT_COLOR, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Win rate", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Wins / closed. A trade is a WIN once TP1 is touched.")
        table.cell(dashTable, 1, r, dashWR, text_color = wrCol, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
        rb := zeb ? TABLE_ROW_ALT : TABLE_BG
        table.cell(dashTable, 0, r, "Form", text_color = TEXT_MUTED, text_size = dashFontSize, bgcolor = rb, tooltip = "Last " + str.tostring(FORM_LEN) + " closed trades.  ▰ = win · ▱ = loss (newest right).")
        table.cell(dashTable, 1, r, dashForm, text_color = FORM_COLOR, text_size = dashFontSize, bgcolor = rb)
        r += 1
        zeb := not zeb
 
    table.cell(dashTable, 0, r, "Mirage LSP · " + INDICATOR_VERSION, text_color = TEXT_MUTED, text_size = dashFontSizeSub, text_halign = text.align_center)
    table.merge_cells(dashTable, 0, r, 1, r)
 
// ══════════════════════════════════════════════════════════
// 14. WATERMARK
// ══════════════════════════════════════════════════════════
if barstate.islast and showWatermarkInput
    var table wmTable = table.new(position.bottom_center, 1, 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))
 
// ══════════════════════════════════════════════════════════
// 15. ALERTS
// ══════════════════════════════════════════════════════════
if openLong or openShort
    string aPrice = str.tostring(close, format.mintick)
    string aScore = str.tostring(nz(sigScore), "#")
    string aSL    = str.tostring(nz(activeSL),  format.mintick)
    string aTP1   = str.tostring(nz(activeTP1), format.mintick)
    string aTP2   = str.tostring(nz(activeTP2), format.mintick)
    string aTP3   = str.tostring(nz(activeTP3), format.mintick)
    string aRR    = str.tostring(effTP1, "#.#")
 
    if openLong
        string jsonBuy = '{"action":"buy","ticker":"' + syminfo.tickerid + '","price":' + aPrice + ',"score":' + aScore + ',"sl":' + aSL + ',"tp1":' + aTP1 + ',"tp2":' + aTP2 + ',"tp3":' + aTP3 + ',"rr":' + aRR + '}'
        string textBuy = "🟢 LONG (Sweep) | " + syminfo.tickerid + " | TF: " + timeframe.period + " | Score: " + aScore + " | Price: " + aPrice + " | SL: " + aSL + " | TP1: " + aTP1 + " | TP2: " + aTP2 + " | TP3: " + aTP3 + " | R:R: " + aRR
        alert(webhookInput ? jsonBuy : textBuy, alert.freq_once_per_bar_close)
 
    if openShort
        string jsonSell = '{"action":"sell","ticker":"' + syminfo.tickerid + '","price":' + aPrice + ',"score":' + aScore + ',"sl":' + aSL + ',"tp1":' + aTP1 + ',"tp2":' + aTP2 + ',"tp3":' + aTP3 + ',"rr":' + aRR + '}'
        string textSell = "🔴 SHORT (Sweep) | " + syminfo.tickerid + " | TF: " + timeframe.period + " | Score: " + aScore + " | Price: " + aPrice + " | SL: " + aSL + " | TP1: " + aTP1 + " | TP2: " + aTP2 + " | TP3: " + aTP3 + " | R:R: " + aRR
        alert(webhookInput ? jsonSell : textSell, alert.freq_once_per_bar_close)
 
if slHit and showSlHitAlertInput
    string textSlHit = (exitBeActive ? "🛡️ BE STOP-OUT | " : "🛑 SL HIT | ") + syminfo.tickerid + " | Entry: " + str.tostring(nz(hitEntry), format.mintick) + " | SL: " + str.tostring(nz(hitSL), format.mintick)
    alert(textSlHit, alert.freq_once_per_bar_close)
 
if beJustActivated and alertTpHitInput
    alert("🛡️ BREAK-EVEN | " + syminfo.tickerid + " | SL moved to " + str.tostring(nz(exitEntry), format.mintick), alert.freq_once_per_bar_close)
if tp1FirstTouch and alertTpHitInput
    alert("🎯 TP1 HIT | " + syminfo.tickerid + " | Price: " + str.tostring(nz(exitTP1), format.mintick), alert.freq_once_per_bar_close)
if tp2FirstTouch and alertTpHitInput
    alert("🎯🎯 TP2 HIT | " + syminfo.tickerid + " | Price: " + str.tostring(nz(exitTP2), format.mintick), alert.freq_once_per_bar_close)
if tp3FirstTouch and alertTpHitInput
    alert("🏆 TP3 HIT | " + syminfo.tickerid + " | Price: " + str.tostring(nz(exitTP3), format.mintick), alert.freq_once_per_bar_close)

← 返回列表