Indicator · Pine Script v6 · On the chart

MIDAS 2.0 [WavesUnchained]

by casoon

This indicator plots an anchored MIDAS support and resistance curve from launch-anchored volume-weighted price on the price chart.

View on GitHub

Repaints

GitHub stars9on the repository
Pine versionv6current
Lines of code1,072
Settings40inputs
LicenceMIT
Last update3 days ago27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,072 lines of Pine Script v6 from indicators/mean_reversion/midas_curves/midas_curves.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

midas_curves.pine
//@version=6
// ============================================================================
// MIDAS 2.0 [WavesUnchained]
// Version: 2.7.3
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Anchored MIDAS support/resistance curve (launch-anchored volume-weighted price)
// with a true topfinder / bottomfinder accelerated curve — an accelerated
// exhaustion estimate, not a forecast. Location + Exhaustion sensor: it reports
// where price sits relative to the MIDAS curve/bands and how far the launched
// move has run toward its projected end. Context markers are Setup-level
// evidence, not hard entries.
//
// Features:
//   ✓ MIDAS curve anchored at manual time, daily open, or auto swing (last / low / high)
//   ✓ Volume-weighted σ bands, ATR bands, or Hybrid (avg) at two configurable multiples
//   ✓ Topfinder / bottomfinder accelerated curve with closed-form auto-fit
//   ✓ Fit-qualified exhaustion read (developing / mature / late / passed + reaction)
//   ✓ Stretched-from-value and reclaim-after-stretch context markers (no BUY/SELL)
//   ✓ Reclaim Quality Score (0-4): close strength, volume, structure break, TBF exhaustion
//   ✓ Value-zone fills, semantic event colors, and persistent live context badge
//   ✓ Active / Full visual modes with a role-separated Trend · Location · Exhaustion read
//   ✓ Backpainted vs Confirmed anchor display mode for auto swing anchors
//   ✓ Auto EMA bias with configurable hold bars to reduce whipsaw flips
//   ✓ Compact / Detailed / Tooltip-only live badge and optional dashboard
//   ✓ Light-theme dashboard, alerts, debug log
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    REQUIRED   real trade volume — feed must deliver volume and
//                         syminfo.volumetype must not be "tick"; otherwise not available
//   OI:        NO
//   Reference: NO
//   Verdict:   Exchange-only
// ============================================================================

indicator("MIDAS 2.0 [WavesUnchained]", "MIDAS 2.0", overlay=true, max_labels_count=500, max_lines_count=500, max_polylines_count=20)

// ─────────────────────────────────────────────────────────────────────────────
// INPUTS
// ─────────────────────────────────────────────────────────────────────────────

g_anchor = "1 · Anchor"
anchorMode = input.string("Auto Last Swing", "Anchor on",
     options=["Manual Time", "Daily Open", "Auto Last Swing", "Auto Swing Low", "Auto Swing High"], group=g_anchor,
     tooltip="Auto Last Swing anchors on the most recent significant swing of either side and sets the TBF direction from it (low → topfinder, high → bottomfinder). Auto Swing Low/High force a side.")
anchorDisplayMode = input.string("Backpainted", "Anchor Display", options=["Backpainted", "Confirmed"], group=g_anchor,
     tooltip="Only affects Auto Swing anchors. Backpainted draws the anchor and starts the MIDAS curve at the real pivot bar (clean history, but that pivot isn't knowable live until Anchor Swing Right bars later). Confirmed starts the curve and marks the anchor only once it's actually known live — more honest for live trading, at the cost of a less clean historical look.")
anchorTime = input.time(timestamp("01 Jan 2026 00:00 +0000"), "Manual Anchor", group=g_anchor)
anchorLeft  = input.int(20, "Anchor Swing Left",  minval=1, group=g_anchor)
anchorRight = input.int(20, "Anchor Swing Right", minval=1, group=g_anchor,
     tooltip="Degree of the swing that defines an anchor. Larger = only significant pivots launch a new MIDAS curve, so it tracks the current leg origin rather than every minor wiggle. Re-seeded from the real pivot bar (anchorRight back).")
src         = input.source(hlc3, "Price Source", group=g_anchor)
volumeWeightMode = input.string("Per-bar fallback", "Volume Weighting",
     options=["Per-bar fallback", "Equal weight"], group=g_anchor,
     tooltip="Per-bar fallback uses reported volume and substitutes 1 only where volume is missing/zero; episode quality is disclosed in the dashboard and hover. Equal weight deliberately uses 1 for every bar, which is useful on instruments without meaningful volume.")

g_bands = "2 · Bands"
showBands = input.bool(true, "Show Bands", group=g_bands)
bandDisplay = input.string("Zones", "Band Display", options=["Zones", "Lines", "Zones + Lines"], group=g_bands,
     tooltip="Zones emphasize distance from MIDAS without turning every boundary into a competing support/resistance line. Lines keeps the classic four-line view.")
bandMode  = input.string("VW StDev", "Band Mode", options=["VW StDev", "ATR", "Hybrid"], group=g_bands,
     tooltip="VW StDev widens with the anchored move (can dull in long trends); ATR is more reactive short-term; Hybrid averages the two.")
bandMult1 = input.float(1.0, "Band 1 Mult", minval=0.0, step=0.25, group=g_bands)
bandMult2 = input.float(2.0, "Band 2 Mult", minval=0.0, step=0.25, group=g_bands)
atrLen    = input.int(14, "ATR Length", minval=1, group=g_bands)

g_tbf = "3 · Topfinder / Bottomfinder"
enableTBF = input.bool(true, "Enable TBF Exhaustion Curve", group=g_tbf,
     tooltip="Accelerated MIDAS curve auto-fitted to the most recent same-direction swing after the launch. Catches up to price and gives an accelerated estimate of how far the launched move has run toward exhaustion.")
warnThresh = input.float(0.85, "Exhaustion Warn (d/D)", minval=0.5, maxval=0.99, step=0.05, group=g_tbf,
     tooltip="When cumulative volume since launch reaches this fraction of the fitted projection D, flag the move as nearing exhaustion.")
fitMinProgress = input.float(0.20, "Minimum Fit Maturity (d/D)", minval=0.05, maxval=0.50, step=0.05, group=g_tbf,
     tooltip="Rejects fits made too early in the projected move. 20% follows the minimum engineering guideline for a usable topfinder/bottomfinder fit; 50% is more conservative.")
fitStableTolerance = input.float(0.25, "Stable Fit: max D change", minval=0.05, maxval=1.0, step=0.05, group=g_tbf,
     tooltip="A repeated fit is Stable when projected total volume D changes by no more than this fraction versus the active fit. A larger shift is held as Candidate 1/2 and cannot replace the active fit until another candidate confirms its new region.")
tbfHistoryMode = input.string("From Fit", "TBF History", options=["From Fit", "Retrospective"], group=g_tbf,
     tooltip="From Fit draws the current projection only from the bar where that fit became known. Retrospective applies today's fit back across the complete anchor episode and is analysis-only.")
tbfStaleProgress = input.float(1.50, "Stale Fit after (d/D)", minval=1.10, maxval=3.0, step=0.05, group=g_tbf,
     tooltip="After this much projected volume without a qualifying price reaction, Passed is downgraded to Stale Fit rather than remaining a permanent high-alert state.")
fitLeft    = input.int(5, "Fit Swing Left",  minval=1, group=g_tbf)
fitRight   = input.int(5, "Fit Swing Right", minval=1, group=g_tbf,
     tooltip="Degree of the pullback swings (smaller than the anchor) the topfinder/bottomfinder is fitted to within the current leg.")

g_bias = "4 · Bias"
curveBias = input.string("Auto EMA", "Trend Bias", options=["Auto EMA", "Up", "Down"], group=g_bias,
     tooltip="Colours the context markers and selects topfinder vs bottomfinder for Manual/Daily anchors. For Auto modes the TBF direction follows the anchor side (low → topfinder, high → bottomfinder). Auto EMA = price vs EMA.")
emaLen    = input.int(55, "Auto Bias EMA", minval=5, group=g_bias)
biasHoldBars = input.int(3, "Auto EMA: min hold bars before flip", minval=1, group=g_bias,
     tooltip="Auto EMA bias only flips once price has closed on the new side for this many consecutive bars — reduces whipsaw in choppy phases. Set to 1 for the old immediate-flip behaviour.")

g_disp = "5 · Display"
visualMode = input.string("Active", "Visual mode", options=["Active", "Full"], group=g_disp,
     tooltip="Active shows the complete current anchor episode only, including its origin. Full restores every historical episode, the TBF curve, and historical events.")
showMarkers = input.bool(true, "Show context markers", group=g_disp,
     tooltip="Stretch, MIDAS reclaim, and confirmed TBF price-reaction markers. Setup-level context, not hard entries.")
reclaimLook = input.int(10, "Reclaim: stretch lookback", minval=1, group=g_disp,
     tooltip="A MIDAS reclaim only fires if price was stretched past the inner band within this many bars before reclaiming the curve.")
reclaimVolLen    = input.int(20, "Reclaim: volume avg length", minval=1, group=g_disp)
reclaimStructLen = input.int(5, "Reclaim: structure-break lookback", minval=1, group=g_disp)
reclaimCloseLocationMin = input.float(0.6, "Reclaim: minimum close-location ratio", minval=0.5, maxval=1.0, step=0.05, group=g_disp,
     tooltip="Required close position within the complete candle range (low to high), not within the body. The Reclaim Quality Score remains an annotation and does not gate the marker.")
markersOnBarClose = input.bool(true, "Context markers only on bar close", group=g_disp,
     tooltip="Prevents stretch, reclaim, TBF phase, and exhaustion-reaction markers from appearing intrabar and disappearing before the candle closes. The live context itself still updates intrabar.")
showLiveContext = input.bool(true, "Show live context badge", group=g_disp,
     tooltip="Shows live structure, location, exhaustion, and the next observable trigger. Hover for the underlying distance/TBF measurements and trigger details.")
liveBadgeMode = input.string("Compact", "Live badge detail", options=["Compact", "Detailed", "Tooltip only"], group=g_disp,
     tooltip="Compact keeps the four decisions short. Detailed shows the full role wording. Tooltip only leaves a small MIDAS info tag; all measurements remain available on hover.")
showTable   = input.bool(false, "Show dashboard", group=g_disp)
dashboardMode = input.string("Compact", "Dashboard detail", options=["Compact", "Detailed"], group=g_disp)
tablePos    = input.string("Top Right", "Dashboard position",
     options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=g_disp)
showAnchor  = input.bool(true, "Show active anchor", group=g_disp)
dbg         = input.bool(false, "Debug log", group=g_disp)

g_col = "6 · Colors"
colMidas = input.color(#ff6f00, "MIDAS curve", group=g_col)
colTBF   = input.color(#7b1fa2, "TBF curve",   group=g_col)
colStretch = input.color(#ffb300, "Stretch zone", group=g_col)
colUp    = input.color(#00c853, "Bullish",     group=g_col)
colDn    = input.color(#d50000, "Bearish",     group=g_col)

// ─────────────────────────────────────────────────────────────────────────────
// DATA VALIDITY  (Exchange-only — see DATA_VALIDITY.md)
// ─────────────────────────────────────────────────────────────────────────────
// Echtes Handelsvolumen? Nur "tick" schließt aus; "n/a" zählt, wenn der Feed Volumen liefert (DATA_VALIDITY §2).
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)
float vol = volumeIsReal ? volume : na

// Kurzer Hinweis unten rechts; was fehlt, steht im Data Contract oben.
var table volWarn = table.new(position.bottom_right, 1, 1)
if barstate.islast and not volumeIsReal
    table.cell(volWarn, 0, 0, "MIDAS 2.0 · not available",
         bgcolor = color.new(color.orange, 20), text_color = color.white, text_size = size.small)

// ─────────────────────────────────────────────────────────────────────────────
// ANCHOR
// ─────────────────────────────────────────────────────────────────────────────
bool volMissing = na(vol) or vol <= 0
float barWeight = not volumeIsReal ? na : volumeWeightMode == "Equal weight" ? 1.0 : volMissing ? 1.0 : vol

// Manual mode must not create an unofficial pre-anchor episode. If the chosen
// time predates the loaded history, the first available bar becomes the honest
// start of the visible episode.
bool anchorActive = anchorMode != "Manual Time" or time >= anchorTime
manualPulse = time >= anchorTime and (na(time[1]) or time[1] < anchorTime)
dailyPulse  = ta.change(time("D")) != 0
// Anchor pivots (significant degree) — define a new leg origin
anchLowConf  = not na(ta.pivotlow(low,  anchorLeft, anchorRight))
anchHighConf = not na(ta.pivothigh(high, anchorLeft, anchorRight))
// Fit pivots (smaller degree) — pullbacks the topfinder/bottomfinder is fitted to
fitLowConf   = not na(ta.pivotlow(low,  fitLeft, fitRight))
fitHighConf  = not na(ta.pivothigh(high, fitLeft, fitRight))

bool isPivotMode = anchorMode == "Auto Swing Low" or anchorMode == "Auto Swing High" or anchorMode == "Auto Last Swing"
bool backpainted = anchorDisplayMode == "Backpainted"

// Auto modes re-anchor on the most recent significant swing — the origin of the
// current leg. Larger anchor degree keeps the curve on the current move, not on
// every minor wiggle (and not pinned to the all-time extreme).
bool anchorPulse = false
if anchorMode == "Manual Time"
    anchorPulse := manualPulse
else if anchorMode == "Daily Open"
    anchorPulse := dailyPulse
else if anchorMode == "Auto Swing Low"
    anchorPulse := anchLowConf
else if anchorMode == "Auto Swing High"
    anchorPulse := anchHighConf
else   // Auto Last Swing — most recent significant swing of either side
    anchorPulse := anchLowConf or anchHighConf

// Backpainted cumulative history is reconstructed only when the pivot becomes
// known. Series values on earlier bars cannot be rewritten, so fits need this
// live-knowledge boundary.
var int anchorConfirmationBar = na
if anchorPulse
    anchorConfirmationBar := bar_index

// Side of the active auto-anchor (used by Auto Last Swing for the TBF direction)
var bool anchorIsLow = true
if anchorPulse and isPivotMode
    anchorIsLow := anchLowConf   // prefer the low if both confirm on the same bar

// ─────────────────────────────────────────────────────────────────────────────
// MIDAS CUMULATIVE SUMS  (A=ΣpV, C=ΣV, P2=Σp²V, B=ΣpVd, E=ΣVd ; d = running ΣV)
// ─────────────────────────────────────────────────────────────────────────────
var float cumPV  = na   // A
var float cumV   = na   // C  (= d, cumulative volume since launch)
var float cumP2V = na   // Σ p² V  (for VW variance)
var float cumPVd = na   // B
var float cumVd  = na   // E
var int   age    = na
var int episodeMissingVolumeBars = 0
var int episodeVolumeBars = 0

// Canonical cumulative episode history. Besides drawing Active TBF, these
// arrays provide the reconstructed statistics for fit pivots that lie between
// a backpainted pivot origin and its later confirmation bar.
const int MAX_ACTIVE_BARS = 2500
var array<int> episodeStatX = array.new<int>()
var array<float> episodeStatA = array.new<float>()
var array<float> episodeStatB = array.new<float>()
var array<float> episodeStatC = array.new<float>()
var array<float> episodeStatE = array.new<float>()

f_storeEpisodeStat(int pointX, float pointA, float pointB, float pointC, float pointE) =>
    array.push(episodeStatX, pointX)
    array.push(episodeStatA, pointA)
    array.push(episodeStatB, pointB)
    array.push(episodeStatC, pointC)
    array.push(episodeStatE, pointE)
    if array.size(episodeStatX) > MAX_ACTIVE_BARS
        array.shift(episodeStatX)
        array.shift(episodeStatA)
        array.shift(episodeStatB)
        array.shift(episodeStatC)
        array.shift(episodeStatE)

f_episodeStatIndex(int pointX) =>
    int result = -1
    int statSize = array.size(episodeStatX)
    if statSize > 0
        for i = statSize - 1 to 0
            if array.get(episodeStatX, i) == pointX
                result := i
                break
    result

if not anchorActive
    cumPV  := na
    cumV   := na
    cumP2V := na
    cumPVd := na
    cumVd  := na
    age    := na
    episodeMissingVolumeBars := 0
    episodeVolumeBars := 0
    array.clear(episodeStatX)
    array.clear(episodeStatA)
    array.clear(episodeStatB)
    array.clear(episodeStatC)
    array.clear(episodeStatE)
else if anchorPulse or na(cumPV)
    cumPV  := 0.0
    cumV   := 0.0
    cumP2V := 0.0
    cumPVd := 0.0
    cumVd  := 0.0
    episodeMissingVolumeBars := 0
    episodeVolumeBars := 0
    array.clear(episodeStatX)
    array.clear(episodeStatA)
    array.clear(episodeStatB)
    array.clear(episodeStatC)
    array.clear(episodeStatE)
    if isPivotMode and anchorPulse and backpainted
        // re-seed from the real pivot bar (anchorRight back), so value accrues
        // from the actual swing rather than the lagging confirmation bar
        for i = anchorRight to 0
            float p = nz(src[i], src)
            bool pointVolumeMissing = na(vol[i]) or vol[i] <= 0
            float v = not volumeIsReal ? na : volumeWeightMode == "Equal weight" ? 1.0 : pointVolumeMissing ? 1.0 : vol[i]
            cumV   := cumV + v          // running cumulative volume d
            cumPV  := cumPV + p * v
            cumP2V := cumP2V + p * p * v
            cumPVd := cumPVd + p * v * cumV
            cumVd  := cumVd + v * cumV
            episodeVolumeBars += 1
            if pointVolumeMissing
                episodeMissingVolumeBars += 1
            f_storeEpisodeStat(bar_index - i, cumPV, cumPVd, cumV, cumVd)
        age := anchorRight + 1
    else
        cumPV  := src * barWeight
        cumV   := barWeight
        cumP2V := src * src * barWeight
        cumPVd := src * barWeight * barWeight
        cumVd  := barWeight * barWeight
        age    := 1
        episodeVolumeBars := 1
        episodeMissingVolumeBars := volMissing ? 1 : 0
        f_storeEpisodeStat(bar_index, cumPV, cumPVd, cumV, cumVd)
else
    float dj = cumV + barWeight          // running cumulative volume after this bar
    cumPVd += src * barWeight * dj
    cumVd  += barWeight * dj
    cumPV  += src * barWeight
    cumV   := dj
    cumP2V += src * src * barWeight
    age    += 1
    episodeVolumeBars += 1
    if volMissing
        episodeMissingVolumeBars += 1
    f_storeEpisodeStat(bar_index, cumPV, cumPVd, cumV, cumVd)

float volumeAvailability = episodeVolumeBars > 0 ? (episodeVolumeBars - episodeMissingVolumeBars) / episodeVolumeBars : na
string volumeQuality = volumeWeightMode == "Equal weight" ? "equal weight" : episodeMissingVolumeBars == 0 ? "volume-weighted" : volumeAvailability >= 0.95 ? "mostly volume-weighted" : volumeAvailability > 0 ? "mixed fallback" : "equal-weight fallback"

midas = not na(cumV) and cumV > 0 ? cumPV / cumV : na

// Bands
vwVar  = math.max(cumP2V / cumV - midas * midas, 0)
vwDev  = math.sqrt(vwVar)
devATR = ta.atr(atrLen)
dev    = bandMode == "VW StDev" ? vwDev : bandMode == "ATR" ? devATR : math.avg(vwDev, devATR)

upper1 = midas + dev * bandMult1
lower1 = midas - dev * bandMult1
upper2 = midas + dev * bandMult2
lower2 = midas - dev * bandMult2

// ─────────────────────────────────────────────────────────────────────────────
// BIAS  (Trend role — colour + TBF direction only, never a hard gate)
// ─────────────────────────────────────────────────────────────────────────────
ema = ta.ema(close, emaLen)
var int bias = 1
if curveBias == "Up"
    bias := 1
else if curveBias == "Down"
    bias := -1
else
    // Auto EMA holds its side until price closes on the opposite side for
    // `biasHoldBars` bars running — reduces whipsaw flips in choppy phases
    var int biasFlipStreak = 0
    bool oppositeSide = (bias == 1 and close < ema) or (bias == -1 and close >= ema)
    if oppositeSide
        biasFlipStreak += 1
        if biasFlipStreak >= biasHoldBars
            bias := bias * -1
            biasFlipStreak := 0
    else
        biasFlipStreak := 0
// TBF direction follows the anchor type (low → topfinder, high → bottomfinder)
// so the accelerated curve is coherent with the launch; bias only decides for
// manual / daily anchors.
bool tbfTop = anchorMode == "Auto Swing Low" ? true : anchorMode == "Auto Swing High" ? false : anchorMode == "Auto Last Swing" ? anchorIsLow : bias == 1
bool tbfDirectionChanged = bar_index > 0 and not isPivotMode and tbfTop != tbfTop[1]

// ─────────────────────────────────────────────────────────────────────────────
// TOPFINDER / BOTTOMFINDER  (Exhaustion role)
//   TBF_j = (A·D − B) / (C·D − E),  weights w_i = 1 − d_i/D
//   Fit at a same-direction swing after launch:  D = (B − T·E)/(A − T·C)
// ─────────────────────────────────────────────────────────────────────────────
var float tbfD = na   // fitted projected total volume D
var int tbfFitCount = 0
var float tbfFitShift = na
var float tbfFitProgress = na
var string tbfFitQuality = "none"
var int tbfFitAcceptedBar = na
var float tbfPendingD = na
var float tbfPendingShift = na
var int tbfPendingConfirmations = 0
var bool tbfFitReactionSeen = false
var int tbfFitBestReactionScore = 0
var bool tbfEpisodeReactionSeen = false
var int tbfEpisodeBestReactionScore = 0

if anchorPulse or tbfDirectionChanged
    tbfD := na
    tbfFitCount := 0
    tbfFitShift := na
    tbfFitProgress := na
    tbfFitQuality := "none"
    tbfFitAcceptedBar := na
    tbfPendingD := na
    tbfPendingShift := na
    tbfPendingConfirmations := 0
    tbfFitReactionSeen := false
    tbfFitBestReactionScore := 0
    tbfEpisodeReactionSeen := false
    tbfEpisodeBestReactionScore := 0

// Re-fit on a confirmed same-direction swing that sits after the launch
float fitT  = na
bool  doFit = false
int fitPivotBar = bar_index - fitRight
bool fitNeedsReconstructedStats = isPivotMode and backpainted and not na(anchorConfirmationBar) and fitPivotBar < anchorConfirmationBar
int fitEpisodeStatIndex = fitNeedsReconstructedStats ? f_episodeStatIndex(fitPivotBar) : -1
bool fitSeriesAvailable = not fitNeedsReconstructedStats or fitEpisodeStatIndex >= 0
bool fitBlockedByHistory = false
if enableTBF and not na(cumPV) and fitSeriesAvailable
    if tbfTop and fitLowConf and age >= fitRight + 1
        fitT  := low[fitRight]
        doFit := true
    else if (not tbfTop) and fitHighConf and age >= fitRight + 1
        fitT  := high[fitRight]
        doFit := true
else if enableTBF and not na(cumPV) and not fitSeriesAvailable and age >= fitRight + 1
    fitBlockedByHistory := (tbfTop and fitLowConf) or (not tbfTop and fitHighConf)

bool fitAccepted = false
bool fitMaterialChange = false
bool unstableCandidateObserved = false
if doFit
    // A fit inside the backpainted origin→confirmation segment must read the
    // reconstructed canonical episode state; immutable Series still belongs to
    // the pre-confirmation calculation and may contain a previous episode.
    float Af = fitNeedsReconstructedStats ? array.get(episodeStatA, fitEpisodeStatIndex) : cumPV[fitRight]
    float Bf = fitNeedsReconstructedStats ? array.get(episodeStatB, fitEpisodeStatIndex) : cumPVd[fitRight]
    float Cf = fitNeedsReconstructedStats ? array.get(episodeStatC, fitEpisodeStatIndex) : cumV[fitRight]
    float Ef = fitNeedsReconstructedStats ? array.get(episodeStatE, fitEpisodeStatIndex) : cumVd[fitRight]
    float den = Af - fitT * Cf
    float Dcand = den != 0 ? (Bf - fitT * Ef) / den : na
    float fitProgressCand = not na(Dcand) and Dcand > 0 ? Cf / Dcand : na
    // valid only if it still projects forward FROM NOW (D > volume already accumulated
    // at the current bar), is not degenerate, and was fitted after enough of the
    // projected move had developed.
    bool forwardFit = not na(Dcand) and Dcand > cumV and Dcand < cumV * 50
    bool matureFit = not na(fitProgressCand) and fitProgressCand >= fitMinProgress
    bool ok = forwardFit and matureFit
    if ok
        float previousD = tbfD
        float currentShift = not na(previousD) and previousD > 0 ? math.abs(Dcand - previousD) / previousD : na
        bool firstFit = na(previousD)
        bool stableWithActive = not firstFit and not na(currentShift) and currentShift <= fitStableTolerance
        if firstFit or stableWithActive
            tbfD := Dcand
            tbfFitCount += 1
            tbfFitShift := currentShift
            tbfFitProgress := fitProgressCand
            tbfFitQuality := firstFit ? "provisional" : "stable"
            tbfFitAcceptedBar := bar_index
            tbfPendingD := na
            tbfPendingShift := na
            tbfPendingConfirmations := 0
            fitAccepted := true
            fitMaterialChange := firstFit
        else
            float pendingAgreement = not na(tbfPendingD) and tbfPendingD > 0 ? math.abs(Dcand - tbfPendingD) / tbfPendingD : na
            bool confirmsPendingRegion = tbfPendingConfirmations > 0 and not na(pendingAgreement) and pendingAgreement <= fitStableTolerance
            if confirmsPendingRegion
                tbfD := Dcand
                tbfFitCount += 1
                tbfFitShift := currentShift
                tbfFitProgress := fitProgressCand
                tbfFitQuality := "stable"
                tbfFitAcceptedBar := bar_index
                tbfPendingD := na
                tbfPendingShift := na
                tbfPendingConfirmations := 0
                fitAccepted := true
                fitMaterialChange := true
            else
                // A single strongly shifted candidate never replaces the active
                // fit. It must be confirmed by another candidate in its region.
                tbfPendingD := Dcand
                tbfPendingShift := currentShift
                tbfPendingConfirmations := 1
                unstableCandidateObserved := true
        if fitAccepted and fitMaterialChange
            tbfFitReactionSeen := false
            tbfFitBestReactionScore := 0
    else if dbg and barstate.isconfirmed
        // why the fit was rejected — distinguishes "already passed / no acceleration" from "degenerate"
        string why = na(Dcand) ? "den=0" : Dcand <= cumV ? "D<=d (already passed / no acceleration)" : Dcand >= cumV * 50 ? "D>50·d (degenerate)" : "fit too early"
        log.info("MIDAS TBF FIT REJECTED dir={0} reason={1} D={2} d={3} fitProgress={4} T={5}",
             tbfTop ? "top" : "bot", why, str.tostring(Dcand, "0"), str.tostring(cumV, "0"), str.tostring(fitProgressCand, "0.00"), str.tostring(fitT, format.mintick))

// TBF curve value + phase. "Mature" begins at 50%, the conservative point at
// which the projection becomes decision-relevant; "late" still is not a turn.
float progress = (not enableTBF or na(tbfD)) ? na : cumV / tbfD
float remainingVolume = na(progress) ? na : math.max(tbfD - cumV, 0)
float episodeAvgVolume = not na(age) and age > 0 ? cumV / age : na
int approxBarsRemaining = not na(remainingVolume) and not na(episodeAvgVolume) and episodeAvgVolume > 0 ? int(math.ceil(remainingVolume / episodeAvgVolume)) : na
float tbf = na
// `not anchorPulse` forces a one-bar gap on every re-anchor so separate projection
// episodes never bridge into one straight line on long-history views
if enableTBF and not na(tbfD) and tbfD > cumV and not anchorPulse
    float denom = cumV * tbfD - cumVd
    tbf := denom != 0 ? (cumPV * tbfD - cumPVd) / denom : na

// "late" = far through the projection, NOT a confirmed turn. "passed" means
// d >= D and still requires a separate price reaction.
string tbfState = not enableTBF ? "off" : na(progress) ? "no fit" : progress >= 1.0 ? "passed" : progress >= warnThresh ? "late" : progress >= 0.50 ? "mature" : "developing"
bool tbfWarn    = tbfState == "late"
bool tbfLateNew = tbfWarn and not tbfWarn[1]
bool tbfPassedNew = tbfState == "passed" and tbfState[1] != "passed"

// Price reaction is separate from projection progress. Persistent events are
// confirmed by default; the live context may still move intrabar.
bool markerOk = not markersOnBarClose or barstate.isconfirmed
float tbfClosePos = high > low ? (close - low) / (high - low) : 0.5
bool tbfReactionCandle = tbfTop ? close < open and tbfClosePos <= 0.35 : close > open and tbfClosePos >= 0.65
float tbfReactionBody = math.abs(close - open)
bool tbfReactionStructure = tbfTop ? close < low[1] and tbfReactionBody >= devATR * 0.5 : close > high[1] and tbfReactionBody >= devATR * 0.5
bool tbfReactionBand = tbfTop ? ta.crossunder(close, upper1) : ta.crossover(close, lower1)
int tbfReactionScore = (tbfReactionCandle ? 1 : 0) + (tbfReactionStructure ? 1 : 0) + (tbfReactionBand ? 1 : 0)
bool tbfAdvanced = tbfState == "late" or tbfState == "passed"
bool tbfReactionQualified = enableTBF and tbfAdvanced and tbfReactionScore >= 2 and markerOk
bool tbfReactionNew = tbfReactionQualified and not tbfFitReactionSeen
if tbfReactionQualified
    tbfFitReactionSeen := true
    tbfFitBestReactionScore := math.max(tbfFitBestReactionScore, tbfReactionScore)
    tbfEpisodeReactionSeen := true
    tbfEpisodeBestReactionScore := math.max(tbfEpisodeBestReactionScore, tbfReactionScore)
bool tbfStale = not na(progress) and progress >= tbfStaleProgress and not tbfFitReactionSeen

// ─────────────────────────────────────────────────────────────────────────────
// LOCATION + CONTEXT MARKERS  (Setup-level evidence, §7/§9 — not BUY/SELL)
// ─────────────────────────────────────────────────────────────────────────────
normalizedDistance = not na(midas) and dev > 0 ? (close - midas) / dev : na

// Reclaim only counts after a genuine stretch past the inner band (not on small dips)
stretchedLowRecently  = ta.barssince(not na(lower1) and close < lower1) <= reclaimLook
stretchedHighRecently = ta.barssince(not na(upper1) and close > upper1) <= reclaimLook

// Detect location/reclaim events independently from EMA bias. Bias classifies
// the context as trend-aligned or counter-bias; it no longer hides the event.
bool rawStretchAbove = not na(upper2) and high > upper2 and close < upper2
bool rawStretchBelow = not na(lower2) and low < lower2 and close > lower2
bool rawReclaimUp = stretchedLowRecently and ta.crossover(close, midas)
bool rawReclaimDn = stretchedHighRecently and ta.crossunder(close, midas)
bool stretchAbove = markerOk and rawStretchAbove
bool stretchBelow = markerOk and rawStretchBelow
bool reclaimUp = markerOk and rawReclaimUp
bool reclaimDn = markerOk and rawReclaimDn

// Reclaim Quality Score (0-4) — context annotation only, does not gate the marker/alert:
// strong candle close, volume above average, small structure break, TBF
// late/passed bonus. The prior stretch is NOT scored: it is already a required
// condition of every reclaim (rawReclaimUp/Dn), so it would add a constant +1.
avgVol        = ta.sma(vol, reclaimVolLen)
volAboveAvg   = not na(vol) and vol > 0 and not na(avgVol) and vol > avgVol
barRange      = high - low
closeLocation = barRange > 0 ? (close - low) / barRange : 0.5
strongCloseUp = closeLocation >= reclaimCloseLocationMin
strongCloseDn = (1 - closeLocation) >= reclaimCloseLocationMin
structBreakUp = close > ta.highest(high[1], reclaimStructLen)
structBreakDn = close < ta.lowest(low[1], reclaimStructLen)
bool tbfExhausted = (tbfState == "late" or tbfState == "passed") and not tbfStale
bool tbfSupportsBullishReclaim = not tbfTop and tbfExhausted
bool tbfSupportsBearishReclaim = tbfTop and tbfExhausted

reclaimUpScore = (strongCloseUp ? 1 : 0) + (volAboveAvg ? 1 : 0) + (structBreakUp ? 1 : 0) + (tbfSupportsBullishReclaim ? 1 : 0)
reclaimDnScore = (strongCloseDn ? 1 : 0) + (volAboveAvg ? 1 : 0) + (structBreakDn ? 1 : 0) + (tbfSupportsBearishReclaim ? 1 : 0)

var int    lastReclaimScore = na
var string lastReclaimDir   = na
if reclaimUp
    lastReclaimScore := reclaimUpScore
    lastReclaimDir   := "▲"
if reclaimDn
    lastReclaimScore := reclaimDnScore
    lastReclaimDir   := "▼"

// ─────────────────────────────────────────────────────────────────────────────
// PLOTS
// ─────────────────────────────────────────────────────────────────────────────
// Active mode draws the complete CURRENT anchor episode from retained points.
// Full mode keeps the normal historical plots for diagnostic review.
bool activeMode = visualMode == "Active"
bool fullMode = not activeMode
bool showBandZones = bandDisplay == "Zones" or bandDisplay == "Zones + Lines"
bool showBandLines = bandDisplay == "Lines" or bandDisplay == "Zones + Lines"

// Historical plots exist only in Full. Anchor bars are gaps so independent
// episodes never connect diagonally.
float fullMidasVisual = fullMode and not anchorPulse ? midas : na
float fullUpper1Visual = fullMode and showBands and not anchorPulse ? upper1 : na
float fullLower1Visual = fullMode and showBands and not anchorPulse ? lower1 : na
float fullUpper2Visual = fullMode and showBands and not anchorPulse ? upper2 : na
float fullLower2Visual = fullMode and showBands and not anchorPulse ? lower2 : na

plot(fullMidasVisual, "MIDAS value", color=color.new(colMidas, 0), linewidth=3, style=plot.style_linebr)
pUpper1 = plot(fullUpper1Visual, "Upper 1", color=showBandLines ? color.new(color.gray, 72) : na, linewidth=1, style=plot.style_linebr)
pLower1 = plot(fullLower1Visual, "Lower 1", color=showBandLines ? color.new(color.gray, 72) : na, linewidth=1, style=plot.style_linebr)
pUpper2 = plot(fullUpper2Visual, "Upper 2", color=showBandLines ? color.new(colStretch, 48) : na, linewidth=1, style=plot.style_linebr)
pLower2 = plot(fullLower2Visual, "Lower 2", color=showBandLines ? color.new(colStretch, 48) : na, linewidth=1, style=plot.style_linebr)

fill(pUpper1, pLower1, color=showBandZones ? color.new(color.gray, 95) : na, title="At-value zone")
fill(pUpper2, pUpper1, color=showBandZones ? color.new(colStretch, 91) : na, title="Upper stretch zone")
fill(pLower1, pLower2, color=showBandZones ? color.new(colStretch, 91) : na, title="Lower stretch zone")

color tbfPlotCol = tbfStale ? color.new(color.gray, 55) : tbfState == "late" or tbfState == "passed" ? color.new(colTBF, 0) : tbfState == "mature" ? color.new(colTBF, 20) : color.new(colTBF, 55)
plot(fullMode ? tbf : na, "TBF exhaustion", color=tbfPlotCol, linewidth=2, style=plot.style_linebr)

// Point stores for the current episode. Backpainted pivots reconstruct the
// origin→confirmation segment so Active never starts from an arbitrary crop.
var array<chart.point> activeMidasPts = array.new<chart.point>()
var array<chart.point> activeUpper1Pts = array.new<chart.point>()
var array<chart.point> activeLower1Pts = array.new<chart.point>()
var array<chart.point> activeUpper2Pts = array.new<chart.point>()
var array<chart.point> activeLower2Pts = array.new<chart.point>()

if anchorPulse
    array.clear(activeMidasPts)
    array.clear(activeUpper1Pts)
    array.clear(activeLower1Pts)
    array.clear(activeUpper2Pts)
    array.clear(activeLower2Pts)
    if isPivotMode and backpainted and volumeIsReal
        float visualCumPV = 0.0
        float visualCumV = 0.0
        float visualCumP2V = 0.0
        float visualCumPVd = 0.0
        float visualCumVd = 0.0
        for i = anchorRight to 0
            float visualP = nz(src[i], src)
            bool visualVolumeMissing = na(vol[i]) or vol[i] <= 0
            float visualV = not volumeIsReal ? na : volumeWeightMode == "Equal weight" ? 1.0 : visualVolumeMissing ? 1.0 : vol[i]
            visualCumPV += visualP * visualV
            visualCumV += visualV
            visualCumP2V += visualP * visualP * visualV
            visualCumPVd += visualP * visualV * visualCumV
            visualCumVd += visualV * visualCumV
            float visualMidas = visualCumPV / visualCumV
            float visualVar = math.max(visualCumP2V / visualCumV - visualMidas * visualMidas, 0)
            float visualVwDev = math.sqrt(visualVar)
            float visualAtrDev = nz(devATR[i], visualVwDev)
            float visualDev = bandMode == "VW StDev" ? visualVwDev : bandMode == "ATR" ? visualAtrDev : math.avg(visualVwDev, visualAtrDev)
            int visualX = bar_index - i
            array.push(activeMidasPts, chart.point.from_index(visualX, visualMidas))
            array.push(activeUpper1Pts, chart.point.from_index(visualX, visualMidas + visualDev * bandMult1))
            array.push(activeLower1Pts, chart.point.from_index(visualX, visualMidas - visualDev * bandMult1))
            array.push(activeUpper2Pts, chart.point.from_index(visualX, visualMidas + visualDev * bandMult2))
            array.push(activeLower2Pts, chart.point.from_index(visualX, visualMidas - visualDev * bandMult2))
    else if not na(midas)
        array.push(activeMidasPts, chart.point.from_index(bar_index, midas))
        if not na(upper1) and not na(lower1) and not na(upper2) and not na(lower2)
            array.push(activeUpper1Pts, chart.point.from_index(bar_index, upper1))
            array.push(activeLower1Pts, chart.point.from_index(bar_index, lower1))
            array.push(activeUpper2Pts, chart.point.from_index(bar_index, upper2))
            array.push(activeLower2Pts, chart.point.from_index(bar_index, lower2))
else if not na(midas)
    array.push(activeMidasPts, chart.point.from_index(bar_index, midas))
    if not na(upper1) and not na(lower1) and not na(upper2) and not na(lower2)
        array.push(activeUpper1Pts, chart.point.from_index(bar_index, upper1))
        array.push(activeLower1Pts, chart.point.from_index(bar_index, lower1))
        array.push(activeUpper2Pts, chart.point.from_index(bar_index, upper2))
        array.push(activeLower2Pts, chart.point.from_index(bar_index, lower2))

// Keep well below TradingView's polyline point ceiling while retaining long
// active episodes. The band polygons use two points per bar.
if array.size(activeMidasPts) > MAX_ACTIVE_BARS
    array.shift(activeMidasPts)
if array.size(activeUpper1Pts) > MAX_ACTIVE_BARS
    array.shift(activeUpper1Pts)
    array.shift(activeLower1Pts)
    array.shift(activeUpper2Pts)
    array.shift(activeLower2Pts)

var polyline activeMidasPl = na
var polyline activeUpper1Pl = na
var polyline activeLower1Pl = na
var polyline activeUpper2Pl = na
var polyline activeLower2Pl = na
var polyline activeValueFillPl = na
var polyline activeUpperFillPl = na
var polyline activeLowerFillPl = na
var polyline activeTbfPl = na

// Drawing objects are subject to Pine's realtime rollback. Rebuild the bounded
// active episode on the last bar so the complete curve survives every update;
// otherwise only a one-bar realtime fragment can remain visible.
if barstate.islast
    polyline.delete(activeMidasPl)
    polyline.delete(activeUpper1Pl)
    polyline.delete(activeLower1Pl)
    polyline.delete(activeUpper2Pl)
    polyline.delete(activeLower2Pl)
    polyline.delete(activeValueFillPl)
    polyline.delete(activeUpperFillPl)
    polyline.delete(activeLowerFillPl)
    polyline.delete(activeTbfPl)

    activeMidasPl := na
    activeUpper1Pl := na
    activeLower1Pl := na
    activeUpper2Pl := na
    activeLower2Pl := na
    activeValueFillPl := na
    activeUpperFillPl := na
    activeLowerFillPl := na
    activeTbfPl := na

    if activeMode and array.size(activeMidasPts) > 1
        int activeBandSize = array.size(activeUpper1Pts)
        if showBands and showBandZones and activeBandSize > 1
            array<chart.point> activeValuePolygon = array.new<chart.point>()
            array<chart.point> activeUpperPolygon = array.new<chart.point>()
            array<chart.point> activeLowerPolygon = array.new<chart.point>()
            for i = 0 to activeBandSize - 1
                array.push(activeValuePolygon, array.get(activeUpper1Pts, i))
                array.push(activeUpperPolygon, array.get(activeUpper2Pts, i))
                array.push(activeLowerPolygon, array.get(activeLower1Pts, i))
            for i = activeBandSize - 1 to 0
                array.push(activeValuePolygon, array.get(activeLower1Pts, i))
                array.push(activeUpperPolygon, array.get(activeUpper1Pts, i))
                array.push(activeLowerPolygon, array.get(activeLower2Pts, i))
            activeValueFillPl := polyline.new(activeValuePolygon, curved=false, closed=true,
                 line_color=color.new(color.gray, 100), fill_color=color.new(color.gray, 95))
            activeUpperFillPl := polyline.new(activeUpperPolygon, curved=false, closed=true,
                 line_color=color.new(colStretch, 100), fill_color=color.new(colStretch, 91))
            activeLowerFillPl := polyline.new(activeLowerPolygon, curved=false, closed=true,
                 line_color=color.new(colStretch, 100), fill_color=color.new(colStretch, 91))

        if showBands and showBandLines and activeBandSize > 1
            activeUpper1Pl := polyline.new(activeUpper1Pts, curved=false, closed=false, line_color=color.new(color.gray, 72), line_width=1)
            activeLower1Pl := polyline.new(activeLower1Pts, curved=false, closed=false, line_color=color.new(color.gray, 72), line_width=1)
            activeUpper2Pl := polyline.new(activeUpper2Pts, curved=false, closed=false, line_color=color.new(colStretch, 48), line_width=1)
            activeLower2Pl := polyline.new(activeLower2Pts, curved=false, closed=false, line_color=color.new(colStretch, 48), line_width=1)

        activeMidasPl := polyline.new(activeMidasPts, curved=false, closed=false, line_color=colMidas, line_width=3)

        // Recalculate the current TBF from the active episode's cumulative
        // statistics and latest accepted D. From Fit (default) respects when
        // that model became live-known; Retrospective is analysis-only.
        int activeTbfSize = array.size(episodeStatX)
        if enableTBF and not na(tbfD) and activeTbfSize > 1
            array<chart.point> activeTbfPts = array.new<chart.point>()
            for i = 0 to activeTbfSize - 1
                float pointA = array.get(episodeStatA, i)
                float pointB = array.get(episodeStatB, i)
                float pointC = array.get(episodeStatC, i)
                float pointE = array.get(episodeStatE, i)
                int pointX = array.get(episodeStatX, i)
                bool pointVisible = tbfHistoryMode == "Retrospective" or (not na(tbfFitAcceptedBar) and pointX >= tbfFitAcceptedBar)
                if pointVisible and pointC < tbfD
                    float pointDenom = pointC * tbfD - pointE
                    if pointDenom != 0
                        float pointTbf = (pointA * tbfD - pointB) / pointDenom
                        array.push(activeTbfPts, chart.point.from_index(pointX, pointTbf))
            if array.size(activeTbfPts) > 1
                activeTbfPl := polyline.new(activeTbfPts, curved=false, closed=false, line_color=tbfPlotCol, line_width=2)

// Anchor marker — Backpainted pivot modes mark the real pivot bar (offset back);
// time/daily and Confirmed pivot modes mark the current (live-known) bar
plotshape(volumeIsReal and fullMode and showAnchor and anchorPulse and isPivotMode and backpainted ? src[anchorRight] : na, "Anchor (pivot, backpainted)",
     style=shape.diamond, location=location.absolute, offset=-anchorRight,
     color=color.new(colMidas, 0), text="A", textcolor=color.white, size=size.small)
plotshape(volumeIsReal and fullMode and showAnchor and anchorPulse and (not isPivotMode or not backpainted) ? src : na, "Anchor",
     style=shape.diamond, location=location.absolute,
     color=color.new(colMidas, 0), text="A", textcolor=color.white, size=size.small)

var label activeAnchorLabel = na
var array<label> activeMarkerLabels = array.new<label>()
const int MAX_ACTIVE_MARKERS = 300
if anchorPulse and activeMode and volumeIsReal
    label.delete(activeAnchorLabel)
    activeAnchorLabel := na
    while array.size(activeMarkerLabels) > 0
        label.delete(array.pop(activeMarkerLabels))
    lastReclaimScore := na
    lastReclaimDir := na
    if showAnchor
        int activeAnchorX = isPivotMode and backpainted ? bar_index - anchorRight : bar_index
        float activeAnchorY = isPivotMode and backpainted ? src[anchorRight] : src
        activeAnchorLabel := label.new(activeAnchorX, activeAnchorY, "A", yloc=yloc.price,
             style=label.style_diamond, color=colMidas, textcolor=color.white, size=size.tiny,
             tooltip="Active MIDAS anchor. Every visible curve and zone in Active mode accumulates from this episode origin.")

plotshape(fullMode and showMarkers and stretchAbove ? upper2 : na, "Stretched above value", style=shape.circle, location=location.absolute,
     color=color.new(colStretch, 15), size=size.tiny)
plotshape(fullMode and showMarkers and stretchBelow ? lower2 : na, "Stretched below value", style=shape.circle, location=location.absolute,
     color=color.new(colStretch, 15), size=size.tiny)

if activeMode and showMarkers and stretchAbove
    string stretchAboveClass = bias == 1 ? "Trend-side overextension" : "Counter-bias rejection"
    label activeStretchUpLabel = label.new(bar_index, upper2, "•", yloc=yloc.price,
         style=label.style_none, textcolor=colStretch, size=size.small,
         tooltip="STRETCH ABOVE VALUE · " + stretchAboveClass + "\nPrice traded beyond the outer band and closed back inside. Context only: wait for a MIDAS loss/reclaim or another price trigger.")
    array.push(activeMarkerLabels, activeStretchUpLabel)
    if array.size(activeMarkerLabels) > MAX_ACTIVE_MARKERS
        label.delete(array.shift(activeMarkerLabels))
if activeMode and showMarkers and stretchBelow
    string stretchBelowClass = bias == -1 ? "Trend-side overextension" : "Counter-bias rejection"
    label activeStretchDnLabel = label.new(bar_index, lower2, "•", yloc=yloc.price,
         style=label.style_none, textcolor=colStretch, size=size.small,
         tooltip="STRETCH BELOW VALUE · " + stretchBelowClass + "\nPrice traded beyond the outer band and closed back inside. Context only: wait for a MIDAS reclaim/loss or another price trigger.")
    array.push(activeMarkerLabels, activeStretchDnLabel)
    if array.size(activeMarkerLabels) > MAX_ACTIVE_MARKERS
        label.delete(array.shift(activeMarkerLabels))

// plotshape's text arg must be a const string, so the dynamic Reclaim Quality
// Score is rendered via label.new instead (shape/position match the old markers)
if showMarkers and reclaimUp
    string reclaimUpClass = bias == 1 ? "Trend-aligned continuation reclaim" : "Counter-bias transition reclaim"
    string reclaimUpTip = "Bullish MIDAS reclaim · " + reclaimUpClass + " · quality " + str.tostring(reclaimUpScore) + "/4\n" +
         "Strong close: " + (strongCloseUp ? "yes" : "no") + " · Volume: " + (volAboveAvg ? "yes" : "no") +
         "\nStructure break: " + (structBreakUp ? "yes" : "no") + " · Bottomfinder aligned: " + (tbfSupportsBullishReclaim ? "yes" : "no")
    label reclaimUpLabel = label.new(bar_index, low, "R " + str.tostring(reclaimUpScore) + "/4", yloc=yloc.belowbar,
         style=label.style_label_up, color=color.new(colUp, reclaimUpScore >= 3 ? 0 : reclaimUpScore >= 1 ? 20 : 45),
         textcolor=color.white, size=reclaimUpScore >= 3 ? size.small : size.tiny, tooltip=reclaimUpTip)
    if activeMode
        array.push(activeMarkerLabels, reclaimUpLabel)
        if array.size(activeMarkerLabels) > MAX_ACTIVE_MARKERS
            label.delete(array.shift(activeMarkerLabels))
if showMarkers and reclaimDn
    string reclaimDnClass = bias == -1 ? "Trend-aligned continuation reclaim" : "Counter-bias transition reclaim"
    string reclaimDnTip = "Bearish MIDAS reclaim · " + reclaimDnClass + " · quality " + str.tostring(reclaimDnScore) + "/4\n" +
         "Strong close: " + (strongCloseDn ? "yes" : "no") + " · Volume: " + (volAboveAvg ? "yes" : "no") +
         "\nStructure break: " + (structBreakDn ? "yes" : "no") + " · Topfinder aligned: " + (tbfSupportsBearishReclaim ? "yes" : "no")
    label reclaimDnLabel = label.new(bar_index, high, "R " + str.tostring(reclaimDnScore) + "/4", yloc=yloc.abovebar,
         style=label.style_label_down, color=color.new(colDn, reclaimDnScore >= 3 ? 0 : reclaimDnScore >= 1 ? 20 : 45),
         textcolor=color.white, size=reclaimDnScore >= 3 ? size.small : size.tiny, tooltip=reclaimDnTip)
    if activeMode
        array.push(activeMarkerLabels, reclaimDnLabel)
        if array.size(activeMarkerLabels) > MAX_ACTIVE_MARKERS
            label.delete(array.shift(activeMarkerLabels))
if showMarkers and tbfReactionNew
    string reactionTip = (tbfTop ? "TOPFINDER" : "BOTTOMFINDER") + " PRICE REACTION · " + str.tostring(tbfReactionScore) + "/3\n" +
         "Rejection candle: " + (tbfReactionCandle ? "yes" : "no") + " · ATR displacement: " + (tbfReactionStructure ? "yes" : "no") +
         "\nReturn through inner band: " + (tbfReactionBand ? "yes" : "no") +
         "\nThe projection is advanced and price has reacted. This confirms exhaustion risk, not a reversal entry."
    label reactionLabel = label.new(bar_index, tbfTop ? high : low, "EXH " + str.tostring(tbfReactionScore) + "/3",
         yloc=tbfTop ? yloc.abovebar : yloc.belowbar,
         style=tbfTop ? label.style_label_down : label.style_label_up,
         color=color.new(colTBF, 0), textcolor=color.white, size=size.tiny, tooltip=reactionTip)
    if activeMode
        array.push(activeMarkerLabels, reactionLabel)
        if array.size(activeMarkerLabels) > MAX_ACTIVE_MARKERS
            label.delete(array.shift(activeMarkerLabels))
plotshape(fullMode and showMarkers and markerOk and tbfLateNew and tbfTop, "TBF late move (top)", style=shape.labeldown, location=location.abovebar,
     color=color.new(colTBF, 0), text="LATE", textcolor=color.white, size=size.tiny)
plotshape(fullMode and showMarkers and markerOk and tbfLateNew and not tbfTop, "TBF late move (bottom)", style=shape.labelup, location=location.belowbar,
     color=color.new(colTBF, 0), text="LATE", textcolor=color.white, size=size.tiny)
plotshape(fullMode and showMarkers and markerOk and tbfPassedNew and tbfTop, "TBF projected end passed (top)", style=shape.diamond, location=location.abovebar,
     color=color.new(colStretch, 0), text="END", textcolor=color.white, size=size.tiny)
plotshape(fullMode and showMarkers and markerOk and tbfPassedNew and not tbfTop, "TBF projected end passed (bottom)", style=shape.diamond, location=location.belowbar,
     color=color.new(colStretch, 0), text="END", textcolor=color.white, size=size.tiny)

// ─────────────────────────────────────────────────────────────────────────────
// INTERPRETATION  (verbalises role outputs and names the next observable trigger)
// ─────────────────────────────────────────────────────────────────────────────
tfMin    = timeframe.in_seconds() / 60.0
tfBucket = tfMin < 240 ? "Intraday" : tfMin < 1440 ? "Swing" : "Position"
higherTf = tfMin >= 240                       // Swing / Position lean on Location & reversion
locMid   = higherTf ? 1.0 : 1.5               // intraday is noisier → a wider stretch bar
locHi    = higherTf ? 2.0 : 2.5

absD       = na(normalizedDistance) ? na : math.abs(normalizedDistance)
locUnit    = bandMode == "VW StDev" ? " σ" : bandMode == "ATR" ? " atr" : " hdev"
locStr     = na(normalizedDistance) ? "n/a" : str.tostring(normalizedDistance, "0.00") + locUnit
dirWord    = na(normalizedDistance) ? "" : normalizedDistance > 0 ? " up" : " down"
locRead    = na(normalizedDistance) ? "n/a" : absD >= locHi ? "overextended" + dirWord : absD >= locMid ? "stretched" + dirWord : "at value"

zone       = na(midas) ? "n/a" : close > upper2 ? "above U2" : close > upper1 ? "U1–U2" : close > lower1 ? "inside" : close > lower2 ? "L1–L2" : "below L2"
zoneRead   = na(midas) ? "n/a" : zone == "inside" ? "at value" : (zone == "above U2" or zone == "below L2") ? "extreme" : "stretched"

midasSlope = na(midas) ? na : midas - midas[3]
trendVal   = na(midas) ? "n/a" : (close > midas ? "above" : "below") + " value" + (na(midasSlope) ? "" : midasSlope > 0 ? " ↑" : midasSlope < 0 ? " ↓" : " →")
trendRead  = na(midas) ? "n/a" : close > midas ? "buyers since launch" : "sellers since launch"

tbfStr     = not enableTBF ? "off" : na(progress) ? "no fit" : tbfState + " " + str.tostring(progress * 100, "0") + "%"
tbfRead    = not enableTBF ? "off" : tbfState == "no fit" ? "no accelerated fit" : tbfStale ? "stale fit · await refit" : tbfPendingConfirmations > 0 ? tbfFitQuality + " active · candidate 1/2" : tbfFitReactionSeen ? "fit reaction " + str.tostring(tbfFitBestReactionScore) + "/3" : tbfEpisodeReactionSeen ? "episode reacted earlier" : tbfState == "developing" ? "early · " + tbfFitQuality : tbfState == "mature" ? "usable · " + tbfFitQuality : "await price reaction"

ageRead    = na(age) ? "n/a" : age < 2 * anchorRight ? "fresh (thin)" : "established"
anchorDelayNote = (isPivotMode and backpainted) ? " (drawn " + str.tostring(anchorRight) + "b back)" : ""

flat = not na(dev) and dev > 0 and not na(midasSlope) and math.abs(midasSlope) < dev * 0.15

// Role-separated live context. Trend, Location, and Exhaustion may disagree; that
// disagreement is information and must remain visible instead of being averaged
// into a single bullish/bearish headline.
bool liveBullStructure = not na(midas) and close > midas and (na(midasSlope) or midasSlope > 0)
bool liveBearStructure = not na(midas) and close < midas and (na(midasSlope) or midasSlope < 0)
bool liveRangeStructure = not na(midas) and zone == "inside" and flat
bool liveLocationExtreme = zoneRead == "extreme"
bool liveLocationStretched = zoneRead == "stretched"

string liveSignedDistance = na(normalizedDistance) ? "n/a" : (normalizedDistance > 0 ? "+" : "") + str.tostring(normalizedDistance, "0.00") + locUnit
string tbfDirectionTag = tbfTop ? "TOP ↑" : "BOTTOM ↓"
string tbfQualityTag = tbfFitQuality == "stable" ? "STABLE" : "PROVISIONAL"
string tbfCandidateTag = tbfPendingConfirmations > 0 ? " · CANDIDATE 1/2" : ""
string tbfAdvancedEvidence = tbfFitReactionSeen ? "FIT REACTION " + str.tostring(tbfFitBestReactionScore) + "/3" : tbfEpisodeReactionSeen ? "EPISODE REACTION " + str.tostring(tbfEpisodeBestReactionScore) + "/3" : "NO REACTION"
string liveTrendValue = not anchorActive ? "— WAITING FOR MANUAL ANCHOR" : na(midas) ? "— WARMING UP" :
     liveRangeStructure ? "↔ RANGE AROUND MIDAS" :
     liveBullStructure ? "↑ ABOVE RISING MIDAS" :
     liveBearStructure ? "↓ BELOW FALLING MIDAS" : "↕ TRANSITION / CONFLICT"
string liveLocationValue = na(normalizedDistance) ? "— UNAVAILABLE" :
     liveLocationExtreme ? "!! EXTREME " + liveSignedDistance :
     liveLocationStretched ? "! STRETCHED " + liveSignedDistance : "· AT VALUE " + liveSignedDistance
string liveExhaustionValue = not enableTBF ? "— OFF" : na(progress) ? "— NO ACCELERATED FIT" :
     tbfStale ? tbfDirectionTag + " · ? STALE FIT " + str.tostring(progress * 100, "0") + "% · " + tbfAdvancedEvidence :
     tbfState == "passed" and progress < 1.15 ? tbfDirectionTag + " · !! RECENTLY PASSED " + str.tostring(progress * 100, "0") + "% · " + tbfAdvancedEvidence :
     tbfState == "passed" ? tbfDirectionTag + " · ! BEYOND FIT " + str.tostring(progress * 100, "0") + "% · " + tbfAdvancedEvidence :
     tbfState == "late" ? tbfDirectionTag + " · ! LATE " + str.tostring(progress * 100, "0") + "% · " + tbfAdvancedEvidence + tbfCandidateTag :
     tbfState == "mature" ? tbfDirectionTag + " · MATURE " + str.tostring(progress * 100, "0") + "% · " + tbfQualityTag + tbfCandidateTag :
     tbfDirectionTag + " · DEVELOPING " + str.tostring(progress * 100, "0") + "% · " + tbfQualityTag + tbfCandidateTag
// The fourth row is not another state summary. It names the next observable
// condition that would make the current context more useful.
string liveNextValue = not anchorActive ? "WAIT UNTIL MANUAL ANCHOR TIME" : na(midas) ? "WAIT FOR ACTIVE MIDAS CONTEXT" :
     tbfStale ? "WAIT FOR NEW STABLE TBF FIT" :
     tbfAdvanced and tbfFitReactionSeen ? (tbfTop ? "WAIT FOR MIDAS LOSS TO CONFIRM REVERSION" : "WAIT FOR MIDAS RECLAIM TO CONFIRM REVERSION") :
     tbfAdvanced ? "WAIT FOR EXH 2/3 PRICE REACTION" :
     liveLocationExtreme ? "WAIT FOR CLOSE BACK INSIDE OUTER BAND" :
     liveLocationStretched and liveBullStructure ? "WAIT FOR PULLBACK INTO INNER VALUE BAND" :
     liveLocationStretched and liveBearStructure ? "WAIT FOR RALLY INTO INNER VALUE BAND" :
     liveLocationStretched ? "WAIT FOR RETURN INSIDE INNER VALUE BAND" :
     liveRangeStructure ? "WAIT FOR INNER BAND TEST" :
     liveBullStructure ? "WATCH MIDAS / INNER BAND ON PULLBACK" :
     liveBearStructure ? "WATCH MIDAS / INNER BAND ON RALLY" :
     "WAIT FOR PRICE + MIDAS SLOPE TO ALIGN"
string liveTrendLine = "TREND · " + liveTrendValue
string liveLocationLine = "LOCATION · " + liveLocationValue
string liveExhaustionLine = "EXHAUSTION · " + liveExhaustionValue
string liveNextLine = "NEXT · " + liveNextValue
string liveDetailedText = liveTrendLine + "\n" + liveLocationLine + "\n" + liveExhaustionLine + "\n" + liveNextLine
string liveTrendCompact = not anchorActive ? "— ANCHOR PENDING" : liveRangeStructure ? "↔ RANGE" : liveBullStructure ? "↑ TREND" : liveBearStructure ? "↓ TREND" : "↕ TRANSITION"
string liveLocationCompact = na(normalizedDistance) ? "LOCATION · —" : liveLocationExtreme ? "LOCATION · !! " + liveSignedDistance : liveLocationStretched ? "LOCATION · ! " + liveSignedDistance : "LOCATION · VALUE " + liveSignedDistance
string liveExhaustionCompact = not enableTBF ? "EXHAUSTION · OFF" : na(progress) ? "EXHAUSTION · NO FIT" : tbfStale ? tbfDirectionTag + " · STALE " + str.tostring(progress * 100, "0") + "%" : tbfDirectionTag + " · " + str.tostring(progress * 100, "0") + "% · " + (tbfFitReactionSeen ? "REACTION " + str.tostring(tbfFitBestReactionScore) + "/3" : "NO REACTION")
string liveNextCompact = not anchorActive ? "ANCHOR TIME" : tbfStale ? "STABLE REFIT" : tbfAdvanced and tbfFitReactionSeen ? (tbfTop ? "MIDAS LOSS" : "MIDAS RECLAIM") : tbfAdvanced ? "EXH 2/3 REACTION" : liveLocationExtreme ? "CLOSE INSIDE OUTER BAND" : liveLocationStretched ? "RETURN TO INNER BAND" : liveRangeStructure ? "INNER BAND TEST" : liveBullStructure ? "PULLBACK TO MIDAS" : liveBearStructure ? "RALLY TO MIDAS" : "PRICE + SLOPE ALIGN"
string liveCompactText = liveTrendCompact + " · " + liveLocationCompact + "\n" + liveExhaustionCompact + "\nNEXT · " + liveNextCompact
string liveContextText = liveBadgeMode == "Detailed" ? liveDetailedText : liveBadgeMode == "Tooltip only" ? "MIDAS · INFO" : liveCompactText
color liveContextCol = color.new(color.gray, 20)

string liveStructureDetail = not anchorActive ? "Manual Time has not been reached. No MIDAS episode or pre-anchor calculation is active." : na(midas) ? "The active MIDAS episode is still warming up." :
     liveBullStructure ? "Price is above a rising MIDAS: buyers control the active anchor episode. MIDAS is a potential support reference only if a pullback actually tests and holds it." :
     liveBearStructure ? "Price is below a falling MIDAS: sellers control the active anchor episode. MIDAS is a potential resistance reference only if a rally actually tests and rejects it." :
     liveRangeStructure ? "MIDAS is nearly flat and price is rotating around value: range context." :
     "Price side and MIDAS slope do not agree: transition context, not a clean directional read."
string liveLocationDetail = na(normalizedDistance) ? "The location reading is not available yet." :
     not liveLocationStretched and not liveLocationExtreme ? "Inside the visible inner bands: location is not extended." :
     liveLocationStretched ? "Between the visible inner and outer band: the trend may be intact, while the current location is already extended." :
     liveLocationExtreme ? "Beyond the visible outer band: reversion risk is elevated, but this is still context rather than a trigger." :
     "The band zone is not available yet."
string tbfShiftText = na(tbfFitShift) ? "n/a (first fit)" : str.tostring(tbfFitShift * 100, "0") + "%"
string tbfFitProgressText = na(tbfFitProgress) ? "n/a" : str.tostring(tbfFitProgress * 100, "0") + "%"
string tbfPendingText = tbfPendingConfirmations > 0 ? "candidate 1/2 · D=" + str.tostring(tbfPendingD, "0") + " · shift=" + str.tostring(tbfPendingShift * 100, "0") + "%" : "none"
string tbfRemainingText = na(remainingVolume) ? "n/a" : str.tostring(remainingVolume, "0")
string tbfBarsText = na(approxBarsRemaining) ? "n/a" : "~" + str.tostring(approxBarsRemaining, "0") + " bars at episode-average volume"
string tbfReactionDetail = tbfFitReactionSeen ? "Current-fit reaction confirmed " + str.tostring(tbfFitBestReactionScore) + "/3: at least two of rejection candle, ATR-sized displacement, and inner-band transition occurred." : tbfEpisodeReactionSeen ? "No reaction on the current fit; this anchor episode reacted earlier at " + str.tostring(tbfEpisodeBestReactionScore) + "/3." : "No qualifying 2/3 price reaction yet. Projection progress alone does not confirm a turn."
string liveTbfDetail = not enableTBF ? "TBF is disabled, so exhaustion timing is not evaluated." :
     na(progress) ? "No valid forward topfinder/bottomfinder projection. Exhaustion timing is unavailable; this is not evidence that the move must continue." :
     (tbfTop ? "Topfinder for the active up-leg." : "Bottomfinder for the active down-leg.") +
     "\nPhase: " + tbfState + " at " + str.tostring(progress * 100, "0") + "% of D." +
     "\nFit: " + tbfFitQuality + " · accepted fits: " + str.tostring(tbfFitCount) + " · latest accepted D change: " + tbfShiftText + " · fit-pivot maturity: " + tbfFitProgressText + "." +
     "\nPending fit: " + tbfPendingText + " · history: " + tbfHistoryMode + "." +
     "\nRemaining projected volume: " + tbfRemainingText + " · rough time translation: " + tbfBarsText + "." +
     "\n" + tbfReactionDetail
string liveChangeDetail = (tbfAdvanced and not tbfFitReactionSeen ? "Exhaustion confirmation: wait for an EXH n/3 current-fit reaction.\n" : "") +
     (liveBearStructure ? "Bullish change: price reclaims MIDAS after a lower-band stretch (green R n/4).\nBearish continuation context: a rally rejects the falling MIDAS." :
     liveBullStructure ? "Bearish change: price loses MIDAS after an upper-band stretch (red R n/4).\nBullish continuation context: a pullback holds the rising MIDAS." :
     "Directional change: price and MIDAS slope align on the same side. A reclaim label adds trigger-level evidence.")
string liveAnchorDetail = na(age) ? "n/a" : str.tostring(age) + " bars" + anchorDelayNote
string volumeAvailabilityText = na(volumeAvailability) ? "n/a" : str.tostring(volumeAvailability * 100, "0") + "%"
string volumeDetail = volumeWeightMode == "Equal weight" ? "Equal weight selected; reported volume is intentionally not used. Data availability: " + volumeAvailabilityText + "." : "Episode weighting: " + volumeQuality + " · reported volume available on " + volumeAvailabilityText + " of bars. Missing/zero bars use weight 1."
string liveMidasText = na(midas) ? "n/a" : str.tostring(midas, format.mintick)
string liveSlopeText = na(midasSlope) ? "n/a" : str.tostring(midasSlope, format.mintick)
string liveContextTip = "MIDAS LIVE CONTEXT\n\nSTRUCTURE\nPrice: " + str.tostring(close, format.mintick) +
     "\nMIDAS: " + liveMidasText + "\n3-bar slope: " + liveSlopeText +
     "\n" + liveStructureDetail + "\n\nLOCATION\nDistance: " + liveSignedDistance + " from MIDAS\nZone: " + zone +
     "\n" + liveLocationDetail + "\n\nEXHAUSTION\n" + liveTbfDetail +
     "\n\nNEXT TRIGGER\n" + liveNextValue + "\n" + liveChangeDetail + "\n\nANCHOR\n" + liveAnchorDetail +
     "\n\nVOLUME QUALITY\n" + volumeDetail

var label liveContextLabel = na
if barstate.islast
    label.delete(liveContextLabel)
    liveContextLabel := na
    if showLiveContext and volumeIsReal
        float liveContextY = not na(midas) ? midas : close
        liveContextLabel := label.new(bar_index + 1, liveContextY, liveContextText, yloc=yloc.price,
             style=label.style_label_left, color=liveContextCol, textcolor=color.white, size=size.small,
             tooltip=liveContextTip)

// ─────────────────────────────────────────────────────────────────────────────
// DASHBOARD  (light theme — see CLAUDE.md)
// ─────────────────────────────────────────────────────────────────────────────
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
posSel = tablePos == "Top Right" ? position.top_right : tablePos == "Top Left" ? position.top_left : tablePos == "Bottom Right" ? position.bottom_right : position.bottom_left
bool compactDashboard = dashboardMode == "Compact"
bool tbfActive = (tbfState == "late" or tbfState == "passed") and not tbfStale
color tbfStatusBg = tbfStale ? color.new(color.gray, 80) : tbfState == "passed" ? color.new(colStretch, 0) : tbfState == "late" ? color.new(colTBF, 0) : color.new(color.gray, 80)
color locationCol = zoneRead == "at value" ? tc : colStretch

if showTable and barstate.islast and volumeIsReal
    var table t = table.new(posSel, compactDashboard ? 2 : 3, compactDashboard ? 5 : 10,
         bgcolor=color.new(color.white, 5),
         border_color=color.new(color.gray, 60),
         border_width=1,
         frame_color=color.new(color.gray, 40),
         frame_width=1)

    if compactDashboard
        // Three state roles plus one next observable trigger.
        table.cell(t, 0, 0, "MIDAS 2.0 · " + tfBucket, text_color=tc, text_size=size.small, bgcolor=hd)
        table.cell(t, 1, 0, visualMode, text_color=tc, text_size=size.small, bgcolor=hd)

        table.cell(t, 0, 1, "Trend", text_color=tc, text_size=size.small)
        table.cell(t, 1, 1, liveTrendValue, text_color=liveBullStructure ? colUp : liveBearStructure ? colDn : tc, text_size=size.small)

        table.cell(t, 0, 2, "Location", text_color=tc, text_size=size.small)
        table.cell(t, 1, 2, liveLocationValue, text_color=locationCol, text_size=size.small)

        table.cell(t, 0, 3, "Exhaustion", text_color=tc, text_size=size.small)
        table.cell(t, 1, 3, liveExhaustionValue, text_color=tbfActive ? color.white : tc, bgcolor=tbfStatusBg, text_size=size.small)

        table.cell(t, 0, 4, "Next", text_color=tc, text_size=size.small, bgcolor=hd)
        table.cell(t, 1, 4, liveNextValue, text_color=tc, text_size=size.small, bgcolor=hd)
    else
        // Detailed Element | Value | Interpretation diagnostic view.
        table.cell(t, 0, 0, "MIDAS 2.0", text_color=tc, text_size=size.normal, bgcolor=hd)
        table.cell(t, 1, 0, tfBucket, text_color=tc, text_size=size.normal, bgcolor=hd)
        table.cell(t, 2, 0, bias == 1 ? "Bull" : "Bear", text_color=bias == 1 ? colUp : colDn, text_size=size.normal, bgcolor=hd)

        table.cell(t, 0, 1, "Location", text_color=tc, text_size=size.small)
        table.cell(t, 1, 1, locStr, text_color=locationCol, text_size=size.small)
        table.cell(t, 2, 1, locRead, text_color=locationCol, text_size=size.small)

        table.cell(t, 0, 2, "Band zone", text_color=tc, text_size=size.small)
        table.cell(t, 1, 2, zone, text_color=locationCol, text_size=size.small)
        table.cell(t, 2, 2, zoneRead, text_color=locationCol, text_size=size.small)

        table.cell(t, 0, 3, tbfTop ? "Topfinder" : "Bottomfinder", text_color=tc, text_size=size.small)
        table.cell(t, 1, 3, tbfStr, text_color=tbfActive ? color.white : tc, bgcolor=tbfStatusBg, text_size=size.small)
        table.cell(t, 2, 3, tbfRead, text_color=tbfActive ? (tbfState == "passed" ? colStretch : colTBF) : tc, text_size=size.small)

        table.cell(t, 0, 4, "Trend", text_color=tc, text_size=size.small)
        table.cell(t, 1, 4, trendVal, text_color=na(midas) ? tc : (close > midas ? colUp : colDn), text_size=size.small)
        table.cell(t, 2, 4, trendRead, text_color=tc, text_size=size.small)

        table.cell(t, 0, 5, "Anchor", text_color=tc, text_size=size.small)
        table.cell(t, 1, 5, na(age) ? "n/a" : str.tostring(age) + " bars", text_color=tc, text_size=size.small)
        table.cell(t, 2, 5, ageRead + anchorDelayNote, text_color=tc, text_size=size.small)

        table.cell(t, 0, 6, "Volume", text_color=tc, text_size=size.small)
        table.cell(t, 1, 6, volumeAvailabilityText + " available", text_color=not na(volumeAvailability) and volumeAvailability < 0.95 ? colStretch : tc, text_size=size.small)
        table.cell(t, 2, 6, volumeQuality, text_color=tc, text_size=size.small)

        table.cell(t, 0, 7, "Reclaim Q", text_color=tc, text_size=size.small)
        table.cell(t, 1, 7, na(lastReclaimScore) ? "n/a" : str.tostring(lastReclaimScore) + "/4 " + lastReclaimDir,
             text_color=na(lastReclaimScore) ? tc : (lastReclaimDir == "▲" ? colUp : colDn), text_size=size.small)
        table.cell(t, 2, 7, na(lastReclaimScore) ? "—" : (lastReclaimScore >= 3 ? "high quality" : lastReclaimScore >= 1 ? "medium" : "weak"), text_color=tc, text_size=size.small)

        table.cell(t, 0, 8, "", bgcolor=color.new(color.gray, 80))
        table.cell(t, 1, 8, "", bgcolor=color.new(color.gray, 80))
        table.cell(t, 2, 8, "", bgcolor=color.new(color.gray, 80))

        table.cell(t, 0, 9, "Next", text_color=tc, text_size=size.normal, bgcolor=hd)
        table.cell(t, 1, 9, liveNextValue, text_color=tc, text_size=size.normal, bgcolor=hd)
        table.cell(t, 2, 9, "", text_color=tc, text_size=size.normal, bgcolor=hd)

// ─────────────────────────────────────────────────────────────────────────────
// ALERTS
// ─────────────────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and rawStretchAbove, "Stretched above value", "MIDAS · STRETCHED ABOVE VALUE · {{ticker}} {{interval}}")
alertcondition(alertOk and rawStretchBelow, "Stretched below value", "MIDAS · STRETCHED BELOW VALUE · {{ticker}} {{interval}}")
alertcondition(alertOk and rawReclaimUp, "MIDAS bullish reclaim", "MIDAS · BULLISH RECLAIM · {{ticker}} {{interval}}")
alertcondition(alertOk and rawReclaimDn, "MIDAS bearish reclaim", "MIDAS · BEARISH RECLAIM · {{ticker}} {{interval}}")
alertcondition(alertOk and (tbfLateNew), "TBF late move", "MIDAS · TBF LATE MOVE · {{ticker}} {{interval}}")
alertcondition(alertOk and (tbfPassedNew), "TBF projection passed", "MIDAS · TBF PROJECTION PASSED · {{ticker}} {{interval}}")
alertcondition(alertOk and (tbfReactionNew), "TBF price reaction", "MIDAS · TBF PRICE REACTION 2/3+ · {{ticker}} {{interval}}")

// ─────────────────────────────────────────────────────────────────────────────
// DEBUG
// ─────────────────────────────────────────────────────────────────────────────
if dbg and barstate.isconfirmed
    if anchorPulse
        log.info("MIDAS ANCHOR mode={0} price={1}", anchorMode, str.tostring(src, format.mintick))
    if fitBlockedByHistory
        log.info("MIDAS TBF FIT BLOCKED pivotBar={0} confirmationBar={1} reason=reconstructed episode statistics unavailable",
             fitPivotBar, anchorConfirmationBar)
    if unstableCandidateObserved
        log.info("MIDAS TBF CANDIDATE HELD D={0} activeD={1} shift={2} confirmations={3}/2",
             str.tostring(tbfPendingD, "0"), str.tostring(tbfD, "0"), str.tostring(tbfPendingShift, "0.00"), tbfPendingConfirmations)
    if fitAccepted
        log.info("MIDAS TBF FIT dir={0} D={1} d={2} T={3} quality={4} shift={5} fitProgress={6}",
             tbfTop ? "top" : "bot", str.tostring(tbfD, "0"), str.tostring(cumV, "0"), str.tostring(fitT, format.mintick),
             tbfFitQuality, str.tostring(tbfFitShift, "0.00"), str.tostring(tbfFitProgress, "0.00"))
    if tbfLateNew
        log.info("MIDAS TBF LATE progress={0} midas={1} close={2}", str.tostring(progress, "0.00"), str.tostring(midas, format.mintick), str.tostring(close, format.mintick))
    if tbfPassedNew
        log.info("MIDAS TBF PASSED progress={0} fit={1}", str.tostring(progress, "0.00"), tbfFitQuality)
    if tbfReactionNew
        log.info("MIDAS TBF REACTION dir={0} score={1}/3 candle={2} displacement={3} band={4}",
             tbfTop ? "top" : "bot", tbfReactionScore, tbfReactionCandle, tbfReactionStructure, tbfReactionBand)

Overview

MIDAS 2.0 [WavesUnchained] adds two configurable volume-weighted standard-deviation, ATR, or hybrid bands and a topfinder or bottomfinder accelerated exhaustion curve. Anchors can be a manual time, the daily open, or an automatic swing. Settings change band multiples, EMA trend bias, and whether auto-swing anchors are backpainted or confirmed. Context markers and a reclaim quality score describe stretch and reclaim; they are not trade entries. The default backpainted swing mode revises the curve until the pivot is confirmed.

Details

Published by

casoon on GitHub

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • Signals can change on the live bar or after it closes (repainting)

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Anchor onstringAuto Last SwinganchorMode
Anchor DisplaystringBackpaintedanchorDisplayMode
Manual Anchortimetimestamp("01 Jan 2026 00:00 +0000")anchorTime
Anchor Swing Leftint20anchorLeft
Anchor Swing Rightint20anchorRight
Price Sourcesourcehlc3src
Volume WeightingstringPer-bar fallbackvolumeWeightMode
Show BandsbooltrueshowBands
Band DisplaystringZonesbandDisplay
Band ModestringVW StDevbandMode
Band 1 Multfloat1.0bandMult1
Band 2 Multfloat2.0bandMult2
Show all 40 settings
SettingTypeDefaultVariable
ATR Lengthint14atrLen
Enable TBF Exhaustion CurvebooltrueenableTBF
Exhaustion Warn (d/D)float0.85warnThresh
Minimum Fit Maturity (d/D)float0.20fitMinProgress
Stable Fit: max D changefloat0.25fitStableTolerance
TBF HistorystringFrom FittbfHistoryMode
Stale Fit after (d/D)float1.50tbfStaleProgress
Fit Swing Leftint5fitLeft
Fit Swing Rightint5fitRight
Trend BiasstringAuto EMAcurveBias
Auto Bias EMAint55emaLen
Auto EMA: min hold bars before flipint3biasHoldBars
Visual modestringActivevisualMode
Show context markersbooltrueshowMarkers
Reclaim: stretch lookbackint10reclaimLook
Reclaim: volume avg lengthint20reclaimVolLen
Reclaim: structure-break lookbackint5reclaimStructLen
Reclaim: minimum close-location ratiofloat0.6reclaimCloseLocationMin
Context markers only on bar closebooltruemarkersOnBarClose
Show live context badgebooltrueshowLiveContext
Live badge detailstringCompactliveBadgeMode
Show dashboardboolfalseshowTable
Dashboard detailstringCompactdashboardMode
Dashboard positionstringTop RighttablePos
Show active anchorbooltrueshowAnchor
Debug logboolfalsedbg
MIDAS curvecolor#ff6f00colMidas
TBF curvecolor#7b1fa2colTBF
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

VWAP Trader NXi

by eddiebelaval
7 stars

VWAP Trader NXi calculates a daily session-anchored Volume Weighted Average Price along with two custom standard deviation bands on the main price chart. It plots the core VWAP, Value Area bands, outer extension zones with background shading, and the previous

Indicator v5 VolumeVolatility & bands
Updated 3 months ago · None stated GitHub repository of VWAP Trader NXi

Snag - VWAP

by dearvn
257 stars

This indicator plots a custom Anchored Volume-Weighted Average Price (VWAP) directly on the price chart along with optional standard deviation bands. Users can anchor the VWAP to standard time periods like sessions, weeks, or months, or to corporate events suc

Indicator v5 Moving averagesVolatility & bands Repaints
Updated 7 weeks ago · None stated GitHub repository of Snag - VWAP

VWAP - Basic

by TradersPost
228 stars

VWAP - Basic plots an anchored Volume Weighted Average Price directly on the main price chart alongside two standard deviation bands. It calculates typical price weighted by volume, resetting the cumulative calculations at user-selected session, weekly, or mon

Indicator v6 VolumeSupport & resistance
Updated 3 months ago · None stated GitHub repository of VWAP - Basic

D/W/M VWAPs

by cryptorife
75 stars

This indicator plots daily, weekly, and monthly volume-weighted average prices on the price chart, using a selectable source that defaults to hlc3. For each enabled period it also draws two standard-deviation bands around the VWAP, with multipliers defaulting

Indicator v4 VolumeVolatility & bands
Updated 3 years ago · GPL-3.0 GitHub repository of D/W/M VWAPs
55 stars

This Pine Script indicator overlays institutional liquidity levels on the price chart. It identifies order block levels using pivot highs and lows, then draws mean reversion lines from the 20 and 50 period simple moving averages, a volatility range midpoint pr

Indicator v6 Support & resistanceSMC / ICT / price action Repaints
Updated 6 months ago · Apache-2.0 GitHub repository of AAPL Institutional Liquidity Grid

LoD Intraday Entry

by ChartsLector
41 stars

This overlay indicator marks the current day's low and a suggested entry zone a fraction of daily ATR below that low. It compares daily volume with a volume moving average, plots daily SMA and EMA distance, and draws an optional ATR-width price cloud. Labels c

Indicator v6 Support & resistanceVolatility & bands
Updated 4 days ago · None stated GitHub repository of LoD Intraday Entry

More Mean reversion scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers