OFS Pro: Order Flow Scalper

A Pine Script v6 indicator and matching strategy for crypto perps, gold and silver. Liquidity sweeps, absorption and trapped traders at ranked key-level zones, filtered by non-repainting HTF context, with full trade plans, a dashboard and a built-in backtest.

Pine v6UDTs + methods5 request callsConfirmed-bar signalsEstimated Delta/CVD

Download indicator Download strategy

00Prompt review

The prompt is strong: clear scope, honest about data limits, and it asks for self-review. These changes made it buildable in real Pine:

IssueWhat I did
ta.requestUpAndDownVolume() isn't needed alongside request.security_lower_tf()Both cost request calls and do the same job. I classify each lower-timeframe bar's volume directly (close vs open, ties by tick direction), which is what the library does internally, without the extra import.
'lookahead_off + confirmed HTF bars' is the slow way to be non-repaintingThe documented non-repainting idiom is expression[1] with lookahead_on: it uses only the last CLOSED HTF bar, identically on history and live, and switches one bar earlier than lookahead_off. Used for PDH/PDL/PW/PM and all HTF filters.
A 'second indicator-compatible pane' can't come from one overlay scriptPine scripts live in one pane. Fix: the indicator runs in its own pane (delta / CVD / equity) and draws every price object on the main chart with force_overlay = true. One script, both views.
Presets can't change input valuesPine inputs are immutable. A preset dropdown overrides the effective values in code (sweep size, reclaim bars, absorption z, min score, cooldown, mode, delta-flip); 'Custom' uses your own inputs. The tooltip says so.
Tables aren't resizable by drag'Resizable' becomes a text-size input (Tiny/Small/Normal/Large) plus a position input.
1s/5s/15s intrabars need a TradingView plan with seconds dataAuto never picks seconds. Manual choice is allowed and documented.
Grade C was undefinedAdded C (<55) so the score scale is complete, and backtest stats split by C too.
'Zero warnings' and 'compile' can't be promised from outside TradingViewThe code is syntax-checked with a Pine v6 parser and reviewed by hand; the first compile in your Pine Editor is the final check.
Missing: stop-width guard and a reason for skipped setupsAdded 'skip if stop wider than X ATR' and a dashboard row that names why the last setup was rejected (HTF, score, session, cooldown…).
Missing: strategy-vs-indicator caveatTradingView's broker emulator orders SL/TP inside a bar from the OHLC path, not 'stop first', so the two will differ slightly. Documented in both.

01Architecture & assumptions

  1. Delta engine. request.security_lower_tf() pulls intrabar open/close/volume (Auto: 1m on 2m–1h charts, 5m to 4h, 15m to 1D). Each intrabar's volume is signed by its direction. If no intrabars come back (1m chart, invalid TF, missing data) it falls back to the candle estimate volume × ((C−L) − (H−C)) / (H−L). CVD resets daily, per session or never; slope and divergence use an un-reset copy.
  2. Levels → zones. PD/PW/PM H/L, Asia/London/NY session H/L (last completed session), anchored VWAP ±1/±2σ, confirmed swing pivots, equal highs/lows, optional volume-profile POC/VAH/VAL. Levels within zone tolerance × ATR merge into zones; each zone carries a confluence score (PM 5, PW 4, PD/EQ 3, session/VWAP 2, pivots/bands 1) plus a touch count.
  3. Sweep state machine (UDT Sweep): wick beyond a zone by ≥ min ATR from a close inside → reclaim within N bars → confirmation candle with body in the signal direction, close beyond the reclaim level (or sweep-candle midpoint) and, optionally, delta flipped (trapped traders).
  4. Absorption (UDT AbsSetup): vol z ≥ X, |delta%| ≥ Y, range ≤ Z × ATR, close in the favourable part of the bar, at a zone or during a sweep. It boosts a sweep's score, and on its own it can arm an absorption-reversal signal confirmed by a close beyond the bar.
  5. HTF context: one request.security call returns EMA-stack trend, market structure, VWAP side, candle-CVD slope, range high/low, prior HTF bar H/L and ATR, all from closed HTF bars.
  6. Score 0–100: weighted level, sweep, absorption, divergence, HTF, session, volatility and R:R-to-liquidity factors, normalised by the weight sum, so any weights work.
  7. Filters (first failure is shown as the skip reason): stop width, HTF mode, HTF level in the way, session, no-trade window, volatility regime, min volume, cooldown, per-session cap, min score.
  8. Plan: market or 50% limit entry, stop beyond the sweep/absorption extreme + ATR buffer, TP1–3 as R multiples or next opposing zones, size from account × risk% ÷ (stop distance × point value), capped by leverage.
  9. Backtest (UDT Trade): one position at a time, partials, BE, ATR/structure trail, time stop, invalidation, commission and slippage, stop-first rule. Stats split by side, grade and session; equity in R in the lower pane.
  10. Request budget: 5 calls (lower-TF, D, W, M, HTF), far below the limit.

Assumptions

02Indicator

Pine Editor → new indicator → paste → Save → Add to chart. Opens in its own pane; price visuals land on the main chart.

ofs_pro_indicator.pine · 1438 linesDownload
//@version=6
// ─────────────────────────────────────────────────────────────────────────────
// OFS Pro: Order Flow Scalper  ·  INDICATOR
// Liquidity sweeps, absorption and trapped traders at key levels, filtered by
// non-repainting HTF context, with full trade plans and a built-in backtest.
// Delta/CVD are ESTIMATES (lower-timeframe or candle based), not true order flow.
// Runs in its own pane (estimated delta / CVD / equity); all price visuals are
// drawn on the main chart with force_overlay.
// ─────────────────────────────────────────────────────────────────────────────
indicator("OFS Pro: Order Flow Scalper", shorttitle = "OFS Pro", overlay = false, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 1000)
// ═══════════════════════════════════════════════════════════════════════════════
// OFS Pro: Order Flow Scalper  |  shared core (identical in indicator + strategy)
// All "order flow" values are ESTIMATES built from OHLCV / lower-timeframe bars.
// Pine Script has no access to the order book or a real footprint.
// ═══════════════════════════════════════════════════════════════════════════════

// ─────────────────────────────────────────────────────────────────────────────
// 0. INPUT GROUPS
// ─────────────────────────────────────────────────────────────────────────────
const string G_PRE = "00 · Presets"
const string G_DEL = "01 · Delta engine"
const string G_LEV = "02 · Key levels"
const string G_SES = "03 · Sessions"
const string G_SWP = "04 · Sweeps & traps"
const string G_ABS = "05 · Absorption"
const string G_HTF = "06 · HTF confirmation"
const string G_SCO = "07 · Scoring"
const string G_FIL = "08 · Filters"
const string G_RSK = "09 · Entry, stop, targets"
const string G_MGT = "10 · Trade management"
const string G_SIZ = "11 · Position size"
const string G_VIS = "12 · Visuals"
const string G_DSH = "13 · Dashboard"
const string G_BT  = "14 · Backtest"
const string G_ALR = "15 · Alerts"

// ─────────────────────────────────────────────────────────────────────────────
// 1. INPUTS
// ─────────────────────────────────────────────────────────────────────────────
presetIn     = input.string("Custom", "Preset", options = ["Custom", "Scalp 1m-5m", "Intraday 15m", "Conservative", "Aggressive"], group = G_PRE, tooltip = "Any preset other than Custom OVERRIDES: min sweep (ATR), max reclaim bars, absorption z-score, min score, cooldown, signal mode and the delta-flip rule. Pick Custom to use your own values below.")

// Delta
ltfIn        = input.string("Auto", "Intrabar precision", options = ["Auto", "1S", "5S", "15S", "1"], group = G_DEL, tooltip = "Lower timeframe used to classify each intrabar's volume as buying or selling. Auto: 1m on 2m-1h charts, 5m up to 4h, 15m up to 1D. On 1m charts Auto uses the candle estimate. 1S/5S/15S need a TradingView plan with seconds data.")
cvdResetIn   = input.string("Daily", "CVD reset", options = ["None", "Daily", "Session"], group = G_DEL)
volZLen      = input.int(50, "Volume z-score length", minval = 5, group = G_DEL)
cvdSlopeLen  = input.int(10, "CVD slope length", minval = 2, group = G_DEL)
divLen       = input.int(20, "Divergence lookback (bars)", minval = 3, group = G_DEL, tooltip = "Price makes a new N-bar high/low while estimated CVD does not.")
divRecent    = input.int(5, "Divergence counts if within (bars)", minval = 1, group = G_DEL)
atrLen       = input.int(14, "ATR length", minval = 1, group = G_DEL, tooltip = "Every distance input in ATR uses this ATR, so settings auto-scale across symbols and timeframes.")

// Levels
usePD        = input.bool(true, "Previous day H/L", group = G_LEV, inline = "l1")
usePW        = input.bool(true, "Previous week H/L", group = G_LEV, inline = "l1")
usePM        = input.bool(true, "Previous month H/L", group = G_LEV, inline = "l2")
useSessLv    = input.bool(true, "Session H/L", group = G_LEV, inline = "l2")
useVwapLv    = input.bool(true, "Anchored VWAP ±SD", group = G_LEV, inline = "l3")
usePivots    = input.bool(true, "Swing pivots", group = G_LEV, inline = "l3")
useEq        = input.bool(true, "Equal highs / lows", group = G_LEV, inline = "l4")
useVP        = input.bool(false, "Volume POC/VAH/VAL (approx.)", group = G_LEV, inline = "l4")
vwapAnchorIn = input.string("Daily", "VWAP anchor", options = ["Daily", "Weekly", "Monthly"], group = G_LEV)
vwapSd1      = input.float(1.0, "VWAP band 1 (SD)", minval = 0.1, step = 0.1, group = G_LEV)
vwapSd2      = input.float(2.0, "VWAP band 2 (SD)", minval = 0.1, step = 0.1, group = G_LEV)
pivLen       = input.int(5, "Pivot strength (bars each side)", minval = 1, group = G_LEV, tooltip = "Pivots are confirmed after this many bars, so they never repaint.")
maxPivots    = input.int(6, "Pivots remembered (per side)", minval = 1, maxval = 20, group = G_LEV)
eqTolAtr     = input.float(0.10, "Equal H/L tolerance (ATR)", minval = 0.01, step = 0.01, group = G_LEV)
vpLen        = input.int(200, "Volume profile lookback", minval = 20, maxval = 500, group = G_LEV)
vpRows       = input.int(30, "Volume profile rows", minval = 10, maxval = 100, group = G_LEV)
vaPct        = input.float(70, "Value area %", minval = 50, maxval = 95, group = G_LEV)
zoneTolAtr   = input.float(0.25, "Merge levels into zones within (ATR)", minval = 0.0, step = 0.05, group = G_LEV)
levelRangeAtr= input.float(12, "Only use levels within (ATR) of price", minval = 1, group = G_LEV)

// Sessions
sessTz       = input.string("America/New_York", "Session timezone", group = G_SES, tooltip = "IANA name, e.g. America/New_York, Europe/London, Etc/UTC.")
asiaSess     = input.session("1900-0400", "Asia", group = G_SES)
lonSess      = input.session("0300-1200", "London", group = G_SES)
nySess       = input.session("0800-1700", "New York", group = G_SES)

// Sweeps & traps
minWickIn    = input.float(0.15, "Min sweep beyond level (ATR)", minval = 0.0, step = 0.01, group = G_SWP)
maxReclaimIn = input.int(3, "Max bars to reclaim", minval = 1, group = G_SWP)
maxConfirm   = input.int(3, "Max bars for confirmation after reclaim", minval = 1, group = G_SWP)
needFlipIn   = input.bool(true, "Require delta flip on confirmation", group = G_SWP, tooltip = "Trapped-trader rule: the confirmation candle's estimated delta must point in the signal direction.")
confRefIn    = input.string("Reclaim level", "Confirmation must close beyond", options = ["Reclaim level", "Sweep candle midpoint"], group = G_SWP)

// Absorption
useAbsSig    = input.bool(true, "Absorption-reversal signals", group = G_ABS, tooltip = "Also signal when absorption at a zone is followed by a confirmation candle, even without a sweep.")
absZIn       = input.float(2.0, "Volume z-score ≥", minval = 0.5, step = 0.1, group = G_ABS)
absDeltaPct  = input.float(25, "|Delta %| of bar volume ≥", minval = 1, maxval = 100, group = G_ABS)
absRangeAtr  = input.float(0.8, "Bar range ≤ (ATR)", minval = 0.1, step = 0.05, group = G_ABS, tooltip = "Effort without result: big volume, small range.")
absClv       = input.float(0.5, "Close location ≥ (0-1, favorable side)", minval = 0.0, maxval = 1.0, step = 0.05, group = G_ABS)
absTolAtr    = input.float(0.25, "Counts as 'at a level' within (ATR)", minval = 0.0, step = 0.05, group = G_ABS)
absConfBars  = input.int(2, "Bars to confirm absorption", minval = 1, group = G_ABS)

// HTF
htfModeIn    = input.string("Auto", "HTF selection", options = ["Auto", "Manual"], group = G_HTF, tooltip = "Auto: ≤1m→15m, ≤5m→1h, ≤1h→4h, ≤4h→1D, else 1W.")
htfManual    = input.timeframe("60", "Manual HTF", group = G_HTF)
htfTrendType = input.string("EMA stack", "HTF trend method", options = ["EMA stack", "Market structure", "Both must agree"], group = G_HTF)
useHtfTrend  = input.bool(true, "Filter: HTF trend", group = G_HTF)
useHtfVwap   = input.bool(true, "Filter: HTF VWAP side", group = G_HTF)
useHtfCvd    = input.bool(true, "Filter: HTF CVD slope (candle estimate)", group = G_HTF)
useHtfKey    = input.bool(true, "Filter: don't trade into HTF level", group = G_HTF)
htfKeyAtr    = input.float(1.0, "…HTF level too close if within (ATR)", minval = 0.1, step = 0.1, group = G_HTF)
htfEmaFast   = input.int(20, "HTF EMA fast", minval = 2, group = G_HTF)
htfEmaSlow   = input.int(50, "HTF EMA slow", minval = 3, group = G_HTF)
htfStructLen = input.int(3, "HTF structure pivot strength", minval = 1, group = G_HTF)
htfRangeLen  = input.int(20, "HTF range lookback (bars)", minval = 5, group = G_HTF)
htfCvdLen    = input.int(5, "HTF CVD slope length", minval = 2, group = G_HTF)
modeIn       = input.string("Balanced", "Signal mode", options = ["Strict", "Balanced", "Aggressive"], group = G_HTF, tooltip = "Strict: every enabled HTF filter must agree. Balanced: no net HTF opposition. Aggressive: counter-trend allowed only near the HTF range extreme, with a score penalty.")
htfExtAtr    = input.float(1.5, "Aggressive: HTF extreme within (HTF ATR)", minval = 0.1, step = 0.1, group = G_HTF)
aggrPenalty  = input.float(0.85, "Aggressive: counter-trend score multiplier", minval = 0.1, maxval = 1.0, step = 0.05, group = G_HTF)

// Scoring
wLevel       = input.float(20, "Weight: level quality", minval = 0, group = G_SCO)
wSweep       = input.float(20, "Weight: sweep quality", minval = 0, group = G_SCO)
wAbs         = input.float(15, "Weight: absorption", minval = 0, group = G_SCO)
wDiv         = input.float(10, "Weight: delta divergence", minval = 0, group = G_SCO)
wHtf         = input.float(15, "Weight: HTF alignment", minval = 0, group = G_SCO)
wSess        = input.float(5, "Weight: session quality", minval = 0, group = G_SCO)
wVol         = input.float(5, "Weight: volatility regime", minval = 0, group = G_SCO)
wRR          = input.float(10, "Weight: R:R to next liquidity", minval = 0, group = G_SCO)
minScoreIn   = input.int(60, "Minimum score to show", minval = 0, maxval = 100, group = G_SCO, tooltip = "Grades: A+ ≥ 85, A 70-84, B 55-69, C < 55.")
touchLook    = input.int(100, "Zone touch lookback (bars)", minval = 1, maxval = 500, group = G_SCO)

// Filters
tradeAsia    = input.bool(true, "Asia", group = G_FIL, inline = "s")
tradeLon     = input.bool(true, "London", group = G_FIL, inline = "s")
tradeNy      = input.bool(true, "New York", group = G_FIL, inline = "s")
tradeOff     = input.bool(false, "Off-hours", group = G_FIL, inline = "s")
regimeLen    = input.int(100, "Volatility baseline length", minval = 10, group = G_FIL)
deadThr      = input.float(0.6, "Dead market if ATR/baseline <", minval = 0.0, step = 0.05, group = G_FIL)
chaosThr     = input.float(2.2, "Chaotic market if ATR/baseline >", minval = 1.0, step = 0.1, group = G_FIL)
useRegime    = input.bool(true, "Skip dead / chaotic markets", group = G_FIL)
minVolMult   = input.float(0.5, "Min volume (× average)", minval = 0.0, step = 0.1, group = G_FIL)
cooldownIn   = input.int(5, "Cooldown bars between signals", minval = 0, group = G_FIL)
maxPerSess   = input.int(4, "Max signals per session", minval = 1, group = G_FIL)
useNoTrade   = input.bool(false, "No-trade window", group = G_FIL, inline = "nt")
noTradeSess  = input.session("0825-0845", "", group = G_FIL, inline = "nt", tooltip = "e.g. around scheduled news. Uses the session timezone.")

// Entry / stop / targets
entryTypeIn  = input.string("Market (confirmation close)", "Entry", options = ["Market (confirmation close)", "Limit (50% of confirmation candle)"], group = G_RSK)
limitExpiry  = input.int(3, "Limit order expiry (bars)", minval = 1, group = G_RSK)
stopBufAtr   = input.float(0.10, "Stop buffer beyond extreme (ATR)", minval = 0.0, step = 0.01, group = G_RSK)
maxStopAtr   = input.float(2.5, "Skip if stop wider than (ATR)", minval = 0.2, step = 0.1, group = G_RSK)
tpModeIn     = input.string("R multiples", "Targets", options = ["R multiples", "Liquidity (auto)"], group = G_RSK, tooltip = "Liquidity: TP1-3 at the next opposing zones (at least 0.5R away). Missing targets fall back to R multiples.")
r1           = input.float(1.0, "TP1 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")
r2           = input.float(2.0, "TP2 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")
r3           = input.float(3.0, "TP3 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")

// Management
tp1Pct       = input.float(50, "Close % at TP1", minval = 0, maxval = 100, group = G_MGT)
tp2Pct       = input.float(30, "Close % at TP2", minval = 0, maxval = 100, group = G_MGT, tooltip = "Remainder closes at TP3.")
moveBE       = input.bool(true, "Move stop to breakeven after TP1", group = G_MGT)
trailModeIn  = input.string("None", "Trailing stop", options = ["None", "ATR", "Structure"], group = G_MGT)
trailAfterTp1= input.bool(true, "Trail only after TP1", group = G_MGT)
trailAtr     = input.float(1.5, "ATR trail distance", minval = 0.1, step = 0.1, group = G_MGT)
trailLen     = input.int(5, "Structure trail lookback (bars)", minval = 1, group = G_MGT)
useTimeStop  = input.bool(true, "Time stop", group = G_MGT, inline = "ts")
timeStopBars = input.int(30, "after bars", minval = 1, group = G_MGT, inline = "ts")
useInval     = input.bool(true, "Exit if signal level is lost (close beyond)", group = G_MGT)

// Position size
acctSize     = input.float(10000, "Account size", minval = 1, group = G_SIZ)
riskPct      = input.float(0.5, "Risk per trade %", minval = 0.01, step = 0.05, group = G_SIZ)
pointValIn   = input.float(0, "Value per point per contract (0 = symbol's)", minval = 0, group = G_SIZ, tooltip = "0 uses syminfo.pointvalue (e.g. GC1! = 100, SI1! = 5000, crypto = 1).")
maxLev       = input.float(10, "Leverage cap (×)", minval = 0.1, step = 0.5, group = G_SIZ)
qtyStep      = input.float(0.001, "Quantity step", minval = 0.000001, group = G_SIZ)

// ─────────────────────────────────────────────────────────────────────────────
// 2. PRESETS → effective values
// ─────────────────────────────────────────────────────────────────────────────
bool   isCustom   = presetIn == "Custom"
float  eMinWick   = isCustom ? minWickIn  : presetIn == "Scalp 1m-5m" ? 0.12 : presetIn == "Intraday 15m" ? 0.15 : presetIn == "Conservative" ? 0.20 : 0.08
int    eMaxReclaim= isCustom ? maxReclaimIn : presetIn == "Scalp 1m-5m" ? 3 : presetIn == "Intraday 15m" ? 2 : presetIn == "Conservative" ? 2 : 4
float  eAbsZ      = isCustom ? absZIn     : presetIn == "Scalp 1m-5m" ? 2.0 : presetIn == "Intraday 15m" ? 1.8 : presetIn == "Conservative" ? 2.5 : 1.5
int    eMinScore  = isCustom ? minScoreIn : presetIn == "Scalp 1m-5m" ? 60 : presetIn == "Intraday 15m" ? 65 : presetIn == "Conservative" ? 75 : 50
int    eCooldown  = isCustom ? cooldownIn : presetIn == "Scalp 1m-5m" ? 5 : presetIn == "Intraday 15m" ? 3 : presetIn == "Conservative" ? 8 : 2
string eMode      = isCustom ? modeIn     : presetIn == "Conservative" ? "Strict" : presetIn == "Aggressive" ? "Aggressive" : "Balanced"
bool   eNeedFlip  = isCustom ? needFlipIn : presetIn != "Aggressive"

// ─────────────────────────────────────────────────────────────────────────────
// 3. TYPES
// ─────────────────────────────────────────────────────────────────────────────
// A single key level.
type Level
    float  price
    string name
    int    weight

// Nearby levels merged into one zone.
type Zone
    float  top
    float  bot
    int    count
    int    score
    string names

// Running high/low for a trading session.
type SessTrack
    float hi     = na
    float lo     = na
    float lastHi = na
    float lastLo = na
    bool  active = false

// A liquidity sweep tracked until confirmation, failure or expiry.
type Sweep
    bool   active     = false
    bool   reclaimed  = false
    bool   absorbed   = false
    float  level      = na
    float  extreme    = na
    float  mid        = na
    int    startBar   = na
    int    reclaimBar = na
    int    zScore     = 0
    int    zCount     = 0
    string zName      = ""
    float  zTop       = na
    float  zBot       = na
    float  depthAtr   = 0.0
    float  sweepDelta = 0.0

// An absorption bar waiting for a confirmation candle.
type AbsSetup
    bool   active   = false
    int    startBar = na
    float  hi       = na
    float  lo       = na
    float  level    = na
    float  volZ     = 0.0
    int    zScore   = 0
    int    zCount   = 0
    string zName    = ""
    float  zTop     = na
    float  zBot     = na

// ─────────────────────────────────────────────────────────────────────────────
// 4. HELPERS
// ─────────────────────────────────────────────────────────────────────────────
f_clamp01(float x) => na(x) ? 0.0 : math.max(0.0, math.min(1.0, x))

f_safeDiv(float a, float b, float dflt) => na(a) or na(b) or b == 0 ? dflt : a / b

f_grade(float s) => s >= 85 ? "A+" : s >= 70 ? "A" : s >= 55 ? "B" : "C"

f_px(float p) => na(p) ? "—" : str.tostring(p, format.mintick)

// Intrabar delta: each lower-timeframe bar's volume is signed by its own direction
// (close vs open); dojis use the change from the previous intrabar close.
f_intrabarDelta(array<float> o, array<float> c, array<float> v) =>
    float d = 0.0
    int   n = na(c) ? 0 : c.size()
    float prevC = na
    if n > 0
        for i = 0 to n - 1
            float cc = c.get(i)
            float oo = o.get(i)
            float vv = nz(v.get(i))
            float sgn = cc > oo ? 1.0 : cc < oo ? -1.0 : na(prevC) ? 0.0 : cc > prevC ? 1.0 : cc < prevC ? -1.0 : 0.0
            d += sgn * vv
            prevC := cc
    [d, n]

// Adds a level when enabled, valid and close enough to price to matter.
f_addLevel(array<Level> arr, float p, string nm, int w, bool on, float ref, float maxDist) =>
    bool ok = on and not na(p) and math.abs(p - ref) <= maxDist
    if ok
        arr.push(Level.new(p, nm, w))
    ok

// Sorts levels by price and merges any within `tol` into zones.
f_buildZones(array<Level> lv, float tol) =>
    array<Zone> zs = array.new<Zone>()
    int n = lv.size()
    if n > 0
        array<float> px = array.new_float()
        for l in lv
            px.push(l.price)
        array<int> idx = px.sort_indices(order.ascending)
        Zone cur = na
        for i in idx
            Level lvl = lv.get(i)
            if na(cur)
                cur := Zone.new(lvl.price, lvl.price, 1, lvl.weight, lvl.name)
            else if lvl.price - cur.top <= tol
                cur.top   := math.max(cur.top, lvl.price)
                cur.count := cur.count + 1
                cur.score := cur.score + lvl.weight
                cur.names := cur.names + " · " + lvl.name
            else
                zs.push(cur)
                cur := Zone.new(lvl.price, lvl.price, 1, lvl.weight, lvl.name)
        if not na(cur)
            zs.push(cur)
    zs

// Best-scoring zone swept on this bar. dir = 1 sweep of lows (long setup), -1 sweep of highs.
f_findSweep(array<Zone> zs, int dir, float minPen, float prevClose) =>
    int best = -1
    int bestScore = -1
    int n = zs.size()
    if n > 0 and not na(prevClose)
        for i = 0 to n - 1
            Zone z = zs.get(i)
            bool hit = dir == 1 ? low <= z.bot - minPen and prevClose > z.bot : high >= z.top + minPen and prevClose < z.top
            if hit and z.score > bestScore
                best := i
                bestScore := z.score
    best

// Zone the current bar is trading at. dir = 1 support, -1 resistance.
f_touchZone(array<Zone> zs, int dir, float tol) =>
    int best = -1
    int bestScore = -1
    int n = zs.size()
    if n > 0
        for i = 0 to n - 1
            Zone z = zs.get(i)
            bool at = dir == 1 ? low <= z.top + tol and high >= z.bot - tol and close >= z.bot : high >= z.bot - tol and low <= z.top + tol and close <= z.top
            if at and z.score > bestScore
                best := i
                bestScore := z.score
    best

// Nearest opposing zone edge at least `minDist` beyond `ref` in direction `dir`.
f_nearestBeyond(array<Zone> zs, int dir, float ref, float minDist) =>
    float best = na
    for z in zs
        float p = dir == 1 ? z.bot : z.top
        if dir * (p - ref) >= minDist
            if na(best) or dir * (p - best) < 0
                best := p
    best

// TP1-3: R multiples or the next opposing liquidity zones, always monotonic.
f_targets(array<Zone> zs, int dir, float entry, float rsk, bool liq) =>
    float t1 = entry + dir * r1 * rsk
    float t2 = entry + dir * r2 * rsk
    float t3 = entry + dir * r3 * rsk
    if liq
        array<float> c = array.new_float()
        for z in zs
            float p = dir == 1 ? z.bot : z.top
            if dir * (p - entry) >= 0.5 * rsk
                c.push(p)
        c.sort(dir == 1 ? order.ascending : order.descending)
        int k = c.size()
        if k > 0
            t1 := c.get(0)
        if k > 1
            t2 := c.get(1)
        if k > 2
            t3 := c.get(2)
    if dir * (t2 - t1) <= 0
        t2 := t1 + dir * math.max(r2 - r1, 0.5) * rsk
    if dir * (t3 - t2) <= 0
        t3 := t2 + dir * math.max(r3 - r2, 0.5) * rsk
    [t1, t2, t3]

// HTF alignment: 0 = fully against, 0.5 = neutral, 1 = fully aligned.
f_alignment(int dir, int t, int v, int c) =>
    int n = 0
    int agree = 0
    int against = 0
    if useHtfTrend
        n += 1
        agree += t == dir ? 1 : 0
        against += t == -dir ? 1 : 0
    if useHtfVwap
        n += 1
        agree += v == dir ? 1 : 0
        against += v == -dir ? 1 : 0
    if useHtfCvd
        n += 1
        agree += c == dir ? 1 : 0
        against += c == -dir ? 1 : 0
    float sc = n > 0 ? (agree - against + n) / (2.0 * n) : 0.5
    [sc, agree, against, n]

// Evaluated INSIDE request.security on the HTF. Every value is shifted [1] and the
// call uses lookahead_on: TradingView's documented non-repainting pattern, so only
// fully closed HTF bars are ever used, identically on history and in realtime.
f_htf(int emaF, int emaS, int structLen, int rangeLen, int cvdLen) =>
    float eF = ta.ema(close, emaF)
    float eS = ta.ema(close, emaS)
    int   emaTrend = close > eF and eF > eS ? 1 : close < eF and eF < eS ? -1 : 0
    float phH = ta.pivothigh(high, structLen, structLen)
    float plH = ta.pivotlow(low, structLen, structLen)
    float lastPH = ta.valuewhen(not na(phH), phH, 0)
    float prevPH = ta.valuewhen(not na(phH), phH, 1)
    float lastPL = ta.valuewhen(not na(plH), plH, 0)
    float prevPL = ta.valuewhen(not na(plH), plH, 1)
    int   structTrend = lastPH > prevPH and lastPL > prevPL ? 1 : lastPH < prevPH and lastPL < prevPL ? -1 : 0
    bool  vwAnchor = timeframe.change(timeframe.isintraday ? "D" : "M")
    float vw = ta.vwap(hlc3, vwAnchor)
    int   vwapSide = close > vw ? 1 : close < vw ? -1 : 0
    float r = high - low
    float cd = r > 0 ? nz(volume) * ((close - low) - (high - close)) / r : 0.0
    float cvdH = ta.cum(cd)
    int   cvdSl = cvdH > cvdH[cvdLen] ? 1 : cvdH < cvdH[cvdLen] ? -1 : 0
    float rHi = ta.highest(high, rangeLen)
    float rLo = ta.lowest(low, rangeLen)
    float hAtrV = ta.atr(14)
    [emaTrend[1], structTrend[1], vwapSide[1], cvdSl[1], rHi[1], rLo[1], high[1], low[1], hAtrV[1]]

// Sweep lifecycle. step() runs once per confirmed bar AFTER the sweep bar and
// returns true when the trapped-trader confirmation candle prints.
method resetSweep(Sweep s) =>
    s.active     := false
    s.reclaimed  := false
    s.absorbed   := false
    s.sweepDelta := 0.0
    s

method step(Sweep s, int dir, float d, bool absHere, int maxReclaim, int maxConf, bool needFlip, bool useMid) =>
    bool fired = false
    if s.active and bar_index > s.startBar
        s.extreme := dir == 1 ? math.min(s.extreme, low) : math.max(s.extreme, high)
        if absHere
            s.absorbed := true
        bool inside = dir == 1 ? close > s.level : close < s.level
        if not s.reclaimed
            s.sweepDelta := s.sweepDelta + d
            if inside
                s.reclaimed  := true
                s.reclaimBar := bar_index
            else if bar_index - s.startBar >= maxReclaim
                s.resetSweep()
        else if not inside
            // Lost the reclaim: back to pending, still bounded by the reclaim window.
            s.reclaimed := false
            s.sweepDelta := s.sweepDelta + d
            if bar_index - s.startBar >= maxReclaim
                s.resetSweep()
        if s.active and s.reclaimed
            float ref    = useMid ? (dir == 1 ? math.max(s.level, s.mid) : math.min(s.level, s.mid)) : s.level
            bool  beyond = dir == 1 ? close > ref : close < ref
            bool  body   = dir == 1 ? close > open : close < open
            bool  flip   = not needFlip or (dir == 1 ? d > 0 : d < 0)
            if beyond and body and flip
                fired := true
            else if bar_index - s.reclaimBar >= maxConf
                s.resetSweep()
    fired

method update(SessTrack s, bool inS) =>
    if inS and not s.active
        s.hi := high
        s.lo := low
        s.active := true
    else if inS
        s.hi := math.max(s.hi, high)
        s.lo := math.min(s.lo, low)
    else if s.active
        s.lastHi := s.hi
        s.lastLo := s.lo
        s.active := false
    s

// ─────────────────────────────────────────────────────────────────────────────
// 5. CORE SERIES (computed on every bar, unconditionally)
// ─────────────────────────────────────────────────────────────────────────────
bool  barConf = barstate.isconfirmed
int   tfSec   = timeframe.in_seconds()
float atrRaw  = ta.atr(atrLen)
float atrS    = math.max(nz(atrRaw, high - low), syminfo.mintick)
float rng     = high - low
float clv     = rng > 0 ? (close - low) / rng : 0.5
bool  newDay  = timeframe.change("D")
bool  newWeek = timeframe.change("W")
bool  newMonth= timeframe.change("M")
float lowN    = ta.lowest(low, trailLen)
float highN   = ta.highest(high, trailLen)

// ─────────────────────────────────────────────────────────────────────────────
// 6. SESSIONS
// ─────────────────────────────────────────────────────────────────────────────
bool inAsia = not na(time(timeframe.period, asiaSess + ":1234567", sessTz))
bool inLon  = not na(time(timeframe.period, lonSess + ":1234567", sessTz))
bool inNy   = not na(time(timeframe.period, nySess + ":1234567", sessTz))
bool inNoTr = useNoTrade and not na(time(timeframe.period, noTradeSess + ":1234567", sessTz))
string sessName  = inLon and inNy ? "London/NY overlap" : inNy ? "New York" : inLon ? "London" : inAsia ? "Asia" : "Off-hours"
int    sessId    = inNy ? 2 : inLon ? 1 : inAsia ? 0 : 3   // stats bucket (overlap counts as NY)
bool   sessChange= sessName != sessName[1]
float  sessQ     = inLon and inNy ? 1.0 : inNy or inLon ? 0.8 : inAsia ? 0.5 : 0.25
bool   sessOk    = (inAsia and tradeAsia) or (inLon and tradeLon) or (inNy and tradeNy) or (not inAsia and not inLon and not inNy and tradeOff)

var SessTrack trAsia = SessTrack.new()
var SessTrack trLon  = SessTrack.new()
var SessTrack trNy   = SessTrack.new()
if barConf
    trAsia.update(inAsia)
    trLon.update(inLon)
    trNy.update(inNy)

// ─────────────────────────────────────────────────────────────────────────────
// 7. DELTA ENGINE (estimated)
// ─────────────────────────────────────────────────────────────────────────────
string ltfAuto = tfSec <= 60 ? "" : tfSec <= 3600 ? "1" : tfSec <= 14400 ? "5" : tfSec <= 86400 ? "15" : "60"
string ltfStr  = ltfIn == "Auto" ? ltfAuto : ltfIn
string ltfReq  = ltfStr == "" ? "1" : ltfStr
bool   useLtf  = ltfStr != "" and timeframe.in_seconds(ltfReq) < tfSec
[ltfO, ltfC, ltfV] = request.security_lower_tf(syminfo.tickerid, ltfReq, [open, close, volume], ignore_invalid_timeframe = true)
[ibDelta, ibCount] = f_intrabarDelta(ltfO, ltfC, ltfV)

float  candleDelta = rng > 0 ? nz(volume) * ((close - low) - (high - close)) / rng : 0.0
bool   ltfOk    = useLtf and ibCount > 1
float  delta    = ltfOk ? ibDelta : candleDelta
string deltaSrc = ltfOk ? "Intrabar " + ltfStr + (str.contains(ltfStr, "S") ? "" : "m") : "Candle estimate"

var float cumVol = 0.0
cumVol += nz(volume)
bool  noVolData = cumVol == 0.0

float deltaPct = nz(volume) > 0 ? 100.0 * delta / volume : 0.0
float vMean    = ta.sma(nz(volume), volZLen)
float vStd     = ta.stdev(nz(volume), volZLen)
float volZ     = vStd > 0 ? (nz(volume) - vMean) / vStd : 0.0

bool  cvdResetNow = cvdResetIn == "Daily" ? newDay : cvdResetIn == "Session" ? sessChange : false
var float cvd = 0.0
if cvdResetNow
    cvd := 0.0
cvd += delta
float cvdRaw   = ta.cum(delta)                      // never reset: used for slope/divergence
float cvdPast  = cvdRaw[cvdSlopeLen]
float cvdSlope = na(cvdPast) ? 0.0 : (cvdRaw - cvdPast) / cvdSlopeLen
int   cvdDir   = cvdSlope > 0 ? 1 : cvdSlope < 0 ? -1 : 0

float loPrev    = ta.lowest(low, divLen)[1]
float hiPrev    = ta.highest(high, divLen)[1]
float cvdLoPrev = ta.lowest(cvdRaw, divLen)[1]
float cvdHiPrev = ta.highest(cvdRaw, divLen)[1]
bool  bullDiv   = low < loPrev and cvdRaw > cvdLoPrev
bool  bearDiv   = high > hiPrev and cvdRaw < cvdHiPrev
int   bsBullDiv = nz(ta.barssince(bullDiv), 9999)
int   bsBearDiv = nz(ta.barssince(bearDiv), 9999)

// ─────────────────────────────────────────────────────────────────────────────
// 8. KEY LEVELS
// ─────────────────────────────────────────────────────────────────────────────
[pdh, pdl] = request.security(syminfo.tickerid, "D", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwh, pwl] = request.security(syminfo.tickerid, "W", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pmh, pml] = request.security(syminfo.tickerid, "M", [high[1], low[1]], lookahead = barmerge.lookahead_on)

bool vwapReset = vwapAnchorIn == "Weekly" ? newWeek : vwapAnchorIn == "Monthly" ? newMonth : newDay
[vwapV, vwU1, vwL1]  = ta.vwap(hlc3, vwapReset, vwapSd1)
[vwapV2, vwU2, vwL2] = ta.vwap(hlc3, vwapReset, vwapSd2)

// Swing pivots + equal highs/lows (resting liquidity pools)
float ph = ta.pivothigh(high, pivLen, pivLen)
float pl = ta.pivotlow(low, pivLen, pivLen)
var array<float> pivH = array.new_float()
var array<float> pivL = array.new_float()
var float eqhLevel = na
var float eqlLevel = na
if barConf
    if not na(ph)
        for p in pivH
            if math.abs(p - ph) <= eqTolAtr * atrS
                eqhLevel := math.max(p, ph)
        pivH.unshift(ph)
        if pivH.size() > maxPivots
            pivH.pop()
    if not na(pl)
        for p in pivL
            if math.abs(p - pl) <= eqTolAtr * atrS
                eqlLevel := math.min(p, pl)
        pivL.unshift(pl)
        if pivL.size() > maxPivots
            pivL.pop()
    // An equal high/low that price has accepted beyond (close past it) is spent.
    if not na(eqhLevel) and close > eqhLevel + eqTolAtr * atrS
        eqhLevel := na
    if not na(eqlLevel) and close < eqlLevel - eqTolAtr * atrS
        eqlLevel := na

// Volume profile approximation (each bar's volume binned at its HLC3), every 10 bars.
var float vpPoc = na
var float vpVah = na
var float vpVal = na
float vpHi = ta.highest(high, vpLen)
float vpLo = ta.lowest(low, vpLen)
if useVP and barConf and bar_index >= vpLen and bar_index % 10 == 0
    float rowH = (vpHi - vpLo) / vpRows
    if rowH > 0
        array<float> bins = array.new_float(vpRows, 0.0)
        for i = 0 to vpLen - 1
            int b = math.min(vpRows - 1, math.max(0, int(math.floor((hlc3[i] - vpLo) / rowH))))
            bins.set(b, bins.get(b) + nz(volume[i]))
        float total = bins.sum()
        if total > 0
            int   pocI = bins.indexof(bins.max())
            float acc  = bins.get(pocI)
            int   up   = pocI
            int   dn   = pocI
            while acc < total * vaPct / 100.0 and (up < vpRows - 1 or dn > 0)
                float vUp = up < vpRows - 1 ? bins.get(up + 1) : -1.0
                float vDn = dn > 0 ? bins.get(dn - 1) : -1.0
                if vUp >= vDn
                    up  += 1
                    acc += vUp
                else
                    dn  -= 1
                    acc += vDn
            vpPoc := vpLo + (pocI + 0.5) * rowH
            vpVah := vpLo + (up + 1) * rowH
            vpVal := vpLo + dn * rowH

// Assemble this bar's level set and merge into ranked zones.
var array<Level> levels = array.new<Level>()
levels.clear()
float maxDist = levelRangeAtr * atrS
f_addLevel(levels, pmh, "PMH", 5, usePM, close, maxDist)
f_addLevel(levels, pml, "PML", 5, usePM, close, maxDist)
f_addLevel(levels, pwh, "PWH", 4, usePW, close, maxDist)
f_addLevel(levels, pwl, "PWL", 4, usePW, close, maxDist)
f_addLevel(levels, pdh, "PDH", 3, usePD, close, maxDist)
f_addLevel(levels, pdl, "PDL", 3, usePD, close, maxDist)
f_addLevel(levels, eqhLevel, "EQH", 3, useEq, close, maxDist)
f_addLevel(levels, eqlLevel, "EQL", 3, useEq, close, maxDist)
f_addLevel(levels, trAsia.lastHi, "Asia H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trAsia.lastLo, "Asia L", 2, useSessLv, close, maxDist)
f_addLevel(levels, trLon.lastHi, "LDN H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trLon.lastLo, "LDN L", 2, useSessLv, close, maxDist)
f_addLevel(levels, trNy.lastHi, "NY H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trNy.lastLo, "NY L", 2, useSessLv, close, maxDist)
f_addLevel(levels, vwapV, "VWAP", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vwU1, "VWAP+1σ", 1, useVwapLv, close, maxDist)
f_addLevel(levels, vwL1, "VWAP-1σ", 1, useVwapLv, close, maxDist)
f_addLevel(levels, vwU2, "VWAP+2σ", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vwL2, "VWAP-2σ", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vpPoc, "POC", 2, useVP, close, maxDist)
f_addLevel(levels, vpVah, "VAH", 1, useVP, close, maxDist)
f_addLevel(levels, vpVal, "VAL", 1, useVP, close, maxDist)
if usePivots
    for p in pivH
        f_addLevel(levels, p, "Swing H", 1, true, close, maxDist)
    for p in pivL
        f_addLevel(levels, p, "Swing L", 1, true, close, maxDist)
array<Zone> zones = f_buildZones(levels, zoneTolAtr * atrS)

// ─────────────────────────────────────────────────────────────────────────────
// 9. HTF CONTEXT (non-repainting)
// ─────────────────────────────────────────────────────────────────────────────
string htfAuto  = tfSec <= 60 ? "15" : tfSec <= 300 ? "60" : tfSec <= 3600 ? "240" : tfSec <= 14400 ? "D" : "W"
string htf      = htfModeIn == "Auto" ? htfAuto : htfManual
bool   htfValid = timeframe.in_seconds(htf) > tfSec
[hEma, hStruct, hVwap, hCvd, hRangeHi, hRangeLo, hPrevHi, hPrevLo, hAtr] = request.security(syminfo.tickerid, htf, f_htf(htfEmaFast, htfEmaSlow, htfStructLen, htfRangeLen, htfCvdLen), lookahead = barmerge.lookahead_on)
int htfTrend = htfTrendType == "EMA stack" ? hEma : htfTrendType == "Market structure" ? hStruct : (hEma == hStruct ? hEma : 0)

// ─────────────────────────────────────────────────────────────────────────────
// 10. VOLATILITY REGIME + VOLUME
// ─────────────────────────────────────────────────────────────────────────────
float  atrBase   = ta.sma(atrS, regimeLen)
float  atrRatio  = f_safeDiv(atrS, atrBase, 1.0)
string volRegime = atrRatio < deadThr ? "Dead" : atrRatio > chaosThr ? "Chaotic" : "Normal"
float  volQ      = volRegime == "Normal" ? 1.0 : volRegime == "Chaotic" ? 0.5 : 0.3
bool   regimeOk  = not useRegime or volRegime == "Normal"
bool   volOk     = noVolData or nz(volume) >= minVolMult * nz(vMean)

// ─────────────────────────────────────────────────────────────────────────────
// 11. ABSORPTION
// ─────────────────────────────────────────────────────────────────────────────
var Sweep swL = Sweep.new()    // sweep of lows  → long setups
var Sweep swS = Sweep.new()    // sweep of highs → short setups
bool absBase    = not noVolData and volZ >= eAbsZ and math.abs(deltaPct) >= absDeltaPct and rng <= absRangeAtr * atrS
bool absBullRaw = absBase and delta < 0 and clv >= absClv          // heavy selling, price held
bool absBearRaw = absBase and delta > 0 and clv <= 1.0 - absClv    // heavy buying, price held
int  absZoneL   = f_touchZone(zones, 1, absTolAtr * atrS)
int  absZoneS   = f_touchZone(zones, -1, absTolAtr * atrS)
bool absBull    = absBullRaw and (absZoneL >= 0 or swL.active)
bool absBear    = absBearRaw and (absZoneS >= 0 or swS.active)
bool absBullC   = absBull and barConf
bool absBearC   = absBear and barConf

var AbsSetup abL = AbsSetup.new()
var AbsSetup abS = AbsSetup.new()
bool absSigL = false
bool absSigS = false
if barConf and useAbsSig
    // confirm, fail or expire existing absorption setups
    if abL.active and bar_index > abL.startBar
        if close < abL.lo
            abL.active := false
        else if close > abL.hi and delta > 0
            absSigL := true
        else if bar_index - abL.startBar >= absConfBars
            abL.active := false
    if abS.active and bar_index > abS.startBar
        if close > abS.hi
            abS.active := false
        else if close < abS.lo and delta < 0
            absSigS := true
        else if bar_index - abS.startBar >= absConfBars
            abS.active := false
    // arm new setups at a zone
    if absBull and absZoneL >= 0 and not abL.active
        Zone z = zones.get(absZoneL)
        abL.active   := true
        abL.startBar := bar_index
        abL.hi       := high
        abL.lo       := low
        abL.level    := z.bot
        abL.volZ     := volZ
        abL.zScore   := z.score
        abL.zCount   := z.count
        abL.zName    := z.names
        abL.zTop     := z.top
        abL.zBot     := z.bot
    if absBear and absZoneS >= 0 and not abS.active
        Zone z = zones.get(absZoneS)
        abS.active   := true
        abS.startBar := bar_index
        abS.hi       := high
        abS.lo       := low
        abS.level    := z.top
        abS.volZ     := volZ
        abS.zScore   := z.score
        abS.zCount   := z.count
        abS.zName    := z.names
        abS.zTop     := z.top
        abS.zBot     := z.bot

// ─────────────────────────────────────────────────────────────────────────────
// 12. SWEEPS + TRAPPED TRADERS
// ─────────────────────────────────────────────────────────────────────────────
bool useMid = confRefIn == "Sweep candle midpoint"
bool trapL = false
bool trapS = false
if barConf
    trapL := swL.step(1, delta, absBull, eMaxReclaim, maxConfirm, eNeedFlip, useMid)
    trapS := swS.step(-1, delta, absBear, eMaxReclaim, maxConfirm, eNeedFlip, useMid)

int  swIdxL = f_findSweep(zones, 1, eMinWick * atrS, close[1])
int  swIdxS = f_findSweep(zones, -1, eMinWick * atrS, close[1])
bool newBullSweep = false
bool newBearSweep = false
if barConf and swIdxL >= 0 and not swL.active and not trapL
    Zone z = zones.get(swIdxL)
    swL.active     := true
    swL.reclaimed  := close > z.bot
    swL.reclaimBar := close > z.bot ? bar_index : int(na)
    swL.absorbed   := absBullRaw
    swL.level      := z.bot
    swL.extreme    := low
    swL.mid        := (high + low) / 2
    swL.startBar   := bar_index
    swL.zScore     := z.score
    swL.zCount     := z.count
    swL.zName      := z.names
    swL.zTop       := z.top
    swL.zBot       := z.bot
    swL.depthAtr   := (z.bot - low) / atrS
    swL.sweepDelta := delta
    newBullSweep   := true
if barConf and swIdxS >= 0 and not swS.active and not trapS
    Zone z = zones.get(swIdxS)
    swS.active     := true
    swS.reclaimed  := close < z.top
    swS.reclaimBar := close < z.top ? bar_index : int(na)
    swS.absorbed   := absBearRaw
    swS.level      := z.top
    swS.extreme    := high
    swS.mid        := (high + low) / 2
    swS.startBar   := bar_index
    swS.zScore     := z.score
    swS.zCount     := z.count
    swS.zName      := z.names
    swS.zTop       := z.top
    swS.zBot       := z.bot
    swS.depthAtr   := (high - z.top) / atrS
    swS.sweepDelta := delta
    newBearSweep   := true

// ─────────────────────────────────────────────────────────────────────────────
// 13. CANDIDATE SETUP (trap preferred over absorption-only; long checked first)
// ─────────────────────────────────────────────────────────────────────────────
int    cDir = 0
string cType = ""
float  cLevel = na
float  cExtreme = na
int    cZScore = 0
int    cZCount = 0
string cZName = ""
float  cZTop = na
float  cZBot = na
float  cDepth = 0.0
bool   cAbs = false
float  cAbsZ = 0.0
int    cReclaimBars = 0
float  cSweepDelta = 0.0
if trapL
    cDir := 1
    cType := "Sweep + trap"
    cLevel := swL.level
    cExtreme := swL.extreme
    cZScore := swL.zScore
    cZCount := swL.zCount
    cZName := swL.zName
    cZTop := swL.zTop
    cZBot := swL.zBot
    cDepth := swL.depthAtr
    cAbs := swL.absorbed
    cReclaimBars := nz(swL.reclaimBar, bar_index) - swL.startBar
    cSweepDelta := swL.sweepDelta
else if absSigL
    cDir := 1
    cType := "Absorption"
    cLevel := abL.level
    cExtreme := abL.lo
    cZScore := abL.zScore
    cZCount := abL.zCount
    cZName := abL.zName
    cZTop := abL.zTop
    cZBot := abL.zBot
    cAbs := true
    cAbsZ := abL.volZ
else if trapS
    cDir := -1
    cType := "Sweep + trap"
    cLevel := swS.level
    cExtreme := swS.extreme
    cZScore := swS.zScore
    cZCount := swS.zCount
    cZName := swS.zName
    cZTop := swS.zTop
    cZBot := swS.zBot
    cDepth := swS.depthAtr
    cAbs := swS.absorbed
    cReclaimBars := nz(swS.reclaimBar, bar_index) - swS.startBar
    cSweepDelta := swS.sweepDelta
else if absSigS
    cDir := -1
    cType := "Absorption"
    cLevel := abS.level
    cExtreme := abS.hi
    cZScore := abS.zScore
    cZCount := abS.zCount
    cZName := abS.zName
    cZTop := abS.zTop
    cZBot := abS.zBot
    cAbs := true
    cAbsZ := abS.volZ

// Every trigger seen this bar is consumed, so none can fire twice.
if trapL
    swL.resetSweep()
if trapS
    swS.resetSweep()
if absSigL
    abL.active := false
if absSigS
    abS.active := false

// Zone touches over the lookback (only computed when a candidate exists).
int cTouches = 0
if cDir != 0
    float tTol = absTolAtr * atrS
    for i = 1 to touchLook
        if low[i] <= cZTop + tTol and high[i] >= cZBot - tTol
            cTouches += 1

// ─────────────────────────────────────────────────────────────────────────────
// 14. TRADE PLAN, SCORE, FILTERS
// ─────────────────────────────────────────────────────────────────────────────
bool   isLimit   = entryTypeIn == "Limit (50% of confirmation candle)"
float  pEntry    = na
float  pStop     = na
float  pRisk     = na
float  pT1       = na
float  pT2       = na
float  pT3       = na
float  pScore    = 0.0
string pGrade    = ""
string pWhyNot   = ""
string pBreak    = ""
float  pQty      = 0.0
float  pRiskCash = 0.0
float  pRRLiq    = na
bool   sigLong   = false
bool   sigShort  = false
var int lastSigBar   = -100000
var int sessSigCount = 0
if barConf and sessChange
    sessSigCount := 0

if cDir != 0
    float entryRaw = isLimit ? (high + low) / 2 : close
    pEntry := math.round_to_mintick(entryRaw)
    float stopBase = cDir == 1 ? math.min(cExtreme, low) : math.max(cExtreme, high)
    pStop  := math.round_to_mintick(stopBase - cDir * stopBufAtr * atrS)
    pRisk  := math.abs(pEntry - pStop)
    bool riskOk = pRisk >= syminfo.mintick and pRisk <= maxStopAtr * atrS
    if riskOk
        [t1, t2, t3] = f_targets(zones, cDir, pEntry, pRisk, tpModeIn == "Liquidity (auto)")
        pT1 := math.round_to_mintick(t1)
        pT2 := math.round_to_mintick(t2)
        pT3 := math.round_to_mintick(t3)
        float liq = f_nearestBeyond(zones, cDir, pEntry, 0.25 * pRisk)
        pRRLiq := na(liq) ? r3 : math.abs(liq - pEntry) / pRisk
    // factor scores (each 0-1)
    float conf    = f_clamp01((cZScore + cZCount) / 10.0)
    float touchQ  = f_clamp01(cTouches / 5.0)
    float qLevel  = f_clamp01(0.7 * conf + 0.3 * touchQ)
    bool  trapped = cDir == 1 ? cSweepDelta < 0 : cSweepDelta > 0
    float qSweep  = cType == "Sweep + trap" ? f_clamp01(0.6 * f_clamp01(cDepth / math.max(2 * eMinWick, 0.05)) + 0.4 * (1.0 - cReclaimBars / (eMaxReclaim + 1.0)) + (trapped ? 0.2 : 0.0)) : 0.0
    float qAbs    = cType == "Absorption" ? f_clamp01(cAbsZ / (eAbsZ * 1.5)) : cAbs ? 1.0 : f_clamp01(volZ / (2 * eAbsZ)) * 0.5
    float qDiv    = (cDir == 1 ? bsBullDiv : bsBearDiv) <= divRecent ? 1.0 : 0.0
    [alSc, alAgree, alAgainst, alN] = f_alignment(cDir, htfTrend, hVwap, hCvd)
    bool  counter = alSc < 0.5
    bool  nearExt = cDir == 1 ? (pEntry - hRangeLo) <= htfExtAtr * nz(hAtr, atrS) : (hRangeHi - pEntry) <= htfExtAtr * nz(hAtr, atrS)
    float qHtf    = counter and eMode == "Aggressive" ? alSc * 0.5 : alSc
    float qRR     = f_clamp01(nz(pRRLiq) / 3.0)
    float wSum    = wLevel + wSweep + wAbs + wDiv + wHtf + wSess + wVol + wRR
    float rawSc   = wSum > 0 ? 100.0 * (wLevel * qLevel + wSweep * qSweep + wAbs * qAbs + wDiv * qDiv + wHtf * qHtf + wSess * sessQ + wVol * volQ + wRR * qRR) / wSum : 0.0
    pScore := math.round(counter and eMode == "Aggressive" ? rawSc * aggrPenalty : rawSc)
    pGrade := f_grade(pScore)
    pBreak := "Level " + str.tostring(qLevel * 100, "#") + " · Sweep " + str.tostring(qSweep * 100, "#") + " · Abs " + str.tostring(qAbs * 100, "#") + " · Div " + str.tostring(qDiv * 100, "#") + " · HTF " + str.tostring(qHtf * 100, "#") + " · Sess " + str.tostring(sessQ * 100, "#") + " · Vol " + str.tostring(volQ * 100, "#") + " · R:R " + str.tostring(qRR * 100, "#")
    // HTF gate
    bool htfPass  = eMode == "Strict" ? (alN == 0 or alAgree == alN) : eMode == "Balanced" ? alSc >= 0.5 : (not counter or nearExt)
    bool keyBlock = false
    if useHtfKey
        float d1 = cDir == 1 ? hPrevHi - pEntry : pEntry - hPrevLo
        float d2 = cDir == 1 ? hRangeHi - pEntry : pEntry - hRangeLo
        keyBlock := (d1 > 0 and d1 < htfKeyAtr * atrS) or (d2 > 0 and d2 < htfKeyAtr * atrS)
    // position size
    float pv  = pointValIn > 0 ? pointValIn : syminfo.pointvalue
    float riskCashTarget = acctSize * riskPct / 100.0
    float qRaw = pRisk > 0 and pv > 0 ? riskCashTarget / (pRisk * pv) : 0.0
    float qCap = pEntry > 0 and pv > 0 ? acctSize * maxLev / (pEntry * pv) : qRaw
    pQty      := math.floor(math.min(qRaw, qCap) / qtyStep) * qtyStep
    pRiskCash := pQty * pRisk * pv
    // first reason a candidate is rejected (empty = signal)
    pWhyNot := not riskOk ? "stop too wide or too tight" : not htfPass ? "HTF disagrees (" + eMode + ")" : keyBlock ? "HTF level in the way" : not sessOk ? "session filtered" : inNoTr ? "no-trade window" : not regimeOk ? volRegime + " market" : not volOk ? "low volume" : bar_index - lastSigBar <= eCooldown ? "cooldown" : sessSigCount >= maxPerSess ? "session signal cap" : pScore < eMinScore ? "score " + str.tostring(pScore, "#") + " < " + str.tostring(eMinScore) : ""
    if pWhyNot == "" and barConf
        sigLong  := cDir == 1
        sigShort := cDir == -1
        lastSigBar := bar_index
        sessSigCount += 1
bool sigAny = sigLong or sigShort
int  sigDir = sigLong ? 1 : sigShort ? -1 : 0

// Last rejected candidate (dashboard context)
var string lastReject = ""
if barConf and cDir != 0 and not sigAny
    lastReject := (cDir == 1 ? "Long " : "Short ") + cType + " skipped: " + pWhyNot

// ═══════════════════════════════════════════════════════════════════════════════
// 15. BUILT-IN BACKTEST ENGINE (indicator only)
// One position at a time, same entry/SL/TP/management rules as the live plan.
// Conservative rules: the stop is checked before targets inside a bar; a limit
// order's fill bar earns no target credit; if TP1 and the breakeven price trade in
// the same bar, the remainder is assumed stopped at breakeven.
// ═══════════════════════════════════════════════════════════════════════════════
btStart     = input.time(timestamp("2020-01-01T00:00:00"), "Test from", group = G_BT)
btEnd       = input.time(timestamp("2099-12-31T00:00:00"), "Test until", group = G_BT)
commPct     = input.float(0.04, "Commission % per side", minval = 0, step = 0.01, group = G_BT)
slipTicks   = input.int(1, "Slippage (ticks) on market/stop fills", minval = 0, group = G_BT)
showBtTable = input.bool(true, "Show backtest table", group = G_BT)
btPosIn     = input.string("Bottom right", "Table position", options = ["Top left", "Top right", "Bottom left", "Bottom right", "Middle right"], group = G_BT)
showWL      = input.bool(true, "Mark wins / losses on chart", group = G_BT)

type Trade
    bool   active    = false
    bool   pending   = false
    int    dir       = 0
    float  entry     = na
    float  stop      = na
    float  initStop  = na
    float  tp1       = na
    float  tp2       = na
    float  tp3       = na
    float  risk      = na
    float  level     = na
    float  remaining = 1.0
    float  realized  = 0.0
    bool   hit1      = false
    bool   hit2      = false
    bool   beMoved   = false
    int    signalBar = na
    int    entryBar  = na
    string grade     = ""
    int    sess      = 3
    float  qty       = 0.0
    box    rBox      = na
    box    sBox      = na

var Trade tr = Trade.new()
var array<float>  btR     = array.new_float()
var array<int>    btDir   = array.new_int()
var array<string> btGrade = array.new_string()
var array<int>    btSess  = array.new_int()
var float eqR    = 0.0
var float eqPeak = 0.0
var float maxDD  = 0.0
var int   curLoseStreak = 0
var int   maxLoseStreak = 0
var int   sumBars = 0
var array<label> wlLabels = array.new<label>()
var array<box>   rrBoxes  = array.new<box>()

bool  inWindow = time >= btStart and time <= btEnd
float slipP    = slipTicks * syminfo.mintick
float pTp1     = math.min(tp1Pct, 100.0) / 100.0
float pTp2     = math.max(0.0, math.min(tp2Pct / 100.0, 1.0 - pTp1))

// events on this bar (alerts / visuals)
bool   evTp1  = false
bool   evTp2  = false
bool   evTp3  = false
bool   evSl   = false
bool   evBe   = false
bool   evFill = false
bool   evExit = false
string exitReason = ""
float  lastTradeR = na

// R earned by closing `frac` of the position at `px`, net of commission.
f_partialR(Trade t, float frac, float px) =>
    float gross = frac * t.dir * (px - t.entry) / t.risk
    float cost  = frac * (t.entry + px) * commPct / 100.0 / t.risk
    gross - cost

// ── manage the open trade with this bar's OHLC ───────────────────────────────
if barConf and tr.active
    bool justFilled = false
    if tr.pending
        if bar_index - tr.signalBar > limitExpiry
            tr.active  := false
            tr.pending := false
            if not na(tr.rBox)
                tr.rBox.delete()
            if not na(tr.sBox)
                tr.sBox.delete()
        else if (tr.dir == 1 and low <= tr.entry) or (tr.dir == -1 and high >= tr.entry)
            tr.pending  := false
            tr.entryBar := bar_index
            justFilled  := true
            evFill      := true
    if tr.active and not tr.pending and (bar_index > tr.entryBar or justFilled)
        bool stopHit = tr.dir == 1 ? low <= tr.stop : high >= tr.stop
        if stopHit
            float gapPx = justFilled ? tr.stop : open
            float px = tr.dir == 1 ? math.min(tr.stop, gapPx) - slipP : math.max(tr.stop, gapPx) + slipP
            tr.realized := tr.realized + f_partialR(tr, tr.remaining, px)
            tr.remaining := 0.0
            exitReason   := tr.beMoved and math.abs(tr.stop - tr.entry) < syminfo.mintick ? "BE" : tr.dir * (tr.stop - tr.initStop) > 0 ? "Trail" : "SL"
            evSl         := exitReason == "SL"
        else if not justFilled
            if not tr.hit1 and (tr.dir == 1 ? high >= tr.tp1 : low <= tr.tp1)
                float f1 = math.min(pTp1, tr.remaining)
                tr.realized := tr.realized + f_partialR(tr, f1, tr.tp1)
                tr.remaining := tr.remaining - f1
                tr.hit1 := true
                evTp1   := true
                if moveBE and tr.remaining > 1e-9
                    tr.stop    := tr.entry
                    tr.beMoved := true
                    evBe       := true
                    // conservative: breakeven also traded this bar → assume it filled after TP1
                    if tr.dir == 1 ? low <= tr.entry : high >= tr.entry
                        tr.realized := tr.realized + f_partialR(tr, tr.remaining, tr.entry)
                        tr.remaining := 0.0
                        exitReason   := "BE"
            if tr.remaining > 1e-9 and tr.hit1 and not tr.hit2 and (tr.dir == 1 ? high >= tr.tp2 : low <= tr.tp2)
                float f2 = math.min(pTp2, tr.remaining)
                tr.realized := tr.realized + f_partialR(tr, f2, tr.tp2)
                tr.remaining := tr.remaining - f2
                tr.hit2 := true
                evTp2   := true
            if tr.remaining > 1e-9 and tr.hit2 and (tr.dir == 1 ? high >= tr.tp3 : low <= tr.tp3)
                tr.realized := tr.realized + f_partialR(tr, tr.remaining, tr.tp3)
                tr.remaining := 0.0
                evTp3        := true
                exitReason   := "TP3"
            if tr.remaining <= 1e-9 and exitReason == ""
                exitReason := tr.hit2 ? "TP2" : "TP1"
            // end-of-bar exits, then trailing for the next bar
            if tr.remaining > 1e-9
                bool invalid = useInval and (tr.dir == 1 ? close < tr.level : close > tr.level)
                bool timeout = useTimeStop and bar_index - tr.entryBar >= timeStopBars
                if invalid or timeout
                    tr.realized := tr.realized + f_partialR(tr, tr.remaining, close - tr.dir * slipP)
                    tr.remaining := 0.0
                    exitReason   := invalid ? "Invalidated" : "Time"
                else if trailModeIn != "None" and (not trailAfterTp1 or tr.hit1)
                    float ns = trailModeIn == "ATR" ? close - tr.dir * trailAtr * atrS : (tr.dir == 1 ? lowN : highN)
                    if tr.dir * (ns - tr.stop) > 0
                        tr.stop := math.round_to_mintick(ns)
        // trade closed this bar → record it
        if tr.remaining <= 1e-9
            float rr = tr.realized
            btR.push(rr)
            btDir.push(tr.dir)
            btGrade.push(tr.grade)
            btSess.push(tr.sess)
            sumBars += bar_index - tr.entryBar
            eqR    += rr
            eqPeak := math.max(eqPeak, eqR)
            maxDD  := math.max(maxDD, eqPeak - eqR)
            curLoseStreak := rr <= 0 ? curLoseStreak + 1 : 0
            maxLoseStreak := math.max(maxLoseStreak, curLoseStreak)
            lastTradeR := rr
            evExit     := true
            if not na(tr.rBox)
                tr.rBox.set_right(bar_index)
            if not na(tr.sBox)
                tr.sBox.set_right(bar_index)
            if showWL
                label wl = label.new(bar_index, tr.dir == 1 ? high : low, (rr > 0 ? "W +" : "L ") + str.tostring(rr, "#.##") + "R · " + exitReason, style = tr.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(rr > 0 ? color.teal : color.red, 20), textcolor = color.white, size = size.tiny, force_overlay = true)
                wlLabels.push(wl)
                if wlLabels.size() > 150
                    wlLabels.shift().delete()
            tr.active := false
    if tr.active and not tr.pending and not na(tr.rBox)
        tr.rBox.set_right(bar_index + 1)
        tr.sBox.set_right(bar_index + 1)

// ── open a new trade on a fresh signal (flat only, inside the test window) ───
if barConf and sigAny and not tr.active and inWindow
    tr := Trade.new()
    tr.active    := true
    tr.pending   := isLimit
    tr.dir       := sigDir
    tr.entry     := isLimit ? pEntry : pEntry + sigDir * slipP
    tr.stop      := pStop
    tr.initStop  := pStop
    tr.tp1       := pT1
    tr.tp2       := pT2
    tr.tp3       := pT3
    tr.risk      := math.max(math.abs(tr.entry - pStop), syminfo.mintick)
    tr.level     := cLevel
    tr.signalBar := bar_index
    tr.entryBar  := bar_index
    tr.grade     := pGrade
    tr.sess      := sessId
    tr.qty       := pQty
    tr.rBox := box.new(bar_index, math.max(tr.entry, pT3), bar_index + 1, math.min(tr.entry, pT3), border_color = color.new(color.teal, 60), bgcolor = color.new(color.teal, 88), force_overlay = true)
    tr.sBox := box.new(bar_index, math.max(tr.entry, pStop), bar_index + 1, math.min(tr.entry, pStop), border_color = color.new(color.red, 60), bgcolor = color.new(color.red, 88), force_overlay = true)
    rrBoxes.push(tr.rBox)
    rrBoxes.push(tr.sBox)
    while rrBoxes.size() > 120
        rrBoxes.shift().delete()

// Trade state exposed to the shared dashboard / visuals.
int   posDir     = tr.active and not tr.pending ? tr.dir : 0
bool  posPending = tr.active and tr.pending
float posEntry   = tr.active ? tr.entry : na
float posStop    = tr.active ? tr.stop : na
float posTp1     = tr.active and not tr.hit1 ? tr.tp1 : na
float posTp2     = tr.active and not tr.hit2 ? tr.tp2 : na
float posTp3     = tr.active ? tr.tp3 : na
int   posPendDir = tr.active and tr.pending ? tr.dir : 0

// ═══════════════════════════════════════════════════════════════════════════════
// 16. VISUALS (shared). Everything price-based is drawn on the main chart with
// force_overlay = true. Object counts are capped and old objects deleted.
// ═══════════════════════════════════════════════════════════════════════════════
themeIn      = input.string("Dark", "Theme", options = ["Dark", "Light", "Custom"], group = G_VIS)
cBullIn      = input.color(#26A69A, "Custom bull", group = G_VIS, inline = "c1")
cBearIn      = input.color(#EF5350, "bear", group = G_VIS, inline = "c1")
cZoneIn      = input.color(#787B86, "zone", group = G_VIS, inline = "c2")
cAccentIn    = input.color(#F7C548, "accent", group = G_VIS, inline = "c2")
cTextIn      = input.color(#D1D4DC, "text", group = G_VIS, inline = "c3")
cPanelIn     = input.color(#131722, "panel", group = G_VIS, inline = "c3")
showZones    = input.bool(true, "Show zones", group = G_VIS, inline = "v1")
showLvLines  = input.bool(false, "Show individual level lines", group = G_VIS, inline = "v1")
maxZonesDraw = input.int(10, "Max zones drawn (nearest first)", minval = 1, maxval = 40, group = G_VIS)
zoneLookback = input.int(60, "Zone left extension (bars)", minval = 5, maxval = 400, group = G_VIS)
zoneMinW     = input.float(0.08, "Min zone height (ATR)", minval = 0.0, step = 0.01, group = G_VIS)
showVwapPlot = input.bool(true, "Plot anchored VWAP + bands", group = G_VIS)
showSweeps   = input.bool(true, "Sweep markers", group = G_VIS, inline = "v2")
showAbs      = input.bool(true, "Absorption markers", group = G_VIS, inline = "v2")
showSigLbl   = input.bool(true, "Signal labels", group = G_VIS, inline = "v3")
lblDetail    = input.string("Full plan", "", options = ["Full plan", "Compact"], group = G_VIS, inline = "v3")
lblSizeIn    = input.string("Small", "Label size", options = ["Tiny", "Small", "Normal"], group = G_VIS)
maxSigLbl    = input.int(100, "Max signal labels kept", minval = 10, maxval = 250, group = G_VIS)
showTrLines  = input.bool(true, "Active trade lines (entry/SL/TPs)", group = G_VIS)
showDash     = input.bool(true, "Show dashboard", group = G_DSH)
dashPosIn    = input.string("Top right", "Dashboard position", options = ["Top left", "Top right", "Bottom left", "Bottom right", "Middle right"], group = G_DSH)
dashSizeIn   = input.string("Small", "Dashboard text size", options = ["Tiny", "Small", "Normal", "Large"], group = G_DSH, tooltip = "Resizes the whole dashboard.")

color cBull   = themeIn == "Custom" ? cBullIn : themeIn == "Light" ? #089981 : #26A69A
color cBear   = themeIn == "Custom" ? cBearIn : themeIn == "Light" ? #F23645 : #EF5350
color cZone   = themeIn == "Custom" ? cZoneIn : themeIn == "Light" ? #9598A1 : #787B86
color cAccent = themeIn == "Custom" ? cAccentIn : themeIn == "Light" ? #B8860B : #F7C548
color cText   = themeIn == "Custom" ? cTextIn : themeIn == "Light" ? #131722 : #D1D4DC
color cPanel  = themeIn == "Custom" ? cPanelIn : themeIn == "Light" ? #F0F3FA : #131722

f_size(string s) => s == "Tiny" ? size.tiny : s == "Small" ? size.small : s == "Large" ? size.large : size.normal

f_pos(string s) => s == "Top left" ? position.top_left : s == "Bottom left" ? position.bottom_left : s == "Bottom right" ? position.bottom_right : s == "Middle right" ? position.middle_right : position.top_right

f_vol(float v) =>
    float a = math.abs(v)
    string s = a >= 1e9 ? str.tostring(v / 1e9, "#.##") + "B" : a >= 1e6 ? str.tostring(v / 1e6, "#.##") + "M" : a >= 1e3 ? str.tostring(v / 1e3, "#.#") + "K" : str.tostring(v, "#.##")
    s

// ── zones + level lines (redrawn on the last bar only) ───────────────────────
var array<box>   zBoxes  = array.new<box>()
var array<line>  lvLines = array.new<line>()
var array<label> lvLbls  = array.new<label>()
if barstate.islast
    for b in zBoxes
        b.delete()
    zBoxes.clear()
    for ln in lvLines
        ln.delete()
    lvLines.clear()
    for lb in lvLbls
        lb.delete()
    lvLbls.clear()
    int zCnt = zones.size()
    if showZones and zCnt > 0
        array<float> dist = array.new_float()
        for z in zones
            dist.push(math.abs((z.top + z.bot) / 2 - close))
        array<int> ordIdx = dist.sort_indices(order.ascending)
        int nDraw = math.min(maxZonesDraw, zCnt)
        for k = 0 to nDraw - 1
            Zone z = zones.get(ordIdx.get(k))
            float half = math.max(z.top - z.bot, zoneMinW * atrS) / 2
            float mid  = (z.top + z.bot) / 2
            color zc   = z.bot > close ? cBear : z.top < close ? cBull : cZone
            int   zTr  = z.score >= 8 ? 72 : z.score >= 5 ? 80 : 88
            zBoxes.push(box.new(bar_index - zoneLookback, mid + half, bar_index + 20, mid - half, border_color = color.new(zc, 55), bgcolor = color.new(zc, zTr), text = z.names + "  [" + str.tostring(z.score) + "]", text_color = cText, text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center, force_overlay = true))
    if showLvLines and levels.size() > 0
        int cnt = 0
        for lv in levels
            if cnt < 40
                lvLines.push(line.new(bar_index - zoneLookback, lv.price, bar_index + 20, lv.price, color = color.new(cZone, 30), style = line.style_dotted, force_overlay = true))
                lvLbls.push(label.new(bar_index + 20, lv.price, lv.name, style = label.style_none, textcolor = cZone, size = size.tiny, textalign = text.align_left, force_overlay = true))
                cnt += 1

// ── VWAP ─────────────────────────────────────────────────────────────────────
plot(showVwapPlot ? vwapV : na, "Anchored VWAP", color = cAccent, linewidth = 1, force_overlay = true)
plot(showVwapPlot ? vwU1 : na, "VWAP +SD1", color = color.new(cAccent, 70), force_overlay = true)
plot(showVwapPlot ? vwL1 : na, "VWAP -SD1", color = color.new(cAccent, 70), force_overlay = true)
plot(showVwapPlot ? vwU2 : na, "VWAP +SD2", color = color.new(cAccent, 50), style = plot.style_circles, force_overlay = true)
plot(showVwapPlot ? vwL2 : na, "VWAP -SD2", color = color.new(cAccent, 50), style = plot.style_circles, force_overlay = true)

// ── sweep + absorption markers (confirmed bars only) ─────────────────────────
plotshape(showSweeps and newBullSweep, "Sweep of lows", shape.xcross, location.belowbar, cBull, size = size.tiny, force_overlay = true)
plotshape(showSweeps and newBearSweep, "Sweep of highs", shape.xcross, location.abovebar, cBear, size = size.tiny, force_overlay = true)
plotshape(showAbs and absBullC, "Bullish absorption", shape.diamond, location.belowbar, color.new(cBull, 40), size = size.tiny, force_overlay = true)
plotshape(showAbs and absBearC, "Bearish absorption", shape.diamond, location.abovebar, color.new(cBear, 40), size = size.tiny, force_overlay = true)

// ── signal labels ────────────────────────────────────────────────────────────
var array<label> sigLbls = array.new<label>()
var string lastSigTxt = "None yet"
if sigAny
    float  rrT3 = f_safeDiv(math.abs(pT3 - pEntry), pRisk, 0.0)
    string head = (sigLong ? "▲ LONG " : "▼ SHORT ") + pGrade + " · " + str.tostring(pScore, "#")
    string body = "\n" + cType + " @ " + cZName + "\nEntry " + f_px(pEntry) + (isLimit ? " (limit)" : "") + "\nSL " + f_px(pStop) + "\nTP1 " + f_px(pT1) + " · TP2 " + f_px(pT2) + " · TP3 " + f_px(pT3) + "\nR:R 1:" + str.tostring(rrT3, "#.#") + " · Size " + str.tostring(pQty, "#.######") + " · Risk " + str.tostring(pRiskCash, "#.##")
    lastSigTxt := head + " @ " + f_px(pEntry)
    if showSigLbl
        sigLbls.push(label.new(bar_index, sigLong ? low : high, lblDetail == "Full plan" ? head + body : head, style = sigLong ? label.style_label_up : label.style_label_down, color = color.new(sigLong ? cBull : cBear, 8), textcolor = color.white, size = f_size(lblSizeIn), textalign = text.align_left, tooltip = head + body + "\nScore factors: " + pBreak, force_overlay = true))
        if sigLbls.size() > maxSigLbl
            sigLbls.shift().delete()

// ── active trade lines ───────────────────────────────────────────────────────
plot(showTrLines ? posEntry : na, "Trade entry", color = cText, style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posStop : na, "Trade stop", color = cBear, style = plot.style_linebr, linewidth = 2, force_overlay = true)
plot(showTrLines ? posTp1 : na, "Trade TP1", color = color.new(cBull, 40), style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posTp2 : na, "Trade TP2", color = color.new(cBull, 20), style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posTp3 : na, "Trade TP3", color = cBull, style = plot.style_linebr, linewidth = 2, force_overlay = true)

// ═══════════════════════════════════════════════════════════════════════════════
// 17. DASHBOARD
// ═══════════════════════════════════════════════════════════════════════════════
var table dash = table.new(f_pos(dashPosIn), 2, 13, bgcolor = color.new(cPanel, 8), frame_color = color.new(cZone, 50), frame_width = 1, border_color = color.new(cZone, 80), border_width = 1, force_overlay = true)

f_row(table t, int r, string k, string v, color vc, string sz) =>
    table.cell(t, 0, r, k, text_color = color.new(cText, 30), text_halign = text.align_left, text_size = sz)
    table.cell(t, 1, r, v, text_color = vc, text_halign = text.align_left, text_size = sz)

if barstate.islast and showDash
    string sz = f_size(dashSizeIn)
    // nearest zones above / below
    float  upP = na
    string upN = ""
    float  dnP = na
    string dnN = ""
    for z in zones
        if z.bot > close and (na(upP) or z.bot < upP)
            upP := z.bot
            upN := z.names
        if z.top < close and (na(dnP) or z.top > dnP)
            dnP := z.top
            dnN := z.names
    int    biasSc  = htfTrend + cvdDir + (close > vwapV ? 1 : close < vwapV ? -1 : 0)
    string biasTxt = biasSc >= 2 ? "▲ Bullish" : biasSc <= -2 ? "▼ Bearish" : "◆ Neutral"
    color  biasCol = biasSc >= 2 ? cBull : biasSc <= -2 ? cBear : cText
    string htfTxt  = (htfTrend == 1 ? "▲ Up" : htfTrend == -1 ? "▼ Down" : "◆ Flat") + " (" + htf + ") · VWAP " + (hVwap == 1 ? "above" : hVwap == -1 ? "below" : "—") + " · CVD " + (hCvd == 1 ? "▲" : hCvd == -1 ? "▼" : "—")
    string warn    = noVolData ? "⚠ No volume data: delta & absorption off" : not htfValid ? "⚠ HTF must be above chart TF" : tfSec > 3600 ? "⚠ TF too high for scalping logic" : ltfOk ? "OK · " + deltaSrc : "Candle-estimated delta (lower confidence)"
    color  warnCol = noVolData or not htfValid or tfSec > 3600 ? cBear : ltfOk ? cBull : cAccent
    string nextTp  = f_px(nz(posTp1, nz(posTp2, posTp3)))
    string todo    = posDir == 1 ? "In LONG: SL " + f_px(posStop) + " · next TP " + nextTp : posDir == -1 ? "In SHORT: SL " + f_px(posStop) + " · next TP " + nextTp : posPending ? "Limit " + (posPendDir == 1 ? "buy" : "sell") + " working at " + f_px(posEntry) : swL.active and swL.reclaimed ? "Lows swept (" + swL.zName + ") & reclaimed: wait for bullish confirmation close" : swS.active and swS.reclaimed ? "Highs swept (" + swS.zName + ") & reclaimed: wait for bearish confirmation close" : swL.active ? "Sweep below " + swL.zName + " in progress: wait for reclaim" : swS.active ? "Sweep above " + swS.zName + " in progress: wait for reclaim" : not regimeOk ? volRegime + " market: stand aside" : not sessOk ? "Outside your sessions: no trades" : "Wait for sweep of " + (dnN != "" ? dnN : "support") + " or " + (upN != "" ? upN : "resistance")
    f_row(dash, 0, "OFS Pro", "Estimated order flow · " + syminfo.ticker + " " + timeframe.period, cAccent, sz)
    f_row(dash, 1, "Bias", biasTxt, biasCol, sz)
    f_row(dash, 2, "HTF", htfTxt, htfTrend == 1 ? cBull : htfTrend == -1 ? cBear : cText, sz)
    f_row(dash, 3, "Session", sessName + (sessOk ? "" : " (filtered)") + (inNoTr ? " · NO-TRADE" : ""), sessOk and not inNoTr ? cText : cAccent, sz)
    f_row(dash, 4, "Volatility", volRegime + " · ATR×" + str.tostring(atrRatio, "#.##"), volRegime == "Normal" ? cBull : cAccent, sz)
    f_row(dash, 5, "Last signal", lastSigTxt, cText, sz)
    f_row(dash, 6, "Zone above", na(upP) ? "none in range" : upN + " · " + str.tostring((upP - close) / atrS, "#.##") + " ATR", cBear, sz)
    f_row(dash, 7, "Zone below", na(dnP) ? "none in range" : dnN + " · " + str.tostring((close - dnP) / atrS, "#.##") + " ATR", cBull, sz)
    f_row(dash, 8, "Est. Delta/CVD", "Δ " + f_vol(delta) + " (" + str.tostring(deltaPct, "#") + "%) · CVD " + (cvdDir == 1 ? "rising" : cvdDir == -1 ? "falling" : "flat") + " · vol z " + str.tostring(volZ, "#.#"), delta >= 0 ? cBull : cBear, sz)
    f_row(dash, 9, "Data quality", warn, warnCol, sz)
    f_row(dash, 10, "What to do now", todo, cAccent, sz)
    f_row(dash, 11, "Last skipped", lastReject == "" ? "—" : lastReject, color.new(cText, 30), sz)
    f_row(dash, 12, "Note", "Estimated Delta/CVD from OHLCV, not true order flow", color.new(cText, 40), sz)

// ═══════════════════════════════════════════════════════════════════════════════
// 18. LOWER PANE: estimated delta / CVD / equity curve (indicator only)
// ═══════════════════════════════════════════════════════════════════════════════
paneMode  = input.string("Delta histogram", "Lower pane shows", options = ["Delta histogram", "CVD", "Equity (R)", "None"], group = G_VIS)
showDivMk = input.bool(true, "Divergence marks in pane", group = G_VIS)
bool paneFlow = paneMode == "Delta histogram" or paneMode == "CVD"

plot(paneMode == "Delta histogram" ? delta : na, "Estimated Delta", color = delta >= 0 ? color.new(cBull, 20) : color.new(cBear, 20), style = plot.style_columns, format = format.volume)
plot(paneMode == "CVD" ? cvd : na, "Estimated CVD", color = cvdDir >= 0 ? cBull : cBear, linewidth = 2, format = format.volume)
plot(paneMode == "Equity (R)" ? eqR : na, "Backtest equity (R)", color = cAccent, linewidth = 2, style = plot.style_stepline)
hline(0, "Zero", color = color.new(#787B86, 60), linestyle = hline.style_dotted)
plotshape(showDivMk and paneFlow and bullDiv and barConf, "Bullish delta divergence", shape.triangleup, location.bottom, cBull, size = size.tiny)
plotshape(showDivMk and paneFlow and bearDiv and barConf, "Bearish delta divergence", shape.triangledown, location.top, cBear, size = size.tiny)

// ═══════════════════════════════════════════════════════════════════════════════
// 19. BACKTEST TABLE (indicator only)
// ═══════════════════════════════════════════════════════════════════════════════
f_stats(int fDir, string fGrade, int fSess) =>
    int   n = 0
    int   wins = 0
    float net = 0.0
    float gw = 0.0
    float gl = 0.0
    int   cnt = btR.size()
    if cnt > 0
        for i = 0 to cnt - 1
            bool ok = (fDir == 0 or btDir.get(i) == fDir) and (fGrade == "" or btGrade.get(i) == fGrade) and (fSess < 0 or btSess.get(i) == fSess)
            if ok
                float r = btR.get(i)
                n   += 1
                net += r
                if r > 0
                    wins += 1
                    gw   += r
                else
                    gl   -= r
    [n, wins, net, gw, gl]

var table bt = table.new(f_pos(btPosIn), 4, 23, bgcolor = color.new(cPanel, 8), frame_color = color.new(cZone, 50), frame_width = 1, border_color = color.new(cZone, 80), border_width = 1, force_overlay = true)

f_btCell(table t, int c, int r, string s, color col, string sz) =>
    table.cell(t, c, r, s, text_color = col, text_halign = c == 0 ? text.align_left : text.align_right, text_size = sz)

f_btKV(table t, int r, string k, string v, color col, string sz) =>
    f_btCell(t, 0, r, k, color.new(cText, 30), sz)
    f_btCell(t, 1, r, v, col, sz)
    table.merge_cells(t, 1, r, 3, r)

f_btSplit(table t, int r, string k, int fDir, string fGrade, int fSess, string sz) =>
    [n, w, net, gw, gl] = f_stats(fDir, fGrade, fSess)
    f_btCell(t, 0, r, k, color.new(cText, 30), sz)
    f_btCell(t, 1, r, str.tostring(n), cText, sz)
    f_btCell(t, 2, r, n > 0 ? str.tostring(100.0 * w / n, "#.#") + "%" : "—", cText, sz)
    f_btCell(t, 3, r, n > 0 ? str.tostring(net, "#.##") : "—", net > 0 ? cBull : net < 0 ? cBear : cText, sz)

if barstate.islast and showBtTable
    string sz = f_size(dashSizeIn)
    [n, w, net, gw, gl] = f_stats(0, "", -1)
    float  wr     = n > 0 ? 100.0 * w / n : na
    float  avgR   = n > 0 ? net / n : na
    float  avgWin = w > 0 ? gw / w : 0.0
    float  avgLos = n - w > 0 ? gl / (n - w) : 0.0
    float  expR   = n > 0 ? (float(w) / n) * avgWin - (float(n - w) / n) * avgLos : na
    string pfTxt  = gl > 0 ? str.tostring(gw / gl, "#.##") : n > 0 ? "∞ (no losses)" : "—"
    f_btCell(bt, 0, 0, "Backtest · " + str.tostring(n) + " trades", cAccent, sz)
    table.merge_cells(bt, 0, 0, 3, 0)
    f_btKV(bt, 1, "Win rate", na(wr) ? "—" : str.tostring(wr, "#.#") + "%", cText, sz)
    f_btKV(bt, 2, "Avg R", na(avgR) ? "—" : str.tostring(avgR, "#.##"), nz(avgR) > 0 ? cBull : cBear, sz)
    f_btKV(bt, 3, "Expectancy (R)", na(expR) ? "—" : str.tostring(expR, "#.##"), nz(expR) > 0 ? cBull : cBear, sz)
    f_btKV(bt, 4, "Profit factor", pfTxt, cText, sz)
    f_btKV(bt, 5, "Net R", str.tostring(net, "#.##"), net > 0 ? cBull : net < 0 ? cBear : cText, sz)
    f_btKV(bt, 6, "Max drawdown (R)", str.tostring(maxDD, "#.##"), cBear, sz)
    f_btKV(bt, 7, "Longest losing streak", str.tostring(maxLoseStreak), cText, sz)
    f_btKV(bt, 8, "Avg bars in trade", n > 0 ? str.tostring(float(sumBars) / n, "#.#") : "—", cText, sz)
    f_btKV(bt, 9, "Costs", str.tostring(commPct, "#.###") + "% / side · " + str.tostring(slipTicks) + " tick slip", color.new(cText, 30), sz)
    f_btCell(bt, 0, 10, "Split", cAccent, sz)
    f_btCell(bt, 1, 10, "Trades", cAccent, sz)
    f_btCell(bt, 2, 10, "Win %", cAccent, sz)
    f_btCell(bt, 3, 10, "Net R", cAccent, sz)
    f_btSplit(bt, 11, "Long", 1, "", -1, sz)
    f_btSplit(bt, 12, "Short", -1, "", -1, sz)
    f_btSplit(bt, 13, "Grade A+", 0, "A+", -1, sz)
    f_btSplit(bt, 14, "Grade A", 0, "A", -1, sz)
    f_btSplit(bt, 15, "Grade B", 0, "B", -1, sz)
    f_btSplit(bt, 16, "Grade C", 0, "C", -1, sz)
    f_btSplit(bt, 17, "Asia", 0, "", 0, sz)
    f_btSplit(bt, 18, "London", 0, "", 1, sz)
    f_btSplit(bt, 19, "New York", 0, "", 2, sz)
    f_btSplit(bt, 20, "Off-hours", 0, "", 3, sz)
    f_btCell(bt, 0, 21, "Window: " + str.format_time(btStart, "yyyy-MM-dd") + " → " + (btEnd > timenow ? "now" : str.format_time(btEnd, "yyyy-MM-dd")), color.new(cText, 30), sz)
    table.merge_cells(bt, 0, 21, 3, 21)
    f_btCell(bt, 0, 22, "Simulated on available chart history. Not a guarantee of future results.", color.new(cText, 30), sz)
    table.merge_cells(bt, 0, 22, 3, 22)

// ═══════════════════════════════════════════════════════════════════════════════
// 20. ALERTS
// ═══════════════════════════════════════════════════════════════════════════════
alSig   = input.bool(true, "alert(): new signal", group = G_ALR)
alTp    = input.bool(true, "alert(): TP hits", group = G_ALR)
alSl    = input.bool(true, "alert(): stop hit", group = G_ALR)
alBe    = input.bool(true, "alert(): breakeven move", group = G_ALR)
alSweep = input.bool(false, "alert(): sweep detected", group = G_ALR)

alertcondition(sigLong, "OFS Long signal", "OFS Pro LONG signal on {{ticker}} {{interval}} at {{close}}")
alertcondition(sigShort, "OFS Short signal", "OFS Pro SHORT signal on {{ticker}} {{interval}} at {{close}}")
alertcondition(evTp1 or evTp2 or evTp3, "OFS Take-profit hit", "OFS Pro take-profit hit on {{ticker}} {{interval}}")
alertcondition(evSl, "OFS Stop hit", "OFS Pro stop hit on {{ticker}} {{interval}}")
alertcondition(evBe, "OFS Breakeven move", "OFS Pro stop moved to breakeven on {{ticker}} {{interval}}")
alertcondition(newBullSweep or newBearSweep, "OFS Sweep detected", "OFS Pro liquidity sweep on {{ticker}} {{interval}} at {{close}}")

f_json(string ev, string side, string extra) =>
    '{"event":"' + ev + '","side":"' + side + '","symbol":"' + syminfo.tickerid + '","tf":"' + timeframe.period + '","price":' + str.tostring(close, format.mintick) + extra + '}'

string sideNow = tr.dir == 1 ? "long" : tr.dir == -1 ? "short" : ""
if barConf
    if alSig and sigAny
        alert(f_json("signal", sigLong ? "long" : "short", ',"grade":"' + pGrade + '","score":' + str.tostring(pScore, "#") + ',"type":"' + cType + '","entry":' + str.tostring(pEntry, format.mintick) + ',"sl":' + str.tostring(pStop, format.mintick) + ',"tp1":' + str.tostring(pT1, format.mintick) + ',"tp2":' + str.tostring(pT2, format.mintick) + ',"tp3":' + str.tostring(pT3, format.mintick) + ',"qty":' + str.tostring(pQty, "#.######") + ',"order":"' + (isLimit ? "limit" : "market") + '"'), alert.freq_once_per_bar_close)
    if alTp and (evTp1 or evTp2 or evTp3)
        alert(f_json("tp_hit", sideNow, ',"tp":' + (evTp3 ? "3" : evTp2 ? "2" : "1")), alert.freq_once_per_bar_close)
    if alSl and evSl
        alert(f_json("sl_hit", sideNow, ',"r":' + str.tostring(nz(lastTradeR), "#.##")), alert.freq_once_per_bar_close)
    if alBe and evBe
        alert(f_json("breakeven", sideNow, ',"stop":' + str.tostring(tr.stop, format.mintick)), alert.freq_once_per_bar_close)
    if alSweep and (newBullSweep or newBearSweep)
        alert(f_json("sweep", newBullSweep ? "lows" : "highs", ',"level":"' + (newBullSweep ? swL.zName : swS.zName) + '"'), alert.freq_once_per_bar_close)

03Strategy

Same signal and plan logic for the Strategy Tester. Set commission and slippage under Settings → Properties.

ofs_pro_strategy.pine · 1200 linesDownload
//@version=6
// ─────────────────────────────────────────────────────────────────────────────
// OFS Pro: Order Flow Scalper  ·  STRATEGY (for TradingView's Strategy Tester)
// Identical signal + plan logic to the indicator. Commission (% per side) and
// slippage (ticks) live in Settings → Properties. Position size = equity × risk %.
// Delta/CVD are ESTIMATES (lower-timeframe or candle based), not true order flow.
// ─────────────────────────────────────────────────────────────────────────────
strategy("OFS Pro: Order Flow Scalper [Strategy]", shorttitle = "OFS Pro S", overlay = true, initial_capital = 10000, default_qty_type = strategy.fixed, default_qty_value = 1, commission_type = strategy.commission.percent, commission_value = 0.04, slippage = 1, process_orders_on_close = true, pyramiding = 0, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 1000)
// ═══════════════════════════════════════════════════════════════════════════════
// OFS Pro: Order Flow Scalper  |  shared core (identical in indicator + strategy)
// All "order flow" values are ESTIMATES built from OHLCV / lower-timeframe bars.
// Pine Script has no access to the order book or a real footprint.
// ═══════════════════════════════════════════════════════════════════════════════

// ─────────────────────────────────────────────────────────────────────────────
// 0. INPUT GROUPS
// ─────────────────────────────────────────────────────────────────────────────
const string G_PRE = "00 · Presets"
const string G_DEL = "01 · Delta engine"
const string G_LEV = "02 · Key levels"
const string G_SES = "03 · Sessions"
const string G_SWP = "04 · Sweeps & traps"
const string G_ABS = "05 · Absorption"
const string G_HTF = "06 · HTF confirmation"
const string G_SCO = "07 · Scoring"
const string G_FIL = "08 · Filters"
const string G_RSK = "09 · Entry, stop, targets"
const string G_MGT = "10 · Trade management"
const string G_SIZ = "11 · Position size"
const string G_VIS = "12 · Visuals"
const string G_DSH = "13 · Dashboard"
const string G_BT  = "14 · Backtest"
const string G_ALR = "15 · Alerts"

// ─────────────────────────────────────────────────────────────────────────────
// 1. INPUTS
// ─────────────────────────────────────────────────────────────────────────────
presetIn     = input.string("Custom", "Preset", options = ["Custom", "Scalp 1m-5m", "Intraday 15m", "Conservative", "Aggressive"], group = G_PRE, tooltip = "Any preset other than Custom OVERRIDES: min sweep (ATR), max reclaim bars, absorption z-score, min score, cooldown, signal mode and the delta-flip rule. Pick Custom to use your own values below.")

// Delta
ltfIn        = input.string("Auto", "Intrabar precision", options = ["Auto", "1S", "5S", "15S", "1"], group = G_DEL, tooltip = "Lower timeframe used to classify each intrabar's volume as buying or selling. Auto: 1m on 2m-1h charts, 5m up to 4h, 15m up to 1D. On 1m charts Auto uses the candle estimate. 1S/5S/15S need a TradingView plan with seconds data.")
cvdResetIn   = input.string("Daily", "CVD reset", options = ["None", "Daily", "Session"], group = G_DEL)
volZLen      = input.int(50, "Volume z-score length", minval = 5, group = G_DEL)
cvdSlopeLen  = input.int(10, "CVD slope length", minval = 2, group = G_DEL)
divLen       = input.int(20, "Divergence lookback (bars)", minval = 3, group = G_DEL, tooltip = "Price makes a new N-bar high/low while estimated CVD does not.")
divRecent    = input.int(5, "Divergence counts if within (bars)", minval = 1, group = G_DEL)
atrLen       = input.int(14, "ATR length", minval = 1, group = G_DEL, tooltip = "Every distance input in ATR uses this ATR, so settings auto-scale across symbols and timeframes.")

// Levels
usePD        = input.bool(true, "Previous day H/L", group = G_LEV, inline = "l1")
usePW        = input.bool(true, "Previous week H/L", group = G_LEV, inline = "l1")
usePM        = input.bool(true, "Previous month H/L", group = G_LEV, inline = "l2")
useSessLv    = input.bool(true, "Session H/L", group = G_LEV, inline = "l2")
useVwapLv    = input.bool(true, "Anchored VWAP ±SD", group = G_LEV, inline = "l3")
usePivots    = input.bool(true, "Swing pivots", group = G_LEV, inline = "l3")
useEq        = input.bool(true, "Equal highs / lows", group = G_LEV, inline = "l4")
useVP        = input.bool(false, "Volume POC/VAH/VAL (approx.)", group = G_LEV, inline = "l4")
vwapAnchorIn = input.string("Daily", "VWAP anchor", options = ["Daily", "Weekly", "Monthly"], group = G_LEV)
vwapSd1      = input.float(1.0, "VWAP band 1 (SD)", minval = 0.1, step = 0.1, group = G_LEV)
vwapSd2      = input.float(2.0, "VWAP band 2 (SD)", minval = 0.1, step = 0.1, group = G_LEV)
pivLen       = input.int(5, "Pivot strength (bars each side)", minval = 1, group = G_LEV, tooltip = "Pivots are confirmed after this many bars, so they never repaint.")
maxPivots    = input.int(6, "Pivots remembered (per side)", minval = 1, maxval = 20, group = G_LEV)
eqTolAtr     = input.float(0.10, "Equal H/L tolerance (ATR)", minval = 0.01, step = 0.01, group = G_LEV)
vpLen        = input.int(200, "Volume profile lookback", minval = 20, maxval = 500, group = G_LEV)
vpRows       = input.int(30, "Volume profile rows", minval = 10, maxval = 100, group = G_LEV)
vaPct        = input.float(70, "Value area %", minval = 50, maxval = 95, group = G_LEV)
zoneTolAtr   = input.float(0.25, "Merge levels into zones within (ATR)", minval = 0.0, step = 0.05, group = G_LEV)
levelRangeAtr= input.float(12, "Only use levels within (ATR) of price", minval = 1, group = G_LEV)

// Sessions
sessTz       = input.string("America/New_York", "Session timezone", group = G_SES, tooltip = "IANA name, e.g. America/New_York, Europe/London, Etc/UTC.")
asiaSess     = input.session("1900-0400", "Asia", group = G_SES)
lonSess      = input.session("0300-1200", "London", group = G_SES)
nySess       = input.session("0800-1700", "New York", group = G_SES)

// Sweeps & traps
minWickIn    = input.float(0.15, "Min sweep beyond level (ATR)", minval = 0.0, step = 0.01, group = G_SWP)
maxReclaimIn = input.int(3, "Max bars to reclaim", minval = 1, group = G_SWP)
maxConfirm   = input.int(3, "Max bars for confirmation after reclaim", minval = 1, group = G_SWP)
needFlipIn   = input.bool(true, "Require delta flip on confirmation", group = G_SWP, tooltip = "Trapped-trader rule: the confirmation candle's estimated delta must point in the signal direction.")
confRefIn    = input.string("Reclaim level", "Confirmation must close beyond", options = ["Reclaim level", "Sweep candle midpoint"], group = G_SWP)

// Absorption
useAbsSig    = input.bool(true, "Absorption-reversal signals", group = G_ABS, tooltip = "Also signal when absorption at a zone is followed by a confirmation candle, even without a sweep.")
absZIn       = input.float(2.0, "Volume z-score ≥", minval = 0.5, step = 0.1, group = G_ABS)
absDeltaPct  = input.float(25, "|Delta %| of bar volume ≥", minval = 1, maxval = 100, group = G_ABS)
absRangeAtr  = input.float(0.8, "Bar range ≤ (ATR)", minval = 0.1, step = 0.05, group = G_ABS, tooltip = "Effort without result: big volume, small range.")
absClv       = input.float(0.5, "Close location ≥ (0-1, favorable side)", minval = 0.0, maxval = 1.0, step = 0.05, group = G_ABS)
absTolAtr    = input.float(0.25, "Counts as 'at a level' within (ATR)", minval = 0.0, step = 0.05, group = G_ABS)
absConfBars  = input.int(2, "Bars to confirm absorption", minval = 1, group = G_ABS)

// HTF
htfModeIn    = input.string("Auto", "HTF selection", options = ["Auto", "Manual"], group = G_HTF, tooltip = "Auto: ≤1m→15m, ≤5m→1h, ≤1h→4h, ≤4h→1D, else 1W.")
htfManual    = input.timeframe("60", "Manual HTF", group = G_HTF)
htfTrendType = input.string("EMA stack", "HTF trend method", options = ["EMA stack", "Market structure", "Both must agree"], group = G_HTF)
useHtfTrend  = input.bool(true, "Filter: HTF trend", group = G_HTF)
useHtfVwap   = input.bool(true, "Filter: HTF VWAP side", group = G_HTF)
useHtfCvd    = input.bool(true, "Filter: HTF CVD slope (candle estimate)", group = G_HTF)
useHtfKey    = input.bool(true, "Filter: don't trade into HTF level", group = G_HTF)
htfKeyAtr    = input.float(1.0, "…HTF level too close if within (ATR)", minval = 0.1, step = 0.1, group = G_HTF)
htfEmaFast   = input.int(20, "HTF EMA fast", minval = 2, group = G_HTF)
htfEmaSlow   = input.int(50, "HTF EMA slow", minval = 3, group = G_HTF)
htfStructLen = input.int(3, "HTF structure pivot strength", minval = 1, group = G_HTF)
htfRangeLen  = input.int(20, "HTF range lookback (bars)", minval = 5, group = G_HTF)
htfCvdLen    = input.int(5, "HTF CVD slope length", minval = 2, group = G_HTF)
modeIn       = input.string("Balanced", "Signal mode", options = ["Strict", "Balanced", "Aggressive"], group = G_HTF, tooltip = "Strict: every enabled HTF filter must agree. Balanced: no net HTF opposition. Aggressive: counter-trend allowed only near the HTF range extreme, with a score penalty.")
htfExtAtr    = input.float(1.5, "Aggressive: HTF extreme within (HTF ATR)", minval = 0.1, step = 0.1, group = G_HTF)
aggrPenalty  = input.float(0.85, "Aggressive: counter-trend score multiplier", minval = 0.1, maxval = 1.0, step = 0.05, group = G_HTF)

// Scoring
wLevel       = input.float(20, "Weight: level quality", minval = 0, group = G_SCO)
wSweep       = input.float(20, "Weight: sweep quality", minval = 0, group = G_SCO)
wAbs         = input.float(15, "Weight: absorption", minval = 0, group = G_SCO)
wDiv         = input.float(10, "Weight: delta divergence", minval = 0, group = G_SCO)
wHtf         = input.float(15, "Weight: HTF alignment", minval = 0, group = G_SCO)
wSess        = input.float(5, "Weight: session quality", minval = 0, group = G_SCO)
wVol         = input.float(5, "Weight: volatility regime", minval = 0, group = G_SCO)
wRR          = input.float(10, "Weight: R:R to next liquidity", minval = 0, group = G_SCO)
minScoreIn   = input.int(60, "Minimum score to show", minval = 0, maxval = 100, group = G_SCO, tooltip = "Grades: A+ ≥ 85, A 70-84, B 55-69, C < 55.")
touchLook    = input.int(100, "Zone touch lookback (bars)", minval = 1, maxval = 500, group = G_SCO)

// Filters
tradeAsia    = input.bool(true, "Asia", group = G_FIL, inline = "s")
tradeLon     = input.bool(true, "London", group = G_FIL, inline = "s")
tradeNy      = input.bool(true, "New York", group = G_FIL, inline = "s")
tradeOff     = input.bool(false, "Off-hours", group = G_FIL, inline = "s")
regimeLen    = input.int(100, "Volatility baseline length", minval = 10, group = G_FIL)
deadThr      = input.float(0.6, "Dead market if ATR/baseline <", minval = 0.0, step = 0.05, group = G_FIL)
chaosThr     = input.float(2.2, "Chaotic market if ATR/baseline >", minval = 1.0, step = 0.1, group = G_FIL)
useRegime    = input.bool(true, "Skip dead / chaotic markets", group = G_FIL)
minVolMult   = input.float(0.5, "Min volume (× average)", minval = 0.0, step = 0.1, group = G_FIL)
cooldownIn   = input.int(5, "Cooldown bars between signals", minval = 0, group = G_FIL)
maxPerSess   = input.int(4, "Max signals per session", minval = 1, group = G_FIL)
useNoTrade   = input.bool(false, "No-trade window", group = G_FIL, inline = "nt")
noTradeSess  = input.session("0825-0845", "", group = G_FIL, inline = "nt", tooltip = "e.g. around scheduled news. Uses the session timezone.")

// Entry / stop / targets
entryTypeIn  = input.string("Market (confirmation close)", "Entry", options = ["Market (confirmation close)", "Limit (50% of confirmation candle)"], group = G_RSK)
limitExpiry  = input.int(3, "Limit order expiry (bars)", minval = 1, group = G_RSK)
stopBufAtr   = input.float(0.10, "Stop buffer beyond extreme (ATR)", minval = 0.0, step = 0.01, group = G_RSK)
maxStopAtr   = input.float(2.5, "Skip if stop wider than (ATR)", minval = 0.2, step = 0.1, group = G_RSK)
tpModeIn     = input.string("R multiples", "Targets", options = ["R multiples", "Liquidity (auto)"], group = G_RSK, tooltip = "Liquidity: TP1-3 at the next opposing zones (at least 0.5R away). Missing targets fall back to R multiples.")
r1           = input.float(1.0, "TP1 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")
r2           = input.float(2.0, "TP2 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")
r3           = input.float(3.0, "TP3 (R)", minval = 0.1, step = 0.1, group = G_RSK, inline = "r")

// Management
tp1Pct       = input.float(50, "Close % at TP1", minval = 0, maxval = 100, group = G_MGT)
tp2Pct       = input.float(30, "Close % at TP2", minval = 0, maxval = 100, group = G_MGT, tooltip = "Remainder closes at TP3.")
moveBE       = input.bool(true, "Move stop to breakeven after TP1", group = G_MGT)
trailModeIn  = input.string("None", "Trailing stop", options = ["None", "ATR", "Structure"], group = G_MGT)
trailAfterTp1= input.bool(true, "Trail only after TP1", group = G_MGT)
trailAtr     = input.float(1.5, "ATR trail distance", minval = 0.1, step = 0.1, group = G_MGT)
trailLen     = input.int(5, "Structure trail lookback (bars)", minval = 1, group = G_MGT)
useTimeStop  = input.bool(true, "Time stop", group = G_MGT, inline = "ts")
timeStopBars = input.int(30, "after bars", minval = 1, group = G_MGT, inline = "ts")
useInval     = input.bool(true, "Exit if signal level is lost (close beyond)", group = G_MGT)

// Position size
acctSize     = input.float(10000, "Account size", minval = 1, group = G_SIZ)
riskPct      = input.float(0.5, "Risk per trade %", minval = 0.01, step = 0.05, group = G_SIZ)
pointValIn   = input.float(0, "Value per point per contract (0 = symbol's)", minval = 0, group = G_SIZ, tooltip = "0 uses syminfo.pointvalue (e.g. GC1! = 100, SI1! = 5000, crypto = 1).")
maxLev       = input.float(10, "Leverage cap (×)", minval = 0.1, step = 0.5, group = G_SIZ)
qtyStep      = input.float(0.001, "Quantity step", minval = 0.000001, group = G_SIZ)

// ─────────────────────────────────────────────────────────────────────────────
// 2. PRESETS → effective values
// ─────────────────────────────────────────────────────────────────────────────
bool   isCustom   = presetIn == "Custom"
float  eMinWick   = isCustom ? minWickIn  : presetIn == "Scalp 1m-5m" ? 0.12 : presetIn == "Intraday 15m" ? 0.15 : presetIn == "Conservative" ? 0.20 : 0.08
int    eMaxReclaim= isCustom ? maxReclaimIn : presetIn == "Scalp 1m-5m" ? 3 : presetIn == "Intraday 15m" ? 2 : presetIn == "Conservative" ? 2 : 4
float  eAbsZ      = isCustom ? absZIn     : presetIn == "Scalp 1m-5m" ? 2.0 : presetIn == "Intraday 15m" ? 1.8 : presetIn == "Conservative" ? 2.5 : 1.5
int    eMinScore  = isCustom ? minScoreIn : presetIn == "Scalp 1m-5m" ? 60 : presetIn == "Intraday 15m" ? 65 : presetIn == "Conservative" ? 75 : 50
int    eCooldown  = isCustom ? cooldownIn : presetIn == "Scalp 1m-5m" ? 5 : presetIn == "Intraday 15m" ? 3 : presetIn == "Conservative" ? 8 : 2
string eMode      = isCustom ? modeIn     : presetIn == "Conservative" ? "Strict" : presetIn == "Aggressive" ? "Aggressive" : "Balanced"
bool   eNeedFlip  = isCustom ? needFlipIn : presetIn != "Aggressive"

// ─────────────────────────────────────────────────────────────────────────────
// 3. TYPES
// ─────────────────────────────────────────────────────────────────────────────
// A single key level.
type Level
    float  price
    string name
    int    weight

// Nearby levels merged into one zone.
type Zone
    float  top
    float  bot
    int    count
    int    score
    string names

// Running high/low for a trading session.
type SessTrack
    float hi     = na
    float lo     = na
    float lastHi = na
    float lastLo = na
    bool  active = false

// A liquidity sweep tracked until confirmation, failure or expiry.
type Sweep
    bool   active     = false
    bool   reclaimed  = false
    bool   absorbed   = false
    float  level      = na
    float  extreme    = na
    float  mid        = na
    int    startBar   = na
    int    reclaimBar = na
    int    zScore     = 0
    int    zCount     = 0
    string zName      = ""
    float  zTop       = na
    float  zBot       = na
    float  depthAtr   = 0.0
    float  sweepDelta = 0.0

// An absorption bar waiting for a confirmation candle.
type AbsSetup
    bool   active   = false
    int    startBar = na
    float  hi       = na
    float  lo       = na
    float  level    = na
    float  volZ     = 0.0
    int    zScore   = 0
    int    zCount   = 0
    string zName    = ""
    float  zTop     = na
    float  zBot     = na

// ─────────────────────────────────────────────────────────────────────────────
// 4. HELPERS
// ─────────────────────────────────────────────────────────────────────────────
f_clamp01(float x) => na(x) ? 0.0 : math.max(0.0, math.min(1.0, x))

f_safeDiv(float a, float b, float dflt) => na(a) or na(b) or b == 0 ? dflt : a / b

f_grade(float s) => s >= 85 ? "A+" : s >= 70 ? "A" : s >= 55 ? "B" : "C"

f_px(float p) => na(p) ? "—" : str.tostring(p, format.mintick)

// Intrabar delta: each lower-timeframe bar's volume is signed by its own direction
// (close vs open); dojis use the change from the previous intrabar close.
f_intrabarDelta(array<float> o, array<float> c, array<float> v) =>
    float d = 0.0
    int   n = na(c) ? 0 : c.size()
    float prevC = na
    if n > 0
        for i = 0 to n - 1
            float cc = c.get(i)
            float oo = o.get(i)
            float vv = nz(v.get(i))
            float sgn = cc > oo ? 1.0 : cc < oo ? -1.0 : na(prevC) ? 0.0 : cc > prevC ? 1.0 : cc < prevC ? -1.0 : 0.0
            d += sgn * vv
            prevC := cc
    [d, n]

// Adds a level when enabled, valid and close enough to price to matter.
f_addLevel(array<Level> arr, float p, string nm, int w, bool on, float ref, float maxDist) =>
    bool ok = on and not na(p) and math.abs(p - ref) <= maxDist
    if ok
        arr.push(Level.new(p, nm, w))
    ok

// Sorts levels by price and merges any within `tol` into zones.
f_buildZones(array<Level> lv, float tol) =>
    array<Zone> zs = array.new<Zone>()
    int n = lv.size()
    if n > 0
        array<float> px = array.new_float()
        for l in lv
            px.push(l.price)
        array<int> idx = px.sort_indices(order.ascending)
        Zone cur = na
        for i in idx
            Level lvl = lv.get(i)
            if na(cur)
                cur := Zone.new(lvl.price, lvl.price, 1, lvl.weight, lvl.name)
            else if lvl.price - cur.top <= tol
                cur.top   := math.max(cur.top, lvl.price)
                cur.count := cur.count + 1
                cur.score := cur.score + lvl.weight
                cur.names := cur.names + " · " + lvl.name
            else
                zs.push(cur)
                cur := Zone.new(lvl.price, lvl.price, 1, lvl.weight, lvl.name)
        if not na(cur)
            zs.push(cur)
    zs

// Best-scoring zone swept on this bar. dir = 1 sweep of lows (long setup), -1 sweep of highs.
f_findSweep(array<Zone> zs, int dir, float minPen, float prevClose) =>
    int best = -1
    int bestScore = -1
    int n = zs.size()
    if n > 0 and not na(prevClose)
        for i = 0 to n - 1
            Zone z = zs.get(i)
            bool hit = dir == 1 ? low <= z.bot - minPen and prevClose > z.bot : high >= z.top + minPen and prevClose < z.top
            if hit and z.score > bestScore
                best := i
                bestScore := z.score
    best

// Zone the current bar is trading at. dir = 1 support, -1 resistance.
f_touchZone(array<Zone> zs, int dir, float tol) =>
    int best = -1
    int bestScore = -1
    int n = zs.size()
    if n > 0
        for i = 0 to n - 1
            Zone z = zs.get(i)
            bool at = dir == 1 ? low <= z.top + tol and high >= z.bot - tol and close >= z.bot : high >= z.bot - tol and low <= z.top + tol and close <= z.top
            if at and z.score > bestScore
                best := i
                bestScore := z.score
    best

// Nearest opposing zone edge at least `minDist` beyond `ref` in direction `dir`.
f_nearestBeyond(array<Zone> zs, int dir, float ref, float minDist) =>
    float best = na
    for z in zs
        float p = dir == 1 ? z.bot : z.top
        if dir * (p - ref) >= minDist
            if na(best) or dir * (p - best) < 0
                best := p
    best

// TP1-3: R multiples or the next opposing liquidity zones, always monotonic.
f_targets(array<Zone> zs, int dir, float entry, float rsk, bool liq) =>
    float t1 = entry + dir * r1 * rsk
    float t2 = entry + dir * r2 * rsk
    float t3 = entry + dir * r3 * rsk
    if liq
        array<float> c = array.new_float()
        for z in zs
            float p = dir == 1 ? z.bot : z.top
            if dir * (p - entry) >= 0.5 * rsk
                c.push(p)
        c.sort(dir == 1 ? order.ascending : order.descending)
        int k = c.size()
        if k > 0
            t1 := c.get(0)
        if k > 1
            t2 := c.get(1)
        if k > 2
            t3 := c.get(2)
    if dir * (t2 - t1) <= 0
        t2 := t1 + dir * math.max(r2 - r1, 0.5) * rsk
    if dir * (t3 - t2) <= 0
        t3 := t2 + dir * math.max(r3 - r2, 0.5) * rsk
    [t1, t2, t3]

// HTF alignment: 0 = fully against, 0.5 = neutral, 1 = fully aligned.
f_alignment(int dir, int t, int v, int c) =>
    int n = 0
    int agree = 0
    int against = 0
    if useHtfTrend
        n += 1
        agree += t == dir ? 1 : 0
        against += t == -dir ? 1 : 0
    if useHtfVwap
        n += 1
        agree += v == dir ? 1 : 0
        against += v == -dir ? 1 : 0
    if useHtfCvd
        n += 1
        agree += c == dir ? 1 : 0
        against += c == -dir ? 1 : 0
    float sc = n > 0 ? (agree - against + n) / (2.0 * n) : 0.5
    [sc, agree, against, n]

// Evaluated INSIDE request.security on the HTF. Every value is shifted [1] and the
// call uses lookahead_on: TradingView's documented non-repainting pattern, so only
// fully closed HTF bars are ever used, identically on history and in realtime.
f_htf(int emaF, int emaS, int structLen, int rangeLen, int cvdLen) =>
    float eF = ta.ema(close, emaF)
    float eS = ta.ema(close, emaS)
    int   emaTrend = close > eF and eF > eS ? 1 : close < eF and eF < eS ? -1 : 0
    float phH = ta.pivothigh(high, structLen, structLen)
    float plH = ta.pivotlow(low, structLen, structLen)
    float lastPH = ta.valuewhen(not na(phH), phH, 0)
    float prevPH = ta.valuewhen(not na(phH), phH, 1)
    float lastPL = ta.valuewhen(not na(plH), plH, 0)
    float prevPL = ta.valuewhen(not na(plH), plH, 1)
    int   structTrend = lastPH > prevPH and lastPL > prevPL ? 1 : lastPH < prevPH and lastPL < prevPL ? -1 : 0
    bool  vwAnchor = timeframe.change(timeframe.isintraday ? "D" : "M")
    float vw = ta.vwap(hlc3, vwAnchor)
    int   vwapSide = close > vw ? 1 : close < vw ? -1 : 0
    float r = high - low
    float cd = r > 0 ? nz(volume) * ((close - low) - (high - close)) / r : 0.0
    float cvdH = ta.cum(cd)
    int   cvdSl = cvdH > cvdH[cvdLen] ? 1 : cvdH < cvdH[cvdLen] ? -1 : 0
    float rHi = ta.highest(high, rangeLen)
    float rLo = ta.lowest(low, rangeLen)
    float hAtrV = ta.atr(14)
    [emaTrend[1], structTrend[1], vwapSide[1], cvdSl[1], rHi[1], rLo[1], high[1], low[1], hAtrV[1]]

// Sweep lifecycle. step() runs once per confirmed bar AFTER the sweep bar and
// returns true when the trapped-trader confirmation candle prints.
method resetSweep(Sweep s) =>
    s.active     := false
    s.reclaimed  := false
    s.absorbed   := false
    s.sweepDelta := 0.0
    s

method step(Sweep s, int dir, float d, bool absHere, int maxReclaim, int maxConf, bool needFlip, bool useMid) =>
    bool fired = false
    if s.active and bar_index > s.startBar
        s.extreme := dir == 1 ? math.min(s.extreme, low) : math.max(s.extreme, high)
        if absHere
            s.absorbed := true
        bool inside = dir == 1 ? close > s.level : close < s.level
        if not s.reclaimed
            s.sweepDelta := s.sweepDelta + d
            if inside
                s.reclaimed  := true
                s.reclaimBar := bar_index
            else if bar_index - s.startBar >= maxReclaim
                s.resetSweep()
        else if not inside
            // Lost the reclaim: back to pending, still bounded by the reclaim window.
            s.reclaimed := false
            s.sweepDelta := s.sweepDelta + d
            if bar_index - s.startBar >= maxReclaim
                s.resetSweep()
        if s.active and s.reclaimed
            float ref    = useMid ? (dir == 1 ? math.max(s.level, s.mid) : math.min(s.level, s.mid)) : s.level
            bool  beyond = dir == 1 ? close > ref : close < ref
            bool  body   = dir == 1 ? close > open : close < open
            bool  flip   = not needFlip or (dir == 1 ? d > 0 : d < 0)
            if beyond and body and flip
                fired := true
            else if bar_index - s.reclaimBar >= maxConf
                s.resetSweep()
    fired

method update(SessTrack s, bool inS) =>
    if inS and not s.active
        s.hi := high
        s.lo := low
        s.active := true
    else if inS
        s.hi := math.max(s.hi, high)
        s.lo := math.min(s.lo, low)
    else if s.active
        s.lastHi := s.hi
        s.lastLo := s.lo
        s.active := false
    s

// ─────────────────────────────────────────────────────────────────────────────
// 5. CORE SERIES (computed on every bar, unconditionally)
// ─────────────────────────────────────────────────────────────────────────────
bool  barConf = barstate.isconfirmed
int   tfSec   = timeframe.in_seconds()
float atrRaw  = ta.atr(atrLen)
float atrS    = math.max(nz(atrRaw, high - low), syminfo.mintick)
float rng     = high - low
float clv     = rng > 0 ? (close - low) / rng : 0.5
bool  newDay  = timeframe.change("D")
bool  newWeek = timeframe.change("W")
bool  newMonth= timeframe.change("M")
float lowN    = ta.lowest(low, trailLen)
float highN   = ta.highest(high, trailLen)

// ─────────────────────────────────────────────────────────────────────────────
// 6. SESSIONS
// ─────────────────────────────────────────────────────────────────────────────
bool inAsia = not na(time(timeframe.period, asiaSess + ":1234567", sessTz))
bool inLon  = not na(time(timeframe.period, lonSess + ":1234567", sessTz))
bool inNy   = not na(time(timeframe.period, nySess + ":1234567", sessTz))
bool inNoTr = useNoTrade and not na(time(timeframe.period, noTradeSess + ":1234567", sessTz))
string sessName  = inLon and inNy ? "London/NY overlap" : inNy ? "New York" : inLon ? "London" : inAsia ? "Asia" : "Off-hours"
int    sessId    = inNy ? 2 : inLon ? 1 : inAsia ? 0 : 3   // stats bucket (overlap counts as NY)
bool   sessChange= sessName != sessName[1]
float  sessQ     = inLon and inNy ? 1.0 : inNy or inLon ? 0.8 : inAsia ? 0.5 : 0.25
bool   sessOk    = (inAsia and tradeAsia) or (inLon and tradeLon) or (inNy and tradeNy) or (not inAsia and not inLon and not inNy and tradeOff)

var SessTrack trAsia = SessTrack.new()
var SessTrack trLon  = SessTrack.new()
var SessTrack trNy   = SessTrack.new()
if barConf
    trAsia.update(inAsia)
    trLon.update(inLon)
    trNy.update(inNy)

// ─────────────────────────────────────────────────────────────────────────────
// 7. DELTA ENGINE (estimated)
// ─────────────────────────────────────────────────────────────────────────────
string ltfAuto = tfSec <= 60 ? "" : tfSec <= 3600 ? "1" : tfSec <= 14400 ? "5" : tfSec <= 86400 ? "15" : "60"
string ltfStr  = ltfIn == "Auto" ? ltfAuto : ltfIn
string ltfReq  = ltfStr == "" ? "1" : ltfStr
bool   useLtf  = ltfStr != "" and timeframe.in_seconds(ltfReq) < tfSec
[ltfO, ltfC, ltfV] = request.security_lower_tf(syminfo.tickerid, ltfReq, [open, close, volume], ignore_invalid_timeframe = true)
[ibDelta, ibCount] = f_intrabarDelta(ltfO, ltfC, ltfV)

float  candleDelta = rng > 0 ? nz(volume) * ((close - low) - (high - close)) / rng : 0.0
bool   ltfOk    = useLtf and ibCount > 1
float  delta    = ltfOk ? ibDelta : candleDelta
string deltaSrc = ltfOk ? "Intrabar " + ltfStr + (str.contains(ltfStr, "S") ? "" : "m") : "Candle estimate"

var float cumVol = 0.0
cumVol += nz(volume)
bool  noVolData = cumVol == 0.0

float deltaPct = nz(volume) > 0 ? 100.0 * delta / volume : 0.0
float vMean    = ta.sma(nz(volume), volZLen)
float vStd     = ta.stdev(nz(volume), volZLen)
float volZ     = vStd > 0 ? (nz(volume) - vMean) / vStd : 0.0

bool  cvdResetNow = cvdResetIn == "Daily" ? newDay : cvdResetIn == "Session" ? sessChange : false
var float cvd = 0.0
if cvdResetNow
    cvd := 0.0
cvd += delta
float cvdRaw   = ta.cum(delta)                      // never reset: used for slope/divergence
float cvdPast  = cvdRaw[cvdSlopeLen]
float cvdSlope = na(cvdPast) ? 0.0 : (cvdRaw - cvdPast) / cvdSlopeLen
int   cvdDir   = cvdSlope > 0 ? 1 : cvdSlope < 0 ? -1 : 0

float loPrev    = ta.lowest(low, divLen)[1]
float hiPrev    = ta.highest(high, divLen)[1]
float cvdLoPrev = ta.lowest(cvdRaw, divLen)[1]
float cvdHiPrev = ta.highest(cvdRaw, divLen)[1]
bool  bullDiv   = low < loPrev and cvdRaw > cvdLoPrev
bool  bearDiv   = high > hiPrev and cvdRaw < cvdHiPrev
int   bsBullDiv = nz(ta.barssince(bullDiv), 9999)
int   bsBearDiv = nz(ta.barssince(bearDiv), 9999)

// ─────────────────────────────────────────────────────────────────────────────
// 8. KEY LEVELS
// ─────────────────────────────────────────────────────────────────────────────
[pdh, pdl] = request.security(syminfo.tickerid, "D", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwh, pwl] = request.security(syminfo.tickerid, "W", [high[1], low[1]], lookahead = barmerge.lookahead_on)
[pmh, pml] = request.security(syminfo.tickerid, "M", [high[1], low[1]], lookahead = barmerge.lookahead_on)

bool vwapReset = vwapAnchorIn == "Weekly" ? newWeek : vwapAnchorIn == "Monthly" ? newMonth : newDay
[vwapV, vwU1, vwL1]  = ta.vwap(hlc3, vwapReset, vwapSd1)
[vwapV2, vwU2, vwL2] = ta.vwap(hlc3, vwapReset, vwapSd2)

// Swing pivots + equal highs/lows (resting liquidity pools)
float ph = ta.pivothigh(high, pivLen, pivLen)
float pl = ta.pivotlow(low, pivLen, pivLen)
var array<float> pivH = array.new_float()
var array<float> pivL = array.new_float()
var float eqhLevel = na
var float eqlLevel = na
if barConf
    if not na(ph)
        for p in pivH
            if math.abs(p - ph) <= eqTolAtr * atrS
                eqhLevel := math.max(p, ph)
        pivH.unshift(ph)
        if pivH.size() > maxPivots
            pivH.pop()
    if not na(pl)
        for p in pivL
            if math.abs(p - pl) <= eqTolAtr * atrS
                eqlLevel := math.min(p, pl)
        pivL.unshift(pl)
        if pivL.size() > maxPivots
            pivL.pop()
    // An equal high/low that price has accepted beyond (close past it) is spent.
    if not na(eqhLevel) and close > eqhLevel + eqTolAtr * atrS
        eqhLevel := na
    if not na(eqlLevel) and close < eqlLevel - eqTolAtr * atrS
        eqlLevel := na

// Volume profile approximation (each bar's volume binned at its HLC3), every 10 bars.
var float vpPoc = na
var float vpVah = na
var float vpVal = na
float vpHi = ta.highest(high, vpLen)
float vpLo = ta.lowest(low, vpLen)
if useVP and barConf and bar_index >= vpLen and bar_index % 10 == 0
    float rowH = (vpHi - vpLo) / vpRows
    if rowH > 0
        array<float> bins = array.new_float(vpRows, 0.0)
        for i = 0 to vpLen - 1
            int b = math.min(vpRows - 1, math.max(0, int(math.floor((hlc3[i] - vpLo) / rowH))))
            bins.set(b, bins.get(b) + nz(volume[i]))
        float total = bins.sum()
        if total > 0
            int   pocI = bins.indexof(bins.max())
            float acc  = bins.get(pocI)
            int   up   = pocI
            int   dn   = pocI
            while acc < total * vaPct / 100.0 and (up < vpRows - 1 or dn > 0)
                float vUp = up < vpRows - 1 ? bins.get(up + 1) : -1.0
                float vDn = dn > 0 ? bins.get(dn - 1) : -1.0
                if vUp >= vDn
                    up  += 1
                    acc += vUp
                else
                    dn  -= 1
                    acc += vDn
            vpPoc := vpLo + (pocI + 0.5) * rowH
            vpVah := vpLo + (up + 1) * rowH
            vpVal := vpLo + dn * rowH

// Assemble this bar's level set and merge into ranked zones.
var array<Level> levels = array.new<Level>()
levels.clear()
float maxDist = levelRangeAtr * atrS
f_addLevel(levels, pmh, "PMH", 5, usePM, close, maxDist)
f_addLevel(levels, pml, "PML", 5, usePM, close, maxDist)
f_addLevel(levels, pwh, "PWH", 4, usePW, close, maxDist)
f_addLevel(levels, pwl, "PWL", 4, usePW, close, maxDist)
f_addLevel(levels, pdh, "PDH", 3, usePD, close, maxDist)
f_addLevel(levels, pdl, "PDL", 3, usePD, close, maxDist)
f_addLevel(levels, eqhLevel, "EQH", 3, useEq, close, maxDist)
f_addLevel(levels, eqlLevel, "EQL", 3, useEq, close, maxDist)
f_addLevel(levels, trAsia.lastHi, "Asia H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trAsia.lastLo, "Asia L", 2, useSessLv, close, maxDist)
f_addLevel(levels, trLon.lastHi, "LDN H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trLon.lastLo, "LDN L", 2, useSessLv, close, maxDist)
f_addLevel(levels, trNy.lastHi, "NY H", 2, useSessLv, close, maxDist)
f_addLevel(levels, trNy.lastLo, "NY L", 2, useSessLv, close, maxDist)
f_addLevel(levels, vwapV, "VWAP", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vwU1, "VWAP+1σ", 1, useVwapLv, close, maxDist)
f_addLevel(levels, vwL1, "VWAP-1σ", 1, useVwapLv, close, maxDist)
f_addLevel(levels, vwU2, "VWAP+2σ", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vwL2, "VWAP-2σ", 2, useVwapLv, close, maxDist)
f_addLevel(levels, vpPoc, "POC", 2, useVP, close, maxDist)
f_addLevel(levels, vpVah, "VAH", 1, useVP, close, maxDist)
f_addLevel(levels, vpVal, "VAL", 1, useVP, close, maxDist)
if usePivots
    for p in pivH
        f_addLevel(levels, p, "Swing H", 1, true, close, maxDist)
    for p in pivL
        f_addLevel(levels, p, "Swing L", 1, true, close, maxDist)
array<Zone> zones = f_buildZones(levels, zoneTolAtr * atrS)

// ─────────────────────────────────────────────────────────────────────────────
// 9. HTF CONTEXT (non-repainting)
// ─────────────────────────────────────────────────────────────────────────────
string htfAuto  = tfSec <= 60 ? "15" : tfSec <= 300 ? "60" : tfSec <= 3600 ? "240" : tfSec <= 14400 ? "D" : "W"
string htf      = htfModeIn == "Auto" ? htfAuto : htfManual
bool   htfValid = timeframe.in_seconds(htf) > tfSec
[hEma, hStruct, hVwap, hCvd, hRangeHi, hRangeLo, hPrevHi, hPrevLo, hAtr] = request.security(syminfo.tickerid, htf, f_htf(htfEmaFast, htfEmaSlow, htfStructLen, htfRangeLen, htfCvdLen), lookahead = barmerge.lookahead_on)
int htfTrend = htfTrendType == "EMA stack" ? hEma : htfTrendType == "Market structure" ? hStruct : (hEma == hStruct ? hEma : 0)

// ─────────────────────────────────────────────────────────────────────────────
// 10. VOLATILITY REGIME + VOLUME
// ─────────────────────────────────────────────────────────────────────────────
float  atrBase   = ta.sma(atrS, regimeLen)
float  atrRatio  = f_safeDiv(atrS, atrBase, 1.0)
string volRegime = atrRatio < deadThr ? "Dead" : atrRatio > chaosThr ? "Chaotic" : "Normal"
float  volQ      = volRegime == "Normal" ? 1.0 : volRegime == "Chaotic" ? 0.5 : 0.3
bool   regimeOk  = not useRegime or volRegime == "Normal"
bool   volOk     = noVolData or nz(volume) >= minVolMult * nz(vMean)

// ─────────────────────────────────────────────────────────────────────────────
// 11. ABSORPTION
// ─────────────────────────────────────────────────────────────────────────────
var Sweep swL = Sweep.new()    // sweep of lows  → long setups
var Sweep swS = Sweep.new()    // sweep of highs → short setups
bool absBase    = not noVolData and volZ >= eAbsZ and math.abs(deltaPct) >= absDeltaPct and rng <= absRangeAtr * atrS
bool absBullRaw = absBase and delta < 0 and clv >= absClv          // heavy selling, price held
bool absBearRaw = absBase and delta > 0 and clv <= 1.0 - absClv    // heavy buying, price held
int  absZoneL   = f_touchZone(zones, 1, absTolAtr * atrS)
int  absZoneS   = f_touchZone(zones, -1, absTolAtr * atrS)
bool absBull    = absBullRaw and (absZoneL >= 0 or swL.active)
bool absBear    = absBearRaw and (absZoneS >= 0 or swS.active)
bool absBullC   = absBull and barConf
bool absBearC   = absBear and barConf

var AbsSetup abL = AbsSetup.new()
var AbsSetup abS = AbsSetup.new()
bool absSigL = false
bool absSigS = false
if barConf and useAbsSig
    // confirm, fail or expire existing absorption setups
    if abL.active and bar_index > abL.startBar
        if close < abL.lo
            abL.active := false
        else if close > abL.hi and delta > 0
            absSigL := true
        else if bar_index - abL.startBar >= absConfBars
            abL.active := false
    if abS.active and bar_index > abS.startBar
        if close > abS.hi
            abS.active := false
        else if close < abS.lo and delta < 0
            absSigS := true
        else if bar_index - abS.startBar >= absConfBars
            abS.active := false
    // arm new setups at a zone
    if absBull and absZoneL >= 0 and not abL.active
        Zone z = zones.get(absZoneL)
        abL.active   := true
        abL.startBar := bar_index
        abL.hi       := high
        abL.lo       := low
        abL.level    := z.bot
        abL.volZ     := volZ
        abL.zScore   := z.score
        abL.zCount   := z.count
        abL.zName    := z.names
        abL.zTop     := z.top
        abL.zBot     := z.bot
    if absBear and absZoneS >= 0 and not abS.active
        Zone z = zones.get(absZoneS)
        abS.active   := true
        abS.startBar := bar_index
        abS.hi       := high
        abS.lo       := low
        abS.level    := z.top
        abS.volZ     := volZ
        abS.zScore   := z.score
        abS.zCount   := z.count
        abS.zName    := z.names
        abS.zTop     := z.top
        abS.zBot     := z.bot

// ─────────────────────────────────────────────────────────────────────────────
// 12. SWEEPS + TRAPPED TRADERS
// ─────────────────────────────────────────────────────────────────────────────
bool useMid = confRefIn == "Sweep candle midpoint"
bool trapL = false
bool trapS = false
if barConf
    trapL := swL.step(1, delta, absBull, eMaxReclaim, maxConfirm, eNeedFlip, useMid)
    trapS := swS.step(-1, delta, absBear, eMaxReclaim, maxConfirm, eNeedFlip, useMid)

int  swIdxL = f_findSweep(zones, 1, eMinWick * atrS, close[1])
int  swIdxS = f_findSweep(zones, -1, eMinWick * atrS, close[1])
bool newBullSweep = false
bool newBearSweep = false
if barConf and swIdxL >= 0 and not swL.active and not trapL
    Zone z = zones.get(swIdxL)
    swL.active     := true
    swL.reclaimed  := close > z.bot
    swL.reclaimBar := close > z.bot ? bar_index : int(na)
    swL.absorbed   := absBullRaw
    swL.level      := z.bot
    swL.extreme    := low
    swL.mid        := (high + low) / 2
    swL.startBar   := bar_index
    swL.zScore     := z.score
    swL.zCount     := z.count
    swL.zName      := z.names
    swL.zTop       := z.top
    swL.zBot       := z.bot
    swL.depthAtr   := (z.bot - low) / atrS
    swL.sweepDelta := delta
    newBullSweep   := true
if barConf and swIdxS >= 0 and not swS.active and not trapS
    Zone z = zones.get(swIdxS)
    swS.active     := true
    swS.reclaimed  := close < z.top
    swS.reclaimBar := close < z.top ? bar_index : int(na)
    swS.absorbed   := absBearRaw
    swS.level      := z.top
    swS.extreme    := high
    swS.mid        := (high + low) / 2
    swS.startBar   := bar_index
    swS.zScore     := z.score
    swS.zCount     := z.count
    swS.zName      := z.names
    swS.zTop       := z.top
    swS.zBot       := z.bot
    swS.depthAtr   := (high - z.top) / atrS
    swS.sweepDelta := delta
    newBearSweep   := true

// ─────────────────────────────────────────────────────────────────────────────
// 13. CANDIDATE SETUP (trap preferred over absorption-only; long checked first)
// ─────────────────────────────────────────────────────────────────────────────
int    cDir = 0
string cType = ""
float  cLevel = na
float  cExtreme = na
int    cZScore = 0
int    cZCount = 0
string cZName = ""
float  cZTop = na
float  cZBot = na
float  cDepth = 0.0
bool   cAbs = false
float  cAbsZ = 0.0
int    cReclaimBars = 0
float  cSweepDelta = 0.0
if trapL
    cDir := 1
    cType := "Sweep + trap"
    cLevel := swL.level
    cExtreme := swL.extreme
    cZScore := swL.zScore
    cZCount := swL.zCount
    cZName := swL.zName
    cZTop := swL.zTop
    cZBot := swL.zBot
    cDepth := swL.depthAtr
    cAbs := swL.absorbed
    cReclaimBars := nz(swL.reclaimBar, bar_index) - swL.startBar
    cSweepDelta := swL.sweepDelta
else if absSigL
    cDir := 1
    cType := "Absorption"
    cLevel := abL.level
    cExtreme := abL.lo
    cZScore := abL.zScore
    cZCount := abL.zCount
    cZName := abL.zName
    cZTop := abL.zTop
    cZBot := abL.zBot
    cAbs := true
    cAbsZ := abL.volZ
else if trapS
    cDir := -1
    cType := "Sweep + trap"
    cLevel := swS.level
    cExtreme := swS.extreme
    cZScore := swS.zScore
    cZCount := swS.zCount
    cZName := swS.zName
    cZTop := swS.zTop
    cZBot := swS.zBot
    cDepth := swS.depthAtr
    cAbs := swS.absorbed
    cReclaimBars := nz(swS.reclaimBar, bar_index) - swS.startBar
    cSweepDelta := swS.sweepDelta
else if absSigS
    cDir := -1
    cType := "Absorption"
    cLevel := abS.level
    cExtreme := abS.hi
    cZScore := abS.zScore
    cZCount := abS.zCount
    cZName := abS.zName
    cZTop := abS.zTop
    cZBot := abS.zBot
    cAbs := true
    cAbsZ := abS.volZ

// Every trigger seen this bar is consumed, so none can fire twice.
if trapL
    swL.resetSweep()
if trapS
    swS.resetSweep()
if absSigL
    abL.active := false
if absSigS
    abS.active := false

// Zone touches over the lookback (only computed when a candidate exists).
int cTouches = 0
if cDir != 0
    float tTol = absTolAtr * atrS
    for i = 1 to touchLook
        if low[i] <= cZTop + tTol and high[i] >= cZBot - tTol
            cTouches += 1

// ─────────────────────────────────────────────────────────────────────────────
// 14. TRADE PLAN, SCORE, FILTERS
// ─────────────────────────────────────────────────────────────────────────────
bool   isLimit   = entryTypeIn == "Limit (50% of confirmation candle)"
float  pEntry    = na
float  pStop     = na
float  pRisk     = na
float  pT1       = na
float  pT2       = na
float  pT3       = na
float  pScore    = 0.0
string pGrade    = ""
string pWhyNot   = ""
string pBreak    = ""
float  pQty      = 0.0
float  pRiskCash = 0.0
float  pRRLiq    = na
bool   sigLong   = false
bool   sigShort  = false
var int lastSigBar   = -100000
var int sessSigCount = 0
if barConf and sessChange
    sessSigCount := 0

if cDir != 0
    float entryRaw = isLimit ? (high + low) / 2 : close
    pEntry := math.round_to_mintick(entryRaw)
    float stopBase = cDir == 1 ? math.min(cExtreme, low) : math.max(cExtreme, high)
    pStop  := math.round_to_mintick(stopBase - cDir * stopBufAtr * atrS)
    pRisk  := math.abs(pEntry - pStop)
    bool riskOk = pRisk >= syminfo.mintick and pRisk <= maxStopAtr * atrS
    if riskOk
        [t1, t2, t3] = f_targets(zones, cDir, pEntry, pRisk, tpModeIn == "Liquidity (auto)")
        pT1 := math.round_to_mintick(t1)
        pT2 := math.round_to_mintick(t2)
        pT3 := math.round_to_mintick(t3)
        float liq = f_nearestBeyond(zones, cDir, pEntry, 0.25 * pRisk)
        pRRLiq := na(liq) ? r3 : math.abs(liq - pEntry) / pRisk
    // factor scores (each 0-1)
    float conf    = f_clamp01((cZScore + cZCount) / 10.0)
    float touchQ  = f_clamp01(cTouches / 5.0)
    float qLevel  = f_clamp01(0.7 * conf + 0.3 * touchQ)
    bool  trapped = cDir == 1 ? cSweepDelta < 0 : cSweepDelta > 0
    float qSweep  = cType == "Sweep + trap" ? f_clamp01(0.6 * f_clamp01(cDepth / math.max(2 * eMinWick, 0.05)) + 0.4 * (1.0 - cReclaimBars / (eMaxReclaim + 1.0)) + (trapped ? 0.2 : 0.0)) : 0.0
    float qAbs    = cType == "Absorption" ? f_clamp01(cAbsZ / (eAbsZ * 1.5)) : cAbs ? 1.0 : f_clamp01(volZ / (2 * eAbsZ)) * 0.5
    float qDiv    = (cDir == 1 ? bsBullDiv : bsBearDiv) <= divRecent ? 1.0 : 0.0
    [alSc, alAgree, alAgainst, alN] = f_alignment(cDir, htfTrend, hVwap, hCvd)
    bool  counter = alSc < 0.5
    bool  nearExt = cDir == 1 ? (pEntry - hRangeLo) <= htfExtAtr * nz(hAtr, atrS) : (hRangeHi - pEntry) <= htfExtAtr * nz(hAtr, atrS)
    float qHtf    = counter and eMode == "Aggressive" ? alSc * 0.5 : alSc
    float qRR     = f_clamp01(nz(pRRLiq) / 3.0)
    float wSum    = wLevel + wSweep + wAbs + wDiv + wHtf + wSess + wVol + wRR
    float rawSc   = wSum > 0 ? 100.0 * (wLevel * qLevel + wSweep * qSweep + wAbs * qAbs + wDiv * qDiv + wHtf * qHtf + wSess * sessQ + wVol * volQ + wRR * qRR) / wSum : 0.0
    pScore := math.round(counter and eMode == "Aggressive" ? rawSc * aggrPenalty : rawSc)
    pGrade := f_grade(pScore)
    pBreak := "Level " + str.tostring(qLevel * 100, "#") + " · Sweep " + str.tostring(qSweep * 100, "#") + " · Abs " + str.tostring(qAbs * 100, "#") + " · Div " + str.tostring(qDiv * 100, "#") + " · HTF " + str.tostring(qHtf * 100, "#") + " · Sess " + str.tostring(sessQ * 100, "#") + " · Vol " + str.tostring(volQ * 100, "#") + " · R:R " + str.tostring(qRR * 100, "#")
    // HTF gate
    bool htfPass  = eMode == "Strict" ? (alN == 0 or alAgree == alN) : eMode == "Balanced" ? alSc >= 0.5 : (not counter or nearExt)
    bool keyBlock = false
    if useHtfKey
        float d1 = cDir == 1 ? hPrevHi - pEntry : pEntry - hPrevLo
        float d2 = cDir == 1 ? hRangeHi - pEntry : pEntry - hRangeLo
        keyBlock := (d1 > 0 and d1 < htfKeyAtr * atrS) or (d2 > 0 and d2 < htfKeyAtr * atrS)
    // position size
    float pv  = pointValIn > 0 ? pointValIn : syminfo.pointvalue
    float riskCashTarget = acctSize * riskPct / 100.0
    float qRaw = pRisk > 0 and pv > 0 ? riskCashTarget / (pRisk * pv) : 0.0
    float qCap = pEntry > 0 and pv > 0 ? acctSize * maxLev / (pEntry * pv) : qRaw
    pQty      := math.floor(math.min(qRaw, qCap) / qtyStep) * qtyStep
    pRiskCash := pQty * pRisk * pv
    // first reason a candidate is rejected (empty = signal)
    pWhyNot := not riskOk ? "stop too wide or too tight" : not htfPass ? "HTF disagrees (" + eMode + ")" : keyBlock ? "HTF level in the way" : not sessOk ? "session filtered" : inNoTr ? "no-trade window" : not regimeOk ? volRegime + " market" : not volOk ? "low volume" : bar_index - lastSigBar <= eCooldown ? "cooldown" : sessSigCount >= maxPerSess ? "session signal cap" : pScore < eMinScore ? "score " + str.tostring(pScore, "#") + " < " + str.tostring(eMinScore) : ""
    if pWhyNot == "" and barConf
        sigLong  := cDir == 1
        sigShort := cDir == -1
        lastSigBar := bar_index
        sessSigCount += 1
bool sigAny = sigLong or sigShort
int  sigDir = sigLong ? 1 : sigShort ? -1 : 0

// Last rejected candidate (dashboard context)
var string lastReject = ""
if barConf and cDir != 0 and not sigAny
    lastReject := (cDir == 1 ? "Long " : "Short ") + cType + " skipped: " + pWhyNot

// ═══════════════════════════════════════════════════════════════════════════════
// 15. STRATEGY ORDER LOGIC (strategy only)
// Same signals and plan as the indicator. Commission and slippage are set in the
// strategy() declaration (Properties tab). Note: TradingView's broker emulator
// decides the intrabar order of SL vs TP from the OHLC path (or Bar Magnifier),
// not the indicator's "stop first" rule, so results will differ slightly.
// ═══════════════════════════════════════════════════════════════════════════════
var int   sDir      = 0
var int   sSigBar   = na
var int   sEntryBar = na
var float sEntry    = na
var float sStop     = na
var float sT1       = na
var float sT2       = na
var float sT3       = na
var float sLevel    = na
var float sQ1       = 0.0
var float sQ2       = 0.0
var bool  sHit1     = false
var bool  sHit2     = false
float  pvS    = pointValIn > 0 ? pointValIn : syminfo.pointvalue
float  posAbs = math.abs(strategy.position_size)

// position closed (by an exit) → reset state
if sDir != 0 and not na(sEntryBar) and posAbs == 0
    sDir := 0
    sEntryBar := na

// record the fill
if sDir != 0 and na(sEntryBar) and posAbs > 0
    sEntryBar := bar_index
    sEntry    := strategy.position_avg_price

// unfilled limit expiry
if sDir != 0 and na(sEntryBar) and bar_index - sSigBar > limitExpiry
    strategy.cancel(sDir == 1 ? "L" : "S")
    sDir := 0

// new signal (flat and no pending order)
if barConf and sigAny and sDir == 0 and posAbs == 0
    float riskCash = strategy.equity * riskPct / 100.0
    float qRawS = pRisk > 0 and pvS > 0 ? riskCash / (pRisk * pvS) : 0.0
    float qCapS = pEntry > 0 and pvS > 0 ? strategy.equity * maxLev / (pEntry * pvS) : qRawS
    float q = math.floor(math.min(qRawS, qCapS) / qtyStep) * qtyStep
    if q > 0
        sDir      := sigDir
        sSigBar   := bar_index
        sEntryBar := na
        sEntry    := pEntry
        sStop     := pStop
        sT1       := pT1
        sT2       := pT2
        sT3       := pT3
        sLevel    := cLevel
        sQ1       := math.floor(q * math.min(tp1Pct, 100.0) / 100.0 / qtyStep) * qtyStep
        sQ2       := math.floor(q * math.max(0.0, math.min(tp2Pct, 100.0 - tp1Pct)) / 100.0 / qtyStep) * qtyStep
        sHit1     := false
        sHit2     := false
        strategy.entry(sDir == 1 ? "L" : "S", sDir == 1 ? strategy.long : strategy.short, qty = q, limit = isLimit ? pEntry : na, comment = pGrade + " " + str.tostring(pScore, "#"))

string sId = sDir == 1 ? "L" : "S"

// manage the open position
if sDir != 0 and not na(sEntryBar) and posAbs > 0 and bar_index > sEntryBar
    if not sHit1 and (sDir == 1 ? high >= sT1 : low <= sT1)
        sHit1 := true
        if moveBE
            sStop := sDir == 1 ? math.max(sStop, sEntry) : math.min(sStop, sEntry)
    if sHit1 and not sHit2 and (sDir == 1 ? high >= sT2 : low <= sT2)
        sHit2 := true
    bool invalidS = useInval and (sDir == 1 ? close < sLevel : close > sLevel)
    bool timeoutS = useTimeStop and bar_index - sEntryBar >= timeStopBars
    if invalidS or timeoutS
        strategy.close(sId, comment = invalidS ? "Invalidated" : "Time stop")
    else if trailModeIn != "None" and (not trailAfterTp1 or sHit1)
        float nsS = trailModeIn == "ATR" ? close - sDir * trailAtr * atrS : (sDir == 1 ? lowN : highN)
        if sDir * (nsS - sStop) > 0
            sStop := math.round_to_mintick(nsS)

// (re)issue the bracket every bar so stop moves take effect
if sDir != 0
    if not sHit1 and sQ1 > 0
        strategy.exit("TP1", sId, qty = sQ1, limit = sT1, stop = sStop, comment_profit = "TP1", comment_loss = "SL")
    if not sHit2 and sQ2 > 0
        strategy.exit("TP2", sId, qty = sQ2, limit = sT2, stop = sStop, comment_profit = "TP2", comment_loss = "SL")
    strategy.exit("TP3", sId, limit = sT3, stop = sStop, comment_profit = "TP3", comment_loss = "SL/BE")

// Trade state exposed to the shared dashboard / visuals.
int   posDir     = strategy.position_size > 0 ? 1 : strategy.position_size < 0 ? -1 : 0
bool  posPending = sDir != 0 and posAbs == 0
float posEntry   = sDir != 0 ? sEntry : na
float posStop    = sDir != 0 ? sStop : na
float posTp1     = sDir != 0 and not sHit1 ? sT1 : na
float posTp2     = sDir != 0 and not sHit2 ? sT2 : na
float posTp3     = sDir != 0 ? sT3 : na
int   posPendDir = posPending ? sDir : 0

// ═══════════════════════════════════════════════════════════════════════════════
// 16. VISUALS (shared). Everything price-based is drawn on the main chart with
// force_overlay = true. Object counts are capped and old objects deleted.
// ═══════════════════════════════════════════════════════════════════════════════
themeIn      = input.string("Dark", "Theme", options = ["Dark", "Light", "Custom"], group = G_VIS)
cBullIn      = input.color(#26A69A, "Custom bull", group = G_VIS, inline = "c1")
cBearIn      = input.color(#EF5350, "bear", group = G_VIS, inline = "c1")
cZoneIn      = input.color(#787B86, "zone", group = G_VIS, inline = "c2")
cAccentIn    = input.color(#F7C548, "accent", group = G_VIS, inline = "c2")
cTextIn      = input.color(#D1D4DC, "text", group = G_VIS, inline = "c3")
cPanelIn     = input.color(#131722, "panel", group = G_VIS, inline = "c3")
showZones    = input.bool(true, "Show zones", group = G_VIS, inline = "v1")
showLvLines  = input.bool(false, "Show individual level lines", group = G_VIS, inline = "v1")
maxZonesDraw = input.int(10, "Max zones drawn (nearest first)", minval = 1, maxval = 40, group = G_VIS)
zoneLookback = input.int(60, "Zone left extension (bars)", minval = 5, maxval = 400, group = G_VIS)
zoneMinW     = input.float(0.08, "Min zone height (ATR)", minval = 0.0, step = 0.01, group = G_VIS)
showVwapPlot = input.bool(true, "Plot anchored VWAP + bands", group = G_VIS)
showSweeps   = input.bool(true, "Sweep markers", group = G_VIS, inline = "v2")
showAbs      = input.bool(true, "Absorption markers", group = G_VIS, inline = "v2")
showSigLbl   = input.bool(true, "Signal labels", group = G_VIS, inline = "v3")
lblDetail    = input.string("Full plan", "", options = ["Full plan", "Compact"], group = G_VIS, inline = "v3")
lblSizeIn    = input.string("Small", "Label size", options = ["Tiny", "Small", "Normal"], group = G_VIS)
maxSigLbl    = input.int(100, "Max signal labels kept", minval = 10, maxval = 250, group = G_VIS)
showTrLines  = input.bool(true, "Active trade lines (entry/SL/TPs)", group = G_VIS)
showDash     = input.bool(true, "Show dashboard", group = G_DSH)
dashPosIn    = input.string("Top right", "Dashboard position", options = ["Top left", "Top right", "Bottom left", "Bottom right", "Middle right"], group = G_DSH)
dashSizeIn   = input.string("Small", "Dashboard text size", options = ["Tiny", "Small", "Normal", "Large"], group = G_DSH, tooltip = "Resizes the whole dashboard.")

color cBull   = themeIn == "Custom" ? cBullIn : themeIn == "Light" ? #089981 : #26A69A
color cBear   = themeIn == "Custom" ? cBearIn : themeIn == "Light" ? #F23645 : #EF5350
color cZone   = themeIn == "Custom" ? cZoneIn : themeIn == "Light" ? #9598A1 : #787B86
color cAccent = themeIn == "Custom" ? cAccentIn : themeIn == "Light" ? #B8860B : #F7C548
color cText   = themeIn == "Custom" ? cTextIn : themeIn == "Light" ? #131722 : #D1D4DC
color cPanel  = themeIn == "Custom" ? cPanelIn : themeIn == "Light" ? #F0F3FA : #131722

f_size(string s) => s == "Tiny" ? size.tiny : s == "Small" ? size.small : s == "Large" ? size.large : size.normal

f_pos(string s) => s == "Top left" ? position.top_left : s == "Bottom left" ? position.bottom_left : s == "Bottom right" ? position.bottom_right : s == "Middle right" ? position.middle_right : position.top_right

f_vol(float v) =>
    float a = math.abs(v)
    string s = a >= 1e9 ? str.tostring(v / 1e9, "#.##") + "B" : a >= 1e6 ? str.tostring(v / 1e6, "#.##") + "M" : a >= 1e3 ? str.tostring(v / 1e3, "#.#") + "K" : str.tostring(v, "#.##")
    s

// ── zones + level lines (redrawn on the last bar only) ───────────────────────
var array<box>   zBoxes  = array.new<box>()
var array<line>  lvLines = array.new<line>()
var array<label> lvLbls  = array.new<label>()
if barstate.islast
    for b in zBoxes
        b.delete()
    zBoxes.clear()
    for ln in lvLines
        ln.delete()
    lvLines.clear()
    for lb in lvLbls
        lb.delete()
    lvLbls.clear()
    int zCnt = zones.size()
    if showZones and zCnt > 0
        array<float> dist = array.new_float()
        for z in zones
            dist.push(math.abs((z.top + z.bot) / 2 - close))
        array<int> ordIdx = dist.sort_indices(order.ascending)
        int nDraw = math.min(maxZonesDraw, zCnt)
        for k = 0 to nDraw - 1
            Zone z = zones.get(ordIdx.get(k))
            float half = math.max(z.top - z.bot, zoneMinW * atrS) / 2
            float mid  = (z.top + z.bot) / 2
            color zc   = z.bot > close ? cBear : z.top < close ? cBull : cZone
            int   zTr  = z.score >= 8 ? 72 : z.score >= 5 ? 80 : 88
            zBoxes.push(box.new(bar_index - zoneLookback, mid + half, bar_index + 20, mid - half, border_color = color.new(zc, 55), bgcolor = color.new(zc, zTr), text = z.names + "  [" + str.tostring(z.score) + "]", text_color = cText, text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center, force_overlay = true))
    if showLvLines and levels.size() > 0
        int cnt = 0
        for lv in levels
            if cnt < 40
                lvLines.push(line.new(bar_index - zoneLookback, lv.price, bar_index + 20, lv.price, color = color.new(cZone, 30), style = line.style_dotted, force_overlay = true))
                lvLbls.push(label.new(bar_index + 20, lv.price, lv.name, style = label.style_none, textcolor = cZone, size = size.tiny, textalign = text.align_left, force_overlay = true))
                cnt += 1

// ── VWAP ─────────────────────────────────────────────────────────────────────
plot(showVwapPlot ? vwapV : na, "Anchored VWAP", color = cAccent, linewidth = 1, force_overlay = true)
plot(showVwapPlot ? vwU1 : na, "VWAP +SD1", color = color.new(cAccent, 70), force_overlay = true)
plot(showVwapPlot ? vwL1 : na, "VWAP -SD1", color = color.new(cAccent, 70), force_overlay = true)
plot(showVwapPlot ? vwU2 : na, "VWAP +SD2", color = color.new(cAccent, 50), style = plot.style_circles, force_overlay = true)
plot(showVwapPlot ? vwL2 : na, "VWAP -SD2", color = color.new(cAccent, 50), style = plot.style_circles, force_overlay = true)

// ── sweep + absorption markers (confirmed bars only) ─────────────────────────
plotshape(showSweeps and newBullSweep, "Sweep of lows", shape.xcross, location.belowbar, cBull, size = size.tiny, force_overlay = true)
plotshape(showSweeps and newBearSweep, "Sweep of highs", shape.xcross, location.abovebar, cBear, size = size.tiny, force_overlay = true)
plotshape(showAbs and absBullC, "Bullish absorption", shape.diamond, location.belowbar, color.new(cBull, 40), size = size.tiny, force_overlay = true)
plotshape(showAbs and absBearC, "Bearish absorption", shape.diamond, location.abovebar, color.new(cBear, 40), size = size.tiny, force_overlay = true)

// ── signal labels ────────────────────────────────────────────────────────────
var array<label> sigLbls = array.new<label>()
var string lastSigTxt = "None yet"
if sigAny
    float  rrT3 = f_safeDiv(math.abs(pT3 - pEntry), pRisk, 0.0)
    string head = (sigLong ? "▲ LONG " : "▼ SHORT ") + pGrade + " · " + str.tostring(pScore, "#")
    string body = "\n" + cType + " @ " + cZName + "\nEntry " + f_px(pEntry) + (isLimit ? " (limit)" : "") + "\nSL " + f_px(pStop) + "\nTP1 " + f_px(pT1) + " · TP2 " + f_px(pT2) + " · TP3 " + f_px(pT3) + "\nR:R 1:" + str.tostring(rrT3, "#.#") + " · Size " + str.tostring(pQty, "#.######") + " · Risk " + str.tostring(pRiskCash, "#.##")
    lastSigTxt := head + " @ " + f_px(pEntry)
    if showSigLbl
        sigLbls.push(label.new(bar_index, sigLong ? low : high, lblDetail == "Full plan" ? head + body : head, style = sigLong ? label.style_label_up : label.style_label_down, color = color.new(sigLong ? cBull : cBear, 8), textcolor = color.white, size = f_size(lblSizeIn), textalign = text.align_left, tooltip = head + body + "\nScore factors: " + pBreak, force_overlay = true))
        if sigLbls.size() > maxSigLbl
            sigLbls.shift().delete()

// ── active trade lines ───────────────────────────────────────────────────────
plot(showTrLines ? posEntry : na, "Trade entry", color = cText, style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posStop : na, "Trade stop", color = cBear, style = plot.style_linebr, linewidth = 2, force_overlay = true)
plot(showTrLines ? posTp1 : na, "Trade TP1", color = color.new(cBull, 40), style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posTp2 : na, "Trade TP2", color = color.new(cBull, 20), style = plot.style_linebr, force_overlay = true)
plot(showTrLines ? posTp3 : na, "Trade TP3", color = cBull, style = plot.style_linebr, linewidth = 2, force_overlay = true)

// ═══════════════════════════════════════════════════════════════════════════════
// 17. DASHBOARD
// ═══════════════════════════════════════════════════════════════════════════════
var table dash = table.new(f_pos(dashPosIn), 2, 13, bgcolor = color.new(cPanel, 8), frame_color = color.new(cZone, 50), frame_width = 1, border_color = color.new(cZone, 80), border_width = 1, force_overlay = true)

f_row(table t, int r, string k, string v, color vc, string sz) =>
    table.cell(t, 0, r, k, text_color = color.new(cText, 30), text_halign = text.align_left, text_size = sz)
    table.cell(t, 1, r, v, text_color = vc, text_halign = text.align_left, text_size = sz)

if barstate.islast and showDash
    string sz = f_size(dashSizeIn)
    // nearest zones above / below
    float  upP = na
    string upN = ""
    float  dnP = na
    string dnN = ""
    for z in zones
        if z.bot > close and (na(upP) or z.bot < upP)
            upP := z.bot
            upN := z.names
        if z.top < close and (na(dnP) or z.top > dnP)
            dnP := z.top
            dnN := z.names
    int    biasSc  = htfTrend + cvdDir + (close > vwapV ? 1 : close < vwapV ? -1 : 0)
    string biasTxt = biasSc >= 2 ? "▲ Bullish" : biasSc <= -2 ? "▼ Bearish" : "◆ Neutral"
    color  biasCol = biasSc >= 2 ? cBull : biasSc <= -2 ? cBear : cText
    string htfTxt  = (htfTrend == 1 ? "▲ Up" : htfTrend == -1 ? "▼ Down" : "◆ Flat") + " (" + htf + ") · VWAP " + (hVwap == 1 ? "above" : hVwap == -1 ? "below" : "—") + " · CVD " + (hCvd == 1 ? "▲" : hCvd == -1 ? "▼" : "—")
    string warn    = noVolData ? "⚠ No volume data: delta & absorption off" : not htfValid ? "⚠ HTF must be above chart TF" : tfSec > 3600 ? "⚠ TF too high for scalping logic" : ltfOk ? "OK · " + deltaSrc : "Candle-estimated delta (lower confidence)"
    color  warnCol = noVolData or not htfValid or tfSec > 3600 ? cBear : ltfOk ? cBull : cAccent
    string nextTp  = f_px(nz(posTp1, nz(posTp2, posTp3)))
    string todo    = posDir == 1 ? "In LONG: SL " + f_px(posStop) + " · next TP " + nextTp : posDir == -1 ? "In SHORT: SL " + f_px(posStop) + " · next TP " + nextTp : posPending ? "Limit " + (posPendDir == 1 ? "buy" : "sell") + " working at " + f_px(posEntry) : swL.active and swL.reclaimed ? "Lows swept (" + swL.zName + ") & reclaimed: wait for bullish confirmation close" : swS.active and swS.reclaimed ? "Highs swept (" + swS.zName + ") & reclaimed: wait for bearish confirmation close" : swL.active ? "Sweep below " + swL.zName + " in progress: wait for reclaim" : swS.active ? "Sweep above " + swS.zName + " in progress: wait for reclaim" : not regimeOk ? volRegime + " market: stand aside" : not sessOk ? "Outside your sessions: no trades" : "Wait for sweep of " + (dnN != "" ? dnN : "support") + " or " + (upN != "" ? upN : "resistance")
    f_row(dash, 0, "OFS Pro", "Estimated order flow · " + syminfo.ticker + " " + timeframe.period, cAccent, sz)
    f_row(dash, 1, "Bias", biasTxt, biasCol, sz)
    f_row(dash, 2, "HTF", htfTxt, htfTrend == 1 ? cBull : htfTrend == -1 ? cBear : cText, sz)
    f_row(dash, 3, "Session", sessName + (sessOk ? "" : " (filtered)") + (inNoTr ? " · NO-TRADE" : ""), sessOk and not inNoTr ? cText : cAccent, sz)
    f_row(dash, 4, "Volatility", volRegime + " · ATR×" + str.tostring(atrRatio, "#.##"), volRegime == "Normal" ? cBull : cAccent, sz)
    f_row(dash, 5, "Last signal", lastSigTxt, cText, sz)
    f_row(dash, 6, "Zone above", na(upP) ? "none in range" : upN + " · " + str.tostring((upP - close) / atrS, "#.##") + " ATR", cBear, sz)
    f_row(dash, 7, "Zone below", na(dnP) ? "none in range" : dnN + " · " + str.tostring((close - dnP) / atrS, "#.##") + " ATR", cBull, sz)
    f_row(dash, 8, "Est. Delta/CVD", "Δ " + f_vol(delta) + " (" + str.tostring(deltaPct, "#") + "%) · CVD " + (cvdDir == 1 ? "rising" : cvdDir == -1 ? "falling" : "flat") + " · vol z " + str.tostring(volZ, "#.#"), delta >= 0 ? cBull : cBear, sz)
    f_row(dash, 9, "Data quality", warn, warnCol, sz)
    f_row(dash, 10, "What to do now", todo, cAccent, sz)
    f_row(dash, 11, "Last skipped", lastReject == "" ? "—" : lastReject, color.new(cText, 30), sz)
    f_row(dash, 12, "Note", "Estimated Delta/CVD from OHLCV, not true order flow", color.new(cText, 40), sz)

04User guide

On the chart

ElementMeaning
Shaded zone boxMerged liquidity zone. Label lists its levels and [confluence score]. Red above price, green below, grey if price is inside. Darker = higher score.
Gold line + bandsAnchored VWAP, ±1σ (faint) and ±2σ (dots).
× below / above barSweep of lows / highs detected (stop run). Not a trade yet.
◆ diamondAbsorption: heavy estimated selling held (green, below) or heavy buying held (red, above).
▲ LONG / ▼ SHORT labelConfirmed signal: grade, score, setup type, zone, entry, SL, TP1–3, R:R to TP3, size and cash risk. Hover for the score breakdown.
Green / red boxesReward (entry→TP3) and risk (entry→SL) of the simulated trade.
Entry / SL / TP linesLive levels of the open simulated trade. SL moves to BE after TP1 and trails if enabled.
W +x.xxR / L −x.xxRClosed simulated trade with exit reason (TP1–3, SL, BE, Trail, Time, Invalidated).

Lower pane

Estimated delta histogram (default), estimated CVD, or backtest equity in R. Triangles mark delta/price divergence (price new N-bar extreme, CVD not).

Dashboard rows

RowRead it as
BiasHTF trend + CVD slope + side of VWAP. Two or three agreeing = bullish/bearish.
HTFHTF trend, HTF VWAP side, HTF CVD slope, with the HTF used.
SessionCurrent session; "(filtered)" if you've switched it off; NO-TRADE inside your window.
VolatilityATR ÷ its baseline. Dead < 0.6, Chaotic > 2.2 by default.
Last signalMost recent signal and entry.
Zone above / belowNearest liquidity and distance in ATR: your next sweep candidates and targets.
Est. Delta/CVDThis bar's estimated delta and %, CVD direction, volume z-score.
Data qualityIntrabar source, candle-estimate fallback, no-volume, HTF or timeframe warnings.
What to do nowPlain instruction from the state machine: wait for sweep, wait for reclaim, wait for confirmation, manage trade, stand aside.
Last skippedThe last setup that didn't qualify and why. Use it to tune filters.

Trading it

05Recommended settings

AssetChart → HTFPresetKey changesPoint value / qty step
BTCUSDT.P / ETHUSDT.P1m → 15mScalp 1m-5mCandle-estimate delta on 1m (or 15S with seconds data). Min score 65. Stop buffer 0.15 ATR. Max per session 3.1 / 0.001
BTC / ETH perps5m → 1hScalp 1m-5mIntrabar 1m. Liquidity targets. Trail ATR 1.5 after TP1. Sessions: London + NY.1 / 0.001
BTC / ETH perps15m → 4hIntraday 15mIntrabar 1m. Commission 0.05%. Time stop 20 bars.1 / 0.001
GC1! (gold futures)1m → 15mScalp 1m-5mSessions London + NY only. No-trade 08:25–08:45 NY on data days. Min sweep 0.12 ATR.100 / 1 (MGC: 10)
GC1! / XAUUSD5m → 1hIntraday 15m or ScalpVolume profile on. Zone tolerance 0.2 ATR. Strict mode in Asia.GC 100 / 1 · XAUUSD per broker
XAUUSD (CFD/spot)anyConservativeSpot volume is tick volume: treat delta as low confidence, raise absorption z to 2.5, rely more on sweeps + HTF.broker contract size
SI1! (silver futures)5m → 1hConservativeWider: stop buffer 0.2 ATR, max stop 3 ATR, cooldown 8. NY session.5000 / 1 (SIL: 1000)
XAGUSD15m → 4hConservativeThin liquidity: min volume 0.8×, Strict mode, R-multiple targets.broker contract size

Starting points, not optimised values. Validate each on your own data with the checklist.

06Testing checklist

  1. Compile: paste each script into a new Pine Editor tab, Save, Add to chart. Note any warning lines and fix before trusting results.
  2. Repaint test: Bar Replay on 1m/5m, step bar by bar. Every label, sweep and absorption mark must appear on a closed bar and never move or vanish. Compare a screenshot before and after reload.
  3. HTF test: on a 5m chart, check the HTF row only changes on the first 5m bar after each 1h close.
  4. Data fallback: load a symbol with no volume (e.g. an index). The dashboard must warn and absorption must stay off.
  5. Indicator vs strategy: same symbol/TF/inputs, compare trade count and net R. Differences should come only from intrabar ordering and sizing; turn on Bar Magnifier for a closer match.
  6. Costs: rerun with commission doubled and slippage 2–3 ticks. An edge that disappears is not an edge.
  7. Robustness: vary min sweep ±30%, absorption z ±0.5, min score ±10, reclaim bars ±1. Results should degrade gently, not flip.
  8. Out-of-sample: tune on one window (Test from/until), then test the untouched next window.
  9. Cross-market: same settings on BTC, ETH, GC and SI. Keep only what works on most.
  10. Sample size: ignore splits with fewer than 30 trades.
  11. Forward test: 2–4 weeks of alerts on paper or minimum size, logging each signal, before real risk.
  12. Alert check: create an "Any alert() function call" alert, confirm the JSON parses in your webhook receiver.

07Self-review: fixes made

Verified with a Pine v6 parser (both scripts parse cleanly). TradingView's own compiler is the final check; if it flags a line, paste the message and it gets fixed.