The engine dashboard and the drift verdict

The console at /admin/ai/binary-engine — what every figure counts and over what window, how the drift verdict is decided, why a new engine reads "too few trades", and how to read the settlement verdict breakdown.

10 min readUpdated 6 August 2026dashboard, drift, win-rate, settlement, monitoring

The dashboard at /admin/ai/binary-engine is the addon's landing screen and the one every other page here sends you back to. It answers three questions and nothing else: is any running engine outside the band you configured, are settlements actually being steered, and did anyone pull the kill switch.

It is a single GET /api/admin/ai/binary-engine/dashboard call, gated on access.ai.binary_engine. Without that permission the menu entry does not render and the endpoint returns 403. The whole console also sits behind the binary_ai_engine licence gate, so an unlicensed install shows the navigation and a licence message rather than figures.

Engine counts, today's live stats, per-engine drift verdicts, the settlement verdict histogram, the last 24h of events, and whether the emergency stop fired

The page polls every 30 seconds, and only while the tab is visible. Only the first load draws skeletons — a background refresh replaces the figures in place, so a screen left open all day never collapses. The live dot at the top left says which state you are in: Live, Updating…, or Figures are stale when the last poll failed.

The page header carries three controls and no others: Refresh, Manage Engines (which opens /admin/ai/binary-engine/engine), and Emergency Stop. Everything else on the screen is a figure or a link into the rows behind it.

Everything here is live money only

Every count, rate and profit figure on this screen is filtered to isDemo: false. Practice and demo trades are deliberately excluded from the day totals, from the settlement histogram, and from the per-engine period counters the drift verdict reads. A busy demo cohort moves nothing on this page.

The panels are also measured over different windows, which is the single most common misreading of the screen — two figures that look like they should agree usually do not cover the same period:

Panel Window
Engine counts (active / paused / stopped) Right now
Today's numbers and the four KPI tiles Since the backend server's local midnight
Per-engine win rate, W/L and profit — and the drift verdict Since that engine's last period reset
Settlement decisions Rolling last 24 hours
Recent engine events Rolling last 24 hours, newest 20

Server midnight is the backend process's local midnight, not UTC and not your browser's. (The hourly buckets under Analytics → Time are UTC — a different convention on a different screen.)

The profit unit is derived, not assumed

platformProfit is a bare decimal on the position row; nothing in the model records a currency. The unit is each market's quote asset, so the console reads the quote off every engine's symbol and prints it only when the whole fleet agrees on one. With BTC/USDT and ETH/BTC both open there is no single unit, so the profit figures are printed as bare numbers with no ticker and no $. That is correct, not a formatting bug.

The summary block

Four counts over every engine row, unbounded — a true total, not a page.

Field What it counts
totalEngines Every row in binary_ai_engine
activeEngines status = ACTIVE
pausedEngines status = PAUSED
stoppedEngines status = STOPPED

They appear as three badges on the Engines card, and activeEngines is what the kill-switch line in the masthead counts — "Emergency stop armed — would halt N running engine(s)", or "Nothing is running to stop" when none are ACTIVE. "Armed" is not a flag the engine exposes; it is simply whether the control still has anything to act on.

Today's numbers

Counted from binary_ai_engine_position rows with settledAt at or after server midnight and an outcome other than PENDING.

Field What it counts
totalOrders Every settled live position today
totalWins Of those, outcome = WIN
totalLosses Of those, outcome = LOSS
overallWinRate totalWins / totalOrders
totalPlatformProfit The sum of platformProfit across all of them

totalOrders is every non-pending settlement, which includes DRAW outcomes. Wins and losses will not always add up to it, and overallWinRate is wins over all settlements — so on a market that produces draws it reads slightly lower than wins over wins-plus-losses. Compare like with like when you check it against an engine's own statistics.

The four KPI tiles are these figures plus one derived comparison:

Tile Shows Opens
Win rate today overallWinRate, described against the unweighted mean target of the running engines Engines
Settled today totalOrders, with the win/loss split beneath it Analytics
Platform profit today totalPlatformProfit, signed — a positive figure is money the desk kept, a negative one is money it paid out Analytics
Engines off target Running engines whose drift verdict is above or below Engines

When no engine is ACTIVE the win-rate tile says so instead of comparing against a target nobody is currently aiming at.

The drift verdict

Every engine on the payload carries a driftVerdict. It is computed on the server, by classifyDrift in utils/assessment.ts — the same function the engine record page opens with, so the console and the drill-down cannot disagree about which engines are drifting. The browser keeps an identical fallback copy of the rule for a payload from an older backend; when the server field is present, it wins.

The inputs are the engine's own live period counters and its own tolerance:

settled = currentPeriodWins + currentPeriodLosses
band    = winRateVariance   (or 0.05 when the column is zero or missing)
delta   = currentWinRate - targetUserWinRate
Verdict Rule What it means
insufficient settled < 20 Not enough has settled this period to make any claim
above delta > band Traders are winning more often than configured — the desk is bleeding
below delta < -band Traders are winning less often than configured — a fairness and complaint risk
on_target otherwise Inside the engine's own band; the engine is not correcting

The band is the engine's own winRateVariance, never a fixed 5%. The DEFAULT_VARIANCE of 0.05 in the same module is only a fallback for an engine whose column reads zero — a zero band would put every engine permanently off target. If you tuned an engine's variance, the verdict respects your number.

DRIFT_MIN_SETTLED = 20

Below twenty settled trades in the current period, the verdict is insufficient and the realised win rate on the card means nothing. This is a display floor and it is ours: the engine has no minimum trade-count parameter to borrow (minPositionForOptimization is a position size in quote units, not a count), so rather than call an engine "above target" on its first winning trade the console declares the floor and says so.

A fresh engine has settled nothing, so the console card draws its win-rate bar at 0.0% with a too few trades badge, and the engine record page shows the target as the current rate along with how many more settlements it needs. Both mean the same thing — nothing has settled yet — and neither is a fault. Nothing on this screen is a claim about that engine until twenty live trades have settled in its current period.

The same applies after every period reset. Rolling a period zeroes currentPeriodWins, currentPeriodLosses and currentPeriodPlatformProfit, so a fleet you have just reset drops back to insufficient across the board until each engine settles twenty more trades. A reset immediately after a target change is usually the right move — see Engines — but expect the drift meter to go grey for a while afterwards.

The drift meter

The band across the masthead splits the running engines by verdict — a paused engine's win rate is a frozen number, not a drift, so it is not counted. Segments are drawn in a declared order, trouble first: above, below, on_target, insufficient. A single-engine segment is widened to a visible minimum rather than rounding away, and the width is taken back from the widest band.

Which engines you see first

The grid previews six engine cards and then offers View all N engines. Which six is a decision, not insertion order: cards are sorted by how much they need an operator, and ties break on the size of the miss, largest first.

Rank Card
1 ACTIVE, above target
2 ACTIVE, below target
3 ACTIVE, insufficient
4 ACTIVE, on_target
5 Anything not ACTIVE

Note that on_target sorts after insufficient, not before it: a measured, healthy engine needs less attention than one you cannot yet judge.

The same ladder is declared server-side as severityRank in utils/assessment.ts, next to the verdict it depends on. That version carries one extra rung above everything else — an engine that is ACTIVE but blocked from steering by a gate the server can check from the database — and it sorts PAUSED ahead of STOPPED at the bottom rather than lumping them together. The two agree on the ordering of the four drift verdicts, which is the part you see on this screen.

What a card shows

Element Source
Symbol The attached market maker's ecosystem market, currency/pair; Unknown when no market maker is attached
Status badge ACTIVE / PAUSED / STOPPED
Practice badge Shown when practiceMode is anything other than DISABLED
Whale detection / User tiers chips The engine's enableWhaleDetection and enableUserTiers switches
Drift badge The verdict, shown on running engines whenever it is not on_target
The bar Fill is the realised period win rate; the tick is the target. The gap between them is the drift
Period W/L, trades currentPeriodWins / currentPeriodLosses since the last reset
Platform profit this period currentPeriodPlatformProfit, signed

The card body opens the engine record page. Beneath it sits one control on its own rail — Pause, Resume or Start, depending on the engine's status.

It changes what every trader on that market settles against from the next settlement onward. The control opens a confirmation naming the exact market, takes an optional reason, and issues PUT /api/admin/ai/binary-engine/engine/{id}/status. The reason is written to the engine action log.

A STOPPED engine's control says Start, not Resume, because that transition runs through the activation path: it re-checks that the attached AI Market Maker is ACTIVE and can legitimately refuse. The server's message is shown as-is when it does — it names a precondition you can go and fix.

The settlement verdict breakdown

The settlementDecisions panel is the one this addon's other pages keep sending you to. It counts every live settlement of the last 24 hours by the reason recorded on the position row (manipulationDetails.settlementDecision), sorted by frequency, with each row's share of the total.

This is a full population, not a sample — every non-demo settlement in the window is counted. The (+Nm) lateness suffix on FAIR:EXPIRY_MINUTE_CLOSED is stripped so the counter groups; the exact lateness stays readable per position on the engine's positions screen.

Reading it takes one rule:

  • STEERED — the close was nudged inside the band and published.
  • STEERED:BUCKET_PIN — the close already published for that expiry minute was reused, so every order in the bucket settled on one number.
  • FAIR:<reason> — the engine refused to steer and settled on the honest close. Every one of these is a deliberate, recorded refusal, not an error.

An ACTIVE engine whose entire 24 hours reads FAIR:* is steering nothing, and the reason is named right there. Each code is explained in Pricing and settlement, and the diagnostic path for each one — which switch, which screen, which fix — is Troubleshooting.

It is the only row this panel flags as a warning, and the only verdict that means something is wrong with the platform rather than with your configuration. It means settlement ran after the candle covering expiry had already sealed, so the steered close could no longer be published into the series clients see. The refusal is correct. A rising share of it means settlements are running late — look at cron health and database latency, not at the engine.

When nothing has settled in the window the panel says so rather than drawing an empty chart.

Recent engine events, and the emergency stop

The events column lists the newest engine actions from the last 24 hours, filtered to EMERGENCY_STOP, CORRELATION_ALERT, WHALE_DETECTED, WHALE_ORDER_DETECTED and RISK_ALERT.

It is a sample, not a population. Never count it, and never conclude from a type being absent that it did not happen — twenty whale rows will push everything else off the list. The caption on the panel states the cap. Anything the console asserts as a fact gets its own query instead.

One of those five is a historical spelling and one is knowingly inert. WHALE_DETECTED is what the audit logger actually writes; WHALE_ORDER_DETECTED is kept only so an older install still surfaces. RISK_ALERT is never written as an action type at all — the risk manager records it as a CONFIG_CHANGE row with details.alertType = "RISK_ALERT" — so it matches nothing. Do not read its absence as "no risk alerts fired".

Each row shows the event, the time on your own wall clock, the symbol, and the readable part of its details — the reason for an emergency stop, the deviation percent for a correlation break. Rows link to the engine they belong to.

"Did the kill switch fire?" is answered separately. lastEmergencyStop is its own server-side lookup over the same 24-hour window, precisely because it cannot be answered from a list capped at 20. When it returns a row, a destructive banner sits above the fold with the time, the reason and who triggered it, and the masthead repeats the time on the kill-switch line.

The Emergency Stop button in the page header stops every ACTIVE engine at once. It requires a typed reason, which is recorded on every affected engine. Recovery is not a single click — each engine has to be reactivated individually, and each reactivation re-runs the market maker check. Reach for Global Pause on the Settings page first if you want something instantly reversible; the full comparison is in Risk controls.

From the verdict to the engine

A drift badge on the console is where the investigation starts; the engine record page is where it continues. Opening a card gives you the same verdict from the same server function, plus the gates that engine is failing, its period clock and its loss budget.

Two constants govern the period meter there, both declared alongside the drift logic in utils/assessment.ts:

  • PERIOD_NEARLY_DUE = 0.9 — once a period is more than 90% spent, the drift alert on the record page offers a Reset period button beside Adjust targeting. Below that threshold retargeting alone is the suggestion, because resetting a period that has most of its window left throws away a measurement you are about to need.
  • The period is overdue when its window is fully spent and the reset has not run. The sweep only visits ACTIVE engines once a minute, so a few seconds of this is normal; a persistent overdue state means the counters the whole win-rate controller reads are stale, and the record page raises its own alert for it.

When the dashboard itself fails

A failed poll never wipes figures that are already on screen. The page keeps the last good values, adds a Refresh failed notice, and the live dot changes to Figures are stale — so a green pulse never sits above a clock that stopped.

A failed first load replaces the body entirely with a Dashboard unavailable message, Try again, and a link to the engine registry, which does not depend on this aggregate. It deliberately does not render a console full of zeros: every zero would be a claim the page cannot support. The header stays mounted on that branch, so Emergency Stop is still reachable when the aggregate is down.

If the screen loads but every figure is zero on a platform you know is trading, that is a real answer and not a failure — check that engines exist, that they are ACTIVE, and that the settlement panel is not reporting FAIR:NO_ENGINE_LOADED, which means the cron is not running.