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
The prompt is strong: clear scope, honest about data limits, and it asks for self-review. These changes made it buildable in real Pine:
| Issue | What 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-repainting | The 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 script | Pine 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 values | Pine 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 data | Auto never picks seconds. Manual choice is allowed and documented. |
| Grade C was undefined | Added 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 TradingView | The 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 setups | Added '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 caveat | TradingView'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. |
volume × ((C−L) − (H−C)) / (H−L). CVD resets daily, per session or never; slope and divergence use an un-reset copy.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).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.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.Pine Editor → new indicator → paste → Save → Add to chart. Opens in its own pane; price visuals land on the main chart.
//@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)
Same signal and plan logic for the Strategy Tester. Set commission and slippage under Settings → Properties.
//@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)
| Element | Meaning |
|---|---|
| Shaded zone box | Merged liquidity zone. Label lists its levels and [confluence score]. Red above price, green below, grey if price is inside. Darker = higher score. |
| Gold line + bands | Anchored VWAP, ±1σ (faint) and ±2σ (dots). |
| × below / above bar | Sweep of lows / highs detected (stop run). Not a trade yet. |
| ◆ diamond | Absorption: heavy estimated selling held (green, below) or heavy buying held (red, above). |
| ▲ LONG / ▼ SHORT label | Confirmed 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 boxes | Reward (entry→TP3) and risk (entry→SL) of the simulated trade. |
| Entry / SL / TP lines | Live levels of the open simulated trade. SL moves to BE after TP1 and trails if enabled. |
| W +x.xxR / L −x.xxR | Closed simulated trade with exit reason (TP1–3, SL, BE, Trail, Time, Invalidated). |
Estimated delta histogram (default), estimated CVD, or backtest equity in R. Triangles mark delta/price divergence (price new N-bar extreme, CVD not).
| Row | Read it as |
|---|---|
| Bias | HTF trend + CVD slope + side of VWAP. Two or three agreeing = bullish/bearish. |
| HTF | HTF trend, HTF VWAP side, HTF CVD slope, with the HTF used. |
| Session | Current session; "(filtered)" if you've switched it off; NO-TRADE inside your window. |
| Volatility | ATR ÷ its baseline. Dead < 0.6, Chaotic > 2.2 by default. |
| Last signal | Most recent signal and entry. |
| Zone above / below | Nearest liquidity and distance in ATR: your next sweep candidates and targets. |
| Est. Delta/CVD | This bar's estimated delta and %, CVD direction, volume z-score. |
| Data quality | Intrabar source, candle-estimate fallback, no-volume, HTF or timeframe warnings. |
| What to do now | Plain instruction from the state machine: wait for sweep, wait for reclaim, wait for confirmation, manage trade, stand aside. |
| Last skipped | The last setup that didn't qualify and why. Use it to tune filters. |
| Asset | Chart → HTF | Preset | Key changes | Point value / qty step |
|---|---|---|---|---|
| BTCUSDT.P / ETHUSDT.P | 1m → 15m | Scalp 1m-5m | Candle-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 perps | 5m → 1h | Scalp 1m-5m | Intrabar 1m. Liquidity targets. Trail ATR 1.5 after TP1. Sessions: London + NY. | 1 / 0.001 |
| BTC / ETH perps | 15m → 4h | Intraday 15m | Intrabar 1m. Commission 0.05%. Time stop 20 bars. | 1 / 0.001 |
| GC1! (gold futures) | 1m → 15m | Scalp 1m-5m | Sessions London + NY only. No-trade 08:25–08:45 NY on data days. Min sweep 0.12 ATR. | 100 / 1 (MGC: 10) |
| GC1! / XAUUSD | 5m → 1h | Intraday 15m or Scalp | Volume profile on. Zone tolerance 0.2 ATR. Strict mode in Asia. | GC 100 / 1 · XAUUSD per broker |
| XAUUSD (CFD/spot) | any | Conservative | Spot 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 → 1h | Conservative | Wider: stop buffer 0.2 ATR, max stop 3 ATR, cooldown 8. NY session. | 5000 / 1 (SIL: 1000) |
| XAGUSD | 15m → 4h | Conservative | Thin 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.
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.