Sweep Desk v2 · Free & open-source
Price breaks a level. What happens next?
Follow the sweep and qualifying inversion close. Read the accepted entry, structural stop and selected target.
Draws on your price chart. The indicator does not place orders.

- Mark the pools
Prior highs and lows worth watching.
- Confirm the sweep
Price crosses, returns, then confirms a shift.
- Map entry and exits
An entry zone, invalidation and target levels.
What is Sweep Desk?
Sweep Desk is a free TradingView indicator that follows a price sweep through a reference level, then checks for a qualifying gap inversion or structure shift. Its dashboard explains the waiting state and accepted reference plan, including the structural stop and target. A separate Strategy Lab script tests execution assumptions.
Does every liquidity sweep create an entry?
No. A qualifying sweep starts a sequence; confirmation, context and entry checks must also pass. The default confirmation is a close through an opposing fair value gap. A reference level being crossed alone does not accept a plan. Read the sweep-to-entry sequence
Why does Sweep Desk wait for higher-timeframe data?
The default setup requires completed higher-timeframe gap context and an eligible first touch. Its sixty-minute source must exceed the chart timeframe. Check the New setups data row and Co-Pilot for the prerequisite that is still missing. Read the dashboard prerequisites
Must price return to the inverted gap before entry?
Not in the default Confirmation close mode, which accepts a qualifying confirmation close. Optional modes wait for a later close inside the zone or an edge or midpoint limit. Their arming candle cannot satisfy the later-entry requirement. Compare the entry modes
From a marked level to clear rules.
Built from a trader’s question: how do marked levels become entry and exit rules?
Inspired by iFVG Ultimate+ by TakingProphets / DodgysDD. These are original public-concept rules; private grades, LRL weights and discretionary refinements are not reproduced.
Start with an earlier low.
The tool marks 64 from earlier price history. It is a level to watch, not proof of orders waiting there.
Fictional prices and supplied history. Defaults also require a first touch of a fresh area from a higher timeframe. See the extra lesson below.
How could this help a trade?
Repeatable rules define qualification, entry and exit. Results depend on price, size and costs. A valid entry can lose.
Which close accepts the plan?
Compare the default confirmation-close entry with an optional return inside.
An earlier low is already marked.
The reference at 64 comes from earlier history. Only one new candle is shown so far.
Entry methods only. The required higher-timeframe check is switched off here; explore it below.
What is supplied in this example?
Fictional prices; previously confirmed low 64 and high 160; ATR 10. Single-gap long-side inversion; Confirmation close or the labelled Close inside alternative. HTF delivery, SMT, momentum, vote minimum, volume and time gates are disabled to isolate entry timing. These six candles do not calculate earlier pools or ATR. The simulator below recalculates ATR from longer fictional history.
Why might it still wait? How do risk and exits work?
- First-touch age
- —
- Required check
- Waiting
- Entry signal
- None here
Start with the larger chart.
A completed bullish one-hour gap, 80–90, is supplied. Wait for the first later five-minute candle close.
Supplied history, aligned time boundaries and any accepted-setup event. Only first-touch context is simulated; other entry checks remain separate.
Does a gap mean nobody traded there?
No. It compares the first and third wicks. The middle candle can trade through the strip. It does not record missing transactions.
Find the same steps on your chart.
Read state → zone → plan.

1 The state row
SCANNING watches levels. SWEPT waits for confirmation. ARMED waits for entry. FILLED tracks the paper trade.
2 The entry zone
Confirmation identifies this fixed area. Default entry uses that confirming close; return modes explicitly wait.
3 The plan
Read entry, stop and selected final target together. The stop can move after milestones.
Explore the full price simulator
Edit the sweep, confirmation, later close and entry mode. This long-side example calculates ATR from fictional history, with supplied levels and context gates off.
Which close begins the plan?
Compare the default confirmation-close entry with an optional return inside.
Closed candles. The last is a later observation.
How the calculation works & more controls
Raid: wick-through ≥ 0.05 ATR, close strictly above pool. Inversion: close strictly above gap, push ≥ one ATR. Later fills include both edges.
Confirmation: twelve bars allowed. Close-entry outcomes begin on later bars. Close inside is an alternative, with thirty armed bars and fill checked before expiry. Its entry recalculates risk from that acceptance close; accepted geometry stays fixed.
Open plan: entry 94.00, stop 54.00, targets 134.00 / 174.00 / 214.00. Opposing liquidity: none qualifies.
Illustrative inputs · changing a slider recalculates this example · no market feed
Paste it into TradingView.
Both complete Pine v6 scripts are free, under MIT.
- 1
Open Pine Editor
Create a new indicator on your chart.
- 2
Paste the code
Copy below and replace the sample code.
- 3
Add to chart
Save it. Keep defaults and choose an HTF FVG source above your chart.
The indicator
Levels, entry plans, dashboard, Co-Pilot and alerts.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// SWEEP DESK — Liquidity · Shift · FVG · Pine Script v6 · v2.0.0
// The liquidity-sweep model made mechanical and honest, with closed-bar structural references and explicit paper execution assumptions.
//
// THE SEQUENCE (every step commits on the CLOSED bar):
// 1 · POOLS resting liquidity = confirmed swing highs/lows, equal highs/lows, killzone-session
// highs/lows, previous day / week highs/lows. Each pool is resting until it is swept.
// 2 · SWEEP a bar wicks THROUGH a pool by ≥ X ATR and CLOSES back inside → the pool is raided.
// 3 · CONFIRM within N bars, either (a) iFVG — a bar CLOSES THROUGH an opposing imbalance printed near the
// raid, so that gap INVERTS and becomes the entry zone (an original, explicitly defined inversion model), or (b) MSS — a
// close beyond the last opposing short-term swing with displacement (leg from the extreme ≥ k ATR).
// 4 · ZONE iFVG: the inverted gap · MSS: the 3-candle imbalance printed inside the shift leg.
// 5 · ENTRY confirmation close (v2 default); optional later close inside or resting edge / midpoint limit.
// 6 · STOP the sweep extreme ± a buffer — the structural invalidation, not a guess.
// 7 · TARGET frozen 1R / 2R / 3R milestones; optional nearest qualifying fresh opposing pool; explicit TP3 fallback.
//
// WHY IT EXISTS "Finding the liquidity is easy; the entries and exits are where the gold is." This
// build answers that with a state machine you can read on the dashboard, stop-first
// accounting, and a track record that counts every setup it ever armed.
// STATUS v2.0.0 · reliability revision · verification scope in docs/build-report.md · paper
// ledger has NO costs · the Strategy Lab measures independent broker fills with costs · not a validated edge.
//
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
indicator("Sweep Desk — Liquidity · Shift · FVG", "SweepDesk", overlay = true, behind_chart = false, max_bars_back = 3000, max_lines_count = 250, max_labels_count = 200, max_boxes_count = 120)
// ─────────────────────────────────────────────────────────────────── INPUTS · liquidity pools
gP = "Liquidity pools"
inPivLen = input.int(5, "Swing pivot length", minval = 2, maxval = 30, group = gP,
tooltip = "Bars on each side that confirm a swing high / low. A swing becomes a resting pool N bars after it forms — no repainting, some lag.", display = display.none)
inEqTol = input.float(0.15, "Equal highs / lows tolerance (× ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = gP,
tooltip = "A new swing within this distance of a resting pool merges into it as an EQUAL high / low (EQH / EQL) — the pools the market loves to raid.", display = display.none)
inPoolSes = input.bool(true, "Killzone session highs / lows", group = gP, tooltip = "When a killzone ends, its high and low become resting pools.", display = display.none)
inPoolPD = input.bool(true, "Previous day high / low", group = gP, display = display.none)
inPoolPW = input.bool(true, "Previous week high / low", group = gP, display = display.none)
inMaxPools = input.int(10, "Max resting pools per side", minval = 3, maxval = 30, group = gP, tooltip = "Oldest swing pools are dropped beyond this (day / week pools are protected; session pools follow the age limit).", display = display.none)
inPoolAge = input.int(400, "Drop a resting swing / session pool after N bars", minval = 50, maxval = 3000, group = gP, display = display.none)
inHtfOn = input.bool(true, "Higher-timeframe swing pools", group = gP, tooltip = "Confirmed swing highs / lows from a higher timeframe rest as pools too (kind HTF). Read from the last CLOSED higher-timeframe bar — no lookahead.", display = display.none)
inHtf = input.timeframe("60", " Higher timeframe", group = gP, display = display.none)
inHtfLen = input.int(5, " HTF pivot length", minval = 2, maxval = 20, group = gP, display = display.none)
inPoolNear = input.float(8.0, "Draw pools within N ATR of price", minval = 1.0, maxval = 50.0, step = 0.5, group = gP, tooltip = "Display only — every pool is still tracked and can be a target. Keeps the chart focused on the liquidity that matters now.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · sweep filters
gS = "Sweep filters (better sweeps, fewer fakes)"
inSweepMin = input.float(0.05, "Wick beyond the pool ≥ ATR ×", minval = 0.0, maxval = 2.0, step = 0.05, group = gS,
tooltip = "A touch is not a sweep. The wick must go through the pool by at least this much ATR and the bar must CLOSE back on the pool's side.", display = display.none)
inSweepVol = input.bool(false, "Require a volume spike on the sweep bar", group = gS, display = display.none)
inSweepVolK = input.float(1.3, "Volume ≥ SMA(20) ×", minval = 1.0, maxval = 5.0, step = 0.1, group = gS, display = display.none)
inSweepKz = input.bool(false, "Only sweeps inside a killzone", group = gS, tooltip = "Off = any time. On = the sweep bar must fall inside one of the killzones below.", display = display.none)
inPoolMin = input.string("Any pool", "A setup needs a sweep of", options = ["Any pool", "Strong pools only"], group = gS,
tooltip = "Strong pools = equal highs/lows, killzone session extremes, higher-timeframe swings, previous day / week levels. Plain swing pools are still tracked and drawn, but sweeping one does not start a setup.", display = display.none)
inDir = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gS, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · shift + FVG
gM = "Confirmation (iFVG / MSS) + zone"
inConfirm = input.string("FVG inversion (iFVG)", "Confirmation", options = ["FVG inversion (iFVG)", "Structure shift (MSS)", "Either"], group = gM,
tooltip = "How the reversal is confirmed after the raid. iFVG = a bar CLOSES through an opposing imbalance printed near the raid — that gap inverts and becomes the entry zone (the inversion model). MSS = a close beyond the last short-term swing with displacement, then the imbalance in that leg. Either = whichever prints first.", display = display.none)
inIfvgBack = input.int(40, "iFVG: the gap may have formed up to N bars before the sweep", minval = 5, maxval = 200, group = gM, display = display.none)
inShiftLeg = input.int(3, "Short-term swing length (the swing the shift must break)", minval = 2, maxval = 10, group = gM, display = display.none)
inShiftWin = input.int(12, "Confirmation must happen within N bars of the sweep", minval = 3, maxval = 60, group = gM, display = display.none)
inDispAtr = input.float(1.0, "Displacement: leg from the sweep extreme ≥ ATR ×", minval = 0.2, maxval = 5.0, step = 0.1, group = gM,
tooltip = "The confirming bar's close must be at least this far (in ATR) from the sweep extreme — a real push, not a drift. Applies to both models.", display = display.none)
inFvgMin = input.float(0.10, "FVG size ≥ ATR ×", minval = 0.0, maxval = 2.0, step = 0.05, group = gM, display = display.none)
inFvgPick = input.string("Nearest", "Which FVG in the leg", options = ["Nearest", "Deepest"], group = gM,
tooltip = "Nearest = the last imbalance printed in the shift leg (price reaches it first). Deepest = the first one (bigger discount, fills less often).", display = display.none)
inFvgWin = input.int(8, "MSS: the FVG must exist within N bars after the shift", minval = 1, maxval = 30, group = gM, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · entry / stop / targets
gE = "Entry · stop · targets"
inEntryMode = input.string("Confirmation close", "Entry", options = ["Confirmation close", "FVG edge", "CE (50%)", "Close inside"], group = gE,
tooltip = "Confirmation close = enter at the confirmed inversion / MSS-zone acceptance close. This is the v2 factory choice. Close entries cannot exit on earlier wicks. FVG edge = limit at the near edge of the gap. CE = limit at the gap's midpoint (consequent encroachment). Close inside = wait for a bar to close inside the gap, enter at that close.", display = display.none)
inSlMode = input.string("Sweep extreme", "Stop anchor", options = ["Auto", "Sweep extreme", "Zone far edge"], group = gE,
tooltip = "Sweep extreme = the structural invalidation: if price trades back through the raided low / high, the idea is wrong. Zone far edge = the other side of the entry zone — tighter, more stop-outs. Auto = the sweep extreme unless it sits more than N ATR from the entry (a stop that far is no longer a structural stop for this timeframe), then the zone far edge.", display = display.none)
inSlMaxAtr = input.float(2.5, "Auto: sweep extreme allowed up to N ATR from the entry", minval = 0.5, maxval = 10.0, step = 0.5, group = gE, display = display.none)
inSlBuf = input.float(0.10, "Stop buffer (× ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = gE, display = display.none)
inTp1 = input.float(1.0, "TP1 (R)", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inTp2 = input.float(2.0, "TP2 (R)", minval = 0.5, maxval = 20, step = 0.1, group = gE, display = display.none)
inTp3 = input.float(3.0, "TP3 (R)", minval = 0.5, maxval = 30, step = 0.1, group = gE, display = display.none)
inTpLiqMinR = input.float(2.0, "Liquidity target must be ≥ R", minval = 0.3, maxval = 10, step = 0.1, group = gE,
tooltip = "The opposing-liquidity target is the nearest resting pool on the other side that is at least this many R away.", display = display.none)
inPrimary = input.string("R ladder (TP3)", "Final target", options = ["Liquidity", "R ladder (TP3)"], group = gE,
tooltip = "Which target closes the trade. Liquidity = the opposing pool (falls back to TP3 when none qualifies). TP1 / TP2 are milestones either way.", display = display.none)
inBE = input.bool(true, "TP1 → stop to breakeven", group = gE, display = display.none)
inExpire = input.int(30, "Unfilled setup expires after N bars", minval = 3, maxval = 300, group = gE, display = display.none)
inMaxTrade = input.int(200, "Open trade times out after N bars", minval = 10, maxval = 2000, group = gE, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · killzones
gK = "Killzones (exchange time)"
inTZ = input.string("America/New_York", "Timezone", group = gK, tooltip = "IANA name. ICT killzones are defined in New York time.", display = display.none)
inKz1 = input.session("0200-0500", "London", group = gK, display = display.none)
inKz2 = input.session("0830-1100", "New York AM", group = gK, display = display.none)
inKz3 = input.session("1330-1600", "New York PM", group = gK, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · style / panels / alerts
gV = "Style"
inView = input.string("Clean", "View preset", options = ["Minimal", "Clean", "Pro", "Full"], group = gV,
tooltip = "Minimal = the active setup only. Clean = + resting pools + setup FVG + MSS. Pro = + every FVG, sweep tags, pool labels. Full = + killzone shading.", display = display.none)
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inZones = input.bool(true, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(true, "Tint candles while a trade is open", group = gV, display = display.none)
inGlow = input.bool(true, "Glow on strong pools (EQ · session · HTF · day / week)", group = gV, display = display.none)
inExtend = input.int(20, "Pool label offset (bars)", minval = 5, maxval = 200, group = gV, display = display.none)
gD = "Dashboard"
inDash = input.bool(true, "Show dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inZebra = input.bool(true, "Row stripes", group = gD, display = display.none)
gN = "Narrative · Co-Pilot"
inNarr = input.string("Standard", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 120, group = gN, display = display.none)
gA = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gA, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// v2 additions follow all existing controls to preserve prior input identities.
gV2 = "v2 · transparent confirmation / target rules"
inGapMode = input.string("Single", "iFVG mode", options = ["Single", "Series"], group = gV2, display = display.none, tooltip = "Single = individual three-candle gap. Series = a union of same-direction gaps born on consecutive chart bars, before inversion; the whole union must be crossed by a later close. This precise adjacency rule is original, not a claim of private parity.")
inMacro = input.string("Off", "Confirmation macro window", options = ["Off", "45–15", "50–10", "US open 30m"], group = gV2, display = display.none, tooltip = "Evaluated at the confirmation candle CLOSE in the selected timezone. 45–15: minute 45 through 14; 50–10: minute 50 through 09; US open 30m: 09:30 inclusive to 10:00 exclusive. No exchange holiday calendar.")
inTargetQuality = input.string("Any fresh pool", "Opposing target quality", options = ["Any fresh pool", "Clustered (2+ touches)"], group = gV2, display = display.none, tooltip = "Clustered requires two separately confirmed chart swing observations merged into one still-fresh EQ pool. This is a transparent level cluster, not a trendline detector. The minimum-R rule and explicit TP3 fallback still apply.")
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
bool vClean = inView != "Minimal"
bool vPro = inView == "Pro" or inView == "Full"
bool vFull = inView == "Full"
fmt(float x) => str.tostring(x, format.mintick)
clamp01(float x) => math.max(0.0, math.min(1.0, x))
// ── PURE RELIABILITY HELPERS BEGIN · shared by literal Pine fixtures and broker adapter
sdSign(float x) =>
na(x) ? 0 : int(nz(x / math.abs(x), 0))
sdCompare(float lhs, float rhs) =>
float tolerance = 4 * 2.220446049250313e-16 * math.max(math.abs(lhs), math.abs(rhs))
sdSign(math.abs(lhs - rhs) - tolerance) <= 0 ? 0 : sdSign(lhs - rhs)
sdBarValid(float o, float h, float l, float c) =>
not na(o) and not na(h) and not na(l) and not na(c) and sdSign(h - l) >= 0 and sdSign(o - l) >= 0 and sdSign(h - o) >= 0 and sdSign(c - l) >= 0 and sdSign(h - c) >= 0
sdTickOut(float px, int roundingDirection, float tick) =>
float raw = px / tick
bool supported = not na(raw) and math.abs(raw) <= 9e18
float nearest = supported ? math.round(raw) : na
// Never absorb a genuine fraction of a tick at a large absolute coordinate.
float tolerance = math.min(1e-5, 8 * 2.220446049250313e-16 * math.max(1, math.abs(raw)))
float normalized = sdSign(math.abs(raw - nearest) - tolerance) <= 0 ? nearest : raw
supported ? (roundingDirection == 1 ? math.ceil(normalized) : math.floor(normalized)) * tick : float(na)
sdDistance(float fromPrice, float toPrice, int direction, float tick) =>
float raw = (toPrice - fromPrice) * direction
float ticks = raw / tick
float nearest = math.round(ticks)
float tolerance = math.min(tick * 1e-5, 8 * 2.220446049250313e-16 * math.max(math.abs(fromPrice), math.abs(toPrice)))
sdSign(math.abs(raw - nearest * tick) - tolerance) <= 0 ? nearest * tick : raw
sdPoolObservation(int side, float level, float h, float l, float c, float minimumDepth) =>
bool traversed = side == 1 ? sdCompare(h, level) > 0 : sdCompare(l, level) < 0
bool raid = traversed and (side == 1 ? sdCompare(h, level + minimumDepth) >= 0 and sdCompare(c, level) < 0 : sdCompare(l, level - minimumDepth) <= 0 and sdCompare(c, level) > 0)
[traversed, raid]
sdPlanGeometry(int direction, float entry, float anchor, float atrValue, float tick, float buffer, float r1, float r2, float r3) =>
bool available = not na(entry) and not na(anchor) and not na(atrValue) and sdSign(atrValue) > 0 and sdSign(tick) > 0
bool ordered = r1 > 0 and sdCompare(r2, r1) > 0 and sdCompare(r3, r2) > 0
float rawStop = anchor - direction * buffer * atrValue
float minimumStop = entry - direction * tick
float sl = sdTickOut(direction == 1 ? math.min(rawStop, minimumStop) : math.max(rawStop, minimumStop), -direction, tick)
float risk = sdDistance(sl, entry, direction, tick)
float t1 = sdTickOut(entry + direction * r1 * risk, direction, tick)
float t2 = sdTickOut(entry + direction * r2 * risk, direction, tick)
float t3 = sdTickOut(entry + direction * r3 * risk, direction, tick)
bool valid = available and ordered and sdCompare(risk, tick) >= 0 and sdCompare((t1 - entry) * direction, 0) > 0 and sdCompare((t2 - t1) * direction, 0) > 0 and sdCompare((t3 - t2) * direction, 0) > 0
string reason = not available ? "Unavailable price / ATR / tick" : not ordered ? "Require 0 < TP1 < TP2 < TP3" : not valid ? "Rounded plan geometry invalid" : "Plan valid"
[sl, risk, t1, t2, t3, valid, reason]
sdOutcome(int direction, float stop, float target, float o, float h, float l, float c, int entryBar, int currentBar, int maximumHold) =>
bool ended = false
float price = na
string why = ""
bool ambiguous = false
if currentBar > entryBar and sdBarValid(o, h, l, c)
bool stopGap = sdCompare(o, stop) * direction <= 0
bool targetGap = sdCompare(o, target) * direction >= 0
bool stopTouch = direction == 1 ? sdCompare(l, stop) <= 0 : sdCompare(h, stop) >= 0
bool targetTouch = direction == 1 ? sdCompare(h, target) >= 0 : sdCompare(l, target) <= 0
if stopGap
price := o
why := "GAP STOP"
else if targetGap
price := target
why := "TARGET"
else if stopTouch
price := stop
why := "STOP"
ambiguous := targetTouch
else if targetTouch
price := target
why := "TARGET"
else if currentBar - entryBar >= maximumHold
price := c
why := "TIME"
ended := not na(price)
[ended, price, why, ambiguous]
sdSeriesJoin(bool enabled, int direction, int previousDirection, int birth, int previousBirth, bool previousInverted) =>
enabled and not previousInverted and direction == previousDirection and birth == previousBirth + 1
sdInversion(int direction, float closedPrice, float top, float bottom) =>
direction == 1 ? sdCompare(closedPrice, bottom) < 0 : sdCompare(closedPrice, top) > 0
sdEscape(string value) =>
str.replace_all(str.replace_all(str.replace_all(str.replace_all(value, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\t", "\\t")
sdNumber(float value) =>
string result = "null"
if not na(value)
if sdSign(value) == 0
result := "0"
else
int exponent = int(math.floor(math.log10(math.abs(value))))
result := exponent < -6 or exponent > 12 ? str.tostring(value / math.pow(10, exponent), "0.################") + "e" + str.tostring(exponent) : str.tostring(value, "0.################")
result
sdMacroWindow(string mode, int localHour, int localMinute) =>
mode == "Off" or (mode == "45–15" ? localMinute >= 45 or localMinute < 15 : mode == "50–10" ? localMinute >= 50 or localMinute < 10 : localHour == 9 and localMinute >= 30)
sdMilestones(int direction, float entry, float stop, float tp1, float tp2, float h, float l, bool previousOne, bool previousTwo, bool moveBreakeven, float tick) =>
bool one = previousOne or (direction == 1 ? sdCompare(h, tp1) >= 0 : sdCompare(l, tp1) <= 0)
bool two = previousTwo or (direction == 1 ? sdCompare(h, tp2) >= 0 : sdCompare(l, tp2) <= 0)
float nextStop = stop
if one and not previousOne and moveBreakeven
float be = sdTickOut(entry, -direction, tick)
nextStop := direction == 1 ? math.max(nextStop, be) : math.min(nextStop, be)
if two and not previousTwo
nextStop := direction == 1 ? math.max(nextStop, tp1) : math.min(nextStop, tp1)
[nextStop, one, two, one and not previousOne, two and not previousTwo]
sdQuantity(bool fixedMode, float fixedUnits, float cashBudget, float unitRisk, float unitNotional, float equity, float capPercent, float increment) =>
bool available = not na(unitRisk) and not na(unitNotional) and not na(equity) and not na(increment) and sdSign(unitRisk) > 0 and sdSign(unitNotional) > 0 and sdSign(equity) > 0 and sdSign(increment) > 0 and sdSign(capPercent) > 0 and (fixedMode ? sdSign(fixedUnits) > 0 : sdSign(cashBudget) > 0)
float requested = fixedMode ? fixedUnits : cashBudget / unitRisk
float capUnits = equity * capPercent / 100 / unitNotional
float steps = available ? math.min(requested, capUnits) / increment : 0
bool supported = steps <= 9e18
float nearest = supported ? math.round(steps) : 0
float tolerance = math.min(1e-5, 4 * 2.220446049250313e-16 * math.max(1, math.abs(steps)))
float normalized = sdSign(math.abs(steps - nearest) - tolerance) <= 0 ? nearest : steps
float quantity = available and supported ? math.floor(normalized) * increment : 0
float actualRisk = quantity * unitRisk
float notional = quantity * unitNotional
bool withinBudget = (fixedMode or sdCompare(actualRisk, cashBudget) <= 0) and sdCompare(notional, equity * capPercent / 100) <= 0
bool valid = available and supported and sdSign(quantity) > 0 and withinBudget
string reason = not available ? "Price / account conversion / sizing unavailable" : not supported ? "Quantity needs more than supported integer steps" : sdSign(quantity) <= 0 ? "No size fits risk / equity cap at minimum increment" : not withinBudget ? "Sizing arithmetic exceeded budget" : "Sizing valid"
[valid ? quantity : 0.0, valid ? actualRisk : float(na), valid ? notional : float(na), valid, reason]
sdPendingCloseDecision(int direction, float closedPrice, float zoneTop, float zoneBottom, float frozenStop) =>
bool inside = sdCompare(closedPrice, zoneBottom) >= 0 and sdCompare(closedPrice, zoneTop) <= 0
bool invalid = sdCompare(closedPrice, frozenStop) * direction < 0
[inside, invalid, inside and not invalid]
sdGapValid(float top, float bottom, float minimumSize) =>
not na(top) and not na(bottom) and not na(minimumSize) and sdCompare(top, bottom) > 0 and sdCompare(top, bottom + minimumSize) >= 0
sdDisplacement(int direction, float closedPrice, float extreme, float minimumDistance) =>
not na(closedPrice) and not na(extreme) and not na(minimumDistance) and sdCompare(closedPrice, extreme + direction * minimumDistance) * direction >= 0
// ── PURE RELIABILITY HELPERS END
if barstate.isfirst
if not chart.is_standard or not timeframe.isintraday
runtime.error("Sweep Desk requires standard time-based intraday candles.")
if not (inTp1 > 0 and inTp2 > inTp1 and inTp3 > inTp2)
runtime.error("Targets must satisfy 0 < TP1 < TP2 < TP3.")
if barstate.isconfirmed and not sdBarValid(open, high, low, close)
runtime.error("Sweep Desk: missing or malformed confirmed OHLC. No reliable price transition can be calculated.")
// ═══════════════════════════════════════════════════════════════════ ENGINE INPUTS (ta.* at global scope)
float atr = ta.atr(14)
float atrU = math.max(atr, syminfo.mintick)
float ph = ta.pivothigh(high, inPivLen, inPivLen)
float pl = ta.pivotlow(low, inPivLen, inPivLen)
float phS = ta.pivothigh(high, inShiftLeg, inShiftLeg)
float plS = ta.pivotlow(low, inShiftLeg, inShiftLeg)
float volMa = ta.sma(nz(volume, 0.0), 20)
bool volOK = not inSweepVol or (not na(volume) and sdSign(volume) > 0 and not na(volMa) and sdSign(volMa) > 0 and sdCompare(volume, inSweepVolK * volMa) >= 0)
bool kz1 = not na(time(timeframe.period, inKz1, inTZ))
bool kz2 = not na(time(timeframe.period, inKz2, inTZ))
bool kz3 = not na(time(timeframe.period, inKz3, inTZ))
bool inKz = kz1 or kz2 or kz3
string kzName = kz1 ? "London" : kz2 ? "NY AM" : kz3 ? "NY PM" : "outside killzones"
// previous day / week highs and lows — the [1] + lookahead_on pair = the last CLOSED period (never the forming one)
[pdStamp, pdh, pdl] = request.security(syminfo.tickerid, "D", [time[1], high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwStamp, pwh, pwl] = request.security(syminfo.tickerid, "W", [time[1], high[1], low[1]], lookahead = barmerge.lookahead_on)
bool dayOK = timeframe.in_seconds() < timeframe.in_seconds("D")
bool weekOK = timeframe.in_seconds() < timeframe.in_seconds("W")
htfPhFn() => ta.pivothigh(high, inHtfLen, inHtfLen)
htfPlFn() => ta.pivotlow(low, inHtfLen, inHtfLen)
float htfPh = request.security(syminfo.tickerid, inHtf, htfPhFn()[1], lookahead = barmerge.lookahead_on)
float htfPl = request.security(syminfo.tickerid, inHtf, htfPlFn()[1], lookahead = barmerge.lookahead_on)
bool htfOK = timeframe.in_seconds(inHtf) > timeframe.in_seconds()
int htfMult = math.max(1, math.round(timeframe.in_seconds(inHtf) / timeframe.in_seconds()))
// CONFLUENCE FRAGMENT BEGIN
// Public confluence concepts, original deterministic definitions. No private DD grade or LRL formula.
gCF = "Confluence · transparent rules"
inCfPda = input.bool(true, "Require first-touch HTF FVG delivery", group = gCF, display = display.none,
tooltip = "A previously untouched, completed higher-timeframe 3-candle gap must be touched first. Its delivery context lasts N chart bars and can admit only one plan. This is our explicit PDA definition, not a private vendor formula.")
inCfHtf = input.timeframe("60", " HTF FVG source", group = gCF, display = display.none)
inCfPdaAge = input.int(6, " First-touch context lasts chart bars", minval = 1, maxval = 30, group = gCF, display = display.none)
inCfPdaLife = input.int(50, " Untouched gap lifetime · HTF periods", minval = 2, maxval = 200, group = gCF, display = display.none)
inCfSmt = input.bool(false, "Require paired-market SMT", group = gCF, display = display.none,
tooltip = "Select a meaningful comparison yourself. Uses strictly aligned, previously CLOSED chart and comparison bars. A missing bar resets the comparison window. Context is deliberately one chart bar late; no automatic correlation claim.")
inCfPair = input.symbol("BINANCE:BTCUSDT", " SMT comparison symbol", group = gCF, display = display.none)
inCfInverse = input.bool(false, " Inverse relationship", group = gCF, display = display.none)
inCfWindow = input.int(10, " SMT aligned-bar reference window", minval = 3, maxval = 100, group = gCF, display = display.none)
inCfSmtAge = input.int(6, " SMT context lasts chart bars", minval = 1, maxval = 30, group = gCF, display = display.none)
inCfMomentum = input.bool(false, "Require directional momentum candle", group = gCF, display = display.none)
inCfBody = input.float(0.5, " Body size ≥ ATR ×", minval = 0.0, maxval = 3.0, step = 0.1, group = gCF, display = display.none)
inCfLocation = input.float(0.7, " Directional close location ≥", minval = 0.5, maxval = 1.0, step = 0.05, group = gCF, display = display.none)
inCfVotes = input.int(0, "Minimum observed confluences · 0–4", minval = 0, maxval = 4, group = gCF, display = display.none,
tooltip = "One vote each: available first-touch HTF FVG, enabled aligned SMT, directional momentum, positive volume ≥ 1.3× its 20-bar mean. This is a transparent checklist, not an A+ grade, probability or optimized threshold. SMT supplies no vote while disabled.")
cfValid(float h, float l) => not na(h) and not na(l) and sdCompare(h, l) >= 0
cfOverlap(float h, float l, float top, float bot) => cfValid(h, l) and cfValid(top, bot) and sdCompare(l, top) <= 0 and sdCompare(h, bot) >= 0
cfPdaAction(int dir, float top, float bot, float h, float l, float c) =>
int action = 0
if not cfValid(h, l) or na(c) or sdCompare(c, h) > 0 or sdCompare(c, l) < 0 or not cfValid(top, bot) or sdCompare(top, bot) <= 0
action := -1
else if (dir == 1 and sdCompare(c, bot) < 0) or (dir == -1 and sdCompare(c, top) > 0)
action := -1
else if cfOverlap(h, l, top, bot)
action := 1
action
cfMomentum(int dir, float o, float h, float l, float c, float a, float minBody, float minLocation) =>
bool valid = cfValid(h, l) and not na(o) and not na(c) and sdCompare(o, l) >= 0 and sdCompare(o, h) <= 0 and sdCompare(c, l) >= 0 and sdCompare(c, h) <= 0 and sdCompare(h, l) > 0 and not na(a) and sdSign(a) > 0
valid and sdSign((c - o) * dir) > 0 and sdCompare(math.abs(c - o), minBody * a) >= 0 and sdCompare(dir == 1 ? (c - l) / (h - l) : (h - c) / (h - l), minLocation) >= 0
type CfPair
array<float> ownH
array<float> ownL
array<float> peerH
array<float> peerL
int stamp = na
int bullAt = na
int bearAt = na
bool ready = false
cfPairReset(CfPair p) =>
array.clear(p.ownH)
array.clear(p.ownL)
array.clear(p.peerH)
array.clear(p.peerL)
p.stamp := na
p.bullAt := na
p.bearAt := na
p.ready := false
cfPairStep(CfPair p, int sourceTime, int peerTime, int sourceEnd, int peerEnd, int interval, float ownH, float ownL, float peerH, float peerL, int window, bool inverse, int observedBar) =>
bool valid = interval > 0 and not na(sourceTime) and sourceTime == peerTime and sourceEnd == peerEnd and sourceEnd - sourceTime == interval and cfValid(ownH, ownL) and cfValid(peerH, peerL)
if not valid
cfPairReset(p)
else if na(p.stamp) or sourceTime != p.stamp
if not na(p.stamp) and sourceTime != p.stamp + interval
cfPairReset(p)
p.ready := array.size(p.ownH) >= window
if p.ready
bool ownNewLow = sdCompare(ownL, array.min(p.ownL)) < 0
bool ownNewHigh = sdCompare(ownH, array.max(p.ownH)) > 0
bool peerNoLow = sdCompare(peerL, array.min(p.peerL)) >= 0
bool peerNoHigh = sdCompare(peerH, array.max(p.peerH)) <= 0
if ownNewLow and (inverse ? peerNoHigh : peerNoLow)
p.bullAt := observedBar
if ownNewHigh and (inverse ? peerNoLow : peerNoHigh)
p.bearAt := observedBar
array.push(p.ownH, ownH)
array.push(p.ownL, ownL)
array.push(p.peerH, peerH)
array.push(p.peerL, peerL)
while array.size(p.ownH) > window
array.shift(p.ownH)
array.shift(p.ownL)
array.shift(p.peerH)
array.shift(p.peerL)
p.stamp := sourceTime
p.ready
type CfPda
float top
float bot
int dir
int formedAt
bool fresh = true
type CfDelivery
int bullAt = na
int bearAt = na
int bullSource = na
int bearSource = na
float bullTop = na
float bullBot = na
float bearTop = na
float bearBot = na
bool bullUsed = false
bool bearUsed = false
cfDeliveryObserve(CfDelivery d, CfPda z, float h, float l, float c, int at) =>
int action = cfPdaAction(z.dir, z.top, z.bot, h, l, c)
if z.fresh and action != 0
z.fresh := false
if action == 1
if z.dir == 1
d.bullAt := at
d.bullSource := z.formedAt
d.bullTop := z.top
d.bullBot := z.bot
d.bullUsed := false
else
d.bearAt := at
d.bearSource := z.formedAt
d.bearTop := z.top
d.bearBot := z.bot
d.bearUsed := false
cfDeliveryReady(CfDelivery d, int dir, int at, int ttl) =>
int touched = dir == 1 ? d.bullAt : d.bearAt
bool used = dir == 1 ? d.bullUsed : d.bearUsed
not used and not na(touched) and at >= touched and at - touched < ttl
cfDeliveryConsume(CfDelivery d, int dir) =>
if dir == 1
d.bullUsed := true
else
d.bearUsed := true
var array<CfPda> cfPdas = array.new<CfPda>()
var CfDelivery cfDelivery = CfDelivery.new()
var CfPair cfPair = CfPair.new(array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>())
var int cfLastHtf = na
float cfChartSeconds = timeframe.in_seconds()
float cfHtfSeconds = timeframe.in_seconds(inCfHtf)
bool cfHtfAllowed = not na(cfChartSeconds) and not na(cfHtfSeconds) and cfHtfSeconds > cfChartSeconds
[cfH1, cfL1, cfH3, cfL3, cfHtfTime, cfHtfEnd] = request.security(syminfo.tickerid, inCfHtf, [high[1], low[1], high[3], low[3], time[1], time_close[1]], lookahead = barmerge.lookahead_on)
[cfPeerH, cfPeerL, cfPeerTime, cfPeerEnd] = request.security(inCfPair, timeframe.period, [high[1], low[1], time[1], time_close[1]], gaps = barmerge.gaps_on, lookahead = barmerge.lookahead_on, ignore_invalid_symbol = true)
bool cfSourceReady = cfHtfAllowed and cfValid(cfH1, cfL1) and cfValid(cfH3, cfL3) and not na(cfHtfEnd) and cfHtfEnd <= time_close
bool cfMomentumLong = cfMomentum(1, open, high, low, close, atr, inCfBody, inCfLocation)
bool cfMomentumShort = cfMomentum(-1, open, high, low, close, atr, inCfBody, inCfLocation)
bool cfVolume = not na(volume) and sdSign(volume) > 0 and not na(volMa) and sdSign(volMa) > 0 and sdCompare(volume, 1.3 * volMa) >= 0
if barstate.isconfirmed
if inCfSmt
cfPairStep(cfPair, time[1], cfPeerTime, time_close[1], cfPeerEnd, int(cfChartSeconds * 1000), high[1], low[1], cfPeerH, cfPeerL, inCfWindow, inCfInverse, bar_index)
else
cfPairReset(cfPair)
if not cfSourceReady
array.clear(cfPdas)
cfDelivery.bullAt := na
cfDelivery.bearAt := na
else
if na(cfLastHtf) or cfHtfTime != cfLastHtf
// A chart bar straddling formation cannot establish post-formation freshness: skip that birth.
if cfHtfEnd <= time
if sdCompare(cfL1, cfH3) > 0
array.push(cfPdas, CfPda.new(cfL1, cfH3, 1, cfHtfEnd))
if sdCompare(cfH1, cfL3) < 0
array.push(cfPdas, CfPda.new(cfL3, cfH1, -1, cfHtfEnd))
cfLastHtf := cfHtfTime
// Oldest-to-newest: if several fresh zones are touched together, the most recently formed wins.
for z in cfPdas
bool expired = time_close - z.formedAt > cfHtfSeconds * 1000 * inCfPdaLife
if expired
z.fresh := false
else
cfDeliveryObserve(cfDelivery, z, high, low, close, bar_index)
if not na(cfDelivery.bullBot) and sdCompare(close, cfDelivery.bullBot) < 0
cfDelivery.bullAt := na
if not na(cfDelivery.bearTop) and sdCompare(close, cfDelivery.bearTop) > 0
cfDelivery.bearAt := na
while array.size(cfPdas) > 40
array.shift(cfPdas)
sdConfluencePda(int dir) => cfSourceReady and cfDeliveryReady(cfDelivery, dir, bar_index, inCfPdaAge)
sdConfluenceSmt(int dir) =>
int at = dir == 1 ? cfPair.bullAt : cfPair.bearAt
inCfSmt and cfPair.ready and not na(at) and bar_index >= at and bar_index - at < inCfSmtAge
sdConfluenceMomentum(int dir) => dir == 1 ? cfMomentumLong : cfMomentumShort
sdConfluenceVotes(int dir) => (sdConfluencePda(dir) ? 1 : 0) + (sdConfluenceSmt(dir) ? 1 : 0) + (sdConfluenceMomentum(dir) ? 1 : 0) + (cfVolume ? 1 : 0)
sdConfluenceAllowed(int dir) => (not inCfPda or sdConfluencePda(dir)) and (not inCfSmt or sdConfluenceSmt(dir)) and (not inCfMomentum or sdConfluenceMomentum(dir)) and sdConfluenceVotes(dir) >= inCfVotes
sdConfluenceReason(int dir) =>
string reason = "confluence ready"
if inCfPda and not cfSourceReady
reason := cfHtfAllowed ? "HTF FVG source unavailable" : "HTF FVG timeframe must be higher than chart"
else if inCfPda and not sdConfluencePda(dir)
reason := "no unused first-touch HTF FVG delivery"
else if inCfSmt and not cfPair.ready
reason := "SMT waiting for aligned closed-bar history"
else if inCfSmt and not sdConfluenceSmt(dir)
reason := "no recent paired-market SMT"
else if inCfMomentum and not sdConfluenceMomentum(dir)
reason := "confirmation body lacks directional momentum"
else if sdConfluenceVotes(dir) < inCfVotes
reason := "observed confluence count below minimum"
reason
sdConfluenceSummary(int dir) => "PDA " + (sdConfluencePda(dir) ? "first touch" : "unavailable/used") + " · SMT " + (not inCfSmt ? "off" : sdConfluenceSmt(dir) ? "confirmed -1 bar" : "waiting") + " · " + str.tostring(sdConfluenceVotes(dir)) + "/4 observed"
sdConfluenceConsume(int dir) =>
if sdConfluencePda(dir)
cfDeliveryConsume(cfDelivery, dir)
// CONFLUENCE FRAGMENT END
bool sdMacroAllowed = sdMacroWindow(inMacro, hour(time_close, inTZ), minute(time_close, inTZ))
sdAdmissionAllowed(int direction) => sdMacroAllowed and sdConfluenceAllowed(direction)
sdAdmissionReason(int direction) => not sdMacroAllowed ? "Confirmation outside selected macro" : sdConfluenceReason(direction)
// ═══════════════════════════════════════════════════════════════════ POOLS (resting liquidity)
type Pool
float px
int bar
int side // +1 = buy-side liquidity (a high) · −1 = sell-side liquidity (a low)
string kind // SWG · EQH · EQL · SES · PDH · PDL · PWH · PWL
int touches = 1
bool swept = false
bool faded = false
int sweptBar = na
line ln
line ln2 // glow (strong pools only)
label lb
var array<Pool> pools = array.new<Pool>()
poolColor(int side, bool swept) =>
color base = side == 1 ? th.down : th.up
swept ? color.new(base, 78) : color.new(base, 35)
poolDelete(Pool p) =>
if not na(p.ln)
line.delete(p.ln)
if not na(p.ln2)
line.delete(p.ln2)
if not na(p.lb)
label.delete(p.lb)
// register (or merge into an equal pool); draw only outside Minimal
poolAdd(float px, int bar, int side, string kind) =>
bool merged = false
if kind == "SWG"
for p in pools
if not p.swept and (p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL") and p.side == side and sdCompare(px, p.px - inEqTol * atrU) >= 0 and sdCompare(px, p.px + inEqTol * atrU) <= 0
p.px := side == 1 ? math.max(p.px, px) : math.min(p.px, px)
p.kind := side == 1 ? "EQH" : "EQL"
p.touches := p.touches + 1
p.bar := math.min(p.bar, bar)
merged := true
break
if not merged
// one PDH / PDL / PWH / PWL at a time — replace the previous period's pool
if (kind == "PDH" or kind == "PDL" or kind == "PWH" or kind == "PWL") and array.size(pools) > 0
for i = array.size(pools) - 1 to 0
Pool q = array.get(pools, i)
if q.kind == kind
poolDelete(q)
array.remove(pools, i)
Pool p = Pool.new(px = px, bar = bar, side = side, kind = kind)
array.push(pools, p)
merged
// Budget caps swing pools; swing and session pools age out. Swept pools linger forty bars.
poolPrune() =>
int up = 0
int dn = 0
for i = array.size(pools) - 1 to 0
if i < 0
break
Pool p = array.get(pools, i)
bool isSwing = p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL" or p.kind == "HTF"
bool old = p.swept ? (bar_index - nz(p.sweptBar, bar_index)) > 40 : ((isSwing or p.kind == "SES") and (bar_index - p.bar) > inPoolAge)
if old
poolDelete(p)
array.remove(pools, i)
else if not p.swept and isSwing
if p.side == 1
up += 1
else
dn += 1
// over budget → drop the oldest resting swing pools
int i = 0
while i < array.size(pools)
int sizeBefore = array.size(pools)
Pool p = array.get(pools, i)
bool isSwing = p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL" or p.kind == "HTF"
if not p.swept and isSwing
if p.side == 1 and up > inMaxPools
poolDelete(p)
array.remove(pools, i)
up -= 1
else if p.side == -1 and dn > inMaxPools
poolDelete(p)
array.remove(pools, i)
dn -= 1
if array.size(pools) == sizeBefore
i += 1
// killzone session pools: when a killzone ends, its high / low rest as liquidity
var float k1H = na
var float k1L = na
var float k2H = na
var float k2L = na
var float k3H = na
var float k3L = na
if kz1
k1H := kz1[1] ? math.max(nz(k1H, high), high) : high
k1L := kz1[1] ? math.min(nz(k1L, low), low) : low
if kz2
k2H := kz2[1] ? math.max(nz(k2H, high), high) : high
k2L := kz2[1] ? math.min(nz(k2L, low), low) : low
if kz3
k3H := kz3[1] ? math.max(nz(k3H, high), high) : high
k3L := kz3[1] ? math.min(nz(k3L, low), low) : low
if barstate.isconfirmed and not na(atr)
if not na(ph)
poolAdd(ph, bar_index - inPivLen, 1, "SWG")
if not na(pl)
poolAdd(pl, bar_index - inPivLen, -1, "SWG")
if inPoolSes
if kz1[1] and not kz1 and not na(k1H)
poolAdd(k1H, bar_index - 1, 1, "SES")
poolAdd(k1L, bar_index - 1, -1, "SES")
if kz2[1] and not kz2 and not na(k2H)
poolAdd(k2H, bar_index - 1, 1, "SES")
poolAdd(k2L, bar_index - 1, -1, "SES")
if kz3[1] and not kz3 and not na(k3H)
poolAdd(k3H, bar_index - 1, 1, "SES")
poolAdd(k3L, bar_index - 1, -1, "SES")
if inPoolPD and dayOK and not na(pdStamp) and (na(pdStamp[1]) or pdStamp != pdStamp[1])
poolAdd(pdh, bar_index, 1, "PDH")
poolAdd(pdl, bar_index, -1, "PDL")
if inPoolPW and weekOK and not na(pwStamp) and (na(pwStamp[1]) or pwStamp != pwStamp[1])
poolAdd(pwh, bar_index, 1, "PWH")
poolAdd(pwl, bar_index, -1, "PWL")
if inHtfOn and htfOK
if not na(htfPh) and (na(htfPh[1]) or htfPh != htfPh[1])
poolAdd(htfPh, bar_index - inHtfLen * htfMult, 1, "HTF")
if not na(htfPl) and (na(htfPl[1]) or htfPl != htfPl[1])
poolAdd(htfPl, bar_index - inHtfLen * htfMult, -1, "HTF")
poolPrune()
// ═══════════════════════════════════════════════════════════════════ SHORT-TERM SWINGS (the swing a shift must break)
var float stHi = na
var int stHiBar = na
var float stLo = na
var int stLoBar = na
if not na(phS)
stHi := phS
stHiBar := bar_index - inShiftLeg
if not na(plS)
stLo := plS
stLoBar := bar_index - inShiftLeg
// ═══════════════════════════════════════════════════════════════════ FVG REGISTRY (for the inversion model)
// every 3-candle gap ≥ the minimum size is tracked until a bar CLOSES through it the wrong way (inversion) or it ages out
type Gap
float top
float bot
int dir // +1 bullish gap · −1 bearish gap
int bar
bool inverted = false
int invBar = na
int lastBar = na
int members = 1
var array<Gap> gaps = array.new<Gap>()
gapRegister(float top, float bot, int direction, int birth) =>
bool joined = false
if inGapMode == "Series" and array.size(gaps) > 0
Gap previous = array.get(gaps, array.size(gaps) - 1)
if sdSeriesJoin(true, direction, previous.dir, birth, previous.lastBar, previous.inverted)
previous.top := math.max(previous.top, top)
previous.bot := math.min(previous.bot, bot)
previous.lastBar := birth
previous.members += 1
joined := true
if not joined
array.push(gaps, Gap.new(top = top, bot = bot, dir = direction, bar = birth, lastBar = birth))
if barstate.isconfirmed and not na(atr)
if sdGapValid(low, high[2], inFvgMin * atrU)
gapRegister(low, high[2], 1, bar_index - 1)
if sdGapValid(low[2], high, inFvgMin * atrU)
gapRegister(low[2], high, -1, bar_index - 1)
for i = array.size(gaps) - 1 to 0
if i < 0
break
Gap g = array.get(gaps, i)
if bar_index - g.bar > 200
array.remove(gaps, i)
else if not g.inverted
if sdInversion(g.dir, close, g.top, g.bot)
g.inverted := true
g.invBar := bar_index
while array.size(gaps) > 60
array.shift(gaps)
// sweep quality 0..1 — DESCRIPTIVE (depth of the raid, strength of the pool, killzone, volume). Never gates anything.
poolWeight(string kind) => kind == "SWG" ? 0.45 : (kind == "EQH" or kind == "EQL") ? 0.8 : kind == "SES" ? 0.75 : 1.0
sweepQuality(float depthAtr, string kind, bool kz) =>
float volTerm = volMa > 0 ? math.min(nz(volume, 0.0) / volMa / 2.0, 1.0) : 0.5
clamp01(0.35 * math.min(depthAtr / 0.5, 1.0) + 0.35 * poolWeight(kind) + 0.15 * (kz ? 1.0 : 0.0) + 0.15 * volTerm)
// ═══════════════════════════════════════════════════════════════════ THE SETUP STATE MACHINE
// states: 0 SCANNING · 1 SWEPT · 2 SHIFTED · 3 ARMED (waiting for the fill) · 4 FILLED (trade open)
type Setup
int dir = 0
int state = 0
float poolPx = na
string poolKind = ""
float ext = na // the sweep extreme — structural invalidation
int sweepBar = na
bool sweepKz = false
float sweepDepth = na
float oppSwing = na
int oppBar = na
int mssBar = na
float mssPx = na
float fvgTop = na
float fvgBot = na
int fvgBar = na
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
float tp3 = na
float tpLiq = na
string tpLiqKind = ""
float tpFinal = na
int armBar = na
int fillBar = na
bool tp1Hit = false
bool tp2Hit = false
string model = "" // "iFVG" or "MSS"
float quality = na // sweep quality 0..1 (descriptive, frozen at the raid)
int armTime = na
int entryTime = na
int endTime = na
int planId = na
bool planValid = false
string planReason = "No plan"
bool ambiguous = false
string confluenceSummary = ""
string stopBasis = ""
type Stat
int swept = 0
int shifted = 0
int armed = 0
int filled = 0
int n = 0
int wins = 0
int scratch = 0
float sumR = 0.0
int ambiguous = 0
int nSwg = 0
int wSwg = 0
int nEq = 0
int wEq = 0
int nSes = 0
int wSes = 0
int nHtf = 0
int wHtf = 0
int nIfvg = 0
int wIfvg = 0
int nMss = 0
int wMss = 0
var Setup s = Setup.new()
var Stat st = Stat.new()
// Frozen completed paper episodes; never aliases the mutable current Setup.
type SdPlanRecord
int planId
int direction
string model
string poolKind
float poolPrice
float zoneHigh
float zoneLow
float entry
float initialStop
float target
float risk
int armTime
int entryTime
int endTime
float exitPrice
float grossR
string outcome
bool ambiguous
string confluences
var array<SdPlanRecord> sdHistory = array.new<SdPlanRecord>()
var int sdLastClosedTime = na
var float sdLastClosedPrice = na
if barstate.isconfirmed
sdLastClosedTime := time_close
sdLastClosedPrice := close
// per-bar event record (a fresh object every bar; functions mutate its fields — globals cannot be rebound in functions)
type Ev
bool sweep = false
bool shift = false
bool armed = false
bool filled = false
bool exit = false
bool cancel = false
bool tp1 = false
bool tp2 = false
string why = ""
float r = na
float px = na
int dir = 0
Ev ev = Ev.new()
// nearest resting pool on the opposite side, at least minR × r away → the liquidity target
liqTarget(int dir, float entry, float r) =>
float best = na
string bk = ""
for p in pools
if not p.swept and (inTargetQuality == "Any fresh pool" or p.touches >= 2)
float targetPx = sdTickOut(p.px, -dir, syminfo.mintick)
if dir == 1 and p.side == 1 and sdCompare(targetPx, entry + inTpLiqMinR * r) >= 0
if na(best) or sdCompare(targetPx, best) < 0
best := targetPx
bk := p.kind
if dir == -1 and p.side == -1 and sdCompare(targetPx, entry - inTpLiqMinR * r) <= 0
if na(best) or sdCompare(targetPx, best) > 0
best := targetPx
bk := p.kind
[best, bk]
// FVG search inside the shift leg (offsets 0..len): bull = low[i] > high[i+2]
findFvg(int dir, int len) =>
float top = na
float bot = na
int at = na
int lim = math.min(math.max(len, 0), 60)
for i = 0 to lim
bool bull = dir == 1 and sdGapValid(low[i], high[i + 2], inFvgMin * atrU)
bool bear = dir == -1 and sdGapValid(low[i + 2], high[i], inFvgMin * atrU)
if bull or bear
float t = bull ? low[i] : low[i + 2]
float b = bull ? high[i + 2] : high[i]
if sdGapValid(t, b, inFvgMin * atrU)
if inFvgPick == "Nearest"
top := t
bot := b
at := bar_index - i
break
else
top := t
bot := b
at := bar_index - i
[top, bot, at]
setLevels(int dir, float entry, bool checkAdmission = true) =>
float zoneFar = dir == 1 ? s.fvgBot : s.fvgTop
bool extNear = sdCompare(entry, s.ext - inSlMaxAtr * atrU) >= 0 and sdCompare(entry, s.ext + inSlMaxAtr * atrU) <= 0
s.stopBasis := inSlMode == "Sweep extreme" ? "sweep extreme" : inSlMode == "Zone far edge" ? "zone far edge" : extNear ? "Auto: sweep extreme" : "Auto: zone far edge"
float anchor = inSlMode == "Sweep extreme" ? s.ext : inSlMode == "Zone far edge" ? zoneFar : (extNear ? s.ext : zoneFar)
[slPx, risk, t1, t2, t3, valid, reason] = sdPlanGeometry(dir, entry, anchor, atrU, syminfo.mintick, inSlBuf, inTp1, inTp2, inTp3)
s.entry := entry
s.sl := slPx
s.sl0 := slPx
s.r := risk
s.tp1 := t1
s.tp2 := t2
s.tp3 := t3
[lq, lk] = liqTarget(dir, entry, risk)
s.tpLiq := lq
s.tpLiqKind := lk
s.tpFinal := inPrimary == "Liquidity" and not na(lq) ? lq : t3
s.planValid := valid and (not checkAdmission or sdAdmissionAllowed(dir))
s.planReason := checkAdmission and not sdAdmissionAllowed(dir) ? sdAdmissionReason(dir) : reason
s.ambiguous := false
valid
closeTrade(float px, string why) =>
float rr = (px - s.entry) * s.dir / s.r
ev.exit := true
ev.why := why
ev.r := rr
ev.px := px
ev.dir := s.dir
s.endTime := time_close
array.unshift(sdHistory, SdPlanRecord.new(s.planId, ev.dir, s.model, s.poolKind, s.poolPx, s.fvgTop, s.fvgBot, s.entry, s.sl0, s.tpFinal, s.r, s.armTime, s.entryTime, s.endTime, px, rr, why, s.ambiguous, s.confluenceSummary))
if array.size(sdHistory) > 60
array.pop(sdHistory)
if s.ambiguous
st.ambiguous += 1
bool win = rr >= 0.05
st.n := st.n + 1
st.wins := st.wins + (win ? 1 : 0)
st.scratch := st.scratch + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if s.poolKind == "SWG"
st.nSwg := st.nSwg + 1
st.wSwg := st.wSwg + (win ? 1 : 0)
else if s.poolKind == "EQH" or s.poolKind == "EQL"
st.nEq := st.nEq + 1
st.wEq := st.wEq + (win ? 1 : 0)
else if s.poolKind == "SES"
st.nSes := st.nSes + 1
st.wSes := st.wSes + (win ? 1 : 0)
else
st.nHtf := st.nHtf + 1
st.wHtf := st.wHtf + (win ? 1 : 0)
if s.model == "iFVG"
st.nIfvg := st.nIfvg + 1
st.wIfvg := st.wIfvg + (win ? 1 : 0)
else
st.nMss := st.nMss + 1
st.wMss := st.wMss + (win ? 1 : 0)
s.state := 0
s.dir := 0
cancelSetup(string why) =>
ev.cancel := true
ev.why := why
ev.dir := s.dir
s.endTime := time_close
s.state := 0
s.dir := 0
bool dirL = inDir != "Short only"
bool dirS = inDir != "Long only"
float observedRaidHi = na
float observedRaidLo = na
string observedKindHi = ""
string observedKindLo = ""
if barstate.isconfirmed and not na(atr)
for p in pools
if not p.swept
[traversed, raid] = sdPoolObservation(p.side, p.px, high, low, close, inSweepMin * atrU)
if traversed
p.swept := true
p.sweptBar := bar_index
if raid and (inPoolMin == "Any pool" or p.kind != "SWG")
if p.side == 1 and (na(observedRaidHi) or sdCompare(p.px, observedRaidHi) > 0)
observedRaidHi := p.px
observedKindHi := p.kind
if p.side == -1 and (na(observedRaidLo) or sdCompare(p.px, observedRaidLo) < 0)
observedRaidLo := p.px
observedKindLo := p.kind
if barstate.isconfirmed and not na(atr) and bar_index > 50
// FILLED: next-bar open ordering, then ambiguous range, then timeout. New stops apply next bar.
if s.state == 4
int d = s.dir
[ended, exitPx, outcome, ambiguous] = sdOutcome(d, s.sl, s.tpFinal, open, high, low, close, s.fillBar, bar_index, inMaxTrade)
if ended
s.ambiguous := s.ambiguous or ambiguous
string reason = outcome == "TARGET" ? ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "TP-LIQ" : "TP3") : outcome == "STOP" ? (s.tp2Hit ? "TRAIL" : (s.tp1Hit and inBE) ? "BE" : "SL") : outcome
closeTrade(exitPx, reason + (ambiguous ? " · AMBIGUOUS" : ""))
else
[nextStop, one, two, newOne, newTwo] = sdMilestones(d, s.entry, s.sl, s.tp1, s.tp2, high, low, s.tp1Hit, s.tp2Hit, inBE, syminfo.mintick)
s.sl := nextStop
s.tp1Hit := one
s.tp2Hit := two
ev.tp1 := newOne
ev.tp2 := newTwo
// ── 3 · ARMED: waiting for the fill
else if s.state == 3
int d = s.dir
[closeInsideZone, invalid, closeEntryAllowed] = sdPendingCloseDecision(d, close, s.fvgTop, s.fvgBot, s.sl)
bool missed = d == 1 ? sdCompare(high, s.tp1) >= 0 : sdCompare(low, s.tp1) <= 0
bool fill = false
float fillPx = s.entry
if inEntryMode == "Close inside"
fill := closeEntryAllowed
fillPx := close
else
fill := d == 1 ? sdCompare(low, s.entry) <= 0 : sdCompare(high, s.entry) >= 0
fillPx := d == 1 ? math.min(s.entry, open) : math.max(s.entry, open)
bool gapPast = inEntryMode != "Close inside" and (d == 1 ? sdCompare(open, s.sl) <= 0 : sdCompare(open, s.sl) >= 0)
if inEntryMode == "Close inside" and invalid
cancelSetup("structural stop lost before close entry")
else if fill and gapPast
// Paper convention: limit and initial stop resolve at the same known open; 0R before costs, not a promised live scratch.
s.entry := open
s.state := 4
s.fillBar := bar_index
s.entryTime := time
st.filled := st.filled + 1
ev.filled := true
ev.dir := d
closeTrade(open, "GAP")
else if fill
if inEntryMode == "Close inside"
setLevels(d, fillPx, false)
else
// A better limit fill changes actual risk, never the already-resting bracket.
s.entry := fillPx
s.r := sdDistance(s.sl0, fillPx, d, syminfo.mintick)
s.planValid := sdCompare(s.r, 0) > 0
if not s.planValid
cancelSetup("Plan rejected: " + s.planReason)
else
s.state := 4
s.fillBar := bar_index
s.entryTime := time_close
s.endTime := na
st.filled += 1
ev.filled := true
ev.dir := d
// A close entry cannot be stopped by earlier wicks. Resting limits may fill intrabar.
if inEntryMode != "Close inside"
bool knownOpenFill = sdCompare(open, fillPx) == 0
if knownOpenFill
[endedAtOpenBar, priceAtOpenBar, reasonAtOpenBar, ambiguousAtOpenBar] = sdOutcome(d, s.sl, s.tpFinal, open, high, low, close, bar_index - 1, bar_index, inMaxTrade)
if endedAtOpenBar
s.ambiguous := ambiguousAtOpenBar
closeTrade(priceAtOpenBar, reasonAtOpenBar == "TARGET" ? ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "TP-LIQ" : "TP3") : "SL" + (ambiguousAtOpenBar ? " · AMBIGUOUS" : ""))
else
[nextStop, one, two, newOne, newTwo] = sdMilestones(d, s.entry, s.sl, s.tp1, s.tp2, high, low, false, false, inBE, syminfo.mintick)
s.sl := nextStop
s.tp1Hit := one
s.tp2Hit := two
ev.tp1 := newOne
ev.tp2 := newTwo
else
bool stopSame = d == 1 ? sdCompare(low, s.sl) <= 0 : sdCompare(high, s.sl) >= 0
bool targetSame = d == 1 ? sdCompare(high, s.tpFinal) >= 0 : sdCompare(low, s.tpFinal) <= 0
// An intrabar limit has unknown entry-vs-target chronology. Stop-first; do not invent a same-bar target win.
s.ambiguous := stopSame or targetSame
if stopSame
closeTrade(s.sl, "SL · AMBIGUOUS")
else if invalid
cancelSetup("invalidated before fill")
else if s.model == "iFVG" and (d == 1 ? sdCompare(close, s.fvgBot) < 0 : sdCompare(close, s.fvgTop) > 0)
cancelSetup("inversion failed — closed back through the zone")
else if missed
cancelSetup("missed — ran to TP1 unfilled")
else if bar_index - s.armBar >= inExpire
cancelSetup("expired unfilled")
// ── 2 · SHIFTED: looking for the FVG in the leg
else if s.state == 2
int d = s.dir
bool failed = d == 1 ? sdCompare(close, s.ext) < 0 : sdCompare(close, s.ext) > 0
if failed
cancelSetup("sweep extreme lost after the shift")
else if bar_index - s.mssBar > inFvgWin
cancelSetup("no FVG after the shift")
else
[ft, fb, fa] = findFvg(d, bar_index - s.sweepBar)
if not na(ft)
s.fvgTop := ft
s.fvgBot := fb
s.fvgBar := fa
s.model := "MSS"
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (ft + fb) / 2 : (d == 1 ? ft : fb)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
// ── 1 · SWEPT: waiting for the confirmation (iFVG inversion and / or structure shift)
else if s.state == 1
int d = s.dir
// the invalidation level follows the leg's extreme while we wait
s.ext := d == 1 ? math.min(s.ext, low) : math.max(s.ext, high)
bool failed = d == 1 ? sdCompare(close, s.poolPx - inDispAtr * atrU) < 0 : sdCompare(close, s.poolPx + inDispAtr * atrU) > 0
if failed
cancelSetup("no rejection — price kept going")
else if bar_index - s.sweepBar > inShiftWin
cancelSetup("no shift in time")
else
bool useIfvg = inConfirm != "Structure shift (MSS)"
bool useMss = inConfirm != "FVG inversion (iFVG)"
bool shift = useMss and (d == 1 ? (not na(s.oppSwing) and sdCompare(close, s.oppSwing) > 0) : (not na(s.oppSwing) and sdCompare(close, s.oppSwing) < 0))
bool disp = sdDisplacement(d, close, s.ext, inDispAtr * atrU)
// iFVG: an opposing gap printed near the raid that THIS bar closed through → it inverts; the nearest one to price is the zone
float igTop = na
float igBot = na
int igBar = na
if useIfvg
for g in gaps
if g.inverted and g.invBar == bar_index and g.dir == -d and g.bar >= s.sweepBar - inIfvgBack
bool beyondExt = d == 1 ? sdCompare(g.bot, s.ext) > 0 : sdCompare(g.top, s.ext) < 0
if beyondExt and (na(igTop) or (d == 1 ? sdCompare(g.top, igTop) > 0 : sdCompare(g.bot, igBot) < 0))
igTop := g.top
igBot := g.bot
igBar := g.bar
if not na(igTop) and disp
s.fvgTop := igTop
s.fvgBot := igBot
s.fvgBar := igBar
s.model := "iFVG"
s.mssBar := bar_index
s.mssPx := d == 1 ? igTop : igBot
st.shifted := st.shifted + 1
ev.shift := true
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (igTop + igBot) / 2 : (d == 1 ? igTop : igBot)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
else if shift and disp
s.state := 2
s.mssBar := bar_index
s.mssPx := s.oppSwing
st.shifted := st.shifted + 1
ev.shift := true
// the FVG may already be in the leg — arm on the same bar
[ft, fb, fa] = findFvg(d, bar_index - s.sweepBar)
if not na(ft)
s.fvgTop := ft
s.fvgBot := fb
s.fvgBar := fa
s.model := "MSS"
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (ft + fb) / 2 : (d == 1 ? ft : fb)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
if ev.armed
s.confluenceSummary := sdConfluenceSummary(s.dir)
sdConfluenceConsume(s.dir)
// Confirmation-close orders are accepted at this close; no earlier OHLC can end this plan.
if ev.armed and inEntryMode == "Confirmation close" and s.planValid
s.state := 4
s.fillBar := bar_index
s.entryTime := time_close
st.filled += 1
ev.filled := true
ev.dir := s.dir
// ── 0/1/2 · a fresh sweep replaces an idea that has not armed yet; an ARMED limit stands until filled / invalidated / missed / expired; a FILLED trade is never interrupted
if s.state != 4 and s.state != 3 and not ev.exit and not ev.cancel and (not inSweepKz or inKz) and volOK
float bestHi = observedRaidHi
float bestLo = observedRaidLo
string bkHi = observedKindHi
string bkLo = observedKindLo
// sell-side liquidity raided (a low taken, close back above) → long idea
if not na(bestLo) and dirL
s.dir := 1
s.state := 1
s.poolPx := bestLo
s.poolKind := bkLo
s.ext := low
s.sweepBar := bar_index
s.sweepKz := inKz
s.sweepDepth := (bestLo - low) / atrU
s.quality := sweepQuality(s.sweepDepth, bkLo, inKz)
s.model := ""
s.planId := na
s.planValid := false
s.planReason := "Waiting for confirmation"
s.confluenceSummary := ""
s.stopBasis := ""
s.entry := na
s.sl := na
s.sl0 := na
s.r := na
s.tp1 := na
s.tp2 := na
s.tp3 := na
s.tpLiq := na
s.tpFinal := na
s.armTime := na
s.entryTime := na
s.endTime := na
s.ambiguous := false
s.oppSwing := stHi
s.oppBar := stHiBar
s.tp1Hit := false
s.tp2Hit := false
st.swept := st.swept + 1
ev.sweep := true
else if not na(bestHi) and dirS
s.dir := -1
s.state := 1
s.poolPx := bestHi
s.poolKind := bkHi
s.ext := high
s.sweepBar := bar_index
s.sweepKz := inKz
s.sweepDepth := (high - bestHi) / atrU
s.quality := sweepQuality(s.sweepDepth, bkHi, inKz)
s.model := ""
s.planId := na
s.planValid := false
s.planReason := "Waiting for confirmation"
s.confluenceSummary := ""
s.stopBasis := ""
s.entry := na
s.sl := na
s.sl0 := na
s.r := na
s.tp1 := na
s.tp2 := na
s.tp3 := na
s.tpLiq := na
s.tpFinal := na
s.armTime := na
s.entryTime := na
s.endTime := na
s.ambiguous := false
s.oppSwing := stLo
s.oppBar := stLoBar
s.tp1Hit := false
s.tp2Hit := false
st.swept := st.swept + 1
ev.sweep := true
// ═══════════════════════════════════════════════════════════════════ CHART · pools, FVG, MSS, sweeps (drawings)
var array<box> fvgBoxes = array.new<box>()
var array<line> mssLines = array.new<line>()
var array<label> tags = array.new<label>()
capBoxes(int cap) =>
while array.size(fvgBoxes) > cap
box.delete(array.shift(fvgBoxes))
capLines(int cap) =>
while array.size(mssLines) > cap
line.delete(array.shift(mssLines))
capTags(int cap) =>
while array.size(tags) > cap
label.delete(array.shift(tags))
bool showPools = vClean
bool showTags = vPro
// a zone drawn as four stacked bands, densest at the edge price touches first (top for a long, bottom for a short), plus an outline
gradZone(int dir, float top, float bot, int x1, int x2) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = dir == 1 ? 80 + k * 4 : 92 - k * 4
array.push(fvgBoxes, box.new(x1, bTop, x2, bBot, border_color = color.new(th.bg, 100), bgcolor = color.new(dir == 1 ? th.up : th.down, alpha)))
array.push(fvgBoxes, box.new(x1, top, x2, bot, border_color = color.new(dir == 1 ? th.up : th.down, 40), bgcolor = color.new(th.bg, 100)))
if barstate.isconfirmed
if ev.sweep and showTags
array.push(tags, label.new(bar_index, s.dir == 1 ? low : high, (s.dir == 1 ? "SSL swept · " : "BSL swept · ") + s.poolKind, style = s.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(th.bg, 20), textcolor = s.dir == 1 ? th.up : th.down, size = size.tiny))
if ev.shift and vClean and s.model == "MSS"
array.push(mssLines, line.new(s.oppBar, s.mssPx, bar_index, s.mssPx, color = color.new(s.dir == 1 ? th.up : th.down, 25), style = line.style_dashed, width = 1))
if showTags
array.push(tags, label.new(bar_index, s.mssPx, "MSS", style = s.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 20), textcolor = s.dir == 1 ? th.up : th.down, size = size.tiny))
if ev.armed and vClean
gradZone(s.dir, s.fvgTop, s.fvgBot, s.fvgBar - 1, bar_index + inExtend)
if showTags
array.push(tags, label.new(s.fvgBar - 1, s.dir == 1 ? s.fvgTop : s.fvgBot, s.model == "iFVG" ? "iFVG ↺" : "FVG", style = label.style_label_right, color = color.new(th.bg, 100), textcolor = color.new(s.dir == 1 ? th.up : th.down, 20), size = size.tiny))
capBoxes(40)
capLines(30)
capTags(60)
// pool lines: created once; x2 refreshed every 20 bars while resting (cheap); frozen + faded ONCE when swept.
// Far pools are deleted on the last bar, not just hidden — TradingView's auto-scale counts every line object.
if barstate.isconfirmed or barstate.islast
for p in pools
if showPools
if na(p.ln)
p.ln := line.new(math.max(p.bar, bar_index - 250), p.px, bar_index + inExtend + 20, p.px, color = poolColor(p.side, false), width = p.kind == "EQH" or p.kind == "EQL" ? 2 : 1, style = line.style_solid)
if inGlow and p.kind != "SWG"
p.ln2 := line.new(math.max(p.bar, bar_index - 250), p.px, bar_index + inExtend + 20, p.px, color = color.new(p.side == 1 ? th.down : th.up, 88), width = 5, style = line.style_solid)
if p.swept and not p.faded
line.set_x2(p.ln, nz(p.sweptBar, bar_index))
line.set_color(p.ln, poolColor(p.side, true))
line.set_style(p.ln, line.style_dotted)
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
p.faded := true
else if not p.swept and bar_index % 20 == 0
line.set_x2(p.ln, bar_index + inExtend + 20)
if not na(p.ln2)
line.set_x2(p.ln2, bar_index + inExtend + 20)
else
if not na(p.ln)
line.delete(p.ln)
p.ln := na
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
// focus + labels: last bar only (display)
if barstate.islast
for p in pools
bool near = math.abs(p.px - close) <= inPoolNear * atrU
if showPools and near
if not na(p.ln) and not p.swept
line.set_x2(p.ln, bar_index + inExtend)
if not na(p.ln2)
line.set_x2(p.ln2, bar_index + inExtend)
if showTags and not p.swept
string ptxt = (p.side == 1 ? "BSL" : "SSL") + " · " + p.kind + (p.touches > 1 ? " ×" + str.tostring(p.touches) : "")
if na(p.lb)
p.lb := label.new(bar_index + inExtend, p.px, ptxt, style = label.style_label_left, color = color.new(th.bg, 100), textcolor = color.new(p.side == 1 ? th.down : th.up, 25), size = size.tiny)
else
label.set_xy(p.lb, bar_index + inExtend, p.px)
label.set_text(p.lb, ptxt)
else if not na(p.lb)
label.delete(p.lb)
p.lb := na
else
if not na(p.ln)
line.delete(p.ln)
p.ln := na
p.faded := false
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
if not na(p.lb)
label.delete(p.lb)
p.lb := na
// ═══════════════════════════════════════════════════════════════════ CHART · the ticket (plots + gradient zones)
bool pend = s.state == 3
bool live = s.state == 4 or ev.exit
int zDir = s.dir != 0 ? s.dir : ev.dir
float pEntry = pend ? s.entry : na
float pStop = pend ? s.sl : na
float pTpF = pend ? s.tpFinal : na
float aEntry = live ? s.entry : na
float aStop = live ? s.sl : na
float aTp1 = live ? s.tp1 : na
float aTp2 = live ? s.tp2 : na
float aTpF = live ? s.tpFinal : na
float aTpLiq = live and not na(s.tpLiq) and sdCompare(s.tpLiq, s.tpFinal) != 0 ? s.tpLiq : na
plot(pEntry, "Pending · entry", color = color.new(th.txtHi, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(pStop, "Pending · stop", color = color.new(th.down, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(pTpF, "Pending · target", color = color.new(th.up, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plEnt = plot(aEntry, "Trade · entry", color = color.new(th.txtHi, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plStop = plot(aStop, "Trade · stop", color = color.new(th.down, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTp1, "Trade · TP1", color = color.new(th.up, 60), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTp2, "Trade · TP2", color = color.new(th.up, 45), linewidth = 1, style = plot.style_linebr, display = display.pane)
plFin = plot(aTpF, "Trade · final target", color = color.new(th.up, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTpLiq, "Trade · liquidity target", color = color.new(th.accentHi, 25), linewidth = 1, style = plot.style_linebr, display = display.pane)
bool zonesOn = inZones and live
fill(plEnt, plStop, top_value = math.max(aEntry, aStop), bottom_value = math.min(aEntry, aStop),
top_color = zonesOn and zDir == 1 ? color.new(th.down, 78) : color.new(th.down, 100), bottom_color = zonesOn and zDir == -1 ? color.new(th.down, 78) : color.new(th.down, 100), title = "Risk zone", fillgaps = false)
fill(plEnt, plFin, top_value = math.max(aEntry, aTpF), bottom_value = math.min(aEntry, aTpF),
top_color = zonesOn and zDir == -1 ? color.new(th.up, 80) : color.new(th.up, 100), bottom_color = zonesOn and zDir == 1 ? color.new(th.up, 80) : color.new(th.up, 100), title = "Reward zone", fillgaps = false)
bgcolor(vFull ? (kz1 ? color.new(th.accent, 94) : kz2 ? color.new(th.up, 95) : kz3 ? color.new(th.down, 95) : na) : na, title = "Killzone shading")
barcolor(inTint and s.state == 4 ? color.new(s.dir == 1 ? th.up : th.down, 55) : na, title = "Trade tint")
plotshape(ev.sweep and s.dir == 1 and vClean, "Raid · sell-side", shape.circle, location.belowbar, color = color.new(th.up, 35), size = size.tiny, display = display.pane)
plotshape(ev.sweep and s.dir == -1 and vClean, "Raid · buy-side", shape.circle, location.abovebar, color = color.new(th.down, 35), size = size.tiny, display = display.pane)
plotshape(ev.filled and ev.dir == 1, "Filled · long", shape.triangleup, location.belowbar, color = th.up, size = size.tiny, display = display.pane)
plotshape(ev.filled and ev.dir == -1, "Filled · short", shape.triangledown, location.abovebar, color = th.down, size = size.tiny, display = display.pane)
// event labels (confirmed bars only by construction)
if ev.armed
label.new(s.fvgBar, s.dir == 1 ? s.fvgBot : s.fvgTop, (s.dir == 1 ? "LONG armed" : "SHORT armed") + " · " + fmt(s.entry), style = s.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(s.dir == 1 ? th.up : th.down, 20), textcolor = th.bg, size = size.small,
tooltip = "entry " + fmt(s.entry) + "\nstop " + fmt(s.sl) + " (" + fmt(s.r) + " = 1R, " + (s.sl0 == s.ext - inSlBuf * atrU or s.sl0 == s.ext + inSlBuf * atrU ? "sweep extreme" : "zone far edge") + ")\nzone " + (s.model == "iFVG" ? "inverted gap " : "FVG ") + fmt(s.fvgBot) + " – " + fmt(s.fvgTop) + "\nTP1 " + fmt(s.tp1) + " · TP2 " + fmt(s.tp2) + " · TP3 " + fmt(s.tp3) + (na(s.tpLiq) ? "\nno liquidity target ≥ " + str.tostring(inTpLiqMinR, "0.0") + "R" : "\nliquidity " + fmt(s.tpLiq) + " (" + s.tpLiqKind + ", " + str.tostring(math.abs(s.tpLiq - s.entry) / s.r, "0.0") + "R)"))
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "XL" : "XS") + " · " + ev.why + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny)
if ev.cancel and vPro
label.new(bar_index, ev.dir == 1 ? high : low, "✕ " + ev.why, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 40), textcolor = th.flat, size = size.tiny)
// ═══════════════════════════════════════════════════════════════════ WORDS (one source of truth)
string stateWord = s.state == 4 ? "FILLED · " + (s.dir == 1 ? "LONG" : "SHORT") : s.state == 3 ? "ARMED · " + (s.dir == 1 ? "LONG" : "SHORT") : s.state == 2 ? "SHIFTED · " + (s.dir == 1 ? "long" : "short") + " idea" : s.state == 1 ? "SWEPT · waiting for confirmation" : "SCANNING"
color stateColor = s.state >= 3 ? (s.dir == 1 ? th.up : th.down) : s.state > 0 ? th.accentHi : th.txt
string clockTxt = na(sdLastClosedTime) ? "waiting close" : str.format_time(sdLastClosedTime, "HH:mm", inTZ)
var int restingUp = 0
var int restingDn = 0
if barstate.islast
restingUp := 0
restingDn := 0
for p in pools
if not p.swept
if p.side == 1
restingUp += 1
else
restingDn += 1
float rrLiq = s.state >= 3 and not na(s.tpLiq) ? math.abs(s.tpLiq - s.entry) / s.r : na
// ═══════════════════════════════════════════════════════════════════ DASHBOARD
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
kv(table tb, int row, string k, string v, color vc, string tip) =>
color stripe = inZebra and row % 2 == 1 ? color.new(th.accent, 93) : th.bg
table.cell(tb, 0, row, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe, tooltip = tip)
table.cell(tb, 1, row, v, text_color = vc, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = stripe, tooltip = tip)
// Read-only admission diagnostics. They never replace an active plan's state or frozen context.
sdViewAdmissionData() =>
string status = ""
if inCfPda and not cfHtfAllowed
status := "SETTINGS: HTF FVG must exceed chart"
else if inCfPda and not cfSourceReady
status := "DATA WAIT: completed HTF source missing"
if inCfSmt and not cfPair.ready
status += (status == "" ? "" : "\n") + "SMT: waiting for aligned closed bars"
status == "" ? "Data ready; setup gates still apply" : status
bool sdViewDataWait = (inCfPda and not cfSourceReady) or (inCfSmt and not cfPair.ready)
string sdViewDataHelp = "New-setup eligibility only. Set the required HTF FVG source strictly above the chart; missing completed source data blocks admission. Enabled SMT requires contiguous aligned closed-bar history. Existing pending/active plans retain their frozen context and lifecycle. Data ready does not mean a trade signal."
// At most 18 populated rows including admission status, within the 20-row allocation.
var table dash = table.new(posOf(inDashPos), 2, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0)
if barstate.islast and inDash
table.clear(dash, 0, 0, 1, 19)
int r = 0
table.cell(dash, 0, r, "SWEEP DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Liquidity → shift → FVG, with a close-committed entry / stop / target engine. Every step commits on the closed bar.")
table.cell(dash, 1, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
table.cell(dash, 0, r, (s.state >= 3 ? (s.dir == 1 ? "▲ " : "▼ ") : "● ") + stateWord, text_color = stateColor, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "SCANNING = no raid yet · SWEPT = a pool was raided, waiting for the structure shift · SHIFTED = shift confirmed, looking for the FVG · ARMED = levels set, waiting for the fill · FILLED = trade open.")
table.cell(dash, 1, r, s.state == 1 ? str.tostring(bar_index - s.sweepBar) + "/" + str.tostring(inShiftWin) + " bars" : s.state == 3 ? str.tostring(bar_index - s.armBar) + "/" + str.tostring(inExpire) + " bars" : s.state == 4 ? str.tostring(bar_index - s.fillBar) + " bars in" : clockTxt + " · " + kzName, text_color = th.txt, text_size = szBody(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Timers: bars since the sweep (shift window) · bars since armed (expiry) · bars in the trade.")
r += 1
kv(dash, r, "New setups · data", sdViewAdmissionData(), sdViewDataWait ? th.down : th.flat, sdViewDataHelp)
r += 1
kv(dash, r, "Pool", s.state > 0 ? (s.dir == 1 ? "SSL " : "BSL ") + s.poolKind + " " + fmt(s.poolPx) + " · " + str.tostring(s.sweepDepth, "0.00") + " ATR" : str.tostring(restingUp) + " BSL · " + str.tostring(restingDn) + " SSL resting", th.txt, "The raided pool: its kind (SWG swing · EQH/EQL equal highs/lows · SES killzone session · HTF higher-timeframe swing · PDH/PDL/PWH/PWL previous day/week) and how far the wick went through it in ATR. When scanning: how many pools are resting on each side.")
r += 1
// the level rows only exist while a setup is alive — an idle dashboard stays short
if s.state > 0
kv(dash, r, "Sweep", s.state > 0 ? (s.sweepKz ? "in killzone" : "outside killzones") + (inSweepVol ? " · vol ✓" : "") : "—", s.state > 0 ? (s.sweepKz ? th.up : th.txt) : th.flat, "Where the sweep happened (killzone or not) and whether the volume filter passed.")
r += 1
float qv = s.state > 0 ? nz(s.quality, 0.0) : na
string qBar = na(qv) ? "—" : (qv >= 0.8 ? "▮▮▮▮▮" : qv >= 0.6 ? "▮▮▮▮▯" : qv >= 0.4 ? "▮▮▮▯▯" : qv >= 0.2 ? "▮▮▯▯▯" : "▮▯▯▯▯") + " " + str.tostring(math.round(qv * 100))
color qBg = na(qv) ? th.bg : color.new(color.from_gradient(qv, 0.0, 1.0, th.down, th.up), 72)
table.cell(dash, 0, r, "Sweep quality", text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Descriptive, frozen at the raid: depth of the wick through the pool, strength of the pool kind, killzone, volume. It never gates a setup and it is not a prediction.")
table.cell(dash, 1, r, qBar, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = qBg, tooltip = "Descriptive, frozen at the raid: depth of the wick through the pool, strength of the pool kind, killzone, volume. It never gates a setup and it is not a prediction.")
r += 1
string needTxt = inConfirm == "FVG inversion (iFVG)" ? "needs a close through an opposing gap" : inConfirm == "Structure shift (MSS)" ? "needs close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "—" : fmt(s.oppSwing)) : "close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "—" : fmt(s.oppSwing)) + " or through a gap"
kv(dash, r, "Confirm", s.state >= 2 ? (s.model == "iFVG" ? "iFVG inverted" : "MSS " + fmt(s.mssPx)) + " · " + str.tostring(math.abs(close - s.ext) / atrU, "0.0") + " ATR leg" : s.state == 1 ? needTxt : "—", s.state >= 2 ? th.txtHi : th.txt, "How the reversal was confirmed: iFVG = a close through an opposing gap near the raid (the gap inverts) · MSS = a close beyond the last short-term swing with displacement. Plus the leg from the sweep extreme in ATR.")
r += 1
kv(dash, r, "Zone", s.state >= 3 ? fmt(s.fvgBot) + " – " + fmt(s.fvgTop) + (s.model == "iFVG" ? " · inverted gap" : " · leg FVG") : "—", th.txt, "The entry zone: the inverted gap (iFVG) or the imbalance inside the shift leg (MSS).")
r += 1
if s.state >= 3
kv(dash, r, "Confluence · frozen", s.confluenceSummary, th.txt, "Observed at acceptance, before its first-touch token was consumed. A transparent checklist, not a probability or private grade.")
r += 1
kv(dash, r, "Entry", s.state >= 3 ? fmt(s.entry) + (s.state == 3 ? (inEntryMode == "Close inside" ? " · waiting close" : " · limit") : "") : "—", th.txtHi, "Confirmation close (default), later close inside, or resting edge/midpoint: see Entry setting. Paper reference; broker fills may differ.")
r += 1
kv(dash, r, "Stop", s.state >= 3 ? fmt(s.sl) + " · " + fmt(s.r) + " = 1R" + (s.tp2Hit ? " · TRAIL" : s.tp1Hit and inBE ? " · BE" : "") : "—", th.down, "Anchored to the sweep extreme (structural invalidation) plus a buffer — or the zone's far edge when the extreme is too far away (Auto) or when you chose it.")
r += 1
kv(dash, r, "R ladder", s.state >= 3 ? fmt(s.tp1) + " · " + fmt(s.tp2) + " · " + fmt(s.tp3) : "—", th.up, "Rounded R milestones. TP1 entry-stop move is optional; TP2 trails to TP1. Updates apply next bar, with no partial position exits.")
r += 1
kv(dash, r, "Liquidity", s.state >= 3 ? (na(s.tpLiq) ? "none ≥ " + str.tostring(inTpLiqMinR, "0.0") + "R resting" : fmt(s.tpLiq) + " · " + s.tpLiqKind + " · " + str.tostring(rrLiq, "0.0") + "R") : "—", na(rrLiq) ? th.flat : th.accentHi, "The nearest resting pool on the other side — where the market's next magnet sits. Final target = " + inPrimary + ".")
r += 1
kv(dash, r, "Swept · Confirm · Armed · Filled", str.tostring(st.swept) + " · " + str.tostring(st.shifted) + " · " + str.tostring(st.armed) + " · " + str.tostring(st.filled), th.txt, "How many raids were confirmed (iFVG or MSS), how many confirmations armed a zone, how many got filled — on the loaded history. The drop-offs are the filters doing their job.")
r += 1
int decided = st.n - st.scratch
kv(dash, r, "Record", st.n < 5 ? str.tostring(st.n) + " closed · too few" : str.tostring(st.n) + " closed · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win" + (st.scratch > 0 ? " · " + str.tostring(st.scratch) + " flat" : "") + " · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", th.txt, "Closed trades on this chart: count · win% of ALL closed (flat = breakeven / gap scratches, counted as non-wins, same basis as the By-pool and By-model rows) · average R. Paper, no costs, small sample — a description of the past on THIS chart, not an edge.")
r += 1
string byPool = (st.nSwg > 0 ? "SWG " + str.tostring(math.round(st.wSwg / st.nSwg * 100)) + "%/" + str.tostring(st.nSwg) : "SWG —") + " · " + (st.nEq > 0 ? "EQ " + str.tostring(math.round(st.wEq / st.nEq * 100)) + "%/" + str.tostring(st.nEq) : "EQ —") + " · " + (st.nSes > 0 ? "SES " + str.tostring(math.round(st.wSes / st.nSes * 100)) + "%/" + str.tostring(st.nSes) : "SES —") + " · " + (st.nHtf > 0 ? "HTF " + str.tostring(math.round(st.wHtf / st.nHtf * 100)) + "%/" + str.tostring(st.nHtf) : "HTF —")
kv(dash, r, "By pool", byPool, th.txt, "Win% / count by the kind of liquidity that was raided — swing, equal highs/lows, killzone session, or HTF (higher-timeframe swings + previous day / week). Which pools actually pay on YOUR chart.")
r += 1
string byModel = (st.nIfvg > 0 ? "iFVG " + str.tostring(math.round(st.wIfvg / st.nIfvg * 100)) + "%/" + str.tostring(st.nIfvg) : "iFVG —") + " · " + (st.nMss > 0 ? "MSS " + str.tostring(math.round(st.wMss / st.nMss * 100)) + "%/" + str.tostring(st.nMss) : "MSS —")
kv(dash, r, "By model", byModel, th.txt, "Win% / count by confirmation model — the inversion (iFVG) entries versus the structure-shift (MSS) entries on this chart.")
r += 1
table.cell(dash, 0, r, (inConfirm == "Either" ? "iFVG+MSS" : inConfirm == "FVG inversion (iFVG)" ? "iFVG" : "MSS") + " · " + inEntryMode + " · " + (inSlMode == "Sweep extreme" ? "SL@sweep" : inSlMode == "Zone far edge" ? "SL@zone" : "SL@auto") + " · " + (inPrimary == "Liquidity" ? "TP@liq" : "TP@" + str.tostring(inTp3) + "R"), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Entry mode · stop anchor · final target.")
table.cell(dash, 1, r, (inSweepKz ? "killzones only" : "any time") + " · " + str.tostring(array.size(pools)) + " pools", text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Sweep time filter and how many pools (resting + recently swept) are tracked.")
// ═══════════════════════════════════════════════════════════════════ NARRATIVE · Co-Pilot (translates state; never a new signal)
wrap(string sIn, int width) =>
array<string> words = str.split(sIn, " ")
string out = ""
string line = ""
for wd in words
if line != "" and str.length(line) + str.length(wd) + 1 > width
out := out + (out == "" ? "" : "\n") + line
line := wd
else
line := line == "" ? wd : line + " " + wd
out == "" ? line : out + "\n" + line
narrNow() =>
string o = ""
if s.state == 4
float finalR = (s.tpFinal - s.entry) * s.dir / s.r
o := "paper " + (s.dir == 1 ? "long" : "short") + " from " + fmt(s.entry) + " — current stop " + fmt(s.sl) + (s.tp2Hit ? " (TP2 trail)" : s.tp1Hit and inBE ? " (entry stop)" : " (" + s.stopBasis + ")") + ", final " + fmt(s.tpFinal) + " (" + str.tostring(finalR, "0.00") + "R" + ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "; " + s.tpLiqKind : "; fixed ladder") + ")"
else if s.state == 3
o := (s.dir == 1 ? "long" : "short") + " plan armed — " + (inEntryMode == "Close inside" ? "waiting for a valid close inside " + fmt(s.fvgBot) + "–" + fmt(s.fvgTop) : "resting " + inEntryMode + " limit " + fmt(s.entry)) + "; pending stop " + fmt(s.sl0) + " (" + s.stopBasis + "); " + str.tostring(math.max(0, inExpire - (bar_index - s.armBar))) + " bars until unfilled expiry"
else if s.state == 2
o := "structure shifted " + (s.dir == 1 ? "up" : "down") + " after the raid — looking for the imbalance in the leg to arm the entry (" + str.tostring(inFvgWin - (bar_index - s.mssBar)) + " bars left)"
else if s.state == 1
o := (s.dir == 1 ? "sell-side" : "buy-side") + " liquidity at " + fmt(s.poolPx) + " (" + s.poolKind + ") was raided and rejected — waiting for " + (inConfirm == "FVG inversion (iFVG)" ? "a close through an opposing gap (inversion)" : inConfirm == "Structure shift (MSS)" ? "a close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) : "a close through an opposing gap or " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing))) + " with displacement, " + str.tostring(inShiftWin - (bar_index - s.sweepBar)) + " bars left"
else
o := "scanning — " + str.tostring(restingUp) + " buy-side and " + str.tostring(restingDn) + " sell-side pools are resting; nothing has been raided yet"
o
narrWhy() =>
string o = "The sequence is mechanical: a pool is swept (wick through by ≥ " + str.tostring(inSweepMin, "0.00") + " ATR, close back inside), then the reversal is confirmed " + (inConfirm == "FVG inversion (iFVG)" ? "when a bar closes through an opposing gap — the gap inverts and becomes the zone" : inConfirm == "Structure shift (MSS)" ? "when a bar closes beyond the last short-term swing — the gap in that leg becomes the zone" : "by a close through an opposing gap (the gap inverts) or beyond the last short-term swing (the gap in that leg)") + ", always with a leg ≥ " + str.tostring(inDispAtr, "0.0") + " ATR from the extreme. "
o := o + (s.state > 0 ? "This one: " + s.poolKind + " pool, wick " + str.tostring(s.sweepDepth, "0.00") + " ATR through it, " + (s.sweepKz ? "inside a killzone" : "outside the killzones") + "." : "Killzone now: " + kzName + ".")
o
narrPlan() =>
string o = ""
if s.state >= 3
float finalR = (s.tpFinal - s.entry) * s.dir / s.r
string targetBasis = (not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "nearest qualifying " + s.tpLiqKind : inPrimary == "Liquidity" and na(s.tpLiq) ? "explicit TP3 fallback" : "selected TP3 rung"
o := "Entry " + fmt(s.entry) + "; initial stop " + fmt(s.sl0) + " (" + s.stopBasis + "), current stop " + fmt(s.sl) + ". TP1 " + fmt(s.tp1) + (inBE ? " moves the stop toward entry" : " is a checkpoint; entry-stop move is off") + "; TP2 " + fmt(s.tp2) + " moves it toward TP1. These stop changes apply from the next bar. Final " + fmt(s.tpFinal) + " = " + str.tostring(finalR, "0.00") + "R (" + targetBasis + "). Known open gaps resolve first; stop-first only for unordered range hits. Closed-bar entries exclude all earlier entry-bar wicks."
else if s.state == 1
o := (inConfirm == "FVG inversion (iFVG)" ? "If a bar closes through an opposing gap near the raid with displacement, that gap inverts and the entry is armed on it. " : inConfirm == "Structure shift (MSS)" ? "If a bar closes " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) + " with displacement, the shift is in. " : "A close through an opposing gap (inversion) arms the entry on that gap; a close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) + " arms it on the gap in that leg. ") + "If price closes back through the raided pool by " + str.tostring(inDispAtr, "0.0") + " ATR, the raid was not a raid."
o
narrTrack() =>
int decided = st.n - st.scratch
"Funnel here: " + str.tostring(st.swept) + " sweeps → " + str.tostring(st.shifted) + " confirmed → " + str.tostring(st.armed) + " armed → " + str.tostring(st.filled) + " filled. " + (st.n < 5 ? "Too few closed trades to say anything yet." : str.tostring(st.n) + " closed, " + str.tostring(math.round(st.wins / st.n * 100)) + "% won" + (st.scratch > 0 ? " (" + str.tostring(st.scratch) + " flat)" : "") + ", avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R (paper, no costs, small sample — NOT an edge).")
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
string p = narrPlan()
if p != ""
array.push(labs, "PLAN")
array.push(vals, wrap(p, inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Pool = resting liquidity (stops) at a swing, equal highs/lows, a session or a day/week extreme. Sweep = a wick through it that closes back. iFVG = an opposing gap that price closes through, so it inverts and becomes the zone. MSS = a close beyond the last short-term swing the other way; then the gap in that leg is the zone. The selected stop anchor is sweep extreme, zone far edge, or Auto; the accepted ticket shows its actual basis.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. Paper ledger has no costs; the strategy twin measures it with costs.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ═══════════════════════════════════════════════════════════════════ ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"SweepDesk","symbol":"' + sdEscape(syminfo.tickerid) + '","tf":"' + sdEscape(timeframe.period) + '","event":"' + evName + '","side":"' + (s.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + '","model":"' + sdEscape(s.model) + '","pool":"' + sdEscape(s.poolKind) + '","entry":' + sdNumber(s.entry) + ',"sl":' + sdNumber(s.sl) + ',"initial_sl":' + sdNumber(s.sl0) + ',"tp1":' + sdNumber(s.tp1) + ',"tp2":' + sdNumber(s.tp2) + ',"tp3":' + sdNumber(s.tp3) + ',"tp_liq":' + sdNumber(s.tpLiq) + ',"target":' + sdNumber(s.tpFinal) + ',"reason":"' + sdEscape(why) + '","confluences":"' + sdEscape(s.confluenceSummary) + '","entry_time":' + sdNumber(s.entryTime) + ',"end_observed_time":' + sdNumber(s.endTime) + ',"exit_price":' + sdNumber(ev.exit ? ev.px : na) + ',"gross_r":' + sdNumber(ev.r) + '}'
string tx = "Sweep Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · " + (s.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + " · entry " + fmt(nz(s.entry, ev.px)) + " · SL " + fmt(nz(s.sl, 0)) + " · TP " + fmt(nz(s.tp1, 0)) + " / " + fmt(nz(s.tp2, 0)) + " / " + fmt(nz(s.tp3, 0)) + (na(s.tpLiq) ? "" : " · liq " + fmt(s.tpLiq)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
alertcondition(ev.armed and s.dir == 1, "Long setup armed", "Sweep Desk: LONG plan armed — inspect entry mode and frozen structural levels")
alertcondition(ev.armed and s.dir == -1, "Short setup armed", "Sweep Desk: SHORT plan armed — inspect entry mode and frozen structural levels")
alertcondition(ev.filled and ev.dir == 1, "Long filled", "Sweep Desk: LONG filled")
alertcondition(ev.filled and ev.dir == -1, "Short filled", "Sweep Desk: SHORT filled")
alertcondition(ev.tp1, "TP1 reached", "Sweep Desk: TP1 reached — stop moves per settings")
alertcondition(ev.tp2, "TP2 reached", "Sweep Desk: TP2 reached — stop to TP1")
alertcondition(ev.exit and (ev.why == "TP-LIQ" or ev.why == "TP3"), "Final target reached", "Sweep Desk: final target reached")
alertcondition(ev.exit and (str.contains(ev.why, "SL") or str.contains(ev.why, "BE") or str.contains(ev.why, "TRAIL") or ev.why == "GAP STOP"), "Stopped", "Sweep Desk: stop hit")
alertcondition(ev.cancel, "Setup cancelled", "Sweep Desk: setup cancelled")
alertcondition(ev.sweep, "Liquidity swept", "Sweep Desk: a pool was raided — watching for the confirmation")
alertcondition(ev.shift and s.model == "iFVG", "FVG inverted (iFVG)", "Sweep Desk: an opposing gap inverted after the raid — zone armed")
alertcondition(ev.sweep and (s.poolKind == "HTF" or s.poolKind == "PDH" or s.poolKind == "PDL" or s.poolKind == "PWH" or s.poolKind == "PWL"), "Higher-timeframe pool swept", "Sweep Desk: a higher-timeframe pool (HTF swing · previous day · previous week) was raided")
// One envelope per confirmed update; no same-bar FILLED/EXIT or TP1/TP2 event is lost.
if barstate.isconfirmed and (ev.armed or ev.filled or ev.tp1 or ev.tp2 or ev.exit or ev.cancel)
string kinds = ""
if ev.armed
kinds += "\"ARMED\""
if ev.filled
kinds += (kinds == "" ? "" : ",") + "\"FILLED\""
if ev.tp1
kinds += (kinds == "" ? "" : ",") + "\"TP1\""
if ev.tp2
kinds += (kinds == "" ? "" : ",") + "\"TP2\""
if ev.exit
kinds += (kinds == "" ? "" : ",") + "\"EXIT\""
if ev.cancel
kinds += (kinds == "" ? "" : ",") + "\"CANCEL\""
string primary = ev.exit ? "EXIT" : ev.cancel ? "CANCEL" : ev.filled ? "FILLED" : ev.armed ? "ARMED" : ev.tp2 ? "TP2" : "TP1"
string message = payload(primary, ev.why)
if inAlertJson
message := str.substring(message, 0, str.length(message) - 1) + ",\"schema\":\"sweep-desk.v2\",\"events\":[" + kinds + "],\"confirmed\":true,\"fill_confirmed\":false,\"costs_included\":false,\"bar_close_ms\":" + str.tostring(time_close) + ",\"plan_id\":" + (na(s.planId) ? "null" : str.tostring(s.planId)) + ",\"ambiguous\":" + (s.ambiguous ? "true" : "false") + "}"
alert(message, alert.freq_once_per_bar_close)
The strategy twin · optional
Measure the rules with costs on a second chart.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// SWEEP DESK — Liquidity · Shift · FVG · Pine Script v6 · v2.0.0
// The liquidity-sweep model made mechanical and honest, with closed-bar structural references and explicit paper execution assumptions.
//
// THE SEQUENCE (every step commits on the CLOSED bar):
// 1 · POOLS resting liquidity = confirmed swing highs/lows, equal highs/lows, killzone-session
// highs/lows, previous day / week highs/lows. Each pool is resting until it is swept.
// 2 · SWEEP a bar wicks THROUGH a pool by ≥ X ATR and CLOSES back inside → the pool is raided.
// 3 · CONFIRM within N bars, either (a) iFVG — a bar CLOSES THROUGH an opposing imbalance printed near the
// raid, so that gap INVERTS and becomes the entry zone (an original, explicitly defined inversion model), or (b) MSS — a
// close beyond the last opposing short-term swing with displacement (leg from the extreme ≥ k ATR).
// 4 · ZONE iFVG: the inverted gap · MSS: the 3-candle imbalance printed inside the shift leg.
// 5 · ENTRY confirmation close (v2 default); optional later close inside or resting edge / midpoint limit.
// 6 · STOP the sweep extreme ± a buffer — the structural invalidation, not a guess.
// 7 · TARGET frozen 1R / 2R / 3R milestones; optional nearest qualifying fresh opposing pool; explicit TP3 fallback.
//
// WHY IT EXISTS "Finding the liquidity is easy; the entries and exits are where the gold is." This
// build answers that with a state machine you can read on the dashboard, stop-first
// accounting, and a track record that counts every setup it ever armed.
// STATUS v2.0.0 · reliability revision · verification scope in docs/build-report.md · paper
// ledger has NO costs · the Strategy Lab measures independent broker fills with costs · not a validated edge.
//
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
strategy("Sweep Desk — Strategy Lab", "SweepDeskS", overlay = true, behind_chart = false, max_bars_back = 3000,
initial_capital = 100000, currency = currency.USD, default_qty_type = strategy.fixed, default_qty_value = 1, pyramiding = 0,
commission_type = strategy.commission.percent, commission_value = 0.02, slippage = 5,
margin_long = 100, margin_short = 100, process_orders_on_close = true,
calc_on_every_tick = false, calc_on_order_fills = false, calc_on_every_history_tick = false,
max_lines_count = 250, max_labels_count = 200, max_boxes_count = 120)
// GENERATED v2.0.0: shared technical events remain paper-paced; broker occupancy/fills/exits are independent.
// Generic costs: 0.02% per order + 5 ticks/fill, 100% margin. Default risk budget USD100 and 90% equity notional cap.
// Explicit USD account conversion at submission; actual gap/slippage risk can exceed the signal-close estimate.
// Use Strategy Tester for broker results. The dashboard is the separate, cost-free paper ledger.
// Keep the declared recalculation settings; intrabar/fill recalculation is unsupported.
// ─────────────────────────────────────────────────────────────────── INPUTS · liquidity pools
gP = "Liquidity pools"
inPivLen = input.int(5, "Swing pivot length", minval = 2, maxval = 30, group = gP,
tooltip = "Bars on each side that confirm a swing high / low. A swing becomes a resting pool N bars after it forms — no repainting, some lag.", display = display.none)
inEqTol = input.float(0.15, "Equal highs / lows tolerance (× ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = gP,
tooltip = "A new swing within this distance of a resting pool merges into it as an EQUAL high / low (EQH / EQL) — the pools the market loves to raid.", display = display.none)
inPoolSes = input.bool(true, "Killzone session highs / lows", group = gP, tooltip = "When a killzone ends, its high and low become resting pools.", display = display.none)
inPoolPD = input.bool(true, "Previous day high / low", group = gP, display = display.none)
inPoolPW = input.bool(true, "Previous week high / low", group = gP, display = display.none)
inMaxPools = input.int(10, "Max resting pools per side", minval = 3, maxval = 30, group = gP, tooltip = "Oldest swing pools are dropped beyond this (day / week pools are protected; session pools follow the age limit).", display = display.none)
inPoolAge = input.int(400, "Drop a resting swing / session pool after N bars", minval = 50, maxval = 3000, group = gP, display = display.none)
inHtfOn = input.bool(true, "Higher-timeframe swing pools", group = gP, tooltip = "Confirmed swing highs / lows from a higher timeframe rest as pools too (kind HTF). Read from the last CLOSED higher-timeframe bar — no lookahead.", display = display.none)
inHtf = input.timeframe("60", " Higher timeframe", group = gP, display = display.none)
inHtfLen = input.int(5, " HTF pivot length", minval = 2, maxval = 20, group = gP, display = display.none)
inPoolNear = input.float(8.0, "Draw pools within N ATR of price", minval = 1.0, maxval = 50.0, step = 0.5, group = gP, tooltip = "Display only — every pool is still tracked and can be a target. Keeps the chart focused on the liquidity that matters now.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · sweep filters
gS = "Sweep filters (better sweeps, fewer fakes)"
inSweepMin = input.float(0.05, "Wick beyond the pool ≥ ATR ×", minval = 0.0, maxval = 2.0, step = 0.05, group = gS,
tooltip = "A touch is not a sweep. The wick must go through the pool by at least this much ATR and the bar must CLOSE back on the pool's side.", display = display.none)
inSweepVol = input.bool(false, "Require a volume spike on the sweep bar", group = gS, display = display.none)
inSweepVolK = input.float(1.3, "Volume ≥ SMA(20) ×", minval = 1.0, maxval = 5.0, step = 0.1, group = gS, display = display.none)
inSweepKz = input.bool(false, "Only sweeps inside a killzone", group = gS, tooltip = "Off = any time. On = the sweep bar must fall inside one of the killzones below.", display = display.none)
inPoolMin = input.string("Any pool", "A setup needs a sweep of", options = ["Any pool", "Strong pools only"], group = gS,
tooltip = "Strong pools = equal highs/lows, killzone session extremes, higher-timeframe swings, previous day / week levels. Plain swing pools are still tracked and drawn, but sweeping one does not start a setup.", display = display.none)
inDir = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gS, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · shift + FVG
gM = "Confirmation (iFVG / MSS) + zone"
inConfirm = input.string("FVG inversion (iFVG)", "Confirmation", options = ["FVG inversion (iFVG)", "Structure shift (MSS)", "Either"], group = gM,
tooltip = "How the reversal is confirmed after the raid. iFVG = a bar CLOSES through an opposing imbalance printed near the raid — that gap inverts and becomes the entry zone (the inversion model). MSS = a close beyond the last short-term swing with displacement, then the imbalance in that leg. Either = whichever prints first.", display = display.none)
inIfvgBack = input.int(40, "iFVG: the gap may have formed up to N bars before the sweep", minval = 5, maxval = 200, group = gM, display = display.none)
inShiftLeg = input.int(3, "Short-term swing length (the swing the shift must break)", minval = 2, maxval = 10, group = gM, display = display.none)
inShiftWin = input.int(12, "Confirmation must happen within N bars of the sweep", minval = 3, maxval = 60, group = gM, display = display.none)
inDispAtr = input.float(1.0, "Displacement: leg from the sweep extreme ≥ ATR ×", minval = 0.2, maxval = 5.0, step = 0.1, group = gM,
tooltip = "The confirming bar's close must be at least this far (in ATR) from the sweep extreme — a real push, not a drift. Applies to both models.", display = display.none)
inFvgMin = input.float(0.10, "FVG size ≥ ATR ×", minval = 0.0, maxval = 2.0, step = 0.05, group = gM, display = display.none)
inFvgPick = input.string("Nearest", "Which FVG in the leg", options = ["Nearest", "Deepest"], group = gM,
tooltip = "Nearest = the last imbalance printed in the shift leg (price reaches it first). Deepest = the first one (bigger discount, fills less often).", display = display.none)
inFvgWin = input.int(8, "MSS: the FVG must exist within N bars after the shift", minval = 1, maxval = 30, group = gM, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · entry / stop / targets
gE = "Entry · stop · targets"
inEntryMode = input.string("Confirmation close", "Entry", options = ["Confirmation close", "FVG edge", "CE (50%)", "Close inside"], group = gE,
tooltip = "Confirmation close = enter at the confirmed inversion / MSS-zone acceptance close. This is the v2 factory choice. Close entries cannot exit on earlier wicks. FVG edge = limit at the near edge of the gap. CE = limit at the gap's midpoint (consequent encroachment). Close inside = wait for a bar to close inside the gap, enter at that close.", display = display.none)
inSlMode = input.string("Sweep extreme", "Stop anchor", options = ["Auto", "Sweep extreme", "Zone far edge"], group = gE,
tooltip = "Sweep extreme = the structural invalidation: if price trades back through the raided low / high, the idea is wrong. Zone far edge = the other side of the entry zone — tighter, more stop-outs. Auto = the sweep extreme unless it sits more than N ATR from the entry (a stop that far is no longer a structural stop for this timeframe), then the zone far edge.", display = display.none)
inSlMaxAtr = input.float(2.5, "Auto: sweep extreme allowed up to N ATR from the entry", minval = 0.5, maxval = 10.0, step = 0.5, group = gE, display = display.none)
inSlBuf = input.float(0.10, "Stop buffer (× ATR)", minval = 0.0, maxval = 1.0, step = 0.05, group = gE, display = display.none)
inTp1 = input.float(1.0, "TP1 (R)", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inTp2 = input.float(2.0, "TP2 (R)", minval = 0.5, maxval = 20, step = 0.1, group = gE, display = display.none)
inTp3 = input.float(3.0, "TP3 (R)", minval = 0.5, maxval = 30, step = 0.1, group = gE, display = display.none)
inTpLiqMinR = input.float(2.0, "Liquidity target must be ≥ R", minval = 0.3, maxval = 10, step = 0.1, group = gE,
tooltip = "The opposing-liquidity target is the nearest resting pool on the other side that is at least this many R away.", display = display.none)
inPrimary = input.string("R ladder (TP3)", "Final target", options = ["Liquidity", "R ladder (TP3)"], group = gE,
tooltip = "Which target closes the trade. Liquidity = the opposing pool (falls back to TP3 when none qualifies). TP1 / TP2 are milestones either way.", display = display.none)
inBE = input.bool(true, "TP1 → stop to breakeven", group = gE, display = display.none)
inExpire = input.int(30, "Unfilled setup expires after N bars", minval = 3, maxval = 300, group = gE, display = display.none)
inMaxTrade = input.int(200, "Open trade times out after N bars", minval = 10, maxval = 2000, group = gE, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · killzones
gK = "Killzones (exchange time)"
inTZ = input.string("America/New_York", "Timezone", group = gK, tooltip = "IANA name. ICT killzones are defined in New York time.", display = display.none)
inKz1 = input.session("0200-0500", "London", group = gK, display = display.none)
inKz2 = input.session("0830-1100", "New York AM", group = gK, display = display.none)
inKz3 = input.session("1330-1600", "New York PM", group = gK, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · style / panels / alerts
gV = "Style"
inView = input.string("Minimal", "View preset", options = ["Minimal", "Clean", "Pro", "Full"], group = gV,
tooltip = "Minimal = the active setup only. Clean = + resting pools + setup FVG + MSS. Pro = + every FVG, sweep tags, pool labels. Full = + killzone shading.", display = display.none)
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inZones = input.bool(true, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(false, "Tint candles while a trade is open", group = gV, display = display.none)
inGlow = input.bool(true, "Glow on strong pools (EQ · session · HTF · day / week)", group = gV, display = display.none)
inExtend = input.int(20, "Pool label offset (bars)", minval = 5, maxval = 200, group = gV, display = display.none)
gD = "Dashboard"
inDash = input.bool(false, "Show dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inZebra = input.bool(true, "Row stripes", group = gD, display = display.none)
gN = "Narrative · Co-Pilot"
inNarr = input.string("Off", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 120, group = gN, display = display.none)
gA = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gA, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// v2 additions follow all existing controls to preserve prior input identities.
gV2 = "v2 · transparent confirmation / target rules"
inGapMode = input.string("Single", "iFVG mode", options = ["Single", "Series"], group = gV2, display = display.none, tooltip = "Single = individual three-candle gap. Series = a union of same-direction gaps born on consecutive chart bars, before inversion; the whole union must be crossed by a later close. This precise adjacency rule is original, not a claim of private parity.")
inMacro = input.string("Off", "Confirmation macro window", options = ["Off", "45–15", "50–10", "US open 30m"], group = gV2, display = display.none, tooltip = "Evaluated at the confirmation candle CLOSE in the selected timezone. 45–15: minute 45 through 14; 50–10: minute 50 through 09; US open 30m: 09:30 inclusive to 10:00 exclusive. No exchange holiday calendar.")
inTargetQuality = input.string("Any fresh pool", "Opposing target quality", options = ["Any fresh pool", "Clustered (2+ touches)"], group = gV2, display = display.none, tooltip = "Clustered requires two separately confirmed chart swing observations merged into one still-fresh EQ pool. This is a transparent level cluster, not a trendline detector. The minimum-R rule and explicit TP3 fallback still apply.")
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
bool vClean = inView != "Minimal"
bool vPro = inView == "Pro" or inView == "Full"
bool vFull = inView == "Full"
fmt(float x) => str.tostring(x, format.mintick)
clamp01(float x) => math.max(0.0, math.min(1.0, x))
// ── PURE RELIABILITY HELPERS BEGIN · shared by literal Pine fixtures and broker adapter
sdSign(float x) =>
na(x) ? 0 : int(nz(x / math.abs(x), 0))
sdCompare(float lhs, float rhs) =>
float tolerance = 4 * 2.220446049250313e-16 * math.max(math.abs(lhs), math.abs(rhs))
sdSign(math.abs(lhs - rhs) - tolerance) <= 0 ? 0 : sdSign(lhs - rhs)
sdBarValid(float o, float h, float l, float c) =>
not na(o) and not na(h) and not na(l) and not na(c) and sdSign(h - l) >= 0 and sdSign(o - l) >= 0 and sdSign(h - o) >= 0 and sdSign(c - l) >= 0 and sdSign(h - c) >= 0
sdTickOut(float px, int roundingDirection, float tick) =>
float raw = px / tick
bool supported = not na(raw) and math.abs(raw) <= 9e18
float nearest = supported ? math.round(raw) : na
// Never absorb a genuine fraction of a tick at a large absolute coordinate.
float tolerance = math.min(1e-5, 8 * 2.220446049250313e-16 * math.max(1, math.abs(raw)))
float normalized = sdSign(math.abs(raw - nearest) - tolerance) <= 0 ? nearest : raw
supported ? (roundingDirection == 1 ? math.ceil(normalized) : math.floor(normalized)) * tick : float(na)
sdDistance(float fromPrice, float toPrice, int direction, float tick) =>
float raw = (toPrice - fromPrice) * direction
float ticks = raw / tick
float nearest = math.round(ticks)
float tolerance = math.min(tick * 1e-5, 8 * 2.220446049250313e-16 * math.max(math.abs(fromPrice), math.abs(toPrice)))
sdSign(math.abs(raw - nearest * tick) - tolerance) <= 0 ? nearest * tick : raw
sdPoolObservation(int side, float level, float h, float l, float c, float minimumDepth) =>
bool traversed = side == 1 ? sdCompare(h, level) > 0 : sdCompare(l, level) < 0
bool raid = traversed and (side == 1 ? sdCompare(h, level + minimumDepth) >= 0 and sdCompare(c, level) < 0 : sdCompare(l, level - minimumDepth) <= 0 and sdCompare(c, level) > 0)
[traversed, raid]
sdPlanGeometry(int direction, float entry, float anchor, float atrValue, float tick, float buffer, float r1, float r2, float r3) =>
bool available = not na(entry) and not na(anchor) and not na(atrValue) and sdSign(atrValue) > 0 and sdSign(tick) > 0
bool ordered = r1 > 0 and sdCompare(r2, r1) > 0 and sdCompare(r3, r2) > 0
float rawStop = anchor - direction * buffer * atrValue
float minimumStop = entry - direction * tick
float sl = sdTickOut(direction == 1 ? math.min(rawStop, minimumStop) : math.max(rawStop, minimumStop), -direction, tick)
float risk = sdDistance(sl, entry, direction, tick)
float t1 = sdTickOut(entry + direction * r1 * risk, direction, tick)
float t2 = sdTickOut(entry + direction * r2 * risk, direction, tick)
float t3 = sdTickOut(entry + direction * r3 * risk, direction, tick)
bool valid = available and ordered and sdCompare(risk, tick) >= 0 and sdCompare((t1 - entry) * direction, 0) > 0 and sdCompare((t2 - t1) * direction, 0) > 0 and sdCompare((t3 - t2) * direction, 0) > 0
string reason = not available ? "Unavailable price / ATR / tick" : not ordered ? "Require 0 < TP1 < TP2 < TP3" : not valid ? "Rounded plan geometry invalid" : "Plan valid"
[sl, risk, t1, t2, t3, valid, reason]
sdOutcome(int direction, float stop, float target, float o, float h, float l, float c, int entryBar, int currentBar, int maximumHold) =>
bool ended = false
float price = na
string why = ""
bool ambiguous = false
if currentBar > entryBar and sdBarValid(o, h, l, c)
bool stopGap = sdCompare(o, stop) * direction <= 0
bool targetGap = sdCompare(o, target) * direction >= 0
bool stopTouch = direction == 1 ? sdCompare(l, stop) <= 0 : sdCompare(h, stop) >= 0
bool targetTouch = direction == 1 ? sdCompare(h, target) >= 0 : sdCompare(l, target) <= 0
if stopGap
price := o
why := "GAP STOP"
else if targetGap
price := target
why := "TARGET"
else if stopTouch
price := stop
why := "STOP"
ambiguous := targetTouch
else if targetTouch
price := target
why := "TARGET"
else if currentBar - entryBar >= maximumHold
price := c
why := "TIME"
ended := not na(price)
[ended, price, why, ambiguous]
sdSeriesJoin(bool enabled, int direction, int previousDirection, int birth, int previousBirth, bool previousInverted) =>
enabled and not previousInverted and direction == previousDirection and birth == previousBirth + 1
sdInversion(int direction, float closedPrice, float top, float bottom) =>
direction == 1 ? sdCompare(closedPrice, bottom) < 0 : sdCompare(closedPrice, top) > 0
sdEscape(string value) =>
str.replace_all(str.replace_all(str.replace_all(str.replace_all(value, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\t", "\\t")
sdNumber(float value) =>
string result = "null"
if not na(value)
if sdSign(value) == 0
result := "0"
else
int exponent = int(math.floor(math.log10(math.abs(value))))
result := exponent < -6 or exponent > 12 ? str.tostring(value / math.pow(10, exponent), "0.################") + "e" + str.tostring(exponent) : str.tostring(value, "0.################")
result
sdMacroWindow(string mode, int localHour, int localMinute) =>
mode == "Off" or (mode == "45–15" ? localMinute >= 45 or localMinute < 15 : mode == "50–10" ? localMinute >= 50 or localMinute < 10 : localHour == 9 and localMinute >= 30)
sdMilestones(int direction, float entry, float stop, float tp1, float tp2, float h, float l, bool previousOne, bool previousTwo, bool moveBreakeven, float tick) =>
bool one = previousOne or (direction == 1 ? sdCompare(h, tp1) >= 0 : sdCompare(l, tp1) <= 0)
bool two = previousTwo or (direction == 1 ? sdCompare(h, tp2) >= 0 : sdCompare(l, tp2) <= 0)
float nextStop = stop
if one and not previousOne and moveBreakeven
float be = sdTickOut(entry, -direction, tick)
nextStop := direction == 1 ? math.max(nextStop, be) : math.min(nextStop, be)
if two and not previousTwo
nextStop := direction == 1 ? math.max(nextStop, tp1) : math.min(nextStop, tp1)
[nextStop, one, two, one and not previousOne, two and not previousTwo]
sdQuantity(bool fixedMode, float fixedUnits, float cashBudget, float unitRisk, float unitNotional, float equity, float capPercent, float increment) =>
bool available = not na(unitRisk) and not na(unitNotional) and not na(equity) and not na(increment) and sdSign(unitRisk) > 0 and sdSign(unitNotional) > 0 and sdSign(equity) > 0 and sdSign(increment) > 0 and sdSign(capPercent) > 0 and (fixedMode ? sdSign(fixedUnits) > 0 : sdSign(cashBudget) > 0)
float requested = fixedMode ? fixedUnits : cashBudget / unitRisk
float capUnits = equity * capPercent / 100 / unitNotional
float steps = available ? math.min(requested, capUnits) / increment : 0
bool supported = steps <= 9e18
float nearest = supported ? math.round(steps) : 0
float tolerance = math.min(1e-5, 4 * 2.220446049250313e-16 * math.max(1, math.abs(steps)))
float normalized = sdSign(math.abs(steps - nearest) - tolerance) <= 0 ? nearest : steps
float quantity = available and supported ? math.floor(normalized) * increment : 0
float actualRisk = quantity * unitRisk
float notional = quantity * unitNotional
bool withinBudget = (fixedMode or sdCompare(actualRisk, cashBudget) <= 0) and sdCompare(notional, equity * capPercent / 100) <= 0
bool valid = available and supported and sdSign(quantity) > 0 and withinBudget
string reason = not available ? "Price / account conversion / sizing unavailable" : not supported ? "Quantity needs more than supported integer steps" : sdSign(quantity) <= 0 ? "No size fits risk / equity cap at minimum increment" : not withinBudget ? "Sizing arithmetic exceeded budget" : "Sizing valid"
[valid ? quantity : 0.0, valid ? actualRisk : float(na), valid ? notional : float(na), valid, reason]
sdPendingCloseDecision(int direction, float closedPrice, float zoneTop, float zoneBottom, float frozenStop) =>
bool inside = sdCompare(closedPrice, zoneBottom) >= 0 and sdCompare(closedPrice, zoneTop) <= 0
bool invalid = sdCompare(closedPrice, frozenStop) * direction < 0
[inside, invalid, inside and not invalid]
sdGapValid(float top, float bottom, float minimumSize) =>
not na(top) and not na(bottom) and not na(minimumSize) and sdCompare(top, bottom) > 0 and sdCompare(top, bottom + minimumSize) >= 0
sdDisplacement(int direction, float closedPrice, float extreme, float minimumDistance) =>
not na(closedPrice) and not na(extreme) and not na(minimumDistance) and sdCompare(closedPrice, extreme + direction * minimumDistance) * direction >= 0
// ── PURE RELIABILITY HELPERS END
if barstate.isfirst
if not chart.is_standard or not timeframe.isintraday
runtime.error("Sweep Desk requires standard time-based intraday candles.")
if not (inTp1 > 0 and inTp2 > inTp1 and inTp3 > inTp2)
runtime.error("Targets must satisfy 0 < TP1 < TP2 < TP3.")
if barstate.isconfirmed and not sdBarValid(open, high, low, close)
runtime.error("Sweep Desk: missing or malformed confirmed OHLC. No reliable price transition can be calculated.")
// ═══════════════════════════════════════════════════════════════════ ENGINE INPUTS (ta.* at global scope)
float atr = ta.atr(14)
float atrU = math.max(atr, syminfo.mintick)
float ph = ta.pivothigh(high, inPivLen, inPivLen)
float pl = ta.pivotlow(low, inPivLen, inPivLen)
float phS = ta.pivothigh(high, inShiftLeg, inShiftLeg)
float plS = ta.pivotlow(low, inShiftLeg, inShiftLeg)
float volMa = ta.sma(nz(volume, 0.0), 20)
bool volOK = not inSweepVol or (not na(volume) and sdSign(volume) > 0 and not na(volMa) and sdSign(volMa) > 0 and sdCompare(volume, inSweepVolK * volMa) >= 0)
bool kz1 = not na(time(timeframe.period, inKz1, inTZ))
bool kz2 = not na(time(timeframe.period, inKz2, inTZ))
bool kz3 = not na(time(timeframe.period, inKz3, inTZ))
bool inKz = kz1 or kz2 or kz3
string kzName = kz1 ? "London" : kz2 ? "NY AM" : kz3 ? "NY PM" : "outside killzones"
// previous day / week highs and lows — the [1] + lookahead_on pair = the last CLOSED period (never the forming one)
[pdStamp, pdh, pdl] = request.security(syminfo.tickerid, "D", [time[1], high[1], low[1]], lookahead = barmerge.lookahead_on)
[pwStamp, pwh, pwl] = request.security(syminfo.tickerid, "W", [time[1], high[1], low[1]], lookahead = barmerge.lookahead_on)
bool dayOK = timeframe.in_seconds() < timeframe.in_seconds("D")
bool weekOK = timeframe.in_seconds() < timeframe.in_seconds("W")
htfPhFn() => ta.pivothigh(high, inHtfLen, inHtfLen)
htfPlFn() => ta.pivotlow(low, inHtfLen, inHtfLen)
float htfPh = request.security(syminfo.tickerid, inHtf, htfPhFn()[1], lookahead = barmerge.lookahead_on)
float htfPl = request.security(syminfo.tickerid, inHtf, htfPlFn()[1], lookahead = barmerge.lookahead_on)
bool htfOK = timeframe.in_seconds(inHtf) > timeframe.in_seconds()
int htfMult = math.max(1, math.round(timeframe.in_seconds(inHtf) / timeframe.in_seconds()))
// CONFLUENCE FRAGMENT BEGIN
// Public confluence concepts, original deterministic definitions. No private DD grade or LRL formula.
gCF = "Confluence · transparent rules"
inCfPda = input.bool(true, "Require first-touch HTF FVG delivery", group = gCF, display = display.none,
tooltip = "A previously untouched, completed higher-timeframe 3-candle gap must be touched first. Its delivery context lasts N chart bars and can admit only one plan. This is our explicit PDA definition, not a private vendor formula.")
inCfHtf = input.timeframe("60", " HTF FVG source", group = gCF, display = display.none)
inCfPdaAge = input.int(6, " First-touch context lasts chart bars", minval = 1, maxval = 30, group = gCF, display = display.none)
inCfPdaLife = input.int(50, " Untouched gap lifetime · HTF periods", minval = 2, maxval = 200, group = gCF, display = display.none)
inCfSmt = input.bool(false, "Require paired-market SMT", group = gCF, display = display.none,
tooltip = "Select a meaningful comparison yourself. Uses strictly aligned, previously CLOSED chart and comparison bars. A missing bar resets the comparison window. Context is deliberately one chart bar late; no automatic correlation claim.")
inCfPair = input.symbol("BINANCE:BTCUSDT", " SMT comparison symbol", group = gCF, display = display.none)
inCfInverse = input.bool(false, " Inverse relationship", group = gCF, display = display.none)
inCfWindow = input.int(10, " SMT aligned-bar reference window", minval = 3, maxval = 100, group = gCF, display = display.none)
inCfSmtAge = input.int(6, " SMT context lasts chart bars", minval = 1, maxval = 30, group = gCF, display = display.none)
inCfMomentum = input.bool(false, "Require directional momentum candle", group = gCF, display = display.none)
inCfBody = input.float(0.5, " Body size ≥ ATR ×", minval = 0.0, maxval = 3.0, step = 0.1, group = gCF, display = display.none)
inCfLocation = input.float(0.7, " Directional close location ≥", minval = 0.5, maxval = 1.0, step = 0.05, group = gCF, display = display.none)
inCfVotes = input.int(0, "Minimum observed confluences · 0–4", minval = 0, maxval = 4, group = gCF, display = display.none,
tooltip = "One vote each: available first-touch HTF FVG, enabled aligned SMT, directional momentum, positive volume ≥ 1.3× its 20-bar mean. This is a transparent checklist, not an A+ grade, probability or optimized threshold. SMT supplies no vote while disabled.")
cfValid(float h, float l) => not na(h) and not na(l) and sdCompare(h, l) >= 0
cfOverlap(float h, float l, float top, float bot) => cfValid(h, l) and cfValid(top, bot) and sdCompare(l, top) <= 0 and sdCompare(h, bot) >= 0
cfPdaAction(int dir, float top, float bot, float h, float l, float c) =>
int action = 0
if not cfValid(h, l) or na(c) or sdCompare(c, h) > 0 or sdCompare(c, l) < 0 or not cfValid(top, bot) or sdCompare(top, bot) <= 0
action := -1
else if (dir == 1 and sdCompare(c, bot) < 0) or (dir == -1 and sdCompare(c, top) > 0)
action := -1
else if cfOverlap(h, l, top, bot)
action := 1
action
cfMomentum(int dir, float o, float h, float l, float c, float a, float minBody, float minLocation) =>
bool valid = cfValid(h, l) and not na(o) and not na(c) and sdCompare(o, l) >= 0 and sdCompare(o, h) <= 0 and sdCompare(c, l) >= 0 and sdCompare(c, h) <= 0 and sdCompare(h, l) > 0 and not na(a) and sdSign(a) > 0
valid and sdSign((c - o) * dir) > 0 and sdCompare(math.abs(c - o), minBody * a) >= 0 and sdCompare(dir == 1 ? (c - l) / (h - l) : (h - c) / (h - l), minLocation) >= 0
type CfPair
array<float> ownH
array<float> ownL
array<float> peerH
array<float> peerL
int stamp = na
int bullAt = na
int bearAt = na
bool ready = false
cfPairReset(CfPair p) =>
array.clear(p.ownH)
array.clear(p.ownL)
array.clear(p.peerH)
array.clear(p.peerL)
p.stamp := na
p.bullAt := na
p.bearAt := na
p.ready := false
cfPairStep(CfPair p, int sourceTime, int peerTime, int sourceEnd, int peerEnd, int interval, float ownH, float ownL, float peerH, float peerL, int window, bool inverse, int observedBar) =>
bool valid = interval > 0 and not na(sourceTime) and sourceTime == peerTime and sourceEnd == peerEnd and sourceEnd - sourceTime == interval and cfValid(ownH, ownL) and cfValid(peerH, peerL)
if not valid
cfPairReset(p)
else if na(p.stamp) or sourceTime != p.stamp
if not na(p.stamp) and sourceTime != p.stamp + interval
cfPairReset(p)
p.ready := array.size(p.ownH) >= window
if p.ready
bool ownNewLow = sdCompare(ownL, array.min(p.ownL)) < 0
bool ownNewHigh = sdCompare(ownH, array.max(p.ownH)) > 0
bool peerNoLow = sdCompare(peerL, array.min(p.peerL)) >= 0
bool peerNoHigh = sdCompare(peerH, array.max(p.peerH)) <= 0
if ownNewLow and (inverse ? peerNoHigh : peerNoLow)
p.bullAt := observedBar
if ownNewHigh and (inverse ? peerNoLow : peerNoHigh)
p.bearAt := observedBar
array.push(p.ownH, ownH)
array.push(p.ownL, ownL)
array.push(p.peerH, peerH)
array.push(p.peerL, peerL)
while array.size(p.ownH) > window
array.shift(p.ownH)
array.shift(p.ownL)
array.shift(p.peerH)
array.shift(p.peerL)
p.stamp := sourceTime
p.ready
type CfPda
float top
float bot
int dir
int formedAt
bool fresh = true
type CfDelivery
int bullAt = na
int bearAt = na
int bullSource = na
int bearSource = na
float bullTop = na
float bullBot = na
float bearTop = na
float bearBot = na
bool bullUsed = false
bool bearUsed = false
cfDeliveryObserve(CfDelivery d, CfPda z, float h, float l, float c, int at) =>
int action = cfPdaAction(z.dir, z.top, z.bot, h, l, c)
if z.fresh and action != 0
z.fresh := false
if action == 1
if z.dir == 1
d.bullAt := at
d.bullSource := z.formedAt
d.bullTop := z.top
d.bullBot := z.bot
d.bullUsed := false
else
d.bearAt := at
d.bearSource := z.formedAt
d.bearTop := z.top
d.bearBot := z.bot
d.bearUsed := false
cfDeliveryReady(CfDelivery d, int dir, int at, int ttl) =>
int touched = dir == 1 ? d.bullAt : d.bearAt
bool used = dir == 1 ? d.bullUsed : d.bearUsed
not used and not na(touched) and at >= touched and at - touched < ttl
cfDeliveryConsume(CfDelivery d, int dir) =>
if dir == 1
d.bullUsed := true
else
d.bearUsed := true
var array<CfPda> cfPdas = array.new<CfPda>()
var CfDelivery cfDelivery = CfDelivery.new()
var CfPair cfPair = CfPair.new(array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>())
var int cfLastHtf = na
float cfChartSeconds = timeframe.in_seconds()
float cfHtfSeconds = timeframe.in_seconds(inCfHtf)
bool cfHtfAllowed = not na(cfChartSeconds) and not na(cfHtfSeconds) and cfHtfSeconds > cfChartSeconds
[cfH1, cfL1, cfH3, cfL3, cfHtfTime, cfHtfEnd] = request.security(syminfo.tickerid, inCfHtf, [high[1], low[1], high[3], low[3], time[1], time_close[1]], lookahead = barmerge.lookahead_on)
[cfPeerH, cfPeerL, cfPeerTime, cfPeerEnd] = request.security(inCfPair, timeframe.period, [high[1], low[1], time[1], time_close[1]], gaps = barmerge.gaps_on, lookahead = barmerge.lookahead_on, ignore_invalid_symbol = true)
bool cfSourceReady = cfHtfAllowed and cfValid(cfH1, cfL1) and cfValid(cfH3, cfL3) and not na(cfHtfEnd) and cfHtfEnd <= time_close
bool cfMomentumLong = cfMomentum(1, open, high, low, close, atr, inCfBody, inCfLocation)
bool cfMomentumShort = cfMomentum(-1, open, high, low, close, atr, inCfBody, inCfLocation)
bool cfVolume = not na(volume) and sdSign(volume) > 0 and not na(volMa) and sdSign(volMa) > 0 and sdCompare(volume, 1.3 * volMa) >= 0
if barstate.isconfirmed
if inCfSmt
cfPairStep(cfPair, time[1], cfPeerTime, time_close[1], cfPeerEnd, int(cfChartSeconds * 1000), high[1], low[1], cfPeerH, cfPeerL, inCfWindow, inCfInverse, bar_index)
else
cfPairReset(cfPair)
if not cfSourceReady
array.clear(cfPdas)
cfDelivery.bullAt := na
cfDelivery.bearAt := na
else
if na(cfLastHtf) or cfHtfTime != cfLastHtf
// A chart bar straddling formation cannot establish post-formation freshness: skip that birth.
if cfHtfEnd <= time
if sdCompare(cfL1, cfH3) > 0
array.push(cfPdas, CfPda.new(cfL1, cfH3, 1, cfHtfEnd))
if sdCompare(cfH1, cfL3) < 0
array.push(cfPdas, CfPda.new(cfL3, cfH1, -1, cfHtfEnd))
cfLastHtf := cfHtfTime
// Oldest-to-newest: if several fresh zones are touched together, the most recently formed wins.
for z in cfPdas
bool expired = time_close - z.formedAt > cfHtfSeconds * 1000 * inCfPdaLife
if expired
z.fresh := false
else
cfDeliveryObserve(cfDelivery, z, high, low, close, bar_index)
if not na(cfDelivery.bullBot) and sdCompare(close, cfDelivery.bullBot) < 0
cfDelivery.bullAt := na
if not na(cfDelivery.bearTop) and sdCompare(close, cfDelivery.bearTop) > 0
cfDelivery.bearAt := na
while array.size(cfPdas) > 40
array.shift(cfPdas)
sdConfluencePda(int dir) => cfSourceReady and cfDeliveryReady(cfDelivery, dir, bar_index, inCfPdaAge)
sdConfluenceSmt(int dir) =>
int at = dir == 1 ? cfPair.bullAt : cfPair.bearAt
inCfSmt and cfPair.ready and not na(at) and bar_index >= at and bar_index - at < inCfSmtAge
sdConfluenceMomentum(int dir) => dir == 1 ? cfMomentumLong : cfMomentumShort
sdConfluenceVotes(int dir) => (sdConfluencePda(dir) ? 1 : 0) + (sdConfluenceSmt(dir) ? 1 : 0) + (sdConfluenceMomentum(dir) ? 1 : 0) + (cfVolume ? 1 : 0)
sdConfluenceAllowed(int dir) => (not inCfPda or sdConfluencePda(dir)) and (not inCfSmt or sdConfluenceSmt(dir)) and (not inCfMomentum or sdConfluenceMomentum(dir)) and sdConfluenceVotes(dir) >= inCfVotes
sdConfluenceReason(int dir) =>
string reason = "confluence ready"
if inCfPda and not cfSourceReady
reason := cfHtfAllowed ? "HTF FVG source unavailable" : "HTF FVG timeframe must be higher than chart"
else if inCfPda and not sdConfluencePda(dir)
reason := "no unused first-touch HTF FVG delivery"
else if inCfSmt and not cfPair.ready
reason := "SMT waiting for aligned closed-bar history"
else if inCfSmt and not sdConfluenceSmt(dir)
reason := "no recent paired-market SMT"
else if inCfMomentum and not sdConfluenceMomentum(dir)
reason := "confirmation body lacks directional momentum"
else if sdConfluenceVotes(dir) < inCfVotes
reason := "observed confluence count below minimum"
reason
sdConfluenceSummary(int dir) => "PDA " + (sdConfluencePda(dir) ? "first touch" : "unavailable/used") + " · SMT " + (not inCfSmt ? "off" : sdConfluenceSmt(dir) ? "confirmed -1 bar" : "waiting") + " · " + str.tostring(sdConfluenceVotes(dir)) + "/4 observed"
sdConfluenceConsume(int dir) =>
if sdConfluencePda(dir)
cfDeliveryConsume(cfDelivery, dir)
// CONFLUENCE FRAGMENT END
bool sdMacroAllowed = sdMacroWindow(inMacro, hour(time_close, inTZ), minute(time_close, inTZ))
sdAdmissionAllowed(int direction) => sdMacroAllowed and sdConfluenceAllowed(direction)
sdAdmissionReason(int direction) => not sdMacroAllowed ? "Confirmation outside selected macro" : sdConfluenceReason(direction)
// ═══════════════════════════════════════════════════════════════════ POOLS (resting liquidity)
type Pool
float px
int bar
int side // +1 = buy-side liquidity (a high) · −1 = sell-side liquidity (a low)
string kind // SWG · EQH · EQL · SES · PDH · PDL · PWH · PWL
int touches = 1
bool swept = false
bool faded = false
int sweptBar = na
line ln
line ln2 // glow (strong pools only)
label lb
var array<Pool> pools = array.new<Pool>()
poolColor(int side, bool swept) =>
color base = side == 1 ? th.down : th.up
swept ? color.new(base, 78) : color.new(base, 35)
poolDelete(Pool p) =>
if not na(p.ln)
line.delete(p.ln)
if not na(p.ln2)
line.delete(p.ln2)
if not na(p.lb)
label.delete(p.lb)
// register (or merge into an equal pool); draw only outside Minimal
poolAdd(float px, int bar, int side, string kind) =>
bool merged = false
if kind == "SWG"
for p in pools
if not p.swept and (p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL") and p.side == side and sdCompare(px, p.px - inEqTol * atrU) >= 0 and sdCompare(px, p.px + inEqTol * atrU) <= 0
p.px := side == 1 ? math.max(p.px, px) : math.min(p.px, px)
p.kind := side == 1 ? "EQH" : "EQL"
p.touches := p.touches + 1
p.bar := math.min(p.bar, bar)
merged := true
break
if not merged
// one PDH / PDL / PWH / PWL at a time — replace the previous period's pool
if (kind == "PDH" or kind == "PDL" or kind == "PWH" or kind == "PWL") and array.size(pools) > 0
for i = array.size(pools) - 1 to 0
Pool q = array.get(pools, i)
if q.kind == kind
poolDelete(q)
array.remove(pools, i)
Pool p = Pool.new(px = px, bar = bar, side = side, kind = kind)
array.push(pools, p)
merged
// Budget caps swing pools; swing and session pools age out. Swept pools linger forty bars.
poolPrune() =>
int up = 0
int dn = 0
for i = array.size(pools) - 1 to 0
if i < 0
break
Pool p = array.get(pools, i)
bool isSwing = p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL" or p.kind == "HTF"
bool old = p.swept ? (bar_index - nz(p.sweptBar, bar_index)) > 40 : ((isSwing or p.kind == "SES") and (bar_index - p.bar) > inPoolAge)
if old
poolDelete(p)
array.remove(pools, i)
else if not p.swept and isSwing
if p.side == 1
up += 1
else
dn += 1
// over budget → drop the oldest resting swing pools
int i = 0
while i < array.size(pools)
int sizeBefore = array.size(pools)
Pool p = array.get(pools, i)
bool isSwing = p.kind == "SWG" or p.kind == "EQH" or p.kind == "EQL" or p.kind == "HTF"
if not p.swept and isSwing
if p.side == 1 and up > inMaxPools
poolDelete(p)
array.remove(pools, i)
up -= 1
else if p.side == -1 and dn > inMaxPools
poolDelete(p)
array.remove(pools, i)
dn -= 1
if array.size(pools) == sizeBefore
i += 1
// killzone session pools: when a killzone ends, its high / low rest as liquidity
var float k1H = na
var float k1L = na
var float k2H = na
var float k2L = na
var float k3H = na
var float k3L = na
if kz1
k1H := kz1[1] ? math.max(nz(k1H, high), high) : high
k1L := kz1[1] ? math.min(nz(k1L, low), low) : low
if kz2
k2H := kz2[1] ? math.max(nz(k2H, high), high) : high
k2L := kz2[1] ? math.min(nz(k2L, low), low) : low
if kz3
k3H := kz3[1] ? math.max(nz(k3H, high), high) : high
k3L := kz3[1] ? math.min(nz(k3L, low), low) : low
if barstate.isconfirmed and not na(atr)
if not na(ph)
poolAdd(ph, bar_index - inPivLen, 1, "SWG")
if not na(pl)
poolAdd(pl, bar_index - inPivLen, -1, "SWG")
if inPoolSes
if kz1[1] and not kz1 and not na(k1H)
poolAdd(k1H, bar_index - 1, 1, "SES")
poolAdd(k1L, bar_index - 1, -1, "SES")
if kz2[1] and not kz2 and not na(k2H)
poolAdd(k2H, bar_index - 1, 1, "SES")
poolAdd(k2L, bar_index - 1, -1, "SES")
if kz3[1] and not kz3 and not na(k3H)
poolAdd(k3H, bar_index - 1, 1, "SES")
poolAdd(k3L, bar_index - 1, -1, "SES")
if inPoolPD and dayOK and not na(pdStamp) and (na(pdStamp[1]) or pdStamp != pdStamp[1])
poolAdd(pdh, bar_index, 1, "PDH")
poolAdd(pdl, bar_index, -1, "PDL")
if inPoolPW and weekOK and not na(pwStamp) and (na(pwStamp[1]) or pwStamp != pwStamp[1])
poolAdd(pwh, bar_index, 1, "PWH")
poolAdd(pwl, bar_index, -1, "PWL")
if inHtfOn and htfOK
if not na(htfPh) and (na(htfPh[1]) or htfPh != htfPh[1])
poolAdd(htfPh, bar_index - inHtfLen * htfMult, 1, "HTF")
if not na(htfPl) and (na(htfPl[1]) or htfPl != htfPl[1])
poolAdd(htfPl, bar_index - inHtfLen * htfMult, -1, "HTF")
poolPrune()
// ═══════════════════════════════════════════════════════════════════ SHORT-TERM SWINGS (the swing a shift must break)
var float stHi = na
var int stHiBar = na
var float stLo = na
var int stLoBar = na
if not na(phS)
stHi := phS
stHiBar := bar_index - inShiftLeg
if not na(plS)
stLo := plS
stLoBar := bar_index - inShiftLeg
// ═══════════════════════════════════════════════════════════════════ FVG REGISTRY (for the inversion model)
// every 3-candle gap ≥ the minimum size is tracked until a bar CLOSES through it the wrong way (inversion) or it ages out
type Gap
float top
float bot
int dir // +1 bullish gap · −1 bearish gap
int bar
bool inverted = false
int invBar = na
int lastBar = na
int members = 1
var array<Gap> gaps = array.new<Gap>()
gapRegister(float top, float bot, int direction, int birth) =>
bool joined = false
if inGapMode == "Series" and array.size(gaps) > 0
Gap previous = array.get(gaps, array.size(gaps) - 1)
if sdSeriesJoin(true, direction, previous.dir, birth, previous.lastBar, previous.inverted)
previous.top := math.max(previous.top, top)
previous.bot := math.min(previous.bot, bot)
previous.lastBar := birth
previous.members += 1
joined := true
if not joined
array.push(gaps, Gap.new(top = top, bot = bot, dir = direction, bar = birth, lastBar = birth))
if barstate.isconfirmed and not na(atr)
if sdGapValid(low, high[2], inFvgMin * atrU)
gapRegister(low, high[2], 1, bar_index - 1)
if sdGapValid(low[2], high, inFvgMin * atrU)
gapRegister(low[2], high, -1, bar_index - 1)
for i = array.size(gaps) - 1 to 0
if i < 0
break
Gap g = array.get(gaps, i)
if bar_index - g.bar > 200
array.remove(gaps, i)
else if not g.inverted
if sdInversion(g.dir, close, g.top, g.bot)
g.inverted := true
g.invBar := bar_index
while array.size(gaps) > 60
array.shift(gaps)
// sweep quality 0..1 — DESCRIPTIVE (depth of the raid, strength of the pool, killzone, volume). Never gates anything.
poolWeight(string kind) => kind == "SWG" ? 0.45 : (kind == "EQH" or kind == "EQL") ? 0.8 : kind == "SES" ? 0.75 : 1.0
sweepQuality(float depthAtr, string kind, bool kz) =>
float volTerm = volMa > 0 ? math.min(nz(volume, 0.0) / volMa / 2.0, 1.0) : 0.5
clamp01(0.35 * math.min(depthAtr / 0.5, 1.0) + 0.35 * poolWeight(kind) + 0.15 * (kz ? 1.0 : 0.0) + 0.15 * volTerm)
// ═══════════════════════════════════════════════════════════════════ THE SETUP STATE MACHINE
// states: 0 SCANNING · 1 SWEPT · 2 SHIFTED · 3 ARMED (waiting for the fill) · 4 FILLED (trade open)
type Setup
int dir = 0
int state = 0
float poolPx = na
string poolKind = ""
float ext = na // the sweep extreme — structural invalidation
int sweepBar = na
bool sweepKz = false
float sweepDepth = na
float oppSwing = na
int oppBar = na
int mssBar = na
float mssPx = na
float fvgTop = na
float fvgBot = na
int fvgBar = na
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
float tp3 = na
float tpLiq = na
string tpLiqKind = ""
float tpFinal = na
int armBar = na
int fillBar = na
bool tp1Hit = false
bool tp2Hit = false
string model = "" // "iFVG" or "MSS"
float quality = na // sweep quality 0..1 (descriptive, frozen at the raid)
int armTime = na
int entryTime = na
int endTime = na
int planId = na
bool planValid = false
string planReason = "No plan"
bool ambiguous = false
string confluenceSummary = ""
string stopBasis = ""
type Stat
int swept = 0
int shifted = 0
int armed = 0
int filled = 0
int n = 0
int wins = 0
int scratch = 0
float sumR = 0.0
int ambiguous = 0
int nSwg = 0
int wSwg = 0
int nEq = 0
int wEq = 0
int nSes = 0
int wSes = 0
int nHtf = 0
int wHtf = 0
int nIfvg = 0
int wIfvg = 0
int nMss = 0
int wMss = 0
var Setup s = Setup.new()
var Stat st = Stat.new()
// Frozen completed paper episodes; never aliases the mutable current Setup.
type SdPlanRecord
int planId
int direction
string model
string poolKind
float poolPrice
float zoneHigh
float zoneLow
float entry
float initialStop
float target
float risk
int armTime
int entryTime
int endTime
float exitPrice
float grossR
string outcome
bool ambiguous
string confluences
var array<SdPlanRecord> sdHistory = array.new<SdPlanRecord>()
var int sdLastClosedTime = na
var float sdLastClosedPrice = na
if barstate.isconfirmed
sdLastClosedTime := time_close
sdLastClosedPrice := close
// per-bar event record (a fresh object every bar; functions mutate its fields — globals cannot be rebound in functions)
type Ev
bool sweep = false
bool shift = false
bool armed = false
bool filled = false
bool exit = false
bool cancel = false
bool tp1 = false
bool tp2 = false
string why = ""
float r = na
float px = na
int dir = 0
Ev ev = Ev.new()
// nearest resting pool on the opposite side, at least minR × r away → the liquidity target
liqTarget(int dir, float entry, float r) =>
float best = na
string bk = ""
for p in pools
if not p.swept and (inTargetQuality == "Any fresh pool" or p.touches >= 2)
float targetPx = sdTickOut(p.px, -dir, syminfo.mintick)
if dir == 1 and p.side == 1 and sdCompare(targetPx, entry + inTpLiqMinR * r) >= 0
if na(best) or sdCompare(targetPx, best) < 0
best := targetPx
bk := p.kind
if dir == -1 and p.side == -1 and sdCompare(targetPx, entry - inTpLiqMinR * r) <= 0
if na(best) or sdCompare(targetPx, best) > 0
best := targetPx
bk := p.kind
[best, bk]
// FVG search inside the shift leg (offsets 0..len): bull = low[i] > high[i+2]
findFvg(int dir, int len) =>
float top = na
float bot = na
int at = na
int lim = math.min(math.max(len, 0), 60)
for i = 0 to lim
bool bull = dir == 1 and sdGapValid(low[i], high[i + 2], inFvgMin * atrU)
bool bear = dir == -1 and sdGapValid(low[i + 2], high[i], inFvgMin * atrU)
if bull or bear
float t = bull ? low[i] : low[i + 2]
float b = bull ? high[i + 2] : high[i]
if sdGapValid(t, b, inFvgMin * atrU)
if inFvgPick == "Nearest"
top := t
bot := b
at := bar_index - i
break
else
top := t
bot := b
at := bar_index - i
[top, bot, at]
setLevels(int dir, float entry, bool checkAdmission = true) =>
float zoneFar = dir == 1 ? s.fvgBot : s.fvgTop
bool extNear = sdCompare(entry, s.ext - inSlMaxAtr * atrU) >= 0 and sdCompare(entry, s.ext + inSlMaxAtr * atrU) <= 0
s.stopBasis := inSlMode == "Sweep extreme" ? "sweep extreme" : inSlMode == "Zone far edge" ? "zone far edge" : extNear ? "Auto: sweep extreme" : "Auto: zone far edge"
float anchor = inSlMode == "Sweep extreme" ? s.ext : inSlMode == "Zone far edge" ? zoneFar : (extNear ? s.ext : zoneFar)
[slPx, risk, t1, t2, t3, valid, reason] = sdPlanGeometry(dir, entry, anchor, atrU, syminfo.mintick, inSlBuf, inTp1, inTp2, inTp3)
s.entry := entry
s.sl := slPx
s.sl0 := slPx
s.r := risk
s.tp1 := t1
s.tp2 := t2
s.tp3 := t3
[lq, lk] = liqTarget(dir, entry, risk)
s.tpLiq := lq
s.tpLiqKind := lk
s.tpFinal := inPrimary == "Liquidity" and not na(lq) ? lq : t3
s.planValid := valid and (not checkAdmission or sdAdmissionAllowed(dir))
s.planReason := checkAdmission and not sdAdmissionAllowed(dir) ? sdAdmissionReason(dir) : reason
s.ambiguous := false
valid
closeTrade(float px, string why) =>
float rr = (px - s.entry) * s.dir / s.r
ev.exit := true
ev.why := why
ev.r := rr
ev.px := px
ev.dir := s.dir
s.endTime := time_close
array.unshift(sdHistory, SdPlanRecord.new(s.planId, ev.dir, s.model, s.poolKind, s.poolPx, s.fvgTop, s.fvgBot, s.entry, s.sl0, s.tpFinal, s.r, s.armTime, s.entryTime, s.endTime, px, rr, why, s.ambiguous, s.confluenceSummary))
if array.size(sdHistory) > 60
array.pop(sdHistory)
if s.ambiguous
st.ambiguous += 1
bool win = rr >= 0.05
st.n := st.n + 1
st.wins := st.wins + (win ? 1 : 0)
st.scratch := st.scratch + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if s.poolKind == "SWG"
st.nSwg := st.nSwg + 1
st.wSwg := st.wSwg + (win ? 1 : 0)
else if s.poolKind == "EQH" or s.poolKind == "EQL"
st.nEq := st.nEq + 1
st.wEq := st.wEq + (win ? 1 : 0)
else if s.poolKind == "SES"
st.nSes := st.nSes + 1
st.wSes := st.wSes + (win ? 1 : 0)
else
st.nHtf := st.nHtf + 1
st.wHtf := st.wHtf + (win ? 1 : 0)
if s.model == "iFVG"
st.nIfvg := st.nIfvg + 1
st.wIfvg := st.wIfvg + (win ? 1 : 0)
else
st.nMss := st.nMss + 1
st.wMss := st.wMss + (win ? 1 : 0)
s.state := 0
s.dir := 0
cancelSetup(string why) =>
ev.cancel := true
ev.why := why
ev.dir := s.dir
s.endTime := time_close
s.state := 0
s.dir := 0
bool dirL = inDir != "Short only"
bool dirS = inDir != "Long only"
float observedRaidHi = na
float observedRaidLo = na
string observedKindHi = ""
string observedKindLo = ""
if barstate.isconfirmed and not na(atr)
for p in pools
if not p.swept
[traversed, raid] = sdPoolObservation(p.side, p.px, high, low, close, inSweepMin * atrU)
if traversed
p.swept := true
p.sweptBar := bar_index
if raid and (inPoolMin == "Any pool" or p.kind != "SWG")
if p.side == 1 and (na(observedRaidHi) or sdCompare(p.px, observedRaidHi) > 0)
observedRaidHi := p.px
observedKindHi := p.kind
if p.side == -1 and (na(observedRaidLo) or sdCompare(p.px, observedRaidLo) < 0)
observedRaidLo := p.px
observedKindLo := p.kind
if barstate.isconfirmed and not na(atr) and bar_index > 50
// FILLED: next-bar open ordering, then ambiguous range, then timeout. New stops apply next bar.
if s.state == 4
int d = s.dir
[ended, exitPx, outcome, ambiguous] = sdOutcome(d, s.sl, s.tpFinal, open, high, low, close, s.fillBar, bar_index, inMaxTrade)
if ended
s.ambiguous := s.ambiguous or ambiguous
string reason = outcome == "TARGET" ? ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "TP-LIQ" : "TP3") : outcome == "STOP" ? (s.tp2Hit ? "TRAIL" : (s.tp1Hit and inBE) ? "BE" : "SL") : outcome
closeTrade(exitPx, reason + (ambiguous ? " · AMBIGUOUS" : ""))
else
[nextStop, one, two, newOne, newTwo] = sdMilestones(d, s.entry, s.sl, s.tp1, s.tp2, high, low, s.tp1Hit, s.tp2Hit, inBE, syminfo.mintick)
s.sl := nextStop
s.tp1Hit := one
s.tp2Hit := two
ev.tp1 := newOne
ev.tp2 := newTwo
// ── 3 · ARMED: waiting for the fill
else if s.state == 3
int d = s.dir
[closeInsideZone, invalid, closeEntryAllowed] = sdPendingCloseDecision(d, close, s.fvgTop, s.fvgBot, s.sl)
bool missed = d == 1 ? sdCompare(high, s.tp1) >= 0 : sdCompare(low, s.tp1) <= 0
bool fill = false
float fillPx = s.entry
if inEntryMode == "Close inside"
fill := closeEntryAllowed
fillPx := close
else
fill := d == 1 ? sdCompare(low, s.entry) <= 0 : sdCompare(high, s.entry) >= 0
fillPx := d == 1 ? math.min(s.entry, open) : math.max(s.entry, open)
bool gapPast = inEntryMode != "Close inside" and (d == 1 ? sdCompare(open, s.sl) <= 0 : sdCompare(open, s.sl) >= 0)
if inEntryMode == "Close inside" and invalid
cancelSetup("structural stop lost before close entry")
else if fill and gapPast
// Paper convention: limit and initial stop resolve at the same known open; 0R before costs, not a promised live scratch.
s.entry := open
s.state := 4
s.fillBar := bar_index
s.entryTime := time
st.filled := st.filled + 1
ev.filled := true
ev.dir := d
closeTrade(open, "GAP")
else if fill
if inEntryMode == "Close inside"
setLevels(d, fillPx, false)
else
// A better limit fill changes actual risk, never the already-resting bracket.
s.entry := fillPx
s.r := sdDistance(s.sl0, fillPx, d, syminfo.mintick)
s.planValid := sdCompare(s.r, 0) > 0
if not s.planValid
cancelSetup("Plan rejected: " + s.planReason)
else
s.state := 4
s.fillBar := bar_index
s.entryTime := time_close
s.endTime := na
st.filled += 1
ev.filled := true
ev.dir := d
// A close entry cannot be stopped by earlier wicks. Resting limits may fill intrabar.
if inEntryMode != "Close inside"
bool knownOpenFill = sdCompare(open, fillPx) == 0
if knownOpenFill
[endedAtOpenBar, priceAtOpenBar, reasonAtOpenBar, ambiguousAtOpenBar] = sdOutcome(d, s.sl, s.tpFinal, open, high, low, close, bar_index - 1, bar_index, inMaxTrade)
if endedAtOpenBar
s.ambiguous := ambiguousAtOpenBar
closeTrade(priceAtOpenBar, reasonAtOpenBar == "TARGET" ? ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "TP-LIQ" : "TP3") : "SL" + (ambiguousAtOpenBar ? " · AMBIGUOUS" : ""))
else
[nextStop, one, two, newOne, newTwo] = sdMilestones(d, s.entry, s.sl, s.tp1, s.tp2, high, low, false, false, inBE, syminfo.mintick)
s.sl := nextStop
s.tp1Hit := one
s.tp2Hit := two
ev.tp1 := newOne
ev.tp2 := newTwo
else
bool stopSame = d == 1 ? sdCompare(low, s.sl) <= 0 : sdCompare(high, s.sl) >= 0
bool targetSame = d == 1 ? sdCompare(high, s.tpFinal) >= 0 : sdCompare(low, s.tpFinal) <= 0
// An intrabar limit has unknown entry-vs-target chronology. Stop-first; do not invent a same-bar target win.
s.ambiguous := stopSame or targetSame
if stopSame
closeTrade(s.sl, "SL · AMBIGUOUS")
else if invalid
cancelSetup("invalidated before fill")
else if s.model == "iFVG" and (d == 1 ? sdCompare(close, s.fvgBot) < 0 : sdCompare(close, s.fvgTop) > 0)
cancelSetup("inversion failed — closed back through the zone")
else if missed
cancelSetup("missed — ran to TP1 unfilled")
else if bar_index - s.armBar >= inExpire
cancelSetup("expired unfilled")
// ── 2 · SHIFTED: looking for the FVG in the leg
else if s.state == 2
int d = s.dir
bool failed = d == 1 ? sdCompare(close, s.ext) < 0 : sdCompare(close, s.ext) > 0
if failed
cancelSetup("sweep extreme lost after the shift")
else if bar_index - s.mssBar > inFvgWin
cancelSetup("no FVG after the shift")
else
[ft, fb, fa] = findFvg(d, bar_index - s.sweepBar)
if not na(ft)
s.fvgTop := ft
s.fvgBot := fb
s.fvgBar := fa
s.model := "MSS"
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (ft + fb) / 2 : (d == 1 ? ft : fb)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
// ── 1 · SWEPT: waiting for the confirmation (iFVG inversion and / or structure shift)
else if s.state == 1
int d = s.dir
// the invalidation level follows the leg's extreme while we wait
s.ext := d == 1 ? math.min(s.ext, low) : math.max(s.ext, high)
bool failed = d == 1 ? sdCompare(close, s.poolPx - inDispAtr * atrU) < 0 : sdCompare(close, s.poolPx + inDispAtr * atrU) > 0
if failed
cancelSetup("no rejection — price kept going")
else if bar_index - s.sweepBar > inShiftWin
cancelSetup("no shift in time")
else
bool useIfvg = inConfirm != "Structure shift (MSS)"
bool useMss = inConfirm != "FVG inversion (iFVG)"
bool shift = useMss and (d == 1 ? (not na(s.oppSwing) and sdCompare(close, s.oppSwing) > 0) : (not na(s.oppSwing) and sdCompare(close, s.oppSwing) < 0))
bool disp = sdDisplacement(d, close, s.ext, inDispAtr * atrU)
// iFVG: an opposing gap printed near the raid that THIS bar closed through → it inverts; the nearest one to price is the zone
float igTop = na
float igBot = na
int igBar = na
if useIfvg
for g in gaps
if g.inverted and g.invBar == bar_index and g.dir == -d and g.bar >= s.sweepBar - inIfvgBack
bool beyondExt = d == 1 ? sdCompare(g.bot, s.ext) > 0 : sdCompare(g.top, s.ext) < 0
if beyondExt and (na(igTop) or (d == 1 ? sdCompare(g.top, igTop) > 0 : sdCompare(g.bot, igBot) < 0))
igTop := g.top
igBot := g.bot
igBar := g.bar
if not na(igTop) and disp
s.fvgTop := igTop
s.fvgBot := igBot
s.fvgBar := igBar
s.model := "iFVG"
s.mssBar := bar_index
s.mssPx := d == 1 ? igTop : igBot
st.shifted := st.shifted + 1
ev.shift := true
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (igTop + igBot) / 2 : (d == 1 ? igTop : igBot)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
else if shift and disp
s.state := 2
s.mssBar := bar_index
s.mssPx := s.oppSwing
st.shifted := st.shifted + 1
ev.shift := true
// the FVG may already be in the leg — arm on the same bar
[ft, fb, fa] = findFvg(d, bar_index - s.sweepBar)
if not na(ft)
s.fvgTop := ft
s.fvgBot := fb
s.fvgBar := fa
s.model := "MSS"
float e = inEntryMode == "Confirmation close" ? close : inEntryMode == "CE (50%)" ? (ft + fb) / 2 : (d == 1 ? ft : fb)
setLevels(d, e)
if s.planValid
s.state := 3
s.armBar := bar_index
s.armTime := time_close
s.planId := time_close
s.entryTime := na
s.endTime := na
st.armed += 1
ev.armed := true
else
cancelSetup("Plan rejected: " + s.planReason)
if ev.armed
s.confluenceSummary := sdConfluenceSummary(s.dir)
sdConfluenceConsume(s.dir)
// Confirmation-close orders are accepted at this close; no earlier OHLC can end this plan.
if ev.armed and inEntryMode == "Confirmation close" and s.planValid
s.state := 4
s.fillBar := bar_index
s.entryTime := time_close
st.filled += 1
ev.filled := true
ev.dir := s.dir
// ── 0/1/2 · a fresh sweep replaces an idea that has not armed yet; an ARMED limit stands until filled / invalidated / missed / expired; a FILLED trade is never interrupted
if s.state != 4 and s.state != 3 and not ev.exit and not ev.cancel and (not inSweepKz or inKz) and volOK
float bestHi = observedRaidHi
float bestLo = observedRaidLo
string bkHi = observedKindHi
string bkLo = observedKindLo
// sell-side liquidity raided (a low taken, close back above) → long idea
if not na(bestLo) and dirL
s.dir := 1
s.state := 1
s.poolPx := bestLo
s.poolKind := bkLo
s.ext := low
s.sweepBar := bar_index
s.sweepKz := inKz
s.sweepDepth := (bestLo - low) / atrU
s.quality := sweepQuality(s.sweepDepth, bkLo, inKz)
s.model := ""
s.planId := na
s.planValid := false
s.planReason := "Waiting for confirmation"
s.confluenceSummary := ""
s.stopBasis := ""
s.entry := na
s.sl := na
s.sl0 := na
s.r := na
s.tp1 := na
s.tp2 := na
s.tp3 := na
s.tpLiq := na
s.tpFinal := na
s.armTime := na
s.entryTime := na
s.endTime := na
s.ambiguous := false
s.oppSwing := stHi
s.oppBar := stHiBar
s.tp1Hit := false
s.tp2Hit := false
st.swept := st.swept + 1
ev.sweep := true
else if not na(bestHi) and dirS
s.dir := -1
s.state := 1
s.poolPx := bestHi
s.poolKind := bkHi
s.ext := high
s.sweepBar := bar_index
s.sweepKz := inKz
s.sweepDepth := (high - bestHi) / atrU
s.quality := sweepQuality(s.sweepDepth, bkHi, inKz)
s.model := ""
s.planId := na
s.planValid := false
s.planReason := "Waiting for confirmation"
s.confluenceSummary := ""
s.stopBasis := ""
s.entry := na
s.sl := na
s.sl0 := na
s.r := na
s.tp1 := na
s.tp2 := na
s.tp3 := na
s.tpLiq := na
s.tpFinal := na
s.armTime := na
s.entryTime := na
s.endTime := na
s.ambiguous := false
s.oppSwing := stLo
s.oppBar := stLoBar
s.tp1Hit := false
s.tp2Hit := false
st.swept := st.swept + 1
ev.sweep := true
// ═══════════════════════════════════════════════════════════════════ CHART · pools, FVG, MSS, sweeps (drawings)
var array<box> fvgBoxes = array.new<box>()
var array<line> mssLines = array.new<line>()
var array<label> tags = array.new<label>()
capBoxes(int cap) =>
while array.size(fvgBoxes) > cap
box.delete(array.shift(fvgBoxes))
capLines(int cap) =>
while array.size(mssLines) > cap
line.delete(array.shift(mssLines))
capTags(int cap) =>
while array.size(tags) > cap
label.delete(array.shift(tags))
bool showPools = vClean
bool showTags = vPro
// a zone drawn as four stacked bands, densest at the edge price touches first (top for a long, bottom for a short), plus an outline
gradZone(int dir, float top, float bot, int x1, int x2) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = dir == 1 ? 80 + k * 4 : 92 - k * 4
array.push(fvgBoxes, box.new(x1, bTop, x2, bBot, border_color = color.new(th.bg, 100), bgcolor = color.new(dir == 1 ? th.up : th.down, alpha)))
array.push(fvgBoxes, box.new(x1, top, x2, bot, border_color = color.new(dir == 1 ? th.up : th.down, 40), bgcolor = color.new(th.bg, 100)))
if barstate.isconfirmed
if ev.sweep and showTags
array.push(tags, label.new(bar_index, s.dir == 1 ? low : high, (s.dir == 1 ? "SSL swept · " : "BSL swept · ") + s.poolKind, style = s.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(th.bg, 20), textcolor = s.dir == 1 ? th.up : th.down, size = size.tiny))
if ev.shift and vClean and s.model == "MSS"
array.push(mssLines, line.new(s.oppBar, s.mssPx, bar_index, s.mssPx, color = color.new(s.dir == 1 ? th.up : th.down, 25), style = line.style_dashed, width = 1))
if showTags
array.push(tags, label.new(bar_index, s.mssPx, "MSS", style = s.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 20), textcolor = s.dir == 1 ? th.up : th.down, size = size.tiny))
if ev.armed and vClean
gradZone(s.dir, s.fvgTop, s.fvgBot, s.fvgBar - 1, bar_index + inExtend)
if showTags
array.push(tags, label.new(s.fvgBar - 1, s.dir == 1 ? s.fvgTop : s.fvgBot, s.model == "iFVG" ? "iFVG ↺" : "FVG", style = label.style_label_right, color = color.new(th.bg, 100), textcolor = color.new(s.dir == 1 ? th.up : th.down, 20), size = size.tiny))
capBoxes(40)
capLines(30)
capTags(60)
// pool lines: created once; x2 refreshed every 20 bars while resting (cheap); frozen + faded ONCE when swept.
// Far pools are deleted on the last bar, not just hidden — TradingView's auto-scale counts every line object.
if barstate.isconfirmed or (barstate.islastconfirmedhistory or barstate.islast)
for p in pools
if showPools
if na(p.ln)
p.ln := line.new(math.max(p.bar, bar_index - 250), p.px, bar_index + inExtend + 20, p.px, color = poolColor(p.side, false), width = p.kind == "EQH" or p.kind == "EQL" ? 2 : 1, style = line.style_solid)
if inGlow and p.kind != "SWG"
p.ln2 := line.new(math.max(p.bar, bar_index - 250), p.px, bar_index + inExtend + 20, p.px, color = color.new(p.side == 1 ? th.down : th.up, 88), width = 5, style = line.style_solid)
if p.swept and not p.faded
line.set_x2(p.ln, nz(p.sweptBar, bar_index))
line.set_color(p.ln, poolColor(p.side, true))
line.set_style(p.ln, line.style_dotted)
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
p.faded := true
else if not p.swept and bar_index % 20 == 0
line.set_x2(p.ln, bar_index + inExtend + 20)
if not na(p.ln2)
line.set_x2(p.ln2, bar_index + inExtend + 20)
else
if not na(p.ln)
line.delete(p.ln)
p.ln := na
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
// focus + labels: last bar only (display)
if (barstate.islastconfirmedhistory or barstate.islast)
for p in pools
bool near = math.abs(p.px - close) <= inPoolNear * atrU
if showPools and near
if not na(p.ln) and not p.swept
line.set_x2(p.ln, bar_index + inExtend)
if not na(p.ln2)
line.set_x2(p.ln2, bar_index + inExtend)
if showTags and not p.swept
string ptxt = (p.side == 1 ? "BSL" : "SSL") + " · " + p.kind + (p.touches > 1 ? " ×" + str.tostring(p.touches) : "")
if na(p.lb)
p.lb := label.new(bar_index + inExtend, p.px, ptxt, style = label.style_label_left, color = color.new(th.bg, 100), textcolor = color.new(p.side == 1 ? th.down : th.up, 25), size = size.tiny)
else
label.set_xy(p.lb, bar_index + inExtend, p.px)
label.set_text(p.lb, ptxt)
else if not na(p.lb)
label.delete(p.lb)
p.lb := na
else
if not na(p.ln)
line.delete(p.ln)
p.ln := na
p.faded := false
if not na(p.ln2)
line.delete(p.ln2)
p.ln2 := na
if not na(p.lb)
label.delete(p.lb)
p.lb := na
// ═══════════════════════════════════════════════════════════════════ CHART · the ticket (plots + gradient zones)
bool pend = false
bool live = false
int zDir = s.dir != 0 ? s.dir : ev.dir
float pEntry = pend ? s.entry : na
float pStop = pend ? s.sl : na
float pTpF = pend ? s.tpFinal : na
float aEntry = live ? s.entry : na
float aStop = live ? s.sl : na
float aTp1 = live ? s.tp1 : na
float aTp2 = live ? s.tp2 : na
float aTpF = live ? s.tpFinal : na
float aTpLiq = live and not na(s.tpLiq) and sdCompare(s.tpLiq, s.tpFinal) != 0 ? s.tpLiq : na
plot(pEntry, "Pending · entry", color = color.new(th.txtHi, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(pStop, "Pending · stop", color = color.new(th.down, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(pTpF, "Pending · target", color = color.new(th.up, 45), linewidth = 1, style = plot.style_circles, display = display.pane)
plEnt = plot(aEntry, "Trade · entry", color = color.new(th.txtHi, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plStop = plot(aStop, "Trade · stop", color = color.new(th.down, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTp1, "Trade · TP1", color = color.new(th.up, 60), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTp2, "Trade · TP2", color = color.new(th.up, 45), linewidth = 1, style = plot.style_linebr, display = display.pane)
plFin = plot(aTpF, "Trade · final target", color = color.new(th.up, 10), linewidth = 1, style = plot.style_linebr, display = display.pane)
plot(aTpLiq, "Trade · liquidity target", color = color.new(th.accentHi, 25), linewidth = 1, style = plot.style_linebr, display = display.pane)
bool zonesOn = inZones and live
fill(plEnt, plStop, top_value = math.max(aEntry, aStop), bottom_value = math.min(aEntry, aStop),
top_color = zonesOn and zDir == 1 ? color.new(th.down, 78) : color.new(th.down, 100), bottom_color = zonesOn and zDir == -1 ? color.new(th.down, 78) : color.new(th.down, 100), title = "Risk zone", fillgaps = false)
fill(plEnt, plFin, top_value = math.max(aEntry, aTpF), bottom_value = math.min(aEntry, aTpF),
top_color = zonesOn and zDir == -1 ? color.new(th.up, 80) : color.new(th.up, 100), bottom_color = zonesOn and zDir == 1 ? color.new(th.up, 80) : color.new(th.up, 100), title = "Reward zone", fillgaps = false)
bgcolor(vFull ? (kz1 ? color.new(th.accent, 94) : kz2 ? color.new(th.up, 95) : kz3 ? color.new(th.down, 95) : na) : na, title = "Killzone shading")
barcolor(inTint and s.state == 4 ? color.new(s.dir == 1 ? th.up : th.down, 55) : na, title = "Trade tint")
plotshape(ev.sweep and s.dir == 1 and vClean, "Raid · sell-side", shape.circle, location.belowbar, color = color.new(th.up, 35), size = size.tiny, display = display.pane)
plotshape(ev.sweep and s.dir == -1 and vClean, "Raid · buy-side", shape.circle, location.abovebar, color = color.new(th.down, 35), size = size.tiny, display = display.pane)
plotshape(ev.filled and ev.dir == 1, "Filled · long", shape.triangleup, location.belowbar, color = th.up, size = size.tiny, display = display.pane)
plotshape(ev.filled and ev.dir == -1, "Filled · short", shape.triangledown, location.abovebar, color = th.down, size = size.tiny, display = display.pane)
// event labels (confirmed bars only by construction)
if ev.armed
label.new(s.fvgBar, s.dir == 1 ? s.fvgBot : s.fvgTop, (s.dir == 1 ? "LONG armed" : "SHORT armed") + " · " + fmt(s.entry), style = s.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(s.dir == 1 ? th.up : th.down, 20), textcolor = th.bg, size = size.small,
tooltip = "entry " + fmt(s.entry) + "\nstop " + fmt(s.sl) + " (" + fmt(s.r) + " = 1R, " + (s.sl0 == s.ext - inSlBuf * atrU or s.sl0 == s.ext + inSlBuf * atrU ? "sweep extreme" : "zone far edge") + ")\nzone " + (s.model == "iFVG" ? "inverted gap " : "FVG ") + fmt(s.fvgBot) + " – " + fmt(s.fvgTop) + "\nTP1 " + fmt(s.tp1) + " · TP2 " + fmt(s.tp2) + " · TP3 " + fmt(s.tp3) + (na(s.tpLiq) ? "\nno liquidity target ≥ " + str.tostring(inTpLiqMinR, "0.0") + "R" : "\nliquidity " + fmt(s.tpLiq) + " (" + s.tpLiqKind + ", " + str.tostring(math.abs(s.tpLiq - s.entry) / s.r, "0.0") + "R)"))
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "XL" : "XS") + " · " + ev.why + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny)
if ev.cancel and vPro
label.new(bar_index, ev.dir == 1 ? high : low, "✕ " + ev.why, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 40), textcolor = th.flat, size = size.tiny)
// ═══════════════════════════════════════════════════════════════════ WORDS (one source of truth)
string stateWord = s.state == 4 ? "FILLED · " + (s.dir == 1 ? "LONG" : "SHORT") : s.state == 3 ? "ARMED · " + (s.dir == 1 ? "LONG" : "SHORT") : s.state == 2 ? "SHIFTED · " + (s.dir == 1 ? "long" : "short") + " idea" : s.state == 1 ? "SWEPT · waiting for confirmation" : "SCANNING"
color stateColor = s.state >= 3 ? (s.dir == 1 ? th.up : th.down) : s.state > 0 ? th.accentHi : th.txt
string clockTxt = na(sdLastClosedTime) ? "waiting close" : str.format_time(sdLastClosedTime, "HH:mm", inTZ)
var int restingUp = 0
var int restingDn = 0
if (barstate.islastconfirmedhistory or barstate.islast)
restingUp := 0
restingDn := 0
for p in pools
if not p.swept
if p.side == 1
restingUp += 1
else
restingDn += 1
float rrLiq = s.state >= 3 and not na(s.tpLiq) ? math.abs(s.tpLiq - s.entry) / s.r : na
// ═══════════════════════════════════════════════════════════════════ DASHBOARD
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
kv(table tb, int row, string k, string v, color vc, string tip) =>
color stripe = inZebra and row % 2 == 1 ? color.new(th.accent, 93) : th.bg
table.cell(tb, 0, row, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe, tooltip = tip)
table.cell(tb, 1, row, v, text_color = vc, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = stripe, tooltip = tip)
// Read-only admission diagnostics. They never replace an active plan's state or frozen context.
sdViewAdmissionData() =>
string status = ""
if inCfPda and not cfHtfAllowed
status := "SETTINGS: HTF FVG must exceed chart"
else if inCfPda and not cfSourceReady
status := "DATA WAIT: completed HTF source missing"
if inCfSmt and not cfPair.ready
status += (status == "" ? "" : "\n") + "SMT: waiting for aligned closed bars"
status == "" ? "Data ready; setup gates still apply" : status
bool sdViewDataWait = (inCfPda and not cfSourceReady) or (inCfSmt and not cfPair.ready)
string sdViewDataHelp = "New-setup eligibility only. Set the required HTF FVG source strictly above the chart; missing completed source data blocks admission. Enabled SMT requires contiguous aligned closed-bar history. Existing pending/active plans retain their frozen context and lifecycle. Data ready does not mean a trade signal."
// At most 18 populated rows including admission status, within the 20-row allocation.
var table dash = table.new(posOf(inDashPos), 2, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0)
if (barstate.islastconfirmedhistory or barstate.islast) and inDash
table.clear(dash, 0, 0, 1, 19)
int r = 0
table.cell(dash, 0, r, "SWEEP DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Liquidity → shift → FVG, with a close-committed entry / stop / target engine. Every step commits on the closed bar.")
table.cell(dash, 1, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
table.cell(dash, 0, r, (s.state >= 3 ? (s.dir == 1 ? "▲ " : "▼ ") : "● ") + stateWord, text_color = stateColor, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "SCANNING = no raid yet · SWEPT = a pool was raided, waiting for the structure shift · SHIFTED = shift confirmed, looking for the FVG · ARMED = levels set, waiting for the fill · FILLED = trade open.")
table.cell(dash, 1, r, s.state == 1 ? str.tostring(bar_index - s.sweepBar) + "/" + str.tostring(inShiftWin) + " bars" : s.state == 3 ? str.tostring(bar_index - s.armBar) + "/" + str.tostring(inExpire) + " bars" : s.state == 4 ? str.tostring(bar_index - s.fillBar) + " bars in" : clockTxt + " · " + kzName, text_color = th.txt, text_size = szBody(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Timers: bars since the sweep (shift window) · bars since armed (expiry) · bars in the trade.")
r += 1
kv(dash, r, "New setups · data", sdViewAdmissionData(), sdViewDataWait ? th.down : th.flat, sdViewDataHelp)
r += 1
kv(dash, r, "Pool", s.state > 0 ? (s.dir == 1 ? "SSL " : "BSL ") + s.poolKind + " " + fmt(s.poolPx) + " · " + str.tostring(s.sweepDepth, "0.00") + " ATR" : str.tostring(restingUp) + " BSL · " + str.tostring(restingDn) + " SSL resting", th.txt, "The raided pool: its kind (SWG swing · EQH/EQL equal highs/lows · SES killzone session · HTF higher-timeframe swing · PDH/PDL/PWH/PWL previous day/week) and how far the wick went through it in ATR. When scanning: how many pools are resting on each side.")
r += 1
// the level rows only exist while a setup is alive — an idle dashboard stays short
if s.state > 0
kv(dash, r, "Sweep", s.state > 0 ? (s.sweepKz ? "in killzone" : "outside killzones") + (inSweepVol ? " · vol ✓" : "") : "—", s.state > 0 ? (s.sweepKz ? th.up : th.txt) : th.flat, "Where the sweep happened (killzone or not) and whether the volume filter passed.")
r += 1
float qv = s.state > 0 ? nz(s.quality, 0.0) : na
string qBar = na(qv) ? "—" : (qv >= 0.8 ? "▮▮▮▮▮" : qv >= 0.6 ? "▮▮▮▮▯" : qv >= 0.4 ? "▮▮▮▯▯" : qv >= 0.2 ? "▮▮▯▯▯" : "▮▯▯▯▯") + " " + str.tostring(math.round(qv * 100))
color qBg = na(qv) ? th.bg : color.new(color.from_gradient(qv, 0.0, 1.0, th.down, th.up), 72)
table.cell(dash, 0, r, "Sweep quality", text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Descriptive, frozen at the raid: depth of the wick through the pool, strength of the pool kind, killzone, volume. It never gates a setup and it is not a prediction.")
table.cell(dash, 1, r, qBar, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, text_font_family = font.family_monospace, bgcolor = qBg, tooltip = "Descriptive, frozen at the raid: depth of the wick through the pool, strength of the pool kind, killzone, volume. It never gates a setup and it is not a prediction.")
r += 1
string needTxt = inConfirm == "FVG inversion (iFVG)" ? "needs a close through an opposing gap" : inConfirm == "Structure shift (MSS)" ? "needs close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "—" : fmt(s.oppSwing)) : "close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "—" : fmt(s.oppSwing)) + " or through a gap"
kv(dash, r, "Confirm", s.state >= 2 ? (s.model == "iFVG" ? "iFVG inverted" : "MSS " + fmt(s.mssPx)) + " · " + str.tostring(math.abs(close - s.ext) / atrU, "0.0") + " ATR leg" : s.state == 1 ? needTxt : "—", s.state >= 2 ? th.txtHi : th.txt, "How the reversal was confirmed: iFVG = a close through an opposing gap near the raid (the gap inverts) · MSS = a close beyond the last short-term swing with displacement. Plus the leg from the sweep extreme in ATR.")
r += 1
kv(dash, r, "Zone", s.state >= 3 ? fmt(s.fvgBot) + " – " + fmt(s.fvgTop) + (s.model == "iFVG" ? " · inverted gap" : " · leg FVG") : "—", th.txt, "The entry zone: the inverted gap (iFVG) or the imbalance inside the shift leg (MSS).")
r += 1
if s.state >= 3
kv(dash, r, "Confluence · frozen", s.confluenceSummary, th.txt, "Observed at acceptance, before its first-touch token was consumed. A transparent checklist, not a probability or private grade.")
r += 1
kv(dash, r, "Entry", s.state >= 3 ? fmt(s.entry) + (s.state == 3 ? (inEntryMode == "Close inside" ? " · waiting close" : " · limit") : "") : "—", th.txtHi, "Confirmation close (default), later close inside, or resting edge/midpoint: see Entry setting. Paper reference; broker fills may differ.")
r += 1
kv(dash, r, "Stop", s.state >= 3 ? fmt(s.sl) + " · " + fmt(s.r) + " = 1R" + (s.tp2Hit ? " · TRAIL" : s.tp1Hit and inBE ? " · BE" : "") : "—", th.down, "Anchored to the sweep extreme (structural invalidation) plus a buffer — or the zone's far edge when the extreme is too far away (Auto) or when you chose it.")
r += 1
kv(dash, r, "R ladder", s.state >= 3 ? fmt(s.tp1) + " · " + fmt(s.tp2) + " · " + fmt(s.tp3) : "—", th.up, "Rounded R milestones. TP1 entry-stop move is optional; TP2 trails to TP1. Updates apply next bar, with no partial position exits.")
r += 1
kv(dash, r, "Liquidity", s.state >= 3 ? (na(s.tpLiq) ? "none ≥ " + str.tostring(inTpLiqMinR, "0.0") + "R resting" : fmt(s.tpLiq) + " · " + s.tpLiqKind + " · " + str.tostring(rrLiq, "0.0") + "R") : "—", na(rrLiq) ? th.flat : th.accentHi, "The nearest resting pool on the other side — where the market's next magnet sits. Final target = " + inPrimary + ".")
r += 1
kv(dash, r, "Swept · Confirm · Armed · Filled", str.tostring(st.swept) + " · " + str.tostring(st.shifted) + " · " + str.tostring(st.armed) + " · " + str.tostring(st.filled), th.txt, "How many raids were confirmed (iFVG or MSS), how many confirmations armed a zone, how many got filled — on the loaded history. The drop-offs are the filters doing their job.")
r += 1
int decided = st.n - st.scratch
kv(dash, r, "Record", st.n < 5 ? str.tostring(st.n) + " closed · too few" : str.tostring(st.n) + " closed · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win" + (st.scratch > 0 ? " · " + str.tostring(st.scratch) + " flat" : "") + " · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", th.txt, "Closed trades on this chart: count · win% of ALL closed (flat = breakeven / gap scratches, counted as non-wins, same basis as the By-pool and By-model rows) · average R. Paper, no costs, small sample — a description of the past on THIS chart, not an edge.")
r += 1
string byPool = (st.nSwg > 0 ? "SWG " + str.tostring(math.round(st.wSwg / st.nSwg * 100)) + "%/" + str.tostring(st.nSwg) : "SWG —") + " · " + (st.nEq > 0 ? "EQ " + str.tostring(math.round(st.wEq / st.nEq * 100)) + "%/" + str.tostring(st.nEq) : "EQ —") + " · " + (st.nSes > 0 ? "SES " + str.tostring(math.round(st.wSes / st.nSes * 100)) + "%/" + str.tostring(st.nSes) : "SES —") + " · " + (st.nHtf > 0 ? "HTF " + str.tostring(math.round(st.wHtf / st.nHtf * 100)) + "%/" + str.tostring(st.nHtf) : "HTF —")
kv(dash, r, "By pool", byPool, th.txt, "Win% / count by the kind of liquidity that was raided — swing, equal highs/lows, killzone session, or HTF (higher-timeframe swings + previous day / week). Which pools actually pay on YOUR chart.")
r += 1
string byModel = (st.nIfvg > 0 ? "iFVG " + str.tostring(math.round(st.wIfvg / st.nIfvg * 100)) + "%/" + str.tostring(st.nIfvg) : "iFVG —") + " · " + (st.nMss > 0 ? "MSS " + str.tostring(math.round(st.wMss / st.nMss * 100)) + "%/" + str.tostring(st.nMss) : "MSS —")
kv(dash, r, "By model", byModel, th.txt, "Win% / count by confirmation model — the inversion (iFVG) entries versus the structure-shift (MSS) entries on this chart.")
r += 1
table.cell(dash, 0, r, (inConfirm == "Either" ? "iFVG+MSS" : inConfirm == "FVG inversion (iFVG)" ? "iFVG" : "MSS") + " · " + inEntryMode + " · " + (inSlMode == "Sweep extreme" ? "SL@sweep" : inSlMode == "Zone far edge" ? "SL@zone" : "SL@auto") + " · " + (inPrimary == "Liquidity" ? "TP@liq" : "TP@" + str.tostring(inTp3) + "R"), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Entry mode · stop anchor · final target.")
table.cell(dash, 1, r, (inSweepKz ? "killzones only" : "any time") + " · " + str.tostring(array.size(pools)) + " pools", text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Sweep time filter and how many pools (resting + recently swept) are tracked.")
// ═══════════════════════════════════════════════════════════════════ NARRATIVE · Co-Pilot (translates state; never a new signal)
wrap(string sIn, int width) =>
array<string> words = str.split(sIn, " ")
string out = ""
string line = ""
for wd in words
if line != "" and str.length(line) + str.length(wd) + 1 > width
out := out + (out == "" ? "" : "\n") + line
line := wd
else
line := line == "" ? wd : line + " " + wd
out == "" ? line : out + "\n" + line
narrNow() =>
string o = ""
if s.state == 4
float finalR = (s.tpFinal - s.entry) * s.dir / s.r
o := "paper " + (s.dir == 1 ? "long" : "short") + " from " + fmt(s.entry) + " — current stop " + fmt(s.sl) + (s.tp2Hit ? " (TP2 trail)" : s.tp1Hit and inBE ? " (entry stop)" : " (" + s.stopBasis + ")") + ", final " + fmt(s.tpFinal) + " (" + str.tostring(finalR, "0.00") + "R" + ((not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "; " + s.tpLiqKind : "; fixed ladder") + ")"
else if s.state == 3
o := (s.dir == 1 ? "long" : "short") + " plan armed — " + (inEntryMode == "Close inside" ? "waiting for a valid close inside " + fmt(s.fvgBot) + "–" + fmt(s.fvgTop) : "resting " + inEntryMode + " limit " + fmt(s.entry)) + "; pending stop " + fmt(s.sl0) + " (" + s.stopBasis + "); " + str.tostring(math.max(0, inExpire - (bar_index - s.armBar))) + " bars until unfilled expiry"
else if s.state == 2
o := "structure shifted " + (s.dir == 1 ? "up" : "down") + " after the raid — looking for the imbalance in the leg to arm the entry (" + str.tostring(inFvgWin - (bar_index - s.mssBar)) + " bars left)"
else if s.state == 1
o := (s.dir == 1 ? "sell-side" : "buy-side") + " liquidity at " + fmt(s.poolPx) + " (" + s.poolKind + ") was raided and rejected — waiting for " + (inConfirm == "FVG inversion (iFVG)" ? "a close through an opposing gap (inversion)" : inConfirm == "Structure shift (MSS)" ? "a close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) : "a close through an opposing gap or " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing))) + " with displacement, " + str.tostring(inShiftWin - (bar_index - s.sweepBar)) + " bars left"
else
o := "scanning — " + str.tostring(restingUp) + " buy-side and " + str.tostring(restingDn) + " sell-side pools are resting; nothing has been raided yet"
o
narrWhy() =>
string o = "The sequence is mechanical: a pool is swept (wick through by ≥ " + str.tostring(inSweepMin, "0.00") + " ATR, close back inside), then the reversal is confirmed " + (inConfirm == "FVG inversion (iFVG)" ? "when a bar closes through an opposing gap — the gap inverts and becomes the zone" : inConfirm == "Structure shift (MSS)" ? "when a bar closes beyond the last short-term swing — the gap in that leg becomes the zone" : "by a close through an opposing gap (the gap inverts) or beyond the last short-term swing (the gap in that leg)") + ", always with a leg ≥ " + str.tostring(inDispAtr, "0.0") + " ATR from the extreme. "
o := o + (s.state > 0 ? "This one: " + s.poolKind + " pool, wick " + str.tostring(s.sweepDepth, "0.00") + " ATR through it, " + (s.sweepKz ? "inside a killzone" : "outside the killzones") + "." : "Killzone now: " + kzName + ".")
o
narrPlan() =>
string o = ""
if s.state >= 3
float finalR = (s.tpFinal - s.entry) * s.dir / s.r
string targetBasis = (not na(s.tpLiq) and sdCompare(s.tpFinal, s.tpLiq) == 0) ? "nearest qualifying " + s.tpLiqKind : inPrimary == "Liquidity" and na(s.tpLiq) ? "explicit TP3 fallback" : "selected TP3 rung"
o := "Entry " + fmt(s.entry) + "; initial stop " + fmt(s.sl0) + " (" + s.stopBasis + "), current stop " + fmt(s.sl) + ". TP1 " + fmt(s.tp1) + (inBE ? " moves the stop toward entry" : " is a checkpoint; entry-stop move is off") + "; TP2 " + fmt(s.tp2) + " moves it toward TP1. These stop changes apply from the next bar. Final " + fmt(s.tpFinal) + " = " + str.tostring(finalR, "0.00") + "R (" + targetBasis + "). Known open gaps resolve first; stop-first only for unordered range hits. Closed-bar entries exclude all earlier entry-bar wicks."
else if s.state == 1
o := (inConfirm == "FVG inversion (iFVG)" ? "If a bar closes through an opposing gap near the raid with displacement, that gap inverts and the entry is armed on it. " : inConfirm == "Structure shift (MSS)" ? "If a bar closes " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) + " with displacement, the shift is in. " : "A close through an opposing gap (inversion) arms the entry on that gap; a close " + (s.dir == 1 ? "above " : "below ") + (na(s.oppSwing) ? "the last short-term swing" : fmt(s.oppSwing)) + " arms it on the gap in that leg. ") + "If price closes back through the raided pool by " + str.tostring(inDispAtr, "0.0") + " ATR, the raid was not a raid."
o
narrTrack() =>
int decided = st.n - st.scratch
"Funnel here: " + str.tostring(st.swept) + " sweeps → " + str.tostring(st.shifted) + " confirmed → " + str.tostring(st.armed) + " armed → " + str.tostring(st.filled) + " filled. " + (st.n < 5 ? "Too few closed trades to say anything yet." : str.tostring(st.n) + " closed, " + str.tostring(math.round(st.wins / st.n * 100)) + "% won" + (st.scratch > 0 ? " (" + str.tostring(st.scratch) + " flat)" : "") + ", avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R (paper, no costs, small sample — NOT an edge).")
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if (barstate.islastconfirmedhistory or barstate.islast)
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
string p = narrPlan()
if p != ""
array.push(labs, "PLAN")
array.push(vals, wrap(p, inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Pool = resting liquidity (stops) at a swing, equal highs/lows, a session or a day/week extreme. Sweep = a wick through it that closes back. iFVG = an opposing gap that price closes through, so it inverts and becomes the zone. MSS = a close beyond the last short-term swing the other way; then the gap in that leg is the zone. The selected stop anchor is sweep extreme, zone far edge, or Auto; the accepted ticket shows its actual basis.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. Paper ledger has no costs; the strategy twin measures it with costs.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ── BROKER OWNERSHIP BEGIN · no paper EXIT/CANCEL/SYNC determines a broker exit
gLab = "Strategy Lab · account risk"
labMode = input.string("Cash risk", "Lab sizing", options = ["Cash risk", "Fixed units"], group = gLab, display = display.none)
labRisk = input.float(100, "Cash risk · account currency", minval = 0.01, group = gLab, display = display.none, tooltip = "Signal-close stop-distance budget. Uses strategy account conversion and symbol point value. Commissions, slippage and gaps can increase loss beyond this estimate.")
labFixed = input.float(1, "Fixed units", minval = 0.000001, group = gLab, display = display.none, active = labMode == "Fixed units")
labCap = input.float(90, "Maximum notional · % current equity", minval = 1, maxval = 100, group = gLab, display = display.none, tooltip = "Also caps Fixed units. Does not change 100% margin or eliminate margin calls after price movement.")
labStepMode = input.string("Symbol minimum", "Quantity increment", options = ["Symbol minimum", "Manual"], group = gLab, display = display.none)
labStep = input.float(1, "Manual quantity increment", minval = 0.00000001, group = gLab, display = display.none, active = labStepMode == "Manual")
var string brokerStatus = "Waiting for accepted technical plan"
var float brokerQuantity = na
var float brokerRiskEstimate = na
var float brokerNotional = na
var int brokerState = 0 // 0 flat, 1 submitted/resting, 2 position observed
var int brokerDirection = 0
var int brokerSubmitBar = na
var int brokerFillBar = na
var int brokerLastExitBar = na
var int brokerClosedSeen = 0
var float brokerEntry = na
var float brokerStop = na
var float brokerInitialStop = na
var float brokerTarget = na
var float brokerTp1 = na
var float brokerTp2 = na
var float brokerZoneHigh = na
var float brokerZoneLow = na
var bool brokerTp1Hit = false
var bool brokerTp2Hit = false
var string brokerModel = ""
bool brokerSubmitted = false
bool brokerClosedThisBar = false
if barstate.isconfirmed
bool brokerOccupied = sdSign(strategy.position_size) != 0
if strategy.closedtrades > brokerClosedSeen
// Partial margin liquidation does not release ownership of a nonzero remainder.
brokerClosedSeen := strategy.closedtrades
if not brokerOccupied
strategy.cancel("Entry")
strategy.cancel("Bracket")
brokerState := 0
brokerLastExitBar := bar_index
brokerClosedThisBar := true
brokerStatus := "Broker position closed; no same-bar replacement"
if brokerState == 1 and brokerOccupied
brokerState := 2
brokerEntry := strategy.position_avg_price
brokerFillBar := strategy.opentrades.entry_bar_index(0)
brokerStatus := "Broker fill observed; frozen bracket owned"
if brokerState == 1 and not brokerOccupied and bar_index > brokerSubmitBar
bool marketMode = inEntryMode == "Close inside" or inEntryMode == "Confirmation close"
bool invalidStop = sdCompare(close, brokerInitialStop) * brokerDirection < 0
bool invalidZone = brokerModel == "iFVG" and (brokerDirection == 1 ? sdCompare(close, brokerZoneLow) < 0 : sdCompare(close, brokerZoneHigh) > 0)
bool missed = brokerDirection == 1 ? sdCompare(high, brokerTp1) >= 0 : sdCompare(low, brokerTp1) <= 0
if marketMode or invalidStop or invalidZone or missed or bar_index - brokerSubmitBar >= inExpire
strategy.cancel("Entry")
strategy.cancel("Bracket")
brokerState := 0
brokerLastExitBar := bar_index
brokerClosedThisBar := true
brokerStatus := marketMode ? "No broker fill; inspect margin / quantity" : "Resting order cancelled by its own invalidation / expiry"
if brokerState == 2 and brokerOccupied
// Do not use the entry bar's pre-fill high/low to advance a broker stop.
if bar_index > brokerFillBar
[nextStop, one, two, newOne, newTwo] = sdMilestones(brokerDirection, brokerEntry, brokerStop, brokerTp1, brokerTp2, high, low, brokerTp1Hit, brokerTp2Hit, inBE, syminfo.mintick)
brokerStop := nextStop
brokerTp1Hit := one
brokerTp2Hit := two
strategy.exit("Bracket", from_entry = "Entry", stop = brokerStop, limit = brokerTarget, comment_profit = "TARGET", comment_loss = brokerTp2Hit ? "TRAIL" : brokerTp1Hit and inBE ? "BE" : "STOP")
if bar_index - brokerFillBar >= inMaxTrade
strategy.close("Entry", comment = "TIME", immediately = true)
bool marketMode = inEntryMode == "Close inside" or inEntryMode == "Confirmation close"
bool sharedIntent = marketMode ? ev.filled : ev.armed
bool available = brokerState == 0 and not brokerOccupied and not brokerClosedThisBar and (na(brokerLastExitBar) or bar_index > brokerLastExitBar)
float increment = labStepMode == "Manual" ? labStep : syminfo.mincontract
float accountUnitRisk = math.abs(strategy.convert_to_account(s.r * syminfo.pointvalue))
float accountUnitNotional = math.abs(strategy.convert_to_account(s.entry * syminfo.pointvalue))
[plannedQuantity, plannedRisk, plannedNotional, sizeValid, sizeReason] = sdQuantity(labMode == "Fixed units", labFixed, labRisk, accountUnitRisk, accountUnitNotional, strategy.equity, labCap, increment)
if sharedIntent and available and s.planValid and not sizeValid
brokerStatus := "SKIPPED · " + sizeReason
if sharedIntent and available and s.planValid and sizeValid
brokerQuantity := plannedQuantity
brokerRiskEstimate := plannedRisk
brokerNotional := plannedNotional
brokerStatus := "Submitted; broker fill not yet confirmed"
brokerState := 1
brokerDirection := marketMode ? ev.dir : s.dir
brokerSubmitBar := bar_index
brokerFillBar := na
brokerEntry := s.entry
brokerStop := s.sl0
brokerInitialStop := s.sl0
brokerTarget := s.tpFinal
brokerTp1 := s.tp1
brokerTp2 := s.tp2
brokerZoneHigh := s.fvgTop
brokerZoneLow := s.fvgBot
brokerModel := s.model
brokerTp1Hit := false
brokerTp2Hit := false
strategy.entry("Entry", brokerDirection == 1 ? strategy.long : strategy.short, qty = brokerQuantity, limit = marketMode ? na : brokerEntry, comment = "STRUCTURAL " + brokerModel)
// Same-calculation bracket, including before position_size changes.
strategy.exit("Bracket", from_entry = "Entry", stop = brokerStop, limit = brokerTarget, comment_profit = "TARGET", comment_loss = "STOP")
brokerSubmitted := true
plot(brokerState > 0 ? brokerEntry : na, "Broker entry reference", color = color.white, style = plot.style_linebr, display = display.pane)
plot(brokerState > 0 ? brokerStop : na, "Broker owned stop", color = color.red, style = plot.style_linebr, display = display.pane)
plot(brokerState > 0 ? brokerTarget : na, "Broker owned target", color = color.lime, style = plot.style_linebr, display = display.pane)
var table brokerTable = table.new(position.top_right, 2, 7, bgcolor = color.rgb(12,18,28), frame_color = color.gray, frame_width = 1)
// Fixed seven-row operational HUD: update every confirmed calculation, including closed-feed reloads.
if barstate.isconfirmed
table.cell(brokerTable,0,0,"SWEEP · STRATEGY LAB",text_color=color.white)
table.cell(brokerTable,1,0,brokerState == 2 ? "BROKER OPEN" : brokerState == 1 ? "SUBMITTED" : "FLAT",text_color=color.aqua)
table.cell(brokerTable,0,1,"Status",text_color=color.silver)
table.cell(brokerTable,1,1,brokerStatus,text_color=color.white)
table.cell(brokerTable,0,2,"New setups / data",text_color=color.silver,tooltip=sdViewDataHelp)
table.cell(brokerTable,1,2,sdViewAdmissionData(),text_color=sdViewDataWait ? color.orange : color.silver,tooltip=sdViewDataHelp)
table.cell(brokerTable,0,3,"Quantity / risk est.",text_color=color.silver)
table.cell(brokerTable,1,3,sdNumber(brokerQuantity)+" / "+sdNumber(brokerRiskEstimate)+" "+strategy.account_currency,text_color=color.white)
table.cell(brokerTable,0,4,"Entry / SL / target",text_color=color.silver)
table.cell(brokerTable,1,4,brokerState > 0 ? fmt(brokerEntry)+" / "+fmt(brokerStop)+" / "+fmt(brokerTarget) : "No owned bracket",text_color=color.white)
table.cell(brokerTable,0,5,"Technical engine",text_color=color.silver)
table.cell(brokerTable,1,5,stateWord+" · paper paced",text_color=color.white)
table.cell(brokerTable,0,6,"Header defaults / cap",text_color=color.silver)
table.cell(brokerTable,1,6,"0.02% + 5 ticks; margin 100% · cap "+str.tostring(labCap)+"%",tooltip="Verify Strategy Properties: commission, slippage and margin can be overridden there. Pine cannot read those overrides.",text_color=color.white)
// ── BROKER OWNERSHIP END
// ═══════════════════════════════════════════════════════════════════ ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"SweepDesk","symbol":"' + sdEscape(syminfo.tickerid) + '","tf":"' + sdEscape(timeframe.period) + '","event":"' + evName + '","side":"' + (s.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + '","model":"' + sdEscape(s.model) + '","pool":"' + sdEscape(s.poolKind) + '","entry":' + sdNumber(s.entry) + ',"sl":' + sdNumber(s.sl) + ',"initial_sl":' + sdNumber(s.sl0) + ',"tp1":' + sdNumber(s.tp1) + ',"tp2":' + sdNumber(s.tp2) + ',"tp3":' + sdNumber(s.tp3) + ',"tp_liq":' + sdNumber(s.tpLiq) + ',"target":' + sdNumber(s.tpFinal) + ',"reason":"' + sdEscape(why) + '","confluences":"' + sdEscape(s.confluenceSummary) + '","entry_time":' + sdNumber(s.entryTime) + ',"end_observed_time":' + sdNumber(s.endTime) + ',"exit_price":' + sdNumber(ev.exit ? ev.px : na) + ',"gross_r":' + sdNumber(ev.r) + '}'
string tx = "Sweep Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · " + (s.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + " · entry " + fmt(nz(s.entry, ev.px)) + " · SL " + fmt(nz(s.sl, 0)) + " · TP " + fmt(nz(s.tp1, 0)) + " / " + fmt(nz(s.tp2, 0)) + " / " + fmt(nz(s.tp3, 0)) + (na(s.tpLiq) ? "" : " · liq " + fmt(s.tpLiq)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
// One envelope per confirmed update; no same-bar FILLED/EXIT or TP1/TP2 event is lost.
if barstate.isconfirmed and (ev.armed or ev.filled or ev.tp1 or ev.tp2 or ev.exit or ev.cancel)
string kinds = ""
if ev.armed
kinds += "\"ARMED\""
if ev.filled
kinds += (kinds == "" ? "" : ",") + "\"FILLED\""
if ev.tp1
kinds += (kinds == "" ? "" : ",") + "\"TP1\""
if ev.tp2
kinds += (kinds == "" ? "" : ",") + "\"TP2\""
if ev.exit
kinds += (kinds == "" ? "" : ",") + "\"EXIT\""
if ev.cancel
kinds += (kinds == "" ? "" : ",") + "\"CANCEL\""
string primary = ev.exit ? "EXIT" : ev.cancel ? "CANCEL" : ev.filled ? "FILLED" : ev.armed ? "ARMED" : ev.tp2 ? "TP2" : "TP1"
string message = payload(primary, ev.why)
if inAlertJson
message := str.substring(message, 0, str.length(message) - 1) + ",\"schema\":\"sweep-desk.v2\",\"events\":[" + kinds + "],\"confirmed\":true,\"fill_confirmed\":false,\"costs_included\":false,\"bar_close_ms\":" + str.tostring(time_close) + ",\"plan_id\":" + (na(s.planId) ? "null" : str.tostring(s.planId)) + ",\"ambiguous\":" + (s.ambiguous ? "true" : "false") + "}"
alert(message, alert.freq_once_per_bar_close)
Release identity and file hashes · MIT licence
No setup yet?
Wait for completed history and unused first-touch context. The default 60-minute source must exceed your chart. Check New setups · data and Co-Pilot. A sweep alone cannot enter; data warnings do not replace an existing plan’s state.
Go deeper, one question at a time.
Full rules and settings, when you need them.
What happens between a sweep and an entry?
- WatchStart with a confirmed high or low.
- SweepPrice passes it, then closes back across.
- ConfirmA later qualifying close prepares a zone.
- AcceptDefault: enter at the qualifying confirmation close.
This long setup mirrors for shorts above a high. Defaults: Confirmation close and Single gaps. Close inside and limit entries are alternatives.
Where do the reference levels come from?
Swings: highs/lows confirmed five candles later by default. Equal levels: nearby highs/lows merged within an ATR tolerance. Sessions: extremes available after a session ends. Higher-timeframe swings and previous day/week levels add context.
“Liquidity pool” is the tool’s name for these price-derived references; it does not observe waiting orders. “Strong” means equal, session, higher-timeframe or day/week by type. It does not establish a better outcome.
Any wick strictly beyond a pool consumes it, even without enough depth or a close back inside. Touching exactly does not. Entry filters cannot make consumed liquidity fresh again. Age and budget prune swing-type pools; the drawing-distance setting only hides distant lines.
How do gap inversion and structure shift differ?
iFVG, the default: after a sweep, a new close through an opposing gap’s far edge can prepare that gap as the zone. In our long example, exactly 90 fails; above 90 can qualify. The gap must also meet age, location and movement checks.
The gap condition confirms. At 96, the newest individual gap needs a strict close above 94. Equality does not confirm.
Original Series rule: same-direction gaps formed on consecutive chart bars, combined before inversion. This isolated illustration supplies valid gap sizes and shows only the inversion check; sweep, displacement, confluences and plan geometry remain separate requirements.
Default HTF prerequisite: a completed 60-minute FVG must receive its first valid touch. It grants six observed chart bars of directional context, ages zero through five, and cannot refresh on repeated touches. An accepted setup consumes it; a pending return retains its frozen context.
Optional SMT compares synchronized prior closed bars and needs a meaningful manually chosen pair. Momentum, a minimum observation count and confirmation-close macro windows are separate checks. Our definitions are explicit; more checks do not prove better results.
MSS, the alternative: price closes beyond the opposing short-term swing captured at the sweep, with enough movement. A qualifying gap in that shift leg becomes the zone. SHIFTED means it is still searching; ARMED means the zone is ready. Nearest picks its newest gap; Deepest picks its oldest.
Either accepts either route, with iFVG first if both qualify together. Confirmation allows twelve candles after the sweep; the next candle is too late. MSS allows eight extra candles after a shift to find the gap.
Default sweep distance is at least 0.05 ATR and the close must return strictly across the level. Confirmation needs at least 1 ATR of movement; gap width at least 0.1 ATR. ATR measures recent price movement including gaps; its length is fixed at fourteen.
Which entry mode starts the reference plan?
Confirmation close, the default: the qualifying inversion or MSS-zone acceptance close is the entry reference. Its earlier wick cannot also exit that plan.
Close inside, the alternative: a later candle closes within either edge, including exactly on an edge. Its actual close becomes entry. The confirmation candle cannot fill too.
FVG edge / CE midpoint: use a paper limit at the zone’s near edge or center. Contact can fill without a close inside. These are alternative entry rules.
The zone is fixed after arming. Close-inside entry recalculates stop and targets using the actual fill close and current ATR. The pending sweep extreme can extend before confirmation.
Why did the idea stop waiting?
Recorded reasons include no confirmation in time, no rejection, invalidation, a close through the wrong side of an inverted zone, price reaching the first target unfilled, or expiry.
A prepared setup has thirty candles to fill. Close inside first checks the frozen pending stop: a close beyond it cancels before entry and cannot move the stop to manufacture a fill. Otherwise, a qualifying fill wins over missed-target and expiry checks, including on the final allowed candle. Once expired, a later return cannot revive it.
Pro labels recorded cancellations. A fresh qualifying sweep can also replace an unarmed idea without a cancellation tag. Armed and filled setups are protected from replacement.
How do the stop and targets end the trade?
1R is initial entry-to-stop distance. Default stop: sweep extreme plus 0.1 ATR buffer. Zone far edge is an alternative. Auto can choose a nearer anchor when the sweep is too far; its 2.5 ATR cap does not constrain the default Sweep extreme setting.
Targets default to 1R, 2R and 3R. TP1 optionally moves stop to entry; TP2 always moves it to TP1. Neither takes partial profit. Protective stop changes apply from the next bar, after the current bar is checked against its existing stop. Breakeven names an entry-price reference; costs can still leave a loss. This tool has no continuous ATR trail.
Liquidity selects the nearest qualifying fresh opposing pool, skipping closer pools below the minimum distance: 2R by default, checked after rounding toward entry. Clustered quality requires at least two confirmed merged swing observations. Choose Liquidity or TP3 as final; Liquidity falls back to TP3 when none qualifies. The selected final target freezes at acceptance; later pool changes cannot move it.
Close entries have no entry-bar outcomes. On later bars, a stop-gap open is the exit reference; a favorable gap beyond the target uses the fixed target. Ordinary both-touch bars resolve stop-first and disclose ambiguity. Unknown intrabar limit fills never invent a same-bar target success. These paper assumptions are separate from broker fills. Default timeout is two hundred candles.
How do I read the dashboard and Co-Pilot?
State shows the stage and timers; Pool / Confirm / Zone identify the setup. Entry / Stop / R ladder / Liquidity describe its plan. Record / By pool / By model summarize closed paper outcomes without costs.
- SweptSelected qualifying sweep ideas.
- ConfirmedIdeas reaching confirmation.
- ArmedPrepared entry zones.
- FilledRecorded paper entries.
These counters exclude sweeps blocked by enabled filters and new sweeps while armed or filled. Sweep quality is descriptive; it gates nothing.
Co-Pilot explains NOW, WHY and PLAN from the same engine. Pro adds pool, sweep, structure, zone and cancellation labels; Full adds session shading. It does not draw every stored gap.

What does each setting change?
Four display defaults differ, labelled Indicator and Lab. The Lab has its own broker dashboard; other defaults are shared.
Reference levels
Confirmed highs/lows provide the references. Pool age and budget affect available levels; drawing distance only changes visibility.
- Swing confirmation length
- 5 candles
- Equal high/low merge distance
- ATR × 0.15
- Completed session highs/lows
- On
- Previous day’s high/low
- On
- Previous week’s high/low
- On
- Swing-type levels kept per side
- 10
- Maximum swing-type level age
- 400 candles
- Higher-timeframe swings
- On
- Higher timeframe
- 60 minutes
- Higher-timeframe swing length
- 5
- Draw levels within this distance
- ATR × 8
Sweep checks
A wick must pass a level and close back across it. Optional checks select which sweeps can start an idea.
- Minimum distance beyond level
- ATR × 0.05
- Require extra volume
- Off
- Minimum volume versus average
- 1.3 ×
- Sweeps only in session windows
- Off
- Allowed level types
- Any pool
- Allowed direction
- Both
Confirmation and zone
Choose gap inversion, structure shift or Either. Nearest/Deepest selects the newest/oldest qualifying MSS gap; it does not change iFVG selection.
- Confirmation method
- FVG inversion (iFVG)
- Lookback before sweep for a gap
- 40 candles
- Structure-confirmation swing length
- 3
- Candles allowed for confirmation
- 12
- Minimum push from sweep extreme
- ATR × 1
- Minimum gap size
- ATR × 0.10
- MSS gap choice
- Nearest
- MSS: extra candles to find gap
- 8
Entry, stop and targets
Choose the entry rule, stop anchor and final target. R means the initial entry-to-stop distance. Milestones move the stop without taking partial profits.
- When an entry is accepted
- Confirmation close
- Initial stop anchor
- Sweep extreme
- Auto stop: maximum sweep distance
- ATR × 2.5
- Space beyond stop anchor
- ATR × 0.10
- First milestone
- 1R
- Second milestone
- 2R
- Final R target
- 3R
- Minimum opposing-level distance
- 2R
- Target that ends the trade
- R ladder (TP3)
- At TP1, move stop to entry
- On
- Candles allowed while unfilled
- 30
- Maximum candles in an open trade
- 200
Session windows
These supply session highs/lows and the optional sweep-time check. They do not force trades to close.
- Timezone
- America/New_York
- London window
- 02:00–05:00
- New York morning
- 08:30–11:00
- New York afternoon
- 13:30–16:00
Chart appearance
Choose Minimal, Clean, Pro or Full; Aurora Neon, Royal Gold or Colorblind-safe. Appearance does not change entry rules.
- Amount of detail
- Indicator: CleanLab: Minimal
- Color theme
- Aurora Neon
- Shade risk and reward
- On
- Color candles during open trade
- Indicator: OnLab: Off
- Highlight stronger level types
- On
- Label distance to the right
- 20 candles
Dashboard
Show and arrange the current setup, counters and paper record.
- Show dashboard
- Indicator: OnLab: Off
- Position
- Top Right
- Text size
- Medium
- Alternate row shading
- On
Co-Pilot
Brief shows NOW; Standard adds WHY and PLAN; Detailed adds the record. Off hides the explanation.
- Amount of explanation
- Indicator: StandardLab: Off
- Position
- Bottom Left
- Text size
- Auto
- Line width
- 64 characters
Alerts
Choose structured JSON or readable text for dynamic messages.
- Send dynamic messages as JSON
- On
Inversion grouping and context
Series combines adjacent gap births before inversion. Clustered target quality requires at least two confirmed merged swings; Any fresh pool also permits single observations. These are original definitions of public concepts.
- iFVG mode
- Single
- Confirmation-close macro window
- Off
- Opposing target quality
- Any fresh pool
Explicit confluences
The default requires one unused HTF first-touch context. Each other enabled gate must pass even when minimum votes is zero. Votes are observations, not a probability or private grade.
- Require first-touch HTF FVG delivery
- On
- HTF FVG source
- 60 minutes · must exceed chart
- First-touch context lifetime
- 6 observed chart bars
- Untouched gap lifetime
- 50 HTF periods
- Require paired-market SMT
- Off
- Comparison symbol
- BINANCE:BTCUSDT · choose a meaningful pair
- Inverse relationship
- Off
- Aligned-bar SMT window
- 10 bars
- SMT context lifetime
- 6 chart bars
- Require directional momentum
- Off
- Minimum body size
- ATR × 0.5
- Directional close location
- 0.7
- Minimum observed confluences
- 0 of 4
Strategy Lab only
- Lab sizing
- Cash risk
- Cash risk in account currency
- 100
- Fixed units alternative
- 1
- Maximum notional versus current equity
- 90 percent
- Quantity increment
- Symbol minimum
- Manual quantity increment alternative
- 1
When required, volume must be positive with a positive twenty-bar mean. Zero or unavailable volume does not pass. The separate observation count is descriptive, not a private grade or probability.
Which events can send an alert?
The indicator has twelve named conditions: long/short armed, long/short filled, TP1, TP2, final target, stopped, cancelled, liquidity swept, FVG inverted and higher-timeframe pool swept. Final target covers TP3 or liquidity.
Any alert() function call sends only ARMED, FILLED, TP1, TP2, EXIT and CANCEL. It does not send separate sweep or inversion payloads. Use Once Per Bar Close.
Both scripts send JSON or readable text. The sweep-desk.v2 schema combines same-bar paper events with entry, initial/current stop, target, times and observed exit. These are not broker fills. The twin submits its bracket with entry and owns its position and deadline; a paper EXIT does not close it. Default Confirmation close uses on-close processing with declared slippage. Recreate alerts after source or setting changes.
How do I test the rules on my chart?
- Keep defaults and follow the state through a complete session.
- Include waiting, cancelled and losing examples.
- Load the twin on a second chart. Set realistic size, costs and slippage.
- Change one input at a time, then check a separate period.
Lab defaults: USD 100,000 capital, cash risk 100, maximum notional 90 percent of equity, commission 0.02 percent per order and five ticks of slippage. Fixed units is an alternative. The broker emulator can differ from the indicator’s cost-free reference ledger. Review Properties and quantity increment for the instrument.
Use the current Lab and its complete trade list. The older Strategy Tester picture belongs to the historical v1 article and is not a result for this release.
What did our measurements show?
The current v2 MNQ Strategy Lab sample lost money after costs. That small observation does not establish an edge.
The linked gold and bitcoin measurements belong to the historical v1 engine. Its close-inside baseline is not evidence for current defaults; v2 revises entry timing and broker ownership.
Keep historical findings and current validation separate. Software checks verify behavior, not economic edge.
Read the lab notesWhat can change the reading?
Signals commit on closed candles. A live candle can change before its close. Different history, feeds, timeframes or inputs can recalculate results. Pivots appear after confirmation; session pools after their session ends. Higher-timeframe context uses its last closed candle.
The engine sees prices, not news, resting orders or execution. Its paper record excludes costs. It does not predict prices. Trading risk is yours; this is research tooling, not financial advice.
Every change, dated
What shipped and what changed in Sweep Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.
The confirming close starts the plan v2.0.0 Updated Sweep Desk
Default inversion-close entries, explicit context checks and an independent broker lifecycle replace the earlier execution model.
- Single and consecutive Series gaps, with optional return-entry modes retained
- First-touch completed HTF context and separate setup-data readiness
- No earlier entry-candle wick exits, plus explicit later gap and both-touch policies
- Separate broker brackets, occupancy and exits; historical v1 measurements stay labeled
Sweep Desk v1.1.0 Shipped
Mapped a sweep, confirmation and return entry to stop and target references.
Entries and exits around the sweep Request Sweep Desk
The ask Sweep Desk was built from. Shipped as v1.1.0 with the inversion-gap confirmation, the stop at the sweep extreme and both targets drawn.
- Received
- Building
- Done
“Finding the liquidity is easy. Getting the actual entries and exits right is where the gold is.”
Measured on two markets before it shipped Lab note Sweep Desk
Run with costs on across two markets. Only the inversion-gap confirmation held on both; the notes name where it barely paid.
Something to change in Sweep Desk?
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.