Sentinel Suite · NinjaTrader 8 · living documentDocs home →
What this project actually is, why it is built the way it is, and where it is going — at altitude, readable without the code. The engineering sequence lives in ROADMAP.md; the product shape in PRODUCT_LADDER.md; the ML mechanics in SENTINEL_ML_SPEC.md. This is the one-page "why."
A chart full of indicators is a room full of experts talking over each other. Sentinel makes them vote — then checks who was right.
It is a suite of NinjaTrader 8 tools that turns a screen of independent indicators into one honest, fitted decision — and then grades that decision against what the market actually did, so the decision can improve itself over time.
Most trading indicators shout in isolation. Sentinel's premise is that the value is not in any one signal but in fusing many orthogonal ones into a single verdict, measuring that verdict's real outcome, and feeding the measurement back into how the fusion is weighted. The end state is not a fixed strategy — it is a learning loop whose parameters are fit from evidence rather than guessed, and which adapts as the market's character changes.
The whole system is designed so it works at any depth of adoption: a single pretty indicator, a fused verdict you read but don't trade, an automated consumer, or the full self-tuning loop. Everything is open-source (MPL-2.0); the ladder is depth-of-adoption, not price.
| Name | What it is | Its one job |
|---|---|---|
| Sensors | indicators (Trend, ADX, Woodies, VolEnvelope, Compression, Liquidity, GodReversal, WAE, + the axes) | watch one thing well and publish a reading |
| Seams | typed …State values on a shared bus (SentinelCore) | let tools read each other without knowing each other |
| Council | a read-only chart tool | fuse every fresh seam into one verdict (bias · conviction · size) |
| Deck / Bridge | manual trader / automated consumer | act on the verdict (the only pieces that place orders) |
| Gate | the pre-submit safety choke point | guard — kill switch, governor, sizing, session/news vetoes |
| Recorder | a no-orders characterization tool | grade — write each decision's real forward outcome to disk. ⚠ Until Core v1.46.0 that meant each Council verdict and nothing else — see §"the corpus could only see the Council" below |
| Lab | offline Python, outside NinjaTrader — now a stood-up data platform (SQLite corpus + JSONL ingester + Streamlit + Grafana) | learn — fit the Council's weights + floor from the graded corpus |
The rest of this document is how those pieces form a single loop, and why the order they were built in matters.
Read this before §2–§6. The sections below describe the fusion approach as designed. This one records what the first honest measurement of it showed, and the change of direction that followed. Both are kept: the reasoning in §2–§6 is still the clearest statement of why the system is shaped the way it is, and it is worth having beside the result.
The measurement. 3,777 replay fires, graded tick-true on real fills, with a 70/30 time holdout and a 0.12R cost bar (break-even p = 0.560).
What it showed.
What this changes, and what it doesn't.
| Area | Where it stands |
|---|---|
| Fusing many signals into one verdict | Unproven on the sensor set we had. Not ruled out — the inputs were mostly price-derived, so they echoed each other |
| "Conviction = agreement, not confirmation" (the caveat this doc already carried) | Borne out, and it looks like the main reason the fusion added little |
| Grading decisions against real outcomes | Working as designed — it is what surfaced all of the above |
| The safety envelope (Gate, governor, kill switch) | Unaffected — independent of whether a signal has edge |
The durable asset is the measurement tooling, not any particular roster: tick-true, real-fill, tick-by-tick outcome per decision — something a trader inside NinjaTrader cannot see. The loop was built to tell us when we were wrong, and the first substantial thing it did was exactly that.
So the current focus is to audition candidate voters standalone and tick-true across a bar-type matrix and build up from whatever holds, rather than to tune the existing fusion. Same corpus, same labels, same holdout discipline.
Two threads are parked as a result: fitting the
ConvictionFloor/weights of the current fusion (there is nothing solid yet to fit against), and the Eye as a voter (breadth-only; its 1.4 weight never enterednetScore).
At the center is the Council — a read-only chart tool that fuses every sensor's published reading into one verdict. Strip away the presentation and the Council computes exactly this:
netScore = Σ ( voteᵢ × weightᵢ ) # each sensor votes −1 / 0 / +1
conviction = | netScore | / declaredWeight # how aligned the awake voters are, 0..1
bias = sign(netScore) if |netScore| > deadband else 0 (FLAT)
sizeMult = 0 if vetoed OR bias == 0 OR conviction < floor else conviction × contextDamping
That is a normalized linear model. And here is the load-bearing observation: its coefficients were chosen by hand and never fit. WeightEye = 1.4, WeightTrend = 1.0, …, the 0.20 conviction floor, the 0.15 bias deadband — every one is an educated guess. They have never been checked against whether verdicts that scored high actually did better than verdicts that scored low.
So "adding machine learning to Sentinel" is not bolting on a neural net. It is the far more modest and honest act of fitting the parameters the Council already has from the Council's own recorded outcomes. The model already exists. We are going to stop guessing its numbers.
The Council today fuses a specific set of sensors — Trend, ADX, Woodies CCI, VolEnvelope, Compression, Liquidity, GodReversal, WAE, plus the orthogonal axes. That set was chosen to build and prove the fusion process, not because the Council is defined by it. The Council is a fusion engine; its voters are an input.
The design principle: any Sentinel-compliant signal is a valid Council input, and the roster is the user's to choose and weight. "Compliant" is a contract, not a lock-in — an indicator earns a seat by (1) following the naming law and drawing to the skin, (2) publishing a normalized vote seam (direction + kind + freshness), and (3) being declared in the roster config. Meet the contract and you are a voter; the specific indicators we happened to test with are not privileged. A user should be able to drop in their own compliant indicator, or remove one of ours, without touching the Council's code.
Today this is only half-true: voters are wired into the fusion in code and weighted by hand, so changing the roster still means a code edit. The near-term architecture closes that gap — a generic vote registry where the Council fuses whatever compliant votes are published for its scope, with weights read from the roster config (Roster.conf already declares the voter set and per-voter weights; it is the seam this grows from). When that lands, "customize your Council" becomes a config choice, and the learning loop below fits the weights for whatever roster the user assembled — not just ours.
Everything in the suite is a stage in one loop:
┌─────────────────────────────────────────────────────────────────────────┐
│ │
▼ │
① PUBLISH each sensor computes its read and publishes a …State seam │
│ (Trend, ADX, CCI, VolEnvelope, Compression, Liquidity, WAE, │
│ GodReversal + the orthogonal axes Clock/Participation/ │
│ Location/MTF/Intermarket + the order-flow axis SentinelFlux — │
│ the first genuinely orthogonal one, a flow-synchronised bar │
│ clock) │
▼ │
② FUSE the Council reads every fresh seam, weights it, sums to a │
│ verdict: bias · conviction · sizeMult · a "why" audit │
▼ │
③ DECIDE the verdict is ADVISORY — a single honest number, not an order │
▼ │
④ ACT + GRADE ┌ the Bridge (opt-in) may act on the verdict │
│ └ the Recorder ALWAYS grades it: from each verdict it tracks │
│ the real forward outcome (which barrier — target or stop — │
│ is touched first) → an append-only corpus on disk │
▼ │
⑤ LEARN the offline Lab reads the corpus and FITS the parameters the │
│ Council guessed: first the conviction floor (where expectancy │
│ actually crosses breakeven), then the voter weights │
▼ │
⑥ ADOPT the fitted parameters FEED BACK into the live Council, which │
│ now decides with evidence-based numbers instead of guesses ──────┘
(dynamic management — the loop closes and repeats, sharper each turn)
Stage ⑥ is what makes it a loop rather than a report. Without the feedback, the Lab produces a nice number in a file and nothing changes. With it, the Council re-reads its fitted Model.conf, decides better, those better decisions get graded, and the next fit is cleaner still. This is the dynamic management target: the system tunes itself from its own measured results, and — because markets drift — keeps re-tuning as their character changes.
Today the loop is open: stages ①–⑤ exist and run; stage ⑥ is the near-term goal. The floor and weights are still hand-set, and the Lab's output is not yet wired back. Closing ⑥ is the point of the current work.
To make it concrete — a single verdict on a gold chart, from seams to graded row:
① PUBLISH Trend +1 (w 1.0) · ADX +1 (0.6) · Woodies +1 (0.8) · Compression +1 (0.7)
· VolEnvelope −1 (0.6) · GodReversal 0 (quiet trigger — abstains)
② FUSE netScore = 1.0 + 0.6 + 0.8 + 0.7 − 0.6 = 2.5
declaredW = 1.0 + 0.6 + 0.8 + 0.7 + 0.6 = 3.7 (quiet trigger excluded)
conviction = |2.5| / 3.7 = 0.68
deadband = 0.15 × 3.7 = 0.56 → netScore 2.5 clears it → bias = +1 (LONG)
floor 0.20 → 0.68 ≥ 0.20 → sizeMult = 0.68 × context(≈1.0) = 0.68
③ DECIDE verdict published: LONG · conviction 0.68 · size× 0.68
④ GRADE Recorder opens a row at fire price; barrier R = max(20t, ATR).
Six bars later the +R target is touched before the −R stop → firstTouch = +1 (win).
Row on disk: conviction 0.68 · sizeMult 0.68 · firstTouch +1
⑤ LEARN this row joins every other verdict near conviction≈0.68 → P(win | conviction) curve
⑥ ADOPT once fit, the floor/weights that produced this verdict are replaced by the ones the
outcomes justify — and the next identical setup is scored a little more truthfully
Note what nobody did: no sensor knew the Council existed, the Council knew nothing about which sensors voted (it read the bus), and the Recorder graded the verdict without any opinion about whether it was good. Each piece does one job and publishes; the coupling is the bus, not the code.
The temptation is to skip to stage ⑤ and fit something today. That is exactly the trap, because a model fit on a contaminated corpus learns the contamination. Most of this project's engineering is not the model — it is making the corpus trustworthy before a single number is fit. Three hard-won examples:
The principle: correctness precedes collection precedes learning. Each is a prerequisite for the next, and getting them out of order produces a confident model that is confidently wrong.
The three above are all contamination — rows that exist and lie. This one is the opposite failure, and it went unnoticed far longer because the corpus was clean; it was simply blind.
The Recorder's only intake was a GetCouncilState poll, so it opened every row on Open("COUNCIL", …). Every strategy was therefore invisible to the corpus by construction. Not degraded, not partial — absent. You could run a strategy for a month and have nothing to grade it with, while every dashboard stayed green, because the thing that was missing had never been a row in the first place.
⭐ That, and not exit policy, was the actual blocker behind SentinelKeel. The system built to grade decisions could not see the decisions of the only tool making them.
The fix is architectural rather than a widening: SentinelCore.NoteSignalFire(scope, dir, tag, …) is a generic door, and the Council becomes one caller among several rather than the sole source. Keel, the Bridge, or any future strategy records through the same path with the same schema. Row schema was deliberately not bumped — the row's signal field already carried the tag, so a KEEL row and a COUNCIL row differ only in a value the Lab already reads: no reader change, no corpus split.
⚠ The generalisation worth carrying: the first three examples were caught by asking whether a row was true. This one could only be caught by asking what the corpus is structurally incapable of containing — a question no amount of validating existing rows will ever raise. Corpus hygiene checks the rows you have; nothing but design review checks the rows you can never get.
Fitting a trading model is where people fool themselves most easily. Sentinel's Lab is built around the ways that happens:
The goal is not a model that looks good. It is a model whose edge survives the tests designed to destroy a fake one.
Closing the loop is not one switch; it is a progression, each rung safer and more adaptive than the last:
0.20. One number, but the one the whole size decision pivots on.WeightEye = 1.4, …. This is where "which sensors actually carry information" stops being opinion.Model.conf on load. Deterministic, auditable, reversible: a flat key=value file the C# side reads with no parser, versioned next to the configs that produced it.Throughout, one rule holds: the learned model advises size and selection; it never touches the safety layer. The kill switch, the governor, the order gate, the news and rollover vetoes are not things the model is allowed to learn its way around. Dynamic management makes the system smarter; the safety substrate keeps it survivable. Those are separate jobs and stay separate.
Model.conf) was built so that when the fit is trustworthy, adopting it is a small, safe, reversible step.The one-sentence version: Sentinel fuses honest signals into one graded decision, measures that decision against reality, and feeds the measurement back — inside a safety envelope it is never allowed to learn around.
That still describes the machine. What §1b changes is the stage: the fusion step is not yet carrying its weight, so the current work is finding signals that hold up standalone before fusing them — same envelope, same corpus, same refusal to trust anything unmeasured.