title: Trust Center description: Operator-facing documentation for the Trust Center reliability dashboard and the evidence spine behind it.
Trust Center
What it's for
The Trust Center (/trust in the authenticated app) answers six questions for
the person who owns this trading system, without requiring them to know the
internal evidence schema:
- Is the trading system trustworthy right now?
- What needs my attention?
- Which strategies are safe, blocked, stale, promoted, or reverted?
- Do broker reality and internal records agree?
- Does live performance still resemble tested (backtested) performance?
- Where can I inspect the evidence behind any of the above?
It is strictly read-only: every /api/trust/* endpoint is a GET request
behind session authentication (get_current_user), and the page contains no
button that places an order, promotes a strategy, or mutates trading state.
Overall status
The header shows one of three states, computed from the same data shown in the cards below it — never from "the API calls returned 200":
- Healthy — the integrity audit last ran clean, there is no unresolved drift, and no strategy is blocked.
- Needs attention — nothing is actively broken, but something needs review: the integrity audit has never run, or a strategy is stale, reverted, or has no validation on record yet.
- Critical — the integrity audit found violations, drift is unresolved, or a strategy is blocked by a rejected validation.
The badge is always shown with a one-line reason (e.g. "2 subject(s) have unresolved drift between broker reality and internal records").
How unresolved drift is calculated
Every reconciliation cycle (holdings, fills, EOD, options orphan/mismatch)
writes an evidence event: RECONCILED when the check passed, or
DRIFT_DETECTED when it found a mismatch.
Unresolved drift = the number of subjects (subject_type/subject_id
pairs, e.g. holding/RELIANCE) whose latest drift-relevant event is
DRIFT_DETECTED. The moment a later RECONCILED event lands for that
subject, it drops out of the unresolved count — regardless of how many
DRIFT_DETECTED events preceded it.
This replaced an earlier, misleading open_drift_last_7d metric that simply
counted DRIFT_DETECTED events emitted in the last 7 days — a drift that was
reconciled minutes after being detected still inflated that count for a full
week. The corrected calculation is exposed as unresolved_drift in
EvidenceReader.overview() / /api/trust/overview, and as
drift.unresolved_count in /api/trust/summary. The old, differently-named
drift_detected_last_7d field keeps the raw emission count for historical
context, clearly labeled as such — it is not used to decide overall status.
EvidenceReader.unresolved_drift_subjects() computes this with one SQL query
(EvidenceStore.latest_per_subject, a DISTINCT ON (subject_type, subject_id) ... ORDER BY ts DESC query) instead of scanning the whole event
table, so it stays cheap as evidence volume grows.
How strategy readiness is determined
arb_bot/evidence/readiness.py::classify_strategies() is a pure function
that combines, per strategy code (GoLiveManager.STRATEGIES):
- The latest
VALIDATED/REJECTEDevent forsubject_type="strategy"(the validation gate). - The latest
PROMOTED/REVERTEDevent forsubject_type="transition"(the go-live transition history). - The strategy's row in
validation.calibration(live-vs-backtest error bars), if any live/backtest trade pairs have been matched.
into one of five labels:
| Label | Meaning |
|---|---|
| ready | Latest gate is VALIDATED and within the freshness window (VALIDATION_FRESHNESS_WINDOW_DAYS, default 30 days). |
| blocked | Latest gate is REJECTED. The rejection reasons (from the validation report payload) are surfaced verbatim. |
| needs_revalidation | Latest gate is VALIDATED but older than the freshness window. |
| insufficient_evidence | No VALIDATED/REJECTED event exists for this strategy at all. |
| reverted | The latest go-live transition is REVERTED — this takes priority over the gate state, since a revert usually means something went wrong live and is worth reviewing before re-promoting. |
The Trust Center never infers ready from the absence of a rejection; a
strategy with no gate event is insufficient_evidence, not ready.
Research-run validation before promotion
Completing a research run records its result and provenance, but it does not
by itself make the run eligible for policy promotion. An independent validation
workflow must record a VALIDATED event for that backtest_run; a rejected or
missing event keeps promotion disabled. The Research page links a
not-yet-validated completed run to this read-only evidence surface so an
operator can inspect its state before running the appropriate validation flow.
This separation prevents a successful execution from being mistaken for
validation evidence.
Health cards
The Overview tab's summary cards are computed from current state, not lifetime event volume:
- Integrity audit — latest audit run's violation count.
- Unresolved drift — see above.
- Strategy validation —
ready / totalstrategies, withblockedtaking priority for the card's severity. - Live vs backtest calibration — how many strategies have at least one matched live/backtest trade pair (no pass/fail threshold is fabricated; the backend does not currently expose a calibration tolerance to the UI).
- Last reconciliation — the most recent
RECONCILEDevent across all subjects, when one exists.
Lifetime counts by event kind (CLAIMED, VALIDATED, PROMOTED, …) remain
available under the collapsed Event diagnostics panel on the Overview
tab.
Attention Required
A single prioritized list, computed client-side from
/api/trust/summary, that never fabricates data the backend didn't provide:
- Integrity audit violations (critical) — with plain-language explanations for the two current codes.
- Unresolved drift (warning) — one row per still-drifting subject.
- Blocked strategies (critical), stale/
needs_revalidationstrategies (warning), and recently reverted strategies (warning).
Each item links straight into the relevant tab — audit violations and drift rows open the Evidence Explorer preselected on that subject; strategy issues open Strategy Readiness.
Tabs
- Overview — health cards, Attention Required, the integrity audit detail panel, and the Event diagnostics expandable.
- Issues & Drift — a filterable table (cards on mobile) of every
DRIFT_DETECTED/RECONCILEDevent, with resolved/unresolved and subject type filters, a search box, and a time window. Clicking a row opens a drawer with the full event detail (payload, code SHA, config hash, data refs, previous-event link) and a link into the Evidence Explorer. - Strategy Readiness — one expandable row per strategy combining its gate, transition, and calibration state.
- Evidence Explorer — the provenance walk, made discoverable: a
subject-type selector backed by
/api/trust/subject-types, an autocomplete subject picker backed by/api/trust/subjects, and a chronological timeline for the selected subject (/api/trust/provenance). Any drift row, strategy, or attention item can deep-link here with the subject preselected.
/provenance <subject_type> <subject_id> (Telegram)
Walk the immutable evidence chain for any subject from Telegram. Returns a
chronological list of events (CLAIMED, VALIDATED, PROMOTED,
RECONCILED, DRIFT_DETECTED, REVERTED) with their actor, git SHA,
config hash, and prior event link — the same data the Evidence Explorer tab
shows.
Examples:
/provenance transition DC— go-live transition history for the DC strategy/provenance holding RELIANCE— holdings reconciliation history/provenance signal DC:20260706103000— the scan decision at that cycle
/explain_signal [strategy] [n]
/explain_signal(no args): the current-cycle in-memory last scan result for the active strategy./explain_signal DC 10: the last 10 persisted scanner decisions for DC, ordered newest first.
Integrity audit job
arb_bot.evidence.audit_job.run_integrity_audit() is an operator-scheduled
(cron / systemd timer) job that verifies the spine:
- BROKEN_CHAIN — an event whose
prev_event_idpoints to an event_id that does not exist inevidence.events. - MISSING_PROVENANCE — an internal event (
sig_type='internal') of kindCLAIMED/VALIDATED/PROMOTED/REVERTEDwith neither acode_shanor aconfig_hash.
Each run writes one evidence.audit_runs row and one
evidence.audit_violations row per violation. The latest run (with its
violations) is queryable at /api/trust/audit and is embedded in
/api/trust/summary. The job is best-effort: it logs and returns on any
storage failure; it never raises into the bot loop.
API reference
All endpoints require authentication (Depends(get_current_user)) and are
read-only.
| Endpoint | Purpose |
|---|---|
| GET /api/trust/summary | Consolidated snapshot driving the header, health cards, Attention Required, and Strategy Readiness — one SQL-aggregated round trip instead of six parallel requests. |
| GET /api/trust/overview | Legacy lightweight counts endpoint. open_drift_last_7d was removed (see semantic correction above) and replaced with unresolved_drift and drift_detected_last_7d. |
| GET /api/trust/drift | Filterable drift/reconciliation timeline. Params: limit (1–500), since (ISO) or since_days (1–3650, computed server-side), subject_type, unresolved (bool), q (subject search). Each row carries an unresolved flag. |
| GET /api/trust/gates | Latest VALIDATED/REJECTED event per subject. Optional subject_type filter (Strategy Readiness uses subject_type=strategy). |
| GET /api/trust/transitions | PROMOTED/REVERTED history, newest first (limit 1–500). |
| GET /api/trust/calibration | validation.calibration mean/max |Δ₹| per strategy. |
| GET /api/trust/audit | Latest completed integrity-audit run, with violations. |
| GET /api/trust/provenance | Full prev_event_id chain for subject_type+subject_id (both required, non-empty). |
| GET /api/trust/subject-types | Distinct subject_type values seen in evidence.events — powers the Evidence Explorer's type selector. |
| GET /api/trust/subjects | Distinct subject_ids for a subject_type, optionally filtered by q, most-recently-active first (limit 1–50). |
overview(), unresolved_drift_subjects(), and gate_status() all use
EvidenceStore.latest_per_subject() (DISTINCT ON) or count_by_kind()
(GROUP BY) instead of pulling the full evidence.events table into Python
— the previous overview() implementation read up to 10,000,000 rows per
call. latest_audit() similarly paginates violations (violations_limit/
violations_offset, default page size 200; /api/trust/summary embeds a
tighter 50-row page) instead of returning every violation for the latest
run in one response — the run's violation_count field is always the true
total, independent of the page size, and /api/trust/audit supports full
pagination through the Audit panel.
Deployed git SHA (GIT_SHA)
EvidenceWriter.emit() auto-fills code_sha via git_sha() for every
internal event. Production containers have neither a .git directory
(scripts/docker_deploy.sh excludes it from the rsync payload) nor the
git binary installed, so the subprocess-based fallback in
git_sha() always silently returned None there. Combined with
high-frequency internal CLAIMED events that never set config_hash
either (e.g. signal scanner-decision claims, one per scan cycle), this
produced a steady stream of false MISSING_PROVENANCE audit violations —
because MISSING_PROVENANCE only fires when both code_sha and
config_hash are missing.
scripts/docker_deploy.sh now resolves the SHA on the deploying machine
(where .git and git are both available) and records it into the remote
.env as GIT_SHA before restarting containers; git_sha() reads that
env var first. This stops new violations of this kind from accumulating
for every internal event.
Separately, signal (the scanner's per-scan-cycle decision claim, see
arb_bot/scanners/persistence.py) is exempt from the MISSING_PROVENANCE
check entirely (_PROVENANCE_EXEMPT_SUBJECT_TYPES in
arb_bot/evidence/audit_job.py): it's high-frequency operational
telemetry, not a go-live-affecting trust claim, so requiring code/config
provenance on every scan cycle produced noise (thousands of violations)
disproportionate to its value. VALIDATED/PROMOTED/REVERTED — the
kinds that actually gate live trading decisions — are unaffected by this
exemption and still require provenance regardless of subject_type. This
exemption applies from the next audit run onward; evidence.events is
append-only, so historical signal events already in the ledger keep
whatever code_sha/config_hash they were written with, but they no
longer count toward missing_provenance on any audit run after this
change shipped.
What the Trust Center does not do
- It does not place orders, promote or revert strategies, or delete/mutate any evidence or trading state — every action is a link to more evidence, never a mutation.
- It does not fabricate a calibration pass/fail verdict — the backend does not currently expose a calibration tolerance, so the UI only reports how many strategies have matched trade pairs and their error bars.
- It does not claim "no issues" when a request failed — loading, empty, and error states are visually and semantically distinct throughout, and a failed fetch never silently renders as a healthy or empty result.
Retention policy
evidence.events is append-only by default. The evidence.retention_policy
table is the only opt-out: an operator inserts a row marking a specific
(subject_type, kind) as prunable=true with a keep_days window, and the
evidence.prune_expired() SQL function deletes only matching rows older than
the window. The table ships empty — by default nothing is pruned.
Convention: the trust-critical kinds (VALIDATED, PROMOTED, REVERTED,
REJECTED, DRIFT_DETECTED, RECONCILED) are never made prunable. Only
high-volume snapshot-style claims (e.g. operator-selected holding CLAIMED)
qualify for bounded retention, mirroring the existing iv_snapshots /
battle_log / broker_health_samples TTLs.
Live math = research math, provably
PolicyLedger reuses research/engine/costs.py + tax.py, so a live policy
and its source backtest share the same cost/tax math by construction. The
spine makes this checkable: both emit evidence events pinned to the same
config_hash (Phase 1 provenance). A parity test
(tests/evidence/test_parity_spine.py) builds a synthetic backtest run and a
policy ledger from the same resolved config and asserts byte-equal P&L. A
drift between the two is a bug, not a regression.