CAMARILLA DESK v2.1.0

Watch the level.Wait for the close.

Read the pivot map and session context. Follow each confirmed episode with its original entry, stop and target.

Seven visible levels · fourteen named plays · full source · no signup

ILLUSTRATED EXAMPLEHA · L3 bounce
H3 · destination107.75Closed above103L3 · frozen level102.25
1 Touch L32 Close above3 References fixed
Supplied H110/L100/C105, prior H107/L97/C102, ATR(14)10: Higher, Similar, valid opening, no bad OPEN. This illustration calculates neither sessions nor ATR. ACTIVE is not an order.
IN THIS MANUAL · Try, understand, install

What is Camarilla Desk?

Camarilla Desk is a free TradingView indicator that builds a frozen session map from completed price data and tracks fourteen play hypotheses. It separates the source used for levels from the session clock used for decisions. Qualifying episodes receive fixed entry, stop and target references; these observations are not confirmed order fills.

Which Camarilla levels does the default map show?

The default map shows H6, H4, H3, previous close, L3, L4 and L6. Levels come from the selected completed source and freeze at the decision opening; they are Camarilla references, not floor pivots or CPR. See the map formulas

Why does Camarilla Desk show DATA WAIT or OPEN UNKNOWN?

The map needs complete source coverage, a valid ATR basis and a known decision opening. Missing boundaries, stale sources or incomplete sessions can block interpretation. Check the resolved source and clock before treating a waiting row as a setup. Check source and session coverage

Does a Higher Camarilla play always mean a long trade?

No. H names the Higher context, while SIDE identifies the direction of the hypothesis. Some Higher plays point short. Read the opening gate, approach region and confirmation rule together; the context label alone does not define an entry. Read the common play questions

00 · WHY THIS TOOL EXISTS

Give today's candles a map.

Camarilla starts with earlier prices and calculates places to watch. This desk explains how price behaves at those places, then checks whether a named play qualifies. First, see where the lines come from.

100105110Earlier sourceWait for source close
High so far 110Low so far 100Source open
Illustrative prices · completed source and known decision opening supplied · three map levels shown · no activation or fill inferred.
00 · 1 / 6

Start with a completed source.

The map comes from an earlier trading period. Here, its candles establish a high and low. While that period is still open, its final close is not known.

01 · START WITH ONE PLAY
CHANGE THE CANDLE. SEE WHY.

Does this candle complete the play?

Change the close and wick. See the rule respond while the Camarilla map stays still.

Earlier candles → test candleTest close 103
Destination107.75Activation reference103L3 · watch here102.25Stop reference99.25120111.25102.593.7585
Reference riskReference distanceDrag the blue close ↕

Illustrated prices · four closed candles · fixed scale while you drag.

Watch the same candle change
103
1.25
How the calculation works & more controls
105
102
10
10
1 R

Opening, source and ATR edits restart this hypothetical replay. The earlier range is centred on its close. Source completeness and the ATR scalar are supplied assumptions: this lesson does not reconstruct a full session feed, ATR seed or missing-session recovery.

ATR Market default: observed RTH for futures unless the decision clock is Exchange day; Provider daily otherwise. This completed ATR(14) is supplied, not calculated or matched to Premium.

Candidate passes context, width and inclusive opening gates without a bad-OPEN latch. Approaching adds the strict price region. Neither is ACTIVE. Similar includes the two-percent width tolerance; ATR zero is unavailable. This lesson fixes structural risk, SMA off and one episode per session.

Explore HA/HB/HE here; all fourteen rules appear below. Recovery accepts either candle-body direction. Stops round outward and targets toward entry. Outcomes begin after activation, with opening gaps then stop-first touches. No broker fill is simulated.

Illustrative inputs · changing a slider recalculates this example · no market feed

THE RULES ARE OPEN

Read each opening gate, setup, confirming close and ending.

Read the complete rules →
02 · READ THE REAL CHART

Map. Context. Then the confirmed episode.

Default Top Left Ribbon: source, range, width. Trading Dark keeps seven levels visible. Open HUD or Inspector for detail.

RIBBON

Source · range · width

Read all three before following a play. SOURCE and ATR basis remain separately inspectable.

TRACKED

The episode stays on its candles

Entry arrows, shaded reference regions and named outcomes identify the episode. Ended geometry stops at its recorded end, not a new entry.

INSPECTOR

Selection changes explanation

Inspect any code’s prerequisites and NEXT. Manual selection cannot switch the tracked plan. Candidate, approach, armed and active remain distinct.

LAST CLOSED

Time belongs beside price

The detailed HUD dates the committed close and map. Display changes cannot change qualification or reference prices.

Actual Camarilla Desk v2.1.0 on ETHUSDT 15m: seven labelled levels, a Daily/Higher/Wide Ribbon with no qualifying setup, and ended HB history.
Actual ETHUSDT 15m · v2.1.0 defaults · 7 September 2026. Daily/Higher/Wide; no qualifying setup. Daily ATR(14) 94.85; closed 15:00 UTC. HB entry/STOP is ended history. Unmodified capture.
Labelled synthetic Camarilla Desk v2.1 UI example: current LA approaching, two separate ended LA episodes, and the full seven-level map.
Synthetic UI example, not a market signal. The retained banner labels invented LA states: approaching now, separate episodes ended TARGET and STOP. Each keeps its own markers and references.
03 · ADD THE TOOL

Indicator first. Strategy Lab separately.

No signup; complete MIT sources. Use the installation walkthrough. Save each source separately.

  1. Open Pine Editor on a standard TradingView candle chart.
  2. Paste the indicator source, then Save and Add to chart.
  3. Read the source and opening status before following a play. Use Strategy Lab separately when inspecting simulated fills and costs.

Camarilla Desk indicator

Seven-level map, context ribbon, explicit ATR basis and frozen reference episodes.

↓ Download camarilla-desk.pine

v2.1.0 · MIT · SHA256 93e60db80ed8a14c475e5812404a0f50016edae741d122202b33db86b620664b

camarilla-desk.pine · Pine v6 · MIT
Read the complete source
//@version=6
// CAMARILLA DESK 2.1.0 · MIT · Original interpretation of public Camarilla concepts.
// CARD: market/timeframe — standard time-based intraday bars, 1m through 1h.
// CARD: idea — confirmed play states, frozen prior-source map and reference plans.
// ACTIVE denotes an observed hypothesis, not a filled order or a validated edge.
// See docs/engine-contract.md for exact rules and docs/build-report.md for evidence.

indicator("Camarilla Desk · Open Source", "Cam Desk", overlay = true, behind_chart = false, max_lines_count = 220, max_labels_count = 160, max_boxes_count = 50, max_bars_back = 3000)
const string CAM_VERSION = "2.1.0"
const bool CAM_IS_LAB = false

// PUBLIC LITE BASELINE PREREQUISITES · original closed-bar execution · no Premium parity.
string CAM_FORMULA = "expanded6-v1"
string camSourceGroup = "Source and decision clock"
string sourceMode = input.string("Market default", "Pivot source", options = ["Market default", "Auto RTH/ETH", "RTH", "ETH", "Exchange daily", "Selected session"], group = camSourceGroup, display = display.none, tooltip = "Market default explicitly chooses RTH for non-crypto and Exchange daily for crypto. RTH/ETH require complete observed one-minute windows. Optional Auto RTH/ETH requires complete pre/postmarket coverage and freezes at the opening; it never silently falls back. Exchange daily remains a provider-defined daily/settlement source, never cash RTH.")
string decisionMode = input.string("Auto", "Decision window", options = ["Auto", "US cash", "Exchange day", "Custom"], group = camSourceGroup, display = display.none, tooltip = "Auto uses Exchange day for crypto and US cash 09:30–16:00 New York for other assets. Set Custom for another exchange or a futures cash window.")
string customSession = input.session("0930-1600", "Custom decision interval", group = camSourceGroup, active = decisionMode == "Custom", display = display.none)
string customTimezone = input.string("America/New_York", "Custom IANA time zone", group = camSourceGroup, active = decisionMode == "Custom", display = display.none, tooltip = "Use an IANA region such as America/New_York. True overnight intervals use the ending trading weekday. Equal start/end is 24 hours and uses the starting calendar weekday.")
bool customWeekdays = input.bool(true, "Custom weekdays only", group = camSourceGroup, active = decisionMode == "Custom", display = display.none)
string regularSession = input.session("0930-1600", "RTH source interval", group = camSourceGroup, display = display.none)
string regularTimezone = input.string("America/New_York", "RTH source IANA zone", group = camSourceGroup, display = display.none, tooltip = "Cash-session definition, including futures emulation. It is independent from the exchange's definition of regular trading.")
string extendedClock = input.string("Auto", "ETH source clock", options = ["Auto", "Custom"], group = camSourceGroup, display = display.none, tooltip = "Auto: US equities 04:00–20:00 New York, futures 18:00–17:00 New York, crypto exchange day. Other venues require a Custom source clock.")
string extendedSession = input.session("1800-1700", "Custom ETH interval", group = camSourceGroup, active = extendedClock == "Custom", display = display.none)
string extendedTimezone = input.string("America/New_York", "Custom ETH IANA zone", group = camSourceGroup, active = extendedClock == "Custom", display = display.none)
string closeMode = input.string("Match source", "Pivot close selection", options = ["Match source", "RTH", "ETH", "Exchange daily"], group = camSourceGroup, display = display.none)
float switchingTolerance = input.float(2, "Auto excursion · % selected ATR", minval = 0, maxval = 100, step = 0.1, group = camSourceGroup, active = sourceMode == "Auto RTH/ETH", display = display.none)
string camPrerequisiteGroup = "Public Lite baseline · confirmed prerequisites"
float rangeThresholdPct = input.float(2, "Neutral close change · % selected ATR", minval = 0, maxval = 100, step = 0.1, group = camPrerequisiteGroup, display = display.none)
float widthThresholdPct = input.float(2, "Similar width tolerance · %", minval = 0, maxval = 99, step = 0.1, group = camPrerequisiteGroup, display = display.none)
bool allowNeutral = input.bool(false, "Allow both directions in Neutral", group = camPrerequisiteGroup, display = display.none)
bool smaGate = input.bool(false, "Original SMA close confirmation", group = camPrerequisiteGroup, display = display.none, tooltip = "Optional original filter: confirmed long closes above the chart SMA, short below. This does not reproduce an undisclosed Premium SMA trigger.")
int smaLength = input.int(20, "Chart SMA length", minval = 2, maxval = 300, group = camPrerequisiteGroup, active = smaGate, display = display.none)
string camPlayGroup = "Original structural hypotheses"
bool enableA = input.bool(true, "A · Third-band traverse", group = camPlayGroup, display = display.none)
bool enableB = input.bool(true, "B · Interior breakout and retest", group = camPlayGroup, display = display.none)
bool enableC = input.bool(true, "C · Opening continuation", group = camPlayGroup, display = display.none)
bool enableD = input.bool(true, "D · Context-opposed continuation", group = camPlayGroup, display = display.none)
bool enableE = input.bool(true, "E · Fourth-tier failure", group = camPlayGroup, display = display.none)
bool enableF = input.bool(true, "F · Outer-tier rejection", group = camPlayGroup, display = display.none)
bool enableG = input.bool(false, "G · Opposite-side outer rejection", group = camPlayGroup, display = display.none)
int setupDeadline = input.int(12, "Setup deadline · completed bars", minval = 1, maxval = 200, group = camPlayGroup, display = display.none, tooltip = "Confirmation is allowed through age 12 at the default. The following closed bar expires a pending setup.")
int activeDeadline = input.int(120, "Active deadline · completed bars", minval = 1, maxval = 1000, group = camPlayGroup, display = display.none, tooltip = "Observed later-bar outcomes precede timeout. Default timeout occurs on the 120th later completed chart bar.")
float minRewardR = input.float(1.0, "Minimum structural reward / risk", minval = 0.1, maxval = 10, step = 0.1, group = camPlayGroup, display = display.none)
int stopBufferTicks = input.int(1, "Stop buffer · minimum ticks", minval = 0, maxval = 100, group = camPlayGroup, display = display.none)
string stopModel = input.string("Structural", "Reference stop model", options = ["Structural", "ATR"], group = camPlayGroup, display = display.none, tooltip = "ATR is an original alternative measured from activation close using the frozen selected ATR basis. Neither option claims a vendor preset.")
float stopAtrMultiple = input.float(0.25, "ATR stop multiple", minval = 0.01, maxval = 10, step = 0.05, group = camPlayGroup, active = stopModel == "ATR", display = display.none)
string targetModel = input.string("Structural", "Reference target model", options = ["Structural", "ATR"], group = camPlayGroup, display = display.none)
float targetAtrMultiple = input.float(0.5, "ATR target multiple", minval = 0.01, maxval = 20, step = 0.05, group = camPlayGroup, active = targetModel == "ATR", display = display.none)
float maxTargetAtr = input.float(0, "Maximum target distance · selected ATR (0 off)", minval = 0, maxval = 20, step = 0.1, group = camPlayGroup, display = display.none, tooltip = "Optional original reachability guard; rejects a too-distant target without silently moving it.")
bool allowRearm = input.bool(false, "Allow another episode after ending", group = camPlayGroup, display = display.none)
int cooldownBars = input.int(3, "Rearm cooldown · completed bars", minval = 1, maxval = 200, group = camPlayGroup, active = allowRearm, display = display.none)
int maxEpisodes = input.int(1, "Maximum episodes per code / session", minval = 1, maxval = 10, group = camPlayGroup, active = allowRearm, display = display.none)
string camAlertGroup = "Aggregate JSON alerts"
bool alertActivation = input.bool(true, "Include activation events", group = camAlertGroup, display = display.none)
bool alertEnding = input.bool(true, "Include ending events", group = camAlertGroup, display = display.none)
bool alertArmed = input.bool(false, "Include armed events", group = camAlertGroup, display = display.none)

// Appended to preserve the ordering of all previous core inputs.
string atrBasisMode = input.string("Market default", "ATR calculation basis", options = ["Market default", "Provider daily", "RTH session"], group = camSourceGroup, display = display.none, tooltip = "Market default uses completed observed RTH-session Wilder ATR(14) for futures with a non-Exchange-day decision clock, and completed provider daily ATR(14) otherwise. RTH uses the configured RTH interval and zone; seed = mean of 14 valid session true ranges, then Wilder smoothing. An observed incomplete/malformed session resets the seed. Entirely absent days are skipped: this is observed-session coverage, not an exchange holiday calendar. No fallback. The chosen basis is frozen at the decision opening and drives range, Auto excursion and optional ATR risk/target checks.")

type CamLevels
    bool valid = false
    bool outerValid = false
    bool complete = false
    float h = na
    float l = na
    float c = na
    float h2 = na
    float l2 = na
    float h3 = na
    float l3 = na
    float h4 = na
    float l4 = na
    float h5 = na
    float l5 = na
    float h6 = na
    float l6 = na
    int sourceStart = na
    int sourceEnd = na
    string reason = "Source unavailable"

type CamWindowBook
    CamLevels latest
    CamLevels prior
    int start = na
    int finish = na
    int lastClose = na
    float h = na
    float l = na
    float c = na
    int samples = 0
    bool complete = false
    bool finished = true
    string reason = "No observed interval"

// Constant-size Wilder state, updated only when an observed RTH window finishes.
type CamAtrBook
    int samples = 0
    float seedSum = 0
    float previousClose = na
    float value = na
    int sourceStart = na
    int sourceEnd = na
    int observedEnd = na

type CamSourcePacket
    CamLevels current
    CamLevels prior
    bool ready = false
    bool openKnown = false
    float opening = na
    int openingTime = na
    int sessionStart = na
    int sessionEnd = na
    int cutoff = na
    string clockLabel = ""
    string sourceKind = ""
    string closeKind = ""
    int closeSourceEnd = na
    int priorCloseSourceEnd = na
    float atr = na
    int atrStamp = na
    int atrBasisCode = 0
    int atrSamples = 0
    int atrStart = na
    bool atrReady = false
    float excursion = na
    float excursionThreshold = na
    string reason = "Waiting for an observed opening in the 1m source context"
    string selectionReason = ""
    string coverage = ""
    int sourceCode = 0
    int closeCode = 0
    int reasonCode = 0
    int selectionCode = 0
    int rthSamples = 0
    int ethSamples = 0
    int selectedSamples = 0
    bool rthComplete = false
    bool ethComplete = false
    bool selectedComplete = false

type Play
    string code = ""
    string name = ""
    string family = ""
    int biasRequired = 0
    int dir = 0
    string state = "DATA WAIT"
    string reason = "Waiting for a decision session"
    string next = "Wait for a fully observed opening"
    string invalidation = "No setup observed"
    bool eligible = false
    bool candidate = false
    bool approaching = false
    bool rangeOk = false
    bool widthOk = false
    bool openOk = false
    bool regionOk = false
    bool blocked = false
    bool smaOk = true
    int episode = 0
    float triggerPrice = na
    string triggerOperator = ""
    string sourceKind = ""
    string closeKind = ""
    string stopMode = "Structural"
    string targetMode = "Structural"
    float atrUsed = na
    int atrStamp = na
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na
    int sourceCutoff = na
    int closeSourceEnd = na
    float level = na
    int setupBar = na
    int setupTime = na
    float armedHigh = na
    float armedLow = na
    int activationBar = na
    int activationTime = na
    int activationSeq = na
    float entry = na
    float stop = na
    float target = na
    float effectiveR = na
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    int endBar = na
    int endTime = na
    float outcomePrice = na
    bool ambiguous = false

type CamPlanSnapshot
    string code = ""
    string name = ""
    int dir = 0
    int episode = 0
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    string sourceKind = ""
    string closeKind = ""
    int setupTime = na
    float triggerPrice = na
    string triggerOperator = ""
    float atrUsed = na
    string stopMode = ""
    string targetMode = ""
    int activationBar = na
    int activationTime = na
    float entry = na
    float stop = na
    float target = na
    float effectiveR = na
    int endBar = na
    int endTime = na
    float outcomePrice = na
    string state = ""
    string reason = ""
    bool ambiguous = false
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na
    int atrStamp = na

type CamEvent
    int seq = 0
    string code = ""
    string kind = ""
    string reason = ""
    int dir = 0
    int eventTime = na
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    float price = na
    float entry = na
    float stop = na
    float target = na
    bool ambiguous = false
    int episode = 0
    string sourceKind = ""
    string closeKind = ""
    int atrStamp = na
    int sourceCutoff = na
    int closeSourceEnd = na
    float atrUsed = na
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na

var array<Play> plays = array.new<Play>()
var array<CamPlanSnapshot> camPlans = array.new<CamPlanSnapshot>()
var array<CamEvent> camEvents = array.new<CamEvent>()
var array<CamEvent> camBarEvents = array.new<CamEvent>()
var array<int> camEventCounter = array.new_int(1, 0)

// Comparison normalization is relative to the actual operand scale, never to 1.
// Self-normalized sign avoids Pine comparison operators' nine-decimal rounding.
f_camSign(float value) =>
    na(value) ? 0 : int(nz(value / math.abs(value), 0))

f_camCompare(float first, float rhs) =>
    float difference = first - rhs
    float tolerance = 4.0 * 2.220446049250313e-16 * math.max(math.abs(first), math.abs(rhs))
    na(difference) ? 0 : f_camSign(math.abs(difference) - tolerance) <= 0 ? 0 : f_camSign(difference)

f_camNumber(float value) =>
    string result = "null"
    if not na(value)
        if f_camSign(value) == 0
            result := "0"
        else
            int exponent = int(math.floor(math.log10(math.abs(value))))
            result := exponent < -6 or exponent > 12 ? str.tostring(value / math.pow(10, exponent), "0.################") + "e" + str.tostring(exponent) : str.tostring(value, "0.################")
    result

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

f_camExact(float value) =>
    na(value) ? "—" : f_camNumber(value)

// Narrative rounding suppresses binary float tails; raw map/plan values stay unchanged.
f_camReadable(float value) =>
    na(value) ? "—" : f_camCompare(math.abs(value), 0.000001) < 0 ? f_camNumber(value) : str.tostring(value, "0.##########")

f_camTerminal(string state) =>
    state == "REJECTED" or state == "INVALIDATED" or state == "TARGET" or state == "STOP" or state == "EXPIRED"

f_camTouched(float level, float barHigh, float barLow) =>
    not na(level) and not na(barLow) and not na(barHigh) and f_camCompare(barLow, level) <= 0 and f_camCompare(barHigh, level) >= 0

// A flat positive bar is valid. Missing or incoherent constituent prices are not.
f_camBarValid(float barOpen, float barHigh, float barLow, float barClose) =>
    not na(barOpen) and not na(barHigh) and not na(barLow) and not na(barClose) and f_camSign(barLow) > 0 and f_camCompare(barHigh, math.max(barOpen, barClose)) >= 0 and f_camCompare(barLow, math.min(barOpen, barClose)) <= 0

f_camLevels(float srcHigh, float srcLow, float srcClose, int srcStart, int srcEnd, bool complete, string incompleteReason, bool requireCloseInside = true) =>
    CamLevels m = CamLevels.new()
    m.h := srcHigh
    m.l := srcLow
    m.c := srcClose
    m.sourceStart := srcStart
    m.sourceEnd := srcEnd
    bool independentValid = not na(srcHigh) and not na(srcLow) and not na(srcClose) and f_camSign(srcLow) > 0 and f_camSign(srcClose) > 0 and f_camCompare(srcHigh, srcLow) >= 0
    m.complete := complete and (requireCloseInside ? f_camBarValid(srcClose, srcHigh, srcLow, srcClose) : independentValid)
    bool coherent = m.complete and f_camCompare(srcHigh, srcLow) > 0
    if complete and coherent
        float width = srcHigh - srcLow
        m.h2 := srcClose + 1.1 * width / 6
        m.l2 := srcClose - 1.1 * width / 6
        m.h3 := srcClose + 1.1 * width / 4
        m.l3 := srcClose - 1.1 * width / 4
        m.h4 := srcClose + 1.1 * width / 2
        m.l4 := srcClose - 1.1 * width / 2
        m.h5 := m.h4 + 1.168 * (m.h4 - m.h3)
        m.l5 := m.l4 - 1.168 * (m.l3 - m.l4)
        m.h6 := srcHigh / srcLow * srcClose
        m.l6 := 2 * srcClose - m.h6
        m.valid := f_camCompare(m.h4, m.h3) > 0 and f_camCompare(m.h3, m.l3) > 0 and f_camCompare(m.l3, m.l4) > 0
        m.outerValid := m.valid and not na(m.h6) and not na(m.l6) and f_camSign(m.l6) > 0 and f_camCompare(m.h6, m.h4) > 0 and f_camCompare(m.l6, m.l4) < 0
        m.reason := not m.valid ? "Collapsed inner geometry" : not m.outerValid ? "Outer ratio geometry unavailable" : "Complete coherent source"
    else
        m.reason := not complete ? incompleteReason : "Missing, nonpositive or incoherent source H/L/C"
    m

f_camRelationship(CamLevels current, CamLevels previous) =>
    string relation = "Unavailable"
    int bias = 0
    float ratio = na
    string widthLabel = "Unavailable"
    if current.valid and previous.valid
        float a = previous.l3
        float b = previous.h3
        float c = current.l3
        float d = current.h3
        int lowerCompare = f_camCompare(c, a)
        int upperCompare = f_camCompare(d, b)
        bias := lowerCompare > 0 and upperCompare > 0 ? 1 : lowerCompare < 0 and upperCompare < 0 ? -1 : 0
        relation := lowerCompare == 0 and upperCompare == 0 ? "Unchanged" : f_camCompare(c, b) > 0 ? "Higher separated" : f_camCompare(d, a) < 0 ? "Lower separated" : lowerCompare >= 0 and upperCompare <= 0 ? "Inside band" : lowerCompare <= 0 and upperCompare >= 0 ? "Outside band" : lowerCompare > 0 and upperCompare > 0 ? "Overlapping higher" : "Overlapping lower"
        ratio := (d - c) / (b - a)
        int widthCompare = f_camCompare(d + a, c + b)
        widthLabel := widthCompare > 0 ? "Wide" : widthCompare < 0 ? "Narrow" : "Equal"
    [relation, bias, ratio, widthLabel]

// Independent transcription of public Lite 2.0 prerequisites, applied on closed bars.
// Original episode triggers below are a separate layer; these flags are not entries.
f_camPrerequisites(string code, CamLevels m, CamLevels prior, float atr, float opening, float barOpen, float barClose, bool priorBlocked, float rangePct, float widthPct, bool neutralAllowed) =>
    string family = str.substring(code, 1, 2)
    bool higher = str.substring(code, 0, 1) == "H"
    bool innerReady = m.valid and prior.valid
    bool atrReady = not na(atr) and f_camSign(atr) > 0
    bool observationReady = not na(opening) and not na(barOpen) and not na(barClose)
    float threshold = atr * rangePct / 100
    float delta = m.c - prior.c
    int bias = innerReady and atrReady ? (f_camCompare(m.c, prior.c + threshold) > 0 ? 1 : f_camCompare(m.c, prior.c - threshold) < 0 ? -1 : 0) : 0
    float width = m.h3 - m.l3
    float previousWidth = prior.h3 - prior.l3
    float ratio = innerReady and f_camSign(previousWidth) > 0 ? width / previousWidth : na
    // Cross-sums retain the original endpoint scale when subtracting nearby prices.
    float upperFactor = 1 + widthPct / 100
    float lowerFactor = 1 - widthPct / 100
    string widthLabel = na(ratio) ? "Unavailable" : f_camCompare(m.h3 + prior.l3 * upperFactor, m.l3 + prior.h3 * upperFactor) > 0 ? "Wide" : f_camCompare(m.h3 + prior.l3 * lowerFactor, m.l3 + prior.h3 * lowerFactor) < 0 ? "Narrow" : "Similar"
    bool rangeOk = innerReady and atrReady and (bias == (higher ? 1 : -1) or bias == 0 and neutralAllowed)
    bool widthOk = not na(ratio) and (family == "F" or family == "G" or widthLabel == "Similar" or (family == "A" or family == "E" ? widthLabel == "Wide" : widthLabel == "Narrow"))
    float openLow = family == "A" ? m.l2 : family == "B" or family == "E" ? (higher ? m.l2 : m.l3) : family == "G" ? (higher ? m.l6 : m.c) : family == "D" ? (higher ? m.l6 : m.h4) : (higher ? m.h4 : m.l6)
    float openHigh = family == "A" ? m.h2 : family == "B" or family == "E" ? (higher ? m.h3 : m.h2) : family == "G" ? (higher ? m.c : m.h6) : family == "D" ? (higher ? m.l4 : m.h6) : (higher ? m.h6 : m.l4)
    bool openOk = observationReady and f_camCompare(opening, openLow) >= 0 and f_camCompare(opening, openHigh) <= 0
    bool upperSide = higher != (family == "D" or family == "G")
    bool regionOk = observationReady and (family == "A" ? (higher ? f_camCompare(barClose, m.l4) > 0 and f_camCompare(barClose, m.c) < 0 : f_camCompare(barClose, m.h4) < 0 and f_camCompare(barClose, m.c) > 0) : family == "B" or family == "E" ? (higher ? f_camCompare(barClose, m.h3) > 0 and f_camCompare(barClose, m.h6) < 0 : f_camCompare(barClose, m.l3) < 0 and f_camCompare(barClose, m.l6) > 0) : family == "C" or family == "D" ? (upperSide ? f_camCompare(barClose, m.h3) > 0 : f_camCompare(barClose, m.l3) < 0) : (upperSide ? f_camCompare(barClose, m.h4) > 0 : f_camCompare(barClose, m.l4) < 0))
    float badLow = family == "A" ? (higher ? m.l4 : m.l2) : family == "B" or family == "E" ? (higher ? m.l2 : m.l6) : upperSide ? (family == "G" ? m.c : m.h3) : m.l6 - (m.l4 - m.l6) * 0.5
    float badHigh = family == "A" ? (higher ? m.h2 : m.h4) : family == "B" or family == "E" ? (higher ? m.h6 : m.h2) : upperSide ? m.h6 + (m.h6 - m.h4) * 0.5 : (family == "G" ? m.c : m.l3)
    bool blocked = priorBlocked or (observationReady and (f_camCompare(barOpen, badLow) < 0 or f_camCompare(barOpen, badHigh) > 0))
    bool geometryReady = innerReady and (family == "A" or m.outerValid)
    bool candidate = geometryReady and atrReady and observationReady and rangeOk and widthOk and openOk and not blocked
    bool approaching = candidate and regionOk
    [bias, ratio, widthLabel, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching]

f_camCodeIndex(string code) =>
    int found = -1
    if array.size(plays) > 0
        for i = 0 to array.size(plays) - 1
            if array.get(plays, i).code == code
                found := i
    found

f_camAutoFocus() =>
    int found = -1
    int latest = na
    if array.size(plays) > 0
        for i = 0 to array.size(plays) - 1
            Play p = array.get(plays, i)
            if p.state == "ACTIVE" and (na(latest) or p.activationTime > latest)
                latest := p.activationTime
                found := i
    found

f_camStorePlan(Play p) =>
    if not na(p.activationTime)
        CamPlanSnapshot snapshot = CamPlanSnapshot.new(p.code, p.name, p.dir, p.episode, p.sessionStart, p.sourceStart, p.sourceEnd, p.sourceKind, p.closeKind, p.setupTime, p.triggerPrice, p.triggerOperator, p.atrUsed, p.stopMode, p.targetMode, p.activationBar, p.activationTime, p.entry, p.stop, p.target, p.effectiveR, p.endBar, p.endTime, p.outcomePrice, p.state, p.reason, p.ambiguous)
        snapshot.atrBasis := p.atrBasis
        snapshot.atrSamples := p.atrSamples
        snapshot.atrStart := p.atrStart
        snapshot.atrStamp := p.atrStamp
        int found = -1
        if array.size(camPlans) > 0
            for i = 0 to array.size(camPlans) - 1
                CamPlanSnapshot old = array.get(camPlans, i)
                if old.code == p.code and old.sessionStart == p.sessionStart and old.episode == p.episode
                    found := i
                    break
        if found >= 0
            array.remove(camPlans, found)
        array.unshift(camPlans, snapshot)
        if array.size(camPlans) > 40
            // Never evict an ongoing plan merely because other codes re-entered.
            int oldestEnded = -1
            for i = array.size(camPlans) - 1 to 0
                if array.get(camPlans, i).state != "ACTIVE"
                    oldestEnded := i
                    break
            if oldestEnded >= 0
                array.remove(camPlans, oldestEnded)
    p

f_camEmit(Play p, string kind, string reason, float price, int stamp, bool ambiguous) =>
    int seq = array.get(camEventCounter, 0) + 1
    array.set(camEventCounter, 0, seq)
    CamEvent e = CamEvent.new(seq, p.code, kind, reason, p.dir, stamp, p.sessionStart, p.sourceStart, p.sourceEnd, price, p.entry, p.stop, p.target, ambiguous)
    e.episode := p.episode
    e.sourceKind := p.sourceKind
    e.closeKind := p.closeKind
    e.atrStamp := p.atrStamp
    e.atrUsed := p.atrUsed
    e.atrBasis := p.atrBasis
    e.atrSamples := p.atrSamples
    e.atrStart := p.atrStart
    e.sourceCutoff := p.sourceCutoff
    e.closeSourceEnd := p.closeSourceEnd
    f_camStorePlan(p)
    array.unshift(camEvents, e)
    array.push(camBarEvents, e)
    if array.size(camEvents) > 60
        array.pop(camEvents)
    seq

f_camArm(Play p, float barHigh, float barLow, int bar, int stamp, string reason) =>
    p.episode += 1
    p.state := "ARMED"
    p.setupBar := bar
    p.setupTime := stamp
    p.armedHigh := barHigh
    p.armedLow := barLow
    p.reason := reason
    f_camEmit(p, "ARMED", reason, p.level, stamp, false)
    p

f_camEnd(Play p, string kind, string reason, float price, int bar, int stamp, bool ambiguous) =>
    p.state := kind
    p.reason := reason
    p.next := "Episode ended · next dated session"
    p.endBar := bar
    p.endTime := stamp
    p.outcomePrice := price
    p.ambiguous := ambiguous
    f_camEmit(p, kind, reason, price, stamp, ambiguous)
    p

// Normalize only machine-noise near a whole tick; retain actual sub-tick prices.
f_camDistance(float first, float rhs, float tick) =>
    float raw = math.abs(first - rhs)
    float ticks = raw / tick
    float nearest = math.round(ticks)
    float noise = 8.0 * 2.220446049250313e-16 * math.max(math.max(math.abs(first), math.abs(rhs)), tick) / tick
    f_camSign(math.abs(ticks - nearest) - math.min(noise, 0.000001)) <= 0 ? nearest * tick : raw

f_camGrid(float value, float tick, bool upward) =>
    float ticks = value / tick
    float nearest = math.round(ticks)
    float tolerance = math.min(8.0 * 2.220446049250313e-16 * math.max(math.abs(ticks), 1), 0.000001)
    float stable = f_camSign(math.abs(ticks - nearest) - tolerance) <= 0 ? nearest : ticks
    (upward ? math.ceil(stable) : math.floor(stable)) * tick

f_camPlan(int dir, string family, CamLevels m, float armedHigh, float armedLow, float entry, bool wide, float tick, int bufferTicks, float minimumR) =>
    float boundary = na
    float destination = na
    if family == "A"
        boundary := dir == 1 ? math.min(m.l4, armedLow) : math.max(m.h4, armedHigh)
        destination := dir == 1 ? m.h3 : m.l3
    else if family == "B" or family == "C" or family == "D"
        boundary := dir == 1 ? m.h3 : m.l3
        destination := dir == 1 ? m.h6 : m.l6
    else if family == "E"
        boundary := dir == 1 ? math.min(m.l6, armedLow) : math.max(m.h6, armedHigh)
        destination := dir == 1 ? m.h4 : m.l4
    else
        boundary := dir == 1 ? math.min(armedLow, m.l6 - 0.25 * (m.l3 - m.l4)) : math.max(armedHigh, m.h6 + 0.25 * (m.h4 - m.h3))
        destination := wide ? (dir == 1 ? m.l4 : m.h4) : m.c
    float stopRaw = boundary - dir * bufferTicks * tick
    float stop = f_camGrid(stopRaw, tick, dir == -1)
    float target = f_camGrid(destination, tick, dir == -1)
    bool positive = not na(entry) and not na(stop) and not na(target) and f_camSign(entry) > 0 and f_camSign(stop) > 0 and f_camSign(target) > 0
    bool directionOk = positive and f_camCompare(entry, stop) * dir > 0 and f_camCompare(target, entry) * dir > 0
    float risk = directionOk ? f_camDistance(entry, stop, tick) : na
    float reward = directionOk ? f_camDistance(entry, target, tick) : na
    float effectiveR = directionOk and f_camSign(risk) > 0 ? reward / risk : na
    bool valid = directionOk and f_camCompare(risk / tick, 1) >= 0 and f_camCompare(effectiveR, minimumR) >= 0
    string reason = not positive ? "Nonpositive or unavailable plan prices" : not directionOk ? "Destination or stop is on the wrong side of entry" : f_camCompare(risk / tick, 1) < 0 ? "Risk distance is smaller than one tick" : f_camCompare(effectiveR, minimumR) < 0 ? "Structural R " + f_camExact(effectiveR) + " < minimum " + f_camExact(minimumR) : "Confirmed structural hypothesis · reference plan only"
    [valid, stop, target, effectiveR, reason]

f_camPlanOptions(int dir, string family, CamLevels m, float armedHigh, float armedLow, float entry, bool wide, float tick, int bufferTicks, float minimumR, float atr, string stopChoice, float stopMultiple, string targetChoice, float targetMultiple, float reachLimit) =>
    [structuralOk, structuralStop, structuralTarget, structuralR, structuralReason] = f_camPlan(dir, family, m, armedHigh, armedLow, entry, wide, tick, bufferTicks, minimumR)
    bool atrReady = not na(atr) and f_camSign(atr) > 0
    float stop = stopChoice == "ATR" and atrReady ? f_camGrid(entry - dir * (atr * stopMultiple + bufferTicks * tick), tick, dir == -1) : structuralStop
    float target = targetChoice == "ATR" and atrReady ? f_camGrid(entry + dir * atr * targetMultiple, tick, dir == -1) : structuralTarget
    bool positive = not na(stop) and not na(target) and f_camSign(entry) > 0 and f_camSign(stop) > 0 and f_camSign(target) > 0
    bool correctSide = positive and f_camCompare(entry, stop) * dir > 0 and f_camCompare(target, entry) * dir > 0
    float risk = correctSide ? f_camDistance(entry, stop, tick) : na
    float reward = correctSide ? f_camDistance(entry, target, tick) : na
    float rr = correctSide and f_camSign(risk) > 0 ? reward / risk : na
    bool enoughRisk = correctSide and f_camCompare(risk / tick, 1) >= 0
    bool enoughReward = enoughRisk and f_camCompare(rr, minimumR) >= 0
    bool reachable = reachLimit == 0 or atrReady and f_camCompare(reward, atr * reachLimit) <= 0
    bool valid = atrReady and enoughReward and reachable
    string reason = not atrReady ? "Confirmed selected ATR unavailable" : not positive ? "Nonpositive or unavailable reference prices" : not correctSide ? "Stop or target is on the wrong side" : not enoughRisk ? "Risk is smaller than one tick" : not enoughReward ? "Reference R " + f_camExact(rr) + " < minimum " + f_camExact(minimumR) : not reachable ? "Target exceeds " + f_camExact(reachLimit) + " selected ATR reach limit" : "Original confirmed reference geometry"
    [valid, stop, target, rr, reason]

f_camOutcome(Play p, float barOpen, float barHigh, float barLow, int bar, int stamp, bool contained, int timeout, bool sessionEnded) =>
    string kind = ""
    string reason = ""
    float price = na
    bool ambiguous = false
    if p.state == "ACTIVE" and bar > p.activationBar
        if contained and f_camBarValid(barOpen, barHigh, barLow, barOpen)
            bool stopGap = f_camCompare(barOpen, p.stop) * p.dir <= 0
            bool targetGap = f_camCompare(barOpen, p.target) * p.dir >= 0
            bool stopTouch = p.dir == 1 ? f_camCompare(barLow, p.stop) <= 0 : f_camCompare(barHigh, p.stop) >= 0
            bool targetTouch = p.dir == 1 ? f_camCompare(barHigh, p.target) >= 0 : f_camCompare(barLow, p.target) <= 0
            if stopGap
                kind := "STOP"
                price := barOpen
                reason := "Opening price beyond stop · reference outcome at observed open"
            else if targetGap
                kind := "TARGET"
                price := p.target
                reason := "Opening price beyond destination · fixed reference destination"
            else if stopTouch
                kind := "STOP"
                price := p.stop
                ambiguous := targetTouch
                reason := targetTouch ? "Both levels touched · stop-first convention; path unknown" : "Reference stop touched on a later bar"
            else if targetTouch
                kind := "TARGET"
                price := p.target
                reason := "Reference destination touched on a later bar"
        if kind == "" and bar - p.activationBar >= timeout
            kind := "EXPIRED"
            reason := "Completed-bar active deadline · no fill inferred"
        if kind == "" and sessionEnded
            kind := "EXPIRED"
            reason := contained ? "Decision session ended · no fill inferred" : "Session end observed later or across a boundary · no fill inferred"
    [kind, price, ambiguous, reason]

f_camFamilyEnabled(int familyIndex) =>
    familyIndex == 0 ? enableA : familyIndex == 1 ? enableB : familyIndex == 2 ? enableC : familyIndex == 3 ? enableD : familyIndex == 4 ? enableE : familyIndex == 5 ? enableF : enableG

f_camNewPlay(int index, CamLevels m, bool mapReady, string dataReason, int bias, float opening, bool openingKnown, int sessionStart) =>
    int familyIndex = int(math.floor(index / 2))
    bool higher = index % 2 == 0
    string family = familyIndex == 0 ? "A" : familyIndex == 1 ? "B" : familyIndex == 2 ? "C" : familyIndex == 3 ? "D" : familyIndex == 4 ? "E" : familyIndex == 5 ? "F" : "G"
    string familyName = family == "A" ? "Traverse" : family == "B" ? "Break and retest" : family == "C" ? "Opening continuation" : family == "D" ? "Opposed continuation" : family == "E" ? "Fourth-tier failure" : family == "F" ? "Outer rejection" : "Opposite outer rejection"
    Play p = Play.new()
    p.code := (higher ? "H" : "L") + family
    p.name := (higher ? "Higher · " : "Lower · ") + familyName
    p.family := family
    p.biasRequired := higher ? 1 : -1
    p.dir := (family == "D" or family == "E" or family == "F") ? (higher ? -1 : 1) : (higher ? 1 : -1)
    p.sessionStart := sessionStart
    p.sourceStart := m.sourceStart
    p.sourceEnd := m.sourceEnd
    bool longSide = p.dir == 1
    p.level := family == "A" ? (longSide ? m.l3 : m.h3) : family == "E" ? (longSide ? m.l4 : m.h4) : family == "F" or family == "G" ? (longSide ? m.l6 : m.h6) : (longSide ? m.h4 : m.l4)
    p.triggerPrice := p.level
    p.triggerOperator := longSide ? ">" : "<"
    string levelText = f_camExact(p.level)
    p.next := family == "B" ? "Closed break of " + levelText + "; later contained retest" : family == "E" ? "Closed excursion beyond " + levelText + "; later failure close" : "Touch " + levelText + "; close " + (longSide ? "above" : "below")
    p.invalidation := family == "A" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.l4) : "≥ " + f_camExact(m.h4)) : family == "B" or family == "C" or family == "D" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.h3) : "≥ " + f_camExact(m.l3)) : family == "E" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.l6) : "≥ " + f_camExact(m.h6)) : "Pending setup deadline; no invented farther pivot"
    if not f_camFamilyEnabled(familyIndex)
        p.state := "DISABLED"
        p.reason := "Family " + family + " is disabled"
        p.next := "Enable this family in settings"
    else if not mapReady
        p.state := "DATA WAIT"
        p.reason := dataReason
    else if not openingKnown
        p.state := "DATA WAIT"
        p.reason := "OPEN UNKNOWN · exact contained session opening not observed"
    else if family != "A" and not m.outerValid
        p.state := "DATA WAIT"
        p.reason := "Outer geometry required by this family's target, stop or cancellation"
    else
        p.state := "WATCH"
        p.reason := "Waiting for confirmed public-baseline prerequisite evaluation"
    p

f_camRefreshPlay(Play p, CamLevels m, CamLevels prior, float atr, float opening, float barOpen, float barClose, bool observationValid, bool inWindow, bool sourceReady, string dataReason, bool smaReady) =>
    [bias, ratio, width, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching] = f_camPrerequisites(p.code, m, prior, atr, opening, barOpen, barClose, p.blocked, rangeThresholdPct, widthThresholdPct, allowNeutral)
    p.rangeOk := rangeOk
    p.widthOk := widthOk
    p.openOk := openOk
    p.regionOk := observationValid and regionOk
    p.blocked := observationValid and inWindow ? blocked : p.blocked
    p.candidate := sourceReady and observationValid and inWindow and candidate and p.state != "DISABLED"
    p.approaching := p.candidate and p.regionOk
    p.eligible := p.candidate
    p.smaOk := smaReady
    if p.state == "WATCH" or p.state == "DATA WAIT"
        bool prerequisitesAvailable = sourceReady and not na(opening) and not na(atr) and f_camSign(atr) > 0 and (p.family == "A" or m.outerValid)
        p.state := prerequisitesAvailable ? "WATCH" : "DATA WAIT"
        string widthNeed = p.family == "A" or p.family == "E" ? "Wide or Similar" : p.family == "F" or p.family == "G" ? "any width" : "Narrow or Similar"
        p.reason := not sourceReady ? dataReason : na(opening) ? "Exact source-minute opening unavailable" : not prerequisitesAvailable ? "Required geometry or confirmed ATR unavailable" : not inWindow ? "Outside decision window" : not observationValid ? "Malformed chart OHLC · price evaluation withheld" : p.blocked ? "Bad OPEN crossed this family's limit · blocked for this session" : not rangeOk ? "Close-delta context is " + (bias == 1 ? "Higher" : bias == -1 ? "Lower" : "Neutral") + "; this code requires " + (p.biasRequired == 1 ? "Higher" : "Lower") : not widthOk ? "Width " + width + "; requires " + widthNeed : not openOk ? "Frozen open " + f_camExact(opening) + " is outside this family's bounded inclusive opening interval" : not regionOk ? "Candidate eligible · closed price is outside its approach region" : "Approaching · prerequisites pass; original confirmed trigger still required"
        p.next := p.blocked or not rangeOk or not widthOk or not openOk ? "Next source session / changed prerequisites" : not inWindow ? "Next decision window" : p.approaching ? "Original close " + p.triggerOperator + " " + f_camExact(p.triggerPrice) + (smaGate and not p.smaOk ? " plus SMA confirmation" : " with the stated setup") : "Enter the play's approach region on a confirmed close"
    p

f_camRearm(Play p, int bar, bool inWindow, bool enabled, int cooldown, int episodeLimit) =>
    if enabled and inWindow and f_camTerminal(p.state) and not na(p.endBar) and bar > p.endBar and bar - p.endBar >= cooldown and p.episode < episodeLimit and p.candidate and not p.blocked
        p.state := "WATCH"
        p.setupBar := na
        p.setupTime := na
        p.armedHigh := na
        p.armedLow := na
        p.activationBar := na
        p.activationTime := na
        p.activationSeq := na
        p.entry := na
        p.stop := na
        p.target := na
        p.effectiveR := na
        p.endBar := na
        p.endTime := na
        p.outcomePrice := na
        p.ambiguous := false
        p.reason := "Cooldown completed · waiting for a new original setup"
    p

// Explanations report the observed family trigger without changing its lifecycle.
f_camActivationReason(Play p, float activationClose) =>
    bool longSide = p.dir == 1
    string tier = p.family == "A" ? (longSide ? "L3" : "H3") : p.family == "E" ? (longSide ? "L4" : "H4") : p.family == "F" or p.family == "G" ? (longSide ? "L6" : "H6") : (longSide ? "H4" : "L4")
    string cause = p.family == "A" ? tier + " touch + " + (longSide ? "reclaim" : "rejection") : p.family == "B" ? tier + " closed break, later retest" : p.family == "C" or p.family == "D" ? "Bounded opening region; " + tier + " retest held" : p.family == "E" ? "Closed " + (longSide ? "below " : "above ") + tier + ", later failure" : tier + " outer touch + recovery"
    cause + "; close " + f_camReadable(activationClose) + (longSide ? " > " : " < ") + tier + " " + f_camReadable(p.level) + (p.family == "D" ? " · against frozen structure" : "")

// Parameterized OHLC/clock step for production and exact-function fixture probes.
// Input p is a single episode row. Only its reference fields and event arrays mutate.
f_camStep(Play p, CamLevels m, float barOpen, float barHigh, float barLow, float barClose, float previousClose, int bar, int stamp, bool contained, bool sessionEnded, bool wide, float tick, int setupLimit, int activeLimit, int bufferTicks, float minimumR) =>
    bool usable = contained and f_camBarValid(barOpen, barHigh, barLow, barClose)
    if p.state == "ACTIVE"
        [kind, price, ambiguous, outcomeReason] = f_camOutcome(p, barOpen, barHigh, barLow, bar, stamp, usable, activeLimit, sessionEnded)
        if kind != ""
            f_camEnd(p, kind, outcomeReason, price, bar, stamp, ambiguous)
    else if p.state == "WATCH" or p.state == "ARMED"
        bool longSide = p.dir == 1
        bool touched = usable and f_camTouched(p.level, barHigh, barLow)
        if usable and p.state == "WATCH" and p.approaching
            bool shouldArm = p.family == "B" ? not na(previousClose) and f_camCompare(previousClose, p.level) * p.dir <= 0 and f_camCompare(barClose, p.level) * p.dir > 0 : p.family == "E" ? f_camCompare(barClose, p.level) * p.dir < 0 : p.family == "C" or p.family == "D" ? true : touched
            if shouldArm
                string armReason = p.family == "B" ? "Confirmed breakout observed · await a later retest" : p.family == "E" ? "Confirmed excursion observed · await a later failure" : p.family == "C" or p.family == "D" ? "Known opening in bounded fourth-to-sixth interval · wait for contained retest" : "Actual bar range touched the reference level"
                f_camArm(p, barHigh, barLow, bar, stamp, armReason)
                p.next := p.family == "E" ? "Later close " + (longSide ? "above " : "below ") + f_camExact(p.level) : "Close " + (longSide ? "above " : "below ") + f_camExact(p.level) + (p.family == "B" ? " after a later retest" : " after the observed test")
        if p.state == "ARMED"
            int age = bar - p.setupBar
            if age > setupLimit
                f_camEnd(p, "EXPIRED", "Setup deadline passed · confirmation allowed only through age " + str.tostring(setupLimit), na, bar, stamp, false)
            else if usable
                p.armedHigh := math.max(p.armedHigh, barHigh)
                p.armedLow := math.min(p.armedLow, barLow)
                bool cancelled = p.blocked or (p.family == "A" ? (longSide ? f_camCompare(barClose, m.l4) <= 0 : f_camCompare(barClose, m.h4) >= 0) : p.family == "B" or p.family == "C" or p.family == "D" ? (longSide ? f_camCompare(barClose, m.h3) <= 0 : f_camCompare(barClose, m.l3) >= 0) : p.family == "E" ? (longSide ? f_camCompare(barClose, m.l6) <= 0 : f_camCompare(barClose, m.h6) >= 0) : false)
                bool recoveryClose = f_camCompare(barClose, p.level) * p.dir > 0
                bool previousWrongSide = not na(previousClose) and f_camCompare(previousClose, p.level) * p.dir <= 0
                bool confirmed = p.family == "B" ? bar > p.setupBar and touched and recoveryClose : p.family == "E" ? bar > p.setupBar and recoveryClose : p.family == "C" or p.family == "D" ? touched and recoveryClose : recoveryClose and ((bar == p.setupBar and touched) or previousWrongSide)
                if cancelled
                    f_camEnd(p, "INVALIDATED", p.blocked ? "Bad OPEN crossed the public-baseline limit · session block latched" : "Pending close crossed the original cancellation boundary", barClose, bar, stamp, false)
                else if sessionEnded
                    f_camEnd(p, "EXPIRED", "Decision session ended before activation · no entry inferred", na, bar, stamp, false)
                else if confirmed and p.approaching and p.smaOk
                    [planOk, stop, target, rewardR, planReason] = f_camPlanOptions(p.dir, p.family, m, p.armedHigh, p.armedLow, barClose, wide, tick, bufferTicks, minimumR, p.atrUsed, p.stopMode, stopAtrMultiple, p.targetMode, targetAtrMultiple, maxTargetAtr)
                    if planOk
                        p.state := "ACTIVE"
                        p.entry := barClose
                        p.stop := stop
                        p.target := target
                        p.effectiveR := rewardR
                        p.activationBar := bar
                        p.activationTime := stamp
                        p.reason := f_camActivationReason(p, barClose)
                        p.next := "Observe later bars: stop " + f_camExact(stop) + " / destination " + f_camExact(target)
                        p.invalidation := "Fixed reference stop " + f_camExact(stop) + "; session end or active deadline"
                        p.activationSeq := f_camEmit(p, "ACTIVE", p.reason, barClose, stamp, false)
                    else
                        f_camEnd(p, "REJECTED", planReason + " · episode consumed", barClose, bar, stamp, false)
            else if sessionEnded
                f_camEnd(p, "EXPIRED", "Session end observed across an unavailable boundary · no fill inferred", na, bar, stamp, false)
    p

f_camClock(int barTime, int dailyStart, int dailyEnd, string mode, string sessionText, string timezone, bool weekdaysOnly) =>
    int startStamp = dailyStart
    int endStamp = dailyEnd
    bool dayAllowed = true
    string label = "Exchange day · provider boundaries"
    if mode != "Exchange day"
        string interval = mode == "US cash" ? "0930-1600" : sessionText
        string zone = mode == "US cash" ? "America/New_York" : timezone
        int startHour = int(str.tonumber(str.substring(interval, 0, 2)))
        int startMinute = int(str.tonumber(str.substring(interval, 2, 4)))
        int endHour = int(str.tonumber(str.substring(interval, 5, 7)))
        int endMinute = int(str.tonumber(str.substring(interval, 7, 9)))
        int firstMinute = startHour * 60 + startMinute
        int lastMinute = endHour * 60 + endMinute
        int nowMinute = hour(barTime, zone) * 60 + minute(barTime, zone)
        bool overnight = lastMinute <= firstMinute
        int dayOffset = overnight and nowMinute < firstMinute ? -1 : 0
        int localYear = year(barTime, zone)
        int localMonth = month(barTime, zone)
        int localDay = dayofmonth(barTime, zone) + dayOffset
        startStamp := timestamp(zone, localYear, localMonth, localDay, startHour, startMinute)
        endStamp := timestamp(zone, localYear, localMonth, localDay + (overnight ? 1 : 0), endHour, endMinute)
        int tradingWeekday = dayofweek(lastMinute < firstMinute ? endStamp : startStamp, zone)
        dayAllowed := (mode == "Custom" and not weekdaysOnly) or (tradingWeekday >= dayofweek.monday and tradingWeekday <= dayofweek.friday)
        label := (mode == "US cash" ? "US cash · " : "Custom · ") + interval + " " + zone
    [startStamp, endStamp, dayAllowed, label]

f_camOpenLocation(float opening, CamLevels m) =>
    na(opening) ? "OPEN UNKNOWN" : not m.valid ? "MAP UNAVAILABLE" : f_camCompare(opening, m.l4) < 0 ? "Below L4" : f_camCompare(opening, m.l4) == 0 ? "At L4" : f_camCompare(opening, m.l3) < 0 ? "L4–L3" : f_camCompare(opening, m.l3) == 0 ? "At L3" : f_camCompare(opening, m.h3) < 0 ? "Inside third band" : f_camCompare(opening, m.h3) == 0 ? "At H3" : f_camCompare(opening, m.h4) < 0 ? "H3–H4" : f_camCompare(opening, m.h4) == 0 ? "At H4" : "Above H4"

//

// SOURCE ADAPTER · one-minute observations, session-frozen scalar packet.
// Lower-timeframe request returns an already frozen context, never one candle's
// OHLC as a proxy for an entire source window. Availability is checked again below.

f_camBookFinish(CamWindowBook book, bool complete, string reason) =>
    book.prior := book.latest
    book.latest := f_camLevels(book.h, book.l, book.c, book.start, book.finish, complete, reason)
    book.finished := true
    book.complete := complete
    book.reason := reason
    book

f_camObserve(CamWindowBook book, int start, int finish, bool allowed, int stamp, int closingStamp, float o, float h, float l, float c) =>
    bool overlap = allowed and not na(start) and not na(finish) and stamp < finish and closingStamp > start
    bool contained = overlap and stamp >= start and closingStamp <= finish
    bool valid = f_camBarValid(o, h, l, c)
    if not book.finished and (stamp >= book.finish or overlap and start != book.start)
        f_camBookFinish(book, false, "Exact final source boundary missing")
    if overlap
        if na(book.start) or book.start != start
            book.start := start
            book.finish := finish
            book.lastClose := na
            book.h := na
            book.l := na
            book.c := na
            book.samples := 0
            book.complete := contained and valid and stamp == start
            book.finished := false
            book.reason := book.complete ? "Collecting contiguous 1m source" : "Exact valid source opening missing"
        if not book.finished
            if not contained or not valid or not na(book.lastClose) and stamp != book.lastClose
                book.complete := false
                book.reason := not contained ? "Source minute straddles boundary" : not valid ? "Malformed source OHLC" : "Missing interior source minute"
            if contained
                if valid
                    book.h := na(book.h) ? h : math.max(book.h, h)
                    book.l := na(book.l) ? l : math.min(book.l, l)
                    book.c := c
                    book.samples += 1
                book.lastClose := closingStamp
            if closingStamp >= finish
                bool complete = book.complete and contained and closingStamp == finish
                f_camBookFinish(book, complete, complete ? "Complete observed 1m interval" : book.reason)
    book

// Original observed-session ATR convention, not an undisclosed vendor formula.
// Missing calendar days are not observations; invalid observed windows reset.
// Empty/duplicate/out-of-order packets cannot count twice or rewind the chain.
f_camAtrConsume(CamAtrBook book, CamLevels source) =>
    if not na(source.sourceEnd) and (na(book.observedEnd) or source.sourceEnd > book.observedEnd)
        book.observedEnd := source.sourceEnd
        bool valid = source.complete and not na(source.sourceStart) and source.sourceEnd > source.sourceStart and (na(book.sourceEnd) or source.sourceStart >= book.sourceEnd) and f_camBarValid(source.c, source.h, source.l, source.c)
        if valid
            float tr = na(book.previousClose) ? source.h - source.l : math.max(source.h - source.l, math.max(math.abs(source.h - book.previousClose), math.abs(source.l - book.previousClose)))
            if book.samples == 0
                book.sourceStart := source.sourceStart
            book.samples += 1
            if book.samples <= 14
                book.seedSum += tr
                book.value := book.samples == 14 ? book.seedSum / 14 : na
            else
                book.value := (book.value * 13 + tr) / 14
            book.previousClose := source.c
            book.sourceEnd := source.sourceEnd
        else
            book.samples := 0
            book.seedSum := 0
            book.previousClose := na
            book.value := na
            book.sourceStart := na
            book.sourceEnd := na
    book

f_camAtrBasisCode(string mode, bool futures, string resolvedDecision) =>
    mode == "RTH session" or mode == "Market default" and futures and resolvedDecision != "Exchange day" ? 2 : 1

f_camAtrBasisName(int code) =>
    code == 2 ? "Observed RTH sessions · Wilder14" : code == 1 ? "Provider daily · ATR14" : "Unavailable"

f_camAtrReady(float value, int sourceEnd, int cutoff) =>
    not na(value) and f_camSign(value) > 0 and not na(sourceEnd) and not na(cutoff) and sourceEnd <= cutoff and cutoff - sourceEnd <= 604800000

f_camAtrFreeze(CamSourcePacket packet, CamAtrBook book, int basis, float providerAtr, int providerStamp, int cutoff) =>
    packet.atrBasisCode := basis
    packet.atr := basis == 2 ? book.value : providerAtr
    packet.atrStamp := basis == 2 ? book.sourceEnd : providerStamp
    packet.atrSamples := basis == 2 ? book.samples : not na(providerAtr) ? 14 : 0
    packet.atrStart := basis == 2 ? book.sourceStart : na
    packet.atrReady := f_camAtrReady(packet.atr, packet.atrStamp, cutoff)
    packet

f_camSourceCode(string kind) =>
    kind == "RTH" ? 1 : kind == "ETH" ? 2 : kind == "Exchange daily" ? 3 : kind == "Selected session" ? 4 : 0

f_camSourceName(int code) =>
    code == 1 ? "RTH" : code == 2 ? "ETH" : code == 3 ? "Exchange daily" : code == 4 ? "Selected session" : "Unavailable"

f_camSourceReason(int code) =>
    code == 1 ? "Complete source pair frozen before the opening minute" : code == 2 ? "Auto requires complete prior postmarket/current premarket ending at RTH open; choose a fixed source if unavailable" : code == 3 ? "Two complete chosen H/L and close windows required" : code == 4 ? "Source or independent-close timestamps not ordered" : code == 5 ? "Source or ATR unavailable before decision open" : code == 6 ? "Source or close older than seven elapsed days" : code == 7 ? "Chosen current inner geometry unavailable" : code == 8 ? "Chosen previous inner geometry unavailable" : code == 9 ? "Selected ATR unavailable, warming or stale" : code == 10 ? "Exact valid one-minute opening unavailable" : "Waiting for an observed source opening"

f_camSelectionReason(int code) =>
    code == 6 ? "Market default: Exchange daily for crypto; 24/7 provider clock" : code == 7 ? "Market default: RTH for non-crypto; configured cash-session source" : code == 1 ? "Auto crypto: Exchange daily, 24/7 provider clock" : code == 2 ? "Fixed source; no automatic session substitution" : code == 3 ? "ETH selected: pre-opening excursion exceeded selected ATR tolerance" : code == 4 ? "RTH selected: complete pre-opening excursion stayed within tolerance" : "Auto selection lacks complete aligned pre/post windows"

f_camSourceEngine() =>
    // Explicit ticker/timeframe in this nested request; every field is completed.
    [dh1, dl1, dc1, ds1, de1, dh2, dl2, dc2, ds2, de2, dailyAtr] = request.security(syminfo.tickerid, "D", [high[1], low[1], close[1], time[1], time_close[1], high[2], low[2], close[2], time[2], time_close[2], ta.atr(14)[1]], gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on, calc_bars_count = 500)
    int exchangeStart = time("D")
    int exchangeEnd = time_close("D")
    bool crypto = syminfo.type == "crypto"
    string resolvedDecision = decisionMode == "Auto" ? (crypto ? "Exchange day" : "US cash") : decisionMode
    [decisionStart, decisionEnd, decisionAllowed, clockLabel] = f_camClock(time, exchangeStart, exchangeEnd, resolvedDecision, customSession, customTimezone, customWeekdays)
    [rthStart, rthEnd, rthAllowed, rthLabel] = f_camClock(time, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    string ethMode = extendedClock == "Auto" and crypto ? "Exchange day" : "Custom"
    string ethText = extendedClock == "Custom" ? extendedSession : syminfo.type == "futures" ? "1800-1700" : "0400-2000"
    string ethZone = extendedClock == "Custom" ? extendedTimezone : "America/New_York"
    [ethStart, ethEnd, ethAllowed, ethLabel] = f_camClock(time, exchangeStart, exchangeEnd, ethMode, ethText, ethZone, not crypto)
    // Find the RTH opening belonging to this ETH interval using local calendar dates.
    [regularAtEthStart, regularEndAtEthStart, ignoredDay, ignoredLabel] = f_camClock(ethStart, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    int nextRegular = regularAtEthStart
    if not na(nextRegular) and nextRegular < ethStart
        nextRegular := timestamp(regularTimezone, year(nextRegular, regularTimezone), month(nextRegular, regularTimezone), dayofmonth(nextRegular, regularTimezone) + 1, hour(nextRegular, regularTimezone), minute(nextRegular, regularTimezone))
    [pairedRegularStart, pairedRegularEnd, pairedDay, pairedLabel] = f_camClock(nextRegular, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    var CamAtrBook rthAtr = CamAtrBook.new()
    var CamWindowBook rth = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook eth = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook selected = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook pre = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook post = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamSourcePacket frozen = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
    bool overlap = decisionAllowed and time < decisionEnd and time_close > decisionStart
    bool newDecision = overlap and (na(frozen.sessionStart) or frozen.sessionStart != decisionStart)
    // Certify any missed closing boundary before selecting predecessors.
    if not rth.finished and time >= rth.finish
        f_camBookFinish(rth, false, "Exact RTH end was not observed")
    if not eth.finished and time >= eth.finish
        f_camBookFinish(eth, false, "Exact ETH end was not observed")
    if not selected.finished and time >= selected.finish
        f_camBookFinish(selected, false, "Exact selected-window end was not observed")
    // A missed observed closing boundary is consumed before freezing this opening.
    f_camAtrConsume(rthAtr, rth.latest)
    if newDecision
        CamSourcePacket next = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
        next.sessionStart := decisionStart
        next.sessionEnd := decisionEnd
        next.cutoff := decisionStart
        next.clockLabel := clockLabel
        next.openKnown := time == decisionStart and time_close <= decisionEnd and f_camBarValid(open, high, low, close)
        next.opening := next.openKnown ? open : na
        next.openingTime := next.openKnown ? time : na
        int atrBasis = f_camAtrBasisCode(atrBasisMode, syminfo.type == "futures", resolvedDecision)
        f_camAtrFreeze(next, rthAtr, atrBasis, dailyAtr, de1, decisionStart)
        CamLevels daily1 = f_camLevels(dh1, dl1, dc1, ds1, de1, not na(de1), "Completed provider daily source unavailable")
        CamLevels daily2 = f_camLevels(dh2, dl2, dc2, ds2, de2, not na(de2), "Second provider daily source unavailable")
        string chosen = sourceMode == "Market default" ? (crypto ? "Exchange daily" : "RTH") : sourceMode == "Auto RTH/ETH" and crypto ? "Exchange daily" : sourceMode
        bool selectionReady = true
        if chosen == "Auto RTH/ETH"
            bool postReady = post.latest.complete and post.latest.sourceStart == rth.latest.sourceEnd and post.latest.sourceEnd <= decisionStart
            bool preReady = pre.latest.complete and pre.latest.sourceEnd == decisionStart and pre.latest.sourceStart >= rth.latest.sourceEnd
            bool alignedOpening = decisionStart == nextRegular
            selectionReady := rth.latest.complete and eth.latest.complete and postReady and preReady and alignedOpening and next.atrReady
            next.excursion := selectionReady ? math.max(math.max(post.latest.h, pre.latest.h) - rth.latest.h, rth.latest.l - math.min(post.latest.l, pre.latest.l)) : na
            next.excursionThreshold := next.atr * switchingTolerance / 100
            bool outside = selectionReady and (f_camCompare(math.max(post.latest.h, pre.latest.h), rth.latest.h + next.excursionThreshold) > 0 or f_camCompare(math.min(post.latest.l, pre.latest.l), rth.latest.l - next.excursionThreshold) < 0)
            chosen := outside ? "ETH" : "RTH"
            next.selectionReason := not selectionReady ? "Auto requires complete prior postmarket and current premarket ending at the chosen RTH open; choose a fixed source if unavailable" : outside ? "ETH selected: observed excursion exceeded selected ATR tolerance before open" : "RTH selected: complete observed excursion stayed within tolerance at open"
            next.selectionCode := not selectionReady ? 5 : outside ? 3 : 4
        else
            next.selectionCode := sourceMode == "Market default" ? (crypto ? 6 : 7) : sourceMode == "Auto RTH/ETH" and crypto ? 1 : 2
            next.selectionReason := f_camSelectionReason(next.selectionCode)
        CamLevels main1 = chosen == "RTH" ? rth.latest : chosen == "ETH" ? eth.latest : chosen == "Selected session" ? selected.latest : daily1
        CamLevels main2 = chosen == "RTH" ? rth.prior : chosen == "ETH" ? eth.prior : chosen == "Selected session" ? selected.prior : daily2
        string chosenClose = closeMode == "Match source" ? chosen : closeMode
        CamLevels close1 = chosenClose == "RTH" ? rth.latest : chosenClose == "ETH" ? eth.latest : chosenClose == "Selected session" ? selected.latest : daily1
        CamLevels close2 = chosenClose == "RTH" ? rth.prior : chosenClose == "ETH" ? eth.prior : chosenClose == "Selected session" ? selected.prior : daily2
        next.sourceKind := chosen + (chosen == "Exchange daily" ? " · provider H/L" : " · observed 1m H/L")
        next.closeKind := chosenClose + (chosenClose == "Exchange daily" ? " · provider daily/settlement C" : " · observed last-trade C")
        next.sourceCode := f_camSourceCode(chosen)
        next.closeCode := f_camSourceCode(chosenClose)
        next.closeSourceEnd := close1.sourceEnd
        next.priorCloseSourceEnd := close2.sourceEnd
        bool completePair = main1.complete and main2.complete and close1.complete and close2.complete
        bool ordered = not na(main1.sourceStart) and not na(main1.sourceEnd) and not na(main2.sourceEnd) and main2.sourceEnd <= main1.sourceStart and close2.sourceEnd <= close1.sourceStart
        bool available = main1.sourceEnd <= decisionStart and close1.sourceEnd <= decisionStart and next.atrStamp <= decisionStart
        bool fresh = not na(main1.sourceEnd) and decisionStart - main1.sourceEnd <= 604800000 and not na(close1.sourceEnd) and decisionStart - close1.sourceEnd <= 604800000
        next.current := f_camLevels(main1.h, main1.l, close1.c, main1.sourceStart, main1.sourceEnd, completePair and selectionReady, "Required H/L or selected close predecessor is incomplete", false)
        next.prior := f_camLevels(main2.h, main2.l, close2.c, main2.sourceStart, main2.sourceEnd, completePair and selectionReady, "Previous chosen H/L or close source is incomplete", false)
        next.ready := selectionReady and completePair and ordered and available and fresh and next.current.valid and next.prior.valid and next.atrReady
        next.reason := not selectionReady ? next.selectionReason : not completePair ? "Two complete chosen H/L and close windows required" : not ordered ? "Source or independent-close timestamps not ordered" : not available ? "Source or ATR was unavailable before decision open" : not fresh ? "Source or close is older than seven elapsed days" : not next.current.valid ? next.current.reason : not next.prior.valid ? next.prior.reason : not next.atrReady ? "Selected ATR unavailable, warming or stale" : not next.openKnown ? "Exact valid one-minute opening unavailable" : "Complete source pair frozen before the opening minute"
        next.reasonCode := not selectionReady ? 2 : not completePair ? 3 : not ordered ? 4 : not available ? 5 : not fresh ? 6 : not next.current.valid ? 7 : not next.prior.valid ? 8 : not next.atrReady ? 9 : not next.openKnown ? 10 : 1
        next.rthSamples := rth.samples
        next.ethSamples := eth.samples
        next.selectedSamples := selected.samples
        next.rthComplete := rth.latest.complete
        next.ethComplete := eth.latest.complete
        next.selectedComplete := selected.latest.complete
        next.coverage := "1m source · RTH " + rth.reason + " / ETH " + eth.reason + "; up to 100,000 source minutes"
        frozen := next
    // Current minute cannot affect this decision's selected map or auto choice.
    f_camObserve(rth, rthStart, rthEnd, rthAllowed, time, time_close, open, high, low, close)
    f_camAtrConsume(rthAtr, rth.latest)
    f_camObserve(eth, ethStart, ethEnd, ethAllowed, time, time_close, open, high, low, close)
    f_camObserve(selected, decisionStart, decisionEnd, decisionAllowed, time, time_close, open, high, low, close)
    f_camObserve(pre, ethStart, nextRegular, ethAllowed and pairedDay and ethStart < nextRegular, time, time_close, open, high, low, close)
    f_camObserve(post, pairedRegularEnd, ethEnd, ethAllowed and pairedDay and pairedRegularEnd < ethEnd, time, time_close, open, high, low, close)
    // Scalar-only transport is essential: returning nested UDTs across 100k
    // source minutes exhausted actual TradingView runtime memory in development.
    [frozen.current.h, frozen.current.l, frozen.current.c, frozen.current.sourceStart, frozen.current.sourceEnd, frozen.current.complete, frozen.prior.h, frozen.prior.l, frozen.prior.c, frozen.prior.sourceStart, frozen.prior.sourceEnd, frozen.prior.complete, frozen.ready, frozen.openKnown, frozen.opening, frozen.openingTime, frozen.sessionStart, frozen.sessionEnd, frozen.cutoff, frozen.atr, frozen.atrStamp, frozen.closeSourceEnd, frozen.priorCloseSourceEnd, frozen.excursion, frozen.excursionThreshold, frozen.sourceCode, frozen.closeCode, frozen.reasonCode, frozen.selectionCode, frozen.rthSamples, frozen.ethSamples, frozen.selectedSamples, frozen.rthComplete, frozen.ethComplete, frozen.selectedComplete, frozen.atrBasisCode, frozen.atrSamples, frozen.atrStart]

// Modify only session selection; retain the chart ticker's other price modifiers.
string camSourceTicker = ticker.modify(syminfo.tickerid, session = session.extended)
[camRawH1, camRawL1, camRawC1, camRawStart1, camRawEnd1, camRawComplete1, camRawH2, camRawL2, camRawC2, camRawStart2, camRawEnd2, camRawComplete2, camRawReady, camRawOpenKnown, camRawOpen, camRawOpenTime, camRawSessionStart, camRawSessionEnd, camRawCutoff, camRawAtr, camRawAtrStamp, camRawCloseEnd1, camRawCloseEnd2, camRawExcursion, camRawExcursionThreshold, camRawSourceCode, camRawCloseCode, camRawReasonCode, camRawSelectionCode, camRawRthSamples, camRawEthSamples, camRawSelectedSamples, camRawRthComplete, camRawEthComplete, camRawSelectedComplete, camRawAtrBasisCode, camRawAtrSamples, camRawAtrStart] = request.security(camSourceTicker, "1", f_camSourceEngine(), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off, calc_bars_count = 100000)
var CamSourcePacket camSourcePacket = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
int camExchangeStart = time("D")
int camExchangeEnd = time_close("D")
string camResolvedDecision = decisionMode == "Auto" ? (syminfo.type == "crypto" ? "Exchange day" : "US cash") : decisionMode
[camScheduledStart, camScheduledEnd, camDayAllowed, camClockLabel] = f_camClock(time, camExchangeStart, camExchangeEnd, camResolvedDecision, customSession, customTimezone, customWeekdays)
bool camChartSupported = chart.is_standard and timeframe.isminutes and timeframe.multiplier >= 1 and timeframe.multiplier <= 60
bool camOverlapNow = camChartSupported and camDayAllowed and not na(time_close) and time < camScheduledEnd and time_close > camScheduledStart
bool camContainedNow = camOverlapNow and time >= camScheduledStart and time_close <= camScheduledEnd
bool camBarOhlcValid = f_camBarValid(open, high, low, close)
float camSma = ta.sma(close, smaLength)

var CamLevels camMap = CamLevels.new()
var CamLevels camPriorMap = CamLevels.new()
var int camSessionStart = na
var int camSessionEnd = na
var int camOpenTime = na
var float camOpen = na
var bool camOpenKnown = false
var bool camMapReady = false
var bool camSourceStale = false
var string camDataReason = "Waiting for first fully contained decision candle"
var string camRelationship = "Unavailable"
var string camWidthLabel = "Unavailable"
var string camOpenLocation = "OPEN UNKNOWN"
var string camDecisionLabel = "Decision window not observed"
var string camSourceLabel = "Source not frozen"
var string camCloseLabel = "Close source not frozen"
var string camSourceSelectionReason = "Waiting for source cutoff"
var int camSourceCutoff = na
var int camOpeningSourceTime = na
var int camAtrStamp = na
var float camAtrReference = na
var string camAtrBasis = "Unavailable"
var bool camAtrReady = false
var int camAtrSamples = 0
var int camAtrStart = na
var float camRangeDelta = na
var float camRangeThreshold = na
var int camBias = 0
var float camWidthRatio = na
var int camClosedBar = na
var int camClosedTime = na
var float camClosedPrice = na
var bool camInSession = false
var float camPreviousClose = na
var string camSelectedCoverage = "Waiting for 1m source snapshot"
var int camSelectedCount = 0
var int camActiveCount = 0
var int camHigherActive = 0
var int camLowerActive = 0
var int camCandidateCount = 0
var int camApproachingCount = 0
var bool camSessionEndedOnce = false
var int camPreviousObservedTime = na
var int camPreviousObservedClose = na

bool camSessionStartPulse = false
bool camSessionEndPulse = false
bool camActivated = false
bool camEnded = false
bool camArmed = false
int camActivationCount = 0
int camEndingCount = 0

if array.size(plays) == 0
    for i = 0 to 13
        array.push(plays, f_camNewPlay(i, camMap, false, "Waiting for supported chart and source opening", 0, na, false, na))

if barstate.isconfirmed
    camClosedBar := bar_index
    camClosedTime := time_close
    camClosedPrice := camBarOhlcValid ? close : na
    array.clear(camBarEvents)
    if not camChartSupported
        camDataReason := "Unsupported chart · use standard minute candles, 1m through 60m"
        camDecisionLabel := "UNSUPPORTED CHART"
        camSelectedCoverage := "DISABLED · unsupported chart"
    bool newDecision = camContainedNow and (na(camSessionStart) or camScheduledStart != camSessionStart)
    if newDecision
        for i = 0 to 13
            Play old = array.get(plays, i)
            if old.state == "ACTIVE" or old.state == "ARMED"
                f_camEnd(old, "EXPIRED", "Late-observed session rollover · no unobserved closing fill inferred", na, bar_index, time_close, false)
        camSessionStartPulse := true
        camSessionStart := camScheduledStart
        camSessionEnd := camScheduledEnd
        camSessionEndedOnce := false
        // Reconstruct cross-context data once per decision, never once per minute.
        CamSourcePacket admitted = CamSourcePacket.new(current = f_camLevels(camRawH1, camRawL1, camRawC1, camRawStart1, camRawEnd1, camRawComplete1, "Incomplete current source", false), prior = f_camLevels(camRawH2, camRawL2, camRawC2, camRawStart2, camRawEnd2, camRawComplete2, "Incomplete previous source", false))
        admitted.ready := camRawReady
        admitted.openKnown := camRawOpenKnown
        admitted.opening := camRawOpen
        admitted.openingTime := camRawOpenTime
        admitted.sessionStart := camRawSessionStart
        admitted.sessionEnd := camRawSessionEnd
        admitted.cutoff := camRawCutoff
        admitted.atr := camRawAtr
        admitted.atrStamp := camRawAtrStamp
        admitted.atrBasisCode := camRawAtrBasisCode
        admitted.atrSamples := camRawAtrSamples
        admitted.atrStart := camRawAtrStart
        admitted.atrReady := f_camAtrReady(camRawAtr, camRawAtrStamp, camRawCutoff)
        admitted.closeSourceEnd := camRawCloseEnd1
        admitted.priorCloseSourceEnd := camRawCloseEnd2
        admitted.excursion := camRawExcursion
        admitted.excursionThreshold := camRawExcursionThreshold
        admitted.sourceCode := camRawSourceCode
        admitted.closeCode := camRawCloseCode
        admitted.reasonCode := camRawReasonCode
        admitted.selectionCode := camRawSelectionCode
        admitted.sourceKind := f_camSourceName(camRawSourceCode) + (camRawSourceCode == 3 ? " · provider H/L" : " · observed 1m H/L")
        admitted.closeKind := f_camSourceName(camRawCloseCode) + (camRawCloseCode == 3 ? " · provider daily/settlement C" : " · observed last-trade C")
        admitted.reason := f_camSourceReason(camRawReasonCode)
        admitted.selectionReason := f_camSelectionReason(camRawSelectionCode)
        admitted.rthSamples := camRawRthSamples
        admitted.ethSamples := camRawEthSamples
        admitted.selectedSamples := camRawSelectedSamples
        admitted.rthComplete := camRawRthComplete
        admitted.ethComplete := camRawEthComplete
        admitted.selectedComplete := camRawSelectedComplete
        admitted.coverage := "1m source · RTH " + (camRawRthComplete ? "complete " : "incomplete ") + str.tostring(camRawRthSamples) + " bars; ETH " + (camRawEthComplete ? "complete " : "incomplete ") + str.tostring(camRawEthSamples) + "; up to 100,000 source minutes"
        camSourcePacket := admitted
        bool packetMatches = not na(camSourcePacket) and camSourcePacket.sessionStart == camSessionStart and camSourcePacket.cutoff == camSessionStart and camSourcePacket.cutoff <= time
        bool chartOpeningObserved = time == camSessionStart or not na(camPreviousObservedTime) and camPreviousObservedTime <= camSessionStart and camPreviousObservedClose >= camSessionStart
        camMap := packetMatches ? camSourcePacket.current : CamLevels.new()
        camPriorMap := packetMatches ? camSourcePacket.prior : CamLevels.new()
        camOpenKnown := packetMatches and camSourcePacket.openKnown and chartOpeningObserved
        camOpen := camOpenKnown ? camSourcePacket.opening : na
        camOpenTime := camOpenKnown ? camSourcePacket.openingTime : na
        camOpeningSourceTime := camOpenTime
        camMapReady := packetMatches and camSourcePacket.ready
        camSourceCutoff := packetMatches ? camSourcePacket.cutoff : na
        camAtrReference := packetMatches ? camSourcePacket.atr : na
        camAtrStamp := packetMatches ? camSourcePacket.atrStamp : na
        camAtrBasis := packetMatches ? f_camAtrBasisName(camSourcePacket.atrBasisCode) : "Unavailable"
        camAtrReady := packetMatches and camSourcePacket.atrReady
        camAtrSamples := packetMatches ? camSourcePacket.atrSamples : 0
        camAtrStart := packetMatches ? camSourcePacket.atrStart : na
        camSourceLabel := packetMatches ? camSourcePacket.sourceKind : "Source packet unavailable at this chart opening"
        camCloseLabel := packetMatches ? camSourcePacket.closeKind : "Unavailable"
        camSourceSelectionReason := packetMatches ? camSourcePacket.selectionReason : "No matching session snapshot admitted"
        camSelectedCoverage := packetMatches ? camSourcePacket.coverage : "Source history unavailable or packet identity differs"
        camDecisionLabel := camClockLabel
        camDataReason := not packetMatches ? "Matching frozen source unavailable at first contained chart candle" : not camSourcePacket.ready ? camSourcePacket.reason : not chartOpeningObserved ? "Chart history starts after the opening boundary · earlier bad-price blocks are unknown" : not camOpenKnown ? "Exact valid source opening unavailable" : camSourcePacket.reason
        camSourceStale := not na(camMap.sourceEnd) and camSessionStart - camMap.sourceEnd > 604800000
        [relation, geometricBias, geometricRatio, geometricWidth] = f_camRelationship(camMap, camPriorMap)
        camRelationship := camMapReady ? relation : "Unavailable"
        [newBias, ratio, widthLabel, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching] = f_camPrerequisites("HA", camMap, camPriorMap, camAtrReference, camOpen, camOpen, camOpen, false, rangeThresholdPct, widthThresholdPct, allowNeutral)
        camBias := camMapReady ? newBias : 0
        camWidthRatio := camMapReady ? ratio : na
        camWidthLabel := camMapReady ? widthLabel : "Unavailable"
        camRangeDelta := camMapReady ? camMap.c - camPriorMap.c : na
        camRangeThreshold := camAtrReference * rangeThresholdPct / 100
        camOpenLocation := f_camOpenLocation(camOpen, camMap)
        camPreviousClose := camOpen
        for i = 0 to 13
            Play p = f_camNewPlay(i, camMap, camMapReady, camDataReason, camBias, camOpen, camOpenKnown, camSessionStart)
            p.sourceKind := camSourceLabel
            p.closeKind := camCloseLabel
            p.atrUsed := camAtrReference
            p.atrStamp := camAtrStamp
            p.atrBasis := camAtrBasis
            p.atrSamples := camAtrSamples
            p.atrStart := camAtrStart
            p.sourceCutoff := camSourceCutoff
            p.closeSourceEnd := packetMatches ? camSourcePacket.closeSourceEnd : na
            p.stopMode := stopModel
            p.targetMode := targetModel
            // The source opening is the first observed OPEN, including on 60m charts.
            if camOpenKnown
                f_camRefreshPlay(p, camMap, camPriorMap, camAtrReference, camOpen, camOpen, camOpen, true, true, camMapReady, camDataReason, true)
            array.set(plays, i, p)
    camInSession := camContainedNow and not na(camSessionStart) and camScheduledStart == camSessionStart
    bool sessionEnded = not na(camSessionEnd) and time_close >= camSessionEnd
    if sessionEnded and not camSessionEndedOnce
        camSessionEndPulse := true
        camSessionEndedOnce := true
    for i = 0 to 13
        Play p = array.get(plays, i)
        bool smaReady = not smaGate or not na(camSma) and bar_index >= smaLength - 1 and f_camCompare(close, camSma) * p.dir > 0
        f_camRefreshPlay(p, camMap, camPriorMap, camAtrReference, camOpen, open, close, camBarOhlcValid, camInSession, camMapReady and camOpenKnown, camDataReason, smaReady)
        f_camRearm(p, bar_index, camInSession and not sessionEnded, allowRearm, cooldownBars, maxEpisodes)
        f_camStep(p, camMap, open, high, low, close, camPreviousClose, bar_index, time_close, camInSession, sessionEnded, camWidthLabel == "Wide", syminfo.mintick, setupDeadline, activeDeadline, stopBufferTicks, minRewardR)
    if camInSession
        camPreviousClose := camBarOhlcValid ? close : na
    camActiveCount := 0
    camHigherActive := 0
    camLowerActive := 0
    camCandidateCount := 0
    camApproachingCount := 0
    for i = 0 to 13
        Play p = array.get(plays, i)
        camActiveCount += p.state == "ACTIVE" ? 1 : 0
        camHigherActive += p.state == "ACTIVE" and p.biasRequired == 1 ? 1 : 0
        camLowerActive += p.state == "ACTIVE" and p.biasRequired == -1 ? 1 : 0
        camCandidateCount += p.candidate ? 1 : 0
        camApproachingCount += p.approaching ? 1 : 0
    if array.size(camBarEvents) > 0
        for i = 0 to array.size(camBarEvents) - 1
            CamEvent e = array.get(camBarEvents, i)
            camActivationCount += e.kind == "ACTIVE" ? 1 : 0
            camEndingCount += f_camTerminal(e.kind) ? 1 : 0
            camArmed := camArmed or e.kind == "ARMED"
    camActivated := camActivationCount > 0
    camEnded := camEndingCount > 0
    camPreviousObservedTime := time
    camPreviousObservedClose := time_close

f_camJsonString(string value) =>
    string escaped = str.replace_all(value, "\\", "\\\\")
    escaped := str.replace_all(escaped, "\"", "\\\"")
    escaped := str.replace_all(escaped, "\n", "\\n")
    escaped := str.replace_all(escaped, "\t", "\\t")
    "\"" + escaped + "\""

f_camJsonNumber(float value) =>
    f_camNumber(value)

f_camJsonTime(int value) =>
    na(value) ? "null" : str.tostring(value)

f_camJsonBool(bool value) =>
    value ? "true" : "false"

f_camSettingsId() =>
    CAM_FORMULA + "|lite2-original2.1|" + atrBasisMode + "|" + sourceMode + "|" + closeMode + "|" + decisionMode + "|" + customSession + "|" + customTimezone + "|" + f_camJsonBool(customWeekdays) + "|" + regularSession + "|" + regularTimezone + "|" + extendedClock + "|" + extendedSession + "|" + extendedTimezone + "|" + f_camJsonNumber(switchingTolerance) + "|" + f_camJsonNumber(rangeThresholdPct) + "|" + f_camJsonNumber(widthThresholdPct) + "|" + f_camJsonBool(allowNeutral) + "|" + f_camJsonBool(smaGate) + "|" + str.tostring(smaLength) + "|" + f_camJsonBool(enableA) + f_camJsonBool(enableB) + f_camJsonBool(enableC) + f_camJsonBool(enableD) + f_camJsonBool(enableE) + f_camJsonBool(enableF) + f_camJsonBool(enableG) + "|" + str.tostring(setupDeadline) + "|" + str.tostring(activeDeadline) + "|" + str.tostring(stopBufferTicks) + "|" + f_camJsonNumber(minRewardR) + "|" + stopModel + "|" + f_camJsonNumber(stopAtrMultiple) + "|" + targetModel + "|" + f_camJsonNumber(targetAtrMultiple) + "|" + f_camJsonNumber(maxTargetAtr) + "|" + f_camJsonBool(allowRearm) + "|" + str.tostring(cooldownBars) + "|" + str.tostring(maxEpisodes)

f_camEventJson(CamEvent e) =>
    string id = syminfo.tickerid + "|" + timeframe.period + "|" + f_camSettingsId() + "|" + str.tostring(e.sessionStart) + "|" + e.code + "|" + str.tostring(e.episode) + "|" + e.kind + "|" + str.tostring(e.eventTime)
    "{\"id\":" + f_camJsonString(id) + ",\"code\":" + f_camJsonString(e.code) + ",\"episode\":" + str.tostring(e.episode) + ",\"type\":" + f_camJsonString(e.kind) + ",\"direction\":" + f_camJsonString(e.dir == 1 ? "long" : "short") + ",\"time\":" + f_camJsonTime(e.eventTime) + ",\"session_start\":" + f_camJsonTime(e.sessionStart) + ",\"source_start\":" + f_camJsonTime(e.sourceStart) + ",\"source_close\":" + f_camJsonTime(e.sourceEnd) + ",\"source_kind\":" + f_camJsonString(e.sourceKind) + ",\"close_kind\":" + f_camJsonString(e.closeKind) + ",\"selected_close_time\":" + f_camJsonTime(e.closeSourceEnd) + ",\"source_cutoff\":" + f_camJsonTime(e.sourceCutoff) + ",\"atr_time\":" + f_camJsonTime(e.atrStamp) + ",\"atr_basis\":" + f_camJsonString(e.atrBasis) + ",\"atr_value\":" + f_camJsonNumber(e.atrUsed) + ",\"atr_samples\":" + str.tostring(e.atrSamples) + ",\"atr_start\":" + f_camJsonTime(e.atrStart) + ",\"price\":" + f_camJsonNumber(e.price) + ",\"entry\":" + f_camJsonNumber(e.entry) + ",\"stop\":" + f_camJsonNumber(e.stop) + ",\"target\":" + f_camJsonNumber(e.target) + ",\"ambiguous\":" + f_camJsonBool(e.ambiguous) + ",\"reason\":" + f_camJsonString(e.reason) + "}"

if barstate.isconfirmed and array.size(camBarEvents) > 0
    string selectedEvents = ""
    int selectedCount = 0
    for i = 0 to array.size(camBarEvents) - 1
        CamEvent e = array.get(camBarEvents, i)
        bool include = e.kind == "ACTIVE" ? alertActivation : e.kind == "ARMED" ? alertArmed : alertEnding and f_camTerminal(e.kind)
        if include
            selectedEvents += (selectedCount > 0 ? "," : "") + f_camEventJson(e)
            selectedCount += 1
    if selectedCount > 0
        string envelope = "{\"schema\":\"camarilla-desk.v2\",\"product_version\":" + f_camJsonString(CAM_VERSION) + ",\"rule_version\":\"lite2-prerequisites.original2.1-execution\",\"formula\":" + f_camJsonString(CAM_FORMULA) + ",\"settings_id\":" + f_camJsonString(f_camSettingsId()) + ",\"source_mode\":" + f_camJsonString(sourceMode) + ",\"decision_mode\":" + f_camJsonString(decisionMode) + ",\"symbol\":" + f_camJsonString(syminfo.tickerid) + ",\"timeframe\":" + f_camJsonString(timeframe.period) + ",\"confirmed_time\":" + f_camJsonTime(time_close) + ",\"fill_confirmed\":false,\"events\":[" + selectedEvents + "]}"
        alert(envelope, alert.freq_once_per_bar_close)

alertcondition(camActivated, "Camarilla play activated", "One or more original Camarilla hypotheses activated on the confirmed close. Reference plans are not order fills. Use Any alert() function call for the aggregate JSON envelope.")
alertcondition(camEnded, "Camarilla episode ended", "One or more Camarilla episodes ended on the confirmed update. Use Any alert() function call for complete event details.")

// VIEW CONTRACT v2: tracked immutable plans are independent from manual inspection.
// Chart-first Ribbon and complete seven-level map. Clean adds plan detail.
// Display inputs do not mutate candidates, episodes, alerts or reference prices.
string vBoardMode = input.string("Ribbon", "Trade HUD", options = ["Ribbon", "Clean", "Context", "Audit", "Off"], group = "Display", tooltip = "Ribbon: compact source, context and current action. Clean: expanded tracked plan. Context: up to five attention rows. Audit: all fourteen plays. Attention priority is not a strength score.", display = display.none)
string vDetail = input.string("Off", "Inspector", options = ["Off", "Brief", "Standard"], group = "Display", tooltip = "Inspect any play without replacing the independently tracked chart plan. Standard adds prerequisites and source evidence.", display = display.none)
string vTheme = input.string("Trading Dark", "Theme", options = ["Trading Dark", "Accessible Dark", "Accessible Light", "Aurora", "Royal Gold"], group = "Display", display = display.none)
string vChartMode = input.string("Map", "Chart density", options = ["Map", "Clean", "Context", "Audit", "Off"], group = "Display", tooltip = "Map: complete seven-level map plus fixed plan and episode markers. Clean: selected plan/trigger only. Context: map and optional middle band. Audit: adds bounded event observations. Off hides drawings only.", display = display.none)
string vScale = input.string("Medium", "Panel & chart tag size", options = ["Small", "Medium", "Large"], group = "Display", active = CAM_IS_LAB or vBoardMode != "Off" or vDetail != "Off" or vChartMode != "Off", tooltip = "Also controls Strategy Lab readout size.", display = display.none)
string vFocusCode = input.string("Auto", "Inspect play", options = ["Auto", "HA", "LA", "HB", "LB", "HC", "LC", "HD", "LD", "HE", "LE", "HF", "LF", "HG", "LG"], group = "Display", active = vDetail != "Off", tooltip = "Auto: active, armed, approaching, then candidate, with fixed-code ties. A manual code changes only the Inspector. H/L describes context, not trade direction.", display = display.none)
bool vCodes = input.bool(false, "Show play codes", group = "Display", display = display.none)
string vBoardCorner = input.string("Top Left", "Trade HUD position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = "Display · placement", active = vBoardMode != "Off", display = display.none)
string vFocusCorner = input.string("Bottom Left", "Inspector position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = "Display · placement", active = vDetail != "Off", tooltip = "Audit reserves its entire column, so the Inspector moves to the other side. Other HUD modes use the free corner on the same side first. Hidden panels reserve no anchor.", display = display.none)
int vHeaderClearance = input.int(12, "Top Left header clearance · %", minval = 0, maxval = 20, group = "Display · placement", tooltip = "Transparent space beneath the native legend; zero if hidden. Bottom Left has a transparent footer above the watermark. These are layout heuristics.", display = display.none)
bool vPriceTags = input.bool(true, "Price tags", group = "Display · chart", active = vChartMode != "Off", tooltip = "Displaced tags keep true-price leaders and unchanged values. Tracked references receive first priority.", display = display.none)
bool vMiddleBand = input.bool(false, "Camarilla middle band", group = "Display · chart", active = vChartMode == "Context" or vChartMode == "Audit", tooltip = "L2–H2 Camarilla band, not floor-pivot CPR.", display = display.none)
bool vPlanZones = input.bool(true, "Reference risk / reward zones", group = "Display · chart", active = vChartMode != "Off", tooltip = "Frozen reference geometry, not fills or returns. Ended geometry stops at its recorded end time.", display = display.none)
int vHistory = input.int(2, "Ended plans shown", minval = 0, maxval = 8, group = "Display · chart", active = vChartMode != "Off", tooltip = "Newest ended snapshots within visible time and nearby price context. Historical prices are frozen. Zero hides ended geometry.", display = display.none)
int vTagCap = input.int(12, "Chart tag budget", minval = 3, maxval = 24, group = "Display · chart", active = vChartMode != "Off" and vPriceTags, tooltip = "Tracked entry, stop and target come first. Default covers the seven map tags plus tracked references. Episode markers use a separate bounded pool.", display = display.none)
string vTagSide = input.string("Right", "Price tag side", options = ["Right", "Left"], group = "Display · chart", active = vChartMode != "Off" and vPriceTags, tooltip = "Right aligns solid price tags beside the current chart. Left restores interior labels. Colliding labels retain their printed prices and true-price leaders.", display = display.none)
bool vEpisodeMarks = input.bool(true, "Entry / ending markers", group = "Display · chart", active = vChartMode != "Off", tooltip = "Local arrows mark confirmed reference entries. Ending labels show TARGET, STOP or EXPIRED at the recorded observation, not broker fills. A ? suffix discloses ambiguous OHLC paths.", display = display.none)
int vZoneOpacity = input.int(14, "Plan zone opacity · %", minval = 4, maxval = 35, group = "Display · chart", active = vChartMode != "Off" and vPlanZones, tooltip = "Visual contrast only. It does not change price references, signal logic or results.", display = display.none)

bool vLight = vTheme == "Accessible Light"
color vBg = vLight ? color.rgb(246, 248, 252) : vTheme == "Royal Gold" ? color.rgb(21, 23, 28) : color.rgb(16, 24, 38)
color vStripe = vLight ? color.rgb(233, 239, 247) : vTheme == "Royal Gold" ? color.rgb(32, 35, 43) : color.rgb(23, 35, 52)
color vHeader = vLight ? color.rgb(220, 230, 242) : vTheme == "Royal Gold" ? color.rgb(46, 43, 36) : color.rgb(30, 47, 68)
color vText = vLight ? color.rgb(24, 36, 53) : color.rgb(236, 243, 251)
color vMuted = vLight ? color.rgb(82, 98, 118) : color.rgb(168, 184, 204)
color vAccent = vTheme == "Trading Dark" ? color.rgb(105, 164, 255) : vTheme == "Royal Gold" ? color.rgb(226, 197, 137) : vTheme == "Aurora" ? color.rgb(190, 187, 237) : vLight ? color.rgb(39, 68, 100) : color.rgb(205, 225, 246)
color vUp = vTheme == "Trading Dark" ? color.rgb(66, 190, 133) : vLight ? color.rgb(0, 105, 165) : vTheme == "Aurora" ? color.rgb(95, 207, 239) : color.rgb(92, 184, 239)
color vDown = vTheme == "Trading Dark" ? color.rgb(240, 101, 119) : vLight ? color.rgb(151, 85, 0) : color.rgb(237, 171, 76)
color vFrame = vLight ? color.rgb(184, 200, 221) : color.rgb(53, 71, 93)
int vBodySize = vScale == "Small" ? 12 : vScale == "Large" ? 16 : 14
int vSmallSize = vScale == "Small" ? 10 : vScale == "Large" ? 14 : 12
int vTitleSize = vScale == "Small" ? 14 : vScale == "Large" ? 18 : 16
int vTagSize = vScale == "Small" ? 10 : vScale == "Large" ? 14 : 12

f_vWrap(string value, int width, int maxLines) =>
    string result = ""
    string current = ""
    int rows = 1
    bool truncated = false
    for word in str.split(value, " ")
        if not truncated
            if str.length(current) + str.length(word) + 1 > width and str.length(current) > 0
                if rows < maxLines
                    result += current + "\n"
                    current := word
                    rows += 1
                else
                    truncated := true
            else
                current += (str.length(current) > 0 ? " " : "") + word
    result + current + (truncated ? "…" : "")
f_vPosition(string choice) =>
    switch choice
        "Top Left" => position.top_left
        "Bottom Left" => position.bottom_left
        "Bottom Right" => position.bottom_right
        "Top Center" => position.top_center
        "Bottom Center" => position.bottom_center
        "Middle Left" => position.middle_left
        "Middle Right" => position.middle_right
        "Middle Center" => position.middle_center
        => position.top_right
// Public view API for the Lab tail: occupied anchors include the entire column
// of a tall Audit HUD, so downstream panels cannot overlap its fourteen rows.
f_vFree(array<string> used, string preferred) =>
    string result = preferred
    if array.includes(used, result)
        string paired = preferred == "Top Left" ? "Bottom Left" : preferred == "Bottom Left" ? "Top Left" : preferred == "Top Right" ? "Bottom Right" : "Top Right"
        string across = preferred == "Top Left" ? "Top Right" : preferred == "Bottom Left" ? "Bottom Right" : preferred == "Top Right" ? "Top Left" : "Bottom Left"
        array<string> order = array.from(paired, across, "Top Left", "Bottom Left", "Top Right", "Bottom Right", "Middle Left", "Middle Right", "Top Center", "Bottom Center", "Middle Center")
        for candidate in order
            if not array.includes(used, candidate)
                result := candidate
                break
    array.push(used, result)
    result
f_vReserveColumn(array<string> used, string anchor) =>
    bool left = str.contains(anchor, "Left")
    array<string> column = left ? array.from("Top Left", "Bottom Left", "Middle Left") : array.from("Top Right", "Bottom Right", "Middle Right")
    for reserved in column
        if not array.includes(used, reserved)
            array.push(used, reserved)
    true
var array<string> vUsedAnchors = array.new<string>()
var string vBoardAnchor = ""
var string vFocusAnchor = ""
if barstate.isfirst
    if vBoardMode != "Off"
        vBoardAnchor := f_vFree(vUsedAnchors, vBoardCorner)
        if vBoardMode == "Audit"
            f_vReserveColumn(vUsedAnchors, vBoardAnchor)
    if vDetail != "Off"
        vFocusAnchor := f_vFree(vUsedAnchors, vFocusCorner)
f_vTime(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "MM-dd HH:mm", "UTC")
f_vDate(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "yyyy-MM-dd", "UTC")
f_vClock(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "HH:mm", "UTC")
f_vSourceShort(string value) =>
    str.contains(value, "SYNTHETIC") ? "TEST RTH" : str.contains(value, "RTH") ? "RTH" : str.contains(value, "ETH") ? "ETH" : str.contains(value, "daily") ? "Daily" : str.contains(value, "Selected") ? "Custom" : "Waiting"
f_vClockShort(string value) =>
    str.contains(value, "SYNTHETIC") ? "TEST" : str.contains(value, "Exchange") ? "Exchange day" : str.contains(value, "cash") ? "US cash" : "Custom"
f_vInk(color tint) =>
    color.r(tint) * 0.2126 + color.g(tint) * 0.7152 + color.b(tint) * 0.0722 > 105 ? color.rgb(10, 18, 28) : color.white
f_vDirection(int direction) =>
    direction == 1 ? "LONG" : direction == -1 ? "SHORT" : "—"
f_vState(Play play, bool inWindow) =>
    play.state == "WATCH" ? not inWindow ? "WAIT SESSION" : not camBarOhlcValid ? "DATA WAIT" : not play.candidate ? "BLOCKED" : play.approaching ? "APPROACHING" : "CANDIDATE" : play.state
f_vStateColor(string state, int direction) =>
    state == "ACTIVE" ? (direction == 1 ? vUp : vDown) : state == "ARMED" or state == "APPROACHING" ? vAccent : state == "STOP" or state == "INVALIDATED" or state == "REJECTED" ? vDown : state == "TARGET" ? vUp : vMuted
f_vShortName(string family, int direction) =>
    switch family
        "A" => direction == 1 ? "L3 traverse" : "H3 traverse"
        "B" => direction == 1 ? "H4 break & retest" : "L4 break & retest"
        "C" => "Opening continuation"
        "D" => "Context-opposed continuation"
        "E" => direction == 1 ? "L4 failure" : "H4 failure"
        "F" => "Outer-tier rejection"
        "G" => "Opposite-side rejection"
        => "Session guide"
f_vName(Play play) =>
    (vCodes ? play.code + " · " : "") + f_vShortName(play.family, play.dir)
f_vSnapshotName(CamPlanSnapshot plan) =>
    (vCodes ? plan.code + " · " : "") + f_vShortName(str.substring(plan.code, 1), plan.dir)
f_vAge(Play play) =>
    int start = play.state == "ACTIVE" ? play.activationBar : play.state == "ARMED" ? play.setupBar : na
    na(start) or na(camClosedBar) ? "" : str.tostring(math.max(0, camClosedBar - start)) + " bars"
f_vTrigger(Play play) =>
    na(play.triggerPrice) ? "No close trigger yet" : "close " + play.triggerOperator + " " + f_camReadable(play.triggerPrice)
f_vPlayTip(Play play) =>
    play.code + " · " + play.name + "\n" + f_vDirection(play.dir) + " · " + f_vState(play, camInSession) + "\nWHY: " + play.reason + "\nNEXT: " + play.next + "\nFAIL: " + play.invalidation + "\nClose trigger: " + f_vTrigger(play) + "\nSetup / activation UTC: " + f_vTime(play.setupTime) + " / " + f_vTime(play.activationTime)
f_vGateWords(Play play) =>
    "Range " + (play.rangeOk ? "PASS" : "BLOCK") + " · Width " + (play.widthOk ? "PASS" : "BLOCK") + "\nOpening " + (play.openOk ? "PASS" : "BLOCK") + " · Region " + (play.regionOk ? "IN" : "OUT") + "\nSMA " + (not smaGate ? "OFF" : play.smaOk ? "PASS" : "BLOCK")
f_vTrackedIndex() =>
    int found = -1
    if array.size(camPlans) > 0
        for i = 0 to array.size(camPlans) - 1
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state == "ACTIVE"
                found := i
                break
    found
f_vFallbackFocus() =>
    int selected = f_camAutoFocus()
    if selected < 0 and camInSession and array.size(plays) > 0
        for priority = 0 to 2
            if selected < 0
                for i = 0 to array.size(plays) - 1
                    Play play = array.get(plays, i)
                    bool match = priority == 0 ? play.state == "ARMED" : priority == 1 ? play.state == "WATCH" and play.approaching : play.state == "WATCH" and play.candidate
                    if match
                        selected := i
                        break
    selected
f_vRows(bool audit) =>
    array<int> rows = array.new<int>()
    if array.size(plays) > 0
        if audit
            for i = 0 to array.size(plays) - 1
                array.push(rows, i)
        else
            for priority = 0 to 3
                for i = 0 to array.size(plays) - 1
                    Play play = array.get(plays, i)
                    bool match = priority == 0 ? play.state == "ACTIVE" : priority == 1 ? play.state == "ARMED" : priority == 2 ? play.state == "WATCH" and play.approaching : play.state == "WATCH" and play.candidate
                    if match and array.size(rows) < 5 and not array.includes(rows, i)
                        array.push(rows, i)
    rows
f_vRecovery() =>
    not camChartSupported ? "Use standard, time-based candles from 1 to 60 minutes." : not camOpenKnown ? "Load the observed decision opening and complete prior source windows." : not camMapReady ? camDataReason : not camInSession ? "Wait for the selected decision window. The last committed map remains inspectable." : "No qualifying setup. Inspect Audit for each prerequisite."
f_vSourceTip() =>
    "PUBLIC CONTEXT\nSource: " + camSourceLabel + " · close: " + camCloseLabel + "\n" + camSourceSelectionReason + "\nDecision: " + camDecisionLabel + "\nMap source UTC: " + f_vTime(camMap.sourceStart) + " → " + f_vTime(camMap.sourceEnd) + "\nSelection cutoff: " + f_vTime(camSourceCutoff) + "\nObserved 1m opening: " + f_vTime(camOpeningSourceTime) + " @ " + f_camExact(camOpen) + "\nH/L/C: " + f_camExact(camMap.h) + " / " + f_camExact(camMap.l) + " / " + f_camExact(camMap.c) + "\n" + camDataReason
f_vRiskTip() =>
    "ORIGINAL EXECUTION\nMinimum R: " + str.tostring(minRewardR) + " · buffer " + str.tostring(stopBufferTicks) + " ticks\nStop: " + stopModel + " · ATR " + str.tostring(stopAtrMultiple) + "\nTarget: " + targetModel + " · ATR " + str.tostring(targetAtrMultiple) + "\nMax target ATR: " + (maxTargetAtr == 0 ? "OFF" : str.tostring(maxTargetAtr)) + "\nSetup / active limits: " + str.tostring(setupDeadline) + " / " + str.tostring(activeDeadline) + " bars\nRearm: " + (allowRearm ? "ON · cooldown " + str.tostring(cooldownBars) + " · cap " + str.tostring(maxEpisodes) : "OFF") + "\nChart SMA: " + (smaGate ? str.tostring(smaLength) : "OFF") + "\nReference plans, not broker fills or a validated edge."

type CamViewRow
    string key = ""
    string value = ""
    string tip = ""
    int kind = 0
f_vAdd(array<CamViewRow> rows, string key, string value, string tip, int kind = 0) =>
    array.push(rows, CamViewRow.new(key, value, tip, kind))
f_vSignature(array<CamViewRow> rows) =>
    string result = ""
    for row in rows
        result += str.tostring(row.kind)
    result
f_vInsetStart(array<CamViewRow> rows, string anchor) =>
    if anchor == "Top Left" and vHeaderClearance > 0
        f_vAdd(rows, "", "", "Transparent chart-header clearance", 5)
f_vInsetEnd(array<CamViewRow> rows, string anchor) =>
    if anchor == "Bottom Left"
        f_vAdd(rows, "", "", "Transparent watermark clearance", 5)
f_vRender(table panel, array<CamViewRow> rows, string anchor) =>
    table.clear(panel, 0, 0, 1, array.size(rows) - 1)
    for i = 0 to array.size(rows) - 1
        CamViewRow row = array.get(rows, i)
        color background = row.kind == 5 ? color.new(vBg, 100) : row.kind == 2 ? vHeader : row.kind == 4 ? vStripe : vBg
        if row.kind == 1
            table.cell(panel, 0, i, row.key, text_color = vMuted, bgcolor = background, text_size = vSmallSize, text_halign = text.align_left, text_font_family = font.family_default, tooltip = row.tip)
            table.cell(panel, 1, i, row.value, text_color = vText, bgcolor = background, text_size = vBodySize, text_halign = text.align_right, text_font_family = font.family_monospace, text_formatting = text.format_bold, tooltip = row.tip)
        else
            // clear() may remove merges: reapply before every full-row write.
            table.merge_cells(panel, 0, i, 1, i)
            table.cell(panel, 0, i, row.value, height = row.kind == 5 ? (anchor == "Top Left" ? (vBoardMode == "Audit" ? math.min(vHeaderClearance, 8) : vHeaderClearance) : 5) : 0, text_color = row.kind == 3 ? vMuted : row.kind == 2 or row.kind == 4 ? vAccent : vText, bgcolor = background, text_size = row.kind == 2 ? vTitleSize : row.kind == 3 ? vSmallSize : vBodySize, text_halign = text.align_left, text_font_family = font.family_default, text_formatting = row.kind == 2 or row.kind == 4 ? text.format_bold : text.format_none, tooltip = row.tip)

// Setter pools survive ordinary price/text changes; surplus handles are deleted.
f_vLine(array<line> pool, int slot, int x1, float y1, int x2, float y2, color tint, string style, int width = 1) =>
    line item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        line.set_xy1(item, x1, y1)
        line.set_xy2(item, x2, y2)
        line.set_color(item, tint)
        line.set_style(item, style)
        line.set_width(item, width)
    else
        item := line.new(x1, y1, x2, y2, xloc = xloc.bar_time, color = tint, style = style, width = width)
        array.push(pool, item)
    item
f_vLabel(array<label> pool, int slot, int stamp, float price, string value, color tint, string tip, bool faceLeft = false, bool solid = false) =>
    label item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        label.set_xy(item, stamp, price)
        label.set_text(item, value)
        label.set_color(item, solid ? tint : vBg)
        label.set_textcolor(item, solid ? f_vInk(tint) : tint)
        label.set_size(item, vTagSize)
        label.set_style(item, faceLeft ? label.style_label_right : label.style_label_left)
        label.set_tooltip(item, tip)
    else
        item := label.new(stamp, price, value, xloc = xloc.bar_time, style = faceLeft ? label.style_label_right : label.style_label_left, color = solid ? tint : vBg, textcolor = solid ? f_vInk(tint) : tint, size = vTagSize, text_font_family = font.family_default, tooltip = tip)
        array.push(pool, item)
    item
f_vMarker(array<label> pool, int slot, int stamp, float price, string value, color tint, string tip, string style) =>
    label item = f_vLabel(pool, slot, stamp, price, value, tint, tip, false, true)
    label.set_style(item, style)
    item
f_vBox(array<box> pool, int slot, int left, float top, int right, float bottom, color tint) =>
    box item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        box.set_lefttop(item, left, top)
        box.set_rightbottom(item, right, bottom)
        box.set_bgcolor(item, tint)
        box.set_border_color(item, color.new(tint, 100))
    else
        item := box.new(left, top, right, bottom, xloc = xloc.bar_time, bgcolor = tint, border_color = color.new(tint, 100))
        array.push(pool, item)
    item
f_vTagY(array<float> used, float price, float gap) =>
    float result = price
    for attempt = 0 to 24
        float candidate = attempt == 0 ? price : price + (attempt % 2 == 1 ? 1 : -1) * math.ceil(attempt / 2.0) * gap
        bool clear = true
        for previous in used
            if math.abs(candidate - previous) < gap * 0.95
                clear := false
        if clear
            result := candidate
            break
    array.push(used, result)
    result
f_vExtent(array<float> bounds, float price) =>
    if not na(price)
        array.set(bounds, 0, math.min(array.get(bounds, 0), price))
        array.set(bounds, 1, math.max(array.get(bounds, 1), price))
    true
f_vTagGap(float renderedRange, float tick, int fontPixels) =>
    // Bounded display heuristic: map/history lines also expand autoscale. Pine
    // exposes no pane pixel height, so this is not a universal collision proof.
    float fraction = math.min(0.07, math.max(0.035, fontPixels * 0.004))
    math.max(renderedRange * fraction, tick * 4)
float vFallbackRange = ta.highest(high[1], 80) - ta.lowest(low[1], 80)
var float vVisibleHigh = na
var float vVisibleLow = na
if time == chart.left_visible_bar_time
    vVisibleHigh := high
    vVisibleLow := low
else if time > chart.left_visible_bar_time and time <= chart.right_visible_bar_time
    vVisibleHigh := math.max(nz(vVisibleHigh, high), high)
    vVisibleLow := math.min(nz(vVisibleLow, low), low)
var array<line> vLines = array.new<line>()
var array<label> vLabels = array.new<label>()
var array<label> vMarkers = array.new<label>()
var array<box> vBoxes = array.new<box>()
var table vBoard = na
var table vFocus = na
var string vBoardSignature = ""
var string vFocusSignature = ""

// VIEW RENDER — exact fixture insertion boundary; no production markers precede it.
if (barstate.islastconfirmedhistory or barstate.isrealtime) and (vBoardMode != "Off" or vDetail != "Off" or vChartMode != "Off")
    int vTrackIndex = f_vTrackedIndex()
    bool vTracking = vTrackIndex >= 0
    CamPlanSnapshot vTracked = vTracking ? array.get(camPlans, vTrackIndex) : CamPlanSnapshot.new()
    int vNextIndex = f_vFallbackFocus()
    Play vUpcoming = vNextIndex >= 0 ? array.get(plays, vNextIndex) : Play.new()
    int vInspectIndex = vFocusCode == "Auto" ? vNextIndex : f_camCodeIndex(vFocusCode)
    bool vHasUpcoming = vNextIndex >= 0 and not vTracking
    string vSourceTip = f_vSourceTip()
    string vRiskTip = f_vRiskTip()
    string vClosedTip = "Last committed chart close, UTC: " + f_vTime(camClosedTime) + "\nPrice: " + f_camExact(camClosedPrice) + "\nForming quotes do not update this value."
    int vBarMs = int(timeframe.in_seconds() * 1000)
    int vNow = na(camClosedTime) ? time : camClosedTime
    int vRight = vNow + vBarMs * 4
    int vLeft = nz(chart.left_visible_bar_time, vNow - vBarMs * 60)
    int vVisibleRight = math.min(vNow, nz(chart.right_visible_bar_time, vNow))
    bool vLeftPanel = array.includes(vUsedAnchors, "Top Left") or array.includes(vUsedAnchors, "Bottom Left") or array.includes(vUsedAnchors, "Middle Left")
    int vTagTime = vTagSide == "Right" ? vRight : vLeft + int(math.max(0, vVisibleRight - vLeft) * (vLeftPanel ? 0.45 : 0.12))
    float vRange = math.max(nz(vVisibleHigh - vVisibleLow, nz(vFallbackRange, syminfo.mintick * 100)), syminfo.mintick * 20)
    int vLineCount = 0
    int vLabelCount = 0
    int vMarkerCount = 0
    int vBoxCount = 0
    int vHistoryShown = 0
    int vHistoryOmitted = 0
    int vContextOmitted = 0
    array<float> vOccupied = array.new<float>()
    bool vCanMap = camChartSupported and camMapReady and camMap.valid and not na(camSessionStart) and vChartMode != "Off"
    float vFocusLevel = vTracking ? vTracked.triggerPrice : vHasUpcoming ? vUpcoming.triggerPrice : na
    int vRoleDirection = vTracking ? vTracked.dir : vHasUpcoming ? vUpcoming.dir : 0
    // Admit before drawing. Candle range still controls the existing history
    // proximity filter; the wider rendered extent controls label spacing only.
    array<float> vExtent = array.from(nz(vVisibleLow, nz(camClosedPrice, close) - vRange * 0.5), nz(vVisibleHigh, nz(camClosedPrice, close) + vRange * 0.5))
    array<float> vMapLevels = array.from(camMap.l6, camMap.l4, camMap.l3, camMap.c, camMap.h3, camMap.h4, camMap.h6)
    array<bool> vMapVisible = array.new_bool(7, false)
    array<int> vHistoryIndices = array.new_int()
    array<int> vAuditIndices = array.new_int()
    int vReservedTags = 0
    if vChartMode != "Off" and vTracking
        for price in array.from(vTracked.entry, vTracked.stop, vTracked.target)
            f_vExtent(vExtent, price)
        vReservedTags := vPriceTags ? 3 : 0
    if vCanMap
        for pass = 0 to 1
            for j = 0 to 6
                float price = array.get(vMapLevels, j)
                bool focused = not na(vFocusLevel) and f_camCompare(price, vFocusLevel) == 0
                bool visible = not na(price) and (vChartMode != "Clean" or focused or (not vTracking and not vHasUpcoming and j == 3))
                if visible and ((pass == 0 and focused) or (pass == 1 and not focused))
                    array.set(vMapVisible, j, true)
                    f_vExtent(vExtent, price)
                    if vPriceTags and vReservedTags < vTagCap
                        vReservedTags += 1
        if vMiddleBand and (vChartMode == "Context" or vChartMode == "Audit")
            f_vExtent(vExtent, camMap.l2)
            f_vExtent(vExtent, camMap.h2)
    if vChartMode != "Off" and array.size(camPlans) > 0
        for i = 0 to array.size(camPlans) - 1
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state != "ACTIVE" and not na(plan.endTime) and not na(plan.activationTime)
                bool timeVisible = plan.endTime >= vLeft and plan.activationTime <= vVisibleRight
                bool priceNear = math.max(plan.entry, math.max(plan.stop, plan.target)) <= nz(vVisibleHigh, plan.entry) + vRange and math.min(plan.entry, math.min(plan.stop, plan.target)) >= nz(vVisibleLow, plan.entry) - vRange
                if timeVisible and priceNear and array.size(vHistoryIndices) < vHistory
                    array.push(vHistoryIndices, i)
                    for price in array.from(plan.entry, plan.stop, plan.target)
                        f_vExtent(vExtent, price)
                    if vEpisodeMarks
                        f_vExtent(vExtent, nz(plan.outcomePrice, plan.entry))
                    if vPriceTags and not vEpisodeMarks and vReservedTags < vTagCap
                        vReservedTags += 1
                else
                    vHistoryOmitted += 1
    if vChartMode == "Audit" and vPriceTags and array.size(camEvents) > 0
        for i = 0 to math.min(3, array.size(camEvents) - 1)
            CamEvent event = array.get(camEvents, i)
            if vReservedTags < vTagCap and event.eventTime >= vLeft and event.eventTime <= vVisibleRight and not na(event.price)
                array.push(vAuditIndices, i)
                f_vExtent(vExtent, event.price)
                vReservedTags += 1
    float vRenderedRange = math.max(array.get(vExtent, 1) - array.get(vExtent, 0), syminfo.mintick * 20)
    float vGap = f_vTagGap(vRenderedRange, syminfo.mintick, vTagSize)
    // Reserve marker text rows before right/left-column price tags. A local
    // ending can share H4/L3 exactly; displace only the tag with its true leader.
    if vChartMode != "Off" and vEpisodeMarks
        if vTracking and vTracked.activationTime >= vLeft
            array.push(vOccupied, vTracked.entry + (vTracked.dir == 1 ? -0.6 : 0.6) * vGap)
        for i in vHistoryIndices
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.activationTime >= vLeft
                array.push(vOccupied, plan.entry + (plan.dir == 1 ? -0.6 : 0.6) * vGap)
            array.push(vOccupied, nz(plan.outcomePrice, plan.state == "STOP" ? plan.stop : plan.state == "TARGET" ? plan.target : plan.entry))
    // Manual vInspectIndex cannot replace this immutable tracked snapshot.
    if vChartMode != "Off" and vTracking
        array<float> prices = array.from(vTracked.entry, vTracked.stop, vTracked.target)
        array<string> names = array.from("PLAN ENTRY REF", "PLAN STOP", "PLAN TARGET")
        array<color> colors = array.from(vAccent, vDown, vUp)
        for j = 0 to 2
            float price = array.get(prices, j)
            color tint = array.get(colors, j)
            f_vLine(vLines, vLineCount, vTracked.activationTime, price, vRight, price, tint, line.style_solid, j == 0 ? 2 : 1)
            vLineCount += 1
            if vTagTime < vTracked.activationTime
                f_vLine(vLines, vLineCount, vTagTime, price, vTracked.activationTime, price, color.new(tint, 45), line.style_dotted)
                vLineCount += 1
            if vPriceTags and vLabelCount < vTagCap
                float tagY = f_vTagY(vOccupied, price, vGap)
                string tip = "Tracked " + vTracked.code + " · episode " + str.tostring(vTracked.episode) + "\n" + array.get(names, j) + " " + f_camExact(price) + "\nFrozen at " + f_vTime(vTracked.activationTime) + " UTC. Dotted left extension is a present reference guide, not historical availability."
                f_vLabel(vLabels, vLabelCount, vTagTime, tagY, array.get(names, j) + " " + f_camPrice(price), tint, tip, vTagSide == "Right", vTagSide == "Right")
                vLabelCount += 1
                if tagY != price
                    f_vLine(vLines, vLineCount, vTagTime, price, vTagTime, tagY, color.new(tint, 40), line.style_dotted)
                    vLineCount += 1
        if vPlanZones
            f_vBox(vBoxes, vBoxCount, vTracked.activationTime, math.max(vTracked.entry, vTracked.stop), vRight, math.min(vTracked.entry, vTracked.stop), color.new(vDown, 100 - vZoneOpacity))
            vBoxCount += 1
            f_vBox(vBoxes, vBoxCount, vTracked.activationTime, math.max(vTracked.entry, vTracked.target), vRight, math.min(vTracked.entry, vTracked.target), color.new(vUp, 100 - vZoneOpacity))
            vBoxCount += 1
        if vEpisodeMarks and vTracked.activationTime >= vLeft and vTracked.activationTime <= vVisibleRight
            string tip = "REFERENCE ENTRY · " + vTracked.code + " · episode " + str.tostring(vTracked.episode) + "\nConfirmed " + f_vTime(vTracked.activationTime) + " UTC @ " + f_camExact(vTracked.entry) + "\nFrozen stop / target " + f_camExact(vTracked.stop) + " / " + f_camExact(vTracked.target) + "\n" + vTracked.reason + "\nNot a broker fill or a fresh entry at the latest quote."
            f_vMarker(vMarkers, vMarkerCount, vTracked.activationTime, vTracked.entry, vTracked.code + (vTracked.dir == 1 ? " ▲ ENTRY" : " ▼ ENTRY"), vTracked.dir == 1 ? vUp : vDown, tip, vTracked.dir == 1 ? label.style_label_up : label.style_label_down)
            vMarkerCount += 1
    // Role color belongs to the relevant play, never the current quote's position.
    if vCanMap
        array<string> codes = array.from("L6", "L4", "L3", "PC", "H3", "H4", "H6")
        for pass = 0 to 1
            for j = 0 to 6
                float price = array.get(vMapLevels, j)
                bool focused = not na(vFocusLevel) and f_camCompare(price, vFocusLevel) == 0
                bool visible = array.get(vMapVisible, j)
                if visible and ((pass == 0 and focused) or (pass == 1 and not focused))
                    color tint = focused and vRoleDirection != 0 ? (vRoleDirection == 1 ? vUp : vDown) : j < 3 ? vUp : j > 3 ? vDown : vAccent
                    f_vLine(vLines, vLineCount, camSessionStart, price, vRight, price, color.new(tint, focused ? 0 : vChartMode == "Map" ? 20 : 50), vChartMode == "Map" or focused ? line.style_solid : line.style_dotted, focused or j == 2 or j == 4 ? 2 : 1)
                    vLineCount += 1
                    if vPriceTags
                        if vLabelCount < vTagCap
                            float tagY = f_vTagY(vOccupied, price, vGap)
                            string tip = "CURRENT MAP · " + array.get(codes, j) + " " + f_camExact(price) + "\nMap usable at decision opening " + f_vTime(camSessionStart) + " UTC. A dotted left guide is a current reference only.\n" + vSourceTip
                            f_vLabel(vLabels, vLabelCount, vTagTime, tagY, (focused ? "TRIGGER " : "") + array.get(codes, j) + " " + f_camPrice(price), tint, tip, vTagSide == "Right", vTagSide == "Right")
                            vLabelCount += 1
                            if vTagTime < camSessionStart
                                f_vLine(vLines, vLineCount, vTagTime, price, camSessionStart, price, color.new(tint, 70), line.style_dotted)
                                vLineCount += 1
                            if tagY != price
                                f_vLine(vLines, vLineCount, vTagTime, price, vTagTime, tagY, color.new(tint, 45), line.style_dotted)
                                vLineCount += 1
                        else
                            vContextOmitted += 1
        if vMiddleBand and (vChartMode == "Context" or vChartMode == "Audit")
            f_vBox(vBoxes, vBoxCount, camSessionStart, camMap.h2, vRight, camMap.l2, color.new(vAccent, 96))
            vBoxCount += 1
    // Ended geometry uses its own snapshot and stops exactly at its endTime.
    if array.size(vHistoryIndices) > 0
        for i in vHistoryIndices
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state != "ACTIVE"
                if vHistoryShown < vHistory
                    vHistoryShown += 1
                    array<float> prices = array.from(plan.entry, plan.stop, plan.target)
                    for price in prices
                        color endedTint = price == plan.stop ? vDown : price == plan.target ? vUp : vAccent
                        f_vLine(vLines, vLineCount, plan.activationTime, price, plan.endTime, price, color.new(endedTint, 35), line.style_dashed)
                        vLineCount += 1
                    if vPlanZones
                        f_vBox(vBoxes, vBoxCount, plan.activationTime, math.max(plan.entry, plan.stop), plan.endTime, math.min(plan.entry, plan.stop), color.new(vDown, 100 - math.max(4, vZoneOpacity - 4)))
                        vBoxCount += 1
                        f_vBox(vBoxes, vBoxCount, plan.activationTime, math.max(plan.entry, plan.target), plan.endTime, math.min(plan.entry, plan.target), color.new(vUp, 100 - math.max(4, vZoneOpacity - 4)))
                        vBoxCount += 1
                    if vEpisodeMarks
                        string markerTip = "REFERENCE EPISODE · " + plan.code + " #" + str.tostring(plan.episode) + "\n" + f_vTime(plan.activationTime) + " → " + f_vTime(plan.endTime) + " UTC\nFrozen entry / stop / target " + f_camExact(plan.entry) + " / " + f_camExact(plan.stop) + " / " + f_camExact(plan.target) + "\n" + plan.reason + (plan.ambiguous ? "\nAmbiguous OHLC path; stop-first reference convention." : "") + "\nThese are observations, not broker fills."
                        if plan.activationTime >= vLeft
                            f_vMarker(vMarkers, vMarkerCount, plan.activationTime, plan.entry, plan.code + (plan.dir == 1 ? " ▲ ENTRY" : " ▼ ENTRY"), plan.dir == 1 ? vUp : vDown, markerTip, plan.dir == 1 ? label.style_label_up : label.style_label_down)
                            vMarkerCount += 1
                        float markerPrice = nz(plan.outcomePrice, plan.state == "STOP" ? plan.stop : plan.state == "TARGET" ? plan.target : plan.entry)
                        string ending = plan.code + (plan.state == "TARGET" ? " ✓ " : plan.state == "STOP" ? " × " : " · ") + plan.state + (plan.ambiguous ? " ?" : "")
                        string endingTip = na(plan.outcomePrice) ? "Unpriced ending; marker anchored at frozen reference " + f_camExact(markerPrice) + " for orientation. No exit price or fill inferred." : "Observed end reference " + f_camExact(markerPrice)
                        f_vMarker(vMarkers, vMarkerCount, plan.endTime, markerPrice, ending, plan.state == "TARGET" ? vUp : plan.state == "STOP" ? vDown : vMuted, markerTip + "\n" + endingTip, label.style_label_left)
                        vMarkerCount += 1
                    if vPriceTags and not vEpisodeMarks and vLabelCount < vTagCap
                        float tagY = f_vTagY(vOccupied, plan.entry, vGap)
                        string tip = "ENDED " + plan.code + " · episode " + str.tostring(plan.episode) + "\n" + f_vTime(plan.activationTime) + " → " + f_vTime(plan.endTime) + " UTC\nFrozen entry / stop / target: " + f_camExact(plan.entry) + " / " + f_camExact(plan.stop) + " / " + f_camExact(plan.target) + "\n" + plan.reason + (plan.ambiguous ? "\nSame-bar ambiguity: stop-first reference policy." : "")
                        f_vLabel(vLabels, vLabelCount, plan.endTime, tagY, "ENDED " + f_vDirection(plan.dir) + " · " + plan.state, vMuted, tip, true)
                        vLabelCount += 1
                        if tagY != plan.entry
                            f_vLine(vLines, vLineCount, plan.endTime, plan.entry, plan.endTime, tagY, color.new(vMuted, 65), line.style_dotted)
                            vLineCount += 1
    if array.size(vAuditIndices) > 0
        for i in vAuditIndices
            CamEvent event = array.get(camEvents, i)
            if vLabelCount < vTagCap and event.eventTime >= vLeft and event.eventTime <= vVisibleRight and not na(event.price)
                float tagY = f_vTagY(vOccupied, event.price, vGap)
                f_vLabel(vLabels, vLabelCount, event.eventTime, tagY, event.code + " " + event.kind, vMuted, event.reason + "\nObserved " + f_vTime(event.eventTime) + " UTC · not a broker fill", true)
                vLabelCount += 1
                if tagY != event.price
                    f_vLine(vLines, vLineCount, event.eventTime, event.price, event.eventTime, tagY, color.new(vMuted, 65), line.style_dotted)
                    vLineCount += 1
    while array.size(vLines) > vLineCount
        line.delete(array.pop(vLines))
    while array.size(vLabels) > vLabelCount
        label.delete(array.pop(vLabels))
    while array.size(vMarkers) > vMarkerCount
        label.delete(array.pop(vMarkers))
    while array.size(vBoxes) > vBoxCount
        box.delete(array.pop(vBoxes))

    if vBoardMode != "Off"
        array<CamViewRow> rows = array.new<CamViewRow>()
        f_vInsetStart(rows, vBoardAnchor)
        string vRibbonContext = f_vSourceShort(camSourceLabel) + " · " + (not camMapReady ? "CONTEXT WAIT" : camBias == 1 ? "HIGHER" : camBias == -1 ? "LOWER" : "NEUTRAL") + (camMapReady ? " · " + str.upper(camWidthLabel) : "")
        string vAtrShort = str.contains(camAtrBasis, "RTH") ? "RTH14" : str.contains(camAtrBasis, "Provider") ? "Daily14" : "WAIT"
        string vAtrValue = camAtrReady ? f_camPrice(camAtrReference) : str.tostring(camAtrSamples) + "/14 WAIT"
        string vAtrTip = "ATR basis: " + camAtrBasis + "\nFrozen value " + f_camExact(camAtrReference) + " at " + f_vTime(camAtrStamp) + " UTC\nObserved chain start " + f_vTime(camAtrStart) + " · samples " + str.tostring(camAtrSamples) + "\nRTH counts completed observed sessions; entirely absent dates may be closures or unavailable feed. No provider fallback.\n" + vRiskTip
        if vBoardMode == "Ribbon"
            f_vAdd(rows, "", vRibbonContext, "CAMARILLA DESK " + CAM_VERSION + " · " + syminfo.tickerid + "\n" + vSourceTip, 4)
            string status = vTracking ? vTracked.code + " " + f_vDirection(vTracked.dir) + " · ACTIVE" + (camActiveCount > 1 ? " +" + str.tostring(camActiveCount - 1) : "") : vHasUpcoming ? vUpcoming.code + " " + f_vDirection(vUpcoming.dir) + " · " + f_vState(vUpcoming, camInSession) : not camMapReady ? "DATA WAIT" : not camInSession ? "WAIT SESSION" : "NO QUALIFYING SETUP"
            string statusTip = vTracking ? f_vSnapshotName(vTracked) + "\nConfirmed " + f_vTime(vTracked.activationTime) + " UTC\nEntry / stop / target " + f_camExact(vTracked.entry) + " / " + f_camExact(vTracked.stop) + " / " + f_camExact(vTracked.target) + "\n" + vTracked.reason : vHasUpcoming ? f_vPlayTip(vUpcoming) : f_vRecovery()
            f_vAdd(rows, "", status, statusTip + "\n" + str.tostring(camActiveCount) + " active · " + str.tostring(camApproachingCount) + " approaching · " + str.tostring(camCandidateCount) + " candidates. These counts overlap. Expand Clean/Context/Audit for detail.", 2)
            string next = vTracking ? "NEXT · Stop / target · " + str.tostring(math.max(0, activeDeadline - (camClosedBar - vTracked.activationBar))) + "b max" : vHasUpcoming ? vUpcoming.state == "ARMED" ? "NEXT · " + f_vTrigger(vUpcoming) : vUpcoming.approaching ? "NEXT · Wait for setup" : "NEXT · Wait for approach" : not camMapReady ? "NEXT · Check source / ATR readiness" : not camInSession ? "NEXT · Wait for " + f_vClockShort(camDecisionLabel) : "NEXT · Inspect Audit for prerequisites"
            f_vAdd(rows, "", next, statusTip + "\n" + (vTracking ? "Session end can precede the displayed bar deadline. References are not broker fills." : vHasUpcoming ? vUpcoming.next : f_vRecovery()), 3)
            f_vAdd(rows, "", "ATR " + vAtrShort + " " + vAtrValue + " · CLOSED " + f_vClock(camClosedTime), vAtrTip + "\n" + vClosedTip, 3)
        else
            f_vAdd(rows, "", "CAMARILLA DESK " + CAM_VERSION + " · " + syminfo.ticker, syminfo.tickerid + " · " + timeframe.period + "\n" + vSourceTip, 3)
            if vTracking
                f_vAdd(rows, "", f_vWrap(f_vDirection(vTracked.dir) + " · " + f_vSnapshotName(vTracked), 31, 2), vTracked.reason, 2)
                int age = math.max(0, camClosedBar - vTracked.activationBar)
                f_vAdd(rows, "", "ACTIVE · " + str.tostring(age) + " bars · confirmed " + f_vClock(vTracked.activationTime) + " UTC", "Frozen activation " + f_vTime(vTracked.activationTime) + " UTC · episode " + str.tostring(vTracked.episode), 4)
                f_vAdd(rows, "ENTRY REF", f_camPrice(vTracked.entry), "Observed activation close, not a fill.\n" + f_camExact(vTracked.entry), 1)
                f_vAdd(rows, "STOP", f_camPrice(vTracked.stop), "Frozen " + vTracked.stopMode + " stop\n" + f_camExact(vTracked.stop), 1)
                f_vAdd(rows, "TARGET", f_camPrice(vTracked.target) + " · " + str.tostring(vTracked.effectiveR, "0.00") + "R", "Frozen " + vTracked.targetMode + " target\n" + f_camExact(vTracked.target), 1)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
                f_vAdd(rows, "", "NEXT · stop / target / expiry · " + str.tostring(math.max(0, activeDeadline - age)) + " bars left", "Later completed-bar observations resolve the fixed plan. Session expiry can precede the bar deadline.\n" + vRiskTip, 3)
            else if vHasUpcoming
                f_vAdd(rows, "", f_vWrap(f_vDirection(vUpcoming.dir) + " · " + f_vName(vUpcoming), 31, 2), f_vPlayTip(vUpcoming), 2)
                f_vAdd(rows, "", f_vState(vUpcoming, camInSession) + (f_vAge(vUpcoming) == "" ? "" : " · " + f_vAge(vUpcoming)), f_vPlayTip(vUpcoming), 4)
                f_vAdd(rows, "", "CONFIRM · " + f_vTrigger(vUpcoming), "This close also requires the preceding setup sequence.\n" + vUpcoming.next, 0)
                f_vAdd(rows, "", "NEXT · " + f_vWrap(vUpcoming.next, 36, 2), vUpcoming.next, 3)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
            else
                f_vAdd(rows, "", "SESSION GUIDE", "No active plan or eligible setup is selected.", 2)
                f_vAdd(rows, "", not camMapReady ? "DATA WAIT" : not camInSession ? "WAIT SESSION" : "NO QUALIFYING SETUP", camDataReason, 4)
                f_vAdd(rows, "", f_vWrap(f_vRecovery(), 38, 3), f_vRecovery(), 0)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
                if array.size(camPlans) > 0
                    CamPlanSnapshot last = array.get(camPlans, 0)
                    if last.state != "ACTIVE"
                        f_vAdd(rows, "", f_vWrap("LAST ENDED · " + f_vDirection(last.dir) + " " + f_vSnapshotName(last) + " · " + last.state, 38, 2), last.reason + "\nEnd " + f_vTime(last.endTime) + " UTC; not a new setup.", 3)
            int otherCount = math.max(0, camActiveCount - (vTracking ? 1 : 0))
            if otherCount > 0
                string others = ""
                int shown = 0
                for plan in camPlans
                    if plan.state == "ACTIVE" and (not vTracking or plan.code != vTracked.code or plan.episode != vTracked.episode or plan.sessionStart != vTracked.sessionStart)
                        if shown < 2
                            others += (shown == 0 ? "" : " · ") + f_vDirection(plan.dir) + " " + f_vSnapshotName(plan)
                            shown += 1
                f_vAdd(rows, "", f_vWrap("ALSO ACTIVE · " + others + (otherCount > shown ? " +" + str.tostring(otherCount - shown) : ""), 38, 2), str.tostring(otherCount) + " additional active snapshots. Only the newest active is chart-tracked. Manual inspection cannot replace it.", 3)
            f_vAdd(rows, "", str.tostring(camActiveCount) + " active · " + str.tostring(camApproachingCount) + " approaching · " + str.tostring(camCandidateCount) + (camCandidateCount == 1 ? " candidate" : " candidates"), "Approaching is a candidate whose region prerequisite passes. Counts overlap; they are not probabilities.", 3)
            if vBoardMode == "Context" or vBoardMode == "Audit"
                array<int> attention = f_vRows(vBoardMode == "Audit")
                f_vAdd(rows, "", vBoardMode == "Audit" ? "ALL 14 · PREREQUISITE AUDIT" : "ATTENTION · UP TO FIVE PLAYS", "ACTIVE → ARMED → APPROACHING → CANDIDATE; fixed-code ties. Attention, not strength.", 4)
                for i in attention
                    Play play = array.get(plays, i)
                    string state = f_vState(play, camInSession)
                    string brief = vBoardMode == "Audit" ? play.code + " · " + f_vDirection(play.dir) + " · " + state : f_vName(play) + " · " + f_vDirection(play.dir) + "\n" + state + (play.state == "ARMED" or play.state == "WATCH" and play.candidate ? " · " + f_vTrigger(play) : "")
                    f_vAdd(rows, "", brief, f_vName(play) + "\n" + f_vPlayTip(play) + "\n" + f_vGateWords(play), 3)
            f_vAdd(rows, "", vRibbonContext, vSourceTip, 4)
            f_vAdd(rows, "", "ATR " + vAtrShort + " " + vAtrValue + " · " + f_vClockShort(camDecisionLabel), vAtrTip, 3)
            f_vAdd(rows, "", "MIN " + str.tostring(minRewardR, "0.0") + "R · " + str.tostring(stopBufferTicks) + " tick buffer · " + stopModel + " SL", vRiskTip, 3)
            if vBoardMode == "Audit"
                f_vAdd(rows, "", "CONTEXT " + (camBias == 1 ? "Higher" : camBias == -1 ? "Lower" : "Neutral") + " · WIDTH " + camWidthLabel, "Close delta " + f_camExact(camRangeDelta) + " · neutral threshold " + f_camExact(camRangeThreshold) + "\nGeometric band relation (separate): " + camRelationship + "\nWidth ratio " + f_camExact(camWidthRatio) + "\nSelected ATR " + f_camExact(camAtrReference) + " at " + f_vTime(camAtrStamp) + " UTC", 3)
                f_vAdd(rows, "", "REARM " + (allowRearm ? "ON · " + str.tostring(cooldownBars) + "b · cap " + str.tostring(maxEpisodes) : "OFF") + " · TP " + targetModel, vRiskTip, 3)
            f_vAdd(rows, "", "REFERENCE PLANS · NOT ORDER FILLS", "Drawings: " + str.tostring(vHistoryShown) + " ended shown / " + str.tostring(vHistoryOmitted) + " omitted; " + str.tostring(vContextOmitted) + " context tags omitted.\n" + (CAM_IS_LAB ? "Lab: alert() calls for reference JSON; order-fill alerts for emulated orders. " : "Static activation alert: desired condition + Once Per Bar Close. ") + "Aggregate JSON: Any alert() function call. Recreate saved alerts after changes. Displaying this recipe does not test delivery.", 3)
        f_vInsetEnd(rows, vBoardAnchor)
        string signature = f_vSignature(rows)
        if na(vBoard) or signature != vBoardSignature
            if not na(vBoard)
                table.delete(vBoard)
            vBoard := table.new(f_vPosition(vBoardAnchor), 2, array.size(rows), bgcolor = color.new(vBg, 100), frame_width = 0, border_width = 0)
            vBoardSignature := signature
        f_vRender(vBoard, rows, vBoardAnchor)

    if vDetail != "Off"
        array<CamViewRow> rows = array.new<CamViewRow>()
        f_vInsetStart(rows, vFocusAnchor)
        if vInspectIndex >= 0 and vInspectIndex < array.size(plays)
            Play play = array.get(plays, vInspectIndex)
            string tip = f_vPlayTip(play)
            f_vAdd(rows, "", "INSPECT · " + f_vName(play), tip, 4)
            f_vAdd(rows, "", f_vDirection(play.dir) + " · " + f_vState(play, camInSession) + (f_vAge(play) == "" ? "" : " · " + f_vAge(play)), tip, 2)
            f_vAdd(rows, "", "WHY · " + f_vWrap(play.reason, 38, 3), play.reason, 0)
            f_vAdd(rows, "", "NEXT · " + f_vWrap(play.next, 38, 3), play.next, 0)
            f_vAdd(rows, "", "FAIL · " + f_vWrap(play.invalidation, 38, 2), play.invalidation, 3)
            if vDetail == "Standard"
                f_vAdd(rows, "", f_vGateWords(play), "Direct core flags. Candidate excludes current region; Approaching includes it. SMA is an optional original close filter.", 3)
                f_vAdd(rows, "", "SETUP " + f_vTime(play.setupTime) + " · ACTIVE " + f_vTime(play.activationTime), tip, 3)
                f_vAdd(rows, "", "OPEN " + f_camPrice(camOpen) + " · " + f_vTime(camOpeningSourceTime) + " UTC", vSourceTip, 3)
                f_vAdd(rows, "", f_vWrap("SOURCE " + camSourceLabel + " · close " + camCloseLabel + " · " + camDecisionLabel, 40, 2), vSourceTip, 3)
                f_vAdd(rows, "", "ORIGINAL EXECUTION\n" + stopModel + " SL / " + targetModel + " TP · min " + str.tostring(minRewardR) + "R", vRiskTip, 3)
                f_vAdd(rows, "", CAM_IS_LAB ? "ALERT · alert() calls = reference JSON\nOrder fills = broker-emulated orders" : "ALERT · activation condition + bar close\nJSON · Any alert() function call", "Choose the desired condition; recreate after input/code changes. Reference plans are not fills. No alert delivery is proven by this display.", 3)
        else
            f_vAdd(rows, "", "INSPECT · SESSION GUIDE", "Choose a code to audit its prerequisites. Chart tracking remains independent.", 4)
            f_vAdd(rows, "", f_vWrap(f_vRecovery(), 38, 3), vSourceTip, 0)
        f_vAdd(rows, "", vTracking ? f_vWrap("CHART TRACKS · " + f_vDirection(vTracked.dir) + " " + f_vSnapshotName(vTracked), 38, 2) : "CHART · no active plan", "Manual inspection cannot change tracked snapshot selection.", 3)
        f_vInsetEnd(rows, vFocusAnchor)
        string signature = f_vSignature(rows)
        if na(vFocus) or signature != vFocusSignature
            if not na(vFocus)
                table.delete(vFocus)
            vFocus := table.new(f_vPosition(vFocusAnchor), 2, array.size(rows), bgcolor = color.new(vBg, 100), frame_width = 0, border_width = 0)
            vFocusSignature := signature
        f_vRender(vFocus, rows, vFocusAnchor)

Camarilla Desk Strategy Lab

Optional broker emulator: independent fills, occupancy, sizing and costs; no claimed edge.

↓ Download camarilla-desk-strategy.pine

v2.1.0 · MIT · SHA256 16d881cad4a2acb7ab67a286c1b9b5e19e9d678f974afe1cb72c014a1f694e5f

camarilla-desk-strategy.pine · Pine v6 · MIT
Read the complete source
//@version=6
// CAMARILLA DESK 2.1.0 · MIT · Original interpretation of public Camarilla concepts.
// CARD: market/timeframe — standard time-based intraday bars, 1m through 1h.
// CARD: idea — confirmed play states, frozen prior-source map and reference plans.
// ACTIVE denotes an observed hypothesis, not a filled order or a validated edge.
// See docs/engine-contract.md for exact rules and docs/build-report.md for evidence.

strategy("Camarilla Desk · Strategy Lab", "Cam Lab", overlay = true, behind_chart = false, max_lines_count = 220, max_labels_count = 160, max_boxes_count = 50, max_bars_back = 3000, pyramiding = 0, initial_capital = 100000, currency = currency.USD, commission_type = strategy.commission.percent, commission_value = 0.05, slippage = 1, process_orders_on_close = false, calc_on_every_tick = false, calc_on_order_fills = false, margin_long = 100, margin_short = 100)
const string CAM_VERSION = "2.1.0"
const bool CAM_IS_LAB = true

// PUBLIC LITE BASELINE PREREQUISITES · original closed-bar execution · no Premium parity.
string CAM_FORMULA = "expanded6-v1"
string camSourceGroup = "Source and decision clock"
string sourceMode = input.string("Market default", "Pivot source", options = ["Market default", "Auto RTH/ETH", "RTH", "ETH", "Exchange daily", "Selected session"], group = camSourceGroup, display = display.none, tooltip = "Market default explicitly chooses RTH for non-crypto and Exchange daily for crypto. RTH/ETH require complete observed one-minute windows. Optional Auto RTH/ETH requires complete pre/postmarket coverage and freezes at the opening; it never silently falls back. Exchange daily remains a provider-defined daily/settlement source, never cash RTH.")
string decisionMode = input.string("Auto", "Decision window", options = ["Auto", "US cash", "Exchange day", "Custom"], group = camSourceGroup, display = display.none, tooltip = "Auto uses Exchange day for crypto and US cash 09:30–16:00 New York for other assets. Set Custom for another exchange or a futures cash window.")
string customSession = input.session("0930-1600", "Custom decision interval", group = camSourceGroup, active = decisionMode == "Custom", display = display.none)
string customTimezone = input.string("America/New_York", "Custom IANA time zone", group = camSourceGroup, active = decisionMode == "Custom", display = display.none, tooltip = "Use an IANA region such as America/New_York. True overnight intervals use the ending trading weekday. Equal start/end is 24 hours and uses the starting calendar weekday.")
bool customWeekdays = input.bool(true, "Custom weekdays only", group = camSourceGroup, active = decisionMode == "Custom", display = display.none)
string regularSession = input.session("0930-1600", "RTH source interval", group = camSourceGroup, display = display.none)
string regularTimezone = input.string("America/New_York", "RTH source IANA zone", group = camSourceGroup, display = display.none, tooltip = "Cash-session definition, including futures emulation. It is independent from the exchange's definition of regular trading.")
string extendedClock = input.string("Auto", "ETH source clock", options = ["Auto", "Custom"], group = camSourceGroup, display = display.none, tooltip = "Auto: US equities 04:00–20:00 New York, futures 18:00–17:00 New York, crypto exchange day. Other venues require a Custom source clock.")
string extendedSession = input.session("1800-1700", "Custom ETH interval", group = camSourceGroup, active = extendedClock == "Custom", display = display.none)
string extendedTimezone = input.string("America/New_York", "Custom ETH IANA zone", group = camSourceGroup, active = extendedClock == "Custom", display = display.none)
string closeMode = input.string("Match source", "Pivot close selection", options = ["Match source", "RTH", "ETH", "Exchange daily"], group = camSourceGroup, display = display.none)
float switchingTolerance = input.float(2, "Auto excursion · % selected ATR", minval = 0, maxval = 100, step = 0.1, group = camSourceGroup, active = sourceMode == "Auto RTH/ETH", display = display.none)
string camPrerequisiteGroup = "Public Lite baseline · confirmed prerequisites"
float rangeThresholdPct = input.float(2, "Neutral close change · % selected ATR", minval = 0, maxval = 100, step = 0.1, group = camPrerequisiteGroup, display = display.none)
float widthThresholdPct = input.float(2, "Similar width tolerance · %", minval = 0, maxval = 99, step = 0.1, group = camPrerequisiteGroup, display = display.none)
bool allowNeutral = input.bool(false, "Allow both directions in Neutral", group = camPrerequisiteGroup, display = display.none)
bool smaGate = input.bool(false, "Original SMA close confirmation", group = camPrerequisiteGroup, display = display.none, tooltip = "Optional original filter: confirmed long closes above the chart SMA, short below. This does not reproduce an undisclosed Premium SMA trigger.")
int smaLength = input.int(20, "Chart SMA length", minval = 2, maxval = 300, group = camPrerequisiteGroup, active = smaGate, display = display.none)
string camPlayGroup = "Original structural hypotheses"
bool enableA = input.bool(true, "A · Third-band traverse", group = camPlayGroup, display = display.none)
bool enableB = input.bool(true, "B · Interior breakout and retest", group = camPlayGroup, display = display.none)
bool enableC = input.bool(true, "C · Opening continuation", group = camPlayGroup, display = display.none)
bool enableD = input.bool(true, "D · Context-opposed continuation", group = camPlayGroup, display = display.none)
bool enableE = input.bool(true, "E · Fourth-tier failure", group = camPlayGroup, display = display.none)
bool enableF = input.bool(true, "F · Outer-tier rejection", group = camPlayGroup, display = display.none)
bool enableG = input.bool(false, "G · Opposite-side outer rejection", group = camPlayGroup, display = display.none)
int setupDeadline = input.int(12, "Setup deadline · completed bars", minval = 1, maxval = 200, group = camPlayGroup, display = display.none, tooltip = "Confirmation is allowed through age 12 at the default. The following closed bar expires a pending setup.")
int activeDeadline = input.int(120, "Active deadline · completed bars", minval = 1, maxval = 1000, group = camPlayGroup, display = display.none, tooltip = "Observed later-bar outcomes precede timeout. Default timeout occurs on the 120th later completed chart bar.")
float minRewardR = input.float(1.0, "Minimum structural reward / risk", minval = 0.1, maxval = 10, step = 0.1, group = camPlayGroup, display = display.none)
int stopBufferTicks = input.int(1, "Stop buffer · minimum ticks", minval = 0, maxval = 100, group = camPlayGroup, display = display.none)
string stopModel = input.string("Structural", "Reference stop model", options = ["Structural", "ATR"], group = camPlayGroup, display = display.none, tooltip = "ATR is an original alternative measured from activation close using the frozen selected ATR basis. Neither option claims a vendor preset.")
float stopAtrMultiple = input.float(0.25, "ATR stop multiple", minval = 0.01, maxval = 10, step = 0.05, group = camPlayGroup, active = stopModel == "ATR", display = display.none)
string targetModel = input.string("Structural", "Reference target model", options = ["Structural", "ATR"], group = camPlayGroup, display = display.none)
float targetAtrMultiple = input.float(0.5, "ATR target multiple", minval = 0.01, maxval = 20, step = 0.05, group = camPlayGroup, active = targetModel == "ATR", display = display.none)
float maxTargetAtr = input.float(0, "Maximum target distance · selected ATR (0 off)", minval = 0, maxval = 20, step = 0.1, group = camPlayGroup, display = display.none, tooltip = "Optional original reachability guard; rejects a too-distant target without silently moving it.")
bool allowRearm = input.bool(false, "Allow another episode after ending", group = camPlayGroup, display = display.none)
int cooldownBars = input.int(3, "Rearm cooldown · completed bars", minval = 1, maxval = 200, group = camPlayGroup, active = allowRearm, display = display.none)
int maxEpisodes = input.int(1, "Maximum episodes per code / session", minval = 1, maxval = 10, group = camPlayGroup, active = allowRearm, display = display.none)
string camAlertGroup = "Aggregate JSON alerts"
bool alertActivation = input.bool(true, "Include activation events", group = camAlertGroup, display = display.none)
bool alertEnding = input.bool(true, "Include ending events", group = camAlertGroup, display = display.none)
bool alertArmed = input.bool(false, "Include armed events", group = camAlertGroup, display = display.none)

// Appended to preserve the ordering of all previous core inputs.
string atrBasisMode = input.string("Market default", "ATR calculation basis", options = ["Market default", "Provider daily", "RTH session"], group = camSourceGroup, display = display.none, tooltip = "Market default uses completed observed RTH-session Wilder ATR(14) for futures with a non-Exchange-day decision clock, and completed provider daily ATR(14) otherwise. RTH uses the configured RTH interval and zone; seed = mean of 14 valid session true ranges, then Wilder smoothing. An observed incomplete/malformed session resets the seed. Entirely absent days are skipped: this is observed-session coverage, not an exchange holiday calendar. No fallback. The chosen basis is frozen at the decision opening and drives range, Auto excursion and optional ATR risk/target checks.")

type CamLevels
    bool valid = false
    bool outerValid = false
    bool complete = false
    float h = na
    float l = na
    float c = na
    float h2 = na
    float l2 = na
    float h3 = na
    float l3 = na
    float h4 = na
    float l4 = na
    float h5 = na
    float l5 = na
    float h6 = na
    float l6 = na
    int sourceStart = na
    int sourceEnd = na
    string reason = "Source unavailable"

type CamWindowBook
    CamLevels latest
    CamLevels prior
    int start = na
    int finish = na
    int lastClose = na
    float h = na
    float l = na
    float c = na
    int samples = 0
    bool complete = false
    bool finished = true
    string reason = "No observed interval"

// Constant-size Wilder state, updated only when an observed RTH window finishes.
type CamAtrBook
    int samples = 0
    float seedSum = 0
    float previousClose = na
    float value = na
    int sourceStart = na
    int sourceEnd = na
    int observedEnd = na

type CamSourcePacket
    CamLevels current
    CamLevels prior
    bool ready = false
    bool openKnown = false
    float opening = na
    int openingTime = na
    int sessionStart = na
    int sessionEnd = na
    int cutoff = na
    string clockLabel = ""
    string sourceKind = ""
    string closeKind = ""
    int closeSourceEnd = na
    int priorCloseSourceEnd = na
    float atr = na
    int atrStamp = na
    int atrBasisCode = 0
    int atrSamples = 0
    int atrStart = na
    bool atrReady = false
    float excursion = na
    float excursionThreshold = na
    string reason = "Waiting for an observed opening in the 1m source context"
    string selectionReason = ""
    string coverage = ""
    int sourceCode = 0
    int closeCode = 0
    int reasonCode = 0
    int selectionCode = 0
    int rthSamples = 0
    int ethSamples = 0
    int selectedSamples = 0
    bool rthComplete = false
    bool ethComplete = false
    bool selectedComplete = false

type Play
    string code = ""
    string name = ""
    string family = ""
    int biasRequired = 0
    int dir = 0
    string state = "DATA WAIT"
    string reason = "Waiting for a decision session"
    string next = "Wait for a fully observed opening"
    string invalidation = "No setup observed"
    bool eligible = false
    bool candidate = false
    bool approaching = false
    bool rangeOk = false
    bool widthOk = false
    bool openOk = false
    bool regionOk = false
    bool blocked = false
    bool smaOk = true
    int episode = 0
    float triggerPrice = na
    string triggerOperator = ""
    string sourceKind = ""
    string closeKind = ""
    string stopMode = "Structural"
    string targetMode = "Structural"
    float atrUsed = na
    int atrStamp = na
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na
    int sourceCutoff = na
    int closeSourceEnd = na
    float level = na
    int setupBar = na
    int setupTime = na
    float armedHigh = na
    float armedLow = na
    int activationBar = na
    int activationTime = na
    int activationSeq = na
    float entry = na
    float stop = na
    float target = na
    float effectiveR = na
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    int endBar = na
    int endTime = na
    float outcomePrice = na
    bool ambiguous = false

type CamPlanSnapshot
    string code = ""
    string name = ""
    int dir = 0
    int episode = 0
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    string sourceKind = ""
    string closeKind = ""
    int setupTime = na
    float triggerPrice = na
    string triggerOperator = ""
    float atrUsed = na
    string stopMode = ""
    string targetMode = ""
    int activationBar = na
    int activationTime = na
    float entry = na
    float stop = na
    float target = na
    float effectiveR = na
    int endBar = na
    int endTime = na
    float outcomePrice = na
    string state = ""
    string reason = ""
    bool ambiguous = false
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na
    int atrStamp = na

type CamEvent
    int seq = 0
    string code = ""
    string kind = ""
    string reason = ""
    int dir = 0
    int eventTime = na
    int sessionStart = na
    int sourceStart = na
    int sourceEnd = na
    float price = na
    float entry = na
    float stop = na
    float target = na
    bool ambiguous = false
    int episode = 0
    string sourceKind = ""
    string closeKind = ""
    int atrStamp = na
    int sourceCutoff = na
    int closeSourceEnd = na
    float atrUsed = na
    string atrBasis = "Unavailable"
    int atrSamples = 0
    int atrStart = na

var array<Play> plays = array.new<Play>()
var array<CamPlanSnapshot> camPlans = array.new<CamPlanSnapshot>()
var array<CamEvent> camEvents = array.new<CamEvent>()
var array<CamEvent> camBarEvents = array.new<CamEvent>()
var array<int> camEventCounter = array.new_int(1, 0)

// Comparison normalization is relative to the actual operand scale, never to 1.
// Self-normalized sign avoids Pine comparison operators' nine-decimal rounding.
f_camSign(float value) =>
    na(value) ? 0 : int(nz(value / math.abs(value), 0))

f_camCompare(float first, float rhs) =>
    float difference = first - rhs
    float tolerance = 4.0 * 2.220446049250313e-16 * math.max(math.abs(first), math.abs(rhs))
    na(difference) ? 0 : f_camSign(math.abs(difference) - tolerance) <= 0 ? 0 : f_camSign(difference)

f_camNumber(float value) =>
    string result = "null"
    if not na(value)
        if f_camSign(value) == 0
            result := "0"
        else
            int exponent = int(math.floor(math.log10(math.abs(value))))
            result := exponent < -6 or exponent > 12 ? str.tostring(value / math.pow(10, exponent), "0.################") + "e" + str.tostring(exponent) : str.tostring(value, "0.################")
    result

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

f_camExact(float value) =>
    na(value) ? "—" : f_camNumber(value)

// Narrative rounding suppresses binary float tails; raw map/plan values stay unchanged.
f_camReadable(float value) =>
    na(value) ? "—" : f_camCompare(math.abs(value), 0.000001) < 0 ? f_camNumber(value) : str.tostring(value, "0.##########")

f_camTerminal(string state) =>
    state == "REJECTED" or state == "INVALIDATED" or state == "TARGET" or state == "STOP" or state == "EXPIRED"

f_camTouched(float level, float barHigh, float barLow) =>
    not na(level) and not na(barLow) and not na(barHigh) and f_camCompare(barLow, level) <= 0 and f_camCompare(barHigh, level) >= 0

// A flat positive bar is valid. Missing or incoherent constituent prices are not.
f_camBarValid(float barOpen, float barHigh, float barLow, float barClose) =>
    not na(barOpen) and not na(barHigh) and not na(barLow) and not na(barClose) and f_camSign(barLow) > 0 and f_camCompare(barHigh, math.max(barOpen, barClose)) >= 0 and f_camCompare(barLow, math.min(barOpen, barClose)) <= 0

f_camLevels(float srcHigh, float srcLow, float srcClose, int srcStart, int srcEnd, bool complete, string incompleteReason, bool requireCloseInside = true) =>
    CamLevels m = CamLevels.new()
    m.h := srcHigh
    m.l := srcLow
    m.c := srcClose
    m.sourceStart := srcStart
    m.sourceEnd := srcEnd
    bool independentValid = not na(srcHigh) and not na(srcLow) and not na(srcClose) and f_camSign(srcLow) > 0 and f_camSign(srcClose) > 0 and f_camCompare(srcHigh, srcLow) >= 0
    m.complete := complete and (requireCloseInside ? f_camBarValid(srcClose, srcHigh, srcLow, srcClose) : independentValid)
    bool coherent = m.complete and f_camCompare(srcHigh, srcLow) > 0
    if complete and coherent
        float width = srcHigh - srcLow
        m.h2 := srcClose + 1.1 * width / 6
        m.l2 := srcClose - 1.1 * width / 6
        m.h3 := srcClose + 1.1 * width / 4
        m.l3 := srcClose - 1.1 * width / 4
        m.h4 := srcClose + 1.1 * width / 2
        m.l4 := srcClose - 1.1 * width / 2
        m.h5 := m.h4 + 1.168 * (m.h4 - m.h3)
        m.l5 := m.l4 - 1.168 * (m.l3 - m.l4)
        m.h6 := srcHigh / srcLow * srcClose
        m.l6 := 2 * srcClose - m.h6
        m.valid := f_camCompare(m.h4, m.h3) > 0 and f_camCompare(m.h3, m.l3) > 0 and f_camCompare(m.l3, m.l4) > 0
        m.outerValid := m.valid and not na(m.h6) and not na(m.l6) and f_camSign(m.l6) > 0 and f_camCompare(m.h6, m.h4) > 0 and f_camCompare(m.l6, m.l4) < 0
        m.reason := not m.valid ? "Collapsed inner geometry" : not m.outerValid ? "Outer ratio geometry unavailable" : "Complete coherent source"
    else
        m.reason := not complete ? incompleteReason : "Missing, nonpositive or incoherent source H/L/C"
    m

f_camRelationship(CamLevels current, CamLevels previous) =>
    string relation = "Unavailable"
    int bias = 0
    float ratio = na
    string widthLabel = "Unavailable"
    if current.valid and previous.valid
        float a = previous.l3
        float b = previous.h3
        float c = current.l3
        float d = current.h3
        int lowerCompare = f_camCompare(c, a)
        int upperCompare = f_camCompare(d, b)
        bias := lowerCompare > 0 and upperCompare > 0 ? 1 : lowerCompare < 0 and upperCompare < 0 ? -1 : 0
        relation := lowerCompare == 0 and upperCompare == 0 ? "Unchanged" : f_camCompare(c, b) > 0 ? "Higher separated" : f_camCompare(d, a) < 0 ? "Lower separated" : lowerCompare >= 0 and upperCompare <= 0 ? "Inside band" : lowerCompare <= 0 and upperCompare >= 0 ? "Outside band" : lowerCompare > 0 and upperCompare > 0 ? "Overlapping higher" : "Overlapping lower"
        ratio := (d - c) / (b - a)
        int widthCompare = f_camCompare(d + a, c + b)
        widthLabel := widthCompare > 0 ? "Wide" : widthCompare < 0 ? "Narrow" : "Equal"
    [relation, bias, ratio, widthLabel]

// Independent transcription of public Lite 2.0 prerequisites, applied on closed bars.
// Original episode triggers below are a separate layer; these flags are not entries.
f_camPrerequisites(string code, CamLevels m, CamLevels prior, float atr, float opening, float barOpen, float barClose, bool priorBlocked, float rangePct, float widthPct, bool neutralAllowed) =>
    string family = str.substring(code, 1, 2)
    bool higher = str.substring(code, 0, 1) == "H"
    bool innerReady = m.valid and prior.valid
    bool atrReady = not na(atr) and f_camSign(atr) > 0
    bool observationReady = not na(opening) and not na(barOpen) and not na(barClose)
    float threshold = atr * rangePct / 100
    float delta = m.c - prior.c
    int bias = innerReady and atrReady ? (f_camCompare(m.c, prior.c + threshold) > 0 ? 1 : f_camCompare(m.c, prior.c - threshold) < 0 ? -1 : 0) : 0
    float width = m.h3 - m.l3
    float previousWidth = prior.h3 - prior.l3
    float ratio = innerReady and f_camSign(previousWidth) > 0 ? width / previousWidth : na
    // Cross-sums retain the original endpoint scale when subtracting nearby prices.
    float upperFactor = 1 + widthPct / 100
    float lowerFactor = 1 - widthPct / 100
    string widthLabel = na(ratio) ? "Unavailable" : f_camCompare(m.h3 + prior.l3 * upperFactor, m.l3 + prior.h3 * upperFactor) > 0 ? "Wide" : f_camCompare(m.h3 + prior.l3 * lowerFactor, m.l3 + prior.h3 * lowerFactor) < 0 ? "Narrow" : "Similar"
    bool rangeOk = innerReady and atrReady and (bias == (higher ? 1 : -1) or bias == 0 and neutralAllowed)
    bool widthOk = not na(ratio) and (family == "F" or family == "G" or widthLabel == "Similar" or (family == "A" or family == "E" ? widthLabel == "Wide" : widthLabel == "Narrow"))
    float openLow = family == "A" ? m.l2 : family == "B" or family == "E" ? (higher ? m.l2 : m.l3) : family == "G" ? (higher ? m.l6 : m.c) : family == "D" ? (higher ? m.l6 : m.h4) : (higher ? m.h4 : m.l6)
    float openHigh = family == "A" ? m.h2 : family == "B" or family == "E" ? (higher ? m.h3 : m.h2) : family == "G" ? (higher ? m.c : m.h6) : family == "D" ? (higher ? m.l4 : m.h6) : (higher ? m.h6 : m.l4)
    bool openOk = observationReady and f_camCompare(opening, openLow) >= 0 and f_camCompare(opening, openHigh) <= 0
    bool upperSide = higher != (family == "D" or family == "G")
    bool regionOk = observationReady and (family == "A" ? (higher ? f_camCompare(barClose, m.l4) > 0 and f_camCompare(barClose, m.c) < 0 : f_camCompare(barClose, m.h4) < 0 and f_camCompare(barClose, m.c) > 0) : family == "B" or family == "E" ? (higher ? f_camCompare(barClose, m.h3) > 0 and f_camCompare(barClose, m.h6) < 0 : f_camCompare(barClose, m.l3) < 0 and f_camCompare(barClose, m.l6) > 0) : family == "C" or family == "D" ? (upperSide ? f_camCompare(barClose, m.h3) > 0 : f_camCompare(barClose, m.l3) < 0) : (upperSide ? f_camCompare(barClose, m.h4) > 0 : f_camCompare(barClose, m.l4) < 0))
    float badLow = family == "A" ? (higher ? m.l4 : m.l2) : family == "B" or family == "E" ? (higher ? m.l2 : m.l6) : upperSide ? (family == "G" ? m.c : m.h3) : m.l6 - (m.l4 - m.l6) * 0.5
    float badHigh = family == "A" ? (higher ? m.h2 : m.h4) : family == "B" or family == "E" ? (higher ? m.h6 : m.h2) : upperSide ? m.h6 + (m.h6 - m.h4) * 0.5 : (family == "G" ? m.c : m.l3)
    bool blocked = priorBlocked or (observationReady and (f_camCompare(barOpen, badLow) < 0 or f_camCompare(barOpen, badHigh) > 0))
    bool geometryReady = innerReady and (family == "A" or m.outerValid)
    bool candidate = geometryReady and atrReady and observationReady and rangeOk and widthOk and openOk and not blocked
    bool approaching = candidate and regionOk
    [bias, ratio, widthLabel, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching]

f_camCodeIndex(string code) =>
    int found = -1
    if array.size(plays) > 0
        for i = 0 to array.size(plays) - 1
            if array.get(plays, i).code == code
                found := i
    found

f_camAutoFocus() =>
    int found = -1
    int latest = na
    if array.size(plays) > 0
        for i = 0 to array.size(plays) - 1
            Play p = array.get(plays, i)
            if p.state == "ACTIVE" and (na(latest) or p.activationTime > latest)
                latest := p.activationTime
                found := i
    found

f_camStorePlan(Play p) =>
    if not na(p.activationTime)
        CamPlanSnapshot snapshot = CamPlanSnapshot.new(p.code, p.name, p.dir, p.episode, p.sessionStart, p.sourceStart, p.sourceEnd, p.sourceKind, p.closeKind, p.setupTime, p.triggerPrice, p.triggerOperator, p.atrUsed, p.stopMode, p.targetMode, p.activationBar, p.activationTime, p.entry, p.stop, p.target, p.effectiveR, p.endBar, p.endTime, p.outcomePrice, p.state, p.reason, p.ambiguous)
        snapshot.atrBasis := p.atrBasis
        snapshot.atrSamples := p.atrSamples
        snapshot.atrStart := p.atrStart
        snapshot.atrStamp := p.atrStamp
        int found = -1
        if array.size(camPlans) > 0
            for i = 0 to array.size(camPlans) - 1
                CamPlanSnapshot old = array.get(camPlans, i)
                if old.code == p.code and old.sessionStart == p.sessionStart and old.episode == p.episode
                    found := i
                    break
        if found >= 0
            array.remove(camPlans, found)
        array.unshift(camPlans, snapshot)
        if array.size(camPlans) > 40
            // Never evict an ongoing plan merely because other codes re-entered.
            int oldestEnded = -1
            for i = array.size(camPlans) - 1 to 0
                if array.get(camPlans, i).state != "ACTIVE"
                    oldestEnded := i
                    break
            if oldestEnded >= 0
                array.remove(camPlans, oldestEnded)
    p

f_camEmit(Play p, string kind, string reason, float price, int stamp, bool ambiguous) =>
    int seq = array.get(camEventCounter, 0) + 1
    array.set(camEventCounter, 0, seq)
    CamEvent e = CamEvent.new(seq, p.code, kind, reason, p.dir, stamp, p.sessionStart, p.sourceStart, p.sourceEnd, price, p.entry, p.stop, p.target, ambiguous)
    e.episode := p.episode
    e.sourceKind := p.sourceKind
    e.closeKind := p.closeKind
    e.atrStamp := p.atrStamp
    e.atrUsed := p.atrUsed
    e.atrBasis := p.atrBasis
    e.atrSamples := p.atrSamples
    e.atrStart := p.atrStart
    e.sourceCutoff := p.sourceCutoff
    e.closeSourceEnd := p.closeSourceEnd
    f_camStorePlan(p)
    array.unshift(camEvents, e)
    array.push(camBarEvents, e)
    if array.size(camEvents) > 60
        array.pop(camEvents)
    seq

f_camArm(Play p, float barHigh, float barLow, int bar, int stamp, string reason) =>
    p.episode += 1
    p.state := "ARMED"
    p.setupBar := bar
    p.setupTime := stamp
    p.armedHigh := barHigh
    p.armedLow := barLow
    p.reason := reason
    f_camEmit(p, "ARMED", reason, p.level, stamp, false)
    p

f_camEnd(Play p, string kind, string reason, float price, int bar, int stamp, bool ambiguous) =>
    p.state := kind
    p.reason := reason
    p.next := "Episode ended · next dated session"
    p.endBar := bar
    p.endTime := stamp
    p.outcomePrice := price
    p.ambiguous := ambiguous
    f_camEmit(p, kind, reason, price, stamp, ambiguous)
    p

// Normalize only machine-noise near a whole tick; retain actual sub-tick prices.
f_camDistance(float first, float rhs, float tick) =>
    float raw = math.abs(first - rhs)
    float ticks = raw / tick
    float nearest = math.round(ticks)
    float noise = 8.0 * 2.220446049250313e-16 * math.max(math.max(math.abs(first), math.abs(rhs)), tick) / tick
    f_camSign(math.abs(ticks - nearest) - math.min(noise, 0.000001)) <= 0 ? nearest * tick : raw

f_camGrid(float value, float tick, bool upward) =>
    float ticks = value / tick
    float nearest = math.round(ticks)
    float tolerance = math.min(8.0 * 2.220446049250313e-16 * math.max(math.abs(ticks), 1), 0.000001)
    float stable = f_camSign(math.abs(ticks - nearest) - tolerance) <= 0 ? nearest : ticks
    (upward ? math.ceil(stable) : math.floor(stable)) * tick

f_camPlan(int dir, string family, CamLevels m, float armedHigh, float armedLow, float entry, bool wide, float tick, int bufferTicks, float minimumR) =>
    float boundary = na
    float destination = na
    if family == "A"
        boundary := dir == 1 ? math.min(m.l4, armedLow) : math.max(m.h4, armedHigh)
        destination := dir == 1 ? m.h3 : m.l3
    else if family == "B" or family == "C" or family == "D"
        boundary := dir == 1 ? m.h3 : m.l3
        destination := dir == 1 ? m.h6 : m.l6
    else if family == "E"
        boundary := dir == 1 ? math.min(m.l6, armedLow) : math.max(m.h6, armedHigh)
        destination := dir == 1 ? m.h4 : m.l4
    else
        boundary := dir == 1 ? math.min(armedLow, m.l6 - 0.25 * (m.l3 - m.l4)) : math.max(armedHigh, m.h6 + 0.25 * (m.h4 - m.h3))
        destination := wide ? (dir == 1 ? m.l4 : m.h4) : m.c
    float stopRaw = boundary - dir * bufferTicks * tick
    float stop = f_camGrid(stopRaw, tick, dir == -1)
    float target = f_camGrid(destination, tick, dir == -1)
    bool positive = not na(entry) and not na(stop) and not na(target) and f_camSign(entry) > 0 and f_camSign(stop) > 0 and f_camSign(target) > 0
    bool directionOk = positive and f_camCompare(entry, stop) * dir > 0 and f_camCompare(target, entry) * dir > 0
    float risk = directionOk ? f_camDistance(entry, stop, tick) : na
    float reward = directionOk ? f_camDistance(entry, target, tick) : na
    float effectiveR = directionOk and f_camSign(risk) > 0 ? reward / risk : na
    bool valid = directionOk and f_camCompare(risk / tick, 1) >= 0 and f_camCompare(effectiveR, minimumR) >= 0
    string reason = not positive ? "Nonpositive or unavailable plan prices" : not directionOk ? "Destination or stop is on the wrong side of entry" : f_camCompare(risk / tick, 1) < 0 ? "Risk distance is smaller than one tick" : f_camCompare(effectiveR, minimumR) < 0 ? "Structural R " + f_camExact(effectiveR) + " < minimum " + f_camExact(minimumR) : "Confirmed structural hypothesis · reference plan only"
    [valid, stop, target, effectiveR, reason]

f_camPlanOptions(int dir, string family, CamLevels m, float armedHigh, float armedLow, float entry, bool wide, float tick, int bufferTicks, float minimumR, float atr, string stopChoice, float stopMultiple, string targetChoice, float targetMultiple, float reachLimit) =>
    [structuralOk, structuralStop, structuralTarget, structuralR, structuralReason] = f_camPlan(dir, family, m, armedHigh, armedLow, entry, wide, tick, bufferTicks, minimumR)
    bool atrReady = not na(atr) and f_camSign(atr) > 0
    float stop = stopChoice == "ATR" and atrReady ? f_camGrid(entry - dir * (atr * stopMultiple + bufferTicks * tick), tick, dir == -1) : structuralStop
    float target = targetChoice == "ATR" and atrReady ? f_camGrid(entry + dir * atr * targetMultiple, tick, dir == -1) : structuralTarget
    bool positive = not na(stop) and not na(target) and f_camSign(entry) > 0 and f_camSign(stop) > 0 and f_camSign(target) > 0
    bool correctSide = positive and f_camCompare(entry, stop) * dir > 0 and f_camCompare(target, entry) * dir > 0
    float risk = correctSide ? f_camDistance(entry, stop, tick) : na
    float reward = correctSide ? f_camDistance(entry, target, tick) : na
    float rr = correctSide and f_camSign(risk) > 0 ? reward / risk : na
    bool enoughRisk = correctSide and f_camCompare(risk / tick, 1) >= 0
    bool enoughReward = enoughRisk and f_camCompare(rr, minimumR) >= 0
    bool reachable = reachLimit == 0 or atrReady and f_camCompare(reward, atr * reachLimit) <= 0
    bool valid = atrReady and enoughReward and reachable
    string reason = not atrReady ? "Confirmed selected ATR unavailable" : not positive ? "Nonpositive or unavailable reference prices" : not correctSide ? "Stop or target is on the wrong side" : not enoughRisk ? "Risk is smaller than one tick" : not enoughReward ? "Reference R " + f_camExact(rr) + " < minimum " + f_camExact(minimumR) : not reachable ? "Target exceeds " + f_camExact(reachLimit) + " selected ATR reach limit" : "Original confirmed reference geometry"
    [valid, stop, target, rr, reason]

f_camOutcome(Play p, float barOpen, float barHigh, float barLow, int bar, int stamp, bool contained, int timeout, bool sessionEnded) =>
    string kind = ""
    string reason = ""
    float price = na
    bool ambiguous = false
    if p.state == "ACTIVE" and bar > p.activationBar
        if contained and f_camBarValid(barOpen, barHigh, barLow, barOpen)
            bool stopGap = f_camCompare(barOpen, p.stop) * p.dir <= 0
            bool targetGap = f_camCompare(barOpen, p.target) * p.dir >= 0
            bool stopTouch = p.dir == 1 ? f_camCompare(barLow, p.stop) <= 0 : f_camCompare(barHigh, p.stop) >= 0
            bool targetTouch = p.dir == 1 ? f_camCompare(barHigh, p.target) >= 0 : f_camCompare(barLow, p.target) <= 0
            if stopGap
                kind := "STOP"
                price := barOpen
                reason := "Opening price beyond stop · reference outcome at observed open"
            else if targetGap
                kind := "TARGET"
                price := p.target
                reason := "Opening price beyond destination · fixed reference destination"
            else if stopTouch
                kind := "STOP"
                price := p.stop
                ambiguous := targetTouch
                reason := targetTouch ? "Both levels touched · stop-first convention; path unknown" : "Reference stop touched on a later bar"
            else if targetTouch
                kind := "TARGET"
                price := p.target
                reason := "Reference destination touched on a later bar"
        if kind == "" and bar - p.activationBar >= timeout
            kind := "EXPIRED"
            reason := "Completed-bar active deadline · no fill inferred"
        if kind == "" and sessionEnded
            kind := "EXPIRED"
            reason := contained ? "Decision session ended · no fill inferred" : "Session end observed later or across a boundary · no fill inferred"
    [kind, price, ambiguous, reason]

f_camFamilyEnabled(int familyIndex) =>
    familyIndex == 0 ? enableA : familyIndex == 1 ? enableB : familyIndex == 2 ? enableC : familyIndex == 3 ? enableD : familyIndex == 4 ? enableE : familyIndex == 5 ? enableF : enableG

f_camNewPlay(int index, CamLevels m, bool mapReady, string dataReason, int bias, float opening, bool openingKnown, int sessionStart) =>
    int familyIndex = int(math.floor(index / 2))
    bool higher = index % 2 == 0
    string family = familyIndex == 0 ? "A" : familyIndex == 1 ? "B" : familyIndex == 2 ? "C" : familyIndex == 3 ? "D" : familyIndex == 4 ? "E" : familyIndex == 5 ? "F" : "G"
    string familyName = family == "A" ? "Traverse" : family == "B" ? "Break and retest" : family == "C" ? "Opening continuation" : family == "D" ? "Opposed continuation" : family == "E" ? "Fourth-tier failure" : family == "F" ? "Outer rejection" : "Opposite outer rejection"
    Play p = Play.new()
    p.code := (higher ? "H" : "L") + family
    p.name := (higher ? "Higher · " : "Lower · ") + familyName
    p.family := family
    p.biasRequired := higher ? 1 : -1
    p.dir := (family == "D" or family == "E" or family == "F") ? (higher ? -1 : 1) : (higher ? 1 : -1)
    p.sessionStart := sessionStart
    p.sourceStart := m.sourceStart
    p.sourceEnd := m.sourceEnd
    bool longSide = p.dir == 1
    p.level := family == "A" ? (longSide ? m.l3 : m.h3) : family == "E" ? (longSide ? m.l4 : m.h4) : family == "F" or family == "G" ? (longSide ? m.l6 : m.h6) : (longSide ? m.h4 : m.l4)
    p.triggerPrice := p.level
    p.triggerOperator := longSide ? ">" : "<"
    string levelText = f_camExact(p.level)
    p.next := family == "B" ? "Closed break of " + levelText + "; later contained retest" : family == "E" ? "Closed excursion beyond " + levelText + "; later failure close" : "Touch " + levelText + "; close " + (longSide ? "above" : "below")
    p.invalidation := family == "A" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.l4) : "≥ " + f_camExact(m.h4)) : family == "B" or family == "C" or family == "D" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.h3) : "≥ " + f_camExact(m.l3)) : family == "E" ? "Pending close " + (longSide ? "≤ " + f_camExact(m.l6) : "≥ " + f_camExact(m.h6)) : "Pending setup deadline; no invented farther pivot"
    if not f_camFamilyEnabled(familyIndex)
        p.state := "DISABLED"
        p.reason := "Family " + family + " is disabled"
        p.next := "Enable this family in settings"
    else if not mapReady
        p.state := "DATA WAIT"
        p.reason := dataReason
    else if not openingKnown
        p.state := "DATA WAIT"
        p.reason := "OPEN UNKNOWN · exact contained session opening not observed"
    else if family != "A" and not m.outerValid
        p.state := "DATA WAIT"
        p.reason := "Outer geometry required by this family's target, stop or cancellation"
    else
        p.state := "WATCH"
        p.reason := "Waiting for confirmed public-baseline prerequisite evaluation"
    p

f_camRefreshPlay(Play p, CamLevels m, CamLevels prior, float atr, float opening, float barOpen, float barClose, bool observationValid, bool inWindow, bool sourceReady, string dataReason, bool smaReady) =>
    [bias, ratio, width, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching] = f_camPrerequisites(p.code, m, prior, atr, opening, barOpen, barClose, p.blocked, rangeThresholdPct, widthThresholdPct, allowNeutral)
    p.rangeOk := rangeOk
    p.widthOk := widthOk
    p.openOk := openOk
    p.regionOk := observationValid and regionOk
    p.blocked := observationValid and inWindow ? blocked : p.blocked
    p.candidate := sourceReady and observationValid and inWindow and candidate and p.state != "DISABLED"
    p.approaching := p.candidate and p.regionOk
    p.eligible := p.candidate
    p.smaOk := smaReady
    if p.state == "WATCH" or p.state == "DATA WAIT"
        bool prerequisitesAvailable = sourceReady and not na(opening) and not na(atr) and f_camSign(atr) > 0 and (p.family == "A" or m.outerValid)
        p.state := prerequisitesAvailable ? "WATCH" : "DATA WAIT"
        string widthNeed = p.family == "A" or p.family == "E" ? "Wide or Similar" : p.family == "F" or p.family == "G" ? "any width" : "Narrow or Similar"
        p.reason := not sourceReady ? dataReason : na(opening) ? "Exact source-minute opening unavailable" : not prerequisitesAvailable ? "Required geometry or confirmed ATR unavailable" : not inWindow ? "Outside decision window" : not observationValid ? "Malformed chart OHLC · price evaluation withheld" : p.blocked ? "Bad OPEN crossed this family's limit · blocked for this session" : not rangeOk ? "Close-delta context is " + (bias == 1 ? "Higher" : bias == -1 ? "Lower" : "Neutral") + "; this code requires " + (p.biasRequired == 1 ? "Higher" : "Lower") : not widthOk ? "Width " + width + "; requires " + widthNeed : not openOk ? "Frozen open " + f_camExact(opening) + " is outside this family's bounded inclusive opening interval" : not regionOk ? "Candidate eligible · closed price is outside its approach region" : "Approaching · prerequisites pass; original confirmed trigger still required"
        p.next := p.blocked or not rangeOk or not widthOk or not openOk ? "Next source session / changed prerequisites" : not inWindow ? "Next decision window" : p.approaching ? "Original close " + p.triggerOperator + " " + f_camExact(p.triggerPrice) + (smaGate and not p.smaOk ? " plus SMA confirmation" : " with the stated setup") : "Enter the play's approach region on a confirmed close"
    p

f_camRearm(Play p, int bar, bool inWindow, bool enabled, int cooldown, int episodeLimit) =>
    if enabled and inWindow and f_camTerminal(p.state) and not na(p.endBar) and bar > p.endBar and bar - p.endBar >= cooldown and p.episode < episodeLimit and p.candidate and not p.blocked
        p.state := "WATCH"
        p.setupBar := na
        p.setupTime := na
        p.armedHigh := na
        p.armedLow := na
        p.activationBar := na
        p.activationTime := na
        p.activationSeq := na
        p.entry := na
        p.stop := na
        p.target := na
        p.effectiveR := na
        p.endBar := na
        p.endTime := na
        p.outcomePrice := na
        p.ambiguous := false
        p.reason := "Cooldown completed · waiting for a new original setup"
    p

// Explanations report the observed family trigger without changing its lifecycle.
f_camActivationReason(Play p, float activationClose) =>
    bool longSide = p.dir == 1
    string tier = p.family == "A" ? (longSide ? "L3" : "H3") : p.family == "E" ? (longSide ? "L4" : "H4") : p.family == "F" or p.family == "G" ? (longSide ? "L6" : "H6") : (longSide ? "H4" : "L4")
    string cause = p.family == "A" ? tier + " touch + " + (longSide ? "reclaim" : "rejection") : p.family == "B" ? tier + " closed break, later retest" : p.family == "C" or p.family == "D" ? "Bounded opening region; " + tier + " retest held" : p.family == "E" ? "Closed " + (longSide ? "below " : "above ") + tier + ", later failure" : tier + " outer touch + recovery"
    cause + "; close " + f_camReadable(activationClose) + (longSide ? " > " : " < ") + tier + " " + f_camReadable(p.level) + (p.family == "D" ? " · against frozen structure" : "")

// Parameterized OHLC/clock step for production and exact-function fixture probes.
// Input p is a single episode row. Only its reference fields and event arrays mutate.
f_camStep(Play p, CamLevels m, float barOpen, float barHigh, float barLow, float barClose, float previousClose, int bar, int stamp, bool contained, bool sessionEnded, bool wide, float tick, int setupLimit, int activeLimit, int bufferTicks, float minimumR) =>
    bool usable = contained and f_camBarValid(barOpen, barHigh, barLow, barClose)
    if p.state == "ACTIVE"
        [kind, price, ambiguous, outcomeReason] = f_camOutcome(p, barOpen, barHigh, barLow, bar, stamp, usable, activeLimit, sessionEnded)
        if kind != ""
            f_camEnd(p, kind, outcomeReason, price, bar, stamp, ambiguous)
    else if p.state == "WATCH" or p.state == "ARMED"
        bool longSide = p.dir == 1
        bool touched = usable and f_camTouched(p.level, barHigh, barLow)
        if usable and p.state == "WATCH" and p.approaching
            bool shouldArm = p.family == "B" ? not na(previousClose) and f_camCompare(previousClose, p.level) * p.dir <= 0 and f_camCompare(barClose, p.level) * p.dir > 0 : p.family == "E" ? f_camCompare(barClose, p.level) * p.dir < 0 : p.family == "C" or p.family == "D" ? true : touched
            if shouldArm
                string armReason = p.family == "B" ? "Confirmed breakout observed · await a later retest" : p.family == "E" ? "Confirmed excursion observed · await a later failure" : p.family == "C" or p.family == "D" ? "Known opening in bounded fourth-to-sixth interval · wait for contained retest" : "Actual bar range touched the reference level"
                f_camArm(p, barHigh, barLow, bar, stamp, armReason)
                p.next := p.family == "E" ? "Later close " + (longSide ? "above " : "below ") + f_camExact(p.level) : "Close " + (longSide ? "above " : "below ") + f_camExact(p.level) + (p.family == "B" ? " after a later retest" : " after the observed test")
        if p.state == "ARMED"
            int age = bar - p.setupBar
            if age > setupLimit
                f_camEnd(p, "EXPIRED", "Setup deadline passed · confirmation allowed only through age " + str.tostring(setupLimit), na, bar, stamp, false)
            else if usable
                p.armedHigh := math.max(p.armedHigh, barHigh)
                p.armedLow := math.min(p.armedLow, barLow)
                bool cancelled = p.blocked or (p.family == "A" ? (longSide ? f_camCompare(barClose, m.l4) <= 0 : f_camCompare(barClose, m.h4) >= 0) : p.family == "B" or p.family == "C" or p.family == "D" ? (longSide ? f_camCompare(barClose, m.h3) <= 0 : f_camCompare(barClose, m.l3) >= 0) : p.family == "E" ? (longSide ? f_camCompare(barClose, m.l6) <= 0 : f_camCompare(barClose, m.h6) >= 0) : false)
                bool recoveryClose = f_camCompare(barClose, p.level) * p.dir > 0
                bool previousWrongSide = not na(previousClose) and f_camCompare(previousClose, p.level) * p.dir <= 0
                bool confirmed = p.family == "B" ? bar > p.setupBar and touched and recoveryClose : p.family == "E" ? bar > p.setupBar and recoveryClose : p.family == "C" or p.family == "D" ? touched and recoveryClose : recoveryClose and ((bar == p.setupBar and touched) or previousWrongSide)
                if cancelled
                    f_camEnd(p, "INVALIDATED", p.blocked ? "Bad OPEN crossed the public-baseline limit · session block latched" : "Pending close crossed the original cancellation boundary", barClose, bar, stamp, false)
                else if sessionEnded
                    f_camEnd(p, "EXPIRED", "Decision session ended before activation · no entry inferred", na, bar, stamp, false)
                else if confirmed and p.approaching and p.smaOk
                    [planOk, stop, target, rewardR, planReason] = f_camPlanOptions(p.dir, p.family, m, p.armedHigh, p.armedLow, barClose, wide, tick, bufferTicks, minimumR, p.atrUsed, p.stopMode, stopAtrMultiple, p.targetMode, targetAtrMultiple, maxTargetAtr)
                    if planOk
                        p.state := "ACTIVE"
                        p.entry := barClose
                        p.stop := stop
                        p.target := target
                        p.effectiveR := rewardR
                        p.activationBar := bar
                        p.activationTime := stamp
                        p.reason := f_camActivationReason(p, barClose)
                        p.next := "Observe later bars: stop " + f_camExact(stop) + " / destination " + f_camExact(target)
                        p.invalidation := "Fixed reference stop " + f_camExact(stop) + "; session end or active deadline"
                        p.activationSeq := f_camEmit(p, "ACTIVE", p.reason, barClose, stamp, false)
                    else
                        f_camEnd(p, "REJECTED", planReason + " · episode consumed", barClose, bar, stamp, false)
            else if sessionEnded
                f_camEnd(p, "EXPIRED", "Session end observed across an unavailable boundary · no fill inferred", na, bar, stamp, false)
    p

f_camClock(int barTime, int dailyStart, int dailyEnd, string mode, string sessionText, string timezone, bool weekdaysOnly) =>
    int startStamp = dailyStart
    int endStamp = dailyEnd
    bool dayAllowed = true
    string label = "Exchange day · provider boundaries"
    if mode != "Exchange day"
        string interval = mode == "US cash" ? "0930-1600" : sessionText
        string zone = mode == "US cash" ? "America/New_York" : timezone
        int startHour = int(str.tonumber(str.substring(interval, 0, 2)))
        int startMinute = int(str.tonumber(str.substring(interval, 2, 4)))
        int endHour = int(str.tonumber(str.substring(interval, 5, 7)))
        int endMinute = int(str.tonumber(str.substring(interval, 7, 9)))
        int firstMinute = startHour * 60 + startMinute
        int lastMinute = endHour * 60 + endMinute
        int nowMinute = hour(barTime, zone) * 60 + minute(barTime, zone)
        bool overnight = lastMinute <= firstMinute
        int dayOffset = overnight and nowMinute < firstMinute ? -1 : 0
        int localYear = year(barTime, zone)
        int localMonth = month(barTime, zone)
        int localDay = dayofmonth(barTime, zone) + dayOffset
        startStamp := timestamp(zone, localYear, localMonth, localDay, startHour, startMinute)
        endStamp := timestamp(zone, localYear, localMonth, localDay + (overnight ? 1 : 0), endHour, endMinute)
        int tradingWeekday = dayofweek(lastMinute < firstMinute ? endStamp : startStamp, zone)
        dayAllowed := (mode == "Custom" and not weekdaysOnly) or (tradingWeekday >= dayofweek.monday and tradingWeekday <= dayofweek.friday)
        label := (mode == "US cash" ? "US cash · " : "Custom · ") + interval + " " + zone
    [startStamp, endStamp, dayAllowed, label]

f_camOpenLocation(float opening, CamLevels m) =>
    na(opening) ? "OPEN UNKNOWN" : not m.valid ? "MAP UNAVAILABLE" : f_camCompare(opening, m.l4) < 0 ? "Below L4" : f_camCompare(opening, m.l4) == 0 ? "At L4" : f_camCompare(opening, m.l3) < 0 ? "L4–L3" : f_camCompare(opening, m.l3) == 0 ? "At L3" : f_camCompare(opening, m.h3) < 0 ? "Inside third band" : f_camCompare(opening, m.h3) == 0 ? "At H3" : f_camCompare(opening, m.h4) < 0 ? "H3–H4" : f_camCompare(opening, m.h4) == 0 ? "At H4" : "Above H4"

//

// SOURCE ADAPTER · one-minute observations, session-frozen scalar packet.
// Lower-timeframe request returns an already frozen context, never one candle's
// OHLC as a proxy for an entire source window. Availability is checked again below.

f_camBookFinish(CamWindowBook book, bool complete, string reason) =>
    book.prior := book.latest
    book.latest := f_camLevels(book.h, book.l, book.c, book.start, book.finish, complete, reason)
    book.finished := true
    book.complete := complete
    book.reason := reason
    book

f_camObserve(CamWindowBook book, int start, int finish, bool allowed, int stamp, int closingStamp, float o, float h, float l, float c) =>
    bool overlap = allowed and not na(start) and not na(finish) and stamp < finish and closingStamp > start
    bool contained = overlap and stamp >= start and closingStamp <= finish
    bool valid = f_camBarValid(o, h, l, c)
    if not book.finished and (stamp >= book.finish or overlap and start != book.start)
        f_camBookFinish(book, false, "Exact final source boundary missing")
    if overlap
        if na(book.start) or book.start != start
            book.start := start
            book.finish := finish
            book.lastClose := na
            book.h := na
            book.l := na
            book.c := na
            book.samples := 0
            book.complete := contained and valid and stamp == start
            book.finished := false
            book.reason := book.complete ? "Collecting contiguous 1m source" : "Exact valid source opening missing"
        if not book.finished
            if not contained or not valid or not na(book.lastClose) and stamp != book.lastClose
                book.complete := false
                book.reason := not contained ? "Source minute straddles boundary" : not valid ? "Malformed source OHLC" : "Missing interior source minute"
            if contained
                if valid
                    book.h := na(book.h) ? h : math.max(book.h, h)
                    book.l := na(book.l) ? l : math.min(book.l, l)
                    book.c := c
                    book.samples += 1
                book.lastClose := closingStamp
            if closingStamp >= finish
                bool complete = book.complete and contained and closingStamp == finish
                f_camBookFinish(book, complete, complete ? "Complete observed 1m interval" : book.reason)
    book

// Original observed-session ATR convention, not an undisclosed vendor formula.
// Missing calendar days are not observations; invalid observed windows reset.
// Empty/duplicate/out-of-order packets cannot count twice or rewind the chain.
f_camAtrConsume(CamAtrBook book, CamLevels source) =>
    if not na(source.sourceEnd) and (na(book.observedEnd) or source.sourceEnd > book.observedEnd)
        book.observedEnd := source.sourceEnd
        bool valid = source.complete and not na(source.sourceStart) and source.sourceEnd > source.sourceStart and (na(book.sourceEnd) or source.sourceStart >= book.sourceEnd) and f_camBarValid(source.c, source.h, source.l, source.c)
        if valid
            float tr = na(book.previousClose) ? source.h - source.l : math.max(source.h - source.l, math.max(math.abs(source.h - book.previousClose), math.abs(source.l - book.previousClose)))
            if book.samples == 0
                book.sourceStart := source.sourceStart
            book.samples += 1
            if book.samples <= 14
                book.seedSum += tr
                book.value := book.samples == 14 ? book.seedSum / 14 : na
            else
                book.value := (book.value * 13 + tr) / 14
            book.previousClose := source.c
            book.sourceEnd := source.sourceEnd
        else
            book.samples := 0
            book.seedSum := 0
            book.previousClose := na
            book.value := na
            book.sourceStart := na
            book.sourceEnd := na
    book

f_camAtrBasisCode(string mode, bool futures, string resolvedDecision) =>
    mode == "RTH session" or mode == "Market default" and futures and resolvedDecision != "Exchange day" ? 2 : 1

f_camAtrBasisName(int code) =>
    code == 2 ? "Observed RTH sessions · Wilder14" : code == 1 ? "Provider daily · ATR14" : "Unavailable"

f_camAtrReady(float value, int sourceEnd, int cutoff) =>
    not na(value) and f_camSign(value) > 0 and not na(sourceEnd) and not na(cutoff) and sourceEnd <= cutoff and cutoff - sourceEnd <= 604800000

f_camAtrFreeze(CamSourcePacket packet, CamAtrBook book, int basis, float providerAtr, int providerStamp, int cutoff) =>
    packet.atrBasisCode := basis
    packet.atr := basis == 2 ? book.value : providerAtr
    packet.atrStamp := basis == 2 ? book.sourceEnd : providerStamp
    packet.atrSamples := basis == 2 ? book.samples : not na(providerAtr) ? 14 : 0
    packet.atrStart := basis == 2 ? book.sourceStart : na
    packet.atrReady := f_camAtrReady(packet.atr, packet.atrStamp, cutoff)
    packet

f_camSourceCode(string kind) =>
    kind == "RTH" ? 1 : kind == "ETH" ? 2 : kind == "Exchange daily" ? 3 : kind == "Selected session" ? 4 : 0

f_camSourceName(int code) =>
    code == 1 ? "RTH" : code == 2 ? "ETH" : code == 3 ? "Exchange daily" : code == 4 ? "Selected session" : "Unavailable"

f_camSourceReason(int code) =>
    code == 1 ? "Complete source pair frozen before the opening minute" : code == 2 ? "Auto requires complete prior postmarket/current premarket ending at RTH open; choose a fixed source if unavailable" : code == 3 ? "Two complete chosen H/L and close windows required" : code == 4 ? "Source or independent-close timestamps not ordered" : code == 5 ? "Source or ATR unavailable before decision open" : code == 6 ? "Source or close older than seven elapsed days" : code == 7 ? "Chosen current inner geometry unavailable" : code == 8 ? "Chosen previous inner geometry unavailable" : code == 9 ? "Selected ATR unavailable, warming or stale" : code == 10 ? "Exact valid one-minute opening unavailable" : "Waiting for an observed source opening"

f_camSelectionReason(int code) =>
    code == 6 ? "Market default: Exchange daily for crypto; 24/7 provider clock" : code == 7 ? "Market default: RTH for non-crypto; configured cash-session source" : code == 1 ? "Auto crypto: Exchange daily, 24/7 provider clock" : code == 2 ? "Fixed source; no automatic session substitution" : code == 3 ? "ETH selected: pre-opening excursion exceeded selected ATR tolerance" : code == 4 ? "RTH selected: complete pre-opening excursion stayed within tolerance" : "Auto selection lacks complete aligned pre/post windows"

f_camSourceEngine() =>
    // Explicit ticker/timeframe in this nested request; every field is completed.
    [dh1, dl1, dc1, ds1, de1, dh2, dl2, dc2, ds2, de2, dailyAtr] = request.security(syminfo.tickerid, "D", [high[1], low[1], close[1], time[1], time_close[1], high[2], low[2], close[2], time[2], time_close[2], ta.atr(14)[1]], gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on, calc_bars_count = 500)
    int exchangeStart = time("D")
    int exchangeEnd = time_close("D")
    bool crypto = syminfo.type == "crypto"
    string resolvedDecision = decisionMode == "Auto" ? (crypto ? "Exchange day" : "US cash") : decisionMode
    [decisionStart, decisionEnd, decisionAllowed, clockLabel] = f_camClock(time, exchangeStart, exchangeEnd, resolvedDecision, customSession, customTimezone, customWeekdays)
    [rthStart, rthEnd, rthAllowed, rthLabel] = f_camClock(time, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    string ethMode = extendedClock == "Auto" and crypto ? "Exchange day" : "Custom"
    string ethText = extendedClock == "Custom" ? extendedSession : syminfo.type == "futures" ? "1800-1700" : "0400-2000"
    string ethZone = extendedClock == "Custom" ? extendedTimezone : "America/New_York"
    [ethStart, ethEnd, ethAllowed, ethLabel] = f_camClock(time, exchangeStart, exchangeEnd, ethMode, ethText, ethZone, not crypto)
    // Find the RTH opening belonging to this ETH interval using local calendar dates.
    [regularAtEthStart, regularEndAtEthStart, ignoredDay, ignoredLabel] = f_camClock(ethStart, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    int nextRegular = regularAtEthStart
    if not na(nextRegular) and nextRegular < ethStart
        nextRegular := timestamp(regularTimezone, year(nextRegular, regularTimezone), month(nextRegular, regularTimezone), dayofmonth(nextRegular, regularTimezone) + 1, hour(nextRegular, regularTimezone), minute(nextRegular, regularTimezone))
    [pairedRegularStart, pairedRegularEnd, pairedDay, pairedLabel] = f_camClock(nextRegular, exchangeStart, exchangeEnd, "Custom", regularSession, regularTimezone, not crypto)
    var CamAtrBook rthAtr = CamAtrBook.new()
    var CamWindowBook rth = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook eth = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook selected = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook pre = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamWindowBook post = CamWindowBook.new(CamLevels.new(), CamLevels.new())
    var CamSourcePacket frozen = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
    bool overlap = decisionAllowed and time < decisionEnd and time_close > decisionStart
    bool newDecision = overlap and (na(frozen.sessionStart) or frozen.sessionStart != decisionStart)
    // Certify any missed closing boundary before selecting predecessors.
    if not rth.finished and time >= rth.finish
        f_camBookFinish(rth, false, "Exact RTH end was not observed")
    if not eth.finished and time >= eth.finish
        f_camBookFinish(eth, false, "Exact ETH end was not observed")
    if not selected.finished and time >= selected.finish
        f_camBookFinish(selected, false, "Exact selected-window end was not observed")
    // A missed observed closing boundary is consumed before freezing this opening.
    f_camAtrConsume(rthAtr, rth.latest)
    if newDecision
        CamSourcePacket next = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
        next.sessionStart := decisionStart
        next.sessionEnd := decisionEnd
        next.cutoff := decisionStart
        next.clockLabel := clockLabel
        next.openKnown := time == decisionStart and time_close <= decisionEnd and f_camBarValid(open, high, low, close)
        next.opening := next.openKnown ? open : na
        next.openingTime := next.openKnown ? time : na
        int atrBasis = f_camAtrBasisCode(atrBasisMode, syminfo.type == "futures", resolvedDecision)
        f_camAtrFreeze(next, rthAtr, atrBasis, dailyAtr, de1, decisionStart)
        CamLevels daily1 = f_camLevels(dh1, dl1, dc1, ds1, de1, not na(de1), "Completed provider daily source unavailable")
        CamLevels daily2 = f_camLevels(dh2, dl2, dc2, ds2, de2, not na(de2), "Second provider daily source unavailable")
        string chosen = sourceMode == "Market default" ? (crypto ? "Exchange daily" : "RTH") : sourceMode == "Auto RTH/ETH" and crypto ? "Exchange daily" : sourceMode
        bool selectionReady = true
        if chosen == "Auto RTH/ETH"
            bool postReady = post.latest.complete and post.latest.sourceStart == rth.latest.sourceEnd and post.latest.sourceEnd <= decisionStart
            bool preReady = pre.latest.complete and pre.latest.sourceEnd == decisionStart and pre.latest.sourceStart >= rth.latest.sourceEnd
            bool alignedOpening = decisionStart == nextRegular
            selectionReady := rth.latest.complete and eth.latest.complete and postReady and preReady and alignedOpening and next.atrReady
            next.excursion := selectionReady ? math.max(math.max(post.latest.h, pre.latest.h) - rth.latest.h, rth.latest.l - math.min(post.latest.l, pre.latest.l)) : na
            next.excursionThreshold := next.atr * switchingTolerance / 100
            bool outside = selectionReady and (f_camCompare(math.max(post.latest.h, pre.latest.h), rth.latest.h + next.excursionThreshold) > 0 or f_camCompare(math.min(post.latest.l, pre.latest.l), rth.latest.l - next.excursionThreshold) < 0)
            chosen := outside ? "ETH" : "RTH"
            next.selectionReason := not selectionReady ? "Auto requires complete prior postmarket and current premarket ending at the chosen RTH open; choose a fixed source if unavailable" : outside ? "ETH selected: observed excursion exceeded selected ATR tolerance before open" : "RTH selected: complete observed excursion stayed within tolerance at open"
            next.selectionCode := not selectionReady ? 5 : outside ? 3 : 4
        else
            next.selectionCode := sourceMode == "Market default" ? (crypto ? 6 : 7) : sourceMode == "Auto RTH/ETH" and crypto ? 1 : 2
            next.selectionReason := f_camSelectionReason(next.selectionCode)
        CamLevels main1 = chosen == "RTH" ? rth.latest : chosen == "ETH" ? eth.latest : chosen == "Selected session" ? selected.latest : daily1
        CamLevels main2 = chosen == "RTH" ? rth.prior : chosen == "ETH" ? eth.prior : chosen == "Selected session" ? selected.prior : daily2
        string chosenClose = closeMode == "Match source" ? chosen : closeMode
        CamLevels close1 = chosenClose == "RTH" ? rth.latest : chosenClose == "ETH" ? eth.latest : chosenClose == "Selected session" ? selected.latest : daily1
        CamLevels close2 = chosenClose == "RTH" ? rth.prior : chosenClose == "ETH" ? eth.prior : chosenClose == "Selected session" ? selected.prior : daily2
        next.sourceKind := chosen + (chosen == "Exchange daily" ? " · provider H/L" : " · observed 1m H/L")
        next.closeKind := chosenClose + (chosenClose == "Exchange daily" ? " · provider daily/settlement C" : " · observed last-trade C")
        next.sourceCode := f_camSourceCode(chosen)
        next.closeCode := f_camSourceCode(chosenClose)
        next.closeSourceEnd := close1.sourceEnd
        next.priorCloseSourceEnd := close2.sourceEnd
        bool completePair = main1.complete and main2.complete and close1.complete and close2.complete
        bool ordered = not na(main1.sourceStart) and not na(main1.sourceEnd) and not na(main2.sourceEnd) and main2.sourceEnd <= main1.sourceStart and close2.sourceEnd <= close1.sourceStart
        bool available = main1.sourceEnd <= decisionStart and close1.sourceEnd <= decisionStart and next.atrStamp <= decisionStart
        bool fresh = not na(main1.sourceEnd) and decisionStart - main1.sourceEnd <= 604800000 and not na(close1.sourceEnd) and decisionStart - close1.sourceEnd <= 604800000
        next.current := f_camLevels(main1.h, main1.l, close1.c, main1.sourceStart, main1.sourceEnd, completePair and selectionReady, "Required H/L or selected close predecessor is incomplete", false)
        next.prior := f_camLevels(main2.h, main2.l, close2.c, main2.sourceStart, main2.sourceEnd, completePair and selectionReady, "Previous chosen H/L or close source is incomplete", false)
        next.ready := selectionReady and completePair and ordered and available and fresh and next.current.valid and next.prior.valid and next.atrReady
        next.reason := not selectionReady ? next.selectionReason : not completePair ? "Two complete chosen H/L and close windows required" : not ordered ? "Source or independent-close timestamps not ordered" : not available ? "Source or ATR was unavailable before decision open" : not fresh ? "Source or close is older than seven elapsed days" : not next.current.valid ? next.current.reason : not next.prior.valid ? next.prior.reason : not next.atrReady ? "Selected ATR unavailable, warming or stale" : not next.openKnown ? "Exact valid one-minute opening unavailable" : "Complete source pair frozen before the opening minute"
        next.reasonCode := not selectionReady ? 2 : not completePair ? 3 : not ordered ? 4 : not available ? 5 : not fresh ? 6 : not next.current.valid ? 7 : not next.prior.valid ? 8 : not next.atrReady ? 9 : not next.openKnown ? 10 : 1
        next.rthSamples := rth.samples
        next.ethSamples := eth.samples
        next.selectedSamples := selected.samples
        next.rthComplete := rth.latest.complete
        next.ethComplete := eth.latest.complete
        next.selectedComplete := selected.latest.complete
        next.coverage := "1m source · RTH " + rth.reason + " / ETH " + eth.reason + "; up to 100,000 source minutes"
        frozen := next
    // Current minute cannot affect this decision's selected map or auto choice.
    f_camObserve(rth, rthStart, rthEnd, rthAllowed, time, time_close, open, high, low, close)
    f_camAtrConsume(rthAtr, rth.latest)
    f_camObserve(eth, ethStart, ethEnd, ethAllowed, time, time_close, open, high, low, close)
    f_camObserve(selected, decisionStart, decisionEnd, decisionAllowed, time, time_close, open, high, low, close)
    f_camObserve(pre, ethStart, nextRegular, ethAllowed and pairedDay and ethStart < nextRegular, time, time_close, open, high, low, close)
    f_camObserve(post, pairedRegularEnd, ethEnd, ethAllowed and pairedDay and pairedRegularEnd < ethEnd, time, time_close, open, high, low, close)
    // Scalar-only transport is essential: returning nested UDTs across 100k
    // source minutes exhausted actual TradingView runtime memory in development.
    [frozen.current.h, frozen.current.l, frozen.current.c, frozen.current.sourceStart, frozen.current.sourceEnd, frozen.current.complete, frozen.prior.h, frozen.prior.l, frozen.prior.c, frozen.prior.sourceStart, frozen.prior.sourceEnd, frozen.prior.complete, frozen.ready, frozen.openKnown, frozen.opening, frozen.openingTime, frozen.sessionStart, frozen.sessionEnd, frozen.cutoff, frozen.atr, frozen.atrStamp, frozen.closeSourceEnd, frozen.priorCloseSourceEnd, frozen.excursion, frozen.excursionThreshold, frozen.sourceCode, frozen.closeCode, frozen.reasonCode, frozen.selectionCode, frozen.rthSamples, frozen.ethSamples, frozen.selectedSamples, frozen.rthComplete, frozen.ethComplete, frozen.selectedComplete, frozen.atrBasisCode, frozen.atrSamples, frozen.atrStart]

// Modify only session selection; retain the chart ticker's other price modifiers.
string camSourceTicker = ticker.modify(syminfo.tickerid, session = session.extended)
[camRawH1, camRawL1, camRawC1, camRawStart1, camRawEnd1, camRawComplete1, camRawH2, camRawL2, camRawC2, camRawStart2, camRawEnd2, camRawComplete2, camRawReady, camRawOpenKnown, camRawOpen, camRawOpenTime, camRawSessionStart, camRawSessionEnd, camRawCutoff, camRawAtr, camRawAtrStamp, camRawCloseEnd1, camRawCloseEnd2, camRawExcursion, camRawExcursionThreshold, camRawSourceCode, camRawCloseCode, camRawReasonCode, camRawSelectionCode, camRawRthSamples, camRawEthSamples, camRawSelectedSamples, camRawRthComplete, camRawEthComplete, camRawSelectedComplete, camRawAtrBasisCode, camRawAtrSamples, camRawAtrStart] = request.security(camSourceTicker, "1", f_camSourceEngine(), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off, calc_bars_count = 100000)
var CamSourcePacket camSourcePacket = CamSourcePacket.new(current = CamLevels.new(), prior = CamLevels.new())
int camExchangeStart = time("D")
int camExchangeEnd = time_close("D")
string camResolvedDecision = decisionMode == "Auto" ? (syminfo.type == "crypto" ? "Exchange day" : "US cash") : decisionMode
[camScheduledStart, camScheduledEnd, camDayAllowed, camClockLabel] = f_camClock(time, camExchangeStart, camExchangeEnd, camResolvedDecision, customSession, customTimezone, customWeekdays)
bool camChartSupported = chart.is_standard and timeframe.isminutes and timeframe.multiplier >= 1 and timeframe.multiplier <= 60
bool camOverlapNow = camChartSupported and camDayAllowed and not na(time_close) and time < camScheduledEnd and time_close > camScheduledStart
bool camContainedNow = camOverlapNow and time >= camScheduledStart and time_close <= camScheduledEnd
bool camBarOhlcValid = f_camBarValid(open, high, low, close)
float camSma = ta.sma(close, smaLength)

var CamLevels camMap = CamLevels.new()
var CamLevels camPriorMap = CamLevels.new()
var int camSessionStart = na
var int camSessionEnd = na
var int camOpenTime = na
var float camOpen = na
var bool camOpenKnown = false
var bool camMapReady = false
var bool camSourceStale = false
var string camDataReason = "Waiting for first fully contained decision candle"
var string camRelationship = "Unavailable"
var string camWidthLabel = "Unavailable"
var string camOpenLocation = "OPEN UNKNOWN"
var string camDecisionLabel = "Decision window not observed"
var string camSourceLabel = "Source not frozen"
var string camCloseLabel = "Close source not frozen"
var string camSourceSelectionReason = "Waiting for source cutoff"
var int camSourceCutoff = na
var int camOpeningSourceTime = na
var int camAtrStamp = na
var float camAtrReference = na
var string camAtrBasis = "Unavailable"
var bool camAtrReady = false
var int camAtrSamples = 0
var int camAtrStart = na
var float camRangeDelta = na
var float camRangeThreshold = na
var int camBias = 0
var float camWidthRatio = na
var int camClosedBar = na
var int camClosedTime = na
var float camClosedPrice = na
var bool camInSession = false
var float camPreviousClose = na
var string camSelectedCoverage = "Waiting for 1m source snapshot"
var int camSelectedCount = 0
var int camActiveCount = 0
var int camHigherActive = 0
var int camLowerActive = 0
var int camCandidateCount = 0
var int camApproachingCount = 0
var bool camSessionEndedOnce = false
var int camPreviousObservedTime = na
var int camPreviousObservedClose = na

bool camSessionStartPulse = false
bool camSessionEndPulse = false
bool camActivated = false
bool camEnded = false
bool camArmed = false
int camActivationCount = 0
int camEndingCount = 0

if array.size(plays) == 0
    for i = 0 to 13
        array.push(plays, f_camNewPlay(i, camMap, false, "Waiting for supported chart and source opening", 0, na, false, na))

if barstate.isconfirmed
    camClosedBar := bar_index
    camClosedTime := time_close
    camClosedPrice := camBarOhlcValid ? close : na
    array.clear(camBarEvents)
    if not camChartSupported
        camDataReason := "Unsupported chart · use standard minute candles, 1m through 60m"
        camDecisionLabel := "UNSUPPORTED CHART"
        camSelectedCoverage := "DISABLED · unsupported chart"
    bool newDecision = camContainedNow and (na(camSessionStart) or camScheduledStart != camSessionStart)
    if newDecision
        for i = 0 to 13
            Play old = array.get(plays, i)
            if old.state == "ACTIVE" or old.state == "ARMED"
                f_camEnd(old, "EXPIRED", "Late-observed session rollover · no unobserved closing fill inferred", na, bar_index, time_close, false)
        camSessionStartPulse := true
        camSessionStart := camScheduledStart
        camSessionEnd := camScheduledEnd
        camSessionEndedOnce := false
        // Reconstruct cross-context data once per decision, never once per minute.
        CamSourcePacket admitted = CamSourcePacket.new(current = f_camLevels(camRawH1, camRawL1, camRawC1, camRawStart1, camRawEnd1, camRawComplete1, "Incomplete current source", false), prior = f_camLevels(camRawH2, camRawL2, camRawC2, camRawStart2, camRawEnd2, camRawComplete2, "Incomplete previous source", false))
        admitted.ready := camRawReady
        admitted.openKnown := camRawOpenKnown
        admitted.opening := camRawOpen
        admitted.openingTime := camRawOpenTime
        admitted.sessionStart := camRawSessionStart
        admitted.sessionEnd := camRawSessionEnd
        admitted.cutoff := camRawCutoff
        admitted.atr := camRawAtr
        admitted.atrStamp := camRawAtrStamp
        admitted.atrBasisCode := camRawAtrBasisCode
        admitted.atrSamples := camRawAtrSamples
        admitted.atrStart := camRawAtrStart
        admitted.atrReady := f_camAtrReady(camRawAtr, camRawAtrStamp, camRawCutoff)
        admitted.closeSourceEnd := camRawCloseEnd1
        admitted.priorCloseSourceEnd := camRawCloseEnd2
        admitted.excursion := camRawExcursion
        admitted.excursionThreshold := camRawExcursionThreshold
        admitted.sourceCode := camRawSourceCode
        admitted.closeCode := camRawCloseCode
        admitted.reasonCode := camRawReasonCode
        admitted.selectionCode := camRawSelectionCode
        admitted.sourceKind := f_camSourceName(camRawSourceCode) + (camRawSourceCode == 3 ? " · provider H/L" : " · observed 1m H/L")
        admitted.closeKind := f_camSourceName(camRawCloseCode) + (camRawCloseCode == 3 ? " · provider daily/settlement C" : " · observed last-trade C")
        admitted.reason := f_camSourceReason(camRawReasonCode)
        admitted.selectionReason := f_camSelectionReason(camRawSelectionCode)
        admitted.rthSamples := camRawRthSamples
        admitted.ethSamples := camRawEthSamples
        admitted.selectedSamples := camRawSelectedSamples
        admitted.rthComplete := camRawRthComplete
        admitted.ethComplete := camRawEthComplete
        admitted.selectedComplete := camRawSelectedComplete
        admitted.coverage := "1m source · RTH " + (camRawRthComplete ? "complete " : "incomplete ") + str.tostring(camRawRthSamples) + " bars; ETH " + (camRawEthComplete ? "complete " : "incomplete ") + str.tostring(camRawEthSamples) + "; up to 100,000 source minutes"
        camSourcePacket := admitted
        bool packetMatches = not na(camSourcePacket) and camSourcePacket.sessionStart == camSessionStart and camSourcePacket.cutoff == camSessionStart and camSourcePacket.cutoff <= time
        bool chartOpeningObserved = time == camSessionStart or not na(camPreviousObservedTime) and camPreviousObservedTime <= camSessionStart and camPreviousObservedClose >= camSessionStart
        camMap := packetMatches ? camSourcePacket.current : CamLevels.new()
        camPriorMap := packetMatches ? camSourcePacket.prior : CamLevels.new()
        camOpenKnown := packetMatches and camSourcePacket.openKnown and chartOpeningObserved
        camOpen := camOpenKnown ? camSourcePacket.opening : na
        camOpenTime := camOpenKnown ? camSourcePacket.openingTime : na
        camOpeningSourceTime := camOpenTime
        camMapReady := packetMatches and camSourcePacket.ready
        camSourceCutoff := packetMatches ? camSourcePacket.cutoff : na
        camAtrReference := packetMatches ? camSourcePacket.atr : na
        camAtrStamp := packetMatches ? camSourcePacket.atrStamp : na
        camAtrBasis := packetMatches ? f_camAtrBasisName(camSourcePacket.atrBasisCode) : "Unavailable"
        camAtrReady := packetMatches and camSourcePacket.atrReady
        camAtrSamples := packetMatches ? camSourcePacket.atrSamples : 0
        camAtrStart := packetMatches ? camSourcePacket.atrStart : na
        camSourceLabel := packetMatches ? camSourcePacket.sourceKind : "Source packet unavailable at this chart opening"
        camCloseLabel := packetMatches ? camSourcePacket.closeKind : "Unavailable"
        camSourceSelectionReason := packetMatches ? camSourcePacket.selectionReason : "No matching session snapshot admitted"
        camSelectedCoverage := packetMatches ? camSourcePacket.coverage : "Source history unavailable or packet identity differs"
        camDecisionLabel := camClockLabel
        camDataReason := not packetMatches ? "Matching frozen source unavailable at first contained chart candle" : not camSourcePacket.ready ? camSourcePacket.reason : not chartOpeningObserved ? "Chart history starts after the opening boundary · earlier bad-price blocks are unknown" : not camOpenKnown ? "Exact valid source opening unavailable" : camSourcePacket.reason
        camSourceStale := not na(camMap.sourceEnd) and camSessionStart - camMap.sourceEnd > 604800000
        [relation, geometricBias, geometricRatio, geometricWidth] = f_camRelationship(camMap, camPriorMap)
        camRelationship := camMapReady ? relation : "Unavailable"
        [newBias, ratio, widthLabel, rangeOk, widthOk, openOk, regionOk, blocked, candidate, approaching] = f_camPrerequisites("HA", camMap, camPriorMap, camAtrReference, camOpen, camOpen, camOpen, false, rangeThresholdPct, widthThresholdPct, allowNeutral)
        camBias := camMapReady ? newBias : 0
        camWidthRatio := camMapReady ? ratio : na
        camWidthLabel := camMapReady ? widthLabel : "Unavailable"
        camRangeDelta := camMapReady ? camMap.c - camPriorMap.c : na
        camRangeThreshold := camAtrReference * rangeThresholdPct / 100
        camOpenLocation := f_camOpenLocation(camOpen, camMap)
        camPreviousClose := camOpen
        for i = 0 to 13
            Play p = f_camNewPlay(i, camMap, camMapReady, camDataReason, camBias, camOpen, camOpenKnown, camSessionStart)
            p.sourceKind := camSourceLabel
            p.closeKind := camCloseLabel
            p.atrUsed := camAtrReference
            p.atrStamp := camAtrStamp
            p.atrBasis := camAtrBasis
            p.atrSamples := camAtrSamples
            p.atrStart := camAtrStart
            p.sourceCutoff := camSourceCutoff
            p.closeSourceEnd := packetMatches ? camSourcePacket.closeSourceEnd : na
            p.stopMode := stopModel
            p.targetMode := targetModel
            // The source opening is the first observed OPEN, including on 60m charts.
            if camOpenKnown
                f_camRefreshPlay(p, camMap, camPriorMap, camAtrReference, camOpen, camOpen, camOpen, true, true, camMapReady, camDataReason, true)
            array.set(plays, i, p)
    camInSession := camContainedNow and not na(camSessionStart) and camScheduledStart == camSessionStart
    bool sessionEnded = not na(camSessionEnd) and time_close >= camSessionEnd
    if sessionEnded and not camSessionEndedOnce
        camSessionEndPulse := true
        camSessionEndedOnce := true
    for i = 0 to 13
        Play p = array.get(plays, i)
        bool smaReady = not smaGate or not na(camSma) and bar_index >= smaLength - 1 and f_camCompare(close, camSma) * p.dir > 0
        f_camRefreshPlay(p, camMap, camPriorMap, camAtrReference, camOpen, open, close, camBarOhlcValid, camInSession, camMapReady and camOpenKnown, camDataReason, smaReady)
        f_camRearm(p, bar_index, camInSession and not sessionEnded, allowRearm, cooldownBars, maxEpisodes)
        f_camStep(p, camMap, open, high, low, close, camPreviousClose, bar_index, time_close, camInSession, sessionEnded, camWidthLabel == "Wide", syminfo.mintick, setupDeadline, activeDeadline, stopBufferTicks, minRewardR)
    if camInSession
        camPreviousClose := camBarOhlcValid ? close : na
    camActiveCount := 0
    camHigherActive := 0
    camLowerActive := 0
    camCandidateCount := 0
    camApproachingCount := 0
    for i = 0 to 13
        Play p = array.get(plays, i)
        camActiveCount += p.state == "ACTIVE" ? 1 : 0
        camHigherActive += p.state == "ACTIVE" and p.biasRequired == 1 ? 1 : 0
        camLowerActive += p.state == "ACTIVE" and p.biasRequired == -1 ? 1 : 0
        camCandidateCount += p.candidate ? 1 : 0
        camApproachingCount += p.approaching ? 1 : 0
    if array.size(camBarEvents) > 0
        for i = 0 to array.size(camBarEvents) - 1
            CamEvent e = array.get(camBarEvents, i)
            camActivationCount += e.kind == "ACTIVE" ? 1 : 0
            camEndingCount += f_camTerminal(e.kind) ? 1 : 0
            camArmed := camArmed or e.kind == "ARMED"
    camActivated := camActivationCount > 0
    camEnded := camEndingCount > 0
    camPreviousObservedTime := time
    camPreviousObservedClose := time_close

f_camJsonString(string value) =>
    string escaped = str.replace_all(value, "\\", "\\\\")
    escaped := str.replace_all(escaped, "\"", "\\\"")
    escaped := str.replace_all(escaped, "\n", "\\n")
    escaped := str.replace_all(escaped, "\t", "\\t")
    "\"" + escaped + "\""

f_camJsonNumber(float value) =>
    f_camNumber(value)

f_camJsonTime(int value) =>
    na(value) ? "null" : str.tostring(value)

f_camJsonBool(bool value) =>
    value ? "true" : "false"

f_camSettingsId() =>
    CAM_FORMULA + "|lite2-original2.1|" + atrBasisMode + "|" + sourceMode + "|" + closeMode + "|" + decisionMode + "|" + customSession + "|" + customTimezone + "|" + f_camJsonBool(customWeekdays) + "|" + regularSession + "|" + regularTimezone + "|" + extendedClock + "|" + extendedSession + "|" + extendedTimezone + "|" + f_camJsonNumber(switchingTolerance) + "|" + f_camJsonNumber(rangeThresholdPct) + "|" + f_camJsonNumber(widthThresholdPct) + "|" + f_camJsonBool(allowNeutral) + "|" + f_camJsonBool(smaGate) + "|" + str.tostring(smaLength) + "|" + f_camJsonBool(enableA) + f_camJsonBool(enableB) + f_camJsonBool(enableC) + f_camJsonBool(enableD) + f_camJsonBool(enableE) + f_camJsonBool(enableF) + f_camJsonBool(enableG) + "|" + str.tostring(setupDeadline) + "|" + str.tostring(activeDeadline) + "|" + str.tostring(stopBufferTicks) + "|" + f_camJsonNumber(minRewardR) + "|" + stopModel + "|" + f_camJsonNumber(stopAtrMultiple) + "|" + targetModel + "|" + f_camJsonNumber(targetAtrMultiple) + "|" + f_camJsonNumber(maxTargetAtr) + "|" + f_camJsonBool(allowRearm) + "|" + str.tostring(cooldownBars) + "|" + str.tostring(maxEpisodes)

f_camEventJson(CamEvent e) =>
    string id = syminfo.tickerid + "|" + timeframe.period + "|" + f_camSettingsId() + "|" + str.tostring(e.sessionStart) + "|" + e.code + "|" + str.tostring(e.episode) + "|" + e.kind + "|" + str.tostring(e.eventTime)
    "{\"id\":" + f_camJsonString(id) + ",\"code\":" + f_camJsonString(e.code) + ",\"episode\":" + str.tostring(e.episode) + ",\"type\":" + f_camJsonString(e.kind) + ",\"direction\":" + f_camJsonString(e.dir == 1 ? "long" : "short") + ",\"time\":" + f_camJsonTime(e.eventTime) + ",\"session_start\":" + f_camJsonTime(e.sessionStart) + ",\"source_start\":" + f_camJsonTime(e.sourceStart) + ",\"source_close\":" + f_camJsonTime(e.sourceEnd) + ",\"source_kind\":" + f_camJsonString(e.sourceKind) + ",\"close_kind\":" + f_camJsonString(e.closeKind) + ",\"selected_close_time\":" + f_camJsonTime(e.closeSourceEnd) + ",\"source_cutoff\":" + f_camJsonTime(e.sourceCutoff) + ",\"atr_time\":" + f_camJsonTime(e.atrStamp) + ",\"atr_basis\":" + f_camJsonString(e.atrBasis) + ",\"atr_value\":" + f_camJsonNumber(e.atrUsed) + ",\"atr_samples\":" + str.tostring(e.atrSamples) + ",\"atr_start\":" + f_camJsonTime(e.atrStart) + ",\"price\":" + f_camJsonNumber(e.price) + ",\"entry\":" + f_camJsonNumber(e.entry) + ",\"stop\":" + f_camJsonNumber(e.stop) + ",\"target\":" + f_camJsonNumber(e.target) + ",\"ambiguous\":" + f_camJsonBool(e.ambiguous) + ",\"reason\":" + f_camJsonString(e.reason) + "}"

if barstate.isconfirmed and array.size(camBarEvents) > 0
    string selectedEvents = ""
    int selectedCount = 0
    for i = 0 to array.size(camBarEvents) - 1
        CamEvent e = array.get(camBarEvents, i)
        bool include = e.kind == "ACTIVE" ? alertActivation : e.kind == "ARMED" ? alertArmed : alertEnding and f_camTerminal(e.kind)
        if include
            selectedEvents += (selectedCount > 0 ? "," : "") + f_camEventJson(e)
            selectedCount += 1
    if selectedCount > 0
        string envelope = "{\"schema\":\"camarilla-desk.v2\",\"product_version\":" + f_camJsonString(CAM_VERSION) + ",\"rule_version\":\"lite2-prerequisites.original2.1-execution\",\"formula\":" + f_camJsonString(CAM_FORMULA) + ",\"settings_id\":" + f_camJsonString(f_camSettingsId()) + ",\"source_mode\":" + f_camJsonString(sourceMode) + ",\"decision_mode\":" + f_camJsonString(decisionMode) + ",\"symbol\":" + f_camJsonString(syminfo.tickerid) + ",\"timeframe\":" + f_camJsonString(timeframe.period) + ",\"confirmed_time\":" + f_camJsonTime(time_close) + ",\"fill_confirmed\":false,\"events\":[" + selectedEvents + "]}"
        alert(envelope, alert.freq_once_per_bar_close)



// VIEW CONTRACT v2: tracked immutable plans are independent from manual inspection.
// Chart-first Ribbon and complete seven-level map. Clean adds plan detail.
// Display inputs do not mutate candidates, episodes, alerts or reference prices.
string vBoardMode = input.string("Ribbon", "Trade HUD", options = ["Ribbon", "Clean", "Context", "Audit", "Off"], group = "Display", tooltip = "Ribbon: compact source, context and current action. Clean: expanded tracked plan. Context: up to five attention rows. Audit: all fourteen plays. Attention priority is not a strength score.", display = display.none)
string vDetail = input.string("Off", "Inspector", options = ["Off", "Brief", "Standard"], group = "Display", tooltip = "Inspect any play without replacing the independently tracked chart plan. Standard adds prerequisites and source evidence.", display = display.none)
string vTheme = input.string("Trading Dark", "Theme", options = ["Trading Dark", "Accessible Dark", "Accessible Light", "Aurora", "Royal Gold"], group = "Display", display = display.none)
string vChartMode = input.string("Map", "Chart density", options = ["Map", "Clean", "Context", "Audit", "Off"], group = "Display", tooltip = "Map: complete seven-level map plus fixed plan and episode markers. Clean: selected plan/trigger only. Context: map and optional middle band. Audit: adds bounded event observations. Off hides drawings only.", display = display.none)
string vScale = input.string("Medium", "Panel & chart tag size", options = ["Small", "Medium", "Large"], group = "Display", active = CAM_IS_LAB or vBoardMode != "Off" or vDetail != "Off" or vChartMode != "Off", tooltip = "Also controls Strategy Lab readout size.", display = display.none)
string vFocusCode = input.string("Auto", "Inspect play", options = ["Auto", "HA", "LA", "HB", "LB", "HC", "LC", "HD", "LD", "HE", "LE", "HF", "LF", "HG", "LG"], group = "Display", active = vDetail != "Off", tooltip = "Auto: active, armed, approaching, then candidate, with fixed-code ties. A manual code changes only the Inspector. H/L describes context, not trade direction.", display = display.none)
bool vCodes = input.bool(false, "Show play codes", group = "Display", display = display.none)
string vBoardCorner = input.string("Top Left", "Trade HUD position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = "Display · placement", active = vBoardMode != "Off", display = display.none)
string vFocusCorner = input.string("Bottom Left", "Inspector position", options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group = "Display · placement", active = vDetail != "Off", tooltip = "Audit reserves its entire column, so the Inspector moves to the other side. Other HUD modes use the free corner on the same side first. Hidden panels reserve no anchor.", display = display.none)
int vHeaderClearance = input.int(12, "Top Left header clearance · %", minval = 0, maxval = 20, group = "Display · placement", tooltip = "Transparent space beneath the native legend; zero if hidden. Bottom Left has a transparent footer above the watermark. These are layout heuristics.", display = display.none)
bool vPriceTags = input.bool(true, "Price tags", group = "Display · chart", active = vChartMode != "Off", tooltip = "Displaced tags keep true-price leaders and unchanged values. Tracked references receive first priority.", display = display.none)
bool vMiddleBand = input.bool(false, "Camarilla middle band", group = "Display · chart", active = vChartMode == "Context" or vChartMode == "Audit", tooltip = "L2–H2 Camarilla band, not floor-pivot CPR.", display = display.none)
bool vPlanZones = input.bool(true, "Reference risk / reward zones", group = "Display · chart", active = vChartMode != "Off", tooltip = "Frozen reference geometry, not fills or returns. Ended geometry stops at its recorded end time.", display = display.none)
int vHistory = input.int(2, "Ended plans shown", minval = 0, maxval = 8, group = "Display · chart", active = vChartMode != "Off", tooltip = "Newest ended snapshots within visible time and nearby price context. Historical prices are frozen. Zero hides ended geometry.", display = display.none)
int vTagCap = input.int(12, "Chart tag budget", minval = 3, maxval = 24, group = "Display · chart", active = vChartMode != "Off" and vPriceTags, tooltip = "Tracked entry, stop and target come first. Default covers the seven map tags plus tracked references. Episode markers use a separate bounded pool.", display = display.none)
string vTagSide = input.string("Right", "Price tag side", options = ["Right", "Left"], group = "Display · chart", active = vChartMode != "Off" and vPriceTags, tooltip = "Right aligns solid price tags beside the current chart. Left restores interior labels. Colliding labels retain their printed prices and true-price leaders.", display = display.none)
bool vEpisodeMarks = input.bool(true, "Entry / ending markers", group = "Display · chart", active = vChartMode != "Off", tooltip = "Local arrows mark confirmed reference entries. Ending labels show TARGET, STOP or EXPIRED at the recorded observation, not broker fills. A ? suffix discloses ambiguous OHLC paths.", display = display.none)
int vZoneOpacity = input.int(14, "Plan zone opacity · %", minval = 4, maxval = 35, group = "Display · chart", active = vChartMode != "Off" and vPlanZones, tooltip = "Visual contrast only. It does not change price references, signal logic or results.", display = display.none)

bool vLight = vTheme == "Accessible Light"
color vBg = vLight ? color.rgb(246, 248, 252) : vTheme == "Royal Gold" ? color.rgb(21, 23, 28) : color.rgb(16, 24, 38)
color vStripe = vLight ? color.rgb(233, 239, 247) : vTheme == "Royal Gold" ? color.rgb(32, 35, 43) : color.rgb(23, 35, 52)
color vHeader = vLight ? color.rgb(220, 230, 242) : vTheme == "Royal Gold" ? color.rgb(46, 43, 36) : color.rgb(30, 47, 68)
color vText = vLight ? color.rgb(24, 36, 53) : color.rgb(236, 243, 251)
color vMuted = vLight ? color.rgb(82, 98, 118) : color.rgb(168, 184, 204)
color vAccent = vTheme == "Trading Dark" ? color.rgb(105, 164, 255) : vTheme == "Royal Gold" ? color.rgb(226, 197, 137) : vTheme == "Aurora" ? color.rgb(190, 187, 237) : vLight ? color.rgb(39, 68, 100) : color.rgb(205, 225, 246)
color vUp = vTheme == "Trading Dark" ? color.rgb(66, 190, 133) : vLight ? color.rgb(0, 105, 165) : vTheme == "Aurora" ? color.rgb(95, 207, 239) : color.rgb(92, 184, 239)
color vDown = vTheme == "Trading Dark" ? color.rgb(240, 101, 119) : vLight ? color.rgb(151, 85, 0) : color.rgb(237, 171, 76)
color vFrame = vLight ? color.rgb(184, 200, 221) : color.rgb(53, 71, 93)
int vBodySize = vScale == "Small" ? 12 : vScale == "Large" ? 16 : 14
int vSmallSize = vScale == "Small" ? 10 : vScale == "Large" ? 14 : 12
int vTitleSize = vScale == "Small" ? 14 : vScale == "Large" ? 18 : 16
int vTagSize = vScale == "Small" ? 10 : vScale == "Large" ? 14 : 12

f_vWrap(string value, int width, int maxLines) =>
    string result = ""
    string current = ""
    int rows = 1
    bool truncated = false
    for word in str.split(value, " ")
        if not truncated
            if str.length(current) + str.length(word) + 1 > width and str.length(current) > 0
                if rows < maxLines
                    result += current + "\n"
                    current := word
                    rows += 1
                else
                    truncated := true
            else
                current += (str.length(current) > 0 ? " " : "") + word
    result + current + (truncated ? "…" : "")
f_vPosition(string choice) =>
    switch choice
        "Top Left" => position.top_left
        "Bottom Left" => position.bottom_left
        "Bottom Right" => position.bottom_right
        "Top Center" => position.top_center
        "Bottom Center" => position.bottom_center
        "Middle Left" => position.middle_left
        "Middle Right" => position.middle_right
        "Middle Center" => position.middle_center
        => position.top_right
// Public view API for the Lab tail: occupied anchors include the entire column
// of a tall Audit HUD, so downstream panels cannot overlap its fourteen rows.
f_vFree(array<string> used, string preferred) =>
    string result = preferred
    if array.includes(used, result)
        string paired = preferred == "Top Left" ? "Bottom Left" : preferred == "Bottom Left" ? "Top Left" : preferred == "Top Right" ? "Bottom Right" : "Top Right"
        string across = preferred == "Top Left" ? "Top Right" : preferred == "Bottom Left" ? "Bottom Right" : preferred == "Top Right" ? "Top Left" : "Bottom Left"
        array<string> order = array.from(paired, across, "Top Left", "Bottom Left", "Top Right", "Bottom Right", "Middle Left", "Middle Right", "Top Center", "Bottom Center", "Middle Center")
        for candidate in order
            if not array.includes(used, candidate)
                result := candidate
                break
    array.push(used, result)
    result
f_vReserveColumn(array<string> used, string anchor) =>
    bool left = str.contains(anchor, "Left")
    array<string> column = left ? array.from("Top Left", "Bottom Left", "Middle Left") : array.from("Top Right", "Bottom Right", "Middle Right")
    for reserved in column
        if not array.includes(used, reserved)
            array.push(used, reserved)
    true
var array<string> vUsedAnchors = array.new<string>()
var string vBoardAnchor = ""
var string vFocusAnchor = ""
if barstate.isfirst
    if vBoardMode != "Off"
        vBoardAnchor := f_vFree(vUsedAnchors, vBoardCorner)
        if vBoardMode == "Audit"
            f_vReserveColumn(vUsedAnchors, vBoardAnchor)
    if vDetail != "Off"
        vFocusAnchor := f_vFree(vUsedAnchors, vFocusCorner)
f_vTime(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "MM-dd HH:mm", "UTC")
f_vDate(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "yyyy-MM-dd", "UTC")
f_vClock(int stamp) =>
    na(stamp) ? "—" : str.format_time(stamp, "HH:mm", "UTC")
f_vSourceShort(string value) =>
    str.contains(value, "SYNTHETIC") ? "TEST RTH" : str.contains(value, "RTH") ? "RTH" : str.contains(value, "ETH") ? "ETH" : str.contains(value, "daily") ? "Daily" : str.contains(value, "Selected") ? "Custom" : "Waiting"
f_vClockShort(string value) =>
    str.contains(value, "SYNTHETIC") ? "TEST" : str.contains(value, "Exchange") ? "Exchange day" : str.contains(value, "cash") ? "US cash" : "Custom"
f_vInk(color tint) =>
    color.r(tint) * 0.2126 + color.g(tint) * 0.7152 + color.b(tint) * 0.0722 > 105 ? color.rgb(10, 18, 28) : color.white
f_vDirection(int direction) =>
    direction == 1 ? "LONG" : direction == -1 ? "SHORT" : "—"
f_vState(Play play, bool inWindow) =>
    play.state == "WATCH" ? not inWindow ? "WAIT SESSION" : not camBarOhlcValid ? "DATA WAIT" : not play.candidate ? "BLOCKED" : play.approaching ? "APPROACHING" : "CANDIDATE" : play.state
f_vStateColor(string state, int direction) =>
    state == "ACTIVE" ? (direction == 1 ? vUp : vDown) : state == "ARMED" or state == "APPROACHING" ? vAccent : state == "STOP" or state == "INVALIDATED" or state == "REJECTED" ? vDown : state == "TARGET" ? vUp : vMuted
f_vShortName(string family, int direction) =>
    switch family
        "A" => direction == 1 ? "L3 traverse" : "H3 traverse"
        "B" => direction == 1 ? "H4 break & retest" : "L4 break & retest"
        "C" => "Opening continuation"
        "D" => "Context-opposed continuation"
        "E" => direction == 1 ? "L4 failure" : "H4 failure"
        "F" => "Outer-tier rejection"
        "G" => "Opposite-side rejection"
        => "Session guide"
f_vName(Play play) =>
    (vCodes ? play.code + " · " : "") + f_vShortName(play.family, play.dir)
f_vSnapshotName(CamPlanSnapshot plan) =>
    (vCodes ? plan.code + " · " : "") + f_vShortName(str.substring(plan.code, 1), plan.dir)
f_vAge(Play play) =>
    int start = play.state == "ACTIVE" ? play.activationBar : play.state == "ARMED" ? play.setupBar : na
    na(start) or na(camClosedBar) ? "" : str.tostring(math.max(0, camClosedBar - start)) + " bars"
f_vTrigger(Play play) =>
    na(play.triggerPrice) ? "No close trigger yet" : "close " + play.triggerOperator + " " + f_camReadable(play.triggerPrice)
f_vPlayTip(Play play) =>
    play.code + " · " + play.name + "\n" + f_vDirection(play.dir) + " · " + f_vState(play, camInSession) + "\nWHY: " + play.reason + "\nNEXT: " + play.next + "\nFAIL: " + play.invalidation + "\nClose trigger: " + f_vTrigger(play) + "\nSetup / activation UTC: " + f_vTime(play.setupTime) + " / " + f_vTime(play.activationTime)
f_vGateWords(Play play) =>
    "Range " + (play.rangeOk ? "PASS" : "BLOCK") + " · Width " + (play.widthOk ? "PASS" : "BLOCK") + "\nOpening " + (play.openOk ? "PASS" : "BLOCK") + " · Region " + (play.regionOk ? "IN" : "OUT") + "\nSMA " + (not smaGate ? "OFF" : play.smaOk ? "PASS" : "BLOCK")
f_vTrackedIndex() =>
    int found = -1
    if array.size(camPlans) > 0
        for i = 0 to array.size(camPlans) - 1
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state == "ACTIVE"
                found := i
                break
    found
f_vFallbackFocus() =>
    int selected = f_camAutoFocus()
    if selected < 0 and camInSession and array.size(plays) > 0
        for priority = 0 to 2
            if selected < 0
                for i = 0 to array.size(plays) - 1
                    Play play = array.get(plays, i)
                    bool match = priority == 0 ? play.state == "ARMED" : priority == 1 ? play.state == "WATCH" and play.approaching : play.state == "WATCH" and play.candidate
                    if match
                        selected := i
                        break
    selected
f_vRows(bool audit) =>
    array<int> rows = array.new<int>()
    if array.size(plays) > 0
        if audit
            for i = 0 to array.size(plays) - 1
                array.push(rows, i)
        else
            for priority = 0 to 3
                for i = 0 to array.size(plays) - 1
                    Play play = array.get(plays, i)
                    bool match = priority == 0 ? play.state == "ACTIVE" : priority == 1 ? play.state == "ARMED" : priority == 2 ? play.state == "WATCH" and play.approaching : play.state == "WATCH" and play.candidate
                    if match and array.size(rows) < 5 and not array.includes(rows, i)
                        array.push(rows, i)
    rows
f_vRecovery() =>
    not camChartSupported ? "Use standard, time-based candles from 1 to 60 minutes." : not camOpenKnown ? "Load the observed decision opening and complete prior source windows." : not camMapReady ? camDataReason : not camInSession ? "Wait for the selected decision window. The last committed map remains inspectable." : "No qualifying setup. Inspect Audit for each prerequisite."
f_vSourceTip() =>
    "PUBLIC CONTEXT\nSource: " + camSourceLabel + " · close: " + camCloseLabel + "\n" + camSourceSelectionReason + "\nDecision: " + camDecisionLabel + "\nMap source UTC: " + f_vTime(camMap.sourceStart) + " → " + f_vTime(camMap.sourceEnd) + "\nSelection cutoff: " + f_vTime(camSourceCutoff) + "\nObserved 1m opening: " + f_vTime(camOpeningSourceTime) + " @ " + f_camExact(camOpen) + "\nH/L/C: " + f_camExact(camMap.h) + " / " + f_camExact(camMap.l) + " / " + f_camExact(camMap.c) + "\n" + camDataReason
f_vRiskTip() =>
    "ORIGINAL EXECUTION\nMinimum R: " + str.tostring(minRewardR) + " · buffer " + str.tostring(stopBufferTicks) + " ticks\nStop: " + stopModel + " · ATR " + str.tostring(stopAtrMultiple) + "\nTarget: " + targetModel + " · ATR " + str.tostring(targetAtrMultiple) + "\nMax target ATR: " + (maxTargetAtr == 0 ? "OFF" : str.tostring(maxTargetAtr)) + "\nSetup / active limits: " + str.tostring(setupDeadline) + " / " + str.tostring(activeDeadline) + " bars\nRearm: " + (allowRearm ? "ON · cooldown " + str.tostring(cooldownBars) + " · cap " + str.tostring(maxEpisodes) : "OFF") + "\nChart SMA: " + (smaGate ? str.tostring(smaLength) : "OFF") + "\nReference plans, not broker fills or a validated edge."

type CamViewRow
    string key = ""
    string value = ""
    string tip = ""
    int kind = 0
f_vAdd(array<CamViewRow> rows, string key, string value, string tip, int kind = 0) =>
    array.push(rows, CamViewRow.new(key, value, tip, kind))
f_vSignature(array<CamViewRow> rows) =>
    string result = ""
    for row in rows
        result += str.tostring(row.kind)
    result
f_vInsetStart(array<CamViewRow> rows, string anchor) =>
    if anchor == "Top Left" and vHeaderClearance > 0
        f_vAdd(rows, "", "", "Transparent chart-header clearance", 5)
f_vInsetEnd(array<CamViewRow> rows, string anchor) =>
    if anchor == "Bottom Left"
        f_vAdd(rows, "", "", "Transparent watermark clearance", 5)
f_vRender(table panel, array<CamViewRow> rows, string anchor) =>
    table.clear(panel, 0, 0, 1, array.size(rows) - 1)
    for i = 0 to array.size(rows) - 1
        CamViewRow row = array.get(rows, i)
        color background = row.kind == 5 ? color.new(vBg, 100) : row.kind == 2 ? vHeader : row.kind == 4 ? vStripe : vBg
        if row.kind == 1
            table.cell(panel, 0, i, row.key, text_color = vMuted, bgcolor = background, text_size = vSmallSize, text_halign = text.align_left, text_font_family = font.family_default, tooltip = row.tip)
            table.cell(panel, 1, i, row.value, text_color = vText, bgcolor = background, text_size = vBodySize, text_halign = text.align_right, text_font_family = font.family_monospace, text_formatting = text.format_bold, tooltip = row.tip)
        else
            // clear() may remove merges: reapply before every full-row write.
            table.merge_cells(panel, 0, i, 1, i)
            table.cell(panel, 0, i, row.value, height = row.kind == 5 ? (anchor == "Top Left" ? (vBoardMode == "Audit" ? math.min(vHeaderClearance, 8) : vHeaderClearance) : 5) : 0, text_color = row.kind == 3 ? vMuted : row.kind == 2 or row.kind == 4 ? vAccent : vText, bgcolor = background, text_size = row.kind == 2 ? vTitleSize : row.kind == 3 ? vSmallSize : vBodySize, text_halign = text.align_left, text_font_family = font.family_default, text_formatting = row.kind == 2 or row.kind == 4 ? text.format_bold : text.format_none, tooltip = row.tip)

// Setter pools survive ordinary price/text changes; surplus handles are deleted.
f_vLine(array<line> pool, int slot, int x1, float y1, int x2, float y2, color tint, string style, int width = 1) =>
    line item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        line.set_xy1(item, x1, y1)
        line.set_xy2(item, x2, y2)
        line.set_color(item, tint)
        line.set_style(item, style)
        line.set_width(item, width)
    else
        item := line.new(x1, y1, x2, y2, xloc = xloc.bar_time, color = tint, style = style, width = width)
        array.push(pool, item)
    item
f_vLabel(array<label> pool, int slot, int stamp, float price, string value, color tint, string tip, bool faceLeft = false, bool solid = false) =>
    label item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        label.set_xy(item, stamp, price)
        label.set_text(item, value)
        label.set_color(item, solid ? tint : vBg)
        label.set_textcolor(item, solid ? f_vInk(tint) : tint)
        label.set_size(item, vTagSize)
        label.set_style(item, faceLeft ? label.style_label_right : label.style_label_left)
        label.set_tooltip(item, tip)
    else
        item := label.new(stamp, price, value, xloc = xloc.bar_time, style = faceLeft ? label.style_label_right : label.style_label_left, color = solid ? tint : vBg, textcolor = solid ? f_vInk(tint) : tint, size = vTagSize, text_font_family = font.family_default, tooltip = tip)
        array.push(pool, item)
    item
f_vMarker(array<label> pool, int slot, int stamp, float price, string value, color tint, string tip, string style) =>
    label item = f_vLabel(pool, slot, stamp, price, value, tint, tip, false, true)
    label.set_style(item, style)
    item
f_vBox(array<box> pool, int slot, int left, float top, int right, float bottom, color tint) =>
    box item = na
    if slot < array.size(pool)
        item := array.get(pool, slot)
        box.set_lefttop(item, left, top)
        box.set_rightbottom(item, right, bottom)
        box.set_bgcolor(item, tint)
        box.set_border_color(item, color.new(tint, 100))
    else
        item := box.new(left, top, right, bottom, xloc = xloc.bar_time, bgcolor = tint, border_color = color.new(tint, 100))
        array.push(pool, item)
    item
f_vTagY(array<float> used, float price, float gap) =>
    float result = price
    for attempt = 0 to 24
        float candidate = attempt == 0 ? price : price + (attempt % 2 == 1 ? 1 : -1) * math.ceil(attempt / 2.0) * gap
        bool clear = true
        for previous in used
            if math.abs(candidate - previous) < gap * 0.95
                clear := false
        if clear
            result := candidate
            break
    array.push(used, result)
    result
f_vExtent(array<float> bounds, float price) =>
    if not na(price)
        array.set(bounds, 0, math.min(array.get(bounds, 0), price))
        array.set(bounds, 1, math.max(array.get(bounds, 1), price))
    true
f_vTagGap(float renderedRange, float tick, int fontPixels) =>
    // Bounded display heuristic: map/history lines also expand autoscale. Pine
    // exposes no pane pixel height, so this is not a universal collision proof.
    float fraction = math.min(0.07, math.max(0.035, fontPixels * 0.004))
    math.max(renderedRange * fraction, tick * 4)
float vFallbackRange = ta.highest(high[1], 80) - ta.lowest(low[1], 80)
var float vVisibleHigh = na
var float vVisibleLow = na
if time == chart.left_visible_bar_time
    vVisibleHigh := high
    vVisibleLow := low
else if time > chart.left_visible_bar_time and time <= chart.right_visible_bar_time
    vVisibleHigh := math.max(nz(vVisibleHigh, high), high)
    vVisibleLow := math.min(nz(vVisibleLow, low), low)
var array<line> vLines = array.new<line>()
var array<label> vLabels = array.new<label>()
var array<label> vMarkers = array.new<label>()
var array<box> vBoxes = array.new<box>()
var table vBoard = na
var table vFocus = na
var string vBoardSignature = ""
var string vFocusSignature = ""

// VIEW RENDER — exact fixture insertion boundary; no production markers precede it.
if (barstate.islastconfirmedhistory or barstate.isrealtime) and (vBoardMode != "Off" or vDetail != "Off" or vChartMode != "Off")
    int vTrackIndex = f_vTrackedIndex()
    bool vTracking = vTrackIndex >= 0
    CamPlanSnapshot vTracked = vTracking ? array.get(camPlans, vTrackIndex) : CamPlanSnapshot.new()
    int vNextIndex = f_vFallbackFocus()
    Play vUpcoming = vNextIndex >= 0 ? array.get(plays, vNextIndex) : Play.new()
    int vInspectIndex = vFocusCode == "Auto" ? vNextIndex : f_camCodeIndex(vFocusCode)
    bool vHasUpcoming = vNextIndex >= 0 and not vTracking
    string vSourceTip = f_vSourceTip()
    string vRiskTip = f_vRiskTip()
    string vClosedTip = "Last committed chart close, UTC: " + f_vTime(camClosedTime) + "\nPrice: " + f_camExact(camClosedPrice) + "\nForming quotes do not update this value."
    int vBarMs = int(timeframe.in_seconds() * 1000)
    int vNow = na(camClosedTime) ? time : camClosedTime
    int vRight = vNow + vBarMs * 4
    int vLeft = nz(chart.left_visible_bar_time, vNow - vBarMs * 60)
    int vVisibleRight = math.min(vNow, nz(chart.right_visible_bar_time, vNow))
    bool vLeftPanel = array.includes(vUsedAnchors, "Top Left") or array.includes(vUsedAnchors, "Bottom Left") or array.includes(vUsedAnchors, "Middle Left")
    int vTagTime = vTagSide == "Right" ? vRight : vLeft + int(math.max(0, vVisibleRight - vLeft) * (vLeftPanel ? 0.45 : 0.12))
    float vRange = math.max(nz(vVisibleHigh - vVisibleLow, nz(vFallbackRange, syminfo.mintick * 100)), syminfo.mintick * 20)
    int vLineCount = 0
    int vLabelCount = 0
    int vMarkerCount = 0
    int vBoxCount = 0
    int vHistoryShown = 0
    int vHistoryOmitted = 0
    int vContextOmitted = 0
    array<float> vOccupied = array.new<float>()
    bool vCanMap = camChartSupported and camMapReady and camMap.valid and not na(camSessionStart) and vChartMode != "Off"
    float vFocusLevel = vTracking ? vTracked.triggerPrice : vHasUpcoming ? vUpcoming.triggerPrice : na
    int vRoleDirection = vTracking ? vTracked.dir : vHasUpcoming ? vUpcoming.dir : 0
    // Admit before drawing. Candle range still controls the existing history
    // proximity filter; the wider rendered extent controls label spacing only.
    array<float> vExtent = array.from(nz(vVisibleLow, nz(camClosedPrice, close) - vRange * 0.5), nz(vVisibleHigh, nz(camClosedPrice, close) + vRange * 0.5))
    array<float> vMapLevels = array.from(camMap.l6, camMap.l4, camMap.l3, camMap.c, camMap.h3, camMap.h4, camMap.h6)
    array<bool> vMapVisible = array.new_bool(7, false)
    array<int> vHistoryIndices = array.new_int()
    array<int> vAuditIndices = array.new_int()
    int vReservedTags = 0
    if vChartMode != "Off" and vTracking
        for price in array.from(vTracked.entry, vTracked.stop, vTracked.target)
            f_vExtent(vExtent, price)
        vReservedTags := vPriceTags ? 3 : 0
    if vCanMap
        for pass = 0 to 1
            for j = 0 to 6
                float price = array.get(vMapLevels, j)
                bool focused = not na(vFocusLevel) and f_camCompare(price, vFocusLevel) == 0
                bool visible = not na(price) and (vChartMode != "Clean" or focused or (not vTracking and not vHasUpcoming and j == 3))
                if visible and ((pass == 0 and focused) or (pass == 1 and not focused))
                    array.set(vMapVisible, j, true)
                    f_vExtent(vExtent, price)
                    if vPriceTags and vReservedTags < vTagCap
                        vReservedTags += 1
        if vMiddleBand and (vChartMode == "Context" or vChartMode == "Audit")
            f_vExtent(vExtent, camMap.l2)
            f_vExtent(vExtent, camMap.h2)
    if vChartMode != "Off" and array.size(camPlans) > 0
        for i = 0 to array.size(camPlans) - 1
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state != "ACTIVE" and not na(plan.endTime) and not na(plan.activationTime)
                bool timeVisible = plan.endTime >= vLeft and plan.activationTime <= vVisibleRight
                bool priceNear = math.max(plan.entry, math.max(plan.stop, plan.target)) <= nz(vVisibleHigh, plan.entry) + vRange and math.min(plan.entry, math.min(plan.stop, plan.target)) >= nz(vVisibleLow, plan.entry) - vRange
                if timeVisible and priceNear and array.size(vHistoryIndices) < vHistory
                    array.push(vHistoryIndices, i)
                    for price in array.from(plan.entry, plan.stop, plan.target)
                        f_vExtent(vExtent, price)
                    if vEpisodeMarks
                        f_vExtent(vExtent, nz(plan.outcomePrice, plan.entry))
                    if vPriceTags and not vEpisodeMarks and vReservedTags < vTagCap
                        vReservedTags += 1
                else
                    vHistoryOmitted += 1
    if vChartMode == "Audit" and vPriceTags and array.size(camEvents) > 0
        for i = 0 to math.min(3, array.size(camEvents) - 1)
            CamEvent event = array.get(camEvents, i)
            if vReservedTags < vTagCap and event.eventTime >= vLeft and event.eventTime <= vVisibleRight and not na(event.price)
                array.push(vAuditIndices, i)
                f_vExtent(vExtent, event.price)
                vReservedTags += 1
    float vRenderedRange = math.max(array.get(vExtent, 1) - array.get(vExtent, 0), syminfo.mintick * 20)
    float vGap = f_vTagGap(vRenderedRange, syminfo.mintick, vTagSize)
    // Reserve marker text rows before right/left-column price tags. A local
    // ending can share H4/L3 exactly; displace only the tag with its true leader.
    if vChartMode != "Off" and vEpisodeMarks
        if vTracking and vTracked.activationTime >= vLeft
            array.push(vOccupied, vTracked.entry + (vTracked.dir == 1 ? -0.6 : 0.6) * vGap)
        for i in vHistoryIndices
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.activationTime >= vLeft
                array.push(vOccupied, plan.entry + (plan.dir == 1 ? -0.6 : 0.6) * vGap)
            array.push(vOccupied, nz(plan.outcomePrice, plan.state == "STOP" ? plan.stop : plan.state == "TARGET" ? plan.target : plan.entry))
    // Manual vInspectIndex cannot replace this immutable tracked snapshot.
    if vChartMode != "Off" and vTracking
        array<float> prices = array.from(vTracked.entry, vTracked.stop, vTracked.target)
        array<string> names = array.from("PLAN ENTRY REF", "PLAN STOP", "PLAN TARGET")
        array<color> colors = array.from(vAccent, vDown, vUp)
        for j = 0 to 2
            float price = array.get(prices, j)
            color tint = array.get(colors, j)
            f_vLine(vLines, vLineCount, vTracked.activationTime, price, vRight, price, tint, line.style_solid, j == 0 ? 2 : 1)
            vLineCount += 1
            if vTagTime < vTracked.activationTime
                f_vLine(vLines, vLineCount, vTagTime, price, vTracked.activationTime, price, color.new(tint, 45), line.style_dotted)
                vLineCount += 1
            if vPriceTags and vLabelCount < vTagCap
                float tagY = f_vTagY(vOccupied, price, vGap)
                string tip = "Tracked " + vTracked.code + " · episode " + str.tostring(vTracked.episode) + "\n" + array.get(names, j) + " " + f_camExact(price) + "\nFrozen at " + f_vTime(vTracked.activationTime) + " UTC. Dotted left extension is a present reference guide, not historical availability."
                f_vLabel(vLabels, vLabelCount, vTagTime, tagY, array.get(names, j) + " " + f_camPrice(price), tint, tip, vTagSide == "Right", vTagSide == "Right")
                vLabelCount += 1
                if tagY != price
                    f_vLine(vLines, vLineCount, vTagTime, price, vTagTime, tagY, color.new(tint, 40), line.style_dotted)
                    vLineCount += 1
        if vPlanZones
            f_vBox(vBoxes, vBoxCount, vTracked.activationTime, math.max(vTracked.entry, vTracked.stop), vRight, math.min(vTracked.entry, vTracked.stop), color.new(vDown, 100 - vZoneOpacity))
            vBoxCount += 1
            f_vBox(vBoxes, vBoxCount, vTracked.activationTime, math.max(vTracked.entry, vTracked.target), vRight, math.min(vTracked.entry, vTracked.target), color.new(vUp, 100 - vZoneOpacity))
            vBoxCount += 1
        if vEpisodeMarks and vTracked.activationTime >= vLeft and vTracked.activationTime <= vVisibleRight
            string tip = "REFERENCE ENTRY · " + vTracked.code + " · episode " + str.tostring(vTracked.episode) + "\nConfirmed " + f_vTime(vTracked.activationTime) + " UTC @ " + f_camExact(vTracked.entry) + "\nFrozen stop / target " + f_camExact(vTracked.stop) + " / " + f_camExact(vTracked.target) + "\n" + vTracked.reason + "\nNot a broker fill or a fresh entry at the latest quote."
            f_vMarker(vMarkers, vMarkerCount, vTracked.activationTime, vTracked.entry, vTracked.code + (vTracked.dir == 1 ? " ▲ ENTRY" : " ▼ ENTRY"), vTracked.dir == 1 ? vUp : vDown, tip, vTracked.dir == 1 ? label.style_label_up : label.style_label_down)
            vMarkerCount += 1
    // Role color belongs to the relevant play, never the current quote's position.
    if vCanMap
        array<string> codes = array.from("L6", "L4", "L3", "PC", "H3", "H4", "H6")
        for pass = 0 to 1
            for j = 0 to 6
                float price = array.get(vMapLevels, j)
                bool focused = not na(vFocusLevel) and f_camCompare(price, vFocusLevel) == 0
                bool visible = array.get(vMapVisible, j)
                if visible and ((pass == 0 and focused) or (pass == 1 and not focused))
                    color tint = focused and vRoleDirection != 0 ? (vRoleDirection == 1 ? vUp : vDown) : j < 3 ? vUp : j > 3 ? vDown : vAccent
                    f_vLine(vLines, vLineCount, camSessionStart, price, vRight, price, color.new(tint, focused ? 0 : vChartMode == "Map" ? 20 : 50), vChartMode == "Map" or focused ? line.style_solid : line.style_dotted, focused or j == 2 or j == 4 ? 2 : 1)
                    vLineCount += 1
                    if vPriceTags
                        if vLabelCount < vTagCap
                            float tagY = f_vTagY(vOccupied, price, vGap)
                            string tip = "CURRENT MAP · " + array.get(codes, j) + " " + f_camExact(price) + "\nMap usable at decision opening " + f_vTime(camSessionStart) + " UTC. A dotted left guide is a current reference only.\n" + vSourceTip
                            f_vLabel(vLabels, vLabelCount, vTagTime, tagY, (focused ? "TRIGGER " : "") + array.get(codes, j) + " " + f_camPrice(price), tint, tip, vTagSide == "Right", vTagSide == "Right")
                            vLabelCount += 1
                            if vTagTime < camSessionStart
                                f_vLine(vLines, vLineCount, vTagTime, price, camSessionStart, price, color.new(tint, 70), line.style_dotted)
                                vLineCount += 1
                            if tagY != price
                                f_vLine(vLines, vLineCount, vTagTime, price, vTagTime, tagY, color.new(tint, 45), line.style_dotted)
                                vLineCount += 1
                        else
                            vContextOmitted += 1
        if vMiddleBand and (vChartMode == "Context" or vChartMode == "Audit")
            f_vBox(vBoxes, vBoxCount, camSessionStart, camMap.h2, vRight, camMap.l2, color.new(vAccent, 96))
            vBoxCount += 1
    // Ended geometry uses its own snapshot and stops exactly at its endTime.
    if array.size(vHistoryIndices) > 0
        for i in vHistoryIndices
            CamPlanSnapshot plan = array.get(camPlans, i)
            if plan.state != "ACTIVE"
                if vHistoryShown < vHistory
                    vHistoryShown += 1
                    array<float> prices = array.from(plan.entry, plan.stop, plan.target)
                    for price in prices
                        color endedTint = price == plan.stop ? vDown : price == plan.target ? vUp : vAccent
                        f_vLine(vLines, vLineCount, plan.activationTime, price, plan.endTime, price, color.new(endedTint, 35), line.style_dashed)
                        vLineCount += 1
                    if vPlanZones
                        f_vBox(vBoxes, vBoxCount, plan.activationTime, math.max(plan.entry, plan.stop), plan.endTime, math.min(plan.entry, plan.stop), color.new(vDown, 100 - math.max(4, vZoneOpacity - 4)))
                        vBoxCount += 1
                        f_vBox(vBoxes, vBoxCount, plan.activationTime, math.max(plan.entry, plan.target), plan.endTime, math.min(plan.entry, plan.target), color.new(vUp, 100 - math.max(4, vZoneOpacity - 4)))
                        vBoxCount += 1
                    if vEpisodeMarks
                        string markerTip = "REFERENCE EPISODE · " + plan.code + " #" + str.tostring(plan.episode) + "\n" + f_vTime(plan.activationTime) + " → " + f_vTime(plan.endTime) + " UTC\nFrozen entry / stop / target " + f_camExact(plan.entry) + " / " + f_camExact(plan.stop) + " / " + f_camExact(plan.target) + "\n" + plan.reason + (plan.ambiguous ? "\nAmbiguous OHLC path; stop-first reference convention." : "") + "\nThese are observations, not broker fills."
                        if plan.activationTime >= vLeft
                            f_vMarker(vMarkers, vMarkerCount, plan.activationTime, plan.entry, plan.code + (plan.dir == 1 ? " ▲ ENTRY" : " ▼ ENTRY"), plan.dir == 1 ? vUp : vDown, markerTip, plan.dir == 1 ? label.style_label_up : label.style_label_down)
                            vMarkerCount += 1
                        float markerPrice = nz(plan.outcomePrice, plan.state == "STOP" ? plan.stop : plan.state == "TARGET" ? plan.target : plan.entry)
                        string ending = plan.code + (plan.state == "TARGET" ? " ✓ " : plan.state == "STOP" ? " × " : " · ") + plan.state + (plan.ambiguous ? " ?" : "")
                        string endingTip = na(plan.outcomePrice) ? "Unpriced ending; marker anchored at frozen reference " + f_camExact(markerPrice) + " for orientation. No exit price or fill inferred." : "Observed end reference " + f_camExact(markerPrice)
                        f_vMarker(vMarkers, vMarkerCount, plan.endTime, markerPrice, ending, plan.state == "TARGET" ? vUp : plan.state == "STOP" ? vDown : vMuted, markerTip + "\n" + endingTip, label.style_label_left)
                        vMarkerCount += 1
                    if vPriceTags and not vEpisodeMarks and vLabelCount < vTagCap
                        float tagY = f_vTagY(vOccupied, plan.entry, vGap)
                        string tip = "ENDED " + plan.code + " · episode " + str.tostring(plan.episode) + "\n" + f_vTime(plan.activationTime) + " → " + f_vTime(plan.endTime) + " UTC\nFrozen entry / stop / target: " + f_camExact(plan.entry) + " / " + f_camExact(plan.stop) + " / " + f_camExact(plan.target) + "\n" + plan.reason + (plan.ambiguous ? "\nSame-bar ambiguity: stop-first reference policy." : "")
                        f_vLabel(vLabels, vLabelCount, plan.endTime, tagY, "ENDED " + f_vDirection(plan.dir) + " · " + plan.state, vMuted, tip, true)
                        vLabelCount += 1
                        if tagY != plan.entry
                            f_vLine(vLines, vLineCount, plan.endTime, plan.entry, plan.endTime, tagY, color.new(vMuted, 65), line.style_dotted)
                            vLineCount += 1
    if array.size(vAuditIndices) > 0
        for i in vAuditIndices
            CamEvent event = array.get(camEvents, i)
            if vLabelCount < vTagCap and event.eventTime >= vLeft and event.eventTime <= vVisibleRight and not na(event.price)
                float tagY = f_vTagY(vOccupied, event.price, vGap)
                f_vLabel(vLabels, vLabelCount, event.eventTime, tagY, event.code + " " + event.kind, vMuted, event.reason + "\nObserved " + f_vTime(event.eventTime) + " UTC · not a broker fill", true)
                vLabelCount += 1
                if tagY != event.price
                    f_vLine(vLines, vLineCount, event.eventTime, event.price, event.eventTime, tagY, color.new(vMuted, 65), line.style_dotted)
                    vLineCount += 1
    while array.size(vLines) > vLineCount
        line.delete(array.pop(vLines))
    while array.size(vLabels) > vLabelCount
        label.delete(array.pop(vLabels))
    while array.size(vMarkers) > vMarkerCount
        label.delete(array.pop(vMarkers))
    while array.size(vBoxes) > vBoxCount
        box.delete(array.pop(vBoxes))

    if vBoardMode != "Off"
        array<CamViewRow> rows = array.new<CamViewRow>()
        f_vInsetStart(rows, vBoardAnchor)
        string vRibbonContext = f_vSourceShort(camSourceLabel) + " · " + (not camMapReady ? "CONTEXT WAIT" : camBias == 1 ? "HIGHER" : camBias == -1 ? "LOWER" : "NEUTRAL") + (camMapReady ? " · " + str.upper(camWidthLabel) : "")
        string vAtrShort = str.contains(camAtrBasis, "RTH") ? "RTH14" : str.contains(camAtrBasis, "Provider") ? "Daily14" : "WAIT"
        string vAtrValue = camAtrReady ? f_camPrice(camAtrReference) : str.tostring(camAtrSamples) + "/14 WAIT"
        string vAtrTip = "ATR basis: " + camAtrBasis + "\nFrozen value " + f_camExact(camAtrReference) + " at " + f_vTime(camAtrStamp) + " UTC\nObserved chain start " + f_vTime(camAtrStart) + " · samples " + str.tostring(camAtrSamples) + "\nRTH counts completed observed sessions; entirely absent dates may be closures or unavailable feed. No provider fallback.\n" + vRiskTip
        if vBoardMode == "Ribbon"
            f_vAdd(rows, "", vRibbonContext, "CAMARILLA DESK " + CAM_VERSION + " · " + syminfo.tickerid + "\n" + vSourceTip, 4)
            string status = vTracking ? vTracked.code + " " + f_vDirection(vTracked.dir) + " · ACTIVE" + (camActiveCount > 1 ? " +" + str.tostring(camActiveCount - 1) : "") : vHasUpcoming ? vUpcoming.code + " " + f_vDirection(vUpcoming.dir) + " · " + f_vState(vUpcoming, camInSession) : not camMapReady ? "DATA WAIT" : not camInSession ? "WAIT SESSION" : "NO QUALIFYING SETUP"
            string statusTip = vTracking ? f_vSnapshotName(vTracked) + "\nConfirmed " + f_vTime(vTracked.activationTime) + " UTC\nEntry / stop / target " + f_camExact(vTracked.entry) + " / " + f_camExact(vTracked.stop) + " / " + f_camExact(vTracked.target) + "\n" + vTracked.reason : vHasUpcoming ? f_vPlayTip(vUpcoming) : f_vRecovery()
            f_vAdd(rows, "", status, statusTip + "\n" + str.tostring(camActiveCount) + " active · " + str.tostring(camApproachingCount) + " approaching · " + str.tostring(camCandidateCount) + " candidates. These counts overlap. Expand Clean/Context/Audit for detail.", 2)
            string next = vTracking ? "NEXT · Stop / target · " + str.tostring(math.max(0, activeDeadline - (camClosedBar - vTracked.activationBar))) + "b max" : vHasUpcoming ? vUpcoming.state == "ARMED" ? "NEXT · " + f_vTrigger(vUpcoming) : vUpcoming.approaching ? "NEXT · Wait for setup" : "NEXT · Wait for approach" : not camMapReady ? "NEXT · Check source / ATR readiness" : not camInSession ? "NEXT · Wait for " + f_vClockShort(camDecisionLabel) : "NEXT · Inspect Audit for prerequisites"
            f_vAdd(rows, "", next, statusTip + "\n" + (vTracking ? "Session end can precede the displayed bar deadline. References are not broker fills." : vHasUpcoming ? vUpcoming.next : f_vRecovery()), 3)
            f_vAdd(rows, "", "ATR " + vAtrShort + " " + vAtrValue + " · CLOSED " + f_vClock(camClosedTime), vAtrTip + "\n" + vClosedTip, 3)
        else
            f_vAdd(rows, "", "CAMARILLA DESK " + CAM_VERSION + " · " + syminfo.ticker, syminfo.tickerid + " · " + timeframe.period + "\n" + vSourceTip, 3)
            if vTracking
                f_vAdd(rows, "", f_vWrap(f_vDirection(vTracked.dir) + " · " + f_vSnapshotName(vTracked), 31, 2), vTracked.reason, 2)
                int age = math.max(0, camClosedBar - vTracked.activationBar)
                f_vAdd(rows, "", "ACTIVE · " + str.tostring(age) + " bars · confirmed " + f_vClock(vTracked.activationTime) + " UTC", "Frozen activation " + f_vTime(vTracked.activationTime) + " UTC · episode " + str.tostring(vTracked.episode), 4)
                f_vAdd(rows, "ENTRY REF", f_camPrice(vTracked.entry), "Observed activation close, not a fill.\n" + f_camExact(vTracked.entry), 1)
                f_vAdd(rows, "STOP", f_camPrice(vTracked.stop), "Frozen " + vTracked.stopMode + " stop\n" + f_camExact(vTracked.stop), 1)
                f_vAdd(rows, "TARGET", f_camPrice(vTracked.target) + " · " + str.tostring(vTracked.effectiveR, "0.00") + "R", "Frozen " + vTracked.targetMode + " target\n" + f_camExact(vTracked.target), 1)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
                f_vAdd(rows, "", "NEXT · stop / target / expiry · " + str.tostring(math.max(0, activeDeadline - age)) + " bars left", "Later completed-bar observations resolve the fixed plan. Session expiry can precede the bar deadline.\n" + vRiskTip, 3)
            else if vHasUpcoming
                f_vAdd(rows, "", f_vWrap(f_vDirection(vUpcoming.dir) + " · " + f_vName(vUpcoming), 31, 2), f_vPlayTip(vUpcoming), 2)
                f_vAdd(rows, "", f_vState(vUpcoming, camInSession) + (f_vAge(vUpcoming) == "" ? "" : " · " + f_vAge(vUpcoming)), f_vPlayTip(vUpcoming), 4)
                f_vAdd(rows, "", "CONFIRM · " + f_vTrigger(vUpcoming), "This close also requires the preceding setup sequence.\n" + vUpcoming.next, 0)
                f_vAdd(rows, "", "NEXT · " + f_vWrap(vUpcoming.next, 36, 2), vUpcoming.next, 3)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
            else
                f_vAdd(rows, "", "SESSION GUIDE", "No active plan or eligible setup is selected.", 2)
                f_vAdd(rows, "", not camMapReady ? "DATA WAIT" : not camInSession ? "WAIT SESSION" : "NO QUALIFYING SETUP", camDataReason, 4)
                f_vAdd(rows, "", f_vWrap(f_vRecovery(), 38, 3), f_vRecovery(), 0)
                f_vAdd(rows, "LAST CLOSED", f_camPrice(camClosedPrice) + " · " + f_vClock(camClosedTime), vClosedTip, 1)
                if array.size(camPlans) > 0
                    CamPlanSnapshot last = array.get(camPlans, 0)
                    if last.state != "ACTIVE"
                        f_vAdd(rows, "", f_vWrap("LAST ENDED · " + f_vDirection(last.dir) + " " + f_vSnapshotName(last) + " · " + last.state, 38, 2), last.reason + "\nEnd " + f_vTime(last.endTime) + " UTC; not a new setup.", 3)
            int otherCount = math.max(0, camActiveCount - (vTracking ? 1 : 0))
            if otherCount > 0
                string others = ""
                int shown = 0
                for plan in camPlans
                    if plan.state == "ACTIVE" and (not vTracking or plan.code != vTracked.code or plan.episode != vTracked.episode or plan.sessionStart != vTracked.sessionStart)
                        if shown < 2
                            others += (shown == 0 ? "" : " · ") + f_vDirection(plan.dir) + " " + f_vSnapshotName(plan)
                            shown += 1
                f_vAdd(rows, "", f_vWrap("ALSO ACTIVE · " + others + (otherCount > shown ? " +" + str.tostring(otherCount - shown) : ""), 38, 2), str.tostring(otherCount) + " additional active snapshots. Only the newest active is chart-tracked. Manual inspection cannot replace it.", 3)
            f_vAdd(rows, "", str.tostring(camActiveCount) + " active · " + str.tostring(camApproachingCount) + " approaching · " + str.tostring(camCandidateCount) + (camCandidateCount == 1 ? " candidate" : " candidates"), "Approaching is a candidate whose region prerequisite passes. Counts overlap; they are not probabilities.", 3)
            if vBoardMode == "Context" or vBoardMode == "Audit"
                array<int> attention = f_vRows(vBoardMode == "Audit")
                f_vAdd(rows, "", vBoardMode == "Audit" ? "ALL 14 · PREREQUISITE AUDIT" : "ATTENTION · UP TO FIVE PLAYS", "ACTIVE → ARMED → APPROACHING → CANDIDATE; fixed-code ties. Attention, not strength.", 4)
                for i in attention
                    Play play = array.get(plays, i)
                    string state = f_vState(play, camInSession)
                    string brief = vBoardMode == "Audit" ? play.code + " · " + f_vDirection(play.dir) + " · " + state : f_vName(play) + " · " + f_vDirection(play.dir) + "\n" + state + (play.state == "ARMED" or play.state == "WATCH" and play.candidate ? " · " + f_vTrigger(play) : "")
                    f_vAdd(rows, "", brief, f_vName(play) + "\n" + f_vPlayTip(play) + "\n" + f_vGateWords(play), 3)
            f_vAdd(rows, "", vRibbonContext, vSourceTip, 4)
            f_vAdd(rows, "", "ATR " + vAtrShort + " " + vAtrValue + " · " + f_vClockShort(camDecisionLabel), vAtrTip, 3)
            f_vAdd(rows, "", "MIN " + str.tostring(minRewardR, "0.0") + "R · " + str.tostring(stopBufferTicks) + " tick buffer · " + stopModel + " SL", vRiskTip, 3)
            if vBoardMode == "Audit"
                f_vAdd(rows, "", "CONTEXT " + (camBias == 1 ? "Higher" : camBias == -1 ? "Lower" : "Neutral") + " · WIDTH " + camWidthLabel, "Close delta " + f_camExact(camRangeDelta) + " · neutral threshold " + f_camExact(camRangeThreshold) + "\nGeometric band relation (separate): " + camRelationship + "\nWidth ratio " + f_camExact(camWidthRatio) + "\nSelected ATR " + f_camExact(camAtrReference) + " at " + f_vTime(camAtrStamp) + " UTC", 3)
                f_vAdd(rows, "", "REARM " + (allowRearm ? "ON · " + str.tostring(cooldownBars) + "b · cap " + str.tostring(maxEpisodes) : "OFF") + " · TP " + targetModel, vRiskTip, 3)
            f_vAdd(rows, "", "REFERENCE PLANS · NOT ORDER FILLS", "Drawings: " + str.tostring(vHistoryShown) + " ended shown / " + str.tostring(vHistoryOmitted) + " omitted; " + str.tostring(vContextOmitted) + " context tags omitted.\n" + (CAM_IS_LAB ? "Lab: alert() calls for reference JSON; order-fill alerts for emulated orders. " : "Static activation alert: desired condition + Once Per Bar Close. ") + "Aggregate JSON: Any alert() function call. Recreate saved alerts after changes. Displaying this recipe does not test delivery.", 3)
        f_vInsetEnd(rows, vBoardAnchor)
        string signature = f_vSignature(rows)
        if na(vBoard) or signature != vBoardSignature
            if not na(vBoard)
                table.delete(vBoard)
            vBoard := table.new(f_vPosition(vBoardAnchor), 2, array.size(rows), bgcolor = color.new(vBg, 100), frame_width = 0, border_width = 0)
            vBoardSignature := signature
        f_vRender(vBoard, rows, vBoardAnchor)

    if vDetail != "Off"
        array<CamViewRow> rows = array.new<CamViewRow>()
        f_vInsetStart(rows, vFocusAnchor)
        if vInspectIndex >= 0 and vInspectIndex < array.size(plays)
            Play play = array.get(plays, vInspectIndex)
            string tip = f_vPlayTip(play)
            f_vAdd(rows, "", "INSPECT · " + f_vName(play), tip, 4)
            f_vAdd(rows, "", f_vDirection(play.dir) + " · " + f_vState(play, camInSession) + (f_vAge(play) == "" ? "" : " · " + f_vAge(play)), tip, 2)
            f_vAdd(rows, "", "WHY · " + f_vWrap(play.reason, 38, 3), play.reason, 0)
            f_vAdd(rows, "", "NEXT · " + f_vWrap(play.next, 38, 3), play.next, 0)
            f_vAdd(rows, "", "FAIL · " + f_vWrap(play.invalidation, 38, 2), play.invalidation, 3)
            if vDetail == "Standard"
                f_vAdd(rows, "", f_vGateWords(play), "Direct core flags. Candidate excludes current region; Approaching includes it. SMA is an optional original close filter.", 3)
                f_vAdd(rows, "", "SETUP " + f_vTime(play.setupTime) + " · ACTIVE " + f_vTime(play.activationTime), tip, 3)
                f_vAdd(rows, "", "OPEN " + f_camPrice(camOpen) + " · " + f_vTime(camOpeningSourceTime) + " UTC", vSourceTip, 3)
                f_vAdd(rows, "", f_vWrap("SOURCE " + camSourceLabel + " · close " + camCloseLabel + " · " + camDecisionLabel, 40, 2), vSourceTip, 3)
                f_vAdd(rows, "", "ORIGINAL EXECUTION\n" + stopModel + " SL / " + targetModel + " TP · min " + str.tostring(minRewardR) + "R", vRiskTip, 3)
                f_vAdd(rows, "", CAM_IS_LAB ? "ALERT · alert() calls = reference JSON\nOrder fills = broker-emulated orders" : "ALERT · activation condition + bar close\nJSON · Any alert() function call", "Choose the desired condition; recreate after input/code changes. Reference plans are not fills. No alert delivery is proven by this display.", 3)
        else
            f_vAdd(rows, "", "INSPECT · SESSION GUIDE", "Choose a code to audit its prerequisites. Chart tracking remains independent.", 4)
            f_vAdd(rows, "", f_vWrap(f_vRecovery(), 38, 3), vSourceTip, 0)
        f_vAdd(rows, "", vTracking ? f_vWrap("CHART TRACKS · " + f_vDirection(vTracked.dir) + " " + f_vSnapshotName(vTracked), 38, 2) : "CHART · no active plan", "Manual inspection cannot change tracked snapshot selection.", 3)
        f_vInsetEnd(rows, vFocusAnchor)
        string signature = f_vSignature(rows)
        if na(vFocus) or signature != vFocusSignature
            if not na(vFocus)
                table.delete(vFocus)
            vFocus := table.new(f_vPosition(vFocusAnchor), 2, array.size(rows), bgcolor = color.new(vBg, 100), frame_width = 0, border_width = 0)
            vFocusSignature := signature
        f_vRender(vFocus, rows, vFocusAnchor)

// STRATEGY LAB · broker-emulated experiment, separate from structural observations.
string labGroup = "Strategy Lab · one position · next available open"
string labCode = input.string("All · fixed code order", "Eligible signal selection", options = ["All · fixed code order", "HA", "LA", "HB", "LB", "HC", "LC", "HD", "LD", "HE", "LE", "HF", "LF", "HG", "LG"], group = labGroup, display = display.none, tooltip = "When several ACTIVE events share a close, the first permitted code in HA, LA, HB, LB … HG, LG order wins. This is a deterministic selection rule, not a ranking of signal quality.")
string labSide = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = labGroup, display = display.none)
float labRiskPct = input.float(0.5, "Risk budget · % equity at signal", minval = 0.01, maxval = 5, step = 0.05, group = labGroup, display = display.none)
float labExposurePct = input.float(90, "Maximum notional · % equity", minval = 1, maxval = 100, step = 5, group = labGroup, display = display.none, tooltip = "Default 90% reserves headroom for fees and price movement; 100% remains available for explicit stress tests. This cannot prevent every margin call. Uses symbol point value and account-currency conversion. Risk is estimated at signal price; gaps can exceed it.")
float labQtyStep = input.float(0, "Quantity increment · 0 uses symbol minimum", minval = 0, group = labGroup, display = display.none, tooltip = "Uses syminfo.mincontract when zero. Verify your instrument and broker contract specification. No quantity is rounded up to force a trade.")
int labFrom = input.time(timestamp("01 Jan 2020 00:00 +0000"), "First signal time", group = labGroup, display = display.none)
int labTo = input.time(timestamp("01 Jan 2099 00:00 +0000"), "Last signal time", group = labGroup, display = display.none)
bool labShow = input.bool(true, "Show small Lab readout", group = labGroup, display = display.none)

f_labQuantity(float equity, float budgetPct, float capPct, float riskPerUnit, float valuePerUnit, float step) =>
    bool valid = f_camSign(equity) > 0 and f_camSign(riskPerUnit) > 0 and f_camSign(valuePerUnit) > 0 and f_camSign(step) > 0 and f_camSign(budgetPct) > 0 and f_camSign(capPct) > 0
    float steps = valid ? math.min(equity * budgetPct / 100 / riskPerUnit, equity * capPct / 100 / valuePerUnit) / step : 0.0
    // Normalize only floating representation noise at a whole-step boundary.
    float nearest = math.round(steps)
    float stepTolerance = math.min(1e-5, 4.0 * 2.220446049250313e-16 * math.max(1, math.abs(steps)))
    float normalized = f_camSign(math.abs(steps - nearest) - stepTolerance) <= 0 ? nearest : steps
    valid ? math.floor(normalized) * step : 0.0

var string labTrackedCode = ""
var int labTrackedSession = na
var int labTrackedEpisode = na
var int labSignalBar = na
var int labExpiryBar = na
var int labSessionEnd = na
var int labLastExitBar = na
var int labPreviousClosed = 0
var float labStop = na
var float labTarget = na
var int labSignals = 0
var int labSkipped = 0
var int labMarginExits = 0
var int labFirstReady = na
var int labLastReady = na
var int labReadyBars = 0
var string labStatus = "Waiting for a confirmed ACTIVE event"
var table labPanel = na
if barstate.isfirst and labShow
    string labAnchor = f_vFree(vUsedAnchors, "Bottom Right")
    labPanel := table.new(f_vPosition(labAnchor), 1, 5, bgcolor = vBg, frame_color = vFrame, frame_width = 1)
if barstate.isfirst and labTo <= labFrom
    runtime.error("Strategy Lab: last signal time must be after first signal time.")

if barstate.isconfirmed
    if camMapReady and camOpenKnown and camInSession and camBarOhlcValid and time_close >= labFrom and time_close < labTo
        labFirstReady := na(labFirstReady) ? time_close : labFirstReady
        labLastReady := time_close
        labReadyBars += 1
    // Closed-trade records include partial margin liquidations. A surviving
    // position must keep its original owner, bracket and independent deadlines.
    bool labPartialExit = strategy.closedtrades > labPreviousClosed and f_camSign(strategy.position_size) != 0
    if strategy.closedtrades > labPreviousClosed
        for trade = labPreviousClosed to strategy.closedtrades - 1
            if strategy.closedtrades.exit_comment(trade) == "Margin call"
                labMarginExits += 1
        // This also handles an entry and complete exit between calculations.
        if f_camSign(strategy.position_size) == 0
            labLastExitBar := bar_index
            labTrackedCode := ""
            labTrackedSession := na
            labTrackedEpisode := na
            labSignalBar := na
            labStop := na
            labTarget := na
            labExpiryBar := na
            labSessionEnd := na
            labStatus := "Broker position fully closed · see trade list"
    labPreviousClosed := strategy.closedtrades

    bool labQueued = labTrackedCode != "" and f_camSign(strategy.position_size) == 0 and not na(labSignalBar)
    if labQueued and bar_index > labSignalBar
        // Market entries normally fill at the next available open. If the broker
        // rejected it (margin/quantity), do not silently carry an orphan bracket.
        strategy.cancel_all()
        labTrackedCode := ""
        labLastExitBar := bar_index
        labStatus := "No broker fill · inspect quantity / margin"
        labSkipped += 1

    if f_camSign(strategy.position_size) != 0
        labStatus := labTrackedCode + " broker " + (f_camSign(strategy.position_size) > 0 ? "LONG" : "SHORT") + " @ " + str.tostring(strategy.position_avg_price, format.mintick) + (labPartialExit ? " · partial exit; bracket retained" : "")
        strategy.exit("Lab bracket", "Lab", stop = labStop, limit = labTarget, comment_loss = "Frozen stop", comment_profit = "Frozen target")
        bool labTimeExit = time_close >= labTo or (not na(labExpiryBar) and bar_index >= labExpiryBar) or (not na(labSessionEnd) and time_close >= labSessionEnd)
        if array.size(camBarEvents) > 0
            for i = 0 to array.size(camBarEvents) - 1
                CamEvent e = array.get(camBarEvents, i)
                if e.code == labTrackedCode and e.sessionStart == labTrackedSession and e.episode == labTrackedEpisode and e.kind == "EXPIRED"
                    labTimeExit := true
        if labTimeExit
            // Default market close is next available tick, including overnight gaps.
            strategy.close("Lab", comment = "Time / session expiry")
            labStatus := labTrackedCode + " · time exit queued"

    bool labFlat = f_camSign(strategy.position_size) == 0 and labTrackedCode == "" and (na(labLastExitBar) or bar_index > labLastExitBar)
    bool labWindow = time_close >= labFrom and time_close < labTo
    if labFlat and labWindow and array.size(camBarEvents) > 0
        bool labSelected = false
        for i = 0 to array.size(camBarEvents) - 1
            CamEvent e = array.get(camBarEvents, i)
            bool selectedCode = labCode == "All · fixed code order" or labCode == e.code
            bool selectedSide = labSide == "Both" or (labSide == "Long only" and e.dir == 1) or (labSide == "Short only" and e.dir == -1)
            if not labSelected and e.kind == "ACTIVE" and selectedCode and selectedSide
                labSelected := true
                labSignals += 1
                float step = f_camSign(labQtyStep) > 0 ? labQtyStep : syminfo.mincontract
                float unitRisk = strategy.convert_to_account(math.abs(e.entry - e.stop) * syminfo.pointvalue)
                float unitValue = strategy.convert_to_account(e.entry * syminfo.pointvalue)
                float quantity = f_labQuantity(strategy.equity, labRiskPct, labExposurePct, unitRisk, unitValue, step)
                if f_camSign(quantity) > 0 and not na(e.stop) and not na(e.target)
                    labTrackedCode := e.code
                    labTrackedSession := e.sessionStart
                    labTrackedEpisode := e.episode
                    labSignalBar := bar_index
                    labExpiryBar := bar_index + activeDeadline
                    labSessionEnd := camSessionEnd
                    labStop := e.stop
                    labTarget := e.target
                    strategy.entry("Lab", e.dir == 1 ? strategy.long : strategy.short, qty = quantity, comment = e.code)
                    strategy.exit("Lab bracket", "Lab", stop = labStop, limit = labTarget, comment_loss = "Frozen stop", comment_profit = "Frozen target")
                    labStatus := e.code + " · entry queued for next tick"
                else
                    labSkipped += 1
                    labStatus := e.code + " skipped · quantity / conversion"

if (barstate.islastconfirmedhistory or barstate.isrealtime) and labShow
    table.cell(labPanel, 0, 0, "STRATEGY LAB · " + str.tostring(strategy.closedtrades) + " exits", text_color = vAccent, bgcolor = vHeader, text_size = vBodySize, text_halign = text.align_left, tooltip = "Broker closed records include partial exits. Margin-call records: " + str.tostring(labMarginExits) + ". A remaining position retains its original bracket and deadlines.")
    table.cell(labPanel, 0, 1, f_vWrap(labStatus, 34, 2), text_color = vText, bgcolor = vBg, text_size = vBodySize, text_halign = text.align_left)
    table.cell(labPanel, 0, 2, "Net " + str.tostring(strategy.netprofit, "#.##") + " · Max DD " + str.tostring(strategy.max_drawdown, "#.##"), text_color = vMuted, bgcolor = vBg, text_size = vSmallSize, text_halign = text.align_left, tooltip = "Account-currency amounts from the broker emulator; closed net profit and maximum equity drawdown. Consult Strategy Report for period, open P/L and trade details. The chart HUD shows structural references, not broker fill prices.")
    string labCoverage = na(labFirstReady) ? "No signal-ready source coverage" : "Ready " + str.format_time(labFirstReady, "dd MMM", "Etc/UTC") + "–" + str.format_time(labLastReady, "dd MMM", "Etc/UTC") + " · " + str.tostring(labReadyBars) + " bars"
    table.cell(labPanel, 0, 3, labCoverage, text_color = vMuted, bgcolor = vBg, text_size = vSmallSize, text_halign = text.align_left, tooltip = "Coverage includes valid contained chart bars with source and opening ready inside the selected test window, not necessarily a candidate. First/last dates do not imply uninterrupted coverage. Risk budget " + str.tostring(labRiskPct) + "%; skipped quantities/fills " + str.tostring(labSkipped) + ". Example defaults: 0.05% commission per side and 1 tick slippage. Actual costs are in Properties. Gaps and costs can exceed signal-price risk.")
    table.cell(labPanel, 0, 4, "Emulated · verify costs in Properties", text_color = vMuted, bgcolor = vBg, text_size = vSmallSize, text_halign = text.align_left)

Source identity manifest · MIT licence. Save the two scripts separately; adding the indicator does not run the Lab.

Strategy Lab separately emulates selected activations, occupancy, next-available fills, frozen brackets, sizing and costs. A HUD reference is not its fill. Verify settings and source-ready dates first.

THE RULES ARE OPEN

Read each opening gate, setup, confirming close and ending.

Read the complete rules →
04 · GO DEEPER WHEN YOU NEED IT

The complete tool, one topic at a time.

Open the question you are working on. All original rules, experiments and settings remain here.

Map formula, close context and width

Completed sources first. Today’s evidence second.

Map, opening and completed ATR freeze for the decision session.

SEVEN LEVELS

A complete ladder by default

H6, H4, H3, PC, L3, L4 and L6 have solid lines and right-side price tags. PC is the selected prior close. H/L also means R/S; H2/L2 are not floor-pivot CPR.

CLOSE-DELTA CONTEXT

Higher, Lower or Neutral

Higher means selected close exceeds its predecessor by more than two percent of frozen ATR. Lower mirrors it. Equality is Neutral; Neutral defaults off.

WIDTH GATE

Similar includes the boundary

Compare H3−L3 widths. Ratios 0.98–1.02 are Similar, inclusive. A/E require Wide or Similar; B/C/D Narrow or Similar; F/G any valid width. Band overlap is separate context.

The expanded-six formula

D = H − L; PC = selected C. H2/L2 = C ± 1.1D/6; H3/L3 = C ± 1.1D/4; H4/L4 = C ± 1.1D/2; H6 = (H/L)C; L6 = 2C − H6. H5 = H4 + 1.168(H4 − H3), with L5 mirrored, is audit-only.

Original sources must be positive and coherent; an independent close may lie outside chosen H/L. A needs valid inner geometry; other families also require outer levels.

Source, decision clock and ATR

SOURCE builds the map. CLOCK controls the plays.

MARKET DEFAULT

Crypto daily; other markets RTH

Crypto uses completed Exchange daily data. Non-crypto uses configured RTH, initially 09:30–16:00 New York. A futures provider daily bar is never relabelled cash RTH.

DECISION AUTO

24/7 or US cash

Crypto follows Exchange day. Others use US cash. RTH/ETH intervals, custom sessions and IANA timezones are explicit; configure other exchanges deliberately.

INDEPENDENT CLOSE

Match source, RTH, ETH or daily

H/L and close can use separate source pairs. ATR has its own basis and timestamp, independent of pivot selection.

OPEN UNKNOWN means wait. One-minute opening and chart boundary coverage must be known; loading mid-session cannot certify missing bad-OPEN history.

ATR basis: Provider daily or observed RTH session

ATR Market default selects observed RTH-session ATR(14) for futures unless the decision clock is Exchange day; Provider daily ATR(14) otherwise. Explicit selections override. RTH true ranges use complete sessions, not chart candles.

Fourteen complete observed RTH sessions seed the mean true range, then Wilder smoothing updates it. An observed incomplete or malformed session resets the chain; entirely absent dates are skipped. Missing, nonpositive or more than seven elapsed days old means ATR WAIT without fallback. Basis, value, accepted timestamp, chain start and sample count freeze at decision opening. This aggregation is not verified Premium private-formula parity.

Strict Auto RTH/ETH is opt-in

Auto requires complete aligned pre/postmarket windows, coherent source pairs and ATR. Excursion strictly beyond the ATR tolerance chooses ETH; equality chooses RTH. Missing coverage blocks it. There is no silent fallback.

Market default needs no extended-hours data. Auto freezes at decision opening.

Coverage, coarse charts and freshness

RTH/ETH/Selected session require observed one-minute boundaries, continuous coverage and valid OHLC. The 100,000 minutes requested cover about 69 continuous crypto days; visible chart history can extend farther.

Source pairs must be ordered, completed by decision opening and at most seven elapsed days old. Incomplete windows occupy the slot; absent windows are not classified as holidays. Coarse charts can use a known minute opening after an observed straddled boundary; missing chart coverage blocks.

Malformed OHLC clears previous-close knowledge; clock expiry continues. Overnight weekdays follow the ending date, full local days the starting date. Repeated/skipped DST endpoints remain unverified.

All fourteen plays

Fourteen hypotheses. Separate gates and triggers.

H/L names structure, not direction. O is frozen opening; C is latest committed close. Opening intervals include equality. Approach intervals are strict. A/E accept Wide or Similar, B/C/D Narrow or Similar, F/G any valid width.

PLAY / CONTEXT / SIDEOPENING GATEAPPROACH REGIONORIGINAL SETUP → CONFIRMATIONSTRUCTURAL DESTINATION
HA · L3 bounceHigher · longL2 ≤ O ≤ H2L4 < C < PCTouch L3; close above.H3
LA · H3 fadeLower · shortL2 ≤ O ≤ H2PC < C < H4Touch H3; close below.L3
HB · H4 break + retestHigher · longL2 ≤ O ≤ H3H3 < C < H6Closed break above H4; later containing retest and close above.H6
LB · L4 break + retestLower · shortL3 ≤ O ≤ H2L6 < C < L3Closed break below L4; later containing retest and close below.L6
HC · H4 continuationHigher · longH4 ≤ O ≤ H6C > H3Approach arms; touch H4 and close above.H6
LC · L4 continuationLower · shortL6 ≤ O ≤ L4C < L3Approach arms; touch L4 and close below.L6
HD · L4 counterHigher · shortL6 ≤ O ≤ L4C < L3Approach arms; touch L4 and close below.L6
LD · H4 counterLower · longH4 ≤ O ≤ H6C > H3Approach arms; touch H4 and close above.H6
HE · H4 failureHigher · shortL2 ≤ O ≤ H3H3 < C < H6Close above H4 arms; later close below confirms failure.L4
LE · L4 failureLower · longL3 ≤ O ≤ H2L6 < C < L3Close below L4 arms; later close above confirms failure.H4
HF · H6 rejectionHigher · shortH4 ≤ O ≤ H6C > H4Touch H6; close below.Wide: H4; otherwise PC
LF · L6 rejectionLower · longL6 ≤ O ≤ L4C < L4Touch L6; close above.Wide: L4; otherwise PC
HG · L6 rejectionHigher · long · optionalL6 ≤ O ≤ PCC < L4Touch L6; close above. G defaults off.Wide: L4; otherwise PC
LG · H6 rejectionLower · short · optionalPC ≤ O ≤ H6C > H4Touch H6; close below. G defaults off.Wide: H4; otherwise PC

A touch means low ≤ tier ≤ high. A wrong-side gap is not a touch. Previous contained close starts at the known opening. B requires a strict closed crossing, with previous equality allowed.

The bad-OPEN block persists for the session

Check every valid contained candle OPEN, not wick. Strictly outside these limits blocks the code until a new session: HA [L4,H2]; LA [L2,H4]; HB/HE [L2,H6]; LB/LE [L6,H2]; HC/LD/HF [H3,U]; LC/HD/LF [D,L3]; HG [D,PC]; LG [PC,U]. U = H6 + 0.5(H6−H4), D = L6 − 0.5(L4−L6). Equality does not block.

A blocked pending setup invalidates. ACTIVE retains its ending rules; recovery cannot clear the block.

Same-bar versus later confirmation

A/F/G can touch and recover on the setup candle. Wrong-side touches need a later recovery from the previous wrong-side close. C/D can arm and confirm on a containing approach candle. B/E need a later bar. Confirmation still needs approach region, optional SMA alignment and acceptable geometry.

Setup, cancellation and endings

A candidate need not become a play.

CANDIDATE
Context, width, opening and bad-OPEN checks qualify. This is not an entry.
APPROACHING
Candidate plus closed price in its strict region. The original setup is still required.
BLOCKED / DATA WAIT
A prerequisite failed, bad OPEN latched, or required data is unavailable.
ARMED
An original setup was observed; confirmation and geometry remain required.
ACTIVE
A confirming close accepted fixed reference geometry. No order or fill is implied.
REJECTED / INVALIDATED
Geometry failed / a pending setup reached cancellation or a bad-OPEN block.
TARGET / STOP / EXPIRED
A later reference observation or deadline ended this episode.
DISABLED
The family is switched off; G defaults off.

Flags describe prerequisites, not ranking. Multiple hypotheses, including opposite directions, may coexist.

Cancellation, deadlines and bounded rearming

Cancellation precedes confirmation. Pending A long cancels at close ≤ L4; short at ≥ H4. B/C/D long at ≤ H3; short at ≥ L3. E long at ≤ L6; short at ≥ H6. F/G have no additional pivot cancellation. A bad OPEN invalidates pending setups.

Setup confirmation is allowed through age twelve; age thirteen expires first. Price outcomes precede active expiry at age 120 and known session end. Final session close cannot start ACTIVE. Rollover expires an unfinished episode without inventing a fill.

Factory: one episode per code per session, consumed when armed, including failures. Optional rearming requires cooldown, remaining budget and valid unblocked prerequisites. Cooldown three permits reset at end-bar plus three, never on the ending bar. Rearming cannot clear a bad-OPEN latch.

Fixed references and their geometry

Activation is an observation. The plan is a reference.

ENTRY REF is the confirming close; direction, stop, target, time, source and settings freeze with it. R measures distances, not fills or returns.

Structural defaults and ATR alternatives

A stop is beyond the farther of fourth tier and armed extreme. B/C/D use H3 for longs or L3 for shorts. E extends beyond outer tier and armed extreme. F/G extend beyond both armed extreme and one-quarter of fourth-to-third spacing outside the sixth tier.

The outward buffer defaults to one tick. Long stops round down, short stops up; targets round toward the activation close. Entry retains sub-tick precision. Prices must be positive, direction correct, risk at least one tick and ratio at least 1R after rounding.

Stop and target can independently use configurable multiples of frozen ATR. Optional maximum target distance rejects geometry without moving its destination. The lesson uses structural defaults; SMA and ATR alternatives are not simulated.

Later observations and frozen endings

No activation-bar outcome. Later stop gaps use observed open; target gaps use fixed target. Otherwise stop touch precedes target touch. If both touch, ambiguity is marked instead of inferring an intrabar path.

Invalid or uncontained OHLC cannot settle price outcomes; clock expiry continues. Ended snapshots retain references and end time as LAST REFERENCE. Current distance uses last closed price; it does not move the plan.

Every setting and your first chart

Verify coverage before interpreting a row.

  1. Use standard time-based 1m–60m candles with enough source history.
  2. Read resolved Market default source and Auto clock. Configure RTH/ETH for your exchange; non-crypto does not mean every exchange uses US cash.
  3. Check source, close, ATR and opening provenance. Resolve DATA WAIT or OPEN UNKNOWN first.
  4. Read the seven-level map and Top Left Ribbon, then follow the tracked episode. Open Inspector for its prerequisite, setup and NEXT.
  5. Save a named template; recreate alerts after script or causal setting changes.
Defaults are a baseline, not a selected market configuration

Market default source and ATR basis; Auto clock; matching close. Display: Ribbon, Top Left, Trading Dark, seven levels with right tags; 12% header clearance. Range threshold and Similar tolerance both two percent. A–F on, G off, Neutral off. Structural stop and target, one-tick buffer, minimum 1R, setup twelve, active 120, SMA off, rearming off, one episode.

Exact input names and factory defaults from the current source. The explanations above show how they fit together. A disabled option may require its parent switch.

Source and decision clock
Pivot sourcesourceModeDefault Market default
Decision windowdecisionModeDefault Auto
Custom decision intervalcustomSessionDefault 0930-1600
Custom IANA time zonecustomTimezoneDefault America/New_York
Custom weekdays onlycustomWeekdaysDefault true
RTH source intervalregularSessionDefault 0930-1600
RTH source IANA zoneregularTimezoneDefault America/New_York
ETH source clockextendedClockDefault Auto
Custom ETH intervalextendedSessionDefault 1800-1700
Custom ETH IANA zoneextendedTimezoneDefault America/New_York
Pivot close selectioncloseModeDefault Match source
Auto excursion · % selected ATRswitchingToleranceDefault 2
ATR calculation basisatrBasisModeDefault Market default
Public Lite baseline · confirmed prerequisites
Neutral close change · % selected ATRrangeThresholdPctDefault 2
Similar width tolerance · %widthThresholdPctDefault 2
Allow both directions in NeutralallowNeutralDefault false
Original SMA close confirmationsmaGateDefault false
Chart SMA lengthsmaLengthDefault 20
Original structural hypotheses
A · Third-band traverseenableADefault true
B · Interior breakout and retestenableBDefault true
C · Opening continuationenableCDefault true
D · Context-opposed continuationenableDDefault true
E · Fourth-tier failureenableEDefault true
F · Outer-tier rejectionenableFDefault true
G · Opposite-side outer rejectionenableGDefault false
Setup deadline · completed barssetupDeadlineDefault 12
Active deadline · completed barsactiveDeadlineDefault 120
Minimum structural reward / riskminRewardRDefault 1.0
Stop buffer · minimum ticksstopBufferTicksDefault 1
Reference stop modelstopModelDefault Structural
ATR stop multiplestopAtrMultipleDefault 0.25
Reference target modeltargetModelDefault Structural
ATR target multipletargetAtrMultipleDefault 0.5
Maximum target distance · selected ATR (0 off)maxTargetAtrDefault 0
Allow another episode after endingallowRearmDefault false
Rearm cooldown · completed barscooldownBarsDefault 3
Maximum episodes per code / sessionmaxEpisodesDefault 1
Aggregate JSON alerts
Include activation eventsalertActivationDefault true
Include ending eventsalertEndingDefault true
Include armed eventsalertArmedDefault false
Display
Trade HUDvBoardModeDefault Ribbon
InspectorvDetailDefault Off
ThemevThemeDefault Trading Dark
Chart densityvChartModeDefault Map
Panel & chart tag sizevScaleDefault Medium
Inspect playvFocusCodeDefault Auto
Show play codesvCodesDefault false
Display · placement
Trade HUD positionvBoardCornerDefault Top Left
Inspector positionvFocusCornerDefault Bottom Left
Top Left header clearance · %vHeaderClearanceDefault 12
Display · chart
Price tagsvPriceTagsDefault true
Camarilla middle bandvMiddleBandDefault false
Reference risk / reward zonesvPlanZonesDefault true
Ended plans shownvHistoryDefault 2
Chart tag budgetvTagCapDefault 12
Price tag sidevTagSideDefault Right
Entry / ending markersvEpisodeMarksDefault true
Plan zone opacity · %vZoneOpacityDefault 14
Strategy Lab · one position · next available open
Eligible signal selectionlabCode · companion onlyDefault All · fixed code order
DirectionlabSide · companion onlyDefault Both
Risk budget · % equity at signallabRiskPct · companion onlyDefault 0.5
Maximum notional · % equitylabExposurePct · companion onlyDefault 90
Quantity increment · 0 uses symbol minimumlabQtyStep · companion onlyDefault 0
First signal timelabFrom · companion onlyDefault 01 Jan 2020 00:00 +0000
Last signal timelabTo · companion onlyDefault 01 Jan 2099 00:00 +0000
Show small Lab readoutlabShow · companion onlyDefault true
Alerts and event payloads

One envelope for the closed bar.

Two alert conditions cover activation and episode ending. One “Any alert() function call” alert aggregates selected same-bar events into JSON. Armed events default off.

In TradingView’s alert dialog the two conditions are named “Camarilla play activated” and “Camarilla episode ended”. The envelope from “Any alert() function call” opens with {"schema":"camarilla-desk.v2","product_version":"2.1.0","rule_version":"lite2-prerequisites.original2.1-execution", …} and lists each event’s code, direction, time and fixed references.

The envelope includes settings, symbol, timeframe, source/session and frozen ATR provenance, with no confirmed fill. Recreate alerts after code, input, symbol or timeframe changes. Indicator: named condition or “Any alert() function call”. Strategy Lab: alert() calls for structural JSON; order fills are a separate broker event stream. No alerts are created here.

What the tool can and cannot tell you
READ THE BOUNDARIES

A declared hypothesis is not an order.

  • ACTIVE is a structural episode. STOP/TARGET are observed reference events, and R is activation geometry.
  • Prerequisites follow the author’s public Lite publication. Private Premium triggers and presets remain unverified. Weekly/monthly context is a separate publication, not this daily tool.
  • V2.1 combines public-Lite prerequisite concepts with original execution, source-freezing and ATR conventions. Fixtures prove neither private parity nor a trading edge.
  • Missing data blocks entry. Closed-bar confirmation adds delay, not prediction. Trading decisions and risk remain yours.
Common questions

Before you read ACTIVE

Can the map or an active reference move?

Map and plan freeze at decision opening and activation respectively. Forming bars do not commit changes. Input/history changes recalculate; display changes only presentation.

Why can a Higher play point short?

H names Higher context; SIDE names direction. HE/HF are short hypotheses, while HD is opposed continuation.

Why does a good-looking close still get rejected?

Wrong-side destination, unusable risk or insufficient rounded ratio rejects confirmation and consumes the episode. It does not chase another close.

  • ICT Levels Desk

    Compare calculated Camarilla tiers with previous-period highs, lows, session opens and other recorded-price references.

  • Opening Range Desk

    Study a session-opening range when the opening window matters more than prior-source pivot formulas.

Built by ProEA Lab. Compare all free indicators.

05 · THE LOG

Every change, dated

What shipped and what changed in Camarilla Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.

  1. Seven levels, clearer prerequisites and ATR v2.1.0 Updated Camarilla Desk

    The map, ATR basis, fourteen play rules and separate Strategy Lab now match the current release.

    • Seven map levels, the default Top Left Ribbon and clearly marked historical episodes
    • Explicit completed ATR basis, availability checks and frozen reference provenance
    • Updated candle simulator, full rulebook and separate indicator and Strategy Lab downloads
  2. Camarilla Desk v2.1.0 Shipped

    Builds a session map, then checks a potential trade.

BUILT WITH TRADERS

Something to change in Camarilla Desk?

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

What would help?

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

Add chart details Optional

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

Guides & new tools Optional

Add an email above to choose this.

Show us on your chart Optional

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

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

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

FREE & OPEN SOURCE · CLOSED-BAR RULES · REFERENCE LEVELS

© 2026 ProEA · All rights reservedBuilt for MT5 · Not affiliated with MetaQuotes