//@version=6 indicator("Structure Watch", shorttitle="Structure", overlay=true, max_lines_count=500, max_labels_count=500) // ═══════════════════════════════════════════════════════════════════════════ // Structure levels that tell you when to start watching, and nothing else. // // THE JOB, in the owner's words: show me structure, so that when price walks // away from it and later comes back, I know to pay attention — because from // there it can walk away again, and that is where I look for my entry on a // Rejection Block. // // So this draws the level and runs the clock on it. It does not find the // entry. The Rejection Block indicator does that, and keeping them apart is // deliberate: two small tools you can read beat one that does everything. // // ── HOW THE LEVELS ARE FOUND ─────────────────────────────────────────────── // An ATR zig-zag. The swing turns only when price comes back `Threshold` x ATR // from the running extreme. The threshold is the only scale dial, and it is // the whole reason this stays quiet. // // Measured on NQ 1-minute, 402,000 bars over 361 trading days (2025-2026): // // Threshold Swings/day Known after // 1 ATR 340 1 bar <- the usual confetti // 4 ATR 36 8 bars // 8 ATR 9.3 34 bars <- default // 12 ATR 4.2 90 bars // // WHY NOT THE HIERARCHICAL VERSION. There is a well-regarded Python method // (neurotrader888) that builds the same scale dial by promoting swings up // through levels. It was measured against this one on the same data: its // level 3 gives 9.6 swings a day — the same quietness — but confirms after // 115 bars against 34 here. Promotion has to wait for a LATER swing to // confirm an earlier one, so it is retrospective by construction. Same picture, // three times slower, and it needs recursion, which Pine does not have. // The simple version won on the measurement, not on convenience. // // ── WHY THERE ARE NO CONSOLIDATION BOXES ─────────────────────────────────── // They were the original plan and the data killed them. Counting nested swings // as "balance" puts the market inside a box 65% to 70% of the time at every // threshold tried. A marker that is on two thirds of the time is not // information, so it is not drawn. // // ── WHAT THE ARMING RULE IS FOR ──────────────────────────────────────────── // A level is grey context until price has moved `Arm distance` away from it. // Then it goes live, and the first touch after that is the alert. If price // leaves again it re-arms, because a level can be worth watching more than // once. Without this rule a level is "live" from the moment it is born, right // where price already is, and the word retest stops meaning anything — // measured elsewhere, dropping it inflated rejection rates by 17 points. // ═══════════════════════════════════════════════════════════════════════════ // ───────────────────────────── structure ───────────────────────────── gS = "Structure" mult = input.float(8.0, "Threshold (x ATR)", minval=0.5, maxval=50, step=0.5, group=gS, tooltip="The only scale dial. On NQ 1m: 8 gives about 9 swings a day, 4 gives 36, 12 gives 4. Raise it until the chart is as quiet as you want it.") atrLen = input.int(120, "ATR length", minval=5, group=gS, tooltip="Barely matters — 60, 120 and 390 were measured and gave nearly the same swing count. Left adjustable, not worth tuning.") maxSide = input.int(3, "Levels kept per side", minval=1, maxval=10, group=gS, tooltip="Counted separately for highs and lows, so a one-way market cannot push every level off one edge.") // ───────────────────────────── the clock ───────────────────────────── gA = "Watching" armATR = input.float(1.0, "Arm once price is this far away (x ATR)", minval=0.1, step=0.25, group=gA) breakATR = input.float(0.25, "Dead after a close beyond by (x ATR)", minval=0.0, step=0.05, group=gA, tooltip="A wick through is not a break — a wick through is what makes the next level.") reArm = input.bool(true, "Re-arm after price leaves again", group=gA, tooltip="On: a level can call you back more than once. Off: one alert per level, ever.") // ───────────────────────────── style ───────────────────────────── gV = "Style" colCtx = input.color(color.new(color.gray, 35), "Context (not yet armed)", group=gV) colHigh = input.color(color.rgb(200, 60, 60), "Armed high", inline="a", group=gV) colLow = input.color(color.rgb(0, 150, 90), "Armed low", inline="a", group=gV) colTouch = input.color(color.rgb(230, 170, 40), "Being touched now", group=gV) wCtx = input.int(1, "Width — context", minval=1, maxval=5, inline="w", group=gV) wArm = input.int(2, "armed", minval=1, maxval=5, inline="w", group=gV) extendR = input.int(20, "Extend right (bars)", minval=0, maxval=200, group=gV) showZig = input.bool(false, "Show the zig-zag itself", group=gV, tooltip="Off by default. The zig-zag is how the levels are found, not something to look at.") showLbl = input.bool(true, "Labels", group=gV) lblSize = input.string("small", " └ size", options=["tiny","small","normal"], group=gV) labelSize = lblSize == "tiny" ? size.tiny : lblSize == "small" ? size.small : size.normal atr = ta.atr(atrLen) thr = mult * atr barMs = timeframe.in_seconds() * 1000 // ═══════════════════════ the ATR zig-zag ═══════════════════════ // Non-recursive on purpose: Pine has no recursion, and the measurement said // the simple version is also the faster one. var bool up = true var float pendMax = na var float pendMin = na var int pendMaxT = na var int pendMinT = na if na(pendMax) and not na(atr) pendMax := high pendMin := low pendMaxT := time pendMinT := time bool newSwing = false bool newIsHigh = false float newPrice = na int newTime = na if not na(atr) and not na(pendMax) if up if high > pendMax pendMax := high pendMaxT := time else if low < pendMax - thr newSwing := true newIsHigh := true newPrice := pendMax newTime := pendMaxT up := false pendMin := low pendMinT := time else if low < pendMin pendMin := low pendMinT := time else if high > pendMin + thr newSwing := true newIsHigh := false newPrice := pendMin newTime := pendMinT up := true pendMax := high pendMaxT := time // ═══════════════════════ the levels ═══════════════════════ var array lPrice = array.new_float() var array lIsHigh = array.new_bool() var array lBorn = array.new_int() var array lArmed = array.new_bool() var array lLine = array.new_line() var array