Supply & Demand Desk v1.0 · original open-source rules
Watch the area. Wait for the return.
See where a strong move began, follow price back to that area, then check whether an entry is ready.
Standard five-minute candles. Reference plans are not orders.

What is Supply & Demand Desk?
Supply & Demand Desk is a free TradingView indicator that follows a price zone from its formation to a return and qualifying entry. Designed for standard five-minute candles, it checks departure, pullback, liquidity and rejection before fixing a paper entry, stop and target. A separate strategy script supports broker-emulator testing.
What makes a supply or demand zone ready?
A zone begins with a directional departure, then needs enough movement, a qualifying pullback and a later closing break. A newly drawn area is not ready for entry; a premature touch or expired setup can invalidate it. Read the zone-formation rules
Does every return to a zone create an entry?
No. The visit must pass front liquidity, intersect the ready zone and produce a qualifying close. Risk geometry, an existing paper plan, conflicting conditions or the optional trend gate can still block an entry. Follow the entry sequence
Is the Strategy Lab the same as the indicator's paper plan?
No. The indicator records a plan from the confirming close, while Strategy Lab uses broker-emulator orders, sizing and trading costs. Fill timing and independent position rules can differ, so compare the two models before interpreting results. Compare the two execution models
Where did the move begin?
When price moves sharply away, mark its starting area. The tool watches a later return and checks a sequence before proposing an entry.
One candle holds five minutes.
The thick body joins the opening and closing prices. The thin wick reaches the high and low. This red candle closed lower than it opened.
What does this illustration assume?
Fictional prices, standard five-minute candles, supplied ATR(14) value 10 and tick 0.25. Default full-wick zones and strength filter; optional trend filter off. The move reaches 14 above the zone before pulling back, exceeding the required 8. This shows one zone, not a complete market calculation. No orders, costs or outcome are simulated here.
How does this help a trade?
The area gives you somewhere to watch. The entry checks tell you when the tool is ready; the stop defines where the plan fails. A target is a planned distance, not a promised result. Section 01 connects these parts.
Where did the idea come from?
This original automation subset draws on Mangoe’s public five-minute Liquidity Supply & Demand course. It is not his official indicator. The numerical thresholds, equality rules and accounting policies in the full rules are our explicit choices.
Supply mirrors demand. The zones and “liquidity” levels come from candle prices; they do not reveal resting orders or prove why traders bought or sold.
Would this candle complete the setup?
Follow the fictional candles into the area. Compare a waiting candle, an accepted entry and a losing outcome.
Fictional five-minute candles. The scale stays fixed as you edit the close. A historical base anchor does not mean the zone was known before its departure close.
Which rule is complete?
Wait for the first departure close beyond the base high.
- Pullback candles before break
- 0
- Earlier high or low broken
- Waiting
- Return into the zone
- Waiting
Change the confirming candle
The teaching model supplies ATR(14) value 10 and tick 0.25; trend filtering is off. It computes the shown sequence, excluding the breakout from the two earlier retracement bars. It does not reconstruct market data, size a position, include costs, or emulate broker fills.
Find the area, the waiting reason and the plan.
Read these three parts in this order.
- 1
The area to watch
The rectangle marks an earlier move’s starting area. A ready zone is worth watching; a visit means price has returned. Neither is an entry by itself.
- 2
Why the tool waits
The dashboard says which check is missing. A condition alone is not an accepted entry: another plan or a conflicting setup can still block it.
- 3
The entry, stop and target
Read all three together when a plan is accepted. The default target is four times the initial entry-to-stop distance: 4R. An ended plan is history, not a current position.

Both charts are unmodified TradingView captures of this release. Source and image identities.
Indicator first. Strategy Lab separately.
Complete Pine v6 source, under MIT.
- 1
Open Pine Editor
Use a standard five-minute chart with enough history for ATR14.
- 2
Paste, save, then Add to chart
Replace the editor contents with the full indicator source. Save it and choose Add to chart. Save the separate Lab as a different script for broker testing.
- 3
Check the waiting reason
If it says “ATR14 history unavailable” or “Waiting for ATR history”, load more chart history. Keep defaults initially, then test one change at a time.
The indicator
Explains zones, confirmed conditions and frozen reference plans.
Copy the code as it is. No coding needed.
View the full code
//@version=6
// Supply & Demand Desk 1.0.0 · MIT · Original public-method-inspired 5m subset.
// Not Mangoe's official script or private-method parity. See docs/engine-contract.md.
// Signal-close paper references differ from next-open Strategy Lab fills.
indicator("Supply & Demand Desk · Open Source", "SDD", overlay = true, behind_chart = false, max_boxes_count = 40, max_lines_count = 100, max_labels_count = 100, max_bars_back = 2000)
const string SDD_VERSION = "1.0.0"
const bool SD_IS_LAB = false
// Original closed-bar subset of a public discretionary method. Read docs/engine-contract.md.
string sdG = "01 · Setup rules"
float sdImpulse = input.float(0.8, "Minimum departure · ATR", minval = 0.1, maxval = 5, step = 0.1, group = sdG, tooltip = "Original strength threshold, measured from proximal edge to departure extreme. Birth ATR is frozen.", display = display.none)
int sdRetrMin = input.int(2, "Minimum retracement bars", minval = 2, maxval = 8, group = sdG, tooltip = "Observed retracement bars before the breakout bar; colors may mix.", display = display.none)
float sdNear = input.float(0.5, "Front liquidity · maximum setup fraction", minval = 0.1, maxval = 1, step = 0.05, group = sdG, tooltip = "Demand: (front low − proximal) / (originating high − proximal). Supply mirrors. Our explicit near-half formalization.", display = display.none)
bool sdAccuracy = input.bool(false, "FX accuracy drawing", group = sdG, tooltip = "Optional public FX drawing variant. Excludes a protruding proximal base wick. Default full-wick drawing applies to all symbols; no automatic asset classification.", display = display.none)
int sdRetrWait = input.int(20, "Retracement deadline · bars", minval = 3, maxval = 60, group = sdG, display = display.none)
int sdZoneAge = input.int(120, "Zone lifetime · bars", minval = 10, maxval = 500, group = sdG, display = display.none)
int sdVisitAge = input.int(3, "Entry visit deadline · bars", minval = 0, maxval = 12, group = sdG, tooltip = "Age0 is first touch. Default ages0..3 can confirm; age4 expires. Opposing-color wick rejection may wait for a later directional close.", display = display.none)
bool sdTrendGate = input.bool(false, "Require chart-local HH/HL or LH/LL", group = sdG, tooltip = "Optional original gate: last two confirmed two-left/two-right swing highs and lows. No HTF request. Neutral or incomplete structure blocks when enabled.", display = display.none)
string sdP = "02 · Paper reference"
float sdStopPad = input.float(0, "Stop pad · ATR (minimum one tick)", minval = 0, maxval = 1, step = 0.05, group = sdP, display = display.none)
float sdReward = input.float(4, "Target · R", minval = 1, maxval = 10, step = 0.5, group = sdP, tooltip = "4R follows the public 2026 five-minute course. Changing this is your variant. Rounded actual R may be slightly greater.", display = display.none)
int sdHold = input.int(48, "Maximum paper hold · bars", minval = 1, maxval = 288, group = sdP, tooltip = "Ordinary stop/target checks first, then close at the observed candle close. Broker Lab uses its own next-open market exit.", display = display.none)
float sdCash = input.float(100, "Cash risk budget · symbol currency", minval = 0.01, group = sdP, display = display.none)
float sdCap = input.float(90000, "Notional cap · symbol currency", minval = 1, group = sdP, display = display.none)
float sdFee = input.float(0.05, "Estimated commission · % per side", minval = 0, maxval = 2, step = 0.01, group = sdP, display = display.none)
int sdSlip = input.int(1, "Estimated slippage · ticks per side", minval = 0, maxval = 20, group = sdP, display = display.none)
bool sdJson = input.bool(true, "JSON alert events", group = "03 · Alerts", tooltip = "Create one alert using Any alert() function call. Reference events are not broker fills. Strategy Lab can emit these paper-reference events independently from its order-fill alerts.", display = display.none)
// === TYPES AND PURE HELPERS ===
type SdConfig
float impulse = 0.8
int retrMin = 2
float near = 0.5
int retrWait = 20
int zoneAge = 120
int visitAge = 3
float stopPad = 0
float reward = 4
int hold = 48
float cash = 100
float cap = 90000
float fee = 0.05
int slip = 1
type SdZone
int direction = 0
string id = ""
string state = "INVALID"
string reason = "Unavailable"
int baseBar = na
int baseTime = na
int createdBar = na
int createdTime = na
int phaseBar = na
int readyBar = na
int readyTime = na
int visitBar = na
int signalBar = na
float lower = na
float upper = na
float birthAtr = na
float impulseExtreme = na
float origin = na
float front = na
int retrBars = 0
float deepest = na
float previousHigh = na
float previousLow = na
float previousClose = na
type SdPlan
int direction = 0
string id = ""
string state = "EMPTY"
string reason = "No accepted reference"
int signalBar = na
int signalTime = na
int endBar = na
int endTime = na
int hold = 48
float entry = na
float stop = na
float target = na
float risk = na
float quantity = na
float pointValue = na
float tick = na
float fee = 0
int slip = 0
float netAtTarget = na
float exitPrice = na
float netR = na
bool ambiguous = false
float lower = na
float upper = na
float front = na
int baseTime = na
int createdTime = na
int readyTime = na
f_sdSign(float value) =>
int(nz(value / math.abs(value), 0))
f_sdCompare(float lhs, float rhs) =>
int result = na
if not na(lhs) and not na(rhs)
float difference = lhs - rhs
float noise = 4 * 2.220446049250313e-16 * math.max(math.abs(lhs), math.abs(rhs))
result := f_sdSign(math.abs(difference) - noise) <= 0 ? 0 : f_sdSign(difference)
result
f_sdBarValid(float o, float h, float l, float c) =>
not na(o) and not na(h) and not na(l) and not na(c) and f_sdSign(h - l) >= 0 and f_sdSign(h - math.max(o, c)) >= 0 and f_sdSign(math.min(o, c) - l) >= 0
f_sdUnits(float value) =>
float nearest = math.round(value)
float noise = math.min(1e-7, 8 * 2.220446049250313e-16 * math.max(1, math.abs(value)))
f_sdSign(math.abs(value - nearest) - noise) <= 0 ? nearest : value
f_sdTick(float value, float tick, bool up) =>
float units = f_sdUnits(value / tick)
(up ? math.ceil(units) : math.floor(units)) * tick
f_sdDistance(float firstPrice, float secondPrice, float tick) =>
float distance = math.abs(firstPrice - secondPrice)
float units = distance / tick
float nearest = math.round(units)
float noise = math.min(1e-7, 8 * 2.220446049250313e-16 * math.max(math.abs(firstPrice), math.abs(secondPrice)) / tick)
f_sdSign(math.abs(units - nearest) - noise) <= 0 ? nearest * tick : distance
f_sdQuantity(float entry, float distance, float cash, float cap, float pointValue, float increment) =>
float quantity = na
if not na(entry) and f_sdSign(distance) > 0 and f_sdSign(cash) > 0 and f_sdSign(cap) > 0 and f_sdSign(pointValue) > 0 and f_sdSign(increment) > 0
float byRisk = cash / (distance * pointValue)
float byCap = f_sdSign(math.abs(entry)) > 0 ? cap / (math.abs(entry) * pointValue) : byRisk
quantity := math.floor(f_sdUnits(math.min(byRisk, byCap) / increment)) * increment
quantity
f_sdNetR(SdPlan plan, float exitPrice) =>
float gross = plan.direction * (exitPrice - plan.entry) * plan.quantity * plan.pointValue
float fees = (math.abs(plan.entry) + math.abs(exitPrice)) * plan.quantity * plan.pointValue * plan.fee / 100
float slipCash = 2 * plan.slip * plan.tick * plan.quantity * plan.pointValue
(gross - fees - slipCash) / (plan.risk * plan.quantity * plan.pointValue)
f_sdTerminal(string state) =>
state == "INVALID" or state == "EXPIRED" or state == "USED"
f_sdNewZone(int direction, float bo, float bh, float bl, float bc, float o, float h, float l, float c, float atr, int baseBar, int baseTime, int nowBar, int nowTime, bool accuracy) =>
SdZone zone = SdZone.new()
bool packets = f_sdBarValid(bo, bh, bl, bc) and f_sdBarValid(o, h, l, c) and f_sdSign(atr) > 0
bool colors = direction == 1 ? f_sdCompare(bo, bc) > 0 and f_sdCompare(c, o) > 0 : f_sdCompare(bc, bo) > 0 and f_sdCompare(o, c) > 0
if packets and colors and (direction == 1 or direction == -1)
zone.direction := direction
zone.id := str.tostring(baseTime) + (direction == 1 ? ":D" : ":S")
zone.baseBar := baseBar
zone.baseTime := baseTime
zone.createdBar := nowBar
zone.createdTime := nowTime
zone.phaseBar := nowBar
zone.lower := direction == 1 ? math.min(bl, l) : accuracy and f_sdCompare(bl, l) < 0 ? bo : bl
zone.upper := direction == -1 ? math.max(bh, h) : accuracy and f_sdCompare(bh, h) > 0 ? bo : bh
zone.birthAtr := atr
zone.impulseExtreme := direction == 1 ? h : l
zone.previousHigh := h
zone.previousLow := l
zone.previousClose := c
bool outside = direction == 1 ? f_sdCompare(c, zone.upper) > 0 : f_sdCompare(c, zone.lower) < 0
if outside and f_sdCompare(zone.upper, zone.lower) > 0
zone.state := "DEPARTURE"
zone.reason := "Wait for front liquidity"
zone
f_sdStep(SdZone zone, float o, float h, float l, float c, int atBar, int atTime, SdConfig cfg) =>
bool signal = false
bool valid = f_sdBarValid(o, h, l, c)
if not f_sdTerminal(zone.state) and atBar > zone.createdBar
if zone.state == "SIGNAL"
zone.state := "USED"
zone.reason := "Condition already used"
else if atBar - zone.createdBar > cfg.zoneAge
zone.state := "EXPIRED"
zone.reason := "Zone lifetime reached"
else if not valid
zone.state := "INVALID"
zone.reason := "Malformed observed candle"
else
bool longSide = zone.direction == 1
float proximal = longSide ? zone.upper : zone.lower
bool adverseClose = longSide ? f_sdCompare(c, proximal) <= 0 : f_sdCompare(c, proximal) >= 0
bool touches = f_sdCompare(l, zone.upper) <= 0 and f_sdCompare(h, zone.lower) >= 0
bool beforeReady = zone.state == "DEPARTURE" or zone.state == "RETRACEMENT"
if adverseClose
zone.state := "INVALID"
zone.reason := "Close on/inside or through zone"
else if beforeReady and touches
zone.state := "INVALID"
zone.reason := "Zone touched before readiness"
else if zone.state == "DEPARTURE"
bool continues = longSide ? f_sdCompare(c, o) > 0 and f_sdCompare(c, zone.previousClose) > 0 : f_sdCompare(c, o) < 0 and f_sdCompare(c, zone.previousClose) < 0
bool counter = longSide ? f_sdCompare(c, zone.previousClose) < 0 or f_sdCompare(l, zone.previousLow) < 0 : f_sdCompare(c, zone.previousClose) > 0 or f_sdCompare(h, zone.previousHigh) > 0
if counter
float move = longSide ? zone.impulseExtreme - proximal : proximal - zone.impulseExtreme
if f_sdCompare(move, cfg.impulse * zone.birthAtr) >= 0
zone.origin := zone.impulseExtreme
zone.front := longSide ? l : h
zone.retrBars := 1
zone.phaseBar := atBar
zone.state := "RETRACEMENT"
zone.reason := "Wait for two-bar pullback and own-origin break"
else
zone.state := "INVALID"
zone.reason := "Departure below strength threshold"
else if continues
zone.impulseExtreme := longSide ? math.max(zone.impulseExtreme, h) : math.min(zone.impulseExtreme, l)
else if zone.state == "RETRACEMENT"
bool breaks = longSide ? f_sdCompare(c, zone.origin) > 0 : f_sdCompare(c, zone.origin) < 0
if atBar - zone.phaseBar > cfg.retrWait
zone.state := "EXPIRED"
zone.reason := "Retracement deadline reached"
else if breaks
float frontDistance = longSide ? zone.front - proximal : proximal - zone.front
float setupDistance = longSide ? zone.origin - proximal : proximal - zone.origin
if zone.retrBars < cfg.retrMin
zone.state := "INVALID"
zone.reason := "Origin broken before minimum retracement"
else if f_sdSign(setupDistance) <= 0 or f_sdSign(frontDistance) <= 0 or f_sdCompare(frontDistance / setupDistance, cfg.near) > 0
zone.state := "INVALID"
zone.reason := "Front liquidity outside near-half rule"
else
zone.state := "READY"
zone.readyBar := atBar
zone.readyTime := atTime
zone.reason := "Wait for liquidity raid into zone"
else
zone.retrBars += 1
zone.front := longSide ? math.min(zone.front, l) : math.max(zone.front, h)
else if zone.state == "READY" or zone.state == "VISIT"
bool oldVisit = zone.state == "VISIT"
if oldVisit and atBar - zone.visitBar > cfg.visitAge
zone.state := "EXPIRED"
zone.reason := "Entry visit deadline reached"
else
bool raids = longSide ? f_sdCompare(l, zone.front) < 0 : f_sdCompare(h, zone.front) > 0
if not oldVisit and touches and raids and atBar > zone.readyBar
zone.state := "VISIT"
zone.visitBar := atBar
zone.deepest := longSide ? l : h
zone.reason := "Wick rejected; wait for directional close"
if zone.state == "VISIT"
zone.deepest := longSide ? math.min(zone.deepest, l) : math.max(zone.deepest, h)
bool directional = longSide ? f_sdCompare(c, o) > 0 : f_sdCompare(c, o) < 0
if directional
zone.state := "SIGNAL"
zone.signalBar := atBar
zone.reason := "Raid and directional close confirmed"
signal := true
zone.previousHigh := h
zone.previousLow := l
zone.previousClose := c
signal
f_sdPlan(SdZone zone, float entry, float atr, int atBar, int atTime, float tick, float pointValue, float increment, SdConfig cfg) =>
SdPlan plan = SdPlan.new()
string why = "No current condition"
if zone.state == "SIGNAL" and zone.signalBar == atBar and f_sdSign(tick) > 0 and f_sdSign(atr) > 0
float pad = math.max(tick, atr * cfg.stopPad)
float stop = f_sdTick(zone.deepest - zone.direction * pad, tick, zone.direction == -1)
float risk = f_sdDistance(entry, stop, tick)
bool geometry = zone.direction == 1 ? f_sdCompare(entry, stop) > 0 : f_sdCompare(entry, stop) < 0
float target = f_sdTick(entry + zone.direction * risk * cfg.reward, tick, zone.direction == 1)
float quantity = f_sdQuantity(entry, risk, cfg.cash, cfg.cap, pointValue, increment)
if not geometry or f_sdSign(risk) <= 0 or na(target)
why := "Invalid rounded plan geometry"
else if na(quantity) or f_sdSign(quantity) <= 0
why := "Budget/cap cannot fund minimum quantity"
else
plan.direction := zone.direction
plan.id := zone.id + ":" + str.tostring(atTime)
plan.state := "ACTIVE"
plan.reason := "Paper reference · next-bar outcomes"
plan.signalBar := atBar
plan.signalTime := atTime
plan.hold := cfg.hold
plan.entry := entry
plan.stop := stop
plan.target := target
plan.risk := risk
plan.quantity := quantity
plan.pointValue := pointValue
plan.tick := tick
plan.fee := cfg.fee
plan.slip := cfg.slip
plan.lower := zone.lower
plan.upper := zone.upper
plan.front := zone.front
plan.baseTime := zone.baseTime
plan.createdTime := zone.createdTime
plan.readyTime := zone.readyTime
plan.netAtTarget := f_sdNetR(plan, target)
why := "Accepted paper reference"
[plan, why]
f_sdSelect(array<SdZone> candidates, float entry, float atr, int atBar, int atTime, float tick, float pointValue, float increment, SdConfig cfg) =>
SdPlan chosen = SdPlan.new()
bool hasLong = false
bool hasShort = false
string why = "No current condition"
for zone in candidates
hasLong := hasLong or zone.direction == 1
hasShort := hasShort or zone.direction == -1
bool conflict = hasLong and hasShort
if conflict
why := "Opposite conditions · no admission"
else
for zone in candidates
[candidate, rejection] = f_sdPlan(zone, entry, atr, atBar, atTime, tick, pointValue, increment, cfg)
if candidate.direction != 0 and (chosen.direction == 0 or f_sdCompare(candidate.risk, chosen.risk) < 0)
chosen := candidate
why := rejection
else if chosen.direction == 0
why := rejection
[chosen, why, conflict]
f_sdOutcome(SdPlan plan, float o, float h, float l, float c, int atBar, bool confirmed) =>
string result = ""
float price = na
bool ambiguous = false
if confirmed and plan.state == "ACTIVE" and atBar > plan.signalBar
if not f_sdBarValid(o, h, l, c)
result := "DATA GAP"
else
bool longSide = plan.direction == 1
bool gapStop = longSide ? f_sdCompare(o, plan.stop) <= 0 : f_sdCompare(o, plan.stop) >= 0
bool gapTarget = longSide ? f_sdCompare(o, plan.target) >= 0 : f_sdCompare(o, plan.target) <= 0
bool stopHit = longSide ? f_sdCompare(l, plan.stop) <= 0 : f_sdCompare(h, plan.stop) >= 0
bool targetHit = longSide ? f_sdCompare(h, plan.target) >= 0 : f_sdCompare(l, plan.target) <= 0
if gapStop or gapTarget
result := gapStop ? "STOP" : "TARGET"
price := o
else if stopHit or targetHit
result := stopHit ? "STOP" : "TARGET"
price := stopHit ? plan.stop : plan.target
ambiguous := stopHit and targetHit
else if atBar - plan.signalBar >= plan.hold
result := "TIME"
price := c
[result, price, ambiguous]
f_sdSettle(SdPlan plan, string result, float price, bool ambiguous, int atBar, int atTime) =>
SdPlan settled = plan.copy()
settled.state := result == "DATA GAP" ? "UNRESOLVED" : "ENDED"
settled.reason := result
settled.exitPrice := price
settled.ambiguous := ambiguous
settled.endBar := atBar
settled.endTime := atTime
settled.netR := na(price) ? na : f_sdNetR(plan, price)
settled
f_sdNumber(float value) =>
string rendered = "null"
if not na(value)
if f_sdSign(value) == 0
rendered := "0"
else
int power = int(math.floor(math.log10(math.abs(value))))
rendered := power < -6 or power > 12 ? str.tostring(value / math.pow(10, power), "0.################") + "e" + str.tostring(power) : str.tostring(value, "0.################")
rendered
f_sdEscape(string value) =>
string escaped = str.replace_all(value, "\\", "\\\\")
escaped := str.replace_all(escaped, "\"", "\\\"")
escaped := str.replace_all(escaped, "\n", "\\n")
str.replace_all(escaped, "\t", "\\t")
f_sdJson(SdPlan plan, string event, string symbol, string tf) =>
"{\"schema\":\"sdd1\",\"event\":\"" + event + "\",\"kind\":\"paper_reference\",\"symbol\":\"" + f_sdEscape(symbol) + "\",\"tf\":\"" + tf + "\",\"id\":\"" + plan.id + "\",\"side\":\"" + (plan.direction == 1 ? "long" : "short") + "\",\"signal_time\":" + str.tostring(plan.signalTime) + ",\"entry\":" + f_sdNumber(plan.entry) + ",\"stop\":" + f_sdNumber(plan.stop) + ",\"target\":" + f_sdNumber(plan.target) + ",\"quantity\":" + f_sdNumber(plan.quantity) + ",\"net_at_target_r\":" + f_sdNumber(plan.netAtTarget) + ",\"exit\":" + f_sdNumber(plan.exitPrice) + ",\"net_r\":" + f_sdNumber(plan.netR) + ",\"ambiguous\":" + (plan.ambiguous ? "true" : "false") + "}"
// === CHART ENGINE ===
if barstate.isfirst and (not chart.is_standard or not timeframe.isminutes or timeframe.multiplier != 5)
runtime.error("Supply & Demand Desk requires standard 5-minute candles. Switch chart type to Candles and timeframe to 5m.")
SdConfig sdCfg = SdConfig.new(sdImpulse, sdRetrMin, sdNear, sdRetrWait, sdZoneAge, sdVisitAge, sdStopPad, sdReward, sdHold, sdCash, sdCap, sdFee, sdSlip)
float sdAtr = ta.atr(14)
float sdPh = ta.pivothigh(high, 2, 2)
float sdPl = ta.pivotlow(low, 2, 2)
var float sdLastHigh = na
var float sdPriorHigh = na
var float sdLastLow = na
var float sdPriorLow = na
var int sdTrend = 0
var array<SdZone> sdZones = array.new<SdZone>()
var array<SdZone> sdCandidates = array.new<SdZone>()
var array<SdPlan> sdHistory = array.new<SdPlan>()
var SdPlan sdPaper = SdPlan.new()
var string sdWhy = "Waiting for ATR history"
var int sdLastClosedTime = na
var int sdRetired = 0
var int sdConditions = 0
var int sdBlocked = 0
var int sdLastExit = na
bool sdEntered = false
bool sdExited = false
bool sdCondition = false
bool sdConflict = false
bool sdValid = f_sdBarValid(open, high, low, close)
bool sdReady = not na(sdAtr) and f_sdSign(sdAtr) > 0
if barstate.isconfirmed
sdLastClosedTime := time_close
array.clear(sdCandidates)
if not na(sdPh)
sdPriorHigh := sdLastHigh
sdLastHigh := sdPh
if not na(sdPl)
sdPriorLow := sdLastLow
sdLastLow := sdPl
sdTrend := f_sdCompare(sdLastHigh, sdPriorHigh) > 0 and f_sdCompare(sdLastLow, sdPriorLow) > 0 ? 1 : f_sdCompare(sdLastHigh, sdPriorHigh) < 0 and f_sdCompare(sdLastLow, sdPriorLow) < 0 ? -1 : 0
[sdResult, sdExitPrice, sdAmbiguous] = f_sdOutcome(sdPaper, open, high, low, close, bar_index, true)
if sdResult != ""
sdPaper := f_sdSettle(sdPaper, sdResult, sdExitPrice, sdAmbiguous, bar_index, time_close)
array.push(sdHistory, sdPaper.copy())
if array.size(sdHistory) > 6
array.shift(sdHistory)
sdExited := true
sdLastExit := bar_index
if sdJson
alert(f_sdJson(sdPaper, sdResult, syminfo.tickerid, timeframe.period), alert.freq_all)
for zone in sdZones
bool candidate = f_sdStep(zone, open, high, low, close, bar_index, time_close, sdCfg)
if candidate
sdConditions += 1
sdCondition := true
if not sdTrendGate or sdTrend == zone.direction
array.push(sdCandidates, zone)
else
zone.reason := "Condition blocked by chart-local trend"
sdBlocked += 1
if array.size(sdCandidates) > 0
[sdSelected, sdSelectionWhy, sdHasConflict] = f_sdSelect(sdCandidates, close, sdAtr, bar_index, time_close, syminfo.mintick, syminfo.pointvalue, syminfo.mincontract, sdCfg)
sdConflict := sdHasConflict
if sdPaper.state == "ACTIVE"
sdWhy := "Condition seen · paper reference already active"
sdBlocked += 1
else if sdExited
sdWhy := "Condition seen · no exit-bar replacement"
sdBlocked += 1
else if sdSelected.direction == 0
sdWhy := sdSelectionWhy
sdBlocked += 1
else
sdPaper := sdSelected
sdEntered := true
sdWhy := sdSelectionWhy
if sdJson
alert(f_sdJson(sdPaper, "ENTRY", syminfo.tickerid, timeframe.period), alert.freq_all)
else if not sdValid
sdWhy := "Malformed observed candle · wait for clean data"
else if not sdReady
sdWhy := "ATR14 history unavailable"
else if sdCondition
sdWhy := "Condition blocked by chart-local trend"
else if sdPaper.state == "ACTIVE"
sdWhy := "Fixed paper reference active · no new entry"
else
sdWhy := "No current accepted entry"
bool previousValid = bar_index > 0 and f_sdBarValid(open[1], high[1], low[1], close[1])
if sdValid and previousValid and sdReady
int sdBirthDirection = f_sdCompare(close, open) > 0 and f_sdCompare(close[1], open[1]) < 0 ? 1 : f_sdCompare(close, open) < 0 and f_sdCompare(close[1], open[1]) > 0 ? -1 : 0
if sdBirthDirection != 0
SdZone birth = f_sdNewZone(sdBirthDirection, open[1], high[1], low[1], close[1], open, high, low, close, sdAtr, bar_index - 1, time[1], bar_index, time_close, sdAccuracy)
if birth.state == "DEPARTURE"
if array.size(sdZones) >= 32
int removeIndex = 0
bool foundTerminal = false
for i = 0 to array.size(sdZones) - 1
if f_sdTerminal(array.get(sdZones, i).state) and not foundTerminal
removeIndex := i
foundTerminal := true
array.remove(sdZones, removeIndex)
sdRetired += 1
array.push(sdZones, birth)
plot(sdPaper.state == "ACTIVE" ? sdPaper.entry : na, "Paper entry reference", display = display.none, editable = false)
plot(sdPaper.state == "ACTIVE" ? sdPaper.stop : na, "Paper stop", display = display.none, editable = false)
plot(sdPaper.state == "ACTIVE" ? sdPaper.target : na, "Paper target", display = display.none, editable = false)
plotshape(sdEntered and sdPaper.direction == 1, "Paper long reference", shape.triangleup, location.belowbar, color = color.teal, size = size.tiny, text = "REF L", textcolor = color.teal, display = display.pane)
plotshape(sdEntered and sdPaper.direction == -1, "Paper short reference", shape.triangledown, location.abovebar, color = color.orange, size = size.tiny, text = "REF S", textcolor = color.orange, display = display.pane)
alertcondition(sdEntered, "Paper entry reference", "SDD paper reference {{ticker}} {{interval}} entry={{plot(\"Paper entry reference\")}} stop={{plot(\"Paper stop\")}} target={{plot(\"Paper target\")}}. This is a confirmed reference, not a broker fill.")
alertcondition(sdExited, "Paper reference ended", "SDD paper reference ended on {{ticker}} {{interval}}. Use JSON for exact reason, prices, costs and ambiguity.")
alertcondition(sdCondition, "Zone condition confirmed", "SDD zone condition on {{ticker}} {{interval}}. A condition may be blocked by trend, opposite conditions or plan occupancy; it is not an accepted entry.")
// Rendering consumes committed fields; no price or admission logic uses display inputs.
string vGroup = "05 · Presentation"
string vMode = input.string("Clean", "Chart view", options = ["Off", "Clean", "Inspect"], group = vGroup, display = display.none)
bool vDesk = input.bool(true, "Show WHY / NEXT desk", group = vGroup, display = display.none)
int vZoneCount = input.int(4, "Visible live zones", minval = 1, maxval = 8, group = vGroup, active = vMode != "Off", display = display.none)
bool vHistory = input.bool(true, "Ended paper markers", group = vGroup, active = vMode != "Off", display = display.none)
string vSize = input.string("Normal", "Text size", options = ["Small", "Normal", "Large"], group = vGroup, display = display.none)
color vBull = color.rgb(33, 191, 160)
color vBear = color.rgb(244, 158, 85)
color vInk = color.rgb(17, 29, 43)
color vMuted = color.rgb(158, 177, 196)
string vBodySize = vSize == "Large" ? size.normal : vSize == "Small" ? size.tiny : size.small
string vSmallSize = vSize == "Large" ? size.small : size.tiny
var array<box> vBoxes = array.new<box>()
var array<line> vLines = array.new<line>()
var array<label> vLabels = array.new<label>()
var table vTable = table.new(position.top_right, 2, 11, bgcolor = na, frame_width = 0)
float vRange = ta.highest(high, 80) - ta.lowest(low, 80)
f_vPrice(float price) =>
na(price) ? "—" : str.tostring(price, format.mintick)
f_vClock(int stamp) =>
na(stamp) ? "—" : str.format_time(stamp, "MM-dd HH:mm", "UTC")
f_vRank(string state) =>
state == "VISIT" ? 4 : state == "READY" ? 3 : state == "RETRACEMENT" ? 2 : state == "DEPARTURE" ? 1 : 0
f_vNext(SdZone zone) =>
zone.state == "DEPARTURE" ? "Wait for a pullback above/below this untouched zone" : zone.state == "RETRACEMENT" ? "Need " + str.tostring(sdRetrMin) + " pullback bars then a close through " + f_vPrice(zone.origin) : zone.state == "READY" ? "Raid " + f_vPrice(zone.front) + " into zone; directional close outside" : zone.state == "VISIT" ? "Need " + (zone.direction == 1 ? "bullish close > " + f_vPrice(zone.upper) : "bearish close < " + f_vPrice(zone.lower)) : zone.reason
f_vLevel(float price, string captionText, color ink, int startBar, int endBar, float tagPrice, string tip) =>
array.push(vLines, line.new(startBar, price, endBar, price, xloc = xloc.bar_index, color = color.new(ink, 15), width = 1))
if f_sdCompare(price, tagPrice) != 0
array.push(vLines, line.new(endBar, price, endBar + 1, tagPrice, xloc = xloc.bar_index, color = color.new(ink, 35), style = line.style_dotted))
array.push(vLabels, label.new(endBar + 1, tagPrice, captionText, xloc = xloc.bar_index, style = label.style_label_left, color = color.new(vInk, 5), textcolor = ink, size = vBodySize, tooltip = tip))
if barstate.islastconfirmedhistory or barstate.isrealtime
for item in vBoxes
box.delete(item)
for item in vLines
line.delete(item)
for item in vLabels
label.delete(item)
array.clear(vBoxes)
array.clear(vLines)
array.clear(vLabels)
table.clear(vTable, 0, 0, 1, 10)
table.set_bgcolor(vTable, na)
table.set_frame_color(vTable, vDesk ? color.new(vMuted, 65) : na)
table.set_frame_width(vTable, 0)
int vReadyCount = 0
int vPendingCount = 0
int vDrawn = 0
SdZone vFocus = SdZone.new()
if array.size(sdZones) > 0
for i = array.size(sdZones) - 1 to 0
SdZone zone = array.get(sdZones, i)
bool live = not f_sdTerminal(zone.state) and zone.state != "SIGNAL"
if live
vPendingCount += 1
if zone.state == "READY" or zone.state == "VISIT"
vReadyCount += 1
if f_vRank(zone.state) > f_vRank(vFocus.state)
vFocus := zone
if vMode != "Off" and vDrawn < vZoneCount
color ink = zone.direction == 1 ? vBull : vBear
int start = vMode == "Inspect" ? zone.baseBar : zone.createdBar
array.push(vBoxes, box.new(start, zone.upper, bar_index + 2, zone.lower, border_color = color.new(ink, zone.state == "VISIT" ? 5 : 45), bgcolor = color.new(ink, zone.state == "VISIT" ? 84 : 93), text = (zone.direction == 1 ? "DEMAND · " : "SUPPLY · ") + zone.state, text_color = color.new(ink, 5), text_size = vSmallSize, text_halign = text.align_left, text_valign = text.align_center))
if not na(zone.front)
array.push(vLines, line.new(zone.phaseBar, zone.front, bar_index + 2, zone.front, color = color.new(ink, 25), style = line.style_dashed))
if vMode == "Inspect"
array.push(vLabels, label.new(zone.createdBar, zone.direction == 1 ? zone.lower : zone.upper, "KNOWN " + f_vClock(zone.createdTime), style = zone.direction == 1 ? label.style_label_up : label.style_label_down, color = color.new(vInk, 20), textcolor = ink, size = size.tiny, tooltip = "Base " + f_vClock(zone.baseTime) + " UTC\nCreated/known " + f_vClock(zone.createdTime) + " UTC\n" + zone.reason + "\n" + f_vNext(zone)))
vDrawn += 1
bool vHasPaper = sdPaper.direction != 0
bool vActive = sdPaper.state == "ACTIVE"
bool vRecent = vHasPaper and not na(sdPaper.endBar) and bar_index - sdPaper.endBar <= 6
if vMode != "Off" and vHasPaper and (vActive or vRecent)
float span = math.max(vRange, math.abs(sdPaper.target - sdPaper.stop))
float gap = math.max(4 * syminfo.mintick, span * (vSize == "Large" ? 0.07 : 0.045))
int end = bar_index + 3
color sideColor = sdPaper.direction == 1 ? vBull : vBear
string frozenTip = "Frozen at " + f_vClock(sdPaper.signalTime) + " UTC\nBase " + f_vClock(sdPaper.baseTime) + " · known " + f_vClock(sdPaper.createdTime) + "\nQuantity " + f_sdNumber(sdPaper.quantity) + " · estimated net at target " + f_sdNumber(sdPaper.netAtTarget) + "R\nPaper reference; this is not a broker fill."
array.push(vBoxes, box.new(sdPaper.signalBar, math.max(sdPaper.entry, sdPaper.target), end, math.min(sdPaper.entry, sdPaper.target), border_color = na, bgcolor = color.new(sideColor, vActive ? 92 : 97)))
array.push(vBoxes, box.new(sdPaper.signalBar, math.max(sdPaper.entry, sdPaper.stop), end, math.min(sdPaper.entry, sdPaper.stop), border_color = na, bgcolor = color.new(color.red, vActive ? 93 : 98)))
float entryTag = sdPaper.entry
float stopTag = sdPaper.direction == 1 ? math.min(sdPaper.stop, entryTag - gap) : math.max(sdPaper.stop, entryTag + gap)
float targetTag = sdPaper.direction == 1 ? math.max(sdPaper.target, entryTag + gap) : math.min(sdPaper.target, entryTag - gap)
f_vLevel(sdPaper.entry, (vActive ? "ENTRY REF " : "ENDED REF ") + f_vPrice(sdPaper.entry), color.white, sdPaper.signalBar, end, entryTag, frozenTip)
f_vLevel(sdPaper.stop, "STOP " + f_vPrice(sdPaper.stop), color.rgb(246, 117, 117), sdPaper.signalBar, end, stopTag, frozenTip)
f_vLevel(sdPaper.target, str.tostring(math.abs(sdPaper.target - sdPaper.entry) / sdPaper.risk, "0.##") + "R TARGET " + f_vPrice(sdPaper.target), sideColor, sdPaper.signalBar, end, targetTag, frozenTip)
if vMode != "Off" and vHistory
for ended in sdHistory
if bar_index - ended.endBar <= 200
float marker = na(ended.exitPrice) ? ended.entry : ended.exitPrice
string caption = ended.reason + (ended.ambiguous ? " · BOTH TOUCHED" : "") + (na(ended.netR) ? "\nUNPRICED" : "\n" + str.tostring(ended.netR, "0.00") + "R est. net")
array.push(vLabels, label.new(ended.endBar, marker, caption, style = ended.direction == 1 ? label.style_label_down : label.style_label_up, color = color.new(vInk, 15), textcolor = ended.reason == "TARGET" ? vBull : vMuted, size = vSmallSize, tooltip = na(ended.exitPrice) ? "Unpriced data gap; marker uses frozen entry for orientation. No exit price or fill inferred." : "Historical paper outcome " + f_vClock(ended.endTime) + " UTC. Price " + f_vPrice(ended.exitPrice) + ". Not a currently tracked broker position."))
if vDesk
// Transparent chart-header inset; body rows retain their own background.
table.cell(vTable, 0, 0, "", height = 3, bgcolor = na)
table.cell(vTable, 1, 0, "", height = 3, bgcolor = na)
string nowText = vActive ? "ACTIVE PAPER " + (sdPaper.direction == 1 ? "LONG" : "SHORT") : sdEntered ? "NEW PAPER REFERENCE" : "NO ACTIVE PAPER PLAN"
table.cell(vTable, 0, 1, "SUPPLY & DEMAND", text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 1, "DESK " + SDD_VERSION + " · 5m", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 2, nowText, text_color = vActive ? sdPaper.direction == 1 ? vBull : vBear : vMuted, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 2, "Closed " + f_vClock(sdLastClosedTime) + " UTC", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 3, "WHY", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 3, sdWhy, text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 4, "NEXT", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 4, vActive ? "Fixed stop/target · later candles only" : vFocus.direction != 0 ? f_vNext(vFocus) : "Wait for an untouched base and strong departure", text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 5, "READY / BUILDING", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 5, str.tostring(vReadyCount) + " / " + str.tostring(vPendingCount - vReadyCount) + " · shown " + str.tostring(vDrawn), text_color = vMuted, text_size = vSmallSize, tooltip = "All stored live zones are counted; chart draws at most the configured number, latest first.", bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 6, "TREND", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 6, (sdTrend == 1 ? "HH / HL" : sdTrend == -1 ? "LH / LL" : "MIXED / WARMUP") + (sdTrendGate ? " · gate ON" : " · context only"), text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 7, "PLAN / COSTS", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 7, vActive ? "Qty " + f_sdNumber(sdPaper.quantity) + " · net at TP " + str.tostring(sdPaper.netAtTarget, "0.00") + "R" : "Signal-close reference · no entry pending", text_color = vActive and f_sdSign(sdPaper.netAtTarget) <= 0 ? color.orange : vMuted, text_size = vSmallSize, tooltip = "Indicator cash quantities use symbol currency. Estimated net includes the declared fee/slippage allowance. Nonpositive net-at-TP remains disclosed; it is not profitability.", bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 8, "ALERT", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 8, SD_IS_LAB ? "Any alert() call = paper JSON; order fills = broker" : "Create alert → Any alert() function call", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
if vMode == "Inspect"
table.cell(vTable, 0, 9, "CONDITIONS / BLOCKS", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 9, str.tostring(sdConditions) + " / " + str.tostring(sdBlocked) + " · retired " + str.tostring(sdRetired), text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 10, "ORIGINAL SUBSET", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 10, "Closed bars · no flip-entry or private parity", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
The separate Strategy Lab
Tests independent orders, fills, sizing and declared costs.
Copy the code as it is. No coding needed.
View the full code
//@version=6
// Supply & Demand Desk 1.0.0 · MIT · Original public-method-inspired 5m subset.
// Not Mangoe's official script or private-method parity. See docs/engine-contract.md.
// Signal-close paper references differ from next-open Strategy Lab fills.
strategy("Supply & Demand Desk · Strategy Lab", "SDD Lab", overlay = true, behind_chart = false, max_boxes_count = 40, max_lines_count = 100, max_labels_count = 100, max_bars_back = 2000, initial_capital = 100000, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 1, pyramiding = 0, margin_long = 100, margin_short = 100, process_orders_on_close = false, calc_on_every_tick = false, calc_on_order_fills = false)
const string SDD_VERSION = "1.0.0"
const bool SD_IS_LAB = true
// Original closed-bar subset of a public discretionary method. Read docs/engine-contract.md.
string sdG = "01 · Setup rules"
float sdImpulse = input.float(0.8, "Minimum departure · ATR", minval = 0.1, maxval = 5, step = 0.1, group = sdG, tooltip = "Original strength threshold, measured from proximal edge to departure extreme. Birth ATR is frozen.", display = display.none)
int sdRetrMin = input.int(2, "Minimum retracement bars", minval = 2, maxval = 8, group = sdG, tooltip = "Observed retracement bars before the breakout bar; colors may mix.", display = display.none)
float sdNear = input.float(0.5, "Front liquidity · maximum setup fraction", minval = 0.1, maxval = 1, step = 0.05, group = sdG, tooltip = "Demand: (front low − proximal) / (originating high − proximal). Supply mirrors. Our explicit near-half formalization.", display = display.none)
bool sdAccuracy = input.bool(false, "FX accuracy drawing", group = sdG, tooltip = "Optional public FX drawing variant. Excludes a protruding proximal base wick. Default full-wick drawing applies to all symbols; no automatic asset classification.", display = display.none)
int sdRetrWait = input.int(20, "Retracement deadline · bars", minval = 3, maxval = 60, group = sdG, display = display.none)
int sdZoneAge = input.int(120, "Zone lifetime · bars", minval = 10, maxval = 500, group = sdG, display = display.none)
int sdVisitAge = input.int(3, "Entry visit deadline · bars", minval = 0, maxval = 12, group = sdG, tooltip = "Age0 is first touch. Default ages0..3 can confirm; age4 expires. Opposing-color wick rejection may wait for a later directional close.", display = display.none)
bool sdTrendGate = input.bool(false, "Require chart-local HH/HL or LH/LL", group = sdG, tooltip = "Optional original gate: last two confirmed two-left/two-right swing highs and lows. No HTF request. Neutral or incomplete structure blocks when enabled.", display = display.none)
string sdP = "02 · Paper reference"
float sdStopPad = input.float(0, "Stop pad · ATR (minimum one tick)", minval = 0, maxval = 1, step = 0.05, group = sdP, display = display.none)
float sdReward = input.float(4, "Target · R", minval = 1, maxval = 10, step = 0.5, group = sdP, tooltip = "4R follows the public 2026 five-minute course. Changing this is your variant. Rounded actual R may be slightly greater.", display = display.none)
int sdHold = input.int(48, "Maximum paper hold · bars", minval = 1, maxval = 288, group = sdP, tooltip = "Ordinary stop/target checks first, then close at the observed candle close. Broker Lab uses its own next-open market exit.", display = display.none)
float sdCash = input.float(100, "Cash risk budget · symbol currency", minval = 0.01, group = sdP, display = display.none)
float sdCap = input.float(90000, "Notional cap · symbol currency", minval = 1, group = sdP, display = display.none)
float sdFee = input.float(0.05, "Estimated commission · % per side", minval = 0, maxval = 2, step = 0.01, group = sdP, display = display.none)
int sdSlip = input.int(1, "Estimated slippage · ticks per side", minval = 0, maxval = 20, group = sdP, display = display.none)
bool sdJson = input.bool(true, "JSON alert events", group = "03 · Alerts", tooltip = "Create one alert using Any alert() function call. Reference events are not broker fills. Strategy Lab can emit these paper-reference events independently from its order-fill alerts.", display = display.none)
// === TYPES AND PURE HELPERS ===
type SdConfig
float impulse = 0.8
int retrMin = 2
float near = 0.5
int retrWait = 20
int zoneAge = 120
int visitAge = 3
float stopPad = 0
float reward = 4
int hold = 48
float cash = 100
float cap = 90000
float fee = 0.05
int slip = 1
type SdZone
int direction = 0
string id = ""
string state = "INVALID"
string reason = "Unavailable"
int baseBar = na
int baseTime = na
int createdBar = na
int createdTime = na
int phaseBar = na
int readyBar = na
int readyTime = na
int visitBar = na
int signalBar = na
float lower = na
float upper = na
float birthAtr = na
float impulseExtreme = na
float origin = na
float front = na
int retrBars = 0
float deepest = na
float previousHigh = na
float previousLow = na
float previousClose = na
type SdPlan
int direction = 0
string id = ""
string state = "EMPTY"
string reason = "No accepted reference"
int signalBar = na
int signalTime = na
int endBar = na
int endTime = na
int hold = 48
float entry = na
float stop = na
float target = na
float risk = na
float quantity = na
float pointValue = na
float tick = na
float fee = 0
int slip = 0
float netAtTarget = na
float exitPrice = na
float netR = na
bool ambiguous = false
float lower = na
float upper = na
float front = na
int baseTime = na
int createdTime = na
int readyTime = na
f_sdSign(float value) =>
int(nz(value / math.abs(value), 0))
f_sdCompare(float lhs, float rhs) =>
int result = na
if not na(lhs) and not na(rhs)
float difference = lhs - rhs
float noise = 4 * 2.220446049250313e-16 * math.max(math.abs(lhs), math.abs(rhs))
result := f_sdSign(math.abs(difference) - noise) <= 0 ? 0 : f_sdSign(difference)
result
f_sdBarValid(float o, float h, float l, float c) =>
not na(o) and not na(h) and not na(l) and not na(c) and f_sdSign(h - l) >= 0 and f_sdSign(h - math.max(o, c)) >= 0 and f_sdSign(math.min(o, c) - l) >= 0
f_sdUnits(float value) =>
float nearest = math.round(value)
float noise = math.min(1e-7, 8 * 2.220446049250313e-16 * math.max(1, math.abs(value)))
f_sdSign(math.abs(value - nearest) - noise) <= 0 ? nearest : value
f_sdTick(float value, float tick, bool up) =>
float units = f_sdUnits(value / tick)
(up ? math.ceil(units) : math.floor(units)) * tick
f_sdDistance(float firstPrice, float secondPrice, float tick) =>
float distance = math.abs(firstPrice - secondPrice)
float units = distance / tick
float nearest = math.round(units)
float noise = math.min(1e-7, 8 * 2.220446049250313e-16 * math.max(math.abs(firstPrice), math.abs(secondPrice)) / tick)
f_sdSign(math.abs(units - nearest) - noise) <= 0 ? nearest * tick : distance
f_sdQuantity(float entry, float distance, float cash, float cap, float pointValue, float increment) =>
float quantity = na
if not na(entry) and f_sdSign(distance) > 0 and f_sdSign(cash) > 0 and f_sdSign(cap) > 0 and f_sdSign(pointValue) > 0 and f_sdSign(increment) > 0
float byRisk = cash / (distance * pointValue)
float byCap = f_sdSign(math.abs(entry)) > 0 ? cap / (math.abs(entry) * pointValue) : byRisk
quantity := math.floor(f_sdUnits(math.min(byRisk, byCap) / increment)) * increment
quantity
f_sdNetR(SdPlan plan, float exitPrice) =>
float gross = plan.direction * (exitPrice - plan.entry) * plan.quantity * plan.pointValue
float fees = (math.abs(plan.entry) + math.abs(exitPrice)) * plan.quantity * plan.pointValue * plan.fee / 100
float slipCash = 2 * plan.slip * plan.tick * plan.quantity * plan.pointValue
(gross - fees - slipCash) / (plan.risk * plan.quantity * plan.pointValue)
f_sdTerminal(string state) =>
state == "INVALID" or state == "EXPIRED" or state == "USED"
f_sdNewZone(int direction, float bo, float bh, float bl, float bc, float o, float h, float l, float c, float atr, int baseBar, int baseTime, int nowBar, int nowTime, bool accuracy) =>
SdZone zone = SdZone.new()
bool packets = f_sdBarValid(bo, bh, bl, bc) and f_sdBarValid(o, h, l, c) and f_sdSign(atr) > 0
bool colors = direction == 1 ? f_sdCompare(bo, bc) > 0 and f_sdCompare(c, o) > 0 : f_sdCompare(bc, bo) > 0 and f_sdCompare(o, c) > 0
if packets and colors and (direction == 1 or direction == -1)
zone.direction := direction
zone.id := str.tostring(baseTime) + (direction == 1 ? ":D" : ":S")
zone.baseBar := baseBar
zone.baseTime := baseTime
zone.createdBar := nowBar
zone.createdTime := nowTime
zone.phaseBar := nowBar
zone.lower := direction == 1 ? math.min(bl, l) : accuracy and f_sdCompare(bl, l) < 0 ? bo : bl
zone.upper := direction == -1 ? math.max(bh, h) : accuracy and f_sdCompare(bh, h) > 0 ? bo : bh
zone.birthAtr := atr
zone.impulseExtreme := direction == 1 ? h : l
zone.previousHigh := h
zone.previousLow := l
zone.previousClose := c
bool outside = direction == 1 ? f_sdCompare(c, zone.upper) > 0 : f_sdCompare(c, zone.lower) < 0
if outside and f_sdCompare(zone.upper, zone.lower) > 0
zone.state := "DEPARTURE"
zone.reason := "Wait for front liquidity"
zone
f_sdStep(SdZone zone, float o, float h, float l, float c, int atBar, int atTime, SdConfig cfg) =>
bool signal = false
bool valid = f_sdBarValid(o, h, l, c)
if not f_sdTerminal(zone.state) and atBar > zone.createdBar
if zone.state == "SIGNAL"
zone.state := "USED"
zone.reason := "Condition already used"
else if atBar - zone.createdBar > cfg.zoneAge
zone.state := "EXPIRED"
zone.reason := "Zone lifetime reached"
else if not valid
zone.state := "INVALID"
zone.reason := "Malformed observed candle"
else
bool longSide = zone.direction == 1
float proximal = longSide ? zone.upper : zone.lower
bool adverseClose = longSide ? f_sdCompare(c, proximal) <= 0 : f_sdCompare(c, proximal) >= 0
bool touches = f_sdCompare(l, zone.upper) <= 0 and f_sdCompare(h, zone.lower) >= 0
bool beforeReady = zone.state == "DEPARTURE" or zone.state == "RETRACEMENT"
if adverseClose
zone.state := "INVALID"
zone.reason := "Close on/inside or through zone"
else if beforeReady and touches
zone.state := "INVALID"
zone.reason := "Zone touched before readiness"
else if zone.state == "DEPARTURE"
bool continues = longSide ? f_sdCompare(c, o) > 0 and f_sdCompare(c, zone.previousClose) > 0 : f_sdCompare(c, o) < 0 and f_sdCompare(c, zone.previousClose) < 0
bool counter = longSide ? f_sdCompare(c, zone.previousClose) < 0 or f_sdCompare(l, zone.previousLow) < 0 : f_sdCompare(c, zone.previousClose) > 0 or f_sdCompare(h, zone.previousHigh) > 0
if counter
float move = longSide ? zone.impulseExtreme - proximal : proximal - zone.impulseExtreme
if f_sdCompare(move, cfg.impulse * zone.birthAtr) >= 0
zone.origin := zone.impulseExtreme
zone.front := longSide ? l : h
zone.retrBars := 1
zone.phaseBar := atBar
zone.state := "RETRACEMENT"
zone.reason := "Wait for two-bar pullback and own-origin break"
else
zone.state := "INVALID"
zone.reason := "Departure below strength threshold"
else if continues
zone.impulseExtreme := longSide ? math.max(zone.impulseExtreme, h) : math.min(zone.impulseExtreme, l)
else if zone.state == "RETRACEMENT"
bool breaks = longSide ? f_sdCompare(c, zone.origin) > 0 : f_sdCompare(c, zone.origin) < 0
if atBar - zone.phaseBar > cfg.retrWait
zone.state := "EXPIRED"
zone.reason := "Retracement deadline reached"
else if breaks
float frontDistance = longSide ? zone.front - proximal : proximal - zone.front
float setupDistance = longSide ? zone.origin - proximal : proximal - zone.origin
if zone.retrBars < cfg.retrMin
zone.state := "INVALID"
zone.reason := "Origin broken before minimum retracement"
else if f_sdSign(setupDistance) <= 0 or f_sdSign(frontDistance) <= 0 or f_sdCompare(frontDistance / setupDistance, cfg.near) > 0
zone.state := "INVALID"
zone.reason := "Front liquidity outside near-half rule"
else
zone.state := "READY"
zone.readyBar := atBar
zone.readyTime := atTime
zone.reason := "Wait for liquidity raid into zone"
else
zone.retrBars += 1
zone.front := longSide ? math.min(zone.front, l) : math.max(zone.front, h)
else if zone.state == "READY" or zone.state == "VISIT"
bool oldVisit = zone.state == "VISIT"
if oldVisit and atBar - zone.visitBar > cfg.visitAge
zone.state := "EXPIRED"
zone.reason := "Entry visit deadline reached"
else
bool raids = longSide ? f_sdCompare(l, zone.front) < 0 : f_sdCompare(h, zone.front) > 0
if not oldVisit and touches and raids and atBar > zone.readyBar
zone.state := "VISIT"
zone.visitBar := atBar
zone.deepest := longSide ? l : h
zone.reason := "Wick rejected; wait for directional close"
if zone.state == "VISIT"
zone.deepest := longSide ? math.min(zone.deepest, l) : math.max(zone.deepest, h)
bool directional = longSide ? f_sdCompare(c, o) > 0 : f_sdCompare(c, o) < 0
if directional
zone.state := "SIGNAL"
zone.signalBar := atBar
zone.reason := "Raid and directional close confirmed"
signal := true
zone.previousHigh := h
zone.previousLow := l
zone.previousClose := c
signal
f_sdPlan(SdZone zone, float entry, float atr, int atBar, int atTime, float tick, float pointValue, float increment, SdConfig cfg) =>
SdPlan plan = SdPlan.new()
string why = "No current condition"
if zone.state == "SIGNAL" and zone.signalBar == atBar and f_sdSign(tick) > 0 and f_sdSign(atr) > 0
float pad = math.max(tick, atr * cfg.stopPad)
float stop = f_sdTick(zone.deepest - zone.direction * pad, tick, zone.direction == -1)
float risk = f_sdDistance(entry, stop, tick)
bool geometry = zone.direction == 1 ? f_sdCompare(entry, stop) > 0 : f_sdCompare(entry, stop) < 0
float target = f_sdTick(entry + zone.direction * risk * cfg.reward, tick, zone.direction == 1)
float quantity = f_sdQuantity(entry, risk, cfg.cash, cfg.cap, pointValue, increment)
if not geometry or f_sdSign(risk) <= 0 or na(target)
why := "Invalid rounded plan geometry"
else if na(quantity) or f_sdSign(quantity) <= 0
why := "Budget/cap cannot fund minimum quantity"
else
plan.direction := zone.direction
plan.id := zone.id + ":" + str.tostring(atTime)
plan.state := "ACTIVE"
plan.reason := "Paper reference · next-bar outcomes"
plan.signalBar := atBar
plan.signalTime := atTime
plan.hold := cfg.hold
plan.entry := entry
plan.stop := stop
plan.target := target
plan.risk := risk
plan.quantity := quantity
plan.pointValue := pointValue
plan.tick := tick
plan.fee := cfg.fee
plan.slip := cfg.slip
plan.lower := zone.lower
plan.upper := zone.upper
plan.front := zone.front
plan.baseTime := zone.baseTime
plan.createdTime := zone.createdTime
plan.readyTime := zone.readyTime
plan.netAtTarget := f_sdNetR(plan, target)
why := "Accepted paper reference"
[plan, why]
f_sdSelect(array<SdZone> candidates, float entry, float atr, int atBar, int atTime, float tick, float pointValue, float increment, SdConfig cfg) =>
SdPlan chosen = SdPlan.new()
bool hasLong = false
bool hasShort = false
string why = "No current condition"
for zone in candidates
hasLong := hasLong or zone.direction == 1
hasShort := hasShort or zone.direction == -1
bool conflict = hasLong and hasShort
if conflict
why := "Opposite conditions · no admission"
else
for zone in candidates
[candidate, rejection] = f_sdPlan(zone, entry, atr, atBar, atTime, tick, pointValue, increment, cfg)
if candidate.direction != 0 and (chosen.direction == 0 or f_sdCompare(candidate.risk, chosen.risk) < 0)
chosen := candidate
why := rejection
else if chosen.direction == 0
why := rejection
[chosen, why, conflict]
f_sdOutcome(SdPlan plan, float o, float h, float l, float c, int atBar, bool confirmed) =>
string result = ""
float price = na
bool ambiguous = false
if confirmed and plan.state == "ACTIVE" and atBar > plan.signalBar
if not f_sdBarValid(o, h, l, c)
result := "DATA GAP"
else
bool longSide = plan.direction == 1
bool gapStop = longSide ? f_sdCompare(o, plan.stop) <= 0 : f_sdCompare(o, plan.stop) >= 0
bool gapTarget = longSide ? f_sdCompare(o, plan.target) >= 0 : f_sdCompare(o, plan.target) <= 0
bool stopHit = longSide ? f_sdCompare(l, plan.stop) <= 0 : f_sdCompare(h, plan.stop) >= 0
bool targetHit = longSide ? f_sdCompare(h, plan.target) >= 0 : f_sdCompare(l, plan.target) <= 0
if gapStop or gapTarget
result := gapStop ? "STOP" : "TARGET"
price := o
else if stopHit or targetHit
result := stopHit ? "STOP" : "TARGET"
price := stopHit ? plan.stop : plan.target
ambiguous := stopHit and targetHit
else if atBar - plan.signalBar >= plan.hold
result := "TIME"
price := c
[result, price, ambiguous]
f_sdSettle(SdPlan plan, string result, float price, bool ambiguous, int atBar, int atTime) =>
SdPlan settled = plan.copy()
settled.state := result == "DATA GAP" ? "UNRESOLVED" : "ENDED"
settled.reason := result
settled.exitPrice := price
settled.ambiguous := ambiguous
settled.endBar := atBar
settled.endTime := atTime
settled.netR := na(price) ? na : f_sdNetR(plan, price)
settled
f_sdNumber(float value) =>
string rendered = "null"
if not na(value)
if f_sdSign(value) == 0
rendered := "0"
else
int power = int(math.floor(math.log10(math.abs(value))))
rendered := power < -6 or power > 12 ? str.tostring(value / math.pow(10, power), "0.################") + "e" + str.tostring(power) : str.tostring(value, "0.################")
rendered
f_sdEscape(string value) =>
string escaped = str.replace_all(value, "\\", "\\\\")
escaped := str.replace_all(escaped, "\"", "\\\"")
escaped := str.replace_all(escaped, "\n", "\\n")
str.replace_all(escaped, "\t", "\\t")
f_sdJson(SdPlan plan, string event, string symbol, string tf) =>
"{\"schema\":\"sdd1\",\"event\":\"" + event + "\",\"kind\":\"paper_reference\",\"symbol\":\"" + f_sdEscape(symbol) + "\",\"tf\":\"" + tf + "\",\"id\":\"" + plan.id + "\",\"side\":\"" + (plan.direction == 1 ? "long" : "short") + "\",\"signal_time\":" + str.tostring(plan.signalTime) + ",\"entry\":" + f_sdNumber(plan.entry) + ",\"stop\":" + f_sdNumber(plan.stop) + ",\"target\":" + f_sdNumber(plan.target) + ",\"quantity\":" + f_sdNumber(plan.quantity) + ",\"net_at_target_r\":" + f_sdNumber(plan.netAtTarget) + ",\"exit\":" + f_sdNumber(plan.exitPrice) + ",\"net_r\":" + f_sdNumber(plan.netR) + ",\"ambiguous\":" + (plan.ambiguous ? "true" : "false") + "}"
// === CHART ENGINE ===
if barstate.isfirst and (not chart.is_standard or not timeframe.isminutes or timeframe.multiplier != 5)
runtime.error("Supply & Demand Desk requires standard 5-minute candles. Switch chart type to Candles and timeframe to 5m.")
SdConfig sdCfg = SdConfig.new(sdImpulse, sdRetrMin, sdNear, sdRetrWait, sdZoneAge, sdVisitAge, sdStopPad, sdReward, sdHold, sdCash, sdCap, sdFee, sdSlip)
float sdAtr = ta.atr(14)
float sdPh = ta.pivothigh(high, 2, 2)
float sdPl = ta.pivotlow(low, 2, 2)
var float sdLastHigh = na
var float sdPriorHigh = na
var float sdLastLow = na
var float sdPriorLow = na
var int sdTrend = 0
var array<SdZone> sdZones = array.new<SdZone>()
var array<SdZone> sdCandidates = array.new<SdZone>()
var array<SdPlan> sdHistory = array.new<SdPlan>()
var SdPlan sdPaper = SdPlan.new()
var string sdWhy = "Waiting for ATR history"
var int sdLastClosedTime = na
var int sdRetired = 0
var int sdConditions = 0
var int sdBlocked = 0
var int sdLastExit = na
bool sdEntered = false
bool sdExited = false
bool sdCondition = false
bool sdConflict = false
bool sdValid = f_sdBarValid(open, high, low, close)
bool sdReady = not na(sdAtr) and f_sdSign(sdAtr) > 0
if barstate.isconfirmed
sdLastClosedTime := time_close
array.clear(sdCandidates)
if not na(sdPh)
sdPriorHigh := sdLastHigh
sdLastHigh := sdPh
if not na(sdPl)
sdPriorLow := sdLastLow
sdLastLow := sdPl
sdTrend := f_sdCompare(sdLastHigh, sdPriorHigh) > 0 and f_sdCompare(sdLastLow, sdPriorLow) > 0 ? 1 : f_sdCompare(sdLastHigh, sdPriorHigh) < 0 and f_sdCompare(sdLastLow, sdPriorLow) < 0 ? -1 : 0
[sdResult, sdExitPrice, sdAmbiguous] = f_sdOutcome(sdPaper, open, high, low, close, bar_index, true)
if sdResult != ""
sdPaper := f_sdSettle(sdPaper, sdResult, sdExitPrice, sdAmbiguous, bar_index, time_close)
array.push(sdHistory, sdPaper.copy())
if array.size(sdHistory) > 6
array.shift(sdHistory)
sdExited := true
sdLastExit := bar_index
if sdJson
alert(f_sdJson(sdPaper, sdResult, syminfo.tickerid, timeframe.period), alert.freq_all)
for zone in sdZones
bool candidate = f_sdStep(zone, open, high, low, close, bar_index, time_close, sdCfg)
if candidate
sdConditions += 1
sdCondition := true
if not sdTrendGate or sdTrend == zone.direction
array.push(sdCandidates, zone)
else
zone.reason := "Condition blocked by chart-local trend"
sdBlocked += 1
if array.size(sdCandidates) > 0
[sdSelected, sdSelectionWhy, sdHasConflict] = f_sdSelect(sdCandidates, close, sdAtr, bar_index, time_close, syminfo.mintick, syminfo.pointvalue, syminfo.mincontract, sdCfg)
sdConflict := sdHasConflict
if sdPaper.state == "ACTIVE"
sdWhy := "Condition seen · paper reference already active"
sdBlocked += 1
else if sdExited
sdWhy := "Condition seen · no exit-bar replacement"
sdBlocked += 1
else if sdSelected.direction == 0
sdWhy := sdSelectionWhy
sdBlocked += 1
else
sdPaper := sdSelected
sdEntered := true
sdWhy := sdSelectionWhy
if sdJson
alert(f_sdJson(sdPaper, "ENTRY", syminfo.tickerid, timeframe.period), alert.freq_all)
else if not sdValid
sdWhy := "Malformed observed candle · wait for clean data"
else if not sdReady
sdWhy := "ATR14 history unavailable"
else if sdCondition
sdWhy := "Condition blocked by chart-local trend"
else if sdPaper.state == "ACTIVE"
sdWhy := "Fixed paper reference active · no new entry"
else
sdWhy := "No current accepted entry"
bool previousValid = bar_index > 0 and f_sdBarValid(open[1], high[1], low[1], close[1])
if sdValid and previousValid and sdReady
int sdBirthDirection = f_sdCompare(close, open) > 0 and f_sdCompare(close[1], open[1]) < 0 ? 1 : f_sdCompare(close, open) < 0 and f_sdCompare(close[1], open[1]) > 0 ? -1 : 0
if sdBirthDirection != 0
SdZone birth = f_sdNewZone(sdBirthDirection, open[1], high[1], low[1], close[1], open, high, low, close, sdAtr, bar_index - 1, time[1], bar_index, time_close, sdAccuracy)
if birth.state == "DEPARTURE"
if array.size(sdZones) >= 32
int removeIndex = 0
bool foundTerminal = false
for i = 0 to array.size(sdZones) - 1
if f_sdTerminal(array.get(sdZones, i).state) and not foundTerminal
removeIndex := i
foundTerminal := true
array.remove(sdZones, removeIndex)
sdRetired += 1
array.push(sdZones, birth)
plot(sdPaper.state == "ACTIVE" ? sdPaper.entry : na, "Paper entry reference", display = display.none, editable = false)
plot(sdPaper.state == "ACTIVE" ? sdPaper.stop : na, "Paper stop", display = display.none, editable = false)
plot(sdPaper.state == "ACTIVE" ? sdPaper.target : na, "Paper target", display = display.none, editable = false)
plotshape(sdEntered and sdPaper.direction == 1, "Paper long reference", shape.triangleup, location.belowbar, color = color.teal, size = size.tiny, text = "REF L", textcolor = color.teal, display = display.pane)
plotshape(sdEntered and sdPaper.direction == -1, "Paper short reference", shape.triangledown, location.abovebar, color = color.orange, size = size.tiny, text = "REF S", textcolor = color.orange, display = display.pane)
// Independent broker owner. Paper reference exits never route broker closes.
string labGroup = "04 · Strategy Lab"
float labRisk = input.float(100, "Lab risk budget · account currency", minval = 0.01, group = labGroup, display = display.none)
float labExposure = input.float(90, "Lab maximum equity notional · %", minval = 1, maxval = 100, group = labGroup, tooltip = "90% reserves some room for gaps/fees; margin calls can still occur. Quantity rounds down to symbol minimum contract.", display = display.none)
var SdPlan labOwner = SdPlan.new()
var int labQueuedBar = na
var int labFillBar = na
var int labExitBar = na
var int labSeenClosed = 0
var int labMarginClosures = 0
var int labPartialClosures = 0
var string labWhy = "No queued broker entry"
var int labQueuedCount = 0
if barstate.isconfirmed
bool labFlat = f_sdSign(strategy.position_size) == 0
if strategy.closedtrades > labSeenClosed
for labTrade = labSeenClosed to strategy.closedtrades - 1
if str.contains(strategy.closedtrades.exit_comment(labTrade), "Margin")
labMarginClosures += 1
if labFlat
labOwner := SdPlan.new()
labQueuedBar := na
labFillBar := na
labExitBar := bar_index
labWhy := "Broker flat · no replacement on this bar"
else
labPartialClosures += strategy.closedtrades - labSeenClosed
labWhy := "Partial broker close · frozen bracket retained"
labSeenClosed := strategy.closedtrades
if not labFlat and labOwner.direction != 0
if na(labFillBar)
labFillBar := strategy.opentrades.entry_bar_index(0)
strategy.exit("SD bracket", "SD entry", stop = labOwner.stop, limit = labOwner.target, comment_loss = "SD stop", comment_profit = "SD target", alert_message = "SupplyDemandDesk broker bracket fill; broker prices may differ from paper reference.")
if bar_index - labFillBar >= labOwner.hold or not sdValid
strategy.close("SD entry", comment = not sdValid ? "SD data gap" : "SD time", alert_message = "SupplyDemandDesk independent broker lifecycle exit.")
labWhy := not sdValid ? "Broker data-gap close requested" : "Broker hold deadline · market close requested"
else if strategy.closedtrades == labSeenClosed and labWhy != "Partial broker close · frozen bracket retained"
labWhy := "Broker live · fixed bracket; own hold clock"
if labFlat and labOwner.direction != 0 and not na(labQueuedBar) and bar_index > labQueuedBar + 1
strategy.cancel("SD entry")
strategy.cancel("SD bracket")
labOwner := SdPlan.new()
labQueuedBar := na
labWhy := "Entry not filled · ownership cleared"
if labFlat and labOwner.direction == 0 and (na(labExitBar) or bar_index > labExitBar) and array.size(sdCandidates) > 0
SdConfig labCfg = sdCfg.copy()
labCfg.cash := strategy.convert_to_symbol(labRisk)
labCfg.cap := strategy.convert_to_symbol(math.max(strategy.equity, 0) * labExposure / 100)
[labCandidate, labReason, labConflict] = f_sdSelect(sdCandidates, close, sdAtr, bar_index, time_close, syminfo.mintick, syminfo.pointvalue, syminfo.mincontract, labCfg)
if labCandidate.direction != 0
labOwner := labCandidate
labQueuedBar := bar_index
labFillBar := na
labQueuedCount += 1
labWhy := "Entry queued · next available open"
strategy.entry("SD entry", labCandidate.direction == 1 ? strategy.long : strategy.short, qty = labCandidate.quantity, alert_message = "SupplyDemandDesk broker entry. Frozen signal-close bracket; actual fill follows the emulator.")
strategy.exit("SD bracket", "SD entry", stop = labCandidate.stop, limit = labCandidate.target, comment_loss = "SD stop", comment_profit = "SD target", alert_message = "SupplyDemandDesk broker bracket fill.")
else
labWhy := labReason
var table labTable = table.new(position.bottom_left, 2, 5, bgcolor = color.new(color.rgb(13, 23, 35), 5), frame_color = color.new(color.white, 80), frame_width = 1)
if barstate.islastconfirmedhistory or barstate.isrealtime
table.cell(labTable, 0, 0, "STRATEGY LAB", text_color = color.white, text_size = size.small)
table.cell(labTable, 1, 0, "Next-open broker fills", text_color = color.silver, text_size = size.small)
table.cell(labTable, 0, 1, labWhy, text_color = color.white, text_size = size.small)
table.cell(labTable, 1, 1, "Queued " + str.tostring(labQueuedCount) + " · closed " + str.tostring(strategy.closedtrades), text_color = color.silver, text_size = size.small)
table.cell(labTable, 0, 2, "Partial closes " + str.tostring(labPartialClosures), text_color = color.silver, text_size = size.small)
table.cell(labTable, 1, 2, "Margin events " + str.tostring(labMarginClosures), text_color = color.silver, text_size = size.small)
table.cell(labTable, 0, 3, "Paper ≠ broker position", text_color = color.orange, text_size = size.small)
table.cell(labTable, 1, 3, "Costs: strategy Properties", text_color = color.silver, text_size = size.small)
// Display-only footer clearance for the native TradingView watermark.
table.cell(labTable, 0, 4, "", height = 4, bgcolor = color.new(color.rgb(13, 23, 35), 100))
table.cell(labTable, 1, 4, "", height = 4, bgcolor = color.new(color.rgb(13, 23, 35), 100))
// Rendering consumes committed fields; no price or admission logic uses display inputs.
string vGroup = "05 · Presentation"
string vMode = input.string("Clean", "Chart view", options = ["Off", "Clean", "Inspect"], group = vGroup, display = display.none)
bool vDesk = input.bool(true, "Show WHY / NEXT desk", group = vGroup, display = display.none)
int vZoneCount = input.int(4, "Visible live zones", minval = 1, maxval = 8, group = vGroup, active = vMode != "Off", display = display.none)
bool vHistory = input.bool(true, "Ended paper markers", group = vGroup, active = vMode != "Off", display = display.none)
string vSize = input.string("Normal", "Text size", options = ["Small", "Normal", "Large"], group = vGroup, display = display.none)
color vBull = color.rgb(33, 191, 160)
color vBear = color.rgb(244, 158, 85)
color vInk = color.rgb(17, 29, 43)
color vMuted = color.rgb(158, 177, 196)
string vBodySize = vSize == "Large" ? size.normal : vSize == "Small" ? size.tiny : size.small
string vSmallSize = vSize == "Large" ? size.small : size.tiny
var array<box> vBoxes = array.new<box>()
var array<line> vLines = array.new<line>()
var array<label> vLabels = array.new<label>()
var table vTable = table.new(position.top_right, 2, 11, bgcolor = na, frame_width = 0)
float vRange = ta.highest(high, 80) - ta.lowest(low, 80)
f_vPrice(float price) =>
na(price) ? "—" : str.tostring(price, format.mintick)
f_vClock(int stamp) =>
na(stamp) ? "—" : str.format_time(stamp, "MM-dd HH:mm", "UTC")
f_vRank(string state) =>
state == "VISIT" ? 4 : state == "READY" ? 3 : state == "RETRACEMENT" ? 2 : state == "DEPARTURE" ? 1 : 0
f_vNext(SdZone zone) =>
zone.state == "DEPARTURE" ? "Wait for a pullback above/below this untouched zone" : zone.state == "RETRACEMENT" ? "Need " + str.tostring(sdRetrMin) + " pullback bars then a close through " + f_vPrice(zone.origin) : zone.state == "READY" ? "Raid " + f_vPrice(zone.front) + " into zone; directional close outside" : zone.state == "VISIT" ? "Need " + (zone.direction == 1 ? "bullish close > " + f_vPrice(zone.upper) : "bearish close < " + f_vPrice(zone.lower)) : zone.reason
f_vLevel(float price, string captionText, color ink, int startBar, int endBar, float tagPrice, string tip) =>
array.push(vLines, line.new(startBar, price, endBar, price, xloc = xloc.bar_index, color = color.new(ink, 15), width = 1))
if f_sdCompare(price, tagPrice) != 0
array.push(vLines, line.new(endBar, price, endBar + 1, tagPrice, xloc = xloc.bar_index, color = color.new(ink, 35), style = line.style_dotted))
array.push(vLabels, label.new(endBar + 1, tagPrice, captionText, xloc = xloc.bar_index, style = label.style_label_left, color = color.new(vInk, 5), textcolor = ink, size = vBodySize, tooltip = tip))
if barstate.islastconfirmedhistory or barstate.isrealtime
for item in vBoxes
box.delete(item)
for item in vLines
line.delete(item)
for item in vLabels
label.delete(item)
array.clear(vBoxes)
array.clear(vLines)
array.clear(vLabels)
table.clear(vTable, 0, 0, 1, 10)
table.set_bgcolor(vTable, na)
table.set_frame_color(vTable, vDesk ? color.new(vMuted, 65) : na)
table.set_frame_width(vTable, 0)
int vReadyCount = 0
int vPendingCount = 0
int vDrawn = 0
SdZone vFocus = SdZone.new()
if array.size(sdZones) > 0
for i = array.size(sdZones) - 1 to 0
SdZone zone = array.get(sdZones, i)
bool live = not f_sdTerminal(zone.state) and zone.state != "SIGNAL"
if live
vPendingCount += 1
if zone.state == "READY" or zone.state == "VISIT"
vReadyCount += 1
if f_vRank(zone.state) > f_vRank(vFocus.state)
vFocus := zone
if vMode != "Off" and vDrawn < vZoneCount
color ink = zone.direction == 1 ? vBull : vBear
int start = vMode == "Inspect" ? zone.baseBar : zone.createdBar
array.push(vBoxes, box.new(start, zone.upper, bar_index + 2, zone.lower, border_color = color.new(ink, zone.state == "VISIT" ? 5 : 45), bgcolor = color.new(ink, zone.state == "VISIT" ? 84 : 93), text = (zone.direction == 1 ? "DEMAND · " : "SUPPLY · ") + zone.state, text_color = color.new(ink, 5), text_size = vSmallSize, text_halign = text.align_left, text_valign = text.align_center))
if not na(zone.front)
array.push(vLines, line.new(zone.phaseBar, zone.front, bar_index + 2, zone.front, color = color.new(ink, 25), style = line.style_dashed))
if vMode == "Inspect"
array.push(vLabels, label.new(zone.createdBar, zone.direction == 1 ? zone.lower : zone.upper, "KNOWN " + f_vClock(zone.createdTime), style = zone.direction == 1 ? label.style_label_up : label.style_label_down, color = color.new(vInk, 20), textcolor = ink, size = size.tiny, tooltip = "Base " + f_vClock(zone.baseTime) + " UTC\nCreated/known " + f_vClock(zone.createdTime) + " UTC\n" + zone.reason + "\n" + f_vNext(zone)))
vDrawn += 1
bool vHasPaper = sdPaper.direction != 0
bool vActive = sdPaper.state == "ACTIVE"
bool vRecent = vHasPaper and not na(sdPaper.endBar) and bar_index - sdPaper.endBar <= 6
if vMode != "Off" and vHasPaper and (vActive or vRecent)
float span = math.max(vRange, math.abs(sdPaper.target - sdPaper.stop))
float gap = math.max(4 * syminfo.mintick, span * (vSize == "Large" ? 0.07 : 0.045))
int end = bar_index + 3
color sideColor = sdPaper.direction == 1 ? vBull : vBear
string frozenTip = "Frozen at " + f_vClock(sdPaper.signalTime) + " UTC\nBase " + f_vClock(sdPaper.baseTime) + " · known " + f_vClock(sdPaper.createdTime) + "\nQuantity " + f_sdNumber(sdPaper.quantity) + " · estimated net at target " + f_sdNumber(sdPaper.netAtTarget) + "R\nPaper reference; this is not a broker fill."
array.push(vBoxes, box.new(sdPaper.signalBar, math.max(sdPaper.entry, sdPaper.target), end, math.min(sdPaper.entry, sdPaper.target), border_color = na, bgcolor = color.new(sideColor, vActive ? 92 : 97)))
array.push(vBoxes, box.new(sdPaper.signalBar, math.max(sdPaper.entry, sdPaper.stop), end, math.min(sdPaper.entry, sdPaper.stop), border_color = na, bgcolor = color.new(color.red, vActive ? 93 : 98)))
float entryTag = sdPaper.entry
float stopTag = sdPaper.direction == 1 ? math.min(sdPaper.stop, entryTag - gap) : math.max(sdPaper.stop, entryTag + gap)
float targetTag = sdPaper.direction == 1 ? math.max(sdPaper.target, entryTag + gap) : math.min(sdPaper.target, entryTag - gap)
f_vLevel(sdPaper.entry, (vActive ? "ENTRY REF " : "ENDED REF ") + f_vPrice(sdPaper.entry), color.white, sdPaper.signalBar, end, entryTag, frozenTip)
f_vLevel(sdPaper.stop, "STOP " + f_vPrice(sdPaper.stop), color.rgb(246, 117, 117), sdPaper.signalBar, end, stopTag, frozenTip)
f_vLevel(sdPaper.target, str.tostring(math.abs(sdPaper.target - sdPaper.entry) / sdPaper.risk, "0.##") + "R TARGET " + f_vPrice(sdPaper.target), sideColor, sdPaper.signalBar, end, targetTag, frozenTip)
if vMode != "Off" and vHistory
for ended in sdHistory
if bar_index - ended.endBar <= 200
float marker = na(ended.exitPrice) ? ended.entry : ended.exitPrice
string caption = ended.reason + (ended.ambiguous ? " · BOTH TOUCHED" : "") + (na(ended.netR) ? "\nUNPRICED" : "\n" + str.tostring(ended.netR, "0.00") + "R est. net")
array.push(vLabels, label.new(ended.endBar, marker, caption, style = ended.direction == 1 ? label.style_label_down : label.style_label_up, color = color.new(vInk, 15), textcolor = ended.reason == "TARGET" ? vBull : vMuted, size = vSmallSize, tooltip = na(ended.exitPrice) ? "Unpriced data gap; marker uses frozen entry for orientation. No exit price or fill inferred." : "Historical paper outcome " + f_vClock(ended.endTime) + " UTC. Price " + f_vPrice(ended.exitPrice) + ". Not a currently tracked broker position."))
if vDesk
// Transparent chart-header inset; body rows retain their own background.
table.cell(vTable, 0, 0, "", height = 3, bgcolor = na)
table.cell(vTable, 1, 0, "", height = 3, bgcolor = na)
string nowText = vActive ? "ACTIVE PAPER " + (sdPaper.direction == 1 ? "LONG" : "SHORT") : sdEntered ? "NEW PAPER REFERENCE" : "NO ACTIVE PAPER PLAN"
table.cell(vTable, 0, 1, "SUPPLY & DEMAND", text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 1, "DESK " + SDD_VERSION + " · 5m", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 2, nowText, text_color = vActive ? sdPaper.direction == 1 ? vBull : vBear : vMuted, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 2, "Closed " + f_vClock(sdLastClosedTime) + " UTC", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 3, "WHY", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 3, sdWhy, text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 4, "NEXT", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 4, vActive ? "Fixed stop/target · later candles only" : vFocus.direction != 0 ? f_vNext(vFocus) : "Wait for an untouched base and strong departure", text_color = color.white, text_size = vBodySize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 5, "READY / BUILDING", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 5, str.tostring(vReadyCount) + " / " + str.tostring(vPendingCount - vReadyCount) + " · shown " + str.tostring(vDrawn), text_color = vMuted, text_size = vSmallSize, tooltip = "All stored live zones are counted; chart draws at most the configured number, latest first.", bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 6, "TREND", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 6, (sdTrend == 1 ? "HH / HL" : sdTrend == -1 ? "LH / LL" : "MIXED / WARMUP") + (sdTrendGate ? " · gate ON" : " · context only"), text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 7, "PLAN / COSTS", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 7, vActive ? "Qty " + f_sdNumber(sdPaper.quantity) + " · net at TP " + str.tostring(sdPaper.netAtTarget, "0.00") + "R" : "Signal-close reference · no entry pending", text_color = vActive and f_sdSign(sdPaper.netAtTarget) <= 0 ? color.orange : vMuted, text_size = vSmallSize, tooltip = "Indicator cash quantities use symbol currency. Estimated net includes the declared fee/slippage allowance. Nonpositive net-at-TP remains disclosed; it is not profitability.", bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 8, "ALERT", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 8, SD_IS_LAB ? "Any alert() call = paper JSON; order fills = broker" : "Create alert → Any alert() function call", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
if vMode == "Inspect"
table.cell(vTable, 0, 9, "CONDITIONS / BLOCKS", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 9, str.tostring(sdConditions) + " / " + str.tostring(sdBlocked) + " · retired " + str.tostring(sdRetired), text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 0, 10, "ORIGINAL SUBSET", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
table.cell(vTable, 1, 10, "Closed bars · no flip-entry or private parity", text_color = vMuted, text_size = vSmallSize, bgcolor = color.new(vInk, 4))
Release identity and file hashes · MIT licence
Pine Editor installation walkthroughThe rules, one question at a time.
Open the detail you need.
How does a zone become ready?
A directional departure after an opposite candle creates the zone at its first close. Default bounds use the base's full range, extended only by a lower departure wick for demand or a higher one for supply. The edge facing the departed price is proximal; the far edge is distal. At the first pullback candle, the move must have reached at least 0.8 ATR beyond proximal, using ATR frozen at the zone's birth.
Its pre-retracement extreme becomes a fixed break level. At least two retracement bars must finish before a later strict close breaks that level. The breakout candle cannot count as the second retracement. Front liquidity must remain outside the zone and in the nearer half of the departure.
These thresholds and the optional FX wick adjustment are original formalizations. Five-minute standard candles are required; other chart types do not produce valid conditions.
When does the visit become an entry?
After readiness, price must pass front liquidity and actually intersect the zone. Demand then needs a bullish close strictly above proximal; supply needs a bearish close strictly below. An opposite-color rejecting candle can wait, but a close on the adverse edge invalidates first.
The deepest wick is tracked from first visit through confirmation. Visit age zero through three is allowed; age four expires. A zone has one condition, even if plan admission is blocked. There is no automatic re-entry.
Where do the fixed stop and 4R come from?
In the lesson, entry 108 minus stop 101.75 gives initial risk 6.25, called 1R. Four times that distance is 25, so the 4R target is 133. This is distance before costs, not a chance of winning.
Entry is the confirming close reference. Stop sits beyond the deepest visit wick by at least one tick; optional ATR padding increases that distance. Stop and target round outward to the instrument's tick grid. Default target is four initial risk distances away.
A reference plan cannot exit on its entry candle. On later bars, an open already beyond a reference uses that open. Otherwise touching both stop and target records an ambiguous STOP. A 48-bar hold deadline checks ordinary outcomes before a close-based time exit. No trailing, breakeven move or partial profit is assumed.
Projected net-at-target includes estimated costs. Nonpositive projection does not automatically block admission in this version. It is a scenario calculation, not an expectation or probability.
Why can a condition be blocked?
Only one paper plan is active. The optional chart-local trend filter runs first: long needs HH+HL, short LH+LL, from two-left/two-right confirmed swings. It defaults off; neutral or unavailable structure blocks when enabled. Opposite trend-eligible conditions then conflict before geometry selection. Same-side conditions choose the smallest valid risk distance, then the oldest zone.
Bad metadata, zero rounded quantity or unusable risk also blocks admission. A plan that exits on this bar cannot be replaced on the same bar. No condition is recycled after it is consumed.
What keeps old or incomplete zones out?
A zone touched after departure but before readiness is invalidated. Total age 120 is allowed; age 121 expires. Retracement age 20 is allowed; age 21 expires before a breakout, measured from the first retracement bar. Malformed observed OHLC clears pending setups and ends an active reference as unpriced DATA GAP.
Ages count observed bars. Missing timestamps do not create invented candles. The desk retains at most 32 zones: it removes the oldest ended zone first, or the oldest live zone if none has ended. It keeps six ended paper plans. Inputs and loaded history can change a recalculated result; display controls do not change the engine.
What does every setting change?
All 21 indicator inputs are also in the Lab, with the same defaults. The Lab adds two sizing inputs. The TradingView names below help you find each setting.
Build and time the setup
These rules use completed, standard five-minute candles. ATR describes recent price movement; the departure test freezes its value when the zone is born.
- How far price must leave the zone
- Default: 0.8 × birth ATR
- Checked when the first pullback starts. A weaker departure is rejected.
- TradingView: Minimum departure · ATR
- Pullback candles needed before the break
- Default: 2 candles
- The breakout candle does not count. Pullback candle colors can mix.
- TradingView: Minimum retracement bars
- How close the pullback stays to the zone
- Default: 0.5 · nearer half
- The pullback extreme must stay outside the zone, within half the distance to the fixed departure extreme.
- TradingView: Front liquidity · maximum setup fraction
- Use the optional zone-edge variant
- Default: Off
- When the base wick protrudes past the departure candle, this uses the base open for the near edge. It never detects asset type automatically.
- TradingView: FX accuracy drawing
- How long the pullback may wait for a break
- Default: 20 candles
- Count from the first pullback candle: age 20 is allowed; age 21 expires before the break check.
- TradingView: Retracement deadline · bars
- How long a setup can remain available
- Default: 120 candles
- Count from zone creation: age 120 is allowed; age 121 expires before a condition can confirm.
- TradingView: Zone lifetime · bars
- How long a visit may wait for confirmation
- Default: Ages 0–3
- First touch is age 0. A directional close can confirm through age 3; age 4 expires.
- TradingView: Entry visit deadline · bars
- Also require matching swing direction
- Default: Off
- Uses confirmed two-left/two-right swings on this chart. When enabled, mixed or incomplete structure blocks plan admission.
- TradingView: Require chart-local HH/HL or LH/LL
Set the paper reference
Paper sizing uses the symbol’s currency and rounds quantity down. These estimates describe the indicator’s reference plan; the Lab owns separate broker sizing and costs.
- Extra space beyond the deepest visit wick
- Default: 0 × ATR · minimum 1 tick
- Uses ATR at confirmation. Even at zero, the stop sits at least one tick beyond the wick and rounds outward.
- TradingView: Stop pad · ATR (minimum one tick)
- Target distance compared with initial risk
- Default: 4R
- Four times the entry-to-stop distance. Outward rounding can make actual R slightly larger. This distance is not a success rate.
- TradingView: Target · R
- How long a reference may stay active
- Default: 48 candles
- At the deadline, ordinary stop/target checks run first, then a close-based time exit. The Lab starts its own clock at the fill.
- TradingView: Maximum paper hold · bars
- Paper risk budget before costs
- Default: 100 symbol-currency units
- Quantity depends on the rounded stop distance and instrument metadata. Costs or price gaps can increase the final loss.
- TradingView: Cash risk budget · symbol currency
- Limit the paper position’s entry value
- Default: 90,000 symbol-currency units
- Can reduce quantity below the risk budget. If minimum quantity cannot be funded, no plan is admitted.
- TradingView: Notional cap · symbol currency
- Paper commission estimate
- Default: 0.05% each side
- Applied to entry and exit notionals in reference accounting. Set broker-test commission separately in Strategy Properties.
- TradingView: Estimated commission · % per side
- Paper slippage estimate
- Default: 1 tick each side
- Subtracts an estimated cost without moving paper price lines. The Lab’s fill slippage is a separate Property.
- TradingView: Estimated slippage · ticks per side
Choose the alert message
Creating or changing an input does not create an alert. Reference messages and Lab order-fill messages are different streams.
- Enable structured reference messages
- Default: On
- Choose Any alert() function call when creating the alert. Both scripts can emit paper entry and final-outcome messages.
- TradingView: JSON alert events
Lab-only sizing inputs
The Lab keeps the shared paper inputs, then adds these two broker sizing controls. It converts account-currency amounts to the symbol’s currency before rounding quantity.
- Broker-test risk budget before costs
- Default: 100 account-currency units
- Replaces the paper cash budget for broker sizing. It does not change the indicator’s paper quantity.
- TradingView: Lab risk budget · account currency
- Limit entry value using current Lab equity
- Default: 90% of equity
- Replaces the paper notional cap for broker sizing. The remaining buffer does not rule out margin calls.
- TradingView: Lab maximum equity notional · %
Choose what the chart shows
Display controls do not change the engine. The desk can remain visible when Chart view is Off.
- Amount of chart detail
- Default: Clean
- Off hides drawings. Clean starts boxes when known. Inspect also shows historical base anchors, creation clocks and extra counts.
- TradingView: Chart view
- Show the explanation panel
- Default: On
- Lists the current waiting reason and next requirement. Its focus favors Visit, then Ready, Pullback and Departure.
- TradingView: Show WHY / NEXT desk
- Maximum live boxes drawn
- Default: 4 zones
- Draws the newest live zones first; counts include stored live zones that are not drawn. Available when Chart view is not Off.
- TradingView: Visible live zones
- Show historical outcome markers
- Default: On
- Uses up to six retained outcomes, each within 200 candles. The latest ended price lines can remain for six candles separately.
- TradingView: Ended paper markers
- Size of labels and desk text
- Default: Normal
- Choose Small, Normal or Large. It changes readability, not setup qualification.
- TradingView: Text size
Lab Strategy Properties
These are the shipped broker-test defaults, separate from script Inputs. Saved Properties can override them. Changing paper fee, slippage or sizing inputs does not replace the Lab settings below.
- Initial capital
- Default: 100,000
- Starting balance for the broker test. Check the account currency in Properties.
- Commission method
- Default: Percentage
- Broker commission is a percentage of order value.
- Broker commission
- Default: 0.05% each side
- Separate from the paper commission input.
- Broker slippage
- Default: 1 tick
- Used by TradingView’s fill emulator; separate from paper cost accounting.
- Add entries to an existing position
- Default: 0
- No stacking of entries in the same direction.
- Long-position margin
- Default: 100%
- Full collateral is required by this default broker model.
- Short-position margin
- Default: 100%
- Full collateral is required by this default broker model.
- Process orders on the closing tick
- Default: Off
- A confirmed market entry normally fills at the next available open.
- Recalculate on every tick
- Default: Off
- The supplied broker test calculates on completed candles.
- Extra calculation after an order fill
- Default: Off
- The supplied strategy does not request a separate recalculation after fills.
How is the Strategy Lab different?
The Lab submits its bracket with the signal and normally fills at the next available open. It owns its position and deadline independently of paper outcomes. A partial margin exit keeps the remaining position's bracket.
Defaults include 100,000 initial capital, fixed risk 100, 90-percent equity notional cap, 0.05-percent commission per side and one tick of slippage. Actual broker fills, currency conversion, gaps and costs can differ from chart references. Configure Properties for the instrument and inspect complete outcomes.
Which alert should I use?
Choose Paper entry reference when you want an accepted entry, rather than Zone condition confirmed for every qualifying condition. Paper reference ended is the separate final-outcome condition. Any alert() function call provides the structured lifecycle message. Recreate alerts after source, symbol, timeframe or input changes.
Indicator reference alerts are not broker fills. Lab order-fill notifications are a different stream. This page creates no alert and proves no notification delivery.
What has this established?
The interactive lesson explains a stated hypothesis using fictional candles. It does not measure a trading edge. The download record pins the exact source and actual chart images. Compilation, synthetic cases, real chart checks and broker reconciliation are distinct software checks; none establishes an economic advantage.
The untouched MNQ Strategy Lab sample in the release checks lost money after declared costs. That sample does not establish future performance; it is a reason to inspect losing trades as well as the entry logic. No private indicator parity, success probability or automatic profitability is claimed.
Every change, dated
What shipped and what changed in Supply & Demand 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.
Supply & Demand Desk v1.0.0 Shipped
Checks a return to a price area before planning entry.
Something to change in Supply & Demand Desk?
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.
Explore the other free tools · Compare Sweep Desk's inversion-close model