本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。
//@version=6
// ══════════════════════════════════════════════════════════
// STRUCTURE-ANCHORED VWAP [WillyAlgoTrader]
// ══════════════════════════════════════════════════════════
// Author: Willy | WillyAlgoTrader
// Version: 1.1.0
// ══════════════════════════════════════════════════════════
indicator(
title = "Structure-Anchored VWAP [WillyAlgoTrader]",
shorttitle = "SA-VWAP [WAT]",
overlay = true,
max_bars_back = 5000,
max_lines_count = 500,
max_labels_count = 500,
max_polylines_count = 100)
// ══════════════════════════════════════════════════════════
// 1. CONSTANTS
// ══════════════════════════════════════════════════════════
GRP_MAIN = "⚙️ Main Settings"
GRP_VWAP = "📐 Anchored VWAP"
GRP_BANDS = "📏 Deviation Bands"
GRP_SIGNAL = "🎯 Signals & Levels"
GRP_RISK = "🛡️ Risk Management"
GRP_VISUAL = "🎨 Visual"
GRP_DASH = "📊 Dashboard"
GRP_ALERT = "🔔 Alerts"
GRP_COLORS = "🎨 Colors"
INDICATOR_VERSION = "v1.1.0"
INDICATOR_NAME = "Structure-Anchored VWAP"
BRAND_NAME = "WillyAlgoTrader"
LINE_FORWARD_BARS = 20
LINE_UPDATE_BARS = 5
LABEL_OFFSET_BARS = 1
BE_DIM_TRANSP = 70
FORM_LEN = 10 // recent-form strip length
// ══════════════════════════════════════════════════════════
// 2. INPUTS
// ══════════════════════════════════════════════════════════
// ── Main Settings ───────────────────────────────────────────
pivLInput = input.int(55, "Pivot strength left", minval = 1, maxval = 200, inline = "piv", group = GRP_MAIN)
pivRInput = input.int(55, "right", minval = 1, maxval = 200, inline = "piv", group = GRP_MAIN,
tooltip = "A swing high needs this many lower highs on each side (same idea for lows).\n" +
"• Right = confirmation delay in bars — a pivot is only known that many bars later\n" +
"• 5/5 scalping | 20/20 intraday | 55/55 default, large swings\n" +
"• Higher = fewer, more significant legs")
minAmpInput = input.float(1.5, "Minimum swing size (x ATR)", minval = 0.0, maxval = 10.0, step = 0.25, group = GRP_MAIN,
tooltip = "A new pivot must sit at least this far from the previous opposite pivot.\n" +
"• Filters noise inside ranges\n" +
"• 0 = off | 1.5 = recommended | 3.0+ = only large swings")
atrLenInput = input.int(13, "ATR Length", minval = 2, maxval = 100, group = GRP_MAIN,
tooltip = "ATR period used by the swing-size filter and the equal-high/low tolerance.\n" +
"• Lower = filter reacts faster to volatility changes\n• Recommended: 10-20")
anchorModeInput = input.string("Swing", "Re-anchor on", options = ["Swing", "Structure break", "Fast", "Manual"], group = GRP_MAIN,
tooltip = "Swing: a new leg starts at every confirmed pivot (delay = right bars).\n" +
"Structure break: a new leg starts only when price closes beyond the previous swing (BOS); " +
"the anchor is the extreme that preceded the break. Fewer, cleaner legs.\n" +
"Fast: no confirmation. The bar printing the highest high (lowest low) of the lookback is a new " +
"extreme; the leg flips as soon as an extreme opposite to the previous one appears and is anchored " +
"at that previous extreme. Immediate reaction, more sensitive to noise.\n" +
"Manual: one leg from the date and time below — behaves like the built-in Anchored VWAP tool.")
fastLenInput = input.int(30, "Fast mode: extreme lookback (bars)", minval = 2, maxval = 500, group = GRP_MAIN,
tooltip = "Used by the Fast mode only.\n• Larger = fewer, bigger legs\n• Smaller = flips more often")
eqTolInput = input.float(0.1, "Equal high/low tolerance (x ATR)", minval = 0.0, maxval = 2.0, step = 0.05, group = GRP_MAIN,
tooltip = "Two pivots closer than this are labelled EQH / EQL instead of HH / LL.\n" +
"• 0 = never label equal highs/lows\n• Recommended: 0.05-0.25")
manualTimeInput = input.time(timestamp("2026-09-01 00:00 +0000"), "Manual anchor (date & time)", group = GRP_MAIN,
tooltip = "Manual mode only. The leg starts at the first bar at or after this time.\n" +
"Shown in the chart's timezone, so 00:00 UTC appears as 05:00 on a UTC+5 chart.")
// ── Anchored VWAP ───────────────────────────────────────────
srcInput = input.source(hl2, "Price source", group = GRP_VWAP,
tooltip = "Price used inside the VWAP.\n" +
"• hl2 = (H+L)/2 — matches the built-in Anchored VWAP tool\n" +
"• hlc3 = typical price, gives closes more weight\n• close = most reactive")
weightModeInput = input.string("Cumulative", "Weighting", options = ["Cumulative", "Half-life"], group = GRP_VWAP,
tooltip = "Cumulative: a true anchored VWAP — every bar of the leg keeps its full weight.\n" +
"Half-life: older bars fade, their weight halves every N bars. Hugs price on long legs; " +
"not a textbook VWAP, but useful on instruments without volume.")
halfLifeInput = input.int(21, "Half-life (bars)", minval = 1, maxval = 500, group = GRP_VWAP,
tooltip = "Half-life weighting only. After this many bars a bar's weight is halved.\n" +
"• Lower = line clings to price\n• Higher = closer to a plain VWAP")
clampVolInput = input.bool(true, "Clamp volume spikes at", inline = "clamp", group = GRP_VWAP)
clampMultInput = input.float(4.0, "x median", minval = 1.0, maxval = 50.0, step = 0.5, inline = "clamp", group = GRP_VWAP,
tooltip = "Bars with volume above N x the 50-bar median are counted at the cap, so a single print " +
"cannot drag the whole VWAP.\n• 4 = recommended for crypto\n• Turn off for clean futures data")
maxLegBarsInput = input.int(4000, "Max leg length (bars)", minval = 100, maxval = 4900, group = GRP_VWAP,
tooltip = "A leg longer than this rolls its anchor forward one bar at a time, so the calculation " +
"stays bounded on very old anchors. Lower it if the script feels slow.")
// ── Deviation Bands ─────────────────────────────────────────
showB1Input = input.bool(true, "Band 1", inline = "b1", group = GRP_BANDS)
mult1Input = input.float(0.5, "Sigma", minval = 0.1, maxval = 10.0, step = 0.1, inline = "b1", group = GRP_BANDS,
tooltip = "Inner band, in volume-weighted standard deviations of price around the anchored VWAP, " +
"measured over the leg itself.\n• 0.5 = value area edge\n• 1.0 = classic 1 sigma")
showB2Input = input.bool(false, "Band 2", inline = "b2", group = GRP_BANDS)
mult2Input = input.float(2.0, "Sigma", minval = 0.1, maxval = 10.0, step = 0.1, inline = "b2", group = GRP_BANDS,
tooltip = "Outer band. Also defines the Premium / Discount extremes used by the dashboard and alerts.\n" +
"• 2.0 = statistically stretched\n• 2.5-3.0 = only true extremes")
fillTInput = input.int(90, "Fill transparency", minval = 50, maxval = 99, step = 1, group = GRP_BANDS,
tooltip = "Transparency of the band fill.\n• 90 = subtle (recommended)\n• 99 = outline only")
// ── Signals & Levels ────────────────────────────────────────
retestMinAwayInput = input.int(5, "Bars away before a retest counts", minval = 1, maxval = 100, group = GRP_SIGNAL,
tooltip = "Price must stay on the leg's side of the VWAP, outside the touch tolerance, for this many " +
"bars before a touch counts as a retest entry.\n• Higher = fewer, cleaner signals")
retestTolInput = input.float(0.25, "Touch tolerance (x sigma)", minval = 0.0, maxval = 3.0, step = 0.05, group = GRP_SIGNAL,
tooltip = "How close price must come to the VWAP to count as a touch, in sigma.\n" +
"• 0 = the bar must cross the line exactly\n• 0.25 = recommended")
showMemoInput = input.bool(true, "VWAP memory levels", group = GRP_SIGNAL,
tooltip = "When a leg ends, its last VWAP value stays on the chart as a dashed level until price " +
"trades through it. Old anchored VWAPs often keep acting as support and resistance.")
memoMaxInput = input.int(4, "Max memory levels", minval = 1, maxval = 20, group = GRP_SIGNAL,
tooltip = "How many untouched memory levels to keep. Older ones are dropped first.")
memoKeepInput = input.bool(false, "Keep crossed levels (dotted)", group = GRP_SIGNAL,
tooltip = "On: a level price has traded through stays as a faded dotted line.\nOff: it is removed.")
// ── Risk Management ─────────────────────────────────────────
riskPresetInput = input.string("Balanced", "Risk Preset",
options = ["Conservative", "Balanced", "Aggressive", "Scalping", "Custom"], group = GRP_RISK,
tooltip = "Quick risk presets (SL ×ATR / TP1 / TP2 / TP3 in R):\n" +
"• Conservative: 2.5 / 1R / 2R / 4R\n" +
"• Balanced: 1.5 / 1R / 2R / 3R\n" +
"• Aggressive: 1.0 / 1.5R / 2.5R / 4R\n" +
"• Scalping: 0.8 / 0.8R / 1.5R / 2R\n" +
"• Custom: use manual inputs below")
atrLenRiskInput = input.int(13, "ATR Length (SL)", minval = 5, maxval = 50, group = GRP_RISK,
tooltip = "ATR period for stop-loss distance.\n14 = standard on most timeframes")
slMultInput = input.float(1.5, "SL ×ATR (Custom)", minval = 0.5, maxval = 5.0, step = 0.1, group = GRP_RISK,
tooltip = "Custom stop distance = ATR × this. Used only 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,
tooltip = "TP2 as multiple of risk. 2.0 = 1:2 R:R")
tp3MultInput = input.float(3.0, "TP3 ×Risk (Custom)", minval = 1.5, maxval = 15.0, step = 0.1, group = GRP_RISK,
tooltip = "TP3 as multiple of risk. 3.0 = 1:3 R:R")
showSlTpInput = input.bool(true, "Show SL/TP Lines", group = GRP_RISK,
tooltip = "Draw stop and take-profit lines after each entry.\n" +
"Lines persist showing the last trade's outcome until the next entry.")
showSlTpLabelsInput = input.bool(true, "Show SL/TP Labels", group = GRP_RISK,
tooltip = "Show price labels on the SL/TP lines")
showPctOnLabelsInput = input.bool(true, "Show % Distance on Labels", group = GRP_RISK,
tooltip = "Append % distance from entry to SL/TP labels.\nExample: 'SL 78120.5 (-0.36%)'.")
useBreakEvenInput = input.bool(true, "Break-Even After TP1", group = GRP_RISK,
tooltip = "When TP1 is hit, move stop-loss to entry price.\n" +
"From the next bar onward, any wick touching entry closes the\n" +
"trade at break-even. (TP1 still counts as a WIN.)")
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,
tooltip = "SL is the most critical risk level — keep it visually prominent.")
tpLineStyleInput = input.string("Dashed", "TP Line Style",
options = ["Solid", "Dashed", "Dotted"], group = GRP_RISK,
tooltip = "Style for TP1/TP2/TP3.\nOn hit, a TP line turns solid teal regardless.")
// ── 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 manually")
showSignalsInput = input.bool(true, "Show Buy/Sell Signals", group = GRP_VISUAL,
tooltip = "Print Long ▲ / Short ▼ markers when a trade opens on a VWAP retest")
showLabelsInput = input.bool(true, "Show HH/HL/LH/LL", inline = "lbl", group = GRP_VISUAL)
lblSizeInput = input.string("small", "", options = ["tiny", "small", "normal"], inline = "lbl", group = GRP_VISUAL,
tooltip = "Label every confirmed pivot with its structure type, and its size.")
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. Use Tiny for a compact look.")
showBgInput = input.bool(false, "Show Leg Background", group = GRP_VISUAL,
tooltip = "Tint the chart background with the current leg direction. Off by default to keep candles clean.")
maxLegsInput = input.int(30, "Finished legs kept on chart", minval = 1, maxval = 45, group = GRP_VISUAL,
tooltip = "How many completed legs stay drawn. Lower it if the chart feels crowded or slow.")
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,
tooltip = "Master toggle for the dashboard table")
dashPosStr = input.string("Top Right", "Dashboard Position",
options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right", "Middle Right"],
group = GRP_DASH)
showMarketSecInput = input.bool(true, " ├ Market section", group = GRP_DASH,
tooltip = "Trend, Signal, Strength, Last swing, Timeframe")
showVwapSecInput = input.bool(true, " ├ VWAP section", group = GRP_DASH,
tooltip = "Anchor, VWAP value, distance in sigma and %, zone, volume balance")
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 alerts as JSON for webhook / autotrading integrations")
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.")
alertStructInput = input.bool(false, "Alert on New Leg / VWAP Cross", group = GRP_ALERT,
tooltip = "Fire alerts when a new leg is anchored and when price closes across the VWAP.")
// ── Colors ──────────────────────────────────────────────────
bullColorInput = input.color(#00E676, "Bull", inline = "c1", group = GRP_COLORS)
bearColorInput = input.color(#FF5252, "Bear", inline = "c1", group = GRP_COLORS)
lineWInput = input.int(2, "VWAP line width", minval = 1, maxval = 5, group = GRP_COLORS,
tooltip = "Thickness of the anchored VWAP curve. 2 = recommended.")
// ══════════════════════════════════════════════════════════
// 3. THEME DETECTION & COLORS
// ══════════════════════════════════════════════════════════
isDark = switch themeInput
"Dark" => true
"Light" => false
=> color.r(chart.bg_color) < 128 // Auto
BULL_COLOR = bullColorInput
BEAR_COLOR = bearColorInput
BULL_TEXT = isDark ? #00E676 : #00A152
BEAR_TEXT = isDark ? #FF5252 : #D32F2F
NEUTRAL_TEXT = isDark ? #FFEB3B : #F57F17
DISCOUNT_TEXT = isDark ? #4FC3F7 : #0277BD
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)
// Trend-tinted header backgrounds
HEADER_BG_BULL = color.new(#0F6E56, 0)
HEADER_BG_BEAR = color.new(#A32D2D, 0)
HEADER_BG_FLAT = color.new(#2962FF, 0)
// Risk-Management palette (premium, muted, theme-aware)
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
// ══════════════════════════════════════════════════════════
// 4. TYPES, STATE & HELPERS
// ══════════════════════════════════════════════════════════
// Division that never crashes and never returns na by accident.
safeDiv(float num, float den, float fallback) =>
den != 0 and not na(num) and not na(den) ? num / den : fallback
fmtPrice(float v) =>
na(v) ? "—" : str.tostring(v, format.mintick)
// Signed number for the dashboard: "+1.23%" / "-0.45 sigma" / "—" when na.
fmtSigned(float v, string suffix) =>
na(v) ? "—" : (v >= 0 ? "+" : "") + str.tostring(v, "#.##") + suffix
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
// Unicode block gauge: filled ▰ proportional to value/maxVal across `width` cells
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
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
sizeOf(string s) =>
s == "tiny" ? size.tiny : s == "small" ? size.small : size.normal
// Structure tag for a new pivot against the previous one of the same kind.
classify(float cur, float ref, bool isHigh, float tolAbs) =>
string tag = ""
if na(ref)
tag := isHigh ? "H" : "L"
else if math.abs(cur - ref) <= tolAbs
tag := isHigh ? "EQH" : "EQL"
else if cur > ref
tag := isHigh ? "HH" : "HL"
else
tag := isHigh ? "LH" : "LL"
tag
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)
// ── Types ─────────────────────────────────────────────────
type Leg
int dir // +1 bullish, -1 bearish
int anchor // bar_index of the anchor
array<chart.point> trail // the VWAP curve
array<chart.point> hi1
array<chart.point> lo1
array<chart.point> hi2
array<chart.point> lo2
float anchorPx = na
polyline curve = na
polyline band1 = na
polyline band2 = na
float vwap = na
float sigma = na
bool hitNow = false
// live values and their committed (last confirmed bar) counterparts
float volUp = 0.0
float volDn = 0.0
int away = 0
int retests = 0
float volUpC = 0.0
float volDnC = 0.0
int awayC = 0
int retestsC = 0
type Memo
line ln
float y
color col
bool alive = true
newLeg(int d, int a) =>
Leg.new(d, a, array.new<chart.point>(), array.new<chart.point>(), array.new<chart.point>(),
array.new<chart.point>(), array.new<chart.point>())
// Closed contour for a band: upper edge forward, lower edge backward.
contour(array<chart.point> top, array<chart.point> bottom) =>
array<chart.point> out = array.new<chart.point>()
for i = 0 to top.size() - 1
out.push(top.get(i))
for i = bottom.size() - 1 to 0
out.push(bottom.get(i))
out
// ══════════════════════════════════════════════════════════
// 5. WARMUP
// ══════════════════════════════════════════════════════════
bool settled = barstate.isconfirmed
float atr = nz(ta.atr(atrLenInput), 0.0)
int WARMUP_BARS = math.max(pivLInput + pivRInput, 50)
bool isWarmedUp = bar_index >= WARMUP_BARS
// ══════════════════════════════════════════════════════════
// 6. MARKET STRUCTURE ENGINE
// ══════════════════════════════════════════════════════════
float pivHi = ta.pivothigh(high, pivLInput, pivRInput)
float pivLo = ta.pivotlow(low, pivLInput, pivRInput)
int pivBar = bar_index - pivRInput
float minSwing = minAmpInput * atr
float eqTolAbs = eqTolInput * atr
var int swType = 0 // last confirmed swing: +1 high, -1 low, 0 none
var float swHi = na
var int swHiBar = na
var float swLo = na
var int swLoBar = na
var float refHi = na // previous high of record, for HH / LH
var float refLo = na // previous low of record, for HL / LL
var label swHiLbl = na
var label swLoLbl = na
var string lastStruct = "—"
bool hiEvent = false
bool loEvent = false
bool hiExt = false // previous high superseded by a higher one
bool loExt = false
if settled and not na(pivHi)
if swType == 1
if pivHi > swHi
swHi := pivHi
swHiBar := pivBar
hiEvent := true
hiExt := true
else if swType == 0 or pivHi - swLo >= minSwing
refHi := swHi
swHi := pivHi
swHiBar := pivBar
swType := 1
hiEvent := true
if settled and not na(pivLo)
if swType == -1
if pivLo < swLo
swLo := pivLo
swLoBar := pivBar
loEvent := true
loExt := true
else if swType == 0 or swHi - pivLo >= minSwing
refLo := swLo
swLo := pivLo
swLoBar := pivBar
swType := -1
loEvent := true
if hiEvent
string tag = classify(swHi, refHi, true, eqTolAbs)
lastStruct := tag
if hiExt
label.delete(swHiLbl)
if showLabelsInput
swHiLbl := label.new(swHiBar, swHi, tag, xloc.bar_index, yloc.price, color.new(color.black, 100),
label.style_label_down, tag == "HH" ? BULL_TEXT : NEUTRAL_TEXT, sizeOf(lblSizeInput),
tooltip = "Swing high " + str.tostring(swHi, format.mintick) + "\nconfirmed after " + str.tostring(pivRInput) + " bars")
if loEvent
string tag = classify(swLo, refLo, false, eqTolAbs)
lastStruct := tag
if loExt
label.delete(swLoLbl)
if showLabelsInput
swLoLbl := label.new(swLoBar, swLo, tag, xloc.bar_index, yloc.price, color.new(color.black, 100),
label.style_label_up, tag == "LL" ? BEAR_TEXT : BULL_TEXT, sizeOf(lblSizeInput),
tooltip = "Swing low " + str.tostring(swLo, format.mintick) + "\nconfirmed after " + str.tostring(pivRInput) + " bars")
// ── Fast extremes (Fast mode) ─────────────────────────────
bool newHiN = high >= ta.highest(high, fastLenInput)
bool newLoN = low <= ta.lowest(low, fastLenInput)
var int extType = 0 // last extreme printed: +1 high, -1 low
var int extHiBar = na
var int extLoBar = na
bool fastUp = false // a high after a low -> bull leg from that low
bool fastDn = false // a low after a high -> bear leg from that high
if settled and (newHiN or newLoN)
bool bothWays = newHiN and newLoN
bool takeHi = newHiN and (not bothWays or close >= open)
bool takeLo = newLoN and (not bothWays or close < open)
if takeHi
extHiBar := bar_index
fastUp := extType == -1
extType := 1
if takeLo
extLoBar := bar_index
fastDn := extType == 1
extType := -1
// ══════════════════════════════════════════════════════════
// 7. ANCHORED VWAP — prefix sums & ring buffers
// ══════════════════════════════════════════════════════════
var bool hasVolume = false
hasVolume := hasVolume or nz(volume, 0.0) > 0
float px = nz(srcInput, close)
float volMed = nz(ta.median(volume, 50), 0.0)
float wRaw = hasVolume ? nz(volume, 0.0) : 1.0 // no volume feed -> time-weighted fallback
float wt = clampVolInput and hasVolume and volMed > 0 ? math.min(wRaw, volMed * clampMultInput) : wRaw
// With lam = 1 these are plain cumulative sums (a true VWAP); with lam < 1 every
// bar's weight decays by lam per bar. A decayed prefix sum still allows O(1)
// evaluation of any anchor: sum[a..t] = S(t) - lam^(t-a+1) * S(a-1)
float lam = weightModeInput == "Half-life" ? math.exp(-math.log(2.0) / halfLifeInput) : 1.0
var float cumW = 0.0
var float cumPW = 0.0
var float cumP2 = 0.0
cumW := lam * cumW + wt
cumPW := lam * cumPW + px * wt
cumP2 := lam * cumP2 + px * px * wt
// One entry per bar, newest last. Functions that run only on some bars read
// history from here instead of using [], which would give them a sparse and
// inconsistent history.
int CAP = maxLegBarsInput + 2
var array<float> bufW = array.new<float>()
var array<float> bufPW = array.new<float>()
var array<float> bufP2 = array.new<float>()
var array<float> bufC = array.new<float>()
var array<float> bufH = array.new<float>()
var array<float> bufL = array.new<float>()
var array<float> bufA = array.new<float>()
var int bufBar = -1
record(array<float> buf, float v) =>
if bufBar == bar_index and buf.size() > 0 // realtime tick: overwrite the provisional entry
buf.set(buf.size() - 1, v)
else
buf.push(v)
if buf.size() > CAP
buf.shift()
buf.size()
record(bufW, cumW)
record(bufPW, cumPW)
record(bufP2, cumP2)
record(bufC, close)
record(bufH, high)
record(bufL, low)
record(bufA, atr)
bufBar := bar_index
// Value `back` bars ago from a ring buffer; na when outside the buffer.
hist(array<float> buf, int back) =>
float out = na
int idx = buf.size() - 1 - back
if idx >= 0 and idx < buf.size()
out := buf.get(idx)
out
// VWAP and volume-weighted sigma at bar (bar_index - back)
// for a leg anchored at (bar_index - aBack).
legStats(int back, int aBack) =>
float f = lam == 1.0 ? 1.0 : math.pow(lam, aBack - back + 1)
float v = hist(bufW, back) - f * nz(hist(bufW, aBack + 1))
float pw = hist(bufPW, back) - f * nz(hist(bufPW, aBack + 1))
float p2 = hist(bufP2, back) - f * nz(hist(bufP2, aBack + 1))
float m = v > 0 ? pw / v : na
float sd = v > 0 ? math.sqrt(math.max(p2 / v - m * m, 0.0)) : 0.0
[m, sd]
// Bar of the lowest low (or highest high) strictly after `fromBar`, up to now.
extremeSince(int fromBar, bool wantLow) =>
int span = math.min(bar_index - nz(fromBar, bar_index - maxLegBarsInput), maxLegBarsInput)
int bestOff = 0
float bestVal = wantLow ? low : high
if span > 1
for i = 1 to span - 1
float v = wantLow ? hist(bufL, i) : hist(bufH, i)
if (wantLow and v < bestVal) or (not wantLow and v > bestVal)
bestVal := v
bestOff := i
bar_index - bestOff
// ── Leg methods ───────────────────────────────────────────
method erase(Leg this) =>
this.curve.delete()
this.band1.delete()
this.band2.delete()
method reset(Leg this) =>
this.trail.clear()
this.hi1.clear()
this.lo1.clear()
this.hi2.clear()
this.lo2.clear()
this.volUp := 0.0
this.volDn := 0.0
this.away := 0
this.retests := 0
this.volUpC := 0.0
this.volDnC := 0.0
this.awayC := 0
this.retestsC := 0
method putPoint(Leg this, int back, float m, float sd, bool commit) =>
int x = bar_index - back
// Realtime ticks re-enter the same bar: drop the provisional point first.
if this.trail.size() > 0 and this.trail.last().index == x
this.trail.pop()
if this.hi1.size() > 0 and this.hi1.last().index == x
this.hi1.pop()
this.lo1.pop()
if this.hi2.size() > 0 and this.hi2.last().index == x
this.hi2.pop()
this.lo2.pop()
this.trail.push(chart.point.from_index(x, m))
if showB1Input
this.hi1.push(chart.point.from_index(x, m + sd * mult1Input))
this.lo1.push(chart.point.from_index(x, m - sd * mult1Input))
if showB2Input
this.hi2.push(chart.point.from_index(x, m + sd * mult2Input))
this.lo2.push(chart.point.from_index(x, m - sd * mult2Input))
// Statistics always restart from the committed state, so a provisional bar
// is never counted twice.
float wBar = hist(bufW, back) - lam * nz(hist(bufW, back + 1))
float cB = hist(bufC, back)
float hB = hist(bufH, back)
float lB = hist(bufL, back)
bool above = cB >= m
this.volUp := this.volUpC + (above ? wBar : 0.0)
this.volDn := this.volDnC + (above ? 0.0 : wBar)
float tol = sd > 0 ? sd * retestTolInput : nz(hist(bufA, back), 0.0) * 0.1
bool touch = lB <= m + tol and hB >= m - tol
bool outside = this.dir > 0 ? lB > m + tol : hB < m - tol
bool hit = touch and this.awayC >= retestMinAwayInput
this.away := touch ? 0 : outside ? this.awayC + 1 : 0
this.retests := this.retestsC + (hit ? 1 : 0)
this.hitNow := hit
this.vwap := m
this.sigma := sd
if commit
this.volUpC := this.volUp
this.volDnC := this.volDn
this.awayC := this.away
this.retestsC := this.retests
// Full pass from the anchor to the current bar — runs once per re-anchor.
method rebuild(Leg this) =>
this.reset()
int aBack = math.min(bar_index - this.anchor, maxLegBarsInput)
this.anchor := bar_index - aBack
this.anchorPx := this.dir > 0 ? hist(bufL, aBack) : hist(bufH, aBack)
for i = aBack to 0
[m, s] = legStats(i, aBack)
if not na(m)
this.putPoint(i, m, s, i > 0 or settled)
// One bar forward. Legs beyond the limit roll their anchor to stay bounded.
method advance(Leg this) =>
if bar_index - this.anchor > maxLegBarsInput
this.anchor := bar_index - maxLegBarsInput
while this.trail.size() > 0 and this.trail.first().index < this.anchor
this.trail.shift()
while this.hi1.size() > 0 and this.hi1.first().index < this.anchor
this.hi1.shift()
this.lo1.shift()
while this.hi2.size() > 0 and this.hi2.first().index < this.anchor
this.hi2.shift()
this.lo2.shift()
[m, s] = legStats(0, bar_index - this.anchor)
if not na(m)
this.putPoint(0, m, s, settled)
// Cut the leg at the bar where the next one starts, so segments hand over
// cleanly and the curve never breaks.
method trimTo(Leg this, int lastBar) =>
while this.trail.size() > 0 and this.trail.last().index > lastBar
this.trail.pop()
while this.hi1.size() > 0 and this.hi1.last().index > lastBar
this.hi1.pop()
this.lo1.pop()
while this.hi2.size() > 0 and this.hi2.last().index > lastBar
this.hi2.pop()
this.lo2.pop()
this.vwap := this.trail.size() > 0 ? this.trail.last().price : na
method draw(Leg this) =>
this.erase()
color base = this.dir > 0 ? BULL_COLOR : BEAR_COLOR
if showB2Input and this.hi2.size() >= 2
this.band2 := polyline.new(contour(this.hi2, this.lo2), false, true, xloc.bar_index,
line_color = color.new(base, 85), fill_color = color.new(base, fillTInput),
line_style = line.style_dotted, line_width = 1)
if showB1Input and this.hi1.size() >= 2
this.band1 := polyline.new(contour(this.hi1, this.lo1), false, true, xloc.bar_index,
line_color = color.new(base, 70), fill_color = color.new(base, fillTInput),
line_style = line.style_dotted, line_width = 1)
if this.trail.size() >= 2
this.curve := polyline.new(this.trail, false, false, xloc.bar_index,
line_color = base, line_width = lineWInput)
// ── Memory levels ─────────────────────────────────────────
var array<Memo> memos = array.new<Memo>()
memoCrossed(Memo mm, int atBar) =>
mm.alive := false
mm.ln.set_x2(atBar)
if memoKeepInput
mm.ln.set_style(line.style_dotted)
mm.ln.set_color(color.new(mm.col, 75))
else
mm.ln.delete()
addMemo(float y, int fromBar, color col) =>
if not na(y)
Memo mm = Memo.new(line.new(fromBar, y, bar_index, y, xloc.bar_index, extend.none,
color.new(col, 35), line.style_dashed, 1), y, col)
// Price may already have traded through it between the anchor and now.
int span = bar_index - fromBar
if span > 1
for i = span - 1 to 1
if hist(bufL, i) <= y and hist(bufH, i) >= y
memoCrossed(mm, bar_index - i)
break
if mm.alive or memoKeepInput
memos.push(mm)
while memos.size() > memoMaxInput
Memo old = memos.shift()
old.ln.delete()
updateMemos() =>
if memos.size() > 0
for i = memos.size() - 1 to 0
Memo mm = memos.get(i)
if mm.alive
mm.ln.set_x2(bar_index)
if low <= mm.y and high >= mm.y
memoCrossed(mm, bar_index)
if not memoKeepInput
memos.remove(i)
// ── Anchor selection & leg management ─────────────────────
var int legDir = 0
var int anchorBar = na
int nextDir = 0
int nextAnchor = na
if anchorModeInput == "Swing"
if hiEvent or loEvent
nextDir := swType == 1 ? -1 : 1
nextAnchor := swType == 1 ? swHiBar : swLoBar
else if anchorModeInput == "Fast"
if fastUp
nextDir := 1
nextAnchor := extLoBar
else if fastDn
nextDir := -1
nextAnchor := extHiBar
else if anchorModeInput == "Manual"
if settled and time >= manualTimeInput and nz(time[1], 0) < manualTimeInput
nextDir := 1
nextAnchor := bar_index
else if settled
bool brkUp = legDir != 1 and not na(swHi) and close > swHi
bool brkDn = legDir != -1 and not na(swLo) and close < swLo
if brkUp
nextDir := 1
nextAnchor := extremeSince(swHiBar, true)
else if brkDn
nextDir := -1
nextAnchor := extremeSince(swLoBar, false)
var array<Leg> archive = array.new<Leg>()
var Leg live = na
bool reanchored = false
if not na(nextAnchor)
reanchored := true
if not na(live)
// The previous leg always stays on the chart up to the new anchor, so the
// curve never breaks. This also covers a superseded pivot (a higher high
// after a high, or a lower low after a low): that stretch keeps its VWAP.
live.trimTo(nextAnchor)
live.draw()
if showMemoInput and nextDir != live.dir // memory level only on a real flip
addMemo(live.vwap, nextAnchor, live.dir > 0 ? BULL_COLOR : BEAR_COLOR)
archive.push(live)
while archive.size() > maxLegsInput
Leg old = archive.shift()
old.erase()
live := newLeg(nextDir, nextAnchor)
live.rebuild()
legDir := nextDir
anchorBar := live.anchor
else if not na(live)
live.advance()
updateMemos()
if not na(live) and barstate.islast
live.draw()
// ── Leg readouts ──────────────────────────────────────────
float vwapNow = na
float sigNow = na
float legBalance = na // share of leg weight traded above VWAP, 0..100
float legAnchorPx = na
float legMove = na
int legBars = 0
int legRetests = 0
bool retestBull = false
bool retestBear = false
if not na(live)
vwapNow := live.vwap
sigNow := live.sigma
legBars := bar_index - live.anchor
legRetests := live.retests
legAnchorPx := live.anchorPx
legMove := safeDiv(close - nz(live.anchorPx, close), nz(live.anchorPx, close), 0.0) * 100.0
legBalance := safeDiv(live.volUp, live.volUp + live.volDn, na) * 100.0
retestBull := live.dir > 0 and live.hitNow
retestBear := live.dir < 0 and live.hitNow
float up1 = vwapNow + sigNow * mult1Input
float dn1 = vwapNow - sigNow * mult1Input
float up2 = vwapNow + sigNow * mult2Input
float dn2 = vwapNow - sigNow * mult2Input
float distSig = safeDiv(close - nz(vwapNow, close), nz(sigNow, 0.0), na)
float distPct = safeDiv(close - nz(vwapNow, close), nz(vwapNow, close), 0.0) * 100.0
string zoneStr = na(distSig) ? "—" : distSig > mult2Input ? "Premium" : distSig < -mult2Input ? "Discount" : "Fair value"
color zoneCol = zoneStr == "Premium" ? NEUTRAL_TEXT : zoneStr == "Discount" ? DISCOUNT_TEXT : TEXT_COLOR
// ══════════════════════════════════════════════════════════
// 8. ENTRY SIGNALS
// ══════════════════════════════════════════════════════════
// A retest of the anchored VWAP, in the direction of the current leg.
bool confirmedBuy = retestBull and settled and isWarmedUp
bool confirmedSell = retestBear and settled and isWarmedUp
bool evAnchorBull = reanchored and legDir > 0 and settled and isWarmedUp
bool evAnchorBear = reanchored and legDir < 0 and settled and isWarmedUp
bool hasPrevVwap = not na(vwapNow) and not na(vwapNow[1])
bool evCrossUp = hasPrevVwap and not reanchored and close > vwapNow and close[1] <= vwapNow[1] and settled and isWarmedUp
bool evCrossDn = hasPrevVwap and not reanchored and close < vwapNow and close[1] >= vwapNow[1] and settled and isWarmedUp
var string lastEventStr = "—"
if evAnchorBull
lastEventStr := "New leg ▲"
if evAnchorBear
lastEventStr := "New leg ▼"
if confirmedBuy
lastEventStr := "Retest ▲"
if confirmedSell
lastEventStr := "Retest ▼"
// ── Signal strength (0-100, heuristic composite) ──────────
// 40 pts — leg weight balance agreeing with the leg direction
// 25 pts — price on the leg's side of the VWAP
// 20 pts — price not stretched beyond band 2
// 15 pts — the leg has already produced at least one retest
float balAlign = na(legBalance) ? 50.0 : (legDir > 0 ? legBalance : 100.0 - legBalance)
float scoreRaw = 0.0
scoreRaw += balAlign * 0.4
scoreRaw += (legDir != 0 and not na(distSig) and distSig * legDir > 0) ? 25.0 : 0.0
scoreRaw += (not na(distSig) and math.abs(distSig) <= mult2Input) ? 20.0 : 0.0
scoreRaw += legRetests > 0 ? 15.0 : 0.0
float strengthVal = legDir == 0 ? na : math.min(scoreRaw, 100.0)
// ══════════════════════════════════════════════════════════
// 9. RISK MANAGEMENT — single position, SL/TP/BE, win-rate
// ══════════════════════════════════════════════════════════
[effSLMult, effTP1, effTP2, effTP3] = switch riskPresetInput
"Conservative" => [2.5, 1.0, 2.0, 4.0]
"Aggressive" => [1.0, 1.5, 2.5, 4.0]
"Scalping" => [0.8, 0.8, 1.5, 2.0]
"Custom" => [slMultInput, tp1MultInput, tp2MultInput, tp3MultInput]
=> [1.5, 1.0, 2.0, 3.0] // Balanced
float riskAtr = nz(ta.atr(atrLenRiskInput), 0.0)
float slDistance = riskAtr * effSLMult
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
// Session win-rate counters + recent-form strip (reset on reload / input change)
var int statWins = 0
var int statLosses = 0
var string formStr = ""
bool canOpen = activeDir == 0
bool openLong = confirmedBuy and slDistance > 0 and canOpen
bool openShort = confirmedSell and slDistance > 0 and canOpen
if openLong
activeEntry := close
activeSL := close - slDistance
activeTP1 := close + slDistance * effTP1
activeTP2 := close + slDistance * effTP2
activeTP3 := close + slDistance * effTP3
activeDir := 1
entryBar := bar_index
tp1Reached := false
tp2Reached := false
tp3Reached := false
beActive := false
if openShort
activeEntry := close
activeSL := close + slDistance
activeTP1 := close - slDistance * effTP1
activeTP2 := close - slDistance * effTP2
activeTP3 := close - slDistance * effTP3
activeDir := -1
entryBar := bar_index
tp1Reached := false
tp2Reached := false
tp3Reached := false
beActive := false
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
var float hitSL = na
var float hitEntry = na
if slHit
hitSL := effectiveSL
hitEntry := activeEntry
float exitEntry = activeEntry
float exitSL = effectiveSL
float exitTP1 = activeTP1
float exitTP2 = activeTP2
float exitTP3 = activeTP3
bool exitBeActive = beActiveAtBarStart
bool displayTp1Reached = tp1Reached
bool displayTp2Reached = tp2Reached
bool displayTp3Reached = tp3Reached
// Closure: SL, BE-stop (= SL once moved to entry) or TP3
if (slHit or tp3Hit) and activeDir != 0
bool tradeWin = tp1Reached
if tradeWin
statWins += 1
else
statLosses += 1
// Recent-form strip: ▰ = win (TP1 touched), ▱ = loss
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
// ══════════════════════════════════════════════════════════
// 10. PLOTS (entry markers + data-window series)
// ══════════════════════════════════════════════════════════
plotshape(showSignalsInput and openLong ? low : na, "Buy",
style = shape.labelup, location = location.belowbar,
color = BULL_COLOR, textcolor = BULL_LABEL_TEXT, text = "Long ▲", size = size.small)
plotshape(showSignalsInput and openShort ? high : na, "Sell",
style = shape.labeldown, location = location.abovebar,
color = BEAR_COLOR, textcolor = BEAR_LABEL_TEXT, text = "Short ▼", size = size.small)
plot(vwapNow, "VWAP", display = display.data_window)
plot(up1, "+1 band", display = display.data_window)
plot(dn1, "-1 band", display = display.data_window)
plot(up2, "+2 band", display = display.data_window)
plot(dn2, "-2 band", display = display.data_window)
plot(strengthVal, "Strength", display = display.data_window)
bgcolor(showBgInput and legDir != 0 ? color.new(legDir > 0 ? BULL_COLOR : BEAR_COLOR, 92) : na, title = "Leg Background")
// ══════════════════════════════════════════════════════════
// 11. 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)
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)
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)
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)
// ══════════════════════════════════════════════════════════
// 12. DASHBOARD (sectioned, dynamic height, gauges, tooltips)
// ══════════════════════════════════════════════════════════
var table dashTable = na
if showDashInput and barstate.islast
// ── Values ──
string trendWord = legDir == 1 ? "Bullish" : legDir == -1 ? "Bearish" : "Neutral"
color trendCol = legDir == 1 ? BULL_TEXT : legDir == -1 ? BEAR_TEXT : NEUTRAL_TEXT
color headerBg = legDir == 1 ? HEADER_BG_BULL : legDir == -1 ? HEADER_BG_BEAR : HEADER_BG_FLAT
string signalStr = activeDir == 1 ? "LONG" : activeDir == -1 ? "SHORT" : "Wait"
color signalCol = activeDir == 1 ? BULL_TEXT : activeDir == -1 ? BEAR_TEXT : TEXT_MUTED
string strengthStr = na(strengthVal) ? "—" : str.tostring(strengthVal, "#") + " " + buildGauge(strengthVal, 100, 8)
color strengthCol = na(strengthVal) ? TEXT_MUTED : strengthVal >= 70 ? BULL_TEXT : strengthVal >= 40 ? NEUTRAL_TEXT : BEAR_TEXT
string dashAnchor = legDir == 0 or na(legAnchorPx) ? "—" : lastStruct + " @ " + str.tostring(legAnchorPx, format.mintick)
string dashLegBars = legDir == 0 ? "—" : str.tostring(legBars) + " bars · " + fmtSigned(legMove, "%")
string dashMode = anchorModeInput + " / " + (weightModeInput == "Half-life" ? "half-life " + str.tostring(halfLifeInput) : "cumulative")
string dashVwap = fmtPrice(vwapNow)
string dashDist = na(distSig) ? "—" : fmtSigned(distSig, "σ") + " " + fmtSigned(distPct, "%")
string dashBalance = na(legBalance) ? "—" : str.tostring(legBalance, "#") + "% " + buildGauge(nz(legBalance), 100, 6)
string dashBalLbl = hasVolume ? "Vol above VWAP" : "Bars above VWAP"
color balanceCol = na(legBalance) ? TEXT_COLOR : legBalance >= 50 ? BULL_TEXT : BEAR_TEXT
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 dashDistPct = 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
// ── Row count (dynamic) ──
int marketRows = showMarketSecInput ? 6 : 0
int vwapRows = showVwapSecInput ? 8 : 0
int tradeRows = showTradeSecInput ? (activeDir != 0 ? 7 : 2) : 0
int statsRows = showStatsSecInput ? 5 : 0
int totalRows = 2 + marketRows + vwapRows + tradeRows + statsRows
// ── Rebuild table to fit current state (one table at a time) ──
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
// Header (trend-tinted)
table.cell(dashTable, 0, r, "◆ SA-VWAP · " + trendWord, text_color = HEADER_TEXT, bgcolor = headerBg, text_size = size.small, text_halign = text.align_center)
table.merge_cells(dashTable, 0, r, 1, r)
r += 1
// ── Market ──
if showMarketSecInput
table.cell(dashTable, 0, r, "— Market —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = size.tiny, 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 = size.small, bgcolor = rb, tooltip = "Direction of the current anchored leg.")
table.cell(dashTable, 1, r, trendWord, text_color = trendCol, text_size = size.small, 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 = size.small, bgcolor = rb, tooltip = "Current position. A new trade opens only while flat.")
table.cell(dashTable, 1, r, signalStr, text_color = signalCol, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Strength", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Composite 0-100: volume balance (40) + price on the leg's side (25) + not stretched past band 2 (20) + leg already retested (15). A context score, not a backtested edge.")
table.cell(dashTable, 1, r, strengthStr, text_color = strengthCol, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Last event", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Most recent leg anchor or VWAP retest.")
table.cell(dashTable, 1, r, lastEventStr, text_color = TEXT_COLOR, text_size = size.small, 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 = size.small, bgcolor = rb)
table.cell(dashTable, 1, r, timeframe.period, text_color = TEXT_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
// ── VWAP ──
if showVwapSecInput
table.cell(dashTable, 0, r, "— VWAP —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = size.tiny, 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, "Mode", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Anchor mode / weighting. Without volume data the VWAP falls back to time weighting.")
table.cell(dashTable, 1, r, dashMode, text_color = TEXT_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Anchor", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Structure tag and price of the bar the current leg is anchored to.")
table.cell(dashTable, 1, r, dashAnchor, text_color = TEXT_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Leg age", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Bars since the anchor and price move from it.")
table.cell(dashTable, 1, r, dashLegBars, text_color = TEXT_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "VWAP", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Anchored VWAP value on the current bar.")
table.cell(dashTable, 1, r, dashVwap, text_color = trendCol, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Price vs VWAP", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Distance from the VWAP in leg sigma and in percent.")
table.cell(dashTable, 1, r, dashDist, text_color = TEXT_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Zone", text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Premium above +band 2, Discount below -band 2, Fair value in between.")
table.cell(dashTable, 1, r, zoneStr, text_color = zoneCol, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, dashBalLbl, text_color = TEXT_MUTED, text_size = size.small, bgcolor = rb, tooltip = "Share of the leg's traded weight that changed hands above the VWAP. Above 50% = buyers control the leg.")
table.cell(dashTable, 1, r, dashBalance, text_color = balanceCol, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
// ── Trade (collapses to one row when flat) ──
if showTradeSecInput
table.cell(dashTable, 0, r, "— Trade —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = size.tiny, 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 = size.small, bgcolor = rb, tooltip = "Active stop. 'BE @' = moved to entry after TP1.")
table.cell(dashTable, 1, r, dashSL, text_color = dashSlCol, text_size = size.small, 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 = size.small, bgcolor = rb, tooltip = "✓ = touched (counts as a win).")
table.cell(dashTable, 1, r, dashTP1, text_color = dashTp1Col, text_size = size.small, 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 = size.small, bgcolor = rb)
table.cell(dashTable, 1, r, dashTP2, text_color = dashTp2Col, text_size = size.small, 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 = size.small, bgcolor = rb, tooltip = "Final target. Touching it closes the trade.")
table.cell(dashTable, 1, r, dashTP3, text_color = dashTp3Col, text_size = size.small, 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 = size.small, bgcolor = rb, tooltip = "Reward-to-risk at TP1.")
table.cell(dashTable, 1, r, dashRR, text_color = rrColor, text_size = size.small, 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 = size.small, bgcolor = rb, tooltip = "Stop distance as % of entry (not position risk).")
table.cell(dashTable, 1, r, dashDistPct, text_color = distColor, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
else
rb := zeb ? TABLE_ROW_ALT : TABLE_BG
table.cell(dashTable, 0, r, "Flat · waiting for a VWAP retest", text_color = TEXT_MUTED, text_size = size.small, text_halign = text.align_center, bgcolor = rb)
table.merge_cells(dashTable, 0, r, 1, r)
r += 1
zeb := not zeb
// ── Stats ──
if showStatsSecInput
table.cell(dashTable, 0, r, "— Stats —", text_color = TEXT_MUTED, bgcolor = TABLE_ROW_ALT, text_size = size.tiny, 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 = size.small, bgcolor = rb, tooltip = "Closed trades this session (wins + losses).")
table.cell(dashTable, 1, r, str.tostring(closedTrades), text_color = TEXT_COLOR, text_size = size.small, 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 = size.small, bgcolor = rb)
table.cell(dashTable, 1, r, dashWL, text_color = TEXT_COLOR, text_size = size.small, 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 = size.small, 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 = size.small, 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 = size.small, bgcolor = rb, tooltip = "Last " + str.tostring(FORM_LEN) + " closed trades. ▰ = win · ▱ = loss (newest on the right).")
table.cell(dashTable, 1, r, dashForm, text_color = FORM_COLOR, text_size = size.small, bgcolor = rb)
r += 1
zeb := not zeb
// Footer
table.cell(dashTable, 0, r, INDICATOR_NAME + " · " + INDICATOR_VERSION, text_color = TEXT_MUTED, text_size = size.tiny, text_halign = text.align_center)
table.merge_cells(dashTable, 0, r, 1, r)
// ══════════════════════════════════════════════════════════
// 13. 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, BRAND_NAME, text_color = WM_COLOR, text_size = size.normal, text_halign = text.align_center, bgcolor = color.new(color.black, 100))
// ══════════════════════════════════════════════════════════
// 14. ALERTS (strings built only when an alert fires)
// ══════════════════════════════════════════════════════════
if openLong or openShort
string aPrice = str.tostring(close, format.mintick)
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, "#.#")
string aVwap = str.tostring(nz(vwapNow), format.mintick)
if openLong
string jsonBuy = '{"action":"buy","ticker":"' + syminfo.tickerid + '","tf":"' + timeframe.period + '","price":' + aPrice + ',"vwap":' + aVwap + ',"sl":' + aSL + ',"tp1":' + aTP1 + ',"tp2":' + aTP2 + ',"tp3":' + aTP3 + ',"rr":' + aRR + '}'
string textBuy = "🟢 LONG | VWAP retest | " + syminfo.tickerid + " | TF: " + timeframe.period + " | 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 + '","tf":"' + timeframe.period + '","price":' + aPrice + ',"vwap":' + aVwap + ',"sl":' + aSL + ',"tp1":' + aTP1 + ',"tp2":' + aTP2 + ',"tp3":' + aTP3 + ',"rr":' + aRR + '}'
string textSell = "🔴 SHORT | VWAP retest | " + syminfo.tickerid + " | TF: " + timeframe.period + " | 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)
if evAnchorBull and alertStructInput
alert("🟢 New bullish leg | " + syminfo.tickerid + " | TF: " + timeframe.period + " | Anchor: " + fmtPrice(legAnchorPx), alert.freq_once_per_bar_close)
if evAnchorBear and alertStructInput
alert("🔴 New bearish leg | " + syminfo.tickerid + " | TF: " + timeframe.period + " | Anchor: " + fmtPrice(legAnchorPx), alert.freq_once_per_bar_close)
if evCrossUp and alertStructInput
alert("🔵 Close above VWAP | " + syminfo.tickerid + " | TF: " + timeframe.period + " | VWAP: " + fmtPrice(vwapNow), alert.freq_once_per_bar_close)
if evCrossDn and alertStructInput
alert("🔵 Close below VWAP | " + syminfo.tickerid + " | TF: " + timeframe.period + " | VWAP: " + fmtPrice(vwapNow), alert.freq_once_per_bar_close)