bin/Custom/Indicators/Council_v1_11_0.cs
| Family | Indicators |
| Version | 1.11.0 |
| Size | 2067 lines |
| Scope | public — ships in sentinel-suite |
| Class | Council_v1_11_0 |
| Namespace | NinjaTrader.NinjaScript.Indicators.Sentinel |
| Publishes seams | CouncilState |
| Consumes seams | AdxState, AdxvmaState, BrickState, CciState, ClockState, CompressionState, ConvictionState, CvdState, EnvelopeState, ExhaustionState, FlowState, FluxState, GodReversalState, HarmonicState, IntermarketState, LevelState, LiquidityState, MtfState, ParticipationState, PressureState, ProfileState, RegimeState, SarState, StfState, StructureState, SuperTrendState, TrendState, TrendArchitectState, VidyaState, WaeState, ZScoreState |
| Documented by | SENTINEL_ML_SPEC, SENTINEL_THESIS |
| Depends on this | SentinelBridge_v0_2_0.cs, SentinelCockpit_v0_1_0.cs, SentinelExcursionRecorder_v2_0_0.cs |
Rendered from the published copy in
sentinel-suite/src/, not the author’s private tree — so this page describes the file you actually have.
Council — the Sentinel CONFLUENCE ARBITER ("the brain") | Version v1.11.0
File: Council_v1_11_0.cs | namespace …Indicators.Sentinel | display Name "Council"
⚠ NO ORDERS — read-only advisory indicator. It DECIDES, it never trades. Safe to run anywhere.
WHAT THIS IS — the missing brain of the suite. Every Sentinel sensor already PUBLISHES its own
opinion to SentinelCore as a "…State" seam (Trend, ADX, CCI, VolEnvelope, Liquidity, Brick) plus
the Eye's qualification verdict. Until now each consumer hand-consulted one or two of those ad hoc.
The Council FUSES them all into ONE explainable per-instrument verdict and publishes it back as
SentinelCore.CouncilState so ANY consumer (GTrader21 / Bridge / Deck / Copier / strategies) reads
the SAME decision instead of re-deriving confluence.
THE VERDICT (SentinelCore.CouncilState, SentinelCore ≥ v1.7.0):
• Bias -1/0/+1 the fused direction (0 = no edge / vetoed)
• Conviction 0..1 how ALIGNED the DECLARED voters are (1 = every declared voter unanimous; 0 = split /
none / vetoed). ⚠ v1.1.0: the denominator is the DECLARED weight, not the weight that
happened to show up — a MISSING or NEUTRAL voter now DILUTES conviction instead of
vanishing from the denominator. A verdict fused from 2 of 10 voters can no longer
read as near-unanimity.
• SizeMult 0..1 suggested size multiplier = Conviction × contextMult. 0 when vetoed, when Bias is 0,
or when Conviction < ConvictionFloor. ⚠ v1.2.0: ALL context damping (Clock · MTF ·
Participation · Location · squeeze · breadth) lives HERE, not in Conviction. The floor
gates on AGREEMENT; a poor context makes the trade SMALLER, never silently absent.
• Agree/Disagree/Voters the tally, and a compact Reasons string — the AUDIT of WHY it decided
HOW IT FUSES (weighted vote — the weights ARE the edge; tune them, then let Lens grade them):
Each sensor with a FRESH reading casts a signed vote (+1/-1) with a weight; a stale/absent sensor
simply ABSTAINS (fail-open, matching the suite). netScore = Σ(vote × weight). Bias = sign(netScore)
past a deadband; Conviction = |netScore| / denomW, where denomW (v1.3.0) is KIND-AWARE: STATE voters
always count toward it (neutral is a real reading), a TRIGGER counts only when it fired or is absent —
a quiet trigger is absence of evidence, not evidence against. Breadth/squeeze/context damp the SIZE,
not the agreement (v1.2.0).
⚠ These price-derived sensors are NOT independent — they largely echo the same OHLC. Conviction is
"agreement," which is not the same as "confirmation." The verdict gets genuinely smarter only as the
ORTHOGONAL axes land (Clock/Location/Participation/MTF/Internals/Event — see Docs/ROADMAP.md).
HARD VETOES (account-free; each zeroes conviction + names itself in VetoReason):
global kill · scoped (per-root) kill · rollover block · news lockout · an absorption WALL blocking
the intended side (LiquidityState.BlocksEntry). These mirror what CanEnter bundles so the published
verdict never reads "LONG 0.9" while news lockout is live. The Council is ADVISORY — a consuming
strategy STILL calls its own SentinelCore.GateEntry at submit; the veto here shapes the ADVICE.
SENTINEL WIRING (Docs/SENTINEL_DESIGN_SYSTEM.md):
• PUBLISH: SetCouncilState(...) each update (default ON — the Council exists to publish).
v1.15.0 SCOPE KEYING (in-place, exec-plan 1.2): publishes under a SCOPE ("GC.69697v6" =
instrument x bartype), NOT a bare instrument, so two GC charts on different bar types no longer
overwrite each other's verdict on every tick. Also stamps BarTimeUtc + IsHistorical — UpdatedUtc
is wall-clock even during historical replay, so IsHistorical is the ONLY way a consumer can tell
a replayed verdict from a live one. The fusion math is untouched.
• PLOTS: hidden transparent "Bias" (±1) + "Conviction" (0..1) so Deck SIGNAL ARM / strategies can
read the verdict as a generic PLOT (the suite signal-exposure convention), no drawing-scrape.
• A SentinelSkin.Painter glass card (CardLayout-docked) + Sentinel palette + label remover.
• Records verdict CHANGES to sentinel.log ("Council"); the per-FIRE record belongs to the strategy
(write CouncilState into the Ledger/Log ctx at fire time so Lens can grade the confluence).
CHANGELOG
v1.11.0 (REAL FORK, 2026-07-29) — ⭐ A CHART-DERIVED SENSOR MUST BE ON THIS CHART TO INFLUENCE IT.
THE BUG, as the user put it: "a standard OHLC sensor like Stochastic Triple Filter is voting on
Council without being on the chart. It is not weighted and poisoning the Council."
Sensor seams are process-global and keyed BARE (instrument + bar type). That is CORRECT for a value
that does not vary with the consuming chart — but an OHLC sensor's reading is computed from ITS OWN
chart's bars, so an STF on any other GC/TBars chart was feeding EVERY Council on that instrument+
bartype. And the bite was not the vote: `VetoOnChop` hangs off `stf != null` and is INDEPENDENT of
WeightStf, which defaults 0 — so a sensor you never attached, at weight zero, could zero your
conviction and your sizeMult. Stated intent had always been narrower: the v1.3.1 changelog says the
veto is live "only when the STF sensor is loaded", meaning *on this chart*; the code meant *anywhere*.
⚠ AUDIT FIRST — IT WAS NEVER AN STF BUG. Every weight-independent path was checked, and the class is
SIX: VetoOnChop (STF) · VetoOnWall (LiquidityWalls) · VetoKillWindow (Clock) · DampenOnSqueeze
(VolEnvelope) · FluxAbsorbDamp (Flux) · the whole contextMult chain (Clock/MTF/Participation/
Location/Profile/Regime). Gating the veto on the weight — the obvious one-line fix — would have
repaired ONE ROW of that table and made the rest look deliberate.
THE GATE IS CLASSIFICATION, NOT PRESENCE-FOR-EVERYTHING (the user's distinction, and it is the right
one — "sure there is a use case for having a voter influence the Council and not be on that specific
chart, but indicators like STF should not be allowed to do it"):
• CHART-DERIVED (20: TRND CCI ADX ENV CMP WAE GREV STF STRC EXH AVMA SPRT PSAR ZSC VDYA HARM ARCH
RGME PARTIC MTF) → must be ATTACHED to this chart. No attach ⇒ no vote, no veto, no damp.
• INSTRUMENT-LEVEL (Clock · Intermarket · News · LiquidityWalls · CVD · BSP · Level · Profile) →
never gated. They have no chart of their own; requiring attachment would be meaningless.
• BAR-TYPE SEAMS (BRK / FLUX / CVB) → exempt. The chart IS that bars type.
⭐ IMPLEMENTATION: the gate wraps the SEAM READ (`Local("STF", GetStfState(...))`), not each
modulator. The vote, the veto and the damp all hang off the same `!= null`, so nulling the read
closes all three — one mechanism for the whole class, which is exactly what the audit demanded.
Presence is scanned on the UI THREAD (ChartControl.Indicators off the data thread throws — memory
nt-consume-indicator-plots), async, 5 s throttle, matched by type-name PREFIX so a sensor version
bump does not silently un-gate it. ⚠ "SentinelTrend" needs its trailing underscore or it also
prefix-matches SentinelTrendArchitect.
FAIL DIRECTIONS, both deliberate: no ChartControl at all (headless / offline harness) ⇒ attachment
is UNDEFINED, not false ⇒ fail OPEN, because muting a Council that has no chart to be attached to is
the "crashed sensor is indistinguishable from a quiet one" failure — and it keeps the replay==live
parity gate intact. Scan not yet completed ⇒ fail CLOSED for a beat, so an off-chart sensor cannot
sneak one bar in; the scan lands within a bar and the skip is NAMED.
OBSERVABILITY: everything gated out is listed in the Reasons audit as `off-chart:STF,ENV` — without
it a declared-but-unattached voter looks identical to an attached one that is abstaining, and "why
didn't it fire" is unanswerable. New [Display]-only "Require sensor on chart" (default ON).
⚠ REAL FORK, not in-place: file + class + Name + header all move to v1.11.0 together. This also ends
a standing lie — the file said Council_v1_0_0 and the card said "Sentinel Council v1.0.0" while the
code had been v1.10.0 since 2026-07-24. Council_v1_0_0.cs stays FROZEN as the fallback.
⚠⚠ REMOVE the old Council from a chart BEFORE adding v1.11.0. Both publish CouncilState on the same
scope, so running them together is a SCOPE CONTENTION (last writer wins, alternating per bar).
v1.10.0 (in-place, 2026-07-24) — DECOUPLED vs ABSENT (Core >= v1.40.0 beacon). A missing BAR-TYPE voter
(BRK/FLUX/CVB) now asks SentinelCore.BeaconForeign whether another ASSEMBLY GENERATION is still
publishing it. If it is, the sensor is not absent — it is DECOUPLED, and the fix is an NT RESTART.
WHY: an F5 rebuilds every indicator but NOT a chart's bars-type instance, so the surviving bars
type publishes into the OLD assembly's static seam store while this rebuilt Council reads the NEW
one. The write succeeds into a store nothing reads: guards pass, scope resolves, nothing throws,
and BRK/FLUX/CVB simply never appear. That silently produced the 2026-07-23 audition corpus —
1,866 rows, ZERO bar-type voters, brkUpper/brkLower all 0 — and survived a chart reload, which
is why "reload the chart" was twice believed to be the fix and twice wrong (measured 07-24).
Reported via Conditions (debounce/re-state/auto-clear) and evaluated OUTSIDE the roster-change
gate, because a latch that never re-arms is indistinguishable from a detector that never fires.
v1.9.0 (in-place, 2026-07-23) — LIVE CONFIG RELOAD + Lane.conf CASCADE (Core >= v1.39.0). Roster.conf and
Lane.conf are now POLLED on write time (>=2s, same idiom SentinelSkin uses for cards.off/theme.txt)
and re-applied on the next bar, so changing a floor / veto / voter weight needs NO F5 and NO chart
reload. Both are re-applied together (a lane profile writes over F6 properties, so a roster-only
change must not leave a half-applied state) and the winning paths are LOGGED. Core's LaneIO also
gained the scope-instrument-global cascade RosterIO always had: a chart on a bar type nobody had
used before used to find no Lane.conf, silently keep its F6 floor and record almost nothing --
indistinguishable from every sensor being dead. That asymmetry is what made 'new chart, pick a bar
type, load the template, run' impossible. One Models\<INST>\Lane.conf now covers every bar type.
v1.8.4 (in-place, 2026-07-23) — VetoOnWall: an explicit ON/OFF for the liquidity-wall veto + the
`vetoonwall` Lane.conf key. v1.8.3 tried to disable that veto from the lane with
`wallnearticks = -1`; NT REJECTED IT AT LOAD -- WallNearTicks is [Range(0, double.MaxValue)],
so the chart threw "Value of property 'WallNearTicks' ... is -1 and not in valid range".
And 0 is not off either: BlocksEntry tests `dist <= ticks`, so 0 vetoes whenever a wall sits
exactly AT price. A distance knob simply HAS no disable value -- the other two hard vetoes
(VetoOnChop / VetoKillWindow) each carry a bool and this one did not. Now it matches them.
LESSON: check a property's Range/validation attributes before choosing a sentinel value; a
value that is meaningful in the comparison can still be illegal at the property boundary.
v1.8.3 (in-place, 2026-07-23) — HARD VETOES ARE LANE-SETTABLE. ApplyLaneProfile now also reads
`vetoonchop` / `vetokillwindow` / `wallnearticks` / `minvoters` from Lane.conf (same sparse
"absent ⇒ inherit F6" contract as every other key; MinVoters via TryDouble→(int) so SentinelCore
needs no new API). WHY: a modulator DAMP scales a recorded verdict, but a HARD VETO deletes it —
SentinelExcursionRecorder's gate is `aligned = Bias!=0 && (RecordBelowFloor ? !v.Vetoed : HasEdge)`,
so a vetoed bar never enters the corpus. With VetoOnChop on, the 22-voter sensor AUDITION was
silently dropping every chop-regime bar — it would have graded the arsenal on trend-only data and
reported nothing wrong. Lane.conf being sparse by design covered the damps but not the vetoes.
Found by reading a live roster line on legacy-node (a load burst, every verdict "VETO:chop 66") rather
than by reasoning about the config. Also bumps CouncilVer 1.8.1→1.8.3: the const had never been
bumped for v1.8.2, so recorded rows were stamped cnclVer "1.8.1" and the corpus provenance lied.
v1.8.2 (in-place, 2026-07-20) — new CVB voter (STATE, orthogonal/order-flow). Consumes SentinelCore.ConvictionState
(v1.37.0) — SentinelDrift's (bar type id 212204) FLOW-CONFIRMED trend direction: the brick direction voted
only when the aggregated tape confirms it. Bar-type seam → BARE scope. KnownVoters + AddVote + BaseWeight +
WeightConviction ([Display]-only, region-safe) + SetDefaults + Reasons (`cvb▲/▼`). No fusion/damp change.
v1.8.1 (in-place, 2026-07-18) — FUSION-CORE REWIRE. The inline fuse block (kind-aware denomW · deadband→bias ·
conviction · the full context-damp chain: breadth·squeeze·clock·participation·MTF·location·profile·
regime·flux-absorb) now DELEGATES to AddOns/CouncilFusion.Fuse — the single pure fusion truth shared
with the (coming) replay HARNESS, so a replay-baked verdict == the live verdict on that bar (the
correctness gate, Docs/SENTINEL_REPLAY_SPEC.md §4-5). BEHAVIOUR-PRESERVING: identical math, verified
bit-for-bit (AddVote accumulation ≡ Fuse's recompute; _declaredW ≡ Fuse's denom; damp chain identical;
squeeze damp = 0.6 as before). Modulator seams now read UNCONDITIONALLY (Fuse owns the bias-gating);
the bias-dependent liquidity-WALL veto is resolved AFTER Fuse (zeroing size+conviction, bit-identical
to Vetoed=true). CouncilFusion.cs brought from v1.3.x → v1.8.0 parity (added Profile/Regime/Flux-absorb
+ their damps). VERIFY UNCHANGED via a Market-Replay verdict-diff before trusting a replay bake.
v1.8.0 (in-place, 2026-07-14) — PER-LANE SYSTEM PROFILE (Core ≥ v1.33.0). On load, applies the lane's
Sentinel\Models\<inst>\<bartag>@<lane>\Lane.conf OVER the F6 fusion knobs (ConvictionFloor, bias
deadband, the 6 context-consult toggles, the modulator damps) — SPARSE: only keys present override,
absent keys inherit F6. So an A/B test LANE pins its own decision knobs (not just its roster) without
hand-editing F6 per chart. Roster.conf = voters+weights+kind; Lane.conf = the rest. (System Builder §14.7.)
v1.7.0 (in-place, 2026-07-14) — PER-CHART LANE (Core ≥ v1.32.0). New "Scope Lane" property: set a distinct
value (A/B) on each of two charts that share instrument+bartype+size so their scopes diverge
(GC.212202v6x24@A / @B) instead of clobbering — fixes SCOPE CONTENTION for same-bartype test charts.
Registers the lane (keyed by ChartControl) in DataLoaded so every SENSOR on the chart inherits it;
publishes CouncilState + reads sensor seams on the LANED scope, but reads BAR-TYPE seams (BRK/FLUX)
on the BARE scope (a bars series is shared across charts). Blank lane = bare scope (back-compat).
⚠ Needs the sensor batch (each sensor → ScopeOf(…,ChartControl)) for a laned chart to see its voters.
v1.6.3 (in-place, 2026-07-14) — FLUX voter + absorption modulator (Core ≥ v1.31.0). FLUX = SentinelFlux's net
ORDER-FLOW direction from the imbalance-driven bar close (a STATE voter, w0.7) — the suite's one
order-flow-SUBSTRATE axis: the whole chart clock is flow-synchronized, so it is orthogonal to the
price bloc (TRND/CCI/ADX/ENV all echo the OHLC). Its flow-vs-price DIVERGENCE drives a soft SIZE damp
(FluxAbsorbDamp, default 0.6) when the tape absorbs against the bias — the tape-sourced complement to
the LiquidityWalls book veto. KnownVoters + BaseWeight + AddVote + WeightFlux/FluxAbsorbDamp props +
audit (flux▲/▼/absorb). DefaultKind default already State. Council now fuses 22 voters. Additive only.
v1.6.2 (in-place, 2026-07-12) — TWO NEW VOTERS (candidate-library novel-signals pass; Core ≥ v1.30.0). VDYA
(SentinelVIDYA Chande-CMO adaptive-MA trend — STATE, w0.5) · HARM (SentinelHarmonic XABCD pattern
completion — TRIGGER, w0.4). KnownVoters + BaseWeight + DefaultKind(HARM=Trigger) + AddVote +
weight properties. Council now fuses 21 voters. Additive only.
v1.6.1 (in-place, 2026-07-12) — ARCH voter (SentinelTrendArchitect, the MPL Pine port; Core ≥ v1.29.0). Its
composite PRISM trend + Trend-Regime-Gate publishes TrendArchitectState → a STATE voter (w0.7).
KnownVoters + BaseWeight + AddVote + weight property. Council now fuses 19 voters. Additive only.
v1.6.0 (in-place, 2026-07-12) — FOUR NEW VOTERS (candidate-library Tier-2 pass; Core ≥ v1.28.0). AVMA
(SentinelADXVMA adaptive-MA trend — STATE, w0.6) · SPRT (SentinelSuperTrend ATR trailing flip —
STATE, w0.7) · PSAR (SentinelParabolicSAR Wilder trend/stop — STATE, w0.5) · ZSC (SentinelZScore
mean-reversion — TRIGGER, w0.4). New KnownVoters + BaseWeight + DefaultKind(ZSC=Trigger) + AddVote +
weight properties ([Display]-only). Council now fuses 18 voters + 8 modulators/vetoes. Additive only.
v1.5.1 (in-place, 2026-07-12) — ROSTER I/O EXTRACTED to SentinelCore.RosterIO (Core ≥ v1.27.0; no
behaviour change). LoadRoster now delegates the file cascade + parse to RosterIO.Read (same
scope▸instrument▸global cascade, same grammar, same first-wins dedup); the private ParseRoster
was removed. The DEFAULT declaration (KnownVoters where BaseWeight>0) and all fusion math are
UNCHANGED. Purpose: ONE format owner shared with the System Builder (writer) so reader/writer
can never drift — the substrate for the roster-editor UI (Docs/SENTINEL_SYSTEM_BUILDER_SPEC.md).
v1.5.0 (in-place, 2026-07-12) — THREE NEW VOTERS + TWO NEW MODULATORS (the installed-tree port harvest;
Core ≥ v1.26.0). VOTERS: FLOW (SentinelFlow tick-rule CVD regime — STATE, the one non-price-echo
axis, w 0.9) · STRC (SentinelStructure swing HH/HL·LH/LL — STATE, w 0.7) · EXH (SentinelExhaustion
Leledc reversal — TRIGGER/mean-reversion, w 0.5). MODULATORS (size, not agreement): Profile
(price accepted inside the value area = chop → InValueDamp) · Regime (high-volatility K-means regime
→ HighVolRegimeDamp). New KnownVoters + BaseWeight + KindFor(EXH=Trigger) + Reasons audit
(in-value · hi-vol · ±flowDiv). Weights + consult toggles are [Display]-only (region-safe). The
Council now fuses 14 voters + 8 modulators/vetoes. Backward-compatible (added members only).
v1.4.0 (in-place, 2026-07-11) — PUBLISH THE DECISION VECTOR (ML spec §2.1/§2.2; Core ≥ v1.24.0). The publish
now carries the machine-rea