Trade8:Risk - Detailed Design Document
Audience: maintainers, reviewers, and architects modifying the service. Overview in
README.md; business functionality inBUSINESS.md.
1. Scope & Context
Owns:
- The synchronous
authorizeverdict (pre-trade gate) and its itemized check trace; the trader-state runtime (risk.trader_state); the limit catalog (risk.limit); restrictions and allowlists (risk.execution_restriction,risk.max_orders_restriction,risk.allowed_symbol); the risk-level ladder config (risk.level,risk.action,risk.level_action); theriskevent family (events::FAMILY_RISK = "risk").
Does NOT own (delegates):
- Order state → Trade8:Desk (Risk only returns verdicts; Desk persists).
- Execution and fills → Trade8:Router (Risk consumes
fill:executedfor position counters). - Day P&L and the ladder evaluation trigger → Trade8:Pnl
(
trader:summarydrives ladder evaluation here, positions are Pnl's). - Uptick inputs → Trade8:Feed (mirror comes from print events).
- Scheduling, identity, strategy execution, audit ingest → platform (Sentin8:Cron/Auth/Func/Sys).
2. Runtime Architecture
Request lifecycle - the authorize family (the only hot family)
Mutation lifecycle - limits / trader states
3. Data Model
Schema risk, db trade8; role trade8_risk_service (no BYPASSRLS);
RLS ENABLE+FORCE + the standard 4-policy env block on every table;
outbox + audit journal shell-generated. Declared cross-schema grant:
SELECT on desk.short_exempt.
risk.limit- sixkinds; scope columns nullable by design (env row = default);override_valuewins when non-null.risk.trader_state- upsert per (env, usr); the runtime trading state, not identity. Never written by Auth.risk.level_action- the threshold ladder wiring evaluated from Pnl summaries.- No state machines: rows are current-state upserts; history lives in the audit journal.
4. API Surface
Service-facing (M2M only - not proxied by BFF)
| Endpoint | Gate | Notes |
|---|---|---|
POST /v1/authorize | Shield trade8:risk:authorize | The hot path; verdict + trace; fail-closed handled by the caller |
Public (BFF-proxied, JWT bearer)
| Endpoint | Gate | Events |
|---|---|---|
GET /v1/limits · POST /v1/limits | trade8:risk:limits:read / :write | trade8:risk:limit:updated (on POST) |
GET /v1/trader-state · GET /v1/trader-state/{usr_id} | trade8:risk:limits:read | - |
POST /v1/trader-state | trade8:risk:trader:state:write | trade8:risk:trader:state |
Admin (RFW GET /v1/admin/ui/{page} + data endpoints)
| Page | Data endpoint |
|---|---|
limits | GET /v1/admin/limits |
traders | GET /v1/admin/traders |
restrictions | GET /v1/admin/restrictions |
levels | GET /v1/admin/levels |
exposure | GET /v1/admin/exposure (reserved notional, open orders, positions vs limits) |
Plus the shared Shield policy-admin API block.
Event flow (consumers and producer)
5. Cross-Cutting Concerns
- AuthN/AuthZ - full 8-step pipeline; Shield decides (
requirewithtrade8:risk:*targets); JWTscopeonly pre-filters. - Tenant resolution + RLS - mutations:
set_tenant_contextthenbegin()on the same connection; bootstrap loads run on the boot-env context; explicitWHERE env_id = $1everywhere;desk.short_exemptread is a declared cross-schema grant only. - Idempotency - reservation accounting keyed by order id; limit/ trader-state writes are idempotent upserts; consumer redelivery converges.
- Audit journaling - local
risk.audit_journal(same tx) → Sys ingest retry; deny-trace persistence is Desk's responsibility. - Fail-safety - snapshot bootstrap is logged-not-fatal (empty snapshot authorizes nothing extra: no limits means no limit denies, but trader states/restrictions also default-open - see Open Questions).
- TLS - post-quantum via
sentin8-tls; M2M cert-bound (trade8:risk-{leaf}-style workloads +-tlsinstance-read workload).
6. Failure Modes & Recovery
- Risk down → Desk fails closed (order rejected, nothing persisted).
- Snapshot bootstrap failure → logged, empty snapshot installed; the service still serves verdicts (empty limits bind nothing) - operator alerting owns the gap.
- Outbox/relay outage → mutation is durable and locally effective; remote instances refresh late (eventual consistency documented in §8).
- Crash → in-memory journal counters rebuild from desk/router events;
risk.trader_state/risk.limittruth is in the DB.
7. Concurrency & Performance Model
- The authorize path is allocation-light and lock-free (one
ArcSwapload); trace building is the only allocation-heavy step. - The journal mutex guards O(1) map ops only.
- Pool use is mutation-only (persist paths); the pool cap comes from the
shared
connecthelper. - Consumer groups:
trade8:risk:{family}×4 (desk/router/feed/pnl).
8. Invariants, Insecure-Defaults Rejected, Unsafe Rationale
- The authorize path performs no DB access, no cross-service HTTP, and no allocation-heavy work beyond the trace.
- Unknown traders default to active: Risk authorizes by explicit
restriction, never by absent permission - an env that wants
allow-list-only traders must seed
risk.trader_state(documented alternative: an env-levelcloseddefault flag - open question). - Limit rows bind only when enabled (disabled rows skipped), specificity user > office > env.
- A deny is never "silent": every deny carries a machine-readable
RejectCodeand a full check trace. - Trader-state changes are Risk data only - zero writes to any identity store.
- No
unsafeblocks.ArcSwapguarantees the read-consistency invariant; the journal mutex is scoped to map operations only (no snapshot swaps under the lock).
9. Testing Strategy
- Unit: boundary cases for buying power (reserved + notional = limit), max-position projection (short sides), closing-only close vs open, cancel-denial windows, uptick modes, allowed-list semantics.
tests/sql_matrix.rs(skip-offline): RLS matrix (no-context/cross-env → 0 rows),bootstrap_snapshotall-sections load (limits/states/allowed/ restrictions/uptick/exempt - disabled excluded),persist_limit, trader-state upsert + outbox events.tests/policy.rs: the shared Shield suites (no-set deny, shadow never blocks, M2M parity, live-role freshness, mirror parity).- Deliberately not tested here: Desk submission choreography (Desk design), Pnl ladder math (Pnl design), bus delivery (platform).
10. Open Questions
- Fail-open default for unrestricted traders - an env without seeded
states authorizes everything; a
default_trader_stateenv setting (deny-by-default) is a real option and needs a product decision. - Multi-currency buying power - the schema carries the columns; the
single-pool arithmetic needs a conversion design (
desk.currencyrates). - Ladder persistence in snapshot - move ladder thresholds from
payload-carried to boot-loaded (and hot-swapped on
limit:updated). - Restriction invalidation events - publish
trade8:risk:restriction:updatedfor multi-instance freshness. max_orders_restrictionenforcement point - table + registration exist; the check pipeline currently usesmax_open_ordersonly.
Architecture & Design: trade8:risk and its underlying concepts were designed by Hamid Jahanzad.