Sentinel Suite · NinjaTrader 8 · living document
One document that takes you from "I have no idea what any of this is" to "I can run it, extend it, and trust it." The suite turns a pile of arguing indicators into one nervous system — sensors that watch, a Council that decides, hands that act, a gate that guards, and a ledger that remembers. Everything below is pulled from the live source, not remembered.
Read 0 → III, then the lab (VIII) and the worked example (IX), then operate the safety layer (IV). Skip the code parts.
Read 0 → II for the model, then the seam reference (VII), the recipes (VII), the safety policy (IV), and trace it end-to-end in IX.
Read the lab (VIII) for how outcomes are measured, then X — what changes, what doesn't, how the offline fitting works outside NinjaTrader, and how you name your setups so the fleet stays legible.
Read 0, then IV (safety) before touching anything live. The one rule: a model proposes, the Gate disposes.
Five minutes, no risk
The fastest way to understand Sentinel is to watch it run. This is read-only — no orders, no risk. You'll put four sensors and the brain on a chart and see them fuse live.
New → Chart → GC (front month), any bar type. GC (gold) is the reference instrument throughout this manual.
Right-click → Indicators → open the Sentinel folder and add Eye, SentinelTrend, ADXPro, and WoodiesCCIPro. All publish by default — you've put four voters on the bus without configuring anything.
Add Council from the same folder. It immediately reads whatever is publishing for GC and draws a glass card, top-right.
A LONG / SHORT / FLAT pill (the fused bias), a big conviction % and size ×, a row of voter chips (green agrees, red disagrees, grey neutral), and a red VETO line when something blocks it.
As price moves, sensors republish, chips flip colour, conviction climbs when they line up and falls toward the floor when they fight. Many signals → one honest verdict. That's the whole idea in one card.
Live look
Placeholders for the screenshots that make the quickstart concrete. Capture on SIM or replay, then drop each PNG at the path shown. (For the published artifact, embed as a data URI — its strict CSP blocks external images; the in-repo .html can use the relative img/… path directly.)
The problem
A serious NinjaTrader chart ends up with a dozen indicators screaming at once — a trend line, a CCI, an ADX, a volatility band, a session clock — plus a strategy trying to trade, plus a prop firm's rules hanging over you like a guillotine. Nothing on that chart talks to anything else. The trend line doesn't know the CCI disagrees. The strategy doesn't know a news blackout starts in four minutes. Every tool is an island.
Sentinel is the plumbing that turns that pile of islands into one nervous system. It isn't one indicator — it's a design: a shared spine every tool plugs into, so they share a vocabulary, a safety layer, a memory, and a single fused opinion about what to do next.
The four jobs every trading system needs — every file in the suite belongs to exactly one of them.
| Role | The job, in plain English | Who does it |
|---|---|---|
| Signal sources | "Is there an edge, and which way?" | Trend · CCI · ADX · VolEnvelope · Compression · WAE · Eye + the orthogonal axes |
| Decision | "Given all the signals, what's the verdict?" | The Council (the brain) |
| Execution | "Put the trade on and manage its life." | Deck (manual) · GTrader21 (auto) · Bridge (auto) |
| Observation & safety | "Keep us alive, remember everything, prove it later." | SentinelCore (Gate/Ledger/Alerts) · Risk · Dashboard · Copier · Log · Lens · Arc |
SentinelCore) and read facts back off it. A sensor publishes TrendState; it doesn't know the Council exists. The Council publishes CouncilState; it doesn't know the Bridge exists. This buys fault isolation (a missing publisher just means its fact is absent — every reader treats absent = abstain), independent evolution (rewrite a sensor's internals freely, as long as it still publishes its seam), and one decision, many consumers (the Council fuses once; everyone reads the same verdict).The cast — eleven tools, one nervous system. Nothing calls another directly; each depends only on the shared spine, so any piece can be missing and the rest still runs.
The one shared dependency. Carries the kill-switch, the order gate, the ledger, and every sensor's published state.
exposes every …State seamFuses every sensor's vote into one explainable verdict — direction, conviction, size — and republishes it.
publishes CouncilStateThe autopilot. Consults the Council verdict, gates it, sizes it, fires — recording each shot to the ledger.
reads CouncilState → ordersThe human's order pad. Manual entry, plus SIGNAL ARM: read any indicator's plot and arm or auto-fire on it.
reads any plot → ordersThe one place to answer "is my brain alive, and why isn't it trading?" — a floatable rail that re-reads every seam so no card stays buried. Surfaces the decision, the gate, and the honest stale/floor reasons.
reads every …State seamRuns GodTrades across ~24 bar-types, scores them, qualifies the best direction — a meta-signal.
publishes EyeVerdictMirrors fills from a primary account to followers, honouring prop rules, gating on Eye.
reads EyeVerdict + fillsAnalytics over the ledger + logs — win-rate, expectancy, MAE/MFE, and whether each weight earns its seat.
reads Ledger + LogWatches feed health. On lag or stall it engages the kill-switch, halting new entries until clean.
engages the killFleet orchestration across instruments — which slots are live, leader supervision, per-instrument gating.
the SlotLive gateThe eyes on the tape. Each watches one thing and publishes its read as a typed seam.
publish one …State eachAnatomy of a seam
The whole suite is glued by one tiny idea: a sensor publishes a small typed record — a …State — into SentinelCore, keyed by scope (one chart: instrument × bar type) and stamped with the time. A consumer consults it, checking freshness. Neither knows the other exists.
Stored per master-instrument ("GC"), never per class — so a tool on any chart of that instrument reads it.
The payload is plain int/double/bool. The bus never couples to a private enum — publish (int), the reader interprets.
UpdatedUtc is the freshness clock. Consumers pass a maxAgeSec and get null if it's gone cold.
Each state ships an Aligned(dir) / Near(atr) helper so consumers ask a question, not re-implement logic.
Absent or stale ⇒ the consumer abstains. A quiet sensor never blocks the system — it just stops voting.
The core loop
Follow one signal from birth to graded outcome. Sensors publish into SentinelCore; the Council fuses; the Bridge acts through the Order Gate; every fill lands in the Ledger; and Lens grades the outcome to tune the weights. Publish → fuse → consume → record is the whole suite — everything else is detail.
CouncilState tells you whether there's an edge — it does not put the trade on safely. You still call GateEntry at submit. 2 · Every fire is recorded so it can be graded. Writing the verdict to the Ledger on each fire is what lets Lens later answer "does the Council's confidence actually make money?" 3 · The weights are a hypothesis until the data says otherwise.01 Council fusion
Each sensor with a fresh reading casts a signed, weighted vote; a stale or absent one abstains. Modulators from above scale conviction by context — session, volume, higher-timeframe, location. Hard vetoes from below can zero it outright. Out comes one explainable verdict: direction, conviction, and a size multiplier.
Voters · weighted
Hard vetoes
missing, RosterComplete=false, named in the audit string · present but undeclared → flagged unexpected and not folded into the fusion. The Cockpit shows Roster 8/10 — EYE, BRK missing.Sentinel\Models\<INST>\<bartag>\Roster.conf, else defaults to every known voter whose weight > 0. It must be config-derived, never observation-derived — building the "expected" set by looking at which seams are live would bake today's outage into it, and the roster could never report the very thing it exists to catch. This also makes the model attributable: you can finally tell the model apart from what happened to be loaded on the chart.w = 0 still votes and is still recorded in the mask — but contributes nothing to netScore, activeW, the agree/disagree tally, or the breadth damping. You accumulate a candidate sensor's full history, and can measure exactly what it would have contributed, before it ever influences a single trade. Adding and retiring sensors becomes a config change with zero code and zero risk.The voter roster — the weights are the edge. Note the last column: most voters are price-derived (they echo the same candles), so their agreement is confluence, not confirmation. Only orthogonal axes carry genuinely new information.
| Voter | What it senses | Weight | Axis |
|---|---|---|---|
| Eye | Best-scoring GodTrades direction across ~24 bar-types (a meta-signal) | 1.4 | meta |
| God Reversal | Candle-grammar reversal at a band edge (shaved · engulf · equal-high · VI) | 0.9 | timing · mean-rev |
| Trend | Trailing-line regime direction (ATR + CCI hysteresis) | 1.0 | price |
| CCI | Woodies CCI trend bias (×1.5 when strong) | 0.8 | price |
| Compression | Coil-base breakout, held a few bars | 0.7 | price |
| WAE | Confirmed momentum-explosion breakout | 0.7 | price |
| ADX | Regime strength + DI bias, when trend is on | 0.6 | price |
| Envelope | Volatility regime (honest-Bollinger trend/squeeze) | 0.6 | price |
| Intermarket | Net lean from correlated instruments (ZN/ZB for gold) | 0.6 | macro |
| Brick | Adaptive-brick micro-trend direction | 0.5 | price |
netScore = Σ sign(dir)×weight; declaredW = Σ base weight of the DECLARED voters. Bias = ±1 when |netScore| > deadband·declaredW (deadband 0.15), else FLAT. Conviction = min(1, |netScore| / declaredW) — agreement, and nothing else. contextMult = ×breadth ×squeeze ×clock ×rvol ×mtf ×location (all fail-open, all ≤ 1). SizeMult = 0 if vetoed / Bias 0 / conviction < floor, else conviction × contextMult.activeW (present, directional voters), so a missing voter did not dilute conviction — it vanished from the denominator. One awake sensor of weight 0.6 gave 0.6/0.6 = 1.0: perfect unanimity. The fewer sensors awake, the more certain the Council sounded. Seen live as size=0.57 (1/0, 2v) · roster 2/10 — a tradeable verdict from one of ten voters.size=0.00. Now the floor gates on agreement; a poor context makes the trade smaller, never silently absent.declaredW still conflates two kinds of voter. STATE voters (TRND ADX ENV IMKT) always carry a direction. TRIGGER voters (EYE BRK CMP WAE GREV) are ±1 only on the rare bar they fire, and read ~ otherwise — yet they carry 4.2 of the 7.80 total weight. Over half the model is therefore parked at zero on a typical bar, pinning conviction near 0.16 (measured: mean 0.160, max 0.36 across 97 verdicts).ENV~ genuinely reading a flat regime. Those are different statements: a trigger that has not fired is an absence of evidence, not evidence against. Same one-number-two-jobs bug as ②, one level down. The fix — a voter kind in Roster.conf (CMP w=0.7 trigger), whose weight joins declaredW only on the bar it fires — is not built. Interim: ConvictionFloor lowered 0.35 → 0.20, which sits on a cliff (0.25→11% of verdicts, 0.20→41%, 0.15→60%). Do not nudge it by feel; fit it.02 The order gate
No order reaches the broker without passing GateEntry — the single place kill-switch, feed-health, a rate guard, and risk-based sizing are all enforced. Pass and it submits; fail and it's blocked with an alert. The Risk watchdog can engage the kill on feed lag — exactly what fired on GC recently (a 2.6 s lag → halt → auto-release when clean).
GateEntry · choke point
03 The seam hub
Every tool talks to one small shared core, never to each other. An indicator publishes its …State and forgets it; a consumer consults it fresh — or abstains if it's stale or absent. That fail-open seam is why the suite degrades gracefully instead of breaking when one sensor goes quiet.
Publishers · indicators
Consumers
stale · absent ⇒ consumer abstains — fail-open, with a heartbeat re-stamp so a still market doesn't read as "gone"
04 God Reversal detection
The newest sensor, encoding the candle grammar the structural signals miss. It only looks when price is at a band edge, reads the reversal candle, scores the confluence, clears the no-trade guards, then fires — marking the chart and publishing GodReversalState as the Council's GREV voter. Fires on the candle's close: confirmed, non-repainting.
05 Eye → Copy loop
Eye is a scout, not a signal of its own. It runs the GodTrades logic across ~24 bar-type rows, simulates each, ranks them on rolling performance, and qualifies a direction only when the best row clears its bar. The Copier mirrors a primary fill to followers only when Eye endorses it — and everything is logged so Lens adjudicates whether Eye actually earns its gate.
Eye · scan
06 Deck · SIGNAL ARM
The Deck is the human's manual trader — and its SIGNAL ARM turns any loaded indicator's plot into a trigger, discovered live from the chart (no hardcoding). A rule reads the plot on the just-closed bar — re-checked every tick so the bar-boundary race self-heals — then either arms (highlights BUY/SELL for a human to confirm) or auto-fires through a deliberately fail-closed gate: one shot per bar, flat-only, forced MARKET. Because the Council exposes Bias/Conviction plots, the Deck can arm directly off the Council.
reads the just-closed bar and re-checks each tick → fires on the bar after the signal: confirmed, never repainting
The interdiction layer · Helm
Three ways to trade the suite: the Deck — you drive · the Bridge — it drives · Helm — you grab the wheel of a running Bridge without stopping it. Helm owns nothing: it publishes an intent addressed to one running actor's instanceKey, and that actor executes it with its own order handles and stays the sole owner — because a panel that touches a managed strategy's orders desyncs the position and can terminate the strategy. It is the publish/consult idiom pointed the other way: human input as a seam. Risk-reducing verbs (Flatten · Pause · Skip · tighten-stop) are fail-open; risk-adding verbs (Resume · widen-stop · Scale-up) pass the same GateEntry — the gate does not care whose finger it was. Every intent is written to the Ledger and marks its episode interdicted, so the Lab grades the policy, not the human.
the Bridge publishes HelmState back → the Cockpit renders reality (position · live stop/target · paused/override), never a guess. Verbs: Pause · Resume · SkipNext · FlattenNow · MoveStop · MoveTarget · BreakevenNow · Scale · TakeOver · HandBack.
Live look
What the flows above look like rendered on price — the glass cards, the veto, and the on-chart order tools.
VETO: … line firing — e.g. news lockout or a liquidity wall.
→ img/s2-council-veto.png
07 Fleet orchestration · Arc
A NinjaScript AddOn can't start or stop a chart strategy — so Arc doesn't try. It publishes a plan (per instrument: enabled, contracts, session window) and watches the leader account; Sentinel-aware strategies consult SlotLive at entry and trade only when their slot is live. Load a strategy once per chart, then control the whole fleet from one place.
fail-open: no plan ⇒ the strategy trades normally — Arc adds control without becoming a single point of failure
08 The hardening substrate
The safety system isn't eighteen features — it's three substrates every order path shares. One guards the submit, one records and can restore, one watches and shouts. The question the whole thing exists to answer: could a tired human on a funded account blow it up? → No.
Layer 3, in detail · the rule that governs every detector
Three different things get written as if (set.Add(key)) Warn();, and only one of them is correctly a latch. Confusing them produced four separate bugs in this suite — including 160 false CRITICAL "naked position" alerts, because a stop order transiting ChangePending during a routine trail-step read as no stop at all. The alert you most need to trust had been crying wolf for five days.
| Kind | Means | Correct behaviour | Example |
|---|---|---|---|
| Action latch | "do this once" | latch; clear on the day roll | _hardFlattened — flatten an account once ✔ |
| Transition log | "say when it changed" | fire on change | _govPrevStatus — governor status ✔ |
| Condition alert | "something is wrong now" | debounce transients → report → keep re-stating on a cooldown while true → auto-clear on resolve | naked position · orphan orders · scope contention · ambiguous scope |
Never leave a CRITICAL condition on cooldownSec: 0 — silence would then mean "still broken." For event-shaped detections that have no "false" observation to feed back (scope contention is only ever observed at write time), report on detection and Clear(key) on teardown.
HashSet that fired once per process. The tell was that their roster deviations logged as duplicate same-millisecond lines — impossible for a single instance, since deviations are change-logged. The roster caught the bug in the thing built to catch bugs. That is the general lesson: cross-checking two independent sources finds what neither reports alone, and it is what a future Doctor view should automate.The one policy that governs it all — know which side of it any actor is on:
| Actor | On an ambiguous / absent signal | Why |
|---|---|---|
| Manual (Deck actions) | fail OPEN — allow | a human is in the loop |
| Automated (GTrader21 · Copier · Bridge · Deck auto) | fail CLOSED — block | no human to catch a mistake |
| Exits | never gate | you must always be able to get out |
| A thrown exception | fail OPEN | a bug must not freeze exits |
CanEnter actually checksAn automated entry fires only when all are true: not global kill · not scoped instrument kill · feed healthy · governor allows today (daily cap / loss-stop) · trailing-drawdown cushion intact · inside the account session · not in rollover blackout · not in a news lockout. Then, at submit, GateEntry risk-sizes and returns Clear / Advisory / Hard. Reconcile only ever detects + alerts — it never auto-cancels a real stop.09 A strategy's lifecycle · GTrader21
GTrader21 is the worked example — an unmanaged GodTrades strategy that uses every layer. On restart it restores or reconciles its position (never duplicating a stop). A signal only becomes an order after clearing three gates in series — Arc, Council, then the Order Gate — and every fill is captured for grading.
three gates, all fail-CLOSED for an automated strategy — a signal that clears the panel arm still stops if Arc, the Council, or the risk gate says no
The prop-firm governor
On a funded account, two rules can end it that have nothing to do with your strategy: the trailing drawdown (Risk owns it — the real-time floor vs the firm's −$4,500-type limit) and the consistency rule (the Governor owns it — no single day may exceed R × your total profit). Rather than track a ratio intraday, the Governor caps each day at DailyCap = R × ProfitTarget — if no day ever exceeds that, compliance is automatic.
| Firm | R (ratio) | Target | DailyCap | Preset |
|---|---|---|---|---|
| Lucid | 0.20 | $9,000 | $1,800/day | lucid |
| Bulenox | 0.40 | $9,000 | $3,600/day | bulenox |
| TPT | 0.50 | $9,000 | $4,500/day | tpt |
| Apex | 0.30 | — | — | apex |
resetHour to your firm's rollover (TPT 5 PM ET, etc.) — a wrong reset silently breaks the daily rule. Prop rules drift constantly; always verify against the firm's live docs (Docs/PropFirmRules.md). The most common way a funded account dies isn't strategy — it's the trailing floor ratcheting on an unrealized peak.Operate it
A safety system you haven't watched fire is a story you're telling yourself. Most of the Dashboard Test tab works with the market closed — do these first.
Test tab → Test Critical → hear a sound + a Risk alert row + a ledger line. Set a pushCommand (ntfy/Pushover/Slack), re-fire, confirm the phone push — no NT restart needed.
Pick account + instrument + qty/stop/risk → Evaluate → expect GATE = CLEAR + sized qty. Engage the top-bar KILL → Evaluate → GATE = HARD. No order is ever sent.
Run checks → expect 3/3 PASS (scoped-kill isolation · sizer unaffordable→0 / generous→≥1 · TickValue > 0).
Fire a SIM Deck order, toggle the kill → a JSON line per order + per toggle in Ledger\ledger-YYYY-MM-DD.jsonl, mirrored in the Journal tab (▶ Live streams within ~2s).
Live look
The dashboard tabs where you prove the net actually catches something.
The two files everything depends on
Every subscribed Sentinel indicator hard-references exactly two AddOn files — that's the entire compile dependency. SentinelCore is the bus + the safety substrates (seam registry, kill-switch, feed-health, Gate, Ledger, State, Alerts, and the account registries). SentinelSkin is the look (the glass-card Painter, the palette, and CardLayout's anti-overlap docking). With the services absent, every Get…State returns no-data and the tool degrades to neutral, throwing nothing — that standalone-safe property is a hard rule.
The sensing layer · the voters — each is an Indicators.Sentinel indicator, publishes a seam (default ON), and is wired into the Council.
| Sensor | Publishes | Weight | The read it contributes |
|---|---|---|---|
| Eye | EyeVerdict | 1.4 | Adaptive GodTrades scanner's directional qualification |
| SentinelTrend | TrendState | 1.0 | ATR trailing-line direction (supersedes TrendMagic) |
| God Reversal | GodReversalState | 0.9 | Candle-grammar reversal at a band edge |
| WoodiesCCIPro | CciState | 0.8 | Woodies CCI trend state −2..+2 (×1.5 if strong) |
| CompressionBase | CompressionState | 0.7 | Coil-base breakout direction ±1 |
| Sentinel WAE | WaeState | 0.7 | Waddah Attar confirmed momentum-explosion breakout |
| ADXPro | AdxState | 0.6 | ADX trend on/off + DI bias (×1.25 if strong) |
| VolEnvelope | EnvelopeState | 0.6 | "Honest Bollinger" regime (squeeze/trend/expansion) |
| Intermarket | IntermarketState | 0.6 | Correlated-instrument lean — the one truly orthogonal voter |
| Brick (bar type) | BrickState | 0.5 | Adaptive HA/Renko brick direction |
The orthogonal axes — the independence engine. Four modulate conviction; Intermarket (above) is the fifth and it votes. Plus the one veto sensor.
| Axis | Publishes | Council role |
|---|---|---|
| Clock | ClockState (phase / mins-to-close / kill window) | midday & off-session damp + kill-window veto |
| Participation | ParticipationState (RVOL + climax/dry-up) | thin-tape damp (never inflates) |
| Location | LevelState (VWAP/PDH-PDL/OR/IB + nearest) | into-a-level damp (don't trade into the wall) |
| MTF | MtfState (1/5/15/60/240 ladder) | counter-higher-TF damp |
| LiquidityWalls | LiquidityState (absorption z-score + walls) | hard veto — a wall on the intended side zeroes conviction |
Execution & the service layer — the hands, and everything that keeps them safe and remembered.
| Tool | Role |
|---|---|
| GTrader21 strategy | Automated GodTrades (BG/FC/OBR), unmanaged, panel + risk card; auto-reads lab configs; UseCouncilGate decouple |
| Bridge strategy | The autopilot — consumes CouncilState, sizes ×SizeMult → GateEntry, records every fire. Base Strategies namespace |
| Deck indicator | Manual order deck + full trade management + on-chart order visuals + SIGNAL ARM |
| Dashboard | 12-tab control center: Copy · Log · Risk · Journal · Slippage · Lens · Eye · Arc · Assist · Excursion · Accounts · Test |
| Risk | Feed lag/stall watchdog → engages the kill; rollover; hosts news-lockout + the governor |
| Alert · State · Copier · Log · Lens · Arc | Sound/push · state.json snapshot · fill-mirror · per-trade MAE/MFE · weight-grading · fleet orchestration |
The shape every seam shares
Keyed by SCOPE — "<instrument>.<barTag>", e.g. GC.69697v6x24, from SentinelCore.ScopeOf(Instrument, BarsPeriod). A scope is one chart's worth of context, and exactly the coordinate a model is defined over. Case-insensitive, lock-guarded. Get… returns null if nothing was published or the entry is older than maxAgeSec (pass 0 to disable expiry) — absent/stale = the consumer abstains, the fail-open backbone. The read always travels as int / double / bool; the bus never couples to a private enum.
All seams share one SeamStore<T>, whose Get() resolves in three rungs. (1) exact key — a migrated publisher consulted by scope; the normal path. (2) a scope asked of an instrument-keyed entry — a publisher that has not migrated yet. That rung is what lets the migration land one F5 at a time rather than all at once, and it disappears when the last publisher moves. (3) a bare instrument asked of scope-keyed entries — resolved only if exactly one scope carries it, else null plus a throttled log. That rung is fail-CLOSED on purpose: "I don't know which chart you mean" must never be answered with "here's whichever wrote last."
"GC" they overwrote each other's readings every bar, and a Council could fuse the other chart's ADX and report it as confluence. Scope separates GC from NQ, and GC-TBars from GC-150tick. It cannot separate two charts sharing instrument and bar type — nor two copies of one indicator on a single chart. That case is detected and logged as SCOPE CONTENTION, never silently permitted.| Seam | Key payload | Convenience |
|---|---|---|
| EyeVerdict | Direction(±1/0), Score | — |
| TrendState | Direction, TrendPrice, DistanceTicks, BarsInTrend, Flipped | IsUp/IsDown, Aligned |
| CciState | TrendState(−2..+2), MainCci, TurboCci, Signal, Weakening | Bias, Strong, Aligned |
| AdxState | Adx, DiPlus, DiMinus, Bias, Slope5, Strong | TrendOn, Building, Aligned |
| EnvelopeState | Regime(0–4), Stretch, BandwidthPctile, MultUp/Down | IsSqueeze, IsTrend |
| BrickState | Direction, Atr, SameDirCount, TicksToUpper/Lower | AtrTicks(), Aligned |
| CompressionState | Signal(pulse), BreakDir(held), Coil, Compressed, Armed | JustBroke, Aligned |
| IntermarketState | Lean, Score(−1..1), RefCount, Refs | Aligned |
| WaeState | Signal(±1 confirmed), Power, Explosion, DeadZone, IsExploding | Aligned |
| GodReversalState | Signal(pulse), Dir(held), Quality, Setup, AtBand, Exhausted | JustReversed, Aligned |
| ClockState | Phase(0–3), MinsToClose, InSession, InKillWindow | IsMidday, IsClose |
| ParticipationState | Rvol, VolZ, Climax, DryUp | Backed |
| LevelState | Vwap±bands, Pdh/Pdl, Orh/Orl, NearestName, NearestDistTicks/Atr | Near(), InPath() |
| MtfState | Bias, AlignmentScore(−1..1), AlignedCount, AllAgree, Dirs | Aligned |
| CouncilState | Bias, Conviction, SizeMult, Agree/Disagree/Voters, Vetoed, Reasons | HasEdge, Aligned |
| LiquidityState | Zscore, AbsorbSide, WallAbove/Below, DistAbove/BelowTicks | BlocksEntry(dir,ticks) |
Aligned(int dir): +1=long, −1=short, 0=flat. Also on Core, keyed by account: the kill-switch, scoped kill, governor, trailing drawdown, account profiles, news lockout, rollover, and fleet slots — bundled into the combined entry gate CanEnter.
…State seam carrying its read as int/double/bool (never an enum); (b) gate publishing behind a PublishState property that defaults ON — don't ship it dark; (c) be wired into the Council as a voter, modulator, or veto, appearing in the Reasons audit. A hidden plot alone is not enough — the Council reads seams, not plots.Step 1 · publish your seam
Publish every processed bar so the seam stays fresh — consumers staleness-gate, so a seam you stop refreshing goes stale and your voter silently abstains.
OnStateChange simply never votes, forever, and nothing says so — the Eye did exactly this across 332 verdicts while carrying the heaviest weight in the Council. Guard anything that can throw at construction, and declare your voter in the roster (below) so its silence is reported instead of assumed.SetXState stamps UpdatedUtc = DateTime.UtcNow even while the publisher replays historical bars, so a consumer's freshness gate cannot tell replay from live. Anything that records must gate on State == State.Realtime.Step 2 · wire it into the Council
AddVote collapses the value to its sign, adds sign×weight to netScore and weight to activeW, counts the fresh voter, and appends to the tally / card chip / Reasons token — all generic. Pass 0 when present-but-neutral (still counts toward breadth). Fold strength into the weight (× (mine.Strong ? 1.5 : 1.0)). If your sensor is context, make it a modulator (conviction *= myDamp); if it's a hard gate, add a branch to the veto chain.
Step 3 · consume & record
HasEdge must gate on SizeMult, not Conviction. Since conviction became pure agreement, a below-floor verdict still has Conviction > 0. The old test reported an edge with SizeMult = 0 — and because a consumer computes Math.Max(1, baseQty × SizeMult), it would fire a one-lot on a stand-down. Size is the only number that can say no.SizedQuantity() is not optional. The Bridge skipped it for months, so the account profile's size=, the governor's RecommendedSize(), and ContractLimit were all silently ignored — a governor telling a strategy to size down was not obeyed. It is, in Core's own words, "the one place sizing math lives."riskDollars = 0 to GateEntry when you have already sized. Otherwise the Gate re-sizes from risk and can reject your quantity as "risk too small". That flip silently blocked the Bridge's first live trade.baseQty = 1, SizeMult cannot scale a position down — 1 × 0.19 rounds to 0 and the Max(1,…) floor restores a 1-lot. SizeMult only has resolution at baseQty ≥ 2. ConvictionFloor (SizeMult = 0) is what expresses "do not trade" — SizeMult is not a substitute for it.SentinelCore.Log("MyTool", …) → sentinel.log). Never build a second journal — the Ledger is the one event stream. Strategies stay in the base Strategies namespace (NT hides sub-namespaced strategies); indicators use Indicators.Sentinel. After forking a versioned file, strip all NT generated #regions or a running NT re-appends them (CS0111/CS0102). Position a card from ChartPanel.X/Y/W/H, never ActualHeight.Measure the path, don't trade it
MAE = max adverse excursion (how far a signal went against you). MFE = max favorable. If you know the distribution of both for a signal, you can pick a take-profit and stop that match how it actually behaves — instead of guessing. The trick: record the full, untruncated price path after every signal, so you can simulate any TP/SL against real excursions.
The workflow, click by click — from a blank chart to a live TP/SL rule the strategy obeys.
Drop SentinelExcursionRecorder on each chart / instrument / bar-type you want to characterize (e.g. GC 100T, NQ 1000T). It places no orders and never truncates the path — leave the strategy off, or run a measurement template with every TP / SL / reverse / BE / trailing / cutoff OFF so nothing clips the excursion. It flushes each signal to \Sentinel\Excursions\*.jsonl at end-of-day, and also records the Council verdict as its own signal, tagged by conviction bucket.
Dashboard → Excursion tab → Load / Refresh excursions. The status line shows unique records / files / groups (with duplicate + legacy-schema records skipped). Tick "Confident only (n≥30)" to hide small-sample noise.
One diverging bar per signal group (trend regime): green MFE vs red MAE at 15 min, ranked by edge, a ✓ when HasEdge (median MFE > median MAE). This is your shortlist.
Pick one in "Detail signal" and read the four panels + two referees below — the growth line, the outcome scatter, the 12-config grid, and whether the Eye / Conviction actually paid.
★ = best raw EV (usually a wide-stop mirage — big EV bought with catastrophic risk). ◆ = best responsible config, Stop ≤ TP (R:R ≥ 1) — apply the ◆. "Apply ◆ to GTrader21 config" writes one .conf; "Sync all ◆ configs" writes one for every confident group with positive Exp.
On the target chart's strategy, group "14. Sentinel Integration": set UseSentinelConfig = true and SentinelConfigName = "GC_FC_Short". On DataLoaded it overrides its own TP / SL / trend-filter / Eye-gate with the lab values before any trade, and republishes what it loaded so it appears in the dashboard's Active lab configs list.
Reading the tab — the four analysis panels and the two referees that grade whether a filter earns its seat.
.conf flips useEyeGate on.What Apply writes — a plain key = value file; the strategy applies five keys, the rest is informational.
Live look
The three read-decide panels and the one-click handoff that turn recorded paths into a live rule.
UseSentinelConfig + the Active-lab-configs list.
→ img/s11-conf-handoff.png
One trade, birth to grade
You trade GC and suspect the GodTrades FC (continuation) signal is your edge, but only in a trend. Here's how the suite turns that hunch into a graded, live, risk-gated strategy. (Numbers illustrative; the process is exact.)
Act 1 — Measure. Put the Excursion recorder on a GC chart for a couple of weeks. It never trades — it records the full price path after every FC/BG/OBR fire (MFE/MAE at 1/5/15/60m), tagged with regime and Eye verdict. You end with a few hundred FC-Short fires and their real excursions.
Act 2 — Find the responsible edge. Excursion tab → Load. GC · FC · Short (trend) shows green MFE ≫ red MAE at 15m. The ◆ lands at TP 40t / Stop 30t, ~+6t/trade at 58% hit rate, firing 1.8×/day, n = 140. The Eye referee says endorsed fires out-earn the rest. Click Apply ◆ → writes GC_FC_Short.conf.
Act 3 — Hand it to the strategy. On the live GC chart, set the strategy's UseSentinelConfig = true. On load it overrides its own dialog — TP 40 / Stop 30 / trend filter on — before it can place a single trade. The lab result is now the live rule. (The Bridge does exactly this off the Council; the executor is pluggable.)
Act 4 — A signal fires. Watch the gauntlet:
Act 5 — Fill, record, manage. The sell fills at 2041.2 (intended 2041.4 → 0.2t slip, visible in the Slippage tab). The strategy records the Council verdict on this fire into the Ledger — the seed Lens will grade — sets its 40t target / 30t stop, and persists position-state so a mid-trade restart re-adopts the stop instead of duplicating it. Target hits: +40t minus slip. One clean, fully-audited round trip.
Act 6 — Grade it. Days later, 30+ of these. Dashboard → Lens answers the only question that matters: when the Council was confident (0.7+) on FC-Short, did those trades pay more than the low-conviction ones? If yes → conviction is real edge; lean in. If no → "agreement" was just correlated price-lenses nodding along; the fix isn't more of the same voters, it's an orthogonal axis carrying information the others don't have.
The one-sentence version
The Council computes netScore = Σ(voteᵢ × wᵢ), calls the sign of it the bias, and calls |netScore| ÷ Σ(active weight) the conviction. That is a normalized linear model over ten signed features. The coefficients — WeightEye = 1.4, WeightTrend = 1.0, WeightCci = 0.8 … — the 0.35 conviction floor, and the 0.15 deadband are all numbers a human picked.
So "adding machine learning" to Sentinel does not mean bolting a neural network onto price bars. It means fitting the parameters the Council already has, using the outcomes the Recorder already writes. The architecture doesn't change. Three guesses become three measurements.
The whole machine, as one map
The entire decision-and-learning cycle on one surface. The top row is the live decision path — sensor to order, left to right. The bottom row is the offline learning path — every fire graded against what price did next, the weights refit per bar type, fed back in. The loop closes at COUNCIL, the only box both paths touch — and nothing the bottom row produces can open the Gate on the top.
Model.conf. Up the return arrow, Model.conf becomes the Council's weights — or, if it is missing, stale, or for the wrong instrument, the hand-set weights stand. The machine can get smarter, or fall back to exactly what it is today; it can never get more dangerous, because the Gate lives on the top row and the model on the bottom.What stays the same, what changes — the honest inventory. Nothing in the left column is touched.
| Layer | Stays exactly the same | What the learning loop changes |
|---|---|---|
| Sensors | Publish a …State seam, default on. Never know who reads them. | Nothing. A sensor is a voter whether its weight is 1.4 or 0. |
| SentinelCore | One static bus. Publish / consult. Freshness via UpdatedUtc. | Additive fields only: the decision vector on CouncilState, an EpisodeId, a scope-aware key. |
| Council | Fuses votes → bias · conviction · size×. Hard vetoes. Reasons audit. Publishes one verdict. | Reads its weights from Model.conf when present. Falls back to the hand-set weights if absent, stale, or wrong-instrument. |
| Bridge / Deck / GTrader21 | Consume CouncilState. Size, bracket, fire. ARM is a deliberate click. | Claim a unique instanceKey before arming, and stamp instance · model · policy · episode on every Ledger row. |
| The Gate | Fail-closed. Kill · governor · session · rate. The one pre-submit choke point. | Nothing. Ever. A model is never allowed to widen a limit. |
| Ledger | Append-only JSONL. order · action · fill. | Additive context: strat · instance · model · policy · episode — the five identities that today all read "SentinelBridge". |
| Arc | Publishes a fleet plan; strategies consult SlotLive(), fail-open. | Slots keyed by instanceKey, not by instrument — so it can idle one GC chart and run another. |
| Recorder | Records MFE/MAE for every signal. Places no orders. | Also records the inputs — the voter vector — plus a firstTouch barrier latch. Schema 1.2 → 1.3. |
| You | Arm the chart. Read the Cockpit. Trust the Gate. | Run train.py when the market is closed. Decide whether to promote a challenger. |
_council["GC"], _adx["GC"], _trend["GC"]. Two GC charts on different bar types therefore overwrote each other's readings, and a Bridge on one chart could read — and trade — the other chart's brain. Separately, SetCouncilState stamps UpdatedUtc = DateTime.UtcNow even while replaying historical bars, so the freshness gate could not tell a live verdict from a replayed one.State.Realtime). Batch 1 of the sensors fixed in v1.18.0 — Adx · Trend · Cci · Envelope, the four price-derived voters, now share a SeamStore<T> keyed by scope. Eleven seams remain instrument-keyed (Eye, Liquidity, Brick, Clock, Participation, Level, Mtf, Compression, Intermarket, Wae, GodReversal), as does Arc's _fleet — that is the rest of Phase 1.4. The store's scope→instrument shim means a half-migrated tree still resolves correctly, so batches land one F5 at a time.SCOPE CONTENTION — proved live: two Councils on one NQ chart. See Docs/SENTINEL_ML_SPEC.md §10–11.Across the file boundary
NinjaTrader never runs Python. Python never touches bin\Custom. They meet at exactly two places, and both are flat files on disk. That is the whole integration — no DLLs to reference, no ONNX runtime, no CS0101 collisions, no F5, and no way for a broken experiment to stop the platform from trading.
inside NT · .NET 4.8 · data thread
outside NT · python · no platform access
One row per Council episode — a maximal run of constant bias. The row holds the inputs (which voter said what, at what weight, plus the orthogonal-axis context) and the outcome (the full MFE/MAE path, and which barrier was touched first). It places no orders.
Sentinel\Excursions\*.jsonl and Sentinel\Ledger\*.jsonl. Plain text, append-only, one row per line. Python opens them with json.loads. Nothing is exported, nothing is compiled.
Most of the code is defensive, not predictive — censoring, uniqueness weighting, purged cross-validation. Its job is to stop you believing an edge you don't have.
Model.conf is key = value — so the C# side needs no JSON parser. It lands in the Sentinel\ config-git repo, committed next to the P&L it produced.
It still computes its own verdict, on this chart, this tick. The file supplies numbers. If the file is missing, expired, or for the wrong instrument, the hand-set weights are used and it logs once. Fail-open.
What each offline stage actually does — five ideas, four of which exist only to keep you honest.
EYE = +1 and a short with EYE = −1 are the same evidence — "the Eye agreed." Halves the feature space, doubles effective N.Before the Council trusts a model file — every guard must pass, or the hand-set weights are used and nothing stops.
schema is recognisedelse → hand weightsinstrument matches this charta GC model never runs NQbartype matches this chartbar granularity is the modelexpiresUtc is in the futurea stale model self-disablesw.* voter tag presentno silent partial weightsWho does what
Nothing new is invented. Every part below already exists and already does most of this — the loop mostly consists of teaching them to write down what they knew at the time.
Writes one row per Council episode: the decision vector in, the MFE/MAE path and first-touch barrier out. It is the only component that sees inputs and outcomes together. Places no orders, ever.
writes Excursions\*.jsonl · schema 1.3At runtime the Council is the fitted linear model. Today its coefficients are hand-set; after the loop closes it reads them from disk. Its arithmetic, vetoes and Reasons audit are unchanged either way.
reads Model.conf · publishes CouncilStateThe bus. Gains the decision vector, the EpisodeId, and scope-aware seam keys. Additive only — it is one static class in one assembly and can never be forked.
Unchanged as traders. They gain one obligation: stamp episodeId and modelId on every order, action and fill, so a trade can be traced back to the exact decision and the exact model that produced it.
Completely untouched. Fail-closed. Kill, governor, session and rate limits are policy, not prediction. The learned model proposes; the Gate disposes.
GateEntry — unchangedToday its only identity is a free-text tag, so a fill cannot be joined to its verdict. With episode as a real key it becomes a database rather than a diary — the spine the whole loop hangs from.
Joins fills → episode → verdict → outcome and reports which model paid, per instrument. This is the job it was always described as doing and could not actually do, because the join key didn't exist.
reads Ledger ⋈ ExcursionsPython, outside bin\Custom. Loads the JSONL, folds by direction, labels by barrier, weights by uniqueness, validates by purged walk-forward, and emits one flat artifact. Cannot break the platform because the platform cannot see it.
The operator's answer to "why isn't it trading?" — and now also "which model is loaded, is the roster complete, how old is the verdict." The stale ▸ floor why-line gains a roster rung: Roster 8/10 — EYE, BRK missing. It is also the naming surface (§ setups): the whole fleet in one list, aliases editable in place.
Publishes the fleet plan each chart consults before it fires. Today its slots are keyed by instrument, so it sees one "GC" and cannot idle one GC chart while running another — the same collision the seams have. Slots must key by instanceKey.
GC · NQ · ES · CL
The mechanics of running the fleet reduce to a single rule: a chart is a scope, and a scope is a model. Everything else — file paths, model identity, which verdict the Bridge reads — falls out of that one coordinate.
The coordinate propagates everywhere. Add a fourth chart and nothing collides, because nothing was ever global.
Roster.conf with w = 0. It votes. It is recorded. It contributes nothing to netScore or activeW. You accumulate its full history — and can measure exactly what it would have contributed — before it ever influences a single trade. Adding and retiring sensors becomes a config change with zero risk and zero code.The interface
Every identifier above is content-addressed: gc-tbars-a3f91c2, pol-5f21, GC-20260709-0042. Machine-perfect, human-hostile. No trader will ever type one, and none should ever have to see one. A design this structured is worthless if the interface isn't straightforward — so the naming rules come first, and the hashes hide behind them.
| Thing | Auto-derived default (type nothing) | You may override to |
|---|---|---|
| Setup the instance | GC · TBars 6-24 · Sim101 | "GC Morning" |
| Brain the model | Hand weights — or Model.conf's own alias= | "Balanced v3" |
| Play the policy | TP40/SL20 — or the lab .conf filename | "Tight" |
The keys are derived and immutable; the alias is a mutable display string bound to one. Rename freely — nothing re-keys, no history orphans. Aliases live in Sentinel\Aliases.conf, in the config-git repo. The Ledger writes both: the id as the join key, the alias denormalized beside it so a raw JSONL line reads without a lookup.
You configure a chart. Call it a Setup, and let its name be the only thing you ever type. modelId and policyId become automatically-versioned eras inside it. Widen the stop on "GC Morning" and the policy fingerprint changes while the name does not — so Lens reports "GC Morning — policy changed 2026-08-14", which is how you already think about it.
A per-chart property grid is the worst place to notice you've named two things alike — you can only see one at a time. The Cockpit is the naming surface: the whole fleet at once, alias editable in place.
Humans name the thing they can see; the machine versions everything underneath. Three surfaces, one precedence order.
Setup name string on the strategy. A seed, not the store — a [Display]-only property (never [NinjaScriptProperty]) so it serializes to the workspace without touching the generated region.Aliases.conf > F6 Setup name > auto-derived default.instanceKey → BLOCK the arm. That means two Bridges on the same scope and the same account. NT's account position is shared across strategies, and a managed strategy whose account position moves underneath it desyncs and refuses all new entries until you disable and re-enable it. The configuration that is ambiguous in the Ledger is the same configuration that is dangerous in the account — so naming and safety turn out to be one mechanism, and you get the second one free.Setup name is a runtime instance alias: a different kind of thing, chosen by the trader, changeable at will, and bound to a derived key that never moves. Every actor in the suite — Bridge · Deck · GTrader21 · Copier — should expose it identically.Model.conf with a model it had just rejected). SentinelCore is at v1.15.0: CouncilState is keyed by scope (GC.69697v6x24) and carries BarTimeUtc + IsHistorical, and the Recorder now records Council fires only in State.Realtime — the as-of guard.Eye_v1_1_0 defaulted its TBars and NinzaRenko bar-type IDs to numbers that are not registered bar types, so AddDataSeries threw out of State.Configure on every load. That is why EYE — weight 1.4, the heaviest voter — never voted once in 332 verdicts. It loads now, and every row add is guarded so a bad bar type degrades to "row skipped and logged" instead of taking the sensor offline. ⚠ It is present but still directionally neutral until it qualifies: present in the tally, contributing breadth, but its 1.4 has yet to enter netScore. The declared roster now reports any such absence instead of hiding it, and batch 1 of the sensor seams (Adx/Trend/Cci/Envelope) is scope-keyed.EYE row appears again; batch 1 + 2 of the sensor seams are scope-keyed (Adx · Trend · Cci · Envelope · Brick · Compression · Wae · GodReversal — 8 of 15); the sensors gained an OnMarketData heartbeat, so a healthy OnBarClose voter no longer ages out of the roster in a quiet market (measured: a fully-loaded chart used to report roster 3/10; after the heartbeat only the two un-heartbeated seams ever drop). Conviction was rebuilt twice — absence now dilutes, and context damping moved out of conviction into SizeMult. HasEdge gates on size. SentinelBridge finally routes sizing through SizedQuantity()._fleet — are still keyed by instrument alone (the rest of Phase 1.4; EyeVerdict among them, which is why EYE still drops out of the roster). And scope alone cannot separate two charts sharing instrument and bar type, nor two copies of one indicator on a single chart; that is now detected and logged as SCOPE CONTENTION — proved live.declaredW conflates state voters with trigger voters, so half the model's weight is parked at zero on a typical bar and conviction is pinned near 0.16 (1 of 97 verdicts cleared the 0.35 floor). The floor was lowered to 0.20 as an interim; the real fix — a voter kind in Roster.conf — is not built.Non-repaint timing
A candle's shape isn't final until it closes. So every Sentinel signal is computed on a closed bar and consumed on the next one — read at barsAgo=1, re-checked each tick so the processing-order race between two indicators self-heals. The cost is a one-bar delay; the payoff is a signal that never repaints out from under you.
a one-bar pulse is fleeting, so sensors also publish a held direction for a few bars — that's what the Council actually votes on
Principles
A missing sensor abstains — the system keeps deciding with what it has. A missing safety check blocks: the order gate and auto-fire fail closed. Read loosely, act strictly.
Every order — Bridge, Deck, GTrader21, Copier — passes the same GateEntry. Kill-switch, feed-health, sizing, and rate-limits live in one place, so safety can't be forgotten on one path.
Most voters read the same candles, so their agreement can be an echo. The suite treats conviction as alignment, and only gets truly smarter as orthogonal axes come online.
The Council's vote weights are the edge, and they're not frozen. Every fire records the verdict; Lens grades which voices actually paid — so the weights become measured, not guessed.
The Council advises — it shapes bias and size but never sends an order. The consuming strategy still calls its own gate at submit. Decision and enforcement are separate layers.
Every tool is versioned per-file with an in-file changelog; old versions are frozen, never edited. A safe fallback always exists, and a chart never silently loses an indicator to a rename.
Glossary
…State) that any tool can read.Instrument.MasterInstrument.Name (e.g. "GC") — the seam registry key.File index
| File | Role |
|---|---|
| AddOns/SentinelCore_v1_0_0.cs | The bus + Gate/Ledger/State/Alerts + all registries |
| AddOns/SentinelSkin.cs | Glass-card Painter + palette + CardLayout |
| Indicators/Council_v1_0_0.cs | The fusion brain |
| Indicators/{Eye,SentinelTrend,WoodiesCCIPro,ADXPro,VolEnvelope,CompressionBase,Intermarket,SentinelWAE,SentinelGodReversal}_*.cs | The voters |
| Indicators/{Clock,Participation,Location,Mtf}_v1_0_0.cs | The orthogonal modulator axes |
| Strategies/GTrader21v_0_1_7.cs · SentinelBridge_v0_2_0.cs | The executors |
| Indicators/Deck_v0_2_2.cs | The manual order deck + SIGNAL ARM |
| AddOns/SentinelDashboard + Sentinel{Risk,Alert,State,Copier,Log,Lens,Arc}Service*.cs | The control center + service layer |
| Docs/SENTINEL_DESIGN_SYSTEM.md · SENTINEL_GOD_REVERSAL_DOCTRINE.md · ROADMAP.md | The specs this manual condenses |
| Docs/SENTINEL_ML_SPEC.md | The learning loop (Part X) in full — schema 1.3, scope keys, identity, the fleet |
| Docs/SENTINEL_DATASET_DICTIONARY.md | The corpus reference — scope grammar, bar-type IDs, schema-1.3 fields, the full 22-voter catalog, how the Lab reads it |
| Sentinel/Lab/ | The offline trainer. Python, outside bin\Custom, never compiled by NT |
episodeId exists. The Council reads a shared voter catalog (Models\catalog.conf, emitted from the one C# source), and the offline Lab fits per bar type from the clean schema-1.3 corpus with a data-driven voter set. The honest next step is to bake enough clean rows, then fit the ConvictionFloor + weights — the first look (Lab\voter_edge.py) shows the edge lives largely in bar construction, and the hand-set weights are a hypothesis, not yet a measurement. This document is living; it is updated as the system evolves.