Participation

268
Library "Participation"
Market Participation layer for the Opening Range indicator family (M11).
Holds the pre-signal environment producers (P1–P5, P8), the at-signal snapshot,
the post-signal engine and the participation
table renderer. Factored out of the host to relieve its compiled-token budget
(CE10117); the host keeps only the `var` state declaration, one call per bar,
and `table.new`.

Ownership line, and it is forced rather than chosen: LIBRARIES CANNOT HOLD `var` STATE, so
every persistent value lives host-side in the `ParState` object the host declares and this
library mutates. Libraries also cannot read globals initialised from script inputs, so every
input-derived value arrives as a parameter — that is why `step` takes eleven of them.

Timezone literals are inline rather than parameters on purpose: a string literal is a const
expression, not a global, so the rule above does not touch it. Only the session STRINGS come
from inputs and therefore must be passed in.

Usage (host):
import Mr_NSFT/Participation/1 as par
var par.ParState parState = par.newState( // once; the A4 birth-rung
rvolAdequate = 0.80, rvolStrong = 1.10, // thresholds and the three
dayExtended = 0.90, chaseHigh = 0.90, // lifetime settings are all
tgtName = i_parVerdictTgt, cutoffHours = i_parCutoffHours, // HOST-owned; the
patienceMins = i_parPatienceMins, oiFloor = 0.10, // ladder's calibration
s1Band = 0.05, s2Building = 1.20, s2Fading = 0.83, // rides here too.
s4Hold = 3, s4Reject = 3, decayN = 3) // NEVER positional.
par.step(parState, isNewDay, i_parLookback, i_parVwapSlopeK, // every bar, unconditional
i_parSessAsia, i_parSessLondon, i_parSessNY,
dayLv.highs, dayLv.lows, dayLv.curH, dayLv.curL)
if array.size(parStates) > 0 and barstate.isconfirmed // the POST-signal engine —
par.advance(parStates, parState, barDelta, deltaSrc) // note the gate differs
// from step's, deliberately
var par.ParticipationConfig parCfg = par.ParticipationConfig.new(...)
var table parTable = makeParTable() // host owns table.new
par.renderParticipation(parTable, parCfg, parState)

newState(rvolAdequate, rvolStrong, dayExtended, chaseHigh, tgtName, cutoffHours, patienceMins, oiFloor, s1Band, s2Building, s2Fading, s4Hold, s4Reject, decayN)
  Build an empty producer state. Call once, in a host `var` declaration.
bktTypical seeds to na, not 0.0: the EMA takes its first observation as the seed
value (see foldBucket), which is the standard warm-up and avoids every bucket
spending its first N sessions climbing out of a zero it was never at. bktToday
seeds to 0.0 because it is a running sum that is cleared every session anyway.
Three ledgers, order-fixed: 0 = Asia, 1 = London, 2 = New York — and three snapshot
slots index-aligned with them, allocated once here so the per-bar copy writes into
existing objects instead of allocating three every bar.

EVERY cfg* value is REQUIRED, not defaulted, and that is the point of routing them
through here: a default would be a second source of truth that quietly wins whenever a
caller forgets one. They are `simple` because they are settings — the qualifier is what
makes "config, not state" checkable by the compiler instead of by reading the cfg
prefix, since a series argument would be rejected outright. It is also what lets an
`input` satisfy them: an input-qualified argument satisfies `simple`.
  Parameters:
    rvolAdequate (simple float): Cumulative RVOL at or above which participation is not obviously absent.
    rvolStrong (simple float): Cumulative RVOL at or above which the day is genuinely heavier than normal.
MUST be >= rvolAdequate; see the rung comment in seed() for what breaks.
    dayExtended (simple float): Share of ADR already travelled at which the day counts as extended.
    chaseHigh (simple float): Direction-adjusted entry location at or above which the entry is chasing.
    tgtName (simple string): Verdict-target display token — "TP1" / "TP2" / "TP3". ⚠️ The host must derive
the target PRICE it passes to seed() from the same input, or the outcome
suffix names a target the state was never measured against.
    cutoffHours (simple int): Hours of MARKET time from entry after which tracking ends with no verdict.
    patienceMins (simple int): Patience base in minutes; multiplied per signal type by the ladder.
    oiFloor (simple float): Episode opposing-intensity floor: the magnitude of |Σ opposing delta ÷ Σ volume|
a level's FIRST episode must reach for holding it to score +1 rather than 0.
    s1Band (simple float): Neutral band on the commitment ratio; outside it, S1 is aligned or against.
    s2Building (simple float): recent3 ÷ prior3 at or above which participation pace is building.
    s2Fading (simple float): …and at or below which it is fading. Make the pair multiplicatively symmetric
(0.83 ≈ 1/1.20): an arithmetic pair makes fading HARDER to reach, not easier.
    s4Hold (simple int): H — consecutive closes beyond the origin for `holding`. ⚠️ MUST be >= 2.
    s4Reject (simple int): R — consecutive closes back through it for `rejected`. ⚠️ MUST be >= 1.
    decayN (simple int): N — consecutive bars of S1 `against` + S2 `fading` that stall a signal.
  Returns: A fresh `ParState`.

step(p, isNewDay, lookback, slopeK, sAsia, sLondon, sNY, dayHighs, dayLows, dayCurH, dayCurL)
  Advance the whole participation layer one bar. Mutates `p` by reference; returns
nothing useful.

GATED ON timeframe.isintraday, DELIBERATELY NOT ON the host's tfValid. The layer
describes the trading DAY, not the OR window, so unlike the rest of the indicator
its readings stay meaningful above 15 minutes — and the host's tables render at any
timeframe, so a tfValid gate would leave the participation table showing nothing but
dashes on a 1H chart. The host's "tfValid gates producers" rule does not bind here:
it exists because request.footprint's row count scales with bar range and fed an
unbounded var map that replay's rollback snapshots doubled. This layer's state is
FIXED size — 2 × 48 floats, 3 small UDTs, a dozen scalars — and allocates nothing
per bar, so there is no bar-width-proportional cost to gate away.

isintraday is the ceiling because that is where the producers actually break, not a
round number: at 1D a daily bar sits inside the session on every bar, so a ledger
enters and NEVER exits and its high/low accumulate across every day without bound;
VWAP re-anchors every bar and collapses to hlc3; and above 1D the daily H/L tracker
pushes weekly ranges that ADR would report as daily. Below 1D all of those are
sound. P3 has no ceiling at all. P2 has its own, stricter one — see barRvol.

STATE is never barstate.isconfirmed-gated: `p` is var-held host-side, so Pine reverts
those writes on each realtime tick and commits them at close, and gating would hide
them from same-bar consumers while buying nothing. The only exception is the two
volume RATIOS — barRvol and cumRvol — which are published at close only; the reason
is at their write site, and it is about ratios, not about state.
  Parameters:
    p (ParState): Producer state (mutated).
    isNewDay (bool): The host's holiday-corrected session boundary — never a raw ta.change.
    lookback (simple int): Sessions defining "normal": the ADR window and the RVOL EMA weight.
    slopeK (simple int): Bars back for the VWAP slope.
    sAsia (simple string): Asia session, in TOKYO time.
    sLondon (simple string): London session, in LONDON time.
    sNY (simple string): New York session, in NEW YORK time.
    dayHighs (array<float>): The host daily tracker's completed-day highs.
    dayLows (array<float>): The host daily tracker's completed-day lows.
    dayCurH (float): The forming day's high.
    dayCurL (float): The forming day's low.

sessReading(s, sessOpen)
  One session's Data-Window readings, freshness-gated: a session that has not traded
today returns na on every field rather than its previous occurrence's values.

⚠️ ext AND eff COME OUT AS PERCENTAGES (×100); rng stays in points. The stored
fields are fractions — the scaling is applied here so that this function and the
table's fmtPct agree, because a reader comparing the panel against the Data Window
for the same session must not see 45.7% next to 0.457. Two consequences worth
stating: the ×100 lives in the library rather than at the host's plot() calls
because CE10295 counts the MAIN BODY and six multiplications there are six main-body
tokens spent on a unit conversion; and sessCells() does NOT come through here — it
scales the raw field through fmtPct — so there is no double scaling today, but a new
consumer must pick exactly one of the two paths, never both.

DIRECTION IS CARRIED BY THE SIGN OF `ext`, which is why this returns a signed value
where the table shows an arrow: extPct is a magnitude and never legitimately
negative, so the sign is free carriage and saves three plots. ⚠️ The one ambiguity:
dir = 0 (a session that closed exactly at its open) renders as positive, so a flat
session reads as up. Rare, and the alternative is a third plot per session.

`rng` is returned alongside so the host's three pre-existing range plots can take
the same gate — they were ungated and would show yesterday's range before a session
opens today. It is deliberately NOT scaled: points are already the unit a trader
reads it in.
  Parameters:
    s (SessionLedger): The live ledger.
    sessOpen (int): Host's parState.sessOpen — the day anchor `seenAt` is compared against.
  Returns: [signed ext %ADR, eff %, rng in points], all na when the session has not traded
today.

buildUniverse(histPrice, histCost, histTag, orh, orl, vah, val, poc, cVah, cVal, cPoc, bcOr, bcVa, bcPoc, bcCVa, bcCPoc, bcWeek, dayHi, dayLo, h4Hi, h4Lo, dSwHi, dSwLo, h4SwHi, h4SwLo, h1SwHi, h1SwLo)
  Build the participation level universe — Room's historical memory, plus the current
session's levels, plus the HTF horizontals — as index-aligned
[prices, baseCosts, tags]. The corridor scan (A2) and the two-class watchlist (A3)
need a wider set than Remaining Room does: Room is historical-only by contract,
while participation also has to see what price is trading against right now.

⚠️ THE COPY IS LOAD-BEARING. The host shares `histPrice`/`histCost`/`histTag` with
Context Score's ScoreInput — one flatten now serves both engines — so appending in
place would push the HTF levels into Room and silently change the score. Room's
historical-only contract is preserved by copying, not by call ordering, because
ordering is the kind of invariant that survives exactly until someone moves a line.

THIS LIVES IN THE LIBRARY FOR THE MAIN-BODY BUDGET, and the reason generalises:
Pine instantiates a host function at EVERY call site, so a fat host body called ten
times is counted ten times against CE10295. Library code is exempt from that budget
entirely. Host-side this was ~1,550 tokens × 10 firing branches; here it is free, and
the host pays only the arguments — once, because the universe is identical for every
branch on a given bar.

Tags are the CHART'S OWN drawing names ("D-SwH", "wk-H", "C-POC"). Not cosmetic: the
client asked for this enumeration so he can tell whether an excursion figure came
from an OR level, a VAH, a composite or a weekly, so a tooltip must name a level the
way the chart labels it.

Weekly is deliberately absent from the HTF parameters — it arrives inside the hist
arrays, because weekly is the one HTF type Context Score already scores.
  Parameters:
    histPrice (array<float>): Room's flattened historical levels — price. COPIED, not mutated.
    histCost (array<float>): …base cost, index-aligned.
    histTag (array<string>): …type tag, index-aligned.
    orh (float)
    orl (float)
    vah (float)
    val (float)
    poc (float)
    cVah (float)
    cVal (float)
    cPoc (float)
    bcOr (float)
    bcVa (float)
    bcPoc (float)
    bcCVa (float)
    bcCPoc (float)
    bcWeek (float): The weekly base cost — the calibrated anchor every HTF weight is scaled from.
    dayHi (array<float>)
    dayLo (array<float>)
    h4Hi (array<float>)
    h4Lo (array<float>)
    dSwHi (array<float>)
    dSwLo (array<float>)
    h4SwHi (array<float>)
    h4SwLo (array<float>)
    h1SwHi (array<float>)
    h1SwLo (array<float>)

seed(states, p, bull, sigType, famIdx, entry, stop, target, origin, originTag, originCost, lvlPrice, lvlCost, lvlTag, bcRef, isRetest)
  Latch one signal's at-signal snapshot: A1 environment, A2 stop-side scan, A3
two-class watchlist, A4 birth rung. Pushes the new state into `states` and returns
nothing useful — the host pays one statement per firing branch.

CALLED UNCONDITIONALLY per branch, before the host's tradability gate, so a
Friday-blocked signal is measured in full. Not isconfirmed-gated either: `states` is
a var array host-side, so the push is reverted and re-applied on every realtime tick
and committed at close, exactly like the tradeStates push beside it. Gating would
hide the state from the label builder, which runs later on the same bar.

ORDER INSIDE THE BODY IS LOAD-BEARING: the co-occurrence bits are set on the states
that already exist BEFORE the new one is pushed. Reversed, a signal would record
itself as having co-occurred with itself. Doing both here rather than at the ten host
call sites is what makes that structural instead of a rule somebody has to obey.
  Parameters:
    states (array<ParticipationState>): The host's var array of live states (mutated).
    p (ParState): Producer state — read for the A1 snapshot fields only.
    bull (bool): Trade direction.
    sigType (string): Signal type string, carried for DISPLAY only.
    famIdx (int): Signal family, 0 = Continuation · 1 = Accepted Continuation · 2 = Inflection ·
3 = Velocity. An int, not a parsed string: the type strings are host constants
and a library that compared them to its own literals would mis-classify in
silence the day one was renamed.
    entry (float): Execution-bar open.
    stop (float): Resolved stop; the whole call is skipped when it cannot be resolved.
    target (float): Verdict target — TP1 today; Phase 3's selectable target changes only the caller.
    origin (float): The level the setup is keyed to (S4 reads it; it joins the support class).
    originTag (string): Its type name, for display.
    originCost (float): Its base cost, chosen by the caller because only the firing branch knows which
level type the origin is.
    lvlPrice (array<float>): Flattened level universe — price.
    lvlCost (array<float>): …base cost, index-aligned.
    lvlTag (array<string>): …type tag, index-aligned.
    bcRef (float): Base-cost normaliser for wBase.
    isRetest (bool): Is this the RETEST variant within its family? Only Continuation and Accepted
Continuation have one, and it adds the within-family patience step. A bool
rather than a sixth famIdx value on purpose: famIdx is the CLIENT'S four-family
hierarchy and is read by the display filter, so widening it would change what
that filter means. And a bool keeps the type STRINGS out of this library —
comparing them here would mis-classify in silence the day one was renamed.

advance(states, p, barDelta, deltaSrc)
  Advance every live participation state one CONFIRMED bar: resolve the ones that end on
this bar, writing each one's outcome and removing it in the same pass. Mutates `states`
and its members by reference; the return value carries nothing and exists only to give
the export a defined type — see the note at the bottom of the body.

⚠️ NAMED advance, NOT step. `par.step` is already taken — it is the M11-1 PRODUCER step
and takes a `ParState`, while this takes the array of per-signal states. Whether Pine
v6 resolves user-defined library overloads by parameter type is not stated anywhere in
the v6 reference set, and there is no local compiler to settle it cheaply, so the two
deliberately never share a name.

THE HOST GATE IS `array.size(parStates) > 0 and barstate.isconfirmed`, and BOTH terms
diverge from par.step's own gate on purpose. ⚠️ Do not harmonise them:

• tfValid — which par.step explicitly REJECTS (see its own doc comment) — does bind
here. Post-signal state is array<ParticipationState>, each holding its own
array<WatchedLevel>, so it scales with signal count × level count: exactly the
bar-range-proportional shape the host's "tfValid gates producers" rule was written
for, and not the fixed-size producer state that rule does not reach.
It is delivered by the EMPTINESS TEST rather than spelled out, because the explicit
term is provably dead: par.seed is reachable only from inside the host's
`if i_tmEnabled and orComplete and tfValid` block, so `states` cannot gain a member
while tfValid is false, and changing the chart timeframe recalculates the script
from bar 1, so no populated array can survive into a timeframe above the ceiling.
⚠️ THAT REASONING DIES IF A SEED PATH IS EVER ADDED OUTSIDE THAT BLOCK — the term
would then have to be written out. This note is why the missing test is not read
as an oversight and "fixed" with main-body tokens that can never do anything.

• barstate.isconfirmed binds here and does NOT gate par.step's state writes. Two
independent reasons, either sufficient: S3's vote sign rests on a non-touch bar's
range being STRICTLY one side of the level, which is not true of a forming bar (a
bar that is non-touch at tick 3 can be a touch bar at tick 40, so episodes would
open and un-open through the bar and correctness would rest on rollback rather
than on the stated invariant); and barDelta is na on every non-confirmed tick, so
an ungated engine would accumulate na into cumDelta, which is permanently
absorbing.

⚠️ THE DISPLAY WRITES ARE NOT ON THIS GATE. Resolution writes fire from inside here,
but the live refresh is `barstate.islast` and must never gain isconfirmed.

⚠️ CALL-SITE RULE, AND IT IS A RULE RATHER THAN A LINE NUMBER: **below every signal
`label.new`, above every display read.** Three independent things pin it there.

1. INVARIANT — THE ENGINE ADVANCES A STATE ON THE SAME BAR THAT SEEDED IT, AND NEVER
BEFORE. Being below all ten seed sites and below the footprint block means a state
seeded this bar is advanced on its own entry bar with that bar's true delta,
identically live and on reload. Move the call above the firing block and cumDelta
starts a bar late, and a signal stopped out on its own entry bar is first tested
on bar N+1 against N+1's low — bar N's stop-out is never seen at all, which is
invisible rather than merely late.
2. ⚠️ THE RESOLUTION WRITE NEEDS A LABEL THAT EXISTS. The host's pending-signal
renderers create the handles far below the trade-opening block — and VELOCITY's
renderer is ~1,100 lines below the others, so "under the main label block" is not
far enough and would leave one family broken while looking fixed. Called any
earlier, a signal resolving on its own entry bar hits its only history-surviving
write with a na handle and keeps its birth text, with no outcome suffix, for ever.
3. ⚠️ THE PRUNE WOULD OUTRUN THE AT-SIGNAL RENDERERS. atSignalText/atSignalTip find
their states by `seedBar == bar_index` (renders()), so removing a state on the bar
it resolved — above those renderers — would strip the participation block off that
signal's label entirely. Below them, the removal is free and immediate.

Everything between the trade-opening block and here is safe to skip past: nothing else
reads this array, barDelta is a host global fixed for the bar, and the trailing engine
touches only tradeStates and drawings, while a participation stop is latched at seed
and never trailed.
⚠️ THE BAR'S OHLCV IS READ FROM THE BUILT-INS, NOT PASSED — and that reverses the
plan's §7.2.2 signature on evidence it did not have. Two reasons, the first decisive:

• **Pine rejects an unused argument outright (CE10237)**, so a signature CANNOT be
declared ahead of its consumers. §7.2.2's `(states, p, high, low, close, volume,
barDelta)` was written to be declared once and never touched again; that is not a
thing Pine allows, and the choice became "grow it in three steps" versus "don't
need it". Cost of being wrong here is one host line plus a republish, which is
free during development.
• It is this file's own idiom. `stepLedger` reads open/high/low/close/time directly
and the exported `step` reads `volume` directly — a library function evaluates in
the caller's bar context, so these are the host chart's values. Passing them also
meant FIVE same-typed float parameters at one call site, where a transposition
compiles clean and is invisible on the chart — the hazard the A4 thresholds carry
a standing warning about.

`barDelta` IS the one bar input that is not a built-in — the host computes it from the
footprint or the CLV proxy — which is why it, alone, is a parameter. Producer readings
never join the signature at all: they ride `p`, which is what §7.2.2's surviving
argument was actually about.
  Parameters:
    states (array<ParticipationState>): The host's `var` array of per-signal states. Mutated: resolved, then pruned.
    p (ParState): The producer state — read for its cfg* settings and its per-bar readings.
    barDelta (float): This confirmed bar's signed volume delta, or na where it could not be measured.
    deltaSrc (float): Which source produced it — 1 footprint, 2 CLV proxy. The second host-computed
bar input, and the only reason it is passed: S1 records, per signal, whether any
folded bar used the proxy.

reason(s)
  The one-sentence reason for a signal's current rung. Two halves: **why it is where it
is** ("because") and **what blocks the next rung** ("but").

TEMPLATES, NOT FIXED STRINGS, and that is not a style preference. A conjunct can be
unmet for meaningfully different reasons and whether that matters differs per
component: S1 can be `neutral` OR `against` at DEVELOPING — and `against` alone does
not decay, that needs the sustained conjunction — while S2 can be `steady` or `fading`.
Interpolating the component's own status word carries every distinction at one string
each, and the same templates absorb the unreadable cases for free, which is why the
word is "unreadable" rather than "unknown".

⚠️ ONE BLOCKER IS NAMED, NOT ALL, and completeness is delivered elsewhere: the four
component statuses have their own display rows, always visible, so a reader already
sees that S4 is rotating AND S2 fading AND S1 against. This row's job is the headline;
naming four blockers in prose would duplicate those rows and re-inflate a tooltip that
was deliberately cut from 23 rows to 14.

⚠️ DECLARATION ORDER IS USER-VISIBLE BEHAVIOUR. "First unmet conjunct in declaration
order" is deterministic and free; the alternative is a priority heuristic ranking four
blockers, a judgment with no evidence behind it and a second thing to maintain. But
whichever conjunct is written first gets named most often, so **S4 is declared first**
deliberately — acceptance of the origin is the most legible blocker to a trader — not
by falling out of typing order.

⚠️ Why the DISJUNCTION needs only one phrase: for a conjunction, failure means CHOOSING
which conjunct to name, since several can be false at once. For a disjunction, failure
means EVERY branch is false, so there is nothing to choose. The veto is a separate
conjunct, hence its own phrase.
⚠️ THE RULE THAT DECIDES WHETHER A CLAUSE MAY READ A LIVE FIELD, and both halves of
this function were wrong about it once:
⚠️ AND IT SPEAKS ONLY ABOUT LADDER EVOLUTION — silent where none has happened.
AUCTIONING, DEVELOPING and CONFIRMED are ALL reachable at BIRTH, from A4's entry
environment, which the ladder never evaluated; a phrase describing a transition that
did not occur is simply false. The birth justification is not lost by the silence: the
at-signal block already carries `Birth:<rung>` and the whole `Env:` line, which IS
A4's rule in its own terms, so a birth phrase would restate a reading already on
screen — the duplication §7.5.7 rejects for the blocker list.
📌 AUCTIONING IS THE DELIBERATE EXCEPTION and must NOT be silenced by that rule.
Nothing ever transitions INTO it, so a strict reading would delete its phrase
entirely — but that phrase is a BLOCKER statement wearing "because" clothing, and at
the birth rung the blocker is the whole story.

• **A "because" clause about a TRANSITION is PAST tense and must be LATCHED.** The
statuses keep being re-classified after a transition (STALLED freezes the rung,
not the components), so re-deriving the cause at display time produces a sentence
about a different bar. `stallWhy` and `devWhy` exist for this, and where the cause
pins the status word the clause states the CONSTANT. ⚠️ The tense is load-bearing,
not cosmetic: present tense beside a live "but" clause about the same component
produces a sentence that contradicts itself in a reachable, ordinary state.
📌 AUCTIONING's "because" is the exception and is correctly PRESENT — it is the
birth rung, so it describes an ongoing absence rather than a transition.
• **"but" describes what blocks the NEXT rung — PRESENT tense, so it must be LIVE**,
and each clause must be the exact negation of its conjunct.
Both defects that came from getting this backwards were reachable and silent, and both
are described at the clauses below.
  Parameters:
    s (ParticipationState): The signal state, already classified by advance() this bar.
  Returns: A FRAGMENT, not a sentence — no subject, no leading capital, no trailing stop, and ""
where there is nothing to say. It names the rung's CAUSE and its blocker only, because
liveTip appends it directly after `State:<rung>,` which supplies the subject. That is
an amendment to §7.5.7, whose table is written in full sentences: the rung word and
"because" were pure duplication of an item two characters to the left, and this phrase
was ~30% of the whole per-signal block.
⚠️ IT IS THEREFORE POSITION-DEPENDENT. Rendered anywhere but immediately after the
rung it loses its subject, and the born-DEVELOPING case — which returns a bare
"but …" — becomes ungrammatical rather than merely terse.

atSignalText(states, bar, bull, fam, famAlso)
  The at-signal block for the VERBOSE label — two lines, and deliberately only two.
§537's verbose defaults are the state, the birth state (identical at birth) and the
first magnet flagged as the minimum expected adverse excursion; everything else is
tooltip-only by nature. THE COMPACT LABEL GETS NOTHING — that is the standing
commitment made to the client on 2026-08-08, and it is why this is a separate
function rather than a verbosity flag on atSignalTip.
The magnet names the level's TYPE, per his 2026-08-10 auditability request.

ON A MERGED LABEL THE RUNG IS EMITTED ONCE, and that is provable rather than merely
usual: every signal firing on one bar takes the SAME entry (the host resolves one
`_entry` local for the whole trade-opening block) and every A1 reading is bar-level,
so all five rung clauses evaluate identically for every state on the bar. ⚠️ If entry
ever becomes per-branch, this has to go back to one rung line per signal.
What is NOT shared is the min-AE row: it comes from the stop-side scan, and the stop
differs per signal type. So one rung, N magnets.
  Parameters:
    states (array<ParticipationState>)
    bar (int): The bar whose signals to render — the host passes bar_index.
    bull (bool): Direction to render; a bull label must not pick up a bear state fired on the same bar.
    fam (int): Family this label was created for; see renders().
    famAlso (int): Second family the label carries (merge only). Defaults to -1 — "this label carries
one family" is the overwhelmingly common case, so only the host's merge targets
pass it. Verified on TradingView 2026-08-12 that an export takes a parameter
default; it is not something to rediscover.

atSignalTip(states, bar, bull, fam, famAlso)
  The at-signal block for the TOOLTIP — the full A1 / A2 / A3 readout.
Everything past the label's two lines lives here because the label is on-chart and
chart clutter is the client's open complaint, while a tooltip is invisible until
hovered. The corridor enumeration is his 2026-08-10 request and names level types
for the same auditability reason.

An unknown environment SAYS SO. Reading a na cumRVOL as "not participating" would be
a wrong answer dressed as a real one — it means a volume-less symbol or the first
session of loaded history, not a quiet market.

THE BODY IS SPLIT INTO SHARED AND PER-SIGNAL HALVES, because a merged label carries
more than one signal and most of this block is identical across them:
SHARED (emitted once) — the rung, the whole A1 environment line, the sessions
line. All of it is bar-level or derives from the entry,
and every signal on one bar shares one entry (see
atSignalText for why that is structural, not incidental).
PER-SIGNAL (emitted N×) — origin, the corridor enumeration and every A2 descriptor.
All of these hang off the STOP, which differs per signal
type, so they are the only things that can disagree.
⚠️ `origin` belongs to the per-signal half and used to share the "Birth:" line with
the rung, which does not. Splitting that line is the whole point; do not rejoin them.
  Parameters:
    states (array<ParticipationState>)
    bar (int)
    bull (bool)
    fam (int): Family this label was created for; see renders().
    famAlso (int): Second family the label carries (merge only); defaults to -1. See atSignalText.

bindLabel(states, bar, bull, fam, famAlso, lbl, baseTxt, baseTip)
  Bind a freshly created signal label to the state(s) it renders, and latch the base
strings the live rewrite appends to.

⚠️ THIS CANNOT BE DONE ANYWHERE ELSE, and the two obvious places both fail. The
handle does not exist until label.new RETURNS, so sigLabelText/sigTooltip cannot do
it; and by the time par.advance runs, the host's finalisation blocks have already
pushed each pending handle into its history array and nulled the pending variable.
So binding happens between creation and finalisation, at the ten label sites.

⚠️ THE BASE STRINGS ARE PASSED, NOT READ BACK. Pine has label.get_text but no
label.get_tooltip, so the tooltip half could not be recovered from the handle even
if the text half could. The host holds both in locals it already computed for
label.new, so this costs no extra work — only the argument.

⚠️ RE-BINDING EVERY TICK IS CORRECT, NOT WASTE. The pending renderers are
deliberately not isconfirmed-gated: on a realtime bar rollback deletes the label and
the block creates a fresh one each tick, so the handle churns. It is harmless
because parStates is a var array and the handle is rolled back and re-bound in the
same pass — but a "first tick only" guard would leave the state pointing at a
deleted label for the rest of the bar.

A na handle is a no-op, so a bar with no signal costs nothing.
  Parameters:
    states (array<ParticipationState>)
    bar (int): The bar whose states to bind — the host passes bar_index.
    bull (bool): Direction; a bull label must not bind a bear state fired on the same bar.
    fam (int): Family this label was created for; see renders().
    famAlso (int): Second family the same label carries — the inflection MERGE, and the only case
where this call binds two states. -1 elsewhere.
    lbl (label): The handle label.new just returned.
    baseTxt (string): The label's creation-time text — arrow, score, at-signal block.
    baseTip (string): The label's creation-time tooltip. label.set_text REPLACES, so the rewrite is
always base + block and both halves have to be kept.

render(states, p, oldestOR, oldestVel, verbose)
  The live write pass, and the prune. Rewrites every rendered signal's label text and
tooltip from current state, then removes the states that have resolved.

⚠️ THREE TRIGGERS, AND ONLY ONE OF THEM SURVIVES ON HISTORY. `barstate.islast` fires
on a single bar, so a signal that resolved earlier is never revisited by it — the
RESOLUTION write is the only one that leaves a permanent record.

⚠️ `barstate.islastconfirmedhistory` IS THE THIRD, AND IT EXISTS TO PAY FOR THE HOST'S
`isconfirmed` GATE. Every value this function renders is either latched at seed or
written on a confirmed bar — `advance` for all of it, `par.step` for S2's window,
inside its own isconfirmed block — so NOTHING here can change within a bar, and
running per tick reproduces identical strings. The host therefore calls this once per
bar. But `islast` on a live chart is the FORMING bar, so an isconfirmed gate alone
would leave a live signal with no live block until that bar closed — and since every
input change forces a full recalculation, that is not a rare "fresh load" case, it is
every settings tweak. This trigger fires on the last SETTLED bar (the one before the
realtime bar when the market is open, the dataset's last when closed), which the
historical pass always reaches, so the block is present the moment a recalculation
finishes, carrying the same confirmed-bar content the per-tick writes used to.
⚠️ `barstate.isnew` was considered and rejected: the reference defines it as the first
update of a *newly generated* real-time bar, which a mid-bar recalculation's existing
bar is not — so it would probably not fire in the one case that matters, silently.

⚠️ NO PER-BAR DEDUP GUARD, and that is a decision rather than an omission. SCOPE
specifies a `lastWriteBar` int; rollback defeats it. These states live in a var
array, so Pine reverts the field on every realtime tick and the guard reopens — it
can block nothing, ever. Historical bars execute once anyway, and the writes are
idempotent, exactly as the status table's per-tick render already is.

⚠️ `na(s.lbl)` DOES NOT CATCH A DELETED LABEL, and states routinely outlive their
labels. Signal labels are pruned when their x falls behind a retention threshold, and
`i_pastORs` defaults to 0 — so every session deletes the previous session's OR-keyed
signal labels, while the tracking cutoff defaults to 24 market hours. A live state
whose label is gone is the NORMAL case, and a deleted handle is NOT na. The test is
therefore on `seedBar`: bit for bit the host's own prune test, since a signal label's
x IS its entry bar, but as an integer comparison that never touches a possibly-dead
drawing.

⚠️⚠️ THE THRESHOLD IS PER FAMILY, AND A SINGLE ONE IS A REACHABLE SILENT DEFECT.
`oldestORIdx` prunes six label arrays (continuation, AC, inflection × 2 directions),
but VELOCITY LABELS HAVE THEIR OWN RETENTION RULE — host [7958-7974], keyed on
`velZoneIdx` and governed by `i_velZoneRetention`, DEFAULT 20, against `i_pastORs`
DEFAULT 0. So `seedBar < oldestOR` does not mean "this state has no label"; it means
that for eight of the ten bind sites and is false for the two velocity ones, at the
shipped defaults, by many sessions. Both directions of getting this wrong are silent:
a live velocity signal whose label is VISIBLE stops updating and never receives its
outcome suffix, or — with the retentions set the other way — a write reaches a DELETED
velocity label, which is the one thing the integer comparison exists to avoid.
📌 `famIdx` is assigned by the HOST at the ten parSeed sites (0 cont · 1 AC · 2 infl ·
3 vel), so the literal 3 below is bound to those and to famName's own 3. A library
const would not single-source it — the host's literals are the source.

⚠️ THE STATE IS NOW EVICTED, NOT MERELY LEFT UNRENDERED — this reverses the earlier
rule ("the state keeps being MEASURED; only rendering stops"). Nothing can observe a
state whose label is gone: the write pass skips it, `renders()` keys on
`seedBar == bar_index` and so can never reach one, and there is no telemetry, alert or
retained result array — the LABEL IS THE RECORD. Its only remaining consumer was
rollUp's `Live` count, which it inflated with entries the user cannot inspect
anywhere. So it is resolved as OUTCOME_GONE and pruned, and the per-bar engine cost it
was paying — the whole episode state machine over its `levels` array — stops.
⚠️ IF TELEMETRY IS EVER BUILT, revisit: at that point the outcome becomes readable.

⚠️ EVICTION MARKS, IT DOES NOT REMOVE — so the write gate STILL CARRIES THE AGE TEST,
and dropping it as "now dead code" is a mistake this function was one review away from
shipping. The eviction block sets `resolved` and hands the state to the loop, exactly
as C10's single-eviction-path design requires; removal happens in that loop, AFTER the
write. An aged state therefore reaches the gate with `resolved` true and a non-na but
DELETED handle. Both sites call `agedOut` so there is one definition of the question.

⚠️ ON A MERGED LABEL ONLY THE LEADER WRITES, so whichever of the pair resolves
first would take its final readings out of the array before any write could show
them — and every later write of that label would then omit it. **The departing state
is frozen into the SURVIVOR's base on its way out**, in whichever direction it goes:
the base is what every rewrite is built on, so a resolved partner's last state
becomes permanent exactly as its at-signal block already is, while the survivor goes
on evolving beside it.
⚠️ AND WHEN THE DEPARTING STATE IS THE LEADER, THE SURVIVOR IS PROMOTED. Freezing
alone is not enough there: the write gate skips followers, so an un-promoted
survivor would keep being measured while nothing rendered it, and its own resolution
would never reach the chart — it would read "(—)", still running, for ever. ⚠️ THAT
IS EXACTLY WHAT AN EARLIER VERSION OF THIS FUNCTION DID, because it implemented the
follower→leader freeze and not its mirror. Both directions or neither.
📌 One cosmetic consequence, deliberately not chased: the frozen block always
precedes the survivor's live one, so the two swap order when it is the follower that
leaves. Order carries no meaning — seed order already decides which of the pair
leads — and both blocks stay prefixed with their own family for the label's life.
  Parameters:
    states (array<ParticipationState>): The host's var array. Mutated: resolved states are removed here.
    p (ParState): Producer state — S2's ratio and the S4 thresholds are read from it.
    oldestOR (int): Oldest retained OR bar index — the label-retention threshold for every family
EXCEPT velocity. -1 when none is registered, which disables the test by
construction: seedBar is a bar_index and therefore >= 0, so `seedBar < -1` is
always false and nothing is evicted on age. Correct, because with no OR
registered no label has been pruned. ⚠️ -1 and NOT na — `seedBar < na` is na,
which is falsy, so an na sentinel would appear to work for the wrong reason.
    oldestVel (int): The same, for VELOCITY signals only — host `velZoneIdx`, governed by
i_velZoneRetention. Same -1 semantics.
⚠️ EITHER THRESHOLD MAY LAG, AND LAGGING IS SAFE IN ONE DIRECTION ONLY. Each
stands in for "which labels have actually been deleted"; if it ever disagrees it
must disagree toward an OLDER (smaller) bar index, never a newer one. Too old
means a ghost survives one extra bar — harmless. TOO NEW WOULD EVICT A STATE
WHOSE LABEL IS STILL ALIVE. ✅ Both are exact rather than merely safe today,
because each host prune shifts its index array and deletes its labels in one
block, and both blocks run above this call under the same isconfirmed gate. The
velocity block additionally sits under `velReady and velEngineOn`, so with the
engine off it never fires and the threshold stays at its oldest registered zone
or at -1 — the permissive value, and correct, since no velocity labels are being
deleted either. ⚠️ Do not delete this note on the grounds that the values are
currently exact; it is the guarantee if that adjacency is ever broken.
    verbose (bool): i_sigVerboseLabels. False — the default — makes this tooltip-only.

parTableRows(units)
  Parameters:
    units (int)

parTableCols()

renderParticipation(t, c, p, states)
  Render the participation table: the always-on environment block.
Fixed content height by construction — the readings do not grow with trades — which
is what makes a top anchor viable at all.

Layout, 4 columns × (pad + 8) rows:
(blank offset rows — c.vertOffset, see parPadRows)
Cum RVOL | 0.98 |
Day range | 25.7% ADR | ADR 91.50
VWAP | above | falling
| Asia | London | New York ← header
Rng % ADR | 23.5% ▲ | ● 15.6% ▼ | —
Efficiency | 53.5% | — | —
Live | 3 live · 1 AUCT · 2 DEV
Tracking | to TP1 | 24h cutoff ← M11-3

THE PAD ROWS ARE NOT CONTENT. They clear TradingView's symbol/OHLC legend, which
runs along the top of the pane at exactly the height a top anchor would otherwise
occupy — and the table draws OVER it, so without them the legend text is unreadable.
The fix has to be vertical: the legend's width changes with the symbol name, so
narrowing the table would only postpone the collision to a longer ticker.

The block was a single unconditional row until M11-1 feedback. It is now sized by
the host (M11-1 patch, 2026-08-06), because how much has to be cleared is not
knowable from inside Pine: the legend's height depends on the user's chart font, on
how many indicators share the pane, and on Chart Settings → Status Line → Indicator
Inputs. The symbol/OHLC line cannot be hidden at all, so at a top anchor an offset
of zero WILL sit under it — that is a reachable state by choice, not an oversight.

The sessions own COLUMNS rather than rows, which is what buys back the row the
spacer costs and puts them side by side for the question that block answers — who
spent the day's range. The three rows above them use only columns 0–2 and leave
column 3 unwritten, so they end ragged instead of trailing empty boxes.

On a symbol with no volume feed the Cum RVOL and VWAP value cells read
"No volume data available" instead. The other rows are pure price and keep working,
which is why this does NOT blank the whole table the way the debug table's
noVolData branch does.

The two standalone rows use words rather than symbols ("above", "falling") so they
describe themselves without needing headers of their own — an arrow next to VWAP
would be ambiguous between "price above VWAP" and "VWAP rising".

The host gates the call on its show toggle, a non-na table and barstate.islast;
the internal guard is kept as defence. islast, not isconfirmed: this is a live
environment read and immediacy is the point, matching the status table.
THE TWO M11-3 ROWS ARE NOT MEASUREMENTS OF THIS BAR, and each is here for its own
reason. The roll-up is a fleet view — how many signals are live and at which rungs —
which is the one participation question no per-signal label can answer. The
tracking row is CONFIGURATION: the verdict target and the cutoff are inputs,
identical on every signal, and changing either recalculates the whole script, so no
signal can diverge from what this row shows. Repeating them on every signal's
tooltip would cost characters on the surface §7.6.2 is trying to keep short.
  Parameters:
    t (table): The participation table (host-owned; created via table.new).
    c (ParticipationConfig): Styling, inherited from the host's status-table controls.
    p (ParState): The producer state this library filled in.
    states (array<ParticipationState>): The live signal states, for the roll-up row. Read only — the prune belongs to
render(), which the host calls above this.

SessionLedger
  One main trading session's running summary.
  Fields:
    open (series float)
    high (series float)
    low (series float)
    close (series float)
    rng (series float)
    extPct (series float)
    eff (series float)
    dir (series int)
    seenAt (series int)
    active (series bool)

ParState
  Pre-signal producer state (P1–P5, P8). One object, so the host pays one declaration.
  Fields:
    bktTypical (array<float>)
    bktToday (array<float>)
    sess (array<SessionLedger>)
    sessOpen (series int)
    bkt (series int)
    barsInSess (series int)
    sessCount (series int)
    cumVol (series float)
    cumTyp (series float)
    barRvol (series float)
    cumRvol (series float)
    expVol (series float)
    s2Recent (series float)
    s2Prior (series float)
    adr (series float)
    dayUsed (series float)
    vwap (series float)
    vwapSlope (series float)
    dayHi (series float)
    dayLo (series float)
    live (series bool)
    lastStepBar (series int)
    snapCumRvol (series float)
    snapVwap (series float)
    snapSlope (series float)
    snapDayHi (series float)
    snapDayLo (series float)
    snapDayUsed (series float)
    snapAdr (series float)
    cfgRvolAdequate (series float)
    cfgRvolStrong (series float)
    cfgDayExtended (series float)
    cfgChaseHigh (series float)
    cfgTgtName (series string)
    cfgCutoffHours (series int)
    cfgPatienceMins (series int)
    cfgOiFloor (series float)
    cfgS1Band (series float)
    cfgS2Building (series float)
    cfgS2Fading (series float)
    cfgS4Hold (series int)
    cfgS4Reject (series int)
    cfgDecayN (series int)

ParticipationConfig
  Participation-table styling. Every APPEARANCE field is inherited from the host's STATUS
  Fields:
    txtColor (series color)
    bgColor (series color)
    hdrBgColor (series color)
    txtSize (series string)
    show (series bool)
    vertOffset (series int)

WatchedLevel
  One structural level a signal is watching, with its price frozen at signal time.
  Fields:
    price (series float)
    baseCost (series float)
    wBase (series float)
    tag (series string)
    bornSupport (series bool)
    testing (series bool)
    oppDelta (series float)
    volAcc (series float)
    expVolAcc (series float)
    depthMax (series float)
    lastOI (series float)
    lastDepth (series float)
    tests (series int)
    apprSide (series int)

ParticipationState
  One signal's participation state. Created by seed() at the moment the signal fires, then
  Fields:
    bull (series bool)
    sigType (series string)
    famIdx (series int)
    seedTime (series int)
    seedBar (series int)
    entry (series float)
    stop (series float)
    target (series float)
    r (series float)
    origin (series float)
    originTag (series string)
    a1CumRvol (series float)
    a1DayUsed (series float)
    a1Adr (series float)
    a1EntryLoc (series float)
    a1Vwap (series float)
    a1VwapSlope (series float)
    a1EnvKnown (series bool)
    a2MagnetTag (series string)
    a2MagnetR (series float)
    a2StrongTag (series string)
    a2StrongR (series float)
    a2StrongCost (series float)
    a2SlAdjTag (series string)
    a2SlAdjR (series float)
    a2SlAdjCost (series float)
    a2SlAdjWord (series string)
    a2SlAdjSide (series int)
    a2Corridor (series string)
    a2Obstacles (series string)
    levels (array<WatchedLevel>)
    rung (series string)
    birthRung (series string)
    famMask (series int)
    cumDelta (series float)
    cumVol (series float)
    proxyDelta (series bool)
    s3Tally (series float)
    recTag (series string)
    recSide (series int)
    recOI (series float)
    recD (series float)
    recE (series float)
    corrTag (series string)
    corrSide (series int)
    corrOI (series float)
    corrD (series float)
    corrE (series float)
    recBar (series int)
    recWBase (series float)
    corrBar (series int)
    corrWBase (series float)
    s4Run (series int)
    s1Stat (series string)
    s2Stat (series string)
    s3Stat (series string)
    s4Stat (series string)
    s2Ok (series bool)
    stallWhy (series int)
    devWhy (series int)
    patMult (series float)
    barsAtRung (series int)
    decayRun (series int)
    devAt (series int)
    confAt (series int)
    stallAt (series int)
    runLow (series float)
    runHigh (series float)
    outcome (series string)
    lbl (series label)
    isFollower (series bool)
    leadFam (series int)
    merged (series bool)
    baseTxt (series string)
    baseTip (series string)
    resolved (series bool)

Haftungsausschluss

Die Informationen und Veröffentlichungen sind nicht als Finanz-, Anlage-, Handels- oder andere Arten von Ratschlägen oder Empfehlungen gedacht, die von TradingView bereitgestellt oder gebilligt werden, und stellen diese nicht dar. Lesen Sie mehr in den Nutzungsbedingungen.