New

11 tools. $557.98 if bought one by one. 4 new Premium desks inside.$557.98 one by one

$97 once$97See all 11See it
Free · Pine v6 · MIT · v1.1.0

Gold Sweep Desk Free

It tracks gold's pools, refuses most raids by name, and draws a plan only when a sweep flips the nearest gap a full ATR from its extreme.

GOLD SWEEP DESK16:45
NOW
◌ OUTSIDE THE WINDOWS · next tomorrow London 02:00
WHY
Setups start only in London 02:00–05:00 and New York 07:00–11:00 (New York time). Pools still get used up now.
STEPS
01 POOLS ✓ 02 SWEEP · 03 FLIP · 04 PLAN · 05 RESULT ·
TODAY
02:45 raid Asia high · 03:00 swept Asia high · 04:00 flip · 04:00 ▼ short 3554.77 · 16:30 time −0.6R
Closed bars · New York timeGold Sweep Desk 1.1.0

Day 1 · the first after the warm-up to reach 1R

Simulated fifteen-minute gold candles (the Pine project's own seeded market), not a real market, run through Gold Sweep Desk 1.1.0's own rules, ported line for line and held to its reference engine; it draws what the script draws at each close. R is after the script's 0.40 cost estimate. Day 1 is the first simulated day after the warm-up whose plan reaches its 1R line, so every step of a plan shows; Another day plays the next simulated days with a plan, in order, whatever they did.

Read as text
Times are New York, by the candle's open, as the desk keeps them. The words on the chart and in the desk are the script's own; R is after its cost estimate.
TimeEventPrices
18:00The trading day opensPDH 3619.62 · PDL 3533.50
00:00The Asia range is set: its high and low are pools3563.48–3546.16
02:45Raid · Asia highBar 3566.89 high · 3559.40 low
03:00Swept · Asia high: a close back insideClose 3561.96
04:00Flip: a close through the nearest opposing gapClose 3554.77
04:00▼ SHORT 3554.77 (flip close)Stop 3573.67 · 1R 3535.87 · target 3516.97
05:00The London range is set: its high and low are pools3573.09–3544.30
07:301R reached1R 3535.87
16:30✗ FLAT AT THE TIME EXIT −0.6RExit 3565.09 · −0.5R before costs

No signup · Pine v6 · MIT. New York time on any chart, every decision on closed candles.

Sweeps are not signals.

The close back inside is only step two: the desk waits for a close through the gap, and a sweep that fails first ends without a plan.

NO PLAN

09:45: price reaches 3769.95, and the short at the sweep is stopped. The desk never traded it.

  • Raid: a candle trades past a fresh pool: true, 09:00 · London high raided
  • Sweep: a close back inside within three candles: true, 09:15 · close 3740.91
  • Flip: a close through the gap, a full ATR from the extreme: false, 09:30 · sweep failed
  • A short at the sweep's close, not the desk's rule: false, 09:45 · stopped

The same simulated market and rules: the first simulated day after the warm-up where a sweep failed. The red box is an entry at the sweep's close, with the desk's own stop: a tenth of an ATR past the extreme; it is not the desk's rule.

72

Raids

Candles that traded past a fresh pool inside a window.

54

Sweeps

Raids that closed back inside within three candles.

13

Flips

Sweeps that closed through the nearest gap, a full ATR from the extreme.

12

Plans

Flips whose stop and clock passed the checks, each then drawn to its ending.

The desk's own record on a real chart: OANDA:XAUUSD 15m, the 63 trading days from 30 June 2026, read on 26 September 2026.

Every refusal has a name.

Shape the candles after a raid of Asia's high, and the desk's own engine, run on them, names what it does.

▼ SHORT 3350.00 The flip, 1.43 ATR from the extreme: stop 3364.99, 1R 3335.01, target 3320.02, flat at 16:45.
1

A bullish gap under it

The close after the sweep

Drawn candles, not the simulation. The verdict is the desk's own engine, run on them at its defaults.

3 candles

To close back

A raid that stays beyond the level for three more closes is held, not swept.

30 candles

To find a gap

The nearest opposing fair value gap born in the 30 candles before the sweep.

1 ATR

To flip

A close through that gap, at least one ATR from the extreme, within 12 candles.

6 ATR

Too wide

A stop wider than six ATR, or an entry at 16:45 or later, is refused too.

Three ways out.

A plan ends at its stop, at twice its risk, or flat at 16:45 New York; Pro takes half off at the 1R line on the way.

The desk
+1.99R
Pro, half off at 1R
+1.49R

Pro's half at 1R leaves less on the day that ran.

Three plans from the simulated run: the first simulated plan after the warm-up to end at its target, the first to end at its stop, and the first to reach 1R and end at the 16:45 time exit. R is after the 0.40 cost estimate; Pro's is the same plan with half closed at 1R.

Stop

0.1 ATR past

Beyond the sweep's extreme, fixed when the plan is made, and checked first on every candle.

Target

Twice the risk

The 1R line on the way is drawn; the free desk leaves it to you.

16:45

Flat

Before gold's daily break, a plan still open closes at that candle's close.

Cost

0.40 a round trip

Your spread and slippage, set in input 04; every R here is after it.

Paste it. Run it.

Two files, the complete scripts, ready for TradingView's Pine Editor.

On a phone? Send this page to your desktop.

  1. Copy the indicatorThe button copies the whole file.
  2. Paste it in the Pine EditorOpen a new indicator, replace the sample, save.
  3. Add it to a gold chart5 or 15 minutes, standard candles; set your cost in input 04.
gold-sweep-desk.pine · 1,054 linesDownload gold-sweep-desk.pine
//@version=6// Gold Sweep Desk 1.1.0 · MIT · © 2026 ProEA Lab · built for r/pinescript// Gold liquidity as a desk: the prior-day, Asia and London highs and lows, their sweeps, the flip and the plan, in words.// Every clock is New York time; bars while spot gold is closed are skipped, so spot and 24/7 feeds agree.// Decisions happen on closed bars. The record is this chart's history after an estimated cost, not a forecast.// Rules: docs/engine-contract.md in the release · https://proea.app/en/free/gold-sweep-desk// License: MIT. Share it freely; keep this header.indicator("Gold Sweep Desk", "GoldSweep", overlay = true, behind_chart = false, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 2000)const string GS_VERSION = "1.1.0"const bool GS_IS_STRATEGY = false // ═══ ENGINE · core.pinefrag ═════════════════════════════════════════════════════════════════════════════════════// The engine decides; the renderer (visuals.pinefrag) only reads. Every engine name starts with "gs" and a// capital letter; tests/display.test.mjs refuses any renderer line that assigns one. The rules are written in// docs/engine-contract.md; tests/reference/engine.mjs is the independent reference.////   ① SWEEP  a bar in an entry window trades through a fresh pool (prior day, Asia or London high/low) and a close//            comes back inside it within a few bars.//   ② FLIP   a close goes through the nearest opposing fair value gap: the gap inverts.//   ③ PLAN   entry at that close (or at a later close back inside the gap); stop beyond the sweep's extreme; target//            in R; flat at the time exit. Stop first when a bar reaches both.// Every clock is New York time by the bar's open. Bars while spot gold is closed (Friday 17:00 → Sunday 18:00 and// the daily 17:00–18:00 break) are skipped entirely, so a 24/7 feed and a spot feed give the same bar stream.// Decisions happen on closed bars only. A plan's geometry is frozen at entry. string gsG1 = "01 · Pools and windows"bool gsPoolPd = input.bool(true, "Prior day high / low", group = gsG1, tooltip = "The previous trading day's high and low. Gold's day runs 18:00 → 17:00 New York.")bool gsPoolAsia = input.bool(true, "Asia high / low · 20:00–24:00", group = gsG1, tooltip = "The range of the bars opening 20:00–24:00 New York; a pool from the first bar after midnight.")bool gsPoolLon = input.bool(true, "London high / low · 02:00–05:00", group = gsG1, tooltip = "The range of the bars opening 02:00–05:00 New York; a pool from the first bar after 05:00.")string gsWindows = input.string("London + New York", "Entry windows", options = ["London + New York", "Continuous 01:00–11:00"], group = gsG1, tooltip = "A setup can only start on a bar that opens inside a window (New York time). The pools are used up by any bar that trades through them, window or not.")bool gsWinLon = input.bool(true, "  London 02:00–05:00", group = gsG1)bool gsWinNy = input.bool(true, "  New York 07:00–11:00", group = gsG1) string gsG2 = "02 · Sweep and flip"float gsDepth = input.float(0.05, "Sweep depth · ATR beyond the level", minval = 0, maxval = 5, step = 0.05, group = gsG2, tooltip = "The raid must reach at least this far beyond the outermost pool it takes, in ATR(14).")int gsReclaim = input.int(3, "Close back inside within · bars", minval = 0, maxval = 20, group = gsG2, tooltip = "The close back inside the swept level may come on the raid bar or on this many bars after it. Otherwise price was accepted beyond the level (HELD).")string gsFailOn = input.string("A close back beyond the level", "A sweep fails on", options = ["A close back beyond the level", "Any new extreme"], group = gsG2, tooltip = "While the flip is awaited: a close back beyond the swept level ends the setup, and a new wick only moves the extreme (and the stop) further; or any new extreme ends it.")string gsConfirm = input.string("FVG flip", "Confirmation", options = ["FVG flip", "Close back only"], group = gsG2, tooltip = "FVG flip: a close through the nearest opposing fair value gap. Close back only: the sweep's own close is the trigger.")int gsLookback = input.int(30, "Gap born within · bars before the sweep", minval = 3, maxval = 200, group = gsG2)int gsFlipBars = input.int(12, "Flip within · bars after the sweep", minval = 0, maxval = 100, group = gsG2)float gsMinLeg = input.float(1.0, "Leg from the extreme · at least ATR", minval = 0, maxval = 10, step = 0.1, group = gsG2, tooltip = "The flip close must be at least this far from the sweep's extreme.") string gsG3 = "03 · Plan"string gsEntryMode = input.string("Flip close", "Entry", options = ["Flip close", "Retest close"], group = gsG3, tooltip = "Flip close: the flip's close is the entry. Retest close: after the flip, the first close back inside the gap within the retest bars.")int gsRetestBars = input.int(12, "  Retest within · bars", minval = 1, maxval = 100, group = gsG3)float gsBuffer = input.float(0.1, "Stop buffer · ATR beyond the extreme", minval = 0, maxval = 3, step = 0.05, group = gsG3)float gsMaxStop = input.float(6.0, "Skip if the stop is wider than · ATR", minval = 0.5, maxval = 50, step = 0.5, group = gsG3)float gsTargetR = input.float(2.0, "Target · R", minval = 0.5, maxval = 20, step = 0.5, group = gsG3, tooltip = "A design choice, not a tested setting. 1R is the distance from the entry to the stop; the 1R milestone is recorded on the way.")string gsExitAt = input.string("16:45", "Flat at · HH:MM New York", group = gsG3, tooltip = "An open plan closes at the close of the bar that ends at or after this time; no entry on or after it. Gold's daily break starts at 17:00.")string gsSide = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gsG3) string gsG4 = "04 · Costs and record"float gsCost = input.float(0.40, "Estimated round-trip cost · price", minval = 0, step = 0.05, group = gsG4, tooltip = "Spread plus slippage for entry and exit together, in the chart's price (0.40 = 40 cents an ounce on spot gold). Every R on the desk is after this estimate. Set your own broker's.")int gsPctMin = input.int(20, "Show percentages from N closed plans", minval = 5, maxval = 500, group = gsG4, tooltip = "Below this many closed plans the desk shows counts only: a percentage of a few plans says little.") // ── clock: New York, by the bar's openstring gsTz = "America/New_York"float gsTick = syminfo.mintickbool gsClosed = barstate.isconfirmedf_gsClock(string hm) =>

No coding needed.

View the full code

Strategy Lab · optional

gold-sweep-desk-strategy.pine

The same plans as orders for TradingView's Strategy Tester, with a commission that matches the desk's cost estimate.

Download gold-sweep-desk-strategy.pine

No coding needed.

View the full code

01 · Free

No signup

The full source under MIT; nothing is locked.

02 · Pine v6

Two files

The indicator reads and plans; the Strategy Lab sends the same plans to the Strategy Tester.

03 · One clock

New York

Whatever your chart's time zone; the closed hours are skipped, so every gold feed agrees.

04 · Alerts

Every event

Raids, sweeps, flips, plans and endings, in Text or JSON; alerts never place orders.

The plan before London.

Pro draws both sides of gold's day before the window opens, so you see the level a sweep must reach and the close that would flip it.

Gold Sweep Desk Pro Everything here, plus:

  • The game plan. Both sides drawn before each window, with each trigger.
  • Seven context checks. Each says what it sees; none is a forecast.
  • SMT with silver. Gold makes a new high or low; silver does not.
  • Half off at 1R. And a day guard for your limit of plans or losses.
  • The Radar. Eight markets in one table.
  • Full-plan alerts. On your phone, Discord or Telegram.
$49, one payment.
See Gold Sweep Desk Pro

No subscription Full source 7-day refund before download

Pro is a set of tools for reading and planning gold's day. It does not trade for you and it does not promise results.

What's inside

  • Gold Sweep Desk Pro
  • Gold Sweep Radar Pro
  • Pro Strategy Lab
  • Research Lab
  • Alert relay
  • AI Builder Kit

Straight answers.

Is it really free?

Yes. Both scripts are free under MIT: the full source, no signup, nothing locked.

Does it have an edge?

No tested edge. We tested sixteen rule variants, each registered before its result: none passed, and the one selected failed its check. The desk is a way to read gold's day, not a promise of results. What we measured has every row.

Is a plan a trade?

No. The indicator draws plans and says why; it places no orders. The optional Strategy Lab sends the same plans to TradingView's Strategy Tester.

Why New York time?

Every clock on the desk is New York's, by the candle's open, whatever your chart shows. Gold's day runs 18:00 to 17:00, and the closed hours are skipped, so a spot chart and a 24-hour feed show the same setups.

Which charts does it read?

Gold on 5 or 15 minutes with standard candles. On 30 minutes or more, or under 5, a TIMEFRAME line says why the desk was not built for them.

What does Pro add?

The game plan before each window, seven context checks, SMT with silver, half off at 1R, a day guard, the eight-market Radar and full-plan alerts. The free desk stays complete, free and MIT.

We, ProEA Lab, build Gold Sweep Desk. The films run its rules on simulated candles, not market data. Not financial advice.

Full manual.

Read your chart, install it, and every rule one question at a time.

Open the full manualClose the full manual

00 Why is a raid not a sweep?

Beyond yesterday's high and low and the Asia and London ranges sit other traders' stops. A candle that trades through one of those pools in the London or New York window is a raid. It is a sweep only when a close comes back inside within three candles: the stops were taken and price did not stay there. Try it.

ASIA H · a fresh poolSWEPT ASIA H

● SWEPT · Asia high · waiting for the flip The stops above were taken and price did not stay there.

01 Which gap flips?

After the sweep the desk takes the nearest opposing fair value gap born in the thirty candles before it. The flip is a close through that gap, at least one ATR from the sweep's extreme, within twelve candles. The entry is that close; the stop sits a tenth of an ATR beyond the extreme; the target is twice the risk, with the 1R line on the way.

the sweep's extreme1 ATRthe nearest opposing gapa close below the gap flips it

The gap sits more than one ATR from the extreme, so a close through it is the flip, and the entry is that close.

02 Read your chart.

Gold Sweep Desk on a gold fifteen-minute chart: in the London window the prior day's high is swept and a short is drawn with its stop and target; in New York the prior day's low is swept, the gap flips and a long is drawn with its stop and target; the Asia box and the day's pools are marked, with the desk top right.
OANDA:XAUUSD 15m on TradingView: two sweeps and the plan each one made. The desk counts this chart's history, not a forecast.
  1. The state line: ○ WATCHING, ● RAID, ● SWEPT, ● IN TRADE, ◌ OUTSIDE THE WINDOWS with the next one, ◌ GOLD IS CLOSED.
  2. The ribbon: 01 POOLS · 02 SWEEP · 03 FLIP · 04 PLAN · 05 RESULT, each ✓ done, ▶ in progress or · not yet.
  3. The steps: each ✓ met, ✗ not met or — waiting, all required, with the level that completes it.
  4. Nearest fresh: the closest untouched pool above and below, with its distance.
  5. Today: every raid, sweep, flip, entry and ending of the day, in New York time.
  6. On the chart: the pools and their names (a cross when taken), the Asia and London boxes, SWEPT at the extreme, the flipped gap, the plan's stop and target zones, the 1R line and how each plan ended. A ◌ marks a setup that stopped before a plan, and its tooltip says why.
Gold Sweep Desk on a quiet gold day: the Asia and London boxes, the pools taken during the day marked with crosses, the prior day's low still fresh as a solid gold line, and the desk explaining that it waits for the next window.
A quiet day: the pools taken are crossed, and the desk says which window it waits for.

03 Install and check.

  1. Open a 5- or 15-minute gold chartXAUUSD with standard candles, not Heikin Ashi.
  2. Paste and addPine Editor → New indicator → paste → Save → Add to chart.
  3. Set your costEstimated round-trip cost · price (04) is your broker's spread and slippage: 0.40 means 40 cents an ounce.

Download the indicator (.pine)Download the Strategy Lab (.pine)Download the complete 1.1.0 package (.zip)MIT licence

The indicator · the complete source · gold-sweep-desk.pine
//@version=6
// Gold Sweep Desk 1.1.0 · MIT · © 2026 ProEA Lab · built for r/pinescript
// Gold liquidity as a desk: the prior-day, Asia and London highs and lows, their sweeps, the flip and the plan, in words.
// Every clock is New York time; bars while spot gold is closed are skipped, so spot and 24/7 feeds agree.
// Decisions happen on closed bars. The record is this chart's history after an estimated cost, not a forecast.
// Rules: docs/engine-contract.md in the release · https://proea.app/en/free/gold-sweep-desk
// License: MIT. Share it freely; keep this header.
indicator("Gold Sweep Desk", "GoldSweep", overlay = true, behind_chart = false, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 2000)
const string GS_VERSION = "1.1.0"
const bool GS_IS_STRATEGY = false

// ═══ ENGINE · core.pinefrag ═════════════════════════════════════════════════════════════════════════════════════
// The engine decides; the renderer (visuals.pinefrag) only reads. Every engine name starts with "gs" and a
// capital letter; tests/display.test.mjs refuses any renderer line that assigns one. The rules are written in
// docs/engine-contract.md; tests/reference/engine.mjs is the independent reference.
//
//   ① SWEEP  a bar in an entry window trades through a fresh pool (prior day, Asia or London high/low) and a close
//            comes back inside it within a few bars.
//   ② FLIP   a close goes through the nearest opposing fair value gap: the gap inverts.
//   ③ PLAN   entry at that close (or at a later close back inside the gap); stop beyond the sweep's extreme; target
//            in R; flat at the time exit. Stop first when a bar reaches both.
// Every clock is New York time by the bar's open. Bars while spot gold is closed (Friday 17:00 → Sunday 18:00 and
// the daily 17:00–18:00 break) are skipped entirely, so a 24/7 feed and a spot feed give the same bar stream.
// Decisions happen on closed bars only. A plan's geometry is frozen at entry.

string gsG1 = "01 · Pools and windows"
bool gsPoolPd = input.bool(true, "Prior day high / low", group = gsG1, tooltip = "The previous trading day's high and low. Gold's day runs 18:00 → 17:00 New York.")
bool gsPoolAsia = input.bool(true, "Asia high / low · 20:00–24:00", group = gsG1, tooltip = "The range of the bars opening 20:00–24:00 New York; a pool from the first bar after midnight.")
bool gsPoolLon = input.bool(true, "London high / low · 02:00–05:00", group = gsG1, tooltip = "The range of the bars opening 02:00–05:00 New York; a pool from the first bar after 05:00.")
string gsWindows = input.string("London + New York", "Entry windows", options = ["London + New York", "Continuous 01:00–11:00"], group = gsG1, tooltip = "A setup can only start on a bar that opens inside a window (New York time). The pools are used up by any bar that trades through them, window or not.")
bool gsWinLon = input.bool(true, "  London 02:00–05:00", group = gsG1)
bool gsWinNy = input.bool(true, "  New York 07:00–11:00", group = gsG1)

string gsG2 = "02 · Sweep and flip"
float gsDepth = input.float(0.05, "Sweep depth · ATR beyond the level", minval = 0, maxval = 5, step = 0.05, group = gsG2, tooltip = "The raid must reach at least this far beyond the outermost pool it takes, in ATR(14).")
int gsReclaim = input.int(3, "Close back inside within · bars", minval = 0, maxval = 20, group = gsG2, tooltip = "The close back inside the swept level may come on the raid bar or on this many bars after it. Otherwise price was accepted beyond the level (HELD).")
string gsFailOn = input.string("A close back beyond the level", "A sweep fails on", options = ["A close back beyond the level", "Any new extreme"], group = gsG2, tooltip = "While the flip is awaited: a close back beyond the swept level ends the setup, and a new wick only moves the extreme (and the stop) further; or any new extreme ends it.")
string gsConfirm = input.string("FVG flip", "Confirmation", options = ["FVG flip", "Close back only"], group = gsG2, tooltip = "FVG flip: a close through the nearest opposing fair value gap. Close back only: the sweep's own close is the trigger.")
int gsLookback = input.int(30, "Gap born within · bars before the sweep", minval = 3, maxval = 200, group = gsG2)
int gsFlipBars = input.int(12, "Flip within · bars after the sweep", minval = 0, maxval = 100, group = gsG2)
float gsMinLeg = input.float(1.0, "Leg from the extreme · at least ATR", minval = 0, maxval = 10, step = 0.1, group = gsG2, tooltip = "The flip close must be at least this far from the sweep's extreme.")

string gsG3 = "03 · Plan"
string gsEntryMode = input.string("Flip close", "Entry", options = ["Flip close", "Retest close"], group = gsG3, tooltip = "Flip close: the flip's close is the entry. Retest close: after the flip, the first close back inside the gap within the retest bars.")
int gsRetestBars = input.int(12, "  Retest within · bars", minval = 1, maxval = 100, group = gsG3)
float gsBuffer = input.float(0.1, "Stop buffer · ATR beyond the extreme", minval = 0, maxval = 3, step = 0.05, group = gsG3)
float gsMaxStop = input.float(6.0, "Skip if the stop is wider than · ATR", minval = 0.5, maxval = 50, step = 0.5, group = gsG3)
float gsTargetR = input.float(2.0, "Target · R", minval = 0.5, maxval = 20, step = 0.5, group = gsG3, tooltip = "A design choice, not a tested setting. 1R is the distance from the entry to the stop; the 1R milestone is recorded on the way.")
string gsExitAt = input.string("16:45", "Flat at · HH:MM New York", group = gsG3, tooltip = "An open plan closes at the close of the bar that ends at or after this time; no entry on or after it. Gold's daily break starts at 17:00.")
string gsSide = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gsG3)

string gsG4 = "04 · Costs and record"
float gsCost = input.float(0.40, "Estimated round-trip cost · price", minval = 0, step = 0.05, group = gsG4, tooltip = "Spread plus slippage for entry and exit together, in the chart's price (0.40 = 40 cents an ounce on spot gold). Every R on the desk is after this estimate. Set your own broker's.")
int gsPctMin = input.int(20, "Show percentages from N closed plans", minval = 5, maxval = 500, group = gsG4, tooltip = "Below this many closed plans the desk shows counts only: a percentage of a few plans says little.")

// ── clock: New York, by the bar's open
string gsTz = "America/New_York"
float gsTick = syminfo.mintick
bool gsClosed = barstate.isconfirmed
f_gsClock(string hm) =>
    array<string> parts = str.split(hm, ":")
    int out = -1
    if array.size(parts) == 2
        float h = str.tonumber(array.get(parts, 0))
        float m = str.tonumber(array.get(parts, 1))
        if not na(h) and not na(m)
            out := int(h) * 60 + int(m)
    out
// A trading day starts at 18:00: its id is the New York date six hours later.
f_gsDayId(int t) =>
    int t6 = t + 6 * 3600000
    year(t6, "America/New_York") * 10000 + month(t6, "America/New_York") * 100 + dayofmonth(t6, "America/New_York")
int gsExitMin = f_gsClock(gsExitAt) < 0 ? 16 * 60 + 45 : f_gsClock(gsExitAt)
int gsDow = dayofweek(time, gsTz)
int gsHm = hour(time, gsTz) * 60 + minute(time, gsTz)
int gsDayNow = f_gsDayId(time)
int gsCloseHm = hour(time_close, gsTz) * 60 + minute(time_close, gsTz)
int gsCloseDay = f_gsDayId(time_close)
// dayofweek: 1 = Sunday … 7 = Saturday.
bool gsGoldShut = gsDow == dayofweek.saturday or (gsDow == dayofweek.sunday and gsHm < 1080) or (gsDow == dayofweek.friday and gsHm >= 1020) or (gsHm >= 1020 and gsHm < 1080)
bool gsInAsia = gsHm >= 1200
bool gsInLon = gsHm >= 120 and gsHm < 300
string gsWinNow = gsWindows == "Continuous 01:00–11:00" ? (gsHm >= 60 and gsHm < 660 ? (gsHm < 420 ? "London" : "New York") : "") : (gsWinLon and gsHm >= 120 and gsHm < 300) ? "London" : (gsWinNy and gsHm >= 420 and gsHm < 660) ? "New York" : ""
bool gsAvailable = chart.is_standard and timeframe.isintraday and timeframe.in_seconds() <= 3600
string gsUnavailable = gsAvailable ? "" : not chart.is_standard ? "standard candles needed" : "an intraday chart of 1 hour or less is needed"

// ── state
type GSPool
    string name
    int side
    float price
    bool fresh = true
    int bornBar = na
    int takenBar = na
    int day = 0

type GSGap
    int dir
    int born
    int bornBar
    float bottom
    float top
    bool killed = false

// The setup in progress: 0 idle · 1 raid · 2 swept · 3 flipped (waiting for the retest).
type GSSetup
    int state = 0
    int dir = 0
    string window = ""
    float ref = na
    string pool = ""
    string pools = ""
    int poolsN = 0
    float extreme = na
    float depthAtr = na
    int raidN = na
    int raidBar = na
    int sweepN = na
    int sweepBar = na
    float gapB = na
    float gapT = na
    int gapBar = na
    float gapAtr = na
    int flipN = na
    int flipBar = na
    float legAtr = na
    float atrFlip = na
    float stop = na

// The plan (dir 0 = none). Its geometry is frozen at entry; a copy goes to gsDone when it ends.
type GSPlan
    int dir = 0
    int day = 0
    string window = ""
    string pool = ""
    int poolsN = 0
    float depthAtr = na
    float legAtr = na
    float gapAtr = na
    float riskAtr = na
    int raidBar = na
    int sweepBar = na
    int flipBar = na
    int gapBar = na
    int entryN = na
    int entryBar = na
    int entryTime = na
    string how = ""
    float extreme = na
    float gapB = na
    float gapT = na
    float entry = na
    float stop = na
    float risk = na
    float tp1 = na
    float target = na
    bool tp1Hit = false
    int tp1Bar = na
    int exitBar = na
    int exitTime = na
    float exitPrice = na
    string ending = ""
    float grossR = na
    float netR = na

type GSStat
    int plans = 0
    int wins = 0
    float sumR = 0.0
    int nTarget = 0
    int nStop = 0
    int nTime = 0
    int nTp1 = 0
    int raids = 0
    int sweeps = 0
    int flips = 0
    int held = 0
    int noGap = 0
    int failed = 0
    int noFlip = 0
    int noEntry = 0

// What happened on this bar.
type GSEv
    int raid = 0
    int sweep = 0
    int flip = 0
    int entry = 0
    bool exit = false
    string why = ""
    float r = na
    int exitDir = 0
    bool tp1 = false
    string stopped = ""
    string pool = ""

var GSSetup gsS = GSSetup.new()
var GSPlan gsP = GSPlan.new()
var GSStat gsSt = GSStat.new()
var array<GSPool> gsPools = array.new<GSPool>()
var array<GSPool> gsPoolLog = array.new<GSPool>()
var array<GSGap> gsGaps = array.new<GSGap>()
var array<GSPlan> gsDone = array.new<GSPlan>()
var array<int> gsEvBar = array.new<int>()
var array<string> gsEvWhat = array.new<string>()
var array<float> gsEvPx = array.new<float>()
GSEv gsEv = GSEv.new()
var float gsAtr = na
var float gsTrSum = 0.0
var int gsTrN = 0
var float gsPrevC = na
var int gsN = -1
var int gsDay = 0
var float gsDayH = na
var float gsDayL = na
var float gsAsiaH = na
var float gsAsiaL = na
var bool gsAsiaDone = false
var float gsLonH = na
var float gsLonL = na
var bool gsLonDone = false
var int gsRestN = -1
var float gsH1 = na
var float gsL1 = na
var int gsD1 = 0
var float gsH2 = na
var float gsL2 = na
var int gsD2 = 0
// The day's story for the desk, in words (a field, so the helpers below can extend it).
type GSText
    string log = ""
var GSText gsTx = GSText.new()

f_gsRText(float v) =>
    (v < 0 ? "−" : "+") + str.tostring(math.abs(v), "0.0") + "R"
f_gsHhmm() =>
    str.tostring(hour(time, "America/New_York"), "00") + ":" + str.tostring(minute(time, "America/New_York"), "00")
f_gsLogAdd(string what) =>
    gsTx.log := gsTx.log + (gsTx.log == "" ? "" : " · ") + f_gsHhmm() + what
f_gsNote(string what) =>
    array.push(gsEvBar, bar_index)
    array.push(gsEvWhat, what)
    array.push(gsEvPx, close)
    if array.size(gsEvBar) > 200
        array.shift(gsEvBar)
        array.shift(gsEvWhat)
        array.shift(gsEvPx)
f_gsPoolNew(string name, int side, float price, int day) =>
    GSPool p = GSPool.new(name, side, price, true, bar_index, na, day)
    array.push(gsPools, p)
    array.push(gsPoolLog, p)
    if array.size(gsPoolLog) > 120
        array.shift(gsPoolLog)
f_gsReset(string why) =>
    // A setup ends without a plan: remembered for the desk and the alerts.
    gsEv.stopped := why
    gsS.state := 0
    gsS.dir := 0
    if why == "HELD"
        gsSt.held += 1
    else if why == "NO GAP"
        gsSt.noGap += 1
    else if why == "SWEEP FAILED"
        gsSt.failed += 1
    else if why == "NO FLIP" or why == "NO RETEST"
        gsSt.noFlip += 1
    else
        gsSt.noEntry += 1
    f_gsNote(why)
    f_gsLogAdd(" " + str.lower(why))

// Entry at this close, or a refusal. Returns true when a plan starts.
f_gsEnter(float px, string how) =>
    int d = gsS.dir
    float risk = (px - gsS.stop) * d
    bool ok = false
    if not (risk > 0)
        f_gsReset("NO ENTRY · stop not beyond entry")
    else if risk > gsMaxStop * gsS.atrFlip
        f_gsReset("NO ENTRY · stop too wide")
    else if not (gsCloseDay == gsDayNow and gsCloseHm < gsExitMin)
        f_gsReset("NO ENTRY · too late in the day")
    else
        ok := true
        gsP.dir := d
        gsP.day := gsDayNow
        gsP.window := gsS.window
        gsP.pool := gsS.pool
        gsP.poolsN := gsS.poolsN
        gsP.depthAtr := gsS.depthAtr
        gsP.legAtr := gsS.legAtr
        gsP.gapAtr := gsS.gapAtr
        gsP.riskAtr := risk / gsS.atrFlip
        gsP.raidBar := gsS.raidBar
        gsP.sweepBar := gsS.sweepBar
        gsP.flipBar := gsS.flipBar
        gsP.gapBar := gsS.gapBar
        gsP.entryN := gsN
        gsP.entryBar := bar_index
        gsP.entryTime := time
        gsP.how := how
        gsP.extreme := gsS.extreme
        gsP.gapB := gsS.gapB
        gsP.gapT := gsS.gapT
        gsP.entry := px
        gsP.stop := gsS.stop
        gsP.risk := risk
        gsP.tp1 := px + d * risk
        gsP.target := px + d * gsTargetR * risk
        gsP.tp1Hit := false
        gsP.tp1Bar := na
        gsP.exitBar := na
        gsP.exitTime := na
        gsP.exitPrice := na
        gsP.ending := ""
        gsP.grossR := na
        gsP.netR := na
        gsS.state := 4
        gsEv.entry := d
        f_gsNote(d == 1 ? "ENTRY LONG" : "ENTRY SHORT")
        f_gsLogAdd((d == 1 ? " ▲ long " : " ▼ short ") + str.tostring(px, format.mintick))
    ok

// The flip on this bar, if the close goes through the gap far enough from the extreme.
f_gsFlip() =>
    int d = gsS.dir
    bool through = d == -1 ? close < gsS.gapB : close > gsS.gapT
    float leg = (gsS.extreme - close) * -d
    bool done = false
    if through and leg >= gsMinLeg * gsAtr
        done := true
        gsS.flipN := gsN
        gsS.flipBar := bar_index
        gsS.legAtr := leg / gsAtr
        gsS.atrFlip := gsAtr
        gsS.stop := d == -1 ? math.ceil((gsS.extreme + gsBuffer * gsAtr) / gsTick - 1e-9) * gsTick : math.floor((gsS.extreme - gsBuffer * gsAtr) / gsTick + 1e-9) * gsTick
        gsEv.flip := d
        gsSt.flips += 1
        f_gsNote("FLIP")
        f_gsLogAdd(" flip")
        if gsEntryMode == "Flip close"
            f_gsEnter(close, "flip close")
        else
            gsS.state := 3
    done

// The sweep on this bar: pick the gap to invert (or trigger at once with "Close back only").
f_gsSweep() =>
    gsS.sweepN := gsN
    gsS.sweepBar := bar_index
    gsEv.sweep := gsS.dir
    gsEv.pool := gsS.pool
    gsSt.sweeps += 1
    f_gsNote("SWEEP " + gsS.pool)
    f_gsLogAdd(" swept " + gsS.pool)
    if gsConfirm == "Close back only"
        gsS.legAtr := (gsS.extreme - close) * -gsS.dir / gsAtr
        gsS.atrFlip := gsAtr
        gsS.flipN := gsN
        gsS.flipBar := bar_index
        gsS.stop := gsS.dir == -1 ? math.ceil((gsS.extreme + gsBuffer * gsAtr) / gsTick - 1e-9) * gsTick : math.floor((gsS.extreme - gsBuffer * gsAtr) / gsTick + 1e-9) * gsTick
        f_gsEnter(close, "reclaim close")
    else
        int want = -gsS.dir
        GSGap best = na
        for g in gsGaps
            if g.dir == want and not g.killed and g.born >= gsN - gsLookback and g.born < gsN
                if na(best)
                    best := g
                else if want == 1 ? g.bottom > best.bottom : g.top < best.top
                    best := g
        if na(best)
            f_gsReset("NO GAP")
        else
            gsS.gapB := best.bottom
            gsS.gapT := best.top
            gsS.gapBar := best.bornBar
            gsS.gapAtr := (best.top - best.bottom) / gsAtr
            gsS.state := 2
            f_gsFlip()
    // The branches above return different types; the function returns the setup state.
    gsS.state

f_gsEnd(float px, string why) =>
    float gross = (px - gsP.entry) * gsP.dir / gsP.risk
    float net = gross - gsCost / gsP.risk
    gsP.exitBar := bar_index
    gsP.exitTime := time
    gsP.exitPrice := px
    gsP.ending := why
    gsP.grossR := gross
    gsP.netR := net

if gsClosed and gsAvailable and not gsGoldShut
    gsN += 1
    // (A) ATR over session bars, this bar included.
    float tr = na(gsPrevC) ? high - low : math.max(high - low, math.abs(high - gsPrevC), math.abs(low - gsPrevC))
    if gsTrN < 14
        gsTrSum += tr
        gsTrN += 1
        if gsTrN == 14
            gsAtr := gsTrSum / 14
    else
        gsAtr := (tr + 13 * gsAtr) / 14
    gsPrevC := close

    // (B) A new trading day: yesterday's range becomes the prior-day pools; the session pools start again.
    if gsDayNow != gsDay
        for p in gsPools
            if p.fresh
                p.takenBar := bar_index - 1
        array.clear(gsPools)
        if gsDay != 0 and gsPoolPd and not na(gsDayH)
            f_gsPoolNew("PDH", 1, gsDayH, gsDayNow)
            f_gsPoolNew("PDL", -1, gsDayL, gsDayNow)
        gsDay := gsDayNow
        gsDayH := na
        gsDayL := na
        gsAsiaH := na
        gsAsiaL := na
        gsAsiaDone := false
        gsLonH := na
        gsLonL := na
        gsLonDone := false
        gsTx.log := ""
    if not gsAsiaDone and not na(gsAsiaH) and not gsInAsia
        gsAsiaDone := true
        if gsPoolAsia
            f_gsPoolNew("Asia high", 1, gsAsiaH, gsDayNow)
            f_gsPoolNew("Asia low", -1, gsAsiaL, gsDayNow)
    if not gsLonDone and not na(gsLonH) and not gsInLon
        gsLonDone := true
        if gsPoolLon
            f_gsPoolNew("London high", 1, gsLonH, gsDayNow)
            f_gsPoolNew("London low", -1, gsLonL, gsDayNow)

    // (C) The open plan: a later day, stop first, target, then the time exit.
    if gsP.dir != 0 and gsN > gsP.entryN
        int d = gsP.dir
        bool stopHit = d == 1 ? low <= gsP.stop : high >= gsP.stop
        bool targetHit = d == 1 ? high >= gsP.target : low <= gsP.target
        bool lateDay = gsDayNow != gsP.day
        if lateDay
            f_gsEnd(open, "TIME")
        else if stopHit
            f_gsEnd((d == 1 ? open <= gsP.stop : open >= gsP.stop) ? open : gsP.stop, "STOP")
        else if targetHit
            f_gsEnd(gsP.target, "TARGET")
        else if gsCloseDay == gsP.day and gsCloseHm >= gsExitMin
            f_gsEnd(close, "TIME")
        if not gsP.tp1Hit and not stopHit and not lateDay and (d == 1 ? high >= gsP.tp1 : low <= gsP.tp1)
            gsP.tp1Hit := true
            gsP.tp1Bar := bar_index
            gsEv.tp1 := true
        if gsP.ending != ""
            gsEv.exit := true
            gsEv.why := gsP.ending
            gsEv.r := gsP.netR
            gsEv.exitDir := gsP.dir
            gsSt.plans += 1
            gsSt.wins += gsP.netR > 0 ? 1 : 0
            gsSt.sumR += gsP.netR
            gsSt.nTarget += gsP.ending == "TARGET" ? 1 : 0
            gsSt.nStop += gsP.ending == "STOP" ? 1 : 0
            gsSt.nTime += gsP.ending == "TIME" ? 1 : 0
            gsSt.nTp1 += gsP.tp1Hit ? 1 : 0
            f_gsNote(gsP.ending)
            f_gsLogAdd(" " + str.lower(gsP.ending) + " " + f_gsRText(gsP.netR))
            array.push(gsDone, gsP.copy())
            if array.size(gsDone) > 300
                array.shift(gsDone)
            gsP.dir := 0
            gsRestN := gsN
            gsS.state := 0
            gsS.dir := 0

    // (D) Pools this bar trades through are used up, whatever else happens.
    int penHN = 0
    int penLN = 0
    float penHMax = na
    float penLMin = na
    string penHName = ""
    string penLName = ""
    string penHAll = ""
    string penLAll = ""
    for p in gsPools
        if p.fresh and p.side == 1 and high > p.price
            p.fresh := false
            p.takenBar := bar_index
            penHN += 1
            penHAll += (penHAll == "" ? "" : " + ") + p.name
            if na(penHMax) or p.price > penHMax
                penHMax := p.price
                penHName := p.name
        else if p.fresh and p.side == -1 and low < p.price
            p.fresh := false
            p.takenBar := bar_index
            penLN += 1
            penLAll += (penLAll == "" ? "" : " + ") + p.name
            if na(penLMin) or p.price < penLMin
                penLMin := p.price
                penLName := p.name

    // (E) The setup: RAID → SWEPT → (FLIPPED →) plan.
    if gsP.dir == 0
        if gsS.state == 1
            int d = gsS.dir
            gsS.extreme := d == -1 ? math.max(gsS.extreme, high) : math.min(gsS.extreme, low)
            int moreN = d == -1 ? penHN : penLN
            if moreN > 0
                gsS.poolsN += moreN
                gsS.pools += " + " + (d == -1 ? penHAll : penLAll)
                float outer = d == -1 ? penHMax : penLMin
                if d == -1 ? outer > gsS.ref : outer < gsS.ref
                    gsS.ref := outer
                    gsS.pool := d == -1 ? penHName : penLName
            if d == -1 ? close < gsS.ref : close > gsS.ref
                f_gsSweep()
            else if gsN - gsS.raidN >= gsReclaim
                f_gsReset("HELD")
        else if gsS.state == 2
            int d = gsS.dir
            bool beyond = d == -1 ? high > gsS.extreme : low < gsS.extreme
            bool onClose = gsFailOn == "A close back beyond the level"
            if onClose and beyond
                gsS.extreme := d == -1 ? high : low
            if onClose ? (d == -1 ? close > gsS.ref : close < gsS.ref) : beyond
                f_gsReset("SWEEP FAILED")
            else if not f_gsFlip() and gsN - gsS.sweepN >= gsFlipBars
                f_gsReset("NO FLIP")
        else if gsS.state == 3
            int d = gsS.dir
            if d == -1 ? high >= gsS.stop : low <= gsS.stop
                f_gsReset("NO ENTRY · stop traded before the retest")
            else if close >= gsS.gapB and close <= gsS.gapT
                f_gsEnter(close, "retest close")
            else if gsN - gsS.flipN >= gsRetestBars
                f_gsReset("NO RETEST")
        else if gsS.state == 0 and gsN != gsRestN and not na(gsAtr) and gsTrN >= 14
            bool up = penHN > 0 and gsSide != "Long only"
            bool dn = penLN > 0 and gsSide != "Short only"
            if gsWinNow != "" and up != dn
                int d = up ? -1 : 1
                float outer = up ? penHMax : penLMin
                float depth = d == -1 ? high - outer : outer - low
                if depth >= gsDepth * gsAtr
                    gsS := GSSetup.new(1, d, gsWinNow, outer, up ? penHName : penLName, up ? penHAll : penLAll, up ? penHN : penLN, d == -1 ? high : low, depth / gsAtr, gsN, bar_index)
                    gsEv.raid := d
                    gsEv.pool := gsS.pool
                    gsSt.raids += 1
                    f_gsNote("RAID " + gsS.pool)
                    f_gsLogAdd(" raid " + gsS.pool)
                    if d == -1 ? close < gsS.ref : close > gsS.ref
                        f_gsSweep()

    // (F) Gaps: a close through a gap kills it; a gap completes on this bar (three bars of the same trading day).
    for g in gsGaps
        if not g.killed and g.born < gsN and (g.dir == 1 ? close < g.bottom : close > g.top)
            g.killed := true
    if gsN >= 2 and gsD2 == gsDayNow
        if low > gsH2
            array.push(gsGaps, GSGap.new(1, gsN, bar_index, gsH2, low, false))
        if high < gsL2
            array.push(gsGaps, GSGap.new(-1, gsN, bar_index, high, gsL2, false))
    while array.size(gsGaps) > 0 and array.first(gsGaps).born < gsN - 2 * gsLookback
        array.shift(gsGaps)
    gsH2 := gsH1
    gsL2 := gsL1
    gsD2 := gsD1
    gsH1 := high
    gsL1 := low
    gsD1 := gsDayNow

    // (G) Today's range and the session ranges include this bar.
    gsDayH := na(gsDayH) ? high : math.max(gsDayH, high)
    gsDayL := na(gsDayL) ? low : math.min(gsDayL, low)
    if gsInAsia and not gsAsiaDone
        gsAsiaH := na(gsAsiaH) ? high : math.max(gsAsiaH, high)
        gsAsiaL := na(gsAsiaL) ? low : math.min(gsAsiaL, low)
    if gsInLon and not gsLonDone
        gsLonH := na(gsLonH) ? high : math.max(gsLonH, high)
        gsLonL := na(gsLonL) ? low : math.min(gsLonL, low)

// ═══ ALERTS · alerts.pinefrag (indicator only) ══════════════════════════════════════════════════════════════════
// Engine events are decided on closed bars, so every message goes out once per bar close. For the messages, create
// one TradingView alert on this indicator with "Any alert() function call"; the named conditions below also work
// one by one. JSON keys: src, version, symbol, tf, event, side, pool, entry, sl, tp1, target, reason, r, time; a
// missing number is null.

string gsG5 = "05 · Alerts"
string gsAlertFmt = input.string("Text", "Alert format", options = ["Text", "JSON"], group = gsG5, tooltip = "Create the alert with 'Any alert() function call'. Text sends one readable line; JSON sends one object per event for webhooks and bots.")
bool gsAlertSetup = input.bool(true, "Also alert the sweep and the flip (before an entry)", group = gsG5)

f_gsNum(float v, string fmt) =>
    na(v) ? "null" : str.tostring(v, fmt)
f_gsPx(float v) =>
    na(v) ? "—" : str.tostring(v, format.mintick)
f_gsMsg(string ev, int side, string pool, float entry, float sl, float tp1, float target, string reason, float r) =>
    string sideTxt = side == 1 ? "LONG" : side == -1 ? "SHORT" : "NONE"
    string js = "{\"src\":\"GoldSweepDesk\",\"version\":\"" + GS_VERSION + "\",\"symbol\":\"" + syminfo.ticker + "\",\"tf\":\"" + timeframe.period + "\",\"event\":\"" + ev + "\",\"side\":\"" + sideTxt + "\",\"pool\":\"" + pool + "\",\"entry\":" + f_gsNum(entry, format.mintick) + ",\"sl\":" + f_gsNum(sl, format.mintick) + ",\"tp1\":" + f_gsNum(tp1, format.mintick) + ",\"target\":" + f_gsNum(target, format.mintick) + ",\"reason\":\"" + reason + "\",\"r\":" + f_gsNum(r, "0.00") + ",\"time\":" + str.tostring(time_close) + "}"
    string tx = "Gold Sweep Desk · " + syminfo.ticker + " " + timeframe.period + " · " + ev + (side != 0 and ev != sideTxt ? " " + sideTxt : "") + (pool != "" ? " · " + pool : "") + (na(entry) ? "" : " · entry " + f_gsPx(entry) + " · stop " + f_gsPx(sl) + " · 1R " + f_gsPx(tp1) + " · target " + f_gsPx(target)) + (reason != "" ? " · " + reason : "") + (na(r) ? "" : " · " + (r < 0 ? "−" : "+") + str.tostring(math.abs(r), "0.00") + "R after the estimated cost")
    gsAlertFmt == "JSON" ? js : tx

// In the order they happen on a bar: the 1R milestone, an exit, then a new setup, a sweep, a flip and an entry.
if gsClosed
    if gsEv.tp1 and gsP.dir != 0
        alert(f_gsMsg("ONE_R", gsP.dir, gsP.pool, gsP.entry, gsP.stop, gsP.tp1, gsP.target, "1R reached", na), alert.freq_once_per_bar_close)
    if gsEv.exit and array.size(gsDone) > 0
        GSPlan x = array.last(gsDone)
        alert(f_gsMsg("EXIT", x.dir, x.pool, x.entry, x.stop, x.tp1, x.target, x.ending, x.netR), alert.freq_once_per_bar_close)
    if gsAlertSetup and gsEv.sweep != 0
        alert(f_gsMsg("SWEEP", gsEv.sweep, gsEv.pool, na, na, na, na, "swept, waiting for the flip", na), alert.freq_once_per_bar_close)
    if gsAlertSetup and gsEv.flip != 0 and gsEv.entry == 0
        alert(f_gsMsg("FLIP", gsEv.flip, gsS.pool, na, gsS.stop, na, na, gsEntryMode == "Retest close" ? "waiting for the retest close" : gsEv.stopped, na), alert.freq_once_per_bar_close)
    if gsEv.entry != 0
        alert(f_gsMsg(gsEv.entry == 1 ? "LONG" : "SHORT", gsP.dir, gsP.pool, gsP.entry, gsP.stop, gsP.tp1, gsP.target, gsP.how, na), alert.freq_once_per_bar_close)

alertcondition(gsEv.sweep != 0, "Sweep", "Gold Sweep Desk: a pool was swept; waiting for the flip")
alertcondition(gsEv.entry == 1, "Long", "Gold Sweep Desk: long, a swept low flipped")
alertcondition(gsEv.entry == -1, "Short", "Gold Sweep Desk: short, a swept high flipped")
alertcondition(gsEv.tp1, "1R reached", "Gold Sweep Desk: the plan reached 1R")
alertcondition(gsEv.exit and gsEv.why == "TARGET", "Target", "Gold Sweep Desk: the target was reached")
alertcondition(gsEv.exit and gsEv.why == "STOP", "Stopped", "Gold Sweep Desk: the stop ended the plan")
alertcondition(gsEv.exit and gsEv.why == "TIME", "Flat at the time exit", "Gold Sweep Desk: the time exit closed the plan")

// ═══ DISPLAY · visuals.pinefrag ═════════════════════════════════════════════════════════════════════════════════
// Reads engine state and draws it. It never assigns an engine variable, requests data, trades or alerts
// (tests/display.test.mjs enforces this), so a display change cannot change a plan or a result.
// Desk standard: state line · reason · ribbon · steps (✓ / ✗ / —, ● required) · plan in price and R · today ·
// record (counts first, % only from the threshold) · footer. Co-Pilot: NOW · WHY · PLAN · LIMITS.
// Layers: the desk and the live drawings follow every tick; the history is redrawn once per closed bar.

string gsvG = "06 · View"
string gsvTheme = input.string("Standard", "Theme", options = ["Standard", "Colorblind-safe"], group = gsvG)
string gsvDeskPos = input.string("Top right", "Desk position", options = ["Top right", "Top left", "Bottom right", "Bottom left", "Off"], group = gsvG)
string gsvRows = input.string("Compact", "Desk rows", options = ["Compact", "Full"], group = gsvG, tooltip = "Full adds every pool of the day, the last plan and the average result.")
string gsvPilot = input.string("Brief", "Co-Pilot", options = ["Standard", "Brief", "Off"], group = gsvG, tooltip = "The desk in plain words. Brief shows NOW and PLAN; Standard adds WHY and LIMITS.")
string gsvPilotPos = input.string("Bottom left", "Co-Pilot position", options = ["Bottom left", "Bottom right", "Top left", "Top right"], group = gsvG)
string gsvTextSize = input.string("Normal", "Text size", options = ["Small", "Normal", "Large"], group = gsvG)
int gsvDays = input.int(2, "Days drawn", minval = 1, maxval = 30, group = gsvG, tooltip = "Pools, sessions and plans of this many trading days (today included), within the last 1,500 bars.")
bool gsvPools = input.bool(true, "Liquidity pools", group = gsvG)
bool gsvSessions = input.bool(true, "Asia and London boxes", group = gsvG)
bool gsvZones = input.bool(true, "Risk / reward zones", group = gsvG)
bool gsvMisses = input.bool(true, "Mark setups that ended without a plan (◌)", group = gsvG)
bool gsvTint = input.bool(false, "Tint candles while a plan is open", group = gsvG)

// Colour carries meaning: teal = long / target, coral = short / stop, gold = liquidity and a setup in progress.
bool gsvCb = gsvTheme == "Colorblind-safe"
color gsvUp = gsvCb ? #56B4E9 : #26A69A
color gsvDown = gsvCb ? #E69F00 : #EF5350
color gsvGold = gsvCb ? #F0E442 : #F2B33D
color gsvAsia = #7E8CE0
color gsvInk = #E6E9F0
color gsvMuted = #8A93A6
color gsvGrey = #6B7280
color gsvPanel = color.new(#10151F, 6)
string gsvSz = gsvTextSize == "Small" ? size.tiny : gsvTextSize == "Large" ? size.normal : size.small
string gsvSzState = gsvTextSize == "Small" ? size.small : gsvTextSize == "Large" ? size.large : size.normal

f_gsvCorner(string p) =>
    p == "Top left" ? position.top_left : p == "Bottom right" ? position.bottom_right : p == "Bottom left" ? position.bottom_left : position.top_right
f_gsvPx(float v) =>
    na(v) ? "—" : str.tostring(v, format.mintick)
f_gsvR(float v) =>
    na(v) ? "—" : math.abs(v) < 0.05 ? "0.0R" : (v < 0 ? "−" : "+") + str.tostring(math.abs(v), "0.0") + "R"
f_gsvHm(int mins) =>
    str.tostring(int(mins / 60) % 24, "00") + ":" + str.tostring(mins % 60, "00")
f_gsvAt(int t) =>
    str.tostring(hour(t, "America/New_York"), "00") + ":" + str.tostring(minute(t, "America/New_York"), "00")
f_gsvLeft(int mins) =>
    mins <= 0 ? "closing" : (mins >= 60 ? str.tostring(int(mins / 60)) + " h " : "") + str.tostring(mins % 60) + " m left"
f_gsvMark(int s) =>
    s == 1 ? "✓ met" : s == 0 ? "✗ not met" : s == -1 ? "— no data" : s == -3 ? "— waiting" : "— off"
f_gsvTone(int s) =>
    s == 1 ? gsvUp : s == 0 ? gsvDown : gsvMuted
f_gsvEnding(string why) =>
    why == "TARGET" ? "TARGET" : why == "STOP" ? "STOP" : why == "TIME" ? "FLAT AT THE TIME EXIT" : why
f_gsvEndMark(string why, float r) =>
    why == "TARGET" ? "✓" : why == "STOP" ? "✗" : r > 0.05 ? "✓" : r < -0.05 ? "✗" : "◌"
f_gsvWrap(string txt, int columns) =>
    array<string> words = str.split(txt, " ")
    string output = ""
    int width = 0
    for word in words
        if width > 0 and width + str.length(word) + 1 > columns
            output += "\n"
            width := 0
        output += (width > 0 ? " " : "") + word
        width += str.length(word) + 1
    output
f_gsvSide(int d) =>
    d == 1 ? "long" : "short"

// ── windows, in New York minutes
bool gsvCont = gsWindows == "Continuous 01:00–11:00"
int gsvWinEnd = gsvCont ? 660 : gsWinNow == "London" ? 300 : 660
string gsvWinTxt = gsvCont ? "01:00–11:00" : gsWinNow == "London" ? "London 02:00–05:00" : "New York 07:00–11:00"
// The next window start after this minute of the day: later today, tomorrow, or Monday (no windows at the weekend).
f_gsvNextWin(int hm) =>
    int dow = dayofweek(time, "America/New_York")
    bool later = gsvCont ? hm >= 60 : not ((gsWinLon and hm < 120) or (gsWinNy and hm < 420))
    string day = dow == dayofweek.saturday or dow == dayofweek.sunday or (dow == dayofweek.friday and later) ? "Monday " : later ? "tomorrow " : ""
    string win = gsvCont ? "01:00" : (not later and gsWinLon and hm < 120) or (later and gsWinLon) ? "London 02:00" : gsWinNy ? "New York 07:00" : ""
    win == "" ? "none (both windows off)" : day + win

// ── what the desk shows (display variables; the engine's objects are only read)
string gsvState = ""
string gsvReason = ""
string gsvNow = ""
string gsvPlan = ""
color gsvTone = gsvMuted
string gsvRib = ""
int gsvDir = 0
string gsvNearUp = ""
string gsvNearDn = ""
int gsvFresh = 0
int gsvPoolsToday = 0
if barstate.islastconfirmedhistory or barstate.isrealtime
    // Nearest fresh pools above and below the close.
    float upPx = na
    float dnPx = na
    for p in gsPools
        gsvPoolsToday += 1
        if p.fresh
            gsvFresh += 1
            if p.price >= close and (na(upPx) or p.price < upPx)
                upPx := p.price
                gsvNearUp := p.name + " " + f_gsvPx(p.price) + " (+" + str.tostring(p.price - close, format.mintick) + ")"
            if p.price < close and (na(dnPx) or p.price > dnPx)
                dnPx := p.price
                gsvNearDn := p.name + " " + f_gsvPx(p.price) + " (−" + str.tostring(close - p.price, format.mintick) + ")"
    gsvDir := gsP.dir != 0 ? gsP.dir : gsS.state != 0 ? gsS.dir : 0
    string r1 = gsvFresh > 0 ? "✓" : "·"
    string r2 = "·"
    string r3 = "·"
    string r4 = "·"
    string r5 = "·"
    if not gsAvailable
        gsvState := "— UNAVAILABLE · " + timeframe.period + " chart"
        gsvReason := gsUnavailable == "standard candles needed" ? "Heikin Ashi, Renko and other synthetic candles move the closes. Use standard candles." : "Use an intraday chart of 1 hour or less (5 or 15 minutes on gold)."
        gsvNow := "This chart cannot run the rule. " + gsvReason
    else if gsTrN < 14
        gsvState := "○ WARMING UP"
        gsvReason := "The ATR needs 14 session bars."
        gsvNow := gsvReason
    else if gsP.dir != 0
        float openR = (close - gsP.entry) * gsP.dir / gsP.risk
        gsvState := "● IN TRADE · " + (gsP.dir == 1 ? "LONG" : "SHORT") + " · " + f_gsvR(openR) + (gsP.tp1Hit ? " · 1R reached" : "")
        gsvTone := gsP.dir == 1 ? gsvUp : gsvDown
        gsvReason := "Stop " + f_gsvPx(gsP.stop) + " · 1R " + f_gsvPx(gsP.tp1) + (gsP.tp1Hit ? " ✓" : "") + " · target " + f_gsvPx(gsP.target) + " · flat at " + gsExitAt
        gsvNow := "In a " + f_gsvSide(gsP.dir) + " from " + f_gsvPx(gsP.entry) + " after the " + gsP.pool + " sweep (" + gsP.how + "), " + str.tostring(bar_index - gsP.entryBar) + " bars ago, " + f_gsvR(openR) + " at this close before costs."
        gsvPlan := "Stop first on every closed bar: the stop " + f_gsvPx(gsP.stop) + " is beyond the sweep's extreme " + f_gsvPx(gsP.extreme) + ", where the idea is wrong. The target " + f_gsvPx(gsP.target) + " ends it; otherwise it is flat at " + gsExitAt + " New York."
        r2 := "✓"
        r3 := "✓"
        r4 := "✓"
        r5 := "▶"
    else if gsGoldShut
        gsvState := "◌ GOLD IS CLOSED"
        gsvReason := "Spot gold trades Sunday 18:00 → Friday 17:00 New York, with a break 17:00–18:00. These bars are skipped."
        gsvNow := gsvReason
    else if gsS.state == 1
        int left = math.max(0, gsReclaim - (gsN - gsS.raidN))
        gsvState := "● RAID · " + gsS.pool + " · " + (gsS.dir == -1 ? "above" : "below")
        gsvTone := gsvGold
        gsvReason := "A close back " + (gsS.dir == -1 ? "below " : "above ") + f_gsvPx(gsS.ref) + " makes it a sweep (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left); otherwise price was accepted beyond the level."
        gsvNow := "Price is raiding the " + gsS.pool + " (" + f_gsvPx(gsS.ref) + ") in the " + gsS.window + " window, extreme " + f_gsvPx(gsS.extreme) + ". " + gsvReason
        r2 := "▶"
    else if gsS.state == 2
        int left = math.max(0, gsFlipBars - (gsN - gsS.sweepN))
        float lvl = gsS.dir == -1 ? math.min(gsS.gapB, gsS.extreme - gsMinLeg * gsAtr) : math.max(gsS.gapT, gsS.extreme + gsMinLeg * gsAtr)
        gsvState := "● SWEPT · " + gsS.pool + " · waiting for the flip"
        gsvTone := gsvGold
        gsvReason := "A close " + (gsS.dir == -1 ? "below " : "above ") + f_gsvPx(lvl) + " flips the gap " + f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) + " (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left). " + (gsFailOn == "Any new extreme" ? "A new extreme ends it." : "A close back " + (gsS.dir == -1 ? "above " : "below ") + f_gsvPx(gsS.ref) + " ends it.")
        gsvNow := "The " + gsS.pool + " was swept (extreme " + f_gsvPx(gsS.extreme) + "). The desk now waits for a close through the nearest opposing gap, the flip that confirms the reversal."
        r2 := "✓"
        r3 := "▶"
    else if gsS.state == 3
        int left = math.max(0, gsRetestBars - (gsN - gsS.flipN))
        gsvState := "● FLIPPED · waiting for the retest"
        gsvTone := gsvGold
        gsvReason := "A close back inside " + f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) + " enters " + f_gsvSide(gsS.dir) + " (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left); a touch of the stop " + f_gsvPx(gsS.stop) + " cancels."
        gsvNow := "The gap flipped after the " + gsS.pool + " sweep. Retest mode waits for a close back inside the flipped gap."
        r2 := "✓"
        r3 := "✓"
        r4 := "▶"
    else if gsWinNow != ""
        int left = gsvWinEnd - gsHm
        gsvState := "○ WATCHING · " + gsWinNow + " window · " + str.tostring(gsvFresh) + (gsvFresh == 1 ? " fresh pool" : " fresh pools")
        gsvTone := gsvGold
        gsvReason := gsvFresh == 0 ? "Every pool of today is used up. The next ones: Asia and London ranges, then tomorrow's prior-day levels." : "A raid of a fresh pool, then a close back inside it, starts a setup (" + f_gsvLeft(left) + ")."
        gsvNow := "Watching the " + gsWinNow + " window. " + (gsvNearUp != "" ? "Above: " + gsvNearUp + ". " : "") + (gsvNearDn != "" ? "Below: " + gsvNearDn + ". " : "") + (gsvFresh == 0 ? "No fresh pool is left today." : "")
    else
        gsvState := "◌ OUTSIDE THE WINDOWS · next " + f_gsvNextWin(gsHm)
        gsvReason := "Setups start only in " + (gsvCont ? "the 01:00–11:00 window" : (gsWinLon ? "London 02:00–05:00" : "") + (gsWinLon and gsWinNy ? " and " : "") + (gsWinNy ? "New York 07:00–11:00" : "")) + " (New York time). Pools still get used up now."
        gsvNow := "Outside the entry windows. " + (gsvNearUp != "" ? "Above: " + gsvNearUp + ". " : "") + (gsvNearDn != "" ? "Below: " + gsvNearDn + "." : "")
    gsvRib := "01 POOLS " + r1 + "  02 SWEEP " + r2 + "  03 FLIP " + r3 + "  04 PLAN " + r4 + "  05 RESULT " + r5
    if gsvPlan == ""
        gsvPlan := "Rule: sweep of the prior day, Asia or London high/low in " + (gsvCont ? "01:00–11:00" : "the London or New York window") + " → a close through the nearest opposing gap" + (gsEntryMode == "Retest close" ? " → a close back inside it" : "") + " → entry at that close, stop beyond the sweep's extreme, target " + str.tostring(gsTargetR, "0.0#") + "R, flat at " + gsExitAt + "."

// ── desk and Co-Pilot
var table gsvDesk = table.new(f_gsvCorner(gsvDeskPos), 2, 40, frame_width = 0, border_width = 0)
string gsvPilotCorner = gsvPilotPos != gsvDeskPos ? gsvPilotPos : gsvDeskPos == "Bottom left" ? "Bottom right" : "Bottom left"
var table gsvPilotT = table.new(f_gsvCorner(gsvPilotCorner), 1, 7, frame_width = 0, border_width = 0)
int gsvPOff = gsvPilotCorner == "Top left" ? 1 : 0
int gsvOff = gsvDeskPos == "Top left" ? 1 : 0
f_gsvRow(int r, string a, string b, color ca, color cb) =>
    table.cell(gsvDesk, 0, r + gsvOff, a, text_color = ca, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
    table.cell(gsvDesk, 1, r + gsvOff, b, text_color = cb, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
f_gsvWide(int r, string txt, color c, bool bold, string tip) =>
    table.cell(gsvDesk, 0, r + gsvOff, txt, text_color = c, bgcolor = gsvPanel, text_size = bold ? gsvSzState : gsvSz, text_halign = text.align_left, text_formatting = bold ? text.format_bold : text.format_none, tooltip = tip)
    table.merge_cells(gsvDesk, 0, r + gsvOff, 1, r + gsvOff)
// The rule is built for 5–15 minute candles: from 30 minutes a candle is a large part of a window and the 2R target has
// little room before 16:45; TradingView loads only a few days of 1–4 minute candles, too few for this chart's record.
int gsvTfSec = timeframe.in_seconds()
string gsvTfMins = str.tostring(gsvTfSec / 60, "#")
string gsvTfNote = gsvTfSec >= 1800 ? "TIMEFRAME · built for 5–15 minutes: on " + gsvTfMins + "-minute candles a plan has little room for its target before 16:45" : gsvTfSec < 300 ? "TIMEFRAME · built for 5–15 minutes: TradingView loads only a few days of " + gsvTfMins + "-minute candles, too few for a record" : ""
// One step row: the mark, a detail, and "● required" (every step of the sequence is required).
f_gsvStep(int r, string name, int s, string detail) =>
    f_gsvRow(r, name, f_gsvMark(s) + (detail == "" ? "" : " · " + detail) + "  ● required", gsvInk, f_gsvTone(s))

f_gsvDrawDesk() =>
    table.clear(gsvDesk, 0, 0, 1, 39)
    if gsvDeskPos != "Off"
        if gsvOff == 1
            table.cell(gsvDesk, 0, 0, "", height = 14, bgcolor = na)
            table.cell(gsvDesk, 1, 0, "", height = 14, bgcolor = na)
        bool full = gsvRows == "Full"
        bool inPlan = gsP.dir != 0
        int st = gsS.state
        // Step states: 1 met · 0 not met · -1 no data · -3 waiting.
        int sWin = inPlan or st > 0 ? 1 : gsWinNow != "" ? 1 : 0
        int sPool = inPlan or st > 0 ? 1 : gsvFresh > 0 ? 1 : 0
        int sSweep = inPlan or st >= 2 ? 1 : st == 1 ? -3 : -3
        int sGap = inPlan or st >= 2 ? 1 : -3
        int sFlip = inPlan or st == 3 ? 1 : -3
        int met = (sWin == 1 ? 1 : 0) + (sPool == 1 ? 1 : 0) + (sSweep == 1 ? 1 : 0) + (sGap == 1 ? 1 : 0) + (sFlip == 1 ? 1 : 0)
        f_gsvWide(0, "GOLD SWEEP DESK · " + syminfo.ticker + " · " + timeframe.period + (GS_IS_STRATEGY ? " · STRATEGY LAB" : ""), gsvMuted, false, "Gold Sweep Desk " + GS_VERSION + " · a paper reference decided on closed bars, New York time." + (GS_IS_STRATEGY ? " The Strategy Tester shows TradingView's fills of the same plans." : "") + " Fills at your broker can differ.")
        f_gsvWide(1, gsvState, gsvTone, true, "Pools → raid → sweep → flip → plan → result.")
        f_gsvWide(2, f_gsvWrap(gsvReason, 58), gsvInk, false, "")
        f_gsvWide(3, gsvRib, gsvMuted, false, "01 fresh pools · 02 a raid and a close back inside · 03 the gap flips · 04 the plan · 05 its result")
        f_gsvWide(4, "STEPS" + (gsvDir != 0 ? " · for a " + f_gsvSide(gsvDir) : "") + " · " + str.tostring(met) + " of 5 required met", gsvMuted, false, "")
        f_gsvStep(5, "Window", sWin, inPlan or st > 0 ? (inPlan ? gsP.window : gsS.window) : gsWinNow != "" ? gsvWinTxt + " · " + f_gsvLeft(gsvWinEnd - gsHm) : "closed · next " + f_gsvNextWin(gsHm))
        f_gsvStep(6, "Fresh pool", sPool, inPlan ? gsP.pool : st > 0 ? gsS.pool : str.tostring(gsvFresh) + " of " + str.tostring(gsvPoolsToday) + " today")
        f_gsvStep(7, "Sweep · close back inside", sSweep, inPlan ? gsP.pool + " · extreme " + f_gsvPx(gsP.extreme) : st >= 2 ? gsS.pool + " · extreme " + f_gsvPx(gsS.extreme) : st == 1 ? "raid under way" : "")
        f_gsvStep(8, "Opposing gap", sGap, inPlan ? f_gsvPx(gsP.gapB) + "–" + f_gsvPx(gsP.gapT) : st >= 2 ? f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) : "")
        f_gsvStep(9, "Flip close ≥ " + str.tostring(gsMinLeg, "0.0#") + " ATR from the extreme", sFlip, inPlan ? f_gsvAt(gsP.entryTime) : st == 3 ? "done" : "")
        int r = 10
        if gsvNearUp != "" or gsvNearDn != ""
            f_gsvRow(r, "Nearest fresh", (gsvNearUp != "" ? "▲ " + gsvNearUp : "") + (gsvNearUp != "" and gsvNearDn != "" ? "\n" : "") + (gsvNearDn != "" ? "▼ " + gsvNearDn : ""), gsvMuted, gsvGold)
            r += 1
        if full
            for p in gsPools
                if r < 24
                    f_gsvRow(r, "  " + p.name, f_gsvPx(p.price) + (p.fresh ? " · fresh" : " · taken"), gsvMuted, p.fresh ? gsvGold : gsvGrey)
                    r += 1
        if inPlan
            f_gsvWide(r, "PLAN · " + f_gsvSide(gsP.dir) + " · " + gsP.how + " · frozen at entry", gsvMuted, false, "Frozen at entry.")
            f_gsvRow(r + 1, "Entry", f_gsvPx(gsP.entry) + " · " + str.tostring(bar_index - gsP.entryBar) + " bars ago", gsvMuted, gsvGold)
            f_gsvRow(r + 2, "Stop · 1R", f_gsvPx(gsP.stop) + " · " + str.tostring(math.abs(close - gsP.stop), format.mintick) + " away", gsvMuted, gsvDown)
            f_gsvRow(r + 3, "1R", f_gsvPx(gsP.tp1) + (gsP.tp1Hit ? " ✓" : " · " + str.tostring(math.abs(gsP.tp1 - close), format.mintick) + " away"), gsvMuted, gsvUp)
            f_gsvRow(r + 4, "Target · " + str.tostring(gsTargetR, "0.0#") + "R", f_gsvPx(gsP.target) + " · " + str.tostring(math.abs(gsP.target - close), format.mintick) + " away", gsvMuted, gsvUp)
            r += 5
        else
            f_gsvWide(r, f_gsvWrap("PLAN · " + gsvPlan, 58), gsvMuted, false, "")
            r += 1
        if gsTx.log != ""
            f_gsvWide(r, f_gsvWrap("TODAY " + gsTx.log, 58), gsvInk, false, "New York times.")
            r += 1
        string counts = "THIS CHART " + str.tostring(gsSt.raids) + " raids · " + str.tostring(gsSt.sweeps) + " sweeps · " + str.tostring(gsSt.flips) + " flips · " + (gsSt.plans == 0 ? "no closed plan yet" : str.tostring(gsSt.plans) + " plans: target " + str.tostring(gsSt.nTarget) + " · stop " + str.tostring(gsSt.nStop) + " · time " + str.tostring(gsSt.nTime) + " · reached 1R " + str.tostring(gsSt.nTp1))
        f_gsvWide(r, f_gsvWrap(counts, 58), gsvInk, false, "Counts of this chart's history. History on this chart, not a forecast.")
        r += 1
        if full
            string avgTxt = gsSt.plans == 0 ? "Average: needs a closed plan" : "Average " + f_gsvR(gsSt.sumR / gsSt.plans) + " over " + str.tostring(gsSt.plans) + " plans after an estimated " + str.tostring(gsCost, "0.00") + " round trip" + (gsSt.plans >= gsPctMin ? " · " + str.tostring(math.round(100.0 * gsSt.wins / gsSt.plans)) + "% closed above 0R" : "")
            f_gsvWide(r, f_gsvWrap(avgTxt, 58), gsvMuted, false, "Net R after the estimated cost in 04. A percentage appears only from " + str.tostring(gsPctMin) + " closed plans.")
            string ended = "Ended before a plan: held " + str.tostring(gsSt.held) + " · no gap " + str.tostring(gsSt.noGap) + " · sweep failed " + str.tostring(gsSt.failed) + " · no flip " + str.tostring(gsSt.noFlip) + " · refused " + str.tostring(gsSt.noEntry)
            f_gsvWide(r + 1, f_gsvWrap(ended, 58), gsvMuted, false, "")
            r += 2
        if gsvTfNote != ""
            f_gsvWide(r, f_gsvWrap(gsvTfNote, 58), gsvGold, false, "The rule's windows and its 16:45 exit are set in minutes of the New York day.")
            r += 1
        f_gsvWide(r, "Rules, not odds · history, not a forecast · ProEA Lab", gsvMuted, false, "")

f_gsvDrawPilot() =>
    table.clear(gsvPilotT, 0, 0, 0, 6)
    if gsvPilot != "Off"
        string why = "Above an old high and below an old low sit other traders' stops. A raid through such a level that closes back inside took that liquidity without accepting the new price. The flip, a close through the nearest opposing gap, is the sign that the move has turned; the sweep's extreme is where the idea is wrong."
        string limits = "A rule, not a forecast. The pools are price levels, not an order book. Results are this chart's history after an estimated " + str.tostring(gsCost, "0.00") + " round trip; fills at your broker can differ."
        int o = gsvPOff
        if o == 1
            table.cell(gsvPilotT, 0, 0, "", height = 14, bgcolor = na)
        table.cell(gsvPilotT, 0, 0 + o, "CO-PILOT", text_color = gsvMuted, bgcolor = gsvPanel, text_size = size.tiny, text_halign = text.align_left)
        table.cell(gsvPilotT, 0, 1 + o, f_gsvWrap("NOW · " + gsvNow, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilot == "Standard"
            table.cell(gsvPilotT, 0, 2 + o, f_gsvWrap("WHY · " + why, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        table.cell(gsvPilotT, 0, 3 + o, f_gsvWrap("PLAN · " + gsvPlan, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilot == "Standard"
            table.cell(gsvPilotT, 0, 4 + o, f_gsvWrap("LIMITS · " + limits, 60), text_color = gsvMuted, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilotCorner == "Bottom left" or gsvPilotCorner == "Bottom right"
            table.cell(gsvPilotT, 0, 6, "", height = 5, bgcolor = na)

// ── chart layers: bounded handles, never further back than the history buffer (max_bars_back = 2000)
var array<box> gsvHB = array.new<box>()
var array<line> gsvHL = array.new<line>()
var array<label> gsvHT = array.new<label>()
var array<box> gsvLB = array.new<box>()
var array<line> gsvLL = array.new<line>()
var array<label> gsvLT = array.new<label>()
f_gsvClear(array<box> bs, array<line> ls, array<label> ts) =>
    for b in bs
        box.delete(b)
    for l in ls
        line.delete(l)
    for x in ts
        label.delete(x)
    array.clear(bs)
    array.clear(ls)
    array.clear(ts)
int gsvTfMin = math.max(1, int(timeframe.in_seconds() / 60))

// Short names on the chart; the desk and the tooltips use the full ones.
f_gsvShort(string name) =>
    name == "Asia high" ? "ASIA H" : name == "Asia low" ? "ASIA L" : name == "London high" ? "LDN H" : name == "London low" ? "LDN L" : name
// A pool's tag sits where its line ends: the candle that took it, or just right of the last candle while fresh.
f_gsvTagX(GSPool p) =>
    na(p.takenBar) ? bar_index + 3 : math.max(p.takenBar, math.max(p.bornBar, bar_index - 1500))
// A tag is about eight candles wide: tags within 12 candles and 1 ATR of each other would print over each other,
// so the upper one moves above its line and the lower one below it (a middle one of three stays on its line).
f_gsvPlace(int pi, int oldestDay, array<int> tagDays) =>
    GSPool p = array.get(gsPoolLog, pi)
    int x = f_gsvTagX(p)
    float near = nz(gsAtr, 0)
    bool above = false
    bool below = false
    for [qi, q] in gsPoolLog
        if qi != pi and q.day >= oldestDay and q.bornBar >= bar_index - 1500 and array.includes(tagDays, q.day) and math.abs(f_gsvTagX(q) - x) <= 12 and math.abs(q.price - p.price) <= near
            if q.price > p.price or (q.price == p.price and qi > pi)
                above := true
            else
                below := true
    below and not above ? label.style_label_lower_left : above and not below ? label.style_label_upper_left : label.style_label_left
f_gsvDrawPool(GSPool p, bool today, string place) =>
    int x1 = math.max(p.bornBar, bar_index - 1500)
    int x2 = f_gsvTagX(p)
    bool live = today and p.fresh
    color c = live ? gsvGold : gsvGrey
    array.push(gsvHL, line.new(x1, p.price, x2, p.price, color = color.new(c, live ? 10 : today ? 35 : 60), style = live ? line.style_solid : line.style_dotted, width = live ? 2 : 1))
    if today
        array.push(gsvHT, label.new(x2, p.price, f_gsvShort(p.name) + (p.fresh ? " " + f_gsvPx(p.price) : " ✕"), style = place, color = color.new(#10151F, 20), textcolor = color.new(c, p.fresh ? 0 : 20), size = size.small, tooltip = p.name + " " + f_gsvPx(p.price) + (p.fresh ? " · fresh: no bar has traded through it yet" : " · taken")))

f_gsvDrawSession(GSPool hi, GSPool lo, int mins, color c, string name) =>
    int x2 = hi.bornBar - 1
    int x1 = math.max(x2 - int(mins / gsvTfMin) + 1, bar_index - 1500)
    if x2 > x1
        array.push(gsvHB, box.new(x1, hi.price, x2, lo.price, border_color = color.new(c, 70), bgcolor = color.new(c, 92), text = name, text_color = color.new(c, 30), text_size = size.small, text_halign = text.align_left, text_valign = text.align_top))

f_gsvDrawPlan(GSPlan pl, bool isOpen) =>
    int xa = math.max(pl.entryBar, bar_index - 1500)
    int xb = isOpen ? bar_index + 2 : math.max(pl.exitBar, xa)
    int xg = math.max(pl.gapBar - 2, bar_index - 1500)
    if not na(pl.gapB) and pl.gapBar >= bar_index - 1500
        array.push(gsvHB, box.new(xg, pl.gapT, math.max(pl.flipBar, xg), pl.gapB, border_color = color.new(gsvGold, 55), bgcolor = color.new(gsvGold, 85), text = "FLIP", text_color = color.new(gsvGold, 20), text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center))
    if pl.sweepBar >= bar_index - 1500
        array.push(gsvHT, label.new(pl.sweepBar, pl.extreme, "SWEPT " + pl.pool, style = pl.dir == -1 ? label.style_label_down : label.style_label_up, color = color.new(gsvGold, 20), textcolor = #0C131D, size = size.small, tooltip = "Raid of the " + pl.pool + " and a close back inside · extreme " + f_gsvPx(pl.extreme)))
    if gsvZones
        array.push(gsvHB, box.new(xa, math.max(pl.entry, pl.stop), xb, math.min(pl.entry, pl.stop), border_color = color.new(gsvDown, 100), bgcolor = color.new(gsvDown, isOpen ? 84 : 93)))
        array.push(gsvHB, box.new(xa, math.max(pl.entry, pl.target), xb, math.min(pl.entry, pl.target), border_color = color.new(gsvUp, 100), bgcolor = color.new(gsvUp, isOpen ? 84 : 93)))
    array.push(gsvHL, line.new(xa, pl.tp1, xb, pl.tp1, color = color.new(gsvUp, isOpen ? 35 : 65), style = line.style_dashed))
    array.push(gsvHL, line.new(xa, pl.entry, xb, pl.entry, color = color.new(gsvGold, isOpen ? 10 : 50)))
    if pl.entryBar >= bar_index - 1500
        array.push(gsvHT, label.new(pl.entryBar, pl.entry, (pl.dir == 1 ? "▲ LONG " : "▼ SHORT ") + f_gsvPx(pl.entry), style = pl.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(pl.dir == 1 ? gsvUp : gsvDown, 15), textcolor = #0C131D, size = size.small, tooltip = pl.how + " after the " + pl.pool + " sweep · stop " + f_gsvPx(pl.stop) + " · 1R " + f_gsvPx(pl.tp1) + " · target " + f_gsvPx(pl.target)))
    if not isOpen and pl.exitBar >= bar_index - 1500
        bool above = pl.exitPrice >= pl.entry
        color c = pl.netR > 0.05 ? gsvUp : pl.netR < -0.05 ? gsvDown : gsvGrey
        array.push(gsvHT, label.new(pl.exitBar, pl.exitPrice, f_gsvEndMark(pl.ending, pl.netR) + " " + f_gsvEnding(pl.ending) + " " + f_gsvR(pl.netR), style = above ? label.style_label_down : label.style_label_up, color = color.new(c, 15), textcolor = #0C131D, size = size.small, tooltip = "Exit " + f_gsvPx(pl.exitPrice) + " · " + f_gsvR(pl.grossR) + " before costs, " + f_gsvR(pl.netR) + " after the estimated cost"))

f_gsvDrawHistory() =>
    f_gsvClear(gsvHB, gsvHL, gsvHT)
    // The trading days to draw: the latest gsvDays distinct day ids in the pool log.
    int oldestDay = gsDay
    int seen = 0
    int lastDay = -1
    int i = array.size(gsPoolLog) - 1
    while i >= 0 and seen < gsvDays
        GSPool p = array.get(gsPoolLog, i)
        if p.day != lastDay
            seen += 1
            lastDay := p.day
            oldestDay := p.day
        i -= 1
    // Pool names are written for today and for every drawn day that had a plan.
    array<int> tagDays = array.from(gsDay)
    for pl in gsDone
        if pl.day >= oldestDay
            array.push(tagDays, pl.day)
    GSPool asiaHi = na
    GSPool lonHi = na
    for [pi, p] in gsPoolLog
        if p.day >= oldestDay and p.bornBar >= bar_index - 1500
            if gsvPools
                bool tagged = array.includes(tagDays, p.day)
                f_gsvDrawPool(p, tagged, tagged ? f_gsvPlace(pi, oldestDay, tagDays) : label.style_label_left)
            if gsvSessions and p.name == "Asia high"
                asiaHi := p
            if gsvSessions and p.name == "Asia low" and not na(asiaHi)
                f_gsvDrawSession(asiaHi, p, 240, gsvAsia, "ASIA")
            if gsvSessions and p.name == "London high"
                lonHi := p
            if gsvSessions and p.name == "London low" and not na(lonHi)
                f_gsvDrawSession(lonHi, p, 180, gsvGold, "LONDON")
    int j = array.size(gsDone) - 1
    while j >= 0
        GSPlan pl = array.get(gsDone, j)
        if pl.day < oldestDay or bar_index - pl.entryBar >= 1500
            break
        f_gsvDrawPlan(pl, false)
        j -= 1
    if gsP.dir != 0 and bar_index - gsP.entryBar < 1500
        f_gsvDrawPlan(gsP, true)
    if gsvMisses
        int k = array.size(gsEvBar) - 1
        int marks = 0
        while k >= 0 and marks < 40
            int eb = array.get(gsEvBar, k)
            if bar_index - eb >= 1500
                break
            string w = array.get(gsEvWhat, k)
            if w == "HELD" or w == "NO GAP" or w == "SWEEP FAILED" or w == "NO FLIP" or w == "NO RETEST" or str.startswith(w, "NO ENTRY")
                array.push(gsvHT, label.new(eb, array.get(gsEvPx, k), "◌", style = label.style_label_center, color = color.new(#000000, 100), textcolor = color.new(gsvGrey, 10), size = size.tiny, tooltip = str.lower(w) + " · the setup ended without a plan"))
                marks += 1
            k -= 1

f_gsvDrawLive() =>
    f_gsvClear(gsvLB, gsvLL, gsvLT)
    if gsP.dir == 0 and gsS.state != 0
        int x1 = math.max(gsS.raidBar, bar_index - 1500)
        color c = gsS.dir == -1 ? gsvDown : gsvUp
        array.push(gsvLL, line.new(x1, gsS.extreme, bar_index + 3, gsS.extreme, color = color.new(c, 20), width = 2))
        array.push(gsvLT, label.new(bar_index + 3, gsS.extreme, (gsS.state == 1 ? "RAID " : "SWEPT ") + gsS.pool + " · extreme " + f_gsvPx(gsS.extreme), style = label.style_label_left, color = color.new(#10151F, 10), textcolor = c, size = size.small))
        if gsS.state >= 2 and not na(gsS.gapB)
            int xg = math.max(gsS.gapBar - 2, bar_index - 1500)
            array.push(gsvLB, box.new(xg, gsS.gapT, bar_index + 3, gsS.gapB, border_color = color.new(gsvGold, 20), border_style = line.style_dashed, bgcolor = color.new(gsvGold, 85), text = gsS.state == 2 ? "GAP TO FLIP" : "FLIPPED · RETEST", text_color = color.new(gsvGold, 10), text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center))
        if gsS.state == 3
            array.push(gsvLL, line.new(x1, gsS.stop, bar_index + 3, gsS.stop, color = color.new(gsvDown, 30), style = line.style_dashed))

// Draw on the last historical bar and on every realtime execution. A strategy never runs on the forming bar, so
// barstate.islast alone would leave the Strategy Lab blank while the market is open.
if barstate.islastconfirmedhistory or barstate.isrealtime
    if barstate.islastconfirmedhistory or barstate.isconfirmed
        f_gsvDrawHistory()
    f_gsvDrawLive()
    f_gsvDrawDesk()
    f_gsvDrawPilot()

barcolor(gsvTint and gsP.dir != 0 ? color.new(gsP.dir == 1 ? gsvUp : gsvDown, 15) : na, title = "Plan tint")
The Strategy Lab · the complete source · gold-sweep-desk-strategy.pine
//@version=6
// Gold Sweep Desk 1.1.0 · MIT · © 2026 ProEA Lab · built for r/pinescript
// Gold liquidity as a desk: the prior-day, Asia and London highs and lows, their sweeps, the flip and the plan, in words.
// Every clock is New York time; bars while spot gold is closed are skipped, so spot and 24/7 feeds agree.
// Decisions happen on closed bars. The record is this chart's history after an estimated cost, not a forecast.
// Rules: docs/engine-contract.md in the release · https://proea.app/en/free/gold-sweep-desk
// License: MIT. Share it freely; keep this header.
strategy("Gold Sweep Desk · Strategy Lab", "GS Lab", overlay = true, behind_chart = false, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 2000, initial_capital = 100000, currency = currency.USD, pyramiding = 0, margin_long = 100, margin_short = 100, commission_type = strategy.commission.cash_per_contract, commission_value = 0.20, slippage = 0, process_orders_on_close = true, calc_on_every_tick = false)
const string GS_VERSION = "1.1.0"
const bool GS_IS_STRATEGY = true

// ═══ ENGINE · core.pinefrag ═════════════════════════════════════════════════════════════════════════════════════
// The engine decides; the renderer (visuals.pinefrag) only reads. Every engine name starts with "gs" and a
// capital letter; tests/display.test.mjs refuses any renderer line that assigns one. The rules are written in
// docs/engine-contract.md; tests/reference/engine.mjs is the independent reference.
//
//   ① SWEEP  a bar in an entry window trades through a fresh pool (prior day, Asia or London high/low) and a close
//            comes back inside it within a few bars.
//   ② FLIP   a close goes through the nearest opposing fair value gap: the gap inverts.
//   ③ PLAN   entry at that close (or at a later close back inside the gap); stop beyond the sweep's extreme; target
//            in R; flat at the time exit. Stop first when a bar reaches both.
// Every clock is New York time by the bar's open. Bars while spot gold is closed (Friday 17:00 → Sunday 18:00 and
// the daily 17:00–18:00 break) are skipped entirely, so a 24/7 feed and a spot feed give the same bar stream.
// Decisions happen on closed bars only. A plan's geometry is frozen at entry.

string gsG1 = "01 · Pools and windows"
bool gsPoolPd = input.bool(true, "Prior day high / low", group = gsG1, tooltip = "The previous trading day's high and low. Gold's day runs 18:00 → 17:00 New York.")
bool gsPoolAsia = input.bool(true, "Asia high / low · 20:00–24:00", group = gsG1, tooltip = "The range of the bars opening 20:00–24:00 New York; a pool from the first bar after midnight.")
bool gsPoolLon = input.bool(true, "London high / low · 02:00–05:00", group = gsG1, tooltip = "The range of the bars opening 02:00–05:00 New York; a pool from the first bar after 05:00.")
string gsWindows = input.string("London + New York", "Entry windows", options = ["London + New York", "Continuous 01:00–11:00"], group = gsG1, tooltip = "A setup can only start on a bar that opens inside a window (New York time). The pools are used up by any bar that trades through them, window or not.")
bool gsWinLon = input.bool(true, "  London 02:00–05:00", group = gsG1)
bool gsWinNy = input.bool(true, "  New York 07:00–11:00", group = gsG1)

string gsG2 = "02 · Sweep and flip"
float gsDepth = input.float(0.05, "Sweep depth · ATR beyond the level", minval = 0, maxval = 5, step = 0.05, group = gsG2, tooltip = "The raid must reach at least this far beyond the outermost pool it takes, in ATR(14).")
int gsReclaim = input.int(3, "Close back inside within · bars", minval = 0, maxval = 20, group = gsG2, tooltip = "The close back inside the swept level may come on the raid bar or on this many bars after it. Otherwise price was accepted beyond the level (HELD).")
string gsFailOn = input.string("A close back beyond the level", "A sweep fails on", options = ["A close back beyond the level", "Any new extreme"], group = gsG2, tooltip = "While the flip is awaited: a close back beyond the swept level ends the setup, and a new wick only moves the extreme (and the stop) further; or any new extreme ends it.")
string gsConfirm = input.string("FVG flip", "Confirmation", options = ["FVG flip", "Close back only"], group = gsG2, tooltip = "FVG flip: a close through the nearest opposing fair value gap. Close back only: the sweep's own close is the trigger.")
int gsLookback = input.int(30, "Gap born within · bars before the sweep", minval = 3, maxval = 200, group = gsG2)
int gsFlipBars = input.int(12, "Flip within · bars after the sweep", minval = 0, maxval = 100, group = gsG2)
float gsMinLeg = input.float(1.0, "Leg from the extreme · at least ATR", minval = 0, maxval = 10, step = 0.1, group = gsG2, tooltip = "The flip close must be at least this far from the sweep's extreme.")

string gsG3 = "03 · Plan"
string gsEntryMode = input.string("Flip close", "Entry", options = ["Flip close", "Retest close"], group = gsG3, tooltip = "Flip close: the flip's close is the entry. Retest close: after the flip, the first close back inside the gap within the retest bars.")
int gsRetestBars = input.int(12, "  Retest within · bars", minval = 1, maxval = 100, group = gsG3)
float gsBuffer = input.float(0.1, "Stop buffer · ATR beyond the extreme", minval = 0, maxval = 3, step = 0.05, group = gsG3)
float gsMaxStop = input.float(6.0, "Skip if the stop is wider than · ATR", minval = 0.5, maxval = 50, step = 0.5, group = gsG3)
float gsTargetR = input.float(2.0, "Target · R", minval = 0.5, maxval = 20, step = 0.5, group = gsG3, tooltip = "A design choice, not a tested setting. 1R is the distance from the entry to the stop; the 1R milestone is recorded on the way.")
string gsExitAt = input.string("16:45", "Flat at · HH:MM New York", group = gsG3, tooltip = "An open plan closes at the close of the bar that ends at or after this time; no entry on or after it. Gold's daily break starts at 17:00.")
string gsSide = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = gsG3)

string gsG4 = "04 · Costs and record"
float gsCost = input.float(0.40, "Estimated round-trip cost · price", minval = 0, step = 0.05, group = gsG4, tooltip = "Spread plus slippage for entry and exit together, in the chart's price (0.40 = 40 cents an ounce on spot gold). Every R on the desk is after this estimate. Set your own broker's.")
int gsPctMin = input.int(20, "Show percentages from N closed plans", minval = 5, maxval = 500, group = gsG4, tooltip = "Below this many closed plans the desk shows counts only: a percentage of a few plans says little.")

// ── clock: New York, by the bar's open
string gsTz = "America/New_York"
float gsTick = syminfo.mintick
bool gsClosed = barstate.isconfirmed
f_gsClock(string hm) =>
    array<string> parts = str.split(hm, ":")
    int out = -1
    if array.size(parts) == 2
        float h = str.tonumber(array.get(parts, 0))
        float m = str.tonumber(array.get(parts, 1))
        if not na(h) and not na(m)
            out := int(h) * 60 + int(m)
    out
// A trading day starts at 18:00: its id is the New York date six hours later.
f_gsDayId(int t) =>
    int t6 = t + 6 * 3600000
    year(t6, "America/New_York") * 10000 + month(t6, "America/New_York") * 100 + dayofmonth(t6, "America/New_York")
int gsExitMin = f_gsClock(gsExitAt) < 0 ? 16 * 60 + 45 : f_gsClock(gsExitAt)
int gsDow = dayofweek(time, gsTz)
int gsHm = hour(time, gsTz) * 60 + minute(time, gsTz)
int gsDayNow = f_gsDayId(time)
int gsCloseHm = hour(time_close, gsTz) * 60 + minute(time_close, gsTz)
int gsCloseDay = f_gsDayId(time_close)
// dayofweek: 1 = Sunday … 7 = Saturday.
bool gsGoldShut = gsDow == dayofweek.saturday or (gsDow == dayofweek.sunday and gsHm < 1080) or (gsDow == dayofweek.friday and gsHm >= 1020) or (gsHm >= 1020 and gsHm < 1080)
bool gsInAsia = gsHm >= 1200
bool gsInLon = gsHm >= 120 and gsHm < 300
string gsWinNow = gsWindows == "Continuous 01:00–11:00" ? (gsHm >= 60 and gsHm < 660 ? (gsHm < 420 ? "London" : "New York") : "") : (gsWinLon and gsHm >= 120 and gsHm < 300) ? "London" : (gsWinNy and gsHm >= 420 and gsHm < 660) ? "New York" : ""
bool gsAvailable = chart.is_standard and timeframe.isintraday and timeframe.in_seconds() <= 3600
string gsUnavailable = gsAvailable ? "" : not chart.is_standard ? "standard candles needed" : "an intraday chart of 1 hour or less is needed"

// ── state
type GSPool
    string name
    int side
    float price
    bool fresh = true
    int bornBar = na
    int takenBar = na
    int day = 0

type GSGap
    int dir
    int born
    int bornBar
    float bottom
    float top
    bool killed = false

// The setup in progress: 0 idle · 1 raid · 2 swept · 3 flipped (waiting for the retest).
type GSSetup
    int state = 0
    int dir = 0
    string window = ""
    float ref = na
    string pool = ""
    string pools = ""
    int poolsN = 0
    float extreme = na
    float depthAtr = na
    int raidN = na
    int raidBar = na
    int sweepN = na
    int sweepBar = na
    float gapB = na
    float gapT = na
    int gapBar = na
    float gapAtr = na
    int flipN = na
    int flipBar = na
    float legAtr = na
    float atrFlip = na
    float stop = na

// The plan (dir 0 = none). Its geometry is frozen at entry; a copy goes to gsDone when it ends.
type GSPlan
    int dir = 0
    int day = 0
    string window = ""
    string pool = ""
    int poolsN = 0
    float depthAtr = na
    float legAtr = na
    float gapAtr = na
    float riskAtr = na
    int raidBar = na
    int sweepBar = na
    int flipBar = na
    int gapBar = na
    int entryN = na
    int entryBar = na
    int entryTime = na
    string how = ""
    float extreme = na
    float gapB = na
    float gapT = na
    float entry = na
    float stop = na
    float risk = na
    float tp1 = na
    float target = na
    bool tp1Hit = false
    int tp1Bar = na
    int exitBar = na
    int exitTime = na
    float exitPrice = na
    string ending = ""
    float grossR = na
    float netR = na

type GSStat
    int plans = 0
    int wins = 0
    float sumR = 0.0
    int nTarget = 0
    int nStop = 0
    int nTime = 0
    int nTp1 = 0
    int raids = 0
    int sweeps = 0
    int flips = 0
    int held = 0
    int noGap = 0
    int failed = 0
    int noFlip = 0
    int noEntry = 0

// What happened on this bar.
type GSEv
    int raid = 0
    int sweep = 0
    int flip = 0
    int entry = 0
    bool exit = false
    string why = ""
    float r = na
    int exitDir = 0
    bool tp1 = false
    string stopped = ""
    string pool = ""

var GSSetup gsS = GSSetup.new()
var GSPlan gsP = GSPlan.new()
var GSStat gsSt = GSStat.new()
var array<GSPool> gsPools = array.new<GSPool>()
var array<GSPool> gsPoolLog = array.new<GSPool>()
var array<GSGap> gsGaps = array.new<GSGap>()
var array<GSPlan> gsDone = array.new<GSPlan>()
var array<int> gsEvBar = array.new<int>()
var array<string> gsEvWhat = array.new<string>()
var array<float> gsEvPx = array.new<float>()
GSEv gsEv = GSEv.new()
var float gsAtr = na
var float gsTrSum = 0.0
var int gsTrN = 0
var float gsPrevC = na
var int gsN = -1
var int gsDay = 0
var float gsDayH = na
var float gsDayL = na
var float gsAsiaH = na
var float gsAsiaL = na
var bool gsAsiaDone = false
var float gsLonH = na
var float gsLonL = na
var bool gsLonDone = false
var int gsRestN = -1
var float gsH1 = na
var float gsL1 = na
var int gsD1 = 0
var float gsH2 = na
var float gsL2 = na
var int gsD2 = 0
// The day's story for the desk, in words (a field, so the helpers below can extend it).
type GSText
    string log = ""
var GSText gsTx = GSText.new()

f_gsRText(float v) =>
    (v < 0 ? "−" : "+") + str.tostring(math.abs(v), "0.0") + "R"
f_gsHhmm() =>
    str.tostring(hour(time, "America/New_York"), "00") + ":" + str.tostring(minute(time, "America/New_York"), "00")
f_gsLogAdd(string what) =>
    gsTx.log := gsTx.log + (gsTx.log == "" ? "" : " · ") + f_gsHhmm() + what
f_gsNote(string what) =>
    array.push(gsEvBar, bar_index)
    array.push(gsEvWhat, what)
    array.push(gsEvPx, close)
    if array.size(gsEvBar) > 200
        array.shift(gsEvBar)
        array.shift(gsEvWhat)
        array.shift(gsEvPx)
f_gsPoolNew(string name, int side, float price, int day) =>
    GSPool p = GSPool.new(name, side, price, true, bar_index, na, day)
    array.push(gsPools, p)
    array.push(gsPoolLog, p)
    if array.size(gsPoolLog) > 120
        array.shift(gsPoolLog)
f_gsReset(string why) =>
    // A setup ends without a plan: remembered for the desk and the alerts.
    gsEv.stopped := why
    gsS.state := 0
    gsS.dir := 0
    if why == "HELD"
        gsSt.held += 1
    else if why == "NO GAP"
        gsSt.noGap += 1
    else if why == "SWEEP FAILED"
        gsSt.failed += 1
    else if why == "NO FLIP" or why == "NO RETEST"
        gsSt.noFlip += 1
    else
        gsSt.noEntry += 1
    f_gsNote(why)
    f_gsLogAdd(" " + str.lower(why))

// Entry at this close, or a refusal. Returns true when a plan starts.
f_gsEnter(float px, string how) =>
    int d = gsS.dir
    float risk = (px - gsS.stop) * d
    bool ok = false
    if not (risk > 0)
        f_gsReset("NO ENTRY · stop not beyond entry")
    else if risk > gsMaxStop * gsS.atrFlip
        f_gsReset("NO ENTRY · stop too wide")
    else if not (gsCloseDay == gsDayNow and gsCloseHm < gsExitMin)
        f_gsReset("NO ENTRY · too late in the day")
    else
        ok := true
        gsP.dir := d
        gsP.day := gsDayNow
        gsP.window := gsS.window
        gsP.pool := gsS.pool
        gsP.poolsN := gsS.poolsN
        gsP.depthAtr := gsS.depthAtr
        gsP.legAtr := gsS.legAtr
        gsP.gapAtr := gsS.gapAtr
        gsP.riskAtr := risk / gsS.atrFlip
        gsP.raidBar := gsS.raidBar
        gsP.sweepBar := gsS.sweepBar
        gsP.flipBar := gsS.flipBar
        gsP.gapBar := gsS.gapBar
        gsP.entryN := gsN
        gsP.entryBar := bar_index
        gsP.entryTime := time
        gsP.how := how
        gsP.extreme := gsS.extreme
        gsP.gapB := gsS.gapB
        gsP.gapT := gsS.gapT
        gsP.entry := px
        gsP.stop := gsS.stop
        gsP.risk := risk
        gsP.tp1 := px + d * risk
        gsP.target := px + d * gsTargetR * risk
        gsP.tp1Hit := false
        gsP.tp1Bar := na
        gsP.exitBar := na
        gsP.exitTime := na
        gsP.exitPrice := na
        gsP.ending := ""
        gsP.grossR := na
        gsP.netR := na
        gsS.state := 4
        gsEv.entry := d
        f_gsNote(d == 1 ? "ENTRY LONG" : "ENTRY SHORT")
        f_gsLogAdd((d == 1 ? " ▲ long " : " ▼ short ") + str.tostring(px, format.mintick))
    ok

// The flip on this bar, if the close goes through the gap far enough from the extreme.
f_gsFlip() =>
    int d = gsS.dir
    bool through = d == -1 ? close < gsS.gapB : close > gsS.gapT
    float leg = (gsS.extreme - close) * -d
    bool done = false
    if through and leg >= gsMinLeg * gsAtr
        done := true
        gsS.flipN := gsN
        gsS.flipBar := bar_index
        gsS.legAtr := leg / gsAtr
        gsS.atrFlip := gsAtr
        gsS.stop := d == -1 ? math.ceil((gsS.extreme + gsBuffer * gsAtr) / gsTick - 1e-9) * gsTick : math.floor((gsS.extreme - gsBuffer * gsAtr) / gsTick + 1e-9) * gsTick
        gsEv.flip := d
        gsSt.flips += 1
        f_gsNote("FLIP")
        f_gsLogAdd(" flip")
        if gsEntryMode == "Flip close"
            f_gsEnter(close, "flip close")
        else
            gsS.state := 3
    done

// The sweep on this bar: pick the gap to invert (or trigger at once with "Close back only").
f_gsSweep() =>
    gsS.sweepN := gsN
    gsS.sweepBar := bar_index
    gsEv.sweep := gsS.dir
    gsEv.pool := gsS.pool
    gsSt.sweeps += 1
    f_gsNote("SWEEP " + gsS.pool)
    f_gsLogAdd(" swept " + gsS.pool)
    if gsConfirm == "Close back only"
        gsS.legAtr := (gsS.extreme - close) * -gsS.dir / gsAtr
        gsS.atrFlip := gsAtr
        gsS.flipN := gsN
        gsS.flipBar := bar_index
        gsS.stop := gsS.dir == -1 ? math.ceil((gsS.extreme + gsBuffer * gsAtr) / gsTick - 1e-9) * gsTick : math.floor((gsS.extreme - gsBuffer * gsAtr) / gsTick + 1e-9) * gsTick
        f_gsEnter(close, "reclaim close")
    else
        int want = -gsS.dir
        GSGap best = na
        for g in gsGaps
            if g.dir == want and not g.killed and g.born >= gsN - gsLookback and g.born < gsN
                if na(best)
                    best := g
                else if want == 1 ? g.bottom > best.bottom : g.top < best.top
                    best := g
        if na(best)
            f_gsReset("NO GAP")
        else
            gsS.gapB := best.bottom
            gsS.gapT := best.top
            gsS.gapBar := best.bornBar
            gsS.gapAtr := (best.top - best.bottom) / gsAtr
            gsS.state := 2
            f_gsFlip()
    // The branches above return different types; the function returns the setup state.
    gsS.state

f_gsEnd(float px, string why) =>
    float gross = (px - gsP.entry) * gsP.dir / gsP.risk
    float net = gross - gsCost / gsP.risk
    gsP.exitBar := bar_index
    gsP.exitTime := time
    gsP.exitPrice := px
    gsP.ending := why
    gsP.grossR := gross
    gsP.netR := net

if gsClosed and gsAvailable and not gsGoldShut
    gsN += 1
    // (A) ATR over session bars, this bar included.
    float tr = na(gsPrevC) ? high - low : math.max(high - low, math.abs(high - gsPrevC), math.abs(low - gsPrevC))
    if gsTrN < 14
        gsTrSum += tr
        gsTrN += 1
        if gsTrN == 14
            gsAtr := gsTrSum / 14
    else
        gsAtr := (tr + 13 * gsAtr) / 14
    gsPrevC := close

    // (B) A new trading day: yesterday's range becomes the prior-day pools; the session pools start again.
    if gsDayNow != gsDay
        for p in gsPools
            if p.fresh
                p.takenBar := bar_index - 1
        array.clear(gsPools)
        if gsDay != 0 and gsPoolPd and not na(gsDayH)
            f_gsPoolNew("PDH", 1, gsDayH, gsDayNow)
            f_gsPoolNew("PDL", -1, gsDayL, gsDayNow)
        gsDay := gsDayNow
        gsDayH := na
        gsDayL := na
        gsAsiaH := na
        gsAsiaL := na
        gsAsiaDone := false
        gsLonH := na
        gsLonL := na
        gsLonDone := false
        gsTx.log := ""
    if not gsAsiaDone and not na(gsAsiaH) and not gsInAsia
        gsAsiaDone := true
        if gsPoolAsia
            f_gsPoolNew("Asia high", 1, gsAsiaH, gsDayNow)
            f_gsPoolNew("Asia low", -1, gsAsiaL, gsDayNow)
    if not gsLonDone and not na(gsLonH) and not gsInLon
        gsLonDone := true
        if gsPoolLon
            f_gsPoolNew("London high", 1, gsLonH, gsDayNow)
            f_gsPoolNew("London low", -1, gsLonL, gsDayNow)

    // (C) The open plan: a later day, stop first, target, then the time exit.
    if gsP.dir != 0 and gsN > gsP.entryN
        int d = gsP.dir
        bool stopHit = d == 1 ? low <= gsP.stop : high >= gsP.stop
        bool targetHit = d == 1 ? high >= gsP.target : low <= gsP.target
        bool lateDay = gsDayNow != gsP.day
        if lateDay
            f_gsEnd(open, "TIME")
        else if stopHit
            f_gsEnd((d == 1 ? open <= gsP.stop : open >= gsP.stop) ? open : gsP.stop, "STOP")
        else if targetHit
            f_gsEnd(gsP.target, "TARGET")
        else if gsCloseDay == gsP.day and gsCloseHm >= gsExitMin
            f_gsEnd(close, "TIME")
        if not gsP.tp1Hit and not stopHit and not lateDay and (d == 1 ? high >= gsP.tp1 : low <= gsP.tp1)
            gsP.tp1Hit := true
            gsP.tp1Bar := bar_index
            gsEv.tp1 := true
        if gsP.ending != ""
            gsEv.exit := true
            gsEv.why := gsP.ending
            gsEv.r := gsP.netR
            gsEv.exitDir := gsP.dir
            gsSt.plans += 1
            gsSt.wins += gsP.netR > 0 ? 1 : 0
            gsSt.sumR += gsP.netR
            gsSt.nTarget += gsP.ending == "TARGET" ? 1 : 0
            gsSt.nStop += gsP.ending == "STOP" ? 1 : 0
            gsSt.nTime += gsP.ending == "TIME" ? 1 : 0
            gsSt.nTp1 += gsP.tp1Hit ? 1 : 0
            f_gsNote(gsP.ending)
            f_gsLogAdd(" " + str.lower(gsP.ending) + " " + f_gsRText(gsP.netR))
            array.push(gsDone, gsP.copy())
            if array.size(gsDone) > 300
                array.shift(gsDone)
            gsP.dir := 0
            gsRestN := gsN
            gsS.state := 0
            gsS.dir := 0

    // (D) Pools this bar trades through are used up, whatever else happens.
    int penHN = 0
    int penLN = 0
    float penHMax = na
    float penLMin = na
    string penHName = ""
    string penLName = ""
    string penHAll = ""
    string penLAll = ""
    for p in gsPools
        if p.fresh and p.side == 1 and high > p.price
            p.fresh := false
            p.takenBar := bar_index
            penHN += 1
            penHAll += (penHAll == "" ? "" : " + ") + p.name
            if na(penHMax) or p.price > penHMax
                penHMax := p.price
                penHName := p.name
        else if p.fresh and p.side == -1 and low < p.price
            p.fresh := false
            p.takenBar := bar_index
            penLN += 1
            penLAll += (penLAll == "" ? "" : " + ") + p.name
            if na(penLMin) or p.price < penLMin
                penLMin := p.price
                penLName := p.name

    // (E) The setup: RAID → SWEPT → (FLIPPED →) plan.
    if gsP.dir == 0
        if gsS.state == 1
            int d = gsS.dir
            gsS.extreme := d == -1 ? math.max(gsS.extreme, high) : math.min(gsS.extreme, low)
            int moreN = d == -1 ? penHN : penLN
            if moreN > 0
                gsS.poolsN += moreN
                gsS.pools += " + " + (d == -1 ? penHAll : penLAll)
                float outer = d == -1 ? penHMax : penLMin
                if d == -1 ? outer > gsS.ref : outer < gsS.ref
                    gsS.ref := outer
                    gsS.pool := d == -1 ? penHName : penLName
            if d == -1 ? close < gsS.ref : close > gsS.ref
                f_gsSweep()
            else if gsN - gsS.raidN >= gsReclaim
                f_gsReset("HELD")
        else if gsS.state == 2
            int d = gsS.dir
            bool beyond = d == -1 ? high > gsS.extreme : low < gsS.extreme
            bool onClose = gsFailOn == "A close back beyond the level"
            if onClose and beyond
                gsS.extreme := d == -1 ? high : low
            if onClose ? (d == -1 ? close > gsS.ref : close < gsS.ref) : beyond
                f_gsReset("SWEEP FAILED")
            else if not f_gsFlip() and gsN - gsS.sweepN >= gsFlipBars
                f_gsReset("NO FLIP")
        else if gsS.state == 3
            int d = gsS.dir
            if d == -1 ? high >= gsS.stop : low <= gsS.stop
                f_gsReset("NO ENTRY · stop traded before the retest")
            else if close >= gsS.gapB and close <= gsS.gapT
                f_gsEnter(close, "retest close")
            else if gsN - gsS.flipN >= gsRetestBars
                f_gsReset("NO RETEST")
        else if gsS.state == 0 and gsN != gsRestN and not na(gsAtr) and gsTrN >= 14
            bool up = penHN > 0 and gsSide != "Long only"
            bool dn = penLN > 0 and gsSide != "Short only"
            if gsWinNow != "" and up != dn
                int d = up ? -1 : 1
                float outer = up ? penHMax : penLMin
                float depth = d == -1 ? high - outer : outer - low
                if depth >= gsDepth * gsAtr
                    gsS := GSSetup.new(1, d, gsWinNow, outer, up ? penHName : penLName, up ? penHAll : penLAll, up ? penHN : penLN, d == -1 ? high : low, depth / gsAtr, gsN, bar_index)
                    gsEv.raid := d
                    gsEv.pool := gsS.pool
                    gsSt.raids += 1
                    f_gsNote("RAID " + gsS.pool)
                    f_gsLogAdd(" raid " + gsS.pool)
                    if d == -1 ? close < gsS.ref : close > gsS.ref
                        f_gsSweep()

    // (F) Gaps: a close through a gap kills it; a gap completes on this bar (three bars of the same trading day).
    for g in gsGaps
        if not g.killed and g.born < gsN and (g.dir == 1 ? close < g.bottom : close > g.top)
            g.killed := true
    if gsN >= 2 and gsD2 == gsDayNow
        if low > gsH2
            array.push(gsGaps, GSGap.new(1, gsN, bar_index, gsH2, low, false))
        if high < gsL2
            array.push(gsGaps, GSGap.new(-1, gsN, bar_index, high, gsL2, false))
    while array.size(gsGaps) > 0 and array.first(gsGaps).born < gsN - 2 * gsLookback
        array.shift(gsGaps)
    gsH2 := gsH1
    gsL2 := gsL1
    gsD2 := gsD1
    gsH1 := high
    gsL1 := low
    gsD1 := gsDayNow

    // (G) Today's range and the session ranges include this bar.
    gsDayH := na(gsDayH) ? high : math.max(gsDayH, high)
    gsDayL := na(gsDayL) ? low : math.min(gsDayL, low)
    if gsInAsia and not gsAsiaDone
        gsAsiaH := na(gsAsiaH) ? high : math.max(gsAsiaH, high)
        gsAsiaL := na(gsAsiaL) ? low : math.min(gsAsiaL, low)
    if gsInLon and not gsLonDone
        gsLonH := na(gsLonH) ? high : math.max(gsLonH, high)
        gsLonL := na(gsLonL) ? low : math.min(gsLonL, low)

// ═══ STRATEGY LAB · strategy.pinefrag (strategy only) ═════════════════════════════════════════════════════════
// TradingView's broker emulator runs the desk's plans. Orders process on the bar close, like the desk's decisions:
//   entry  a market order at the entry close and, in the same calculation, one exit order with the frozen stop and
//          target, so the position is protected from the first bar after entry;
//   end    every plan end cancels pending orders first (cancel_all after close_all would cancel the close), then
//          closes anything still held at that close (the time exit, or an emulator fill that differed).
// The desk stays the reference. The emulator orders a bar's high and low its own way, so its fills can differ; that
// difference is what the Strategy Tester shows. A strategy does not run on the forming candle. Commission is set in
// Properties: cash per contract 0.20 a side = the desk's 0.40 round-trip estimate on spot gold (1 contract = 1 oz).

string gsG5 = "05 · Strategy Lab"
float gsRiskPct = input.float(1.0, "Risk per plan · % of equity", minval = 0.1, maxval = 10, step = 0.1, group = gsG5, tooltip = "The loss at the stop, as a share of equity, before costs.")
float gsExposurePct = input.float(90, "Largest position · % of equity", minval = 1, maxval = 1000, step = 1, group = gsG5, tooltip = "Caps a position's value. The script declares 100 % margin; 90 % leaves room for costs. Raise it only for leveraged trading, together with the margin settings.")

// Quantity: the smaller of the risk budget and the exposure cap, in the account currency (a symbol quoted in
// another currency is converted), rounded down to the symbol's contract step. Zero means no affordable size.
f_gsQty(float risk, float price) =>
    float conv = strategy.convert_to_account(1.0)
    float unitRisk = risk * syminfo.pointvalue * conv
    float unitNotional = math.abs(price) * syminfo.pointvalue * conv
    float step = syminfo.mincontract > 0 ? syminfo.mincontract : 1.0
    float equity = math.max(0, strategy.equity)
    float units = unitRisk > 0 and unitNotional > 0 ? math.min(equity * gsRiskPct / 100 / unitRisk, equity * gsExposurePct / 100 / unitNotional) : 0.0
    math.floor(units / step + 1e-9) * step

var int gsLabSkipped = 0

// Every plan end: cancel pending orders, then close what the emulator still holds.
if gsClosed and gsEv.exit
    strategy.cancel_all()
    if strategy.position_size != 0
        strategy.close_all(comment = "DESK " + gsEv.why)
// Entry and its protection in the same calculation. A position the emulator still holds is not doubled.
if gsClosed and gsEv.entry != 0
    float qty = f_gsQty(gsP.risk, gsP.entry)
    if strategy.position_size != 0
        gsLabSkipped += 1
    else if qty > 0
        string id = gsEv.entry == 1 ? "GS L" : "GS S"
        strategy.entry(id, gsEv.entry == 1 ? strategy.long : strategy.short, qty = qty, comment = (gsEv.entry == 1 ? "▲ " : "▼ ") + gsP.pool)
        strategy.exit(id + " exit", id, stop = gsP.stop, limit = gsP.target, comment_loss = "STOP", comment_profit = "TARGET")

// ═══ DISPLAY · visuals.pinefrag ═════════════════════════════════════════════════════════════════════════════════
// Reads engine state and draws it. It never assigns an engine variable, requests data, trades or alerts
// (tests/display.test.mjs enforces this), so a display change cannot change a plan or a result.
// Desk standard: state line · reason · ribbon · steps (✓ / ✗ / —, ● required) · plan in price and R · today ·
// record (counts first, % only from the threshold) · footer. Co-Pilot: NOW · WHY · PLAN · LIMITS.
// Layers: the desk and the live drawings follow every tick; the history is redrawn once per closed bar.

string gsvG = "06 · View"
string gsvTheme = input.string("Standard", "Theme", options = ["Standard", "Colorblind-safe"], group = gsvG)
string gsvDeskPos = input.string("Top right", "Desk position", options = ["Top right", "Top left", "Bottom right", "Bottom left", "Off"], group = gsvG)
string gsvRows = input.string("Compact", "Desk rows", options = ["Compact", "Full"], group = gsvG, tooltip = "Full adds every pool of the day, the last plan and the average result.")
string gsvPilot = input.string("Brief", "Co-Pilot", options = ["Standard", "Brief", "Off"], group = gsvG, tooltip = "The desk in plain words. Brief shows NOW and PLAN; Standard adds WHY and LIMITS.")
string gsvPilotPos = input.string("Bottom left", "Co-Pilot position", options = ["Bottom left", "Bottom right", "Top left", "Top right"], group = gsvG)
string gsvTextSize = input.string("Normal", "Text size", options = ["Small", "Normal", "Large"], group = gsvG)
int gsvDays = input.int(2, "Days drawn", minval = 1, maxval = 30, group = gsvG, tooltip = "Pools, sessions and plans of this many trading days (today included), within the last 1,500 bars.")
bool gsvPools = input.bool(true, "Liquidity pools", group = gsvG)
bool gsvSessions = input.bool(true, "Asia and London boxes", group = gsvG)
bool gsvZones = input.bool(true, "Risk / reward zones", group = gsvG)
bool gsvMisses = input.bool(true, "Mark setups that ended without a plan (◌)", group = gsvG)
bool gsvTint = input.bool(false, "Tint candles while a plan is open", group = gsvG)

// Colour carries meaning: teal = long / target, coral = short / stop, gold = liquidity and a setup in progress.
bool gsvCb = gsvTheme == "Colorblind-safe"
color gsvUp = gsvCb ? #56B4E9 : #26A69A
color gsvDown = gsvCb ? #E69F00 : #EF5350
color gsvGold = gsvCb ? #F0E442 : #F2B33D
color gsvAsia = #7E8CE0
color gsvInk = #E6E9F0
color gsvMuted = #8A93A6
color gsvGrey = #6B7280
color gsvPanel = color.new(#10151F, 6)
string gsvSz = gsvTextSize == "Small" ? size.tiny : gsvTextSize == "Large" ? size.normal : size.small
string gsvSzState = gsvTextSize == "Small" ? size.small : gsvTextSize == "Large" ? size.large : size.normal

f_gsvCorner(string p) =>
    p == "Top left" ? position.top_left : p == "Bottom right" ? position.bottom_right : p == "Bottom left" ? position.bottom_left : position.top_right
f_gsvPx(float v) =>
    na(v) ? "—" : str.tostring(v, format.mintick)
f_gsvR(float v) =>
    na(v) ? "—" : math.abs(v) < 0.05 ? "0.0R" : (v < 0 ? "−" : "+") + str.tostring(math.abs(v), "0.0") + "R"
f_gsvHm(int mins) =>
    str.tostring(int(mins / 60) % 24, "00") + ":" + str.tostring(mins % 60, "00")
f_gsvAt(int t) =>
    str.tostring(hour(t, "America/New_York"), "00") + ":" + str.tostring(minute(t, "America/New_York"), "00")
f_gsvLeft(int mins) =>
    mins <= 0 ? "closing" : (mins >= 60 ? str.tostring(int(mins / 60)) + " h " : "") + str.tostring(mins % 60) + " m left"
f_gsvMark(int s) =>
    s == 1 ? "✓ met" : s == 0 ? "✗ not met" : s == -1 ? "— no data" : s == -3 ? "— waiting" : "— off"
f_gsvTone(int s) =>
    s == 1 ? gsvUp : s == 0 ? gsvDown : gsvMuted
f_gsvEnding(string why) =>
    why == "TARGET" ? "TARGET" : why == "STOP" ? "STOP" : why == "TIME" ? "FLAT AT THE TIME EXIT" : why
f_gsvEndMark(string why, float r) =>
    why == "TARGET" ? "✓" : why == "STOP" ? "✗" : r > 0.05 ? "✓" : r < -0.05 ? "✗" : "◌"
f_gsvWrap(string txt, int columns) =>
    array<string> words = str.split(txt, " ")
    string output = ""
    int width = 0
    for word in words
        if width > 0 and width + str.length(word) + 1 > columns
            output += "\n"
            width := 0
        output += (width > 0 ? " " : "") + word
        width += str.length(word) + 1
    output
f_gsvSide(int d) =>
    d == 1 ? "long" : "short"

// ── windows, in New York minutes
bool gsvCont = gsWindows == "Continuous 01:00–11:00"
int gsvWinEnd = gsvCont ? 660 : gsWinNow == "London" ? 300 : 660
string gsvWinTxt = gsvCont ? "01:00–11:00" : gsWinNow == "London" ? "London 02:00–05:00" : "New York 07:00–11:00"
// The next window start after this minute of the day: later today, tomorrow, or Monday (no windows at the weekend).
f_gsvNextWin(int hm) =>
    int dow = dayofweek(time, "America/New_York")
    bool later = gsvCont ? hm >= 60 : not ((gsWinLon and hm < 120) or (gsWinNy and hm < 420))
    string day = dow == dayofweek.saturday or dow == dayofweek.sunday or (dow == dayofweek.friday and later) ? "Monday " : later ? "tomorrow " : ""
    string win = gsvCont ? "01:00" : (not later and gsWinLon and hm < 120) or (later and gsWinLon) ? "London 02:00" : gsWinNy ? "New York 07:00" : ""
    win == "" ? "none (both windows off)" : day + win

// ── what the desk shows (display variables; the engine's objects are only read)
string gsvState = ""
string gsvReason = ""
string gsvNow = ""
string gsvPlan = ""
color gsvTone = gsvMuted
string gsvRib = ""
int gsvDir = 0
string gsvNearUp = ""
string gsvNearDn = ""
int gsvFresh = 0
int gsvPoolsToday = 0
if barstate.islastconfirmedhistory or barstate.isrealtime
    // Nearest fresh pools above and below the close.
    float upPx = na
    float dnPx = na
    for p in gsPools
        gsvPoolsToday += 1
        if p.fresh
            gsvFresh += 1
            if p.price >= close and (na(upPx) or p.price < upPx)
                upPx := p.price
                gsvNearUp := p.name + " " + f_gsvPx(p.price) + " (+" + str.tostring(p.price - close, format.mintick) + ")"
            if p.price < close and (na(dnPx) or p.price > dnPx)
                dnPx := p.price
                gsvNearDn := p.name + " " + f_gsvPx(p.price) + " (−" + str.tostring(close - p.price, format.mintick) + ")"
    gsvDir := gsP.dir != 0 ? gsP.dir : gsS.state != 0 ? gsS.dir : 0
    string r1 = gsvFresh > 0 ? "✓" : "·"
    string r2 = "·"
    string r3 = "·"
    string r4 = "·"
    string r5 = "·"
    if not gsAvailable
        gsvState := "— UNAVAILABLE · " + timeframe.period + " chart"
        gsvReason := gsUnavailable == "standard candles needed" ? "Heikin Ashi, Renko and other synthetic candles move the closes. Use standard candles." : "Use an intraday chart of 1 hour or less (5 or 15 minutes on gold)."
        gsvNow := "This chart cannot run the rule. " + gsvReason
    else if gsTrN < 14
        gsvState := "○ WARMING UP"
        gsvReason := "The ATR needs 14 session bars."
        gsvNow := gsvReason
    else if gsP.dir != 0
        float openR = (close - gsP.entry) * gsP.dir / gsP.risk
        gsvState := "● IN TRADE · " + (gsP.dir == 1 ? "LONG" : "SHORT") + " · " + f_gsvR(openR) + (gsP.tp1Hit ? " · 1R reached" : "")
        gsvTone := gsP.dir == 1 ? gsvUp : gsvDown
        gsvReason := "Stop " + f_gsvPx(gsP.stop) + " · 1R " + f_gsvPx(gsP.tp1) + (gsP.tp1Hit ? " ✓" : "") + " · target " + f_gsvPx(gsP.target) + " · flat at " + gsExitAt
        gsvNow := "In a " + f_gsvSide(gsP.dir) + " from " + f_gsvPx(gsP.entry) + " after the " + gsP.pool + " sweep (" + gsP.how + "), " + str.tostring(bar_index - gsP.entryBar) + " bars ago, " + f_gsvR(openR) + " at this close before costs."
        gsvPlan := "Stop first on every closed bar: the stop " + f_gsvPx(gsP.stop) + " is beyond the sweep's extreme " + f_gsvPx(gsP.extreme) + ", where the idea is wrong. The target " + f_gsvPx(gsP.target) + " ends it; otherwise it is flat at " + gsExitAt + " New York."
        r2 := "✓"
        r3 := "✓"
        r4 := "✓"
        r5 := "▶"
    else if gsGoldShut
        gsvState := "◌ GOLD IS CLOSED"
        gsvReason := "Spot gold trades Sunday 18:00 → Friday 17:00 New York, with a break 17:00–18:00. These bars are skipped."
        gsvNow := gsvReason
    else if gsS.state == 1
        int left = math.max(0, gsReclaim - (gsN - gsS.raidN))
        gsvState := "● RAID · " + gsS.pool + " · " + (gsS.dir == -1 ? "above" : "below")
        gsvTone := gsvGold
        gsvReason := "A close back " + (gsS.dir == -1 ? "below " : "above ") + f_gsvPx(gsS.ref) + " makes it a sweep (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left); otherwise price was accepted beyond the level."
        gsvNow := "Price is raiding the " + gsS.pool + " (" + f_gsvPx(gsS.ref) + ") in the " + gsS.window + " window, extreme " + f_gsvPx(gsS.extreme) + ". " + gsvReason
        r2 := "▶"
    else if gsS.state == 2
        int left = math.max(0, gsFlipBars - (gsN - gsS.sweepN))
        float lvl = gsS.dir == -1 ? math.min(gsS.gapB, gsS.extreme - gsMinLeg * gsAtr) : math.max(gsS.gapT, gsS.extreme + gsMinLeg * gsAtr)
        gsvState := "● SWEPT · " + gsS.pool + " · waiting for the flip"
        gsvTone := gsvGold
        gsvReason := "A close " + (gsS.dir == -1 ? "below " : "above ") + f_gsvPx(lvl) + " flips the gap " + f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) + " (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left). " + (gsFailOn == "Any new extreme" ? "A new extreme ends it." : "A close back " + (gsS.dir == -1 ? "above " : "below ") + f_gsvPx(gsS.ref) + " ends it.")
        gsvNow := "The " + gsS.pool + " was swept (extreme " + f_gsvPx(gsS.extreme) + "). The desk now waits for a close through the nearest opposing gap, the flip that confirms the reversal."
        r2 := "✓"
        r3 := "▶"
    else if gsS.state == 3
        int left = math.max(0, gsRetestBars - (gsN - gsS.flipN))
        gsvState := "● FLIPPED · waiting for the retest"
        gsvTone := gsvGold
        gsvReason := "A close back inside " + f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) + " enters " + f_gsvSide(gsS.dir) + " (" + str.tostring(left) + (left == 1 ? " bar" : " bars") + " left); a touch of the stop " + f_gsvPx(gsS.stop) + " cancels."
        gsvNow := "The gap flipped after the " + gsS.pool + " sweep. Retest mode waits for a close back inside the flipped gap."
        r2 := "✓"
        r3 := "✓"
        r4 := "▶"
    else if gsWinNow != ""
        int left = gsvWinEnd - gsHm
        gsvState := "○ WATCHING · " + gsWinNow + " window · " + str.tostring(gsvFresh) + (gsvFresh == 1 ? " fresh pool" : " fresh pools")
        gsvTone := gsvGold
        gsvReason := gsvFresh == 0 ? "Every pool of today is used up. The next ones: Asia and London ranges, then tomorrow's prior-day levels." : "A raid of a fresh pool, then a close back inside it, starts a setup (" + f_gsvLeft(left) + ")."
        gsvNow := "Watching the " + gsWinNow + " window. " + (gsvNearUp != "" ? "Above: " + gsvNearUp + ". " : "") + (gsvNearDn != "" ? "Below: " + gsvNearDn + ". " : "") + (gsvFresh == 0 ? "No fresh pool is left today." : "")
    else
        gsvState := "◌ OUTSIDE THE WINDOWS · next " + f_gsvNextWin(gsHm)
        gsvReason := "Setups start only in " + (gsvCont ? "the 01:00–11:00 window" : (gsWinLon ? "London 02:00–05:00" : "") + (gsWinLon and gsWinNy ? " and " : "") + (gsWinNy ? "New York 07:00–11:00" : "")) + " (New York time). Pools still get used up now."
        gsvNow := "Outside the entry windows. " + (gsvNearUp != "" ? "Above: " + gsvNearUp + ". " : "") + (gsvNearDn != "" ? "Below: " + gsvNearDn + "." : "")
    gsvRib := "01 POOLS " + r1 + "  02 SWEEP " + r2 + "  03 FLIP " + r3 + "  04 PLAN " + r4 + "  05 RESULT " + r5
    if gsvPlan == ""
        gsvPlan := "Rule: sweep of the prior day, Asia or London high/low in " + (gsvCont ? "01:00–11:00" : "the London or New York window") + " → a close through the nearest opposing gap" + (gsEntryMode == "Retest close" ? " → a close back inside it" : "") + " → entry at that close, stop beyond the sweep's extreme, target " + str.tostring(gsTargetR, "0.0#") + "R, flat at " + gsExitAt + "."

// ── desk and Co-Pilot
var table gsvDesk = table.new(f_gsvCorner(gsvDeskPos), 2, 40, frame_width = 0, border_width = 0)
string gsvPilotCorner = gsvPilotPos != gsvDeskPos ? gsvPilotPos : gsvDeskPos == "Bottom left" ? "Bottom right" : "Bottom left"
var table gsvPilotT = table.new(f_gsvCorner(gsvPilotCorner), 1, 7, frame_width = 0, border_width = 0)
int gsvPOff = gsvPilotCorner == "Top left" ? 1 : 0
int gsvOff = gsvDeskPos == "Top left" ? 1 : 0
f_gsvRow(int r, string a, string b, color ca, color cb) =>
    table.cell(gsvDesk, 0, r + gsvOff, a, text_color = ca, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
    table.cell(gsvDesk, 1, r + gsvOff, b, text_color = cb, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
f_gsvWide(int r, string txt, color c, bool bold, string tip) =>
    table.cell(gsvDesk, 0, r + gsvOff, txt, text_color = c, bgcolor = gsvPanel, text_size = bold ? gsvSzState : gsvSz, text_halign = text.align_left, text_formatting = bold ? text.format_bold : text.format_none, tooltip = tip)
    table.merge_cells(gsvDesk, 0, r + gsvOff, 1, r + gsvOff)
// The rule is built for 5–15 minute candles: from 30 minutes a candle is a large part of a window and the 2R target has
// little room before 16:45; TradingView loads only a few days of 1–4 minute candles, too few for this chart's record.
int gsvTfSec = timeframe.in_seconds()
string gsvTfMins = str.tostring(gsvTfSec / 60, "#")
string gsvTfNote = gsvTfSec >= 1800 ? "TIMEFRAME · built for 5–15 minutes: on " + gsvTfMins + "-minute candles a plan has little room for its target before 16:45" : gsvTfSec < 300 ? "TIMEFRAME · built for 5–15 minutes: TradingView loads only a few days of " + gsvTfMins + "-minute candles, too few for a record" : ""
// One step row: the mark, a detail, and "● required" (every step of the sequence is required).
f_gsvStep(int r, string name, int s, string detail) =>
    f_gsvRow(r, name, f_gsvMark(s) + (detail == "" ? "" : " · " + detail) + "  ● required", gsvInk, f_gsvTone(s))

f_gsvDrawDesk() =>
    table.clear(gsvDesk, 0, 0, 1, 39)
    if gsvDeskPos != "Off"
        if gsvOff == 1
            table.cell(gsvDesk, 0, 0, "", height = 14, bgcolor = na)
            table.cell(gsvDesk, 1, 0, "", height = 14, bgcolor = na)
        bool full = gsvRows == "Full"
        bool inPlan = gsP.dir != 0
        int st = gsS.state
        // Step states: 1 met · 0 not met · -1 no data · -3 waiting.
        int sWin = inPlan or st > 0 ? 1 : gsWinNow != "" ? 1 : 0
        int sPool = inPlan or st > 0 ? 1 : gsvFresh > 0 ? 1 : 0
        int sSweep = inPlan or st >= 2 ? 1 : st == 1 ? -3 : -3
        int sGap = inPlan or st >= 2 ? 1 : -3
        int sFlip = inPlan or st == 3 ? 1 : -3
        int met = (sWin == 1 ? 1 : 0) + (sPool == 1 ? 1 : 0) + (sSweep == 1 ? 1 : 0) + (sGap == 1 ? 1 : 0) + (sFlip == 1 ? 1 : 0)
        f_gsvWide(0, "GOLD SWEEP DESK · " + syminfo.ticker + " · " + timeframe.period + (GS_IS_STRATEGY ? " · STRATEGY LAB" : ""), gsvMuted, false, "Gold Sweep Desk " + GS_VERSION + " · a paper reference decided on closed bars, New York time." + (GS_IS_STRATEGY ? " The Strategy Tester shows TradingView's fills of the same plans." : "") + " Fills at your broker can differ.")
        f_gsvWide(1, gsvState, gsvTone, true, "Pools → raid → sweep → flip → plan → result.")
        f_gsvWide(2, f_gsvWrap(gsvReason, 58), gsvInk, false, "")
        f_gsvWide(3, gsvRib, gsvMuted, false, "01 fresh pools · 02 a raid and a close back inside · 03 the gap flips · 04 the plan · 05 its result")
        f_gsvWide(4, "STEPS" + (gsvDir != 0 ? " · for a " + f_gsvSide(gsvDir) : "") + " · " + str.tostring(met) + " of 5 required met", gsvMuted, false, "")
        f_gsvStep(5, "Window", sWin, inPlan or st > 0 ? (inPlan ? gsP.window : gsS.window) : gsWinNow != "" ? gsvWinTxt + " · " + f_gsvLeft(gsvWinEnd - gsHm) : "closed · next " + f_gsvNextWin(gsHm))
        f_gsvStep(6, "Fresh pool", sPool, inPlan ? gsP.pool : st > 0 ? gsS.pool : str.tostring(gsvFresh) + " of " + str.tostring(gsvPoolsToday) + " today")
        f_gsvStep(7, "Sweep · close back inside", sSweep, inPlan ? gsP.pool + " · extreme " + f_gsvPx(gsP.extreme) : st >= 2 ? gsS.pool + " · extreme " + f_gsvPx(gsS.extreme) : st == 1 ? "raid under way" : "")
        f_gsvStep(8, "Opposing gap", sGap, inPlan ? f_gsvPx(gsP.gapB) + "–" + f_gsvPx(gsP.gapT) : st >= 2 ? f_gsvPx(gsS.gapB) + "–" + f_gsvPx(gsS.gapT) : "")
        f_gsvStep(9, "Flip close ≥ " + str.tostring(gsMinLeg, "0.0#") + " ATR from the extreme", sFlip, inPlan ? f_gsvAt(gsP.entryTime) : st == 3 ? "done" : "")
        int r = 10
        if gsvNearUp != "" or gsvNearDn != ""
            f_gsvRow(r, "Nearest fresh", (gsvNearUp != "" ? "▲ " + gsvNearUp : "") + (gsvNearUp != "" and gsvNearDn != "" ? "\n" : "") + (gsvNearDn != "" ? "▼ " + gsvNearDn : ""), gsvMuted, gsvGold)
            r += 1
        if full
            for p in gsPools
                if r < 24
                    f_gsvRow(r, "  " + p.name, f_gsvPx(p.price) + (p.fresh ? " · fresh" : " · taken"), gsvMuted, p.fresh ? gsvGold : gsvGrey)
                    r += 1
        if inPlan
            f_gsvWide(r, "PLAN · " + f_gsvSide(gsP.dir) + " · " + gsP.how + " · frozen at entry", gsvMuted, false, "Frozen at entry.")
            f_gsvRow(r + 1, "Entry", f_gsvPx(gsP.entry) + " · " + str.tostring(bar_index - gsP.entryBar) + " bars ago", gsvMuted, gsvGold)
            f_gsvRow(r + 2, "Stop · 1R", f_gsvPx(gsP.stop) + " · " + str.tostring(math.abs(close - gsP.stop), format.mintick) + " away", gsvMuted, gsvDown)
            f_gsvRow(r + 3, "1R", f_gsvPx(gsP.tp1) + (gsP.tp1Hit ? " ✓" : " · " + str.tostring(math.abs(gsP.tp1 - close), format.mintick) + " away"), gsvMuted, gsvUp)
            f_gsvRow(r + 4, "Target · " + str.tostring(gsTargetR, "0.0#") + "R", f_gsvPx(gsP.target) + " · " + str.tostring(math.abs(gsP.target - close), format.mintick) + " away", gsvMuted, gsvUp)
            r += 5
        else
            f_gsvWide(r, f_gsvWrap("PLAN · " + gsvPlan, 58), gsvMuted, false, "")
            r += 1
        if gsTx.log != ""
            f_gsvWide(r, f_gsvWrap("TODAY " + gsTx.log, 58), gsvInk, false, "New York times.")
            r += 1
        string counts = "THIS CHART " + str.tostring(gsSt.raids) + " raids · " + str.tostring(gsSt.sweeps) + " sweeps · " + str.tostring(gsSt.flips) + " flips · " + (gsSt.plans == 0 ? "no closed plan yet" : str.tostring(gsSt.plans) + " plans: target " + str.tostring(gsSt.nTarget) + " · stop " + str.tostring(gsSt.nStop) + " · time " + str.tostring(gsSt.nTime) + " · reached 1R " + str.tostring(gsSt.nTp1))
        f_gsvWide(r, f_gsvWrap(counts, 58), gsvInk, false, "Counts of this chart's history. History on this chart, not a forecast.")
        r += 1
        if full
            string avgTxt = gsSt.plans == 0 ? "Average: needs a closed plan" : "Average " + f_gsvR(gsSt.sumR / gsSt.plans) + " over " + str.tostring(gsSt.plans) + " plans after an estimated " + str.tostring(gsCost, "0.00") + " round trip" + (gsSt.plans >= gsPctMin ? " · " + str.tostring(math.round(100.0 * gsSt.wins / gsSt.plans)) + "% closed above 0R" : "")
            f_gsvWide(r, f_gsvWrap(avgTxt, 58), gsvMuted, false, "Net R after the estimated cost in 04. A percentage appears only from " + str.tostring(gsPctMin) + " closed plans.")
            string ended = "Ended before a plan: held " + str.tostring(gsSt.held) + " · no gap " + str.tostring(gsSt.noGap) + " · sweep failed " + str.tostring(gsSt.failed) + " · no flip " + str.tostring(gsSt.noFlip) + " · refused " + str.tostring(gsSt.noEntry)
            f_gsvWide(r + 1, f_gsvWrap(ended, 58), gsvMuted, false, "")
            r += 2
        if gsvTfNote != ""
            f_gsvWide(r, f_gsvWrap(gsvTfNote, 58), gsvGold, false, "The rule's windows and its 16:45 exit are set in minutes of the New York day.")
            r += 1
        f_gsvWide(r, "Rules, not odds · history, not a forecast · ProEA Lab", gsvMuted, false, "")

f_gsvDrawPilot() =>
    table.clear(gsvPilotT, 0, 0, 0, 6)
    if gsvPilot != "Off"
        string why = "Above an old high and below an old low sit other traders' stops. A raid through such a level that closes back inside took that liquidity without accepting the new price. The flip, a close through the nearest opposing gap, is the sign that the move has turned; the sweep's extreme is where the idea is wrong."
        string limits = "A rule, not a forecast. The pools are price levels, not an order book. Results are this chart's history after an estimated " + str.tostring(gsCost, "0.00") + " round trip; fills at your broker can differ."
        int o = gsvPOff
        if o == 1
            table.cell(gsvPilotT, 0, 0, "", height = 14, bgcolor = na)
        table.cell(gsvPilotT, 0, 0 + o, "CO-PILOT", text_color = gsvMuted, bgcolor = gsvPanel, text_size = size.tiny, text_halign = text.align_left)
        table.cell(gsvPilotT, 0, 1 + o, f_gsvWrap("NOW · " + gsvNow, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilot == "Standard"
            table.cell(gsvPilotT, 0, 2 + o, f_gsvWrap("WHY · " + why, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        table.cell(gsvPilotT, 0, 3 + o, f_gsvWrap("PLAN · " + gsvPlan, 60), text_color = gsvInk, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilot == "Standard"
            table.cell(gsvPilotT, 0, 4 + o, f_gsvWrap("LIMITS · " + limits, 60), text_color = gsvMuted, bgcolor = gsvPanel, text_size = gsvSz, text_halign = text.align_left)
        if gsvPilotCorner == "Bottom left" or gsvPilotCorner == "Bottom right"
            table.cell(gsvPilotT, 0, 6, "", height = 5, bgcolor = na)

// ── chart layers: bounded handles, never further back than the history buffer (max_bars_back = 2000)
var array<box> gsvHB = array.new<box>()
var array<line> gsvHL = array.new<line>()
var array<label> gsvHT = array.new<label>()
var array<box> gsvLB = array.new<box>()
var array<line> gsvLL = array.new<line>()
var array<label> gsvLT = array.new<label>()
f_gsvClear(array<box> bs, array<line> ls, array<label> ts) =>
    for b in bs
        box.delete(b)
    for l in ls
        line.delete(l)
    for x in ts
        label.delete(x)
    array.clear(bs)
    array.clear(ls)
    array.clear(ts)
int gsvTfMin = math.max(1, int(timeframe.in_seconds() / 60))

// Short names on the chart; the desk and the tooltips use the full ones.
f_gsvShort(string name) =>
    name == "Asia high" ? "ASIA H" : name == "Asia low" ? "ASIA L" : name == "London high" ? "LDN H" : name == "London low" ? "LDN L" : name
// A pool's tag sits where its line ends: the candle that took it, or just right of the last candle while fresh.
f_gsvTagX(GSPool p) =>
    na(p.takenBar) ? bar_index + 3 : math.max(p.takenBar, math.max(p.bornBar, bar_index - 1500))
// A tag is about eight candles wide: tags within 12 candles and 1 ATR of each other would print over each other,
// so the upper one moves above its line and the lower one below it (a middle one of three stays on its line).
f_gsvPlace(int pi, int oldestDay, array<int> tagDays) =>
    GSPool p = array.get(gsPoolLog, pi)
    int x = f_gsvTagX(p)
    float near = nz(gsAtr, 0)
    bool above = false
    bool below = false
    for [qi, q] in gsPoolLog
        if qi != pi and q.day >= oldestDay and q.bornBar >= bar_index - 1500 and array.includes(tagDays, q.day) and math.abs(f_gsvTagX(q) - x) <= 12 and math.abs(q.price - p.price) <= near
            if q.price > p.price or (q.price == p.price and qi > pi)
                above := true
            else
                below := true
    below and not above ? label.style_label_lower_left : above and not below ? label.style_label_upper_left : label.style_label_left
f_gsvDrawPool(GSPool p, bool today, string place) =>
    int x1 = math.max(p.bornBar, bar_index - 1500)
    int x2 = f_gsvTagX(p)
    bool live = today and p.fresh
    color c = live ? gsvGold : gsvGrey
    array.push(gsvHL, line.new(x1, p.price, x2, p.price, color = color.new(c, live ? 10 : today ? 35 : 60), style = live ? line.style_solid : line.style_dotted, width = live ? 2 : 1))
    if today
        array.push(gsvHT, label.new(x2, p.price, f_gsvShort(p.name) + (p.fresh ? " " + f_gsvPx(p.price) : " ✕"), style = place, color = color.new(#10151F, 20), textcolor = color.new(c, p.fresh ? 0 : 20), size = size.small, tooltip = p.name + " " + f_gsvPx(p.price) + (p.fresh ? " · fresh: no bar has traded through it yet" : " · taken")))

f_gsvDrawSession(GSPool hi, GSPool lo, int mins, color c, string name) =>
    int x2 = hi.bornBar - 1
    int x1 = math.max(x2 - int(mins / gsvTfMin) + 1, bar_index - 1500)
    if x2 > x1
        array.push(gsvHB, box.new(x1, hi.price, x2, lo.price, border_color = color.new(c, 70), bgcolor = color.new(c, 92), text = name, text_color = color.new(c, 30), text_size = size.small, text_halign = text.align_left, text_valign = text.align_top))

f_gsvDrawPlan(GSPlan pl, bool isOpen) =>
    int xa = math.max(pl.entryBar, bar_index - 1500)
    int xb = isOpen ? bar_index + 2 : math.max(pl.exitBar, xa)
    int xg = math.max(pl.gapBar - 2, bar_index - 1500)
    if not na(pl.gapB) and pl.gapBar >= bar_index - 1500
        array.push(gsvHB, box.new(xg, pl.gapT, math.max(pl.flipBar, xg), pl.gapB, border_color = color.new(gsvGold, 55), bgcolor = color.new(gsvGold, 85), text = "FLIP", text_color = color.new(gsvGold, 20), text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center))
    if pl.sweepBar >= bar_index - 1500
        array.push(gsvHT, label.new(pl.sweepBar, pl.extreme, "SWEPT " + pl.pool, style = pl.dir == -1 ? label.style_label_down : label.style_label_up, color = color.new(gsvGold, 20), textcolor = #0C131D, size = size.small, tooltip = "Raid of the " + pl.pool + " and a close back inside · extreme " + f_gsvPx(pl.extreme)))
    if gsvZones
        array.push(gsvHB, box.new(xa, math.max(pl.entry, pl.stop), xb, math.min(pl.entry, pl.stop), border_color = color.new(gsvDown, 100), bgcolor = color.new(gsvDown, isOpen ? 84 : 93)))
        array.push(gsvHB, box.new(xa, math.max(pl.entry, pl.target), xb, math.min(pl.entry, pl.target), border_color = color.new(gsvUp, 100), bgcolor = color.new(gsvUp, isOpen ? 84 : 93)))
    array.push(gsvHL, line.new(xa, pl.tp1, xb, pl.tp1, color = color.new(gsvUp, isOpen ? 35 : 65), style = line.style_dashed))
    array.push(gsvHL, line.new(xa, pl.entry, xb, pl.entry, color = color.new(gsvGold, isOpen ? 10 : 50)))
    if pl.entryBar >= bar_index - 1500
        array.push(gsvHT, label.new(pl.entryBar, pl.entry, (pl.dir == 1 ? "▲ LONG " : "▼ SHORT ") + f_gsvPx(pl.entry), style = pl.dir == 1 ? label.style_label_up : label.style_label_down, color = color.new(pl.dir == 1 ? gsvUp : gsvDown, 15), textcolor = #0C131D, size = size.small, tooltip = pl.how + " after the " + pl.pool + " sweep · stop " + f_gsvPx(pl.stop) + " · 1R " + f_gsvPx(pl.tp1) + " · target " + f_gsvPx(pl.target)))
    if not isOpen and pl.exitBar >= bar_index - 1500
        bool above = pl.exitPrice >= pl.entry
        color c = pl.netR > 0.05 ? gsvUp : pl.netR < -0.05 ? gsvDown : gsvGrey
        array.push(gsvHT, label.new(pl.exitBar, pl.exitPrice, f_gsvEndMark(pl.ending, pl.netR) + " " + f_gsvEnding(pl.ending) + " " + f_gsvR(pl.netR), style = above ? label.style_label_down : label.style_label_up, color = color.new(c, 15), textcolor = #0C131D, size = size.small, tooltip = "Exit " + f_gsvPx(pl.exitPrice) + " · " + f_gsvR(pl.grossR) + " before costs, " + f_gsvR(pl.netR) + " after the estimated cost"))

f_gsvDrawHistory() =>
    f_gsvClear(gsvHB, gsvHL, gsvHT)
    // The trading days to draw: the latest gsvDays distinct day ids in the pool log.
    int oldestDay = gsDay
    int seen = 0
    int lastDay = -1
    int i = array.size(gsPoolLog) - 1
    while i >= 0 and seen < gsvDays
        GSPool p = array.get(gsPoolLog, i)
        if p.day != lastDay
            seen += 1
            lastDay := p.day
            oldestDay := p.day
        i -= 1
    // Pool names are written for today and for every drawn day that had a plan.
    array<int> tagDays = array.from(gsDay)
    for pl in gsDone
        if pl.day >= oldestDay
            array.push(tagDays, pl.day)
    GSPool asiaHi = na
    GSPool lonHi = na
    for [pi, p] in gsPoolLog
        if p.day >= oldestDay and p.bornBar >= bar_index - 1500
            if gsvPools
                bool tagged = array.includes(tagDays, p.day)
                f_gsvDrawPool(p, tagged, tagged ? f_gsvPlace(pi, oldestDay, tagDays) : label.style_label_left)
            if gsvSessions and p.name == "Asia high"
                asiaHi := p
            if gsvSessions and p.name == "Asia low" and not na(asiaHi)
                f_gsvDrawSession(asiaHi, p, 240, gsvAsia, "ASIA")
            if gsvSessions and p.name == "London high"
                lonHi := p
            if gsvSessions and p.name == "London low" and not na(lonHi)
                f_gsvDrawSession(lonHi, p, 180, gsvGold, "LONDON")
    int j = array.size(gsDone) - 1
    while j >= 0
        GSPlan pl = array.get(gsDone, j)
        if pl.day < oldestDay or bar_index - pl.entryBar >= 1500
            break
        f_gsvDrawPlan(pl, false)
        j -= 1
    if gsP.dir != 0 and bar_index - gsP.entryBar < 1500
        f_gsvDrawPlan(gsP, true)
    if gsvMisses
        int k = array.size(gsEvBar) - 1
        int marks = 0
        while k >= 0 and marks < 40
            int eb = array.get(gsEvBar, k)
            if bar_index - eb >= 1500
                break
            string w = array.get(gsEvWhat, k)
            if w == "HELD" or w == "NO GAP" or w == "SWEEP FAILED" or w == "NO FLIP" or w == "NO RETEST" or str.startswith(w, "NO ENTRY")
                array.push(gsvHT, label.new(eb, array.get(gsEvPx, k), "◌", style = label.style_label_center, color = color.new(#000000, 100), textcolor = color.new(gsvGrey, 10), size = size.tiny, tooltip = str.lower(w) + " · the setup ended without a plan"))
                marks += 1
            k -= 1

f_gsvDrawLive() =>
    f_gsvClear(gsvLB, gsvLL, gsvLT)
    if gsP.dir == 0 and gsS.state != 0
        int x1 = math.max(gsS.raidBar, bar_index - 1500)
        color c = gsS.dir == -1 ? gsvDown : gsvUp
        array.push(gsvLL, line.new(x1, gsS.extreme, bar_index + 3, gsS.extreme, color = color.new(c, 20), width = 2))
        array.push(gsvLT, label.new(bar_index + 3, gsS.extreme, (gsS.state == 1 ? "RAID " : "SWEPT ") + gsS.pool + " · extreme " + f_gsvPx(gsS.extreme), style = label.style_label_left, color = color.new(#10151F, 10), textcolor = c, size = size.small))
        if gsS.state >= 2 and not na(gsS.gapB)
            int xg = math.max(gsS.gapBar - 2, bar_index - 1500)
            array.push(gsvLB, box.new(xg, gsS.gapT, bar_index + 3, gsS.gapB, border_color = color.new(gsvGold, 20), border_style = line.style_dashed, bgcolor = color.new(gsvGold, 85), text = gsS.state == 2 ? "GAP TO FLIP" : "FLIPPED · RETEST", text_color = color.new(gsvGold, 10), text_size = size.tiny, text_halign = text.align_right, text_valign = text.align_center))
        if gsS.state == 3
            array.push(gsvLL, line.new(x1, gsS.stop, bar_index + 3, gsS.stop, color = color.new(gsvDown, 30), style = line.style_dashed))

// Draw on the last historical bar and on every realtime execution. A strategy never runs on the forming bar, so
// barstate.islast alone would leave the Strategy Lab blank while the market is open.
if barstate.islastconfirmedhistory or barstate.isrealtime
    if barstate.islastconfirmedhistory or barstate.isconfirmed
        f_gsvDrawHistory()
    f_gsvDrawLive()
    f_gsvDrawDesk()
    f_gsvDrawPilot()

barcolor(gsvTint and gsP.dir != 0 ? color.new(gsP.dir == 1 ? gsvUp : gsvDown, 15) : na, title = "Plan tint")

04 Every rule, one question at a time.

What does each desk state mean?
○ WATCHING
Inside the London or New York window, with the number of fresh pools. A raid of one, then a close back inside it, starts a setup.
● RAID
A candle traded through a fresh pool. A close back inside within three candles makes it a sweep; otherwise price was accepted beyond the level.
● SWEPT
Waiting for the flip: the close that would flip the gap, the candles left, and the close that would end it.
● IN TRADE
The plan is live: its stop, the 1R line, its target and flat at 16:45, with where it stands.
◌ OUTSIDE THE WINDOWS
Between windows, with the next one. Pools still get used up.
◌ GOLD IS CLOSED
The weekend or the daily 17:00–18:00 break. Those candles are skipped.
— UNAVAILABLE
A chart the desk cannot read, such as daily candles.
Exactly how is the plan made?

Pools: yesterday's high and low (PDH, PDL), the Asia range (20:00–24:00 New York) and the London range (02:00–05:00). Each is fresh until a candle trades through it, window or not.

Sweep: in a window, a candle reaches at least 0.05 ATR beyond a fresh pool and a close comes back inside on that candle or within the three after it.

Flip: a close through the nearest opposing fair value gap born in the thirty candles before the sweep, at least one ATR from the sweep's extreme, within twelve candles. A new wick beyond the extreme moves the extreme; a close back beyond the swept level ends the setup.

Plan: the entry is the flip close; the stop sits 0.1 ATR beyond the sweep's extreme; the target is twice the risk. The plan is skipped if the stop is wider than six ATR, or if the entry comes at 16:45 or later. The levels are fixed when the plan is made.

Result: on every closed candle the stop is checked first, then the target; otherwise the plan closes at 16:45 New York, before gold's daily break.

Why New York time, and what is skipped?

Every time on the desk is New York time by the candle's open, whatever your chart shows. Gold's day runs 18:00 → 17:00. Candles while spot gold is closed (Friday 17:00 to Sunday 18:00, and the daily 17:00–18:00 break) are skipped entirely, so a spot chart and a 24-hour gold feed show the same setups.

What do the ◌ marks mean?

A setup that ended before a plan: held beyond the level (no close back inside in time), no opposing gap to flip, a failed sweep (a close back beyond the level before the flip), or no flip within twelve candles. The mark's tooltip names which.

How is the Strategy Lab different?

The Strategy Lab is a separate script for TradingView's Strategy Tester. It runs the same plans, with the stop and target placed as orders at the entry and a commission that matches the desk's cost estimate.

Can it send alerts?

Yes: the indicator's alerts carry each event as it happens, in Text or JSON. Alerts never place orders.

What did we measure before release?

Two studies on 2026 gold data, with every candidate and period written down before its result. Sixteen rule variants in all: none passed, and the one selected failed its check. So the defaults are design choices, not tested winners. The desk's own record on your chart counts that chart's history; it is not a forecast. Read what we measured, every candidate and period included.

What can change the reading?

Every decision reads closed candles. Changing the symbol, timeframe, feed or inputs recalculates the history. The desk sees chart candles, not news or your broker's fills, and fills at your broker can differ. It is research tooling, not financial advice.

04 · THE LOG

Every change, dated

What shipped and what changed in Gold 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.

  1. Gold Sweep Desk v1.1.0 Shipped

    Gold's daily liquidity, the sweep, the flip and the plan, in words.

  2. No setting earned a tested default Lab note Gold Sweep Desk

    Two studies on 2026 gold data, registered in advance: no setting earned a place as a tested default, so the defaults are design choices.

    • Every candidate and period written down before the test ran
    • The on-chart record is this chart's history, not a forecast
    • An original build of a widely taught idea, not any vendor's signals
BUILT WITH TRADERS

Something to change in Gold Sweep Desk?

A small friction or a big idea. Tell us what would make this tool work better for you.

What would help?

Describe your rules and what you want the tool to help you see.

Add chart details Optional

For a question about your idea or an update if it ships.

Guides & new tools Optional

Add an email above to choose this.

Show us on your chart Optional

Crop out any account details you don't want to share.

No account needed. Sent privately to the lab.
  1. We read it
  2. We choose what to build
  3. Shipped changes get a date

Requests for a new tool go on the same queue: see what is asked for and what is being built.

  • Sweep Desk

    Compare gold's session pools with a sweep-and-confirmation sequence on any market's reference levels.

  • Opening Range Desk

    Compare a raid of the day's pools with a break of the session's opening range.

Built by ProEA Lab. Compare all free indicators.