Risk Service
The Risk Service (apps/risk-service) is a Go service that gates orders via gRPC PreTradeCheck, maintains PostgreSQL config (risk_profiles, risk_rules, margin_tiers), keeps process-local (in-memory) caches for hot paths, uses Redis as shared / secondary storage fed by Kafka (and startup sync), and publishes Avro events for audit and analytics.
Scope: Portfolio
risk.exposure.v2andrisk.liquidation.trigger.v1are produced by other services (e.g. position-service). This page describes risk-service only.
Responsibilities
Section titled “Responsibilities”- Pre-trade validation: Order Service calls
PreTradeCheckbefore accepting an order; response includes decision, reject reason, margin metrics, and optional rule hits. - Layered caching: In-memory caches (low-latency reads) for mark prices, instruments (from Metadata gRPC), risk rules and margin tiers (refreshed from Postgres), and risk profiles (L1; optional Redis L2). Redis holds Kafka-fed snapshots (positions, wallet balances, open orders, instrument payloads) plus profile materialization where configured.
- Kafka → Redis (+ memory): Consumes user, position, order, engine, wallet, and instrument events so processors update Redis; mark-price and related paths also update in-memory structures used during checks.
- Mark prices: Dedicated consumer on
md.mark.v1updates both in-memory mark cache and Redis (via the mark processor). - Dynamic rules & alerts: From the pre-trade path, can emit
risk.violation.v1,risk.margin_call.v1, andrisk.liquidation.v1when configured rules or outcomes apply. - Audit & analytics Kafka:
risk.pretrade_decision.v1(every published decision) andrisk.exposure_snapshot.v1(when projected notional math ran). - Admin REST (Fiber): CRUD-style APIs for risk profiles, risk rules, and margin tiers (see below).
Architecture
Section titled “Architecture”gRPC API
Section titled “gRPC API”Defined in shared/protos/risk/v1/main.proto.
PreTradeCheck
Section titled “PreTradeCheck”rpc PreTradeCheck(PreTradeCheckRequest) returns (PreTradeCheckResponse);
message PreTradeCheckRequest { string user_id = 1; string account_id = 2; string order_id = 3; string symbol = 4; Side side = 5; OrderType order_type = 6; string quantity = 7; string price = 8; string leverage = 9; bool reduce_only = 10; bool post_only = 11; string trace_id = 12;}
message PreTradeCheckResponse { RiskDecision decision = 1; // APPROVED, REJECTED, FLAGGED RejectReason reject_reason = 2; string message = 3; string projected_margin_ratio = 4; string required_initial_margin = 5; bool liquidation_risk = 6; repeated string rule_hits = 7; string trace_id = 8;}RejectReason includes codes such as USER_FROZEN, KYC_INSUFFICIENT, MAX_LEVERAGE_EXCEEDED, EXPOSURE_LIMIT_EXCEEDED, INSUFFICIENT_MARGIN, RISK_RULE_VIOLATION, LIQUIDATION_RISK, STALE_DATA, and others — see the proto for the full enum.
Validation (high level)
Section titled “Validation (high level)”Checks combine account state (e.g. frozen), KYC, instrument and tier data from cache, open orders and positions, wallet balances, margin and exposure math, static/dynamic risk rules, and optional reduce-only logic. Exact behavior is implemented in internal/service/pretrade/.
Data models (PostgreSQL)
Section titled “Data models (PostgreSQL)”risk_profiles
Section titled “risk_profiles”| Field | Type | Description |
|---|---|---|
id | bigserial (PK) | Row id |
user_id | UUID (unique) | User |
kyc_level | varchar(20) | L0 / L1 / L2 |
exposure_limit_usdt | numeric(30,8) | Exposure cap |
frozen | boolean | Account frozen |
risk_score | numeric(10,4) | Stored risk score |
max_leverage | numeric(30,8) | Per-user max leverage cap |
last_margin_call | timestamptz | Last margin call timestamp |
last_event_id | text | Idempotency / last event |
last_event_ts | timestamptz | Last event time |
created_at / updated_at | timestamptz | Audit timestamps |
risk_rules
Section titled “risk_rules”Dynamic rules (metric, operator, threshold, severity, action REJECT/WARN, optional kyc_level and symbol). Used during pre-trade evaluation and for margin-call style alerts. See migrations under apps/risk-service/internal/infra/db/migrations/.
margin_tiers
Section titled “margin_tiers”Per-symbol (or default symbol bucket) tier ladder: tier, max_notional, maintenance_margin_pct, optional max leverage, active, timestamps.
Messaging (Kafka)
Section titled “Messaging (Kafka)”Topic names are typically environment-suffixed via BuildTopicName (e.g. dev/staging/prod).
Consumed (risk-service)
Section titled “Consumed (risk-service)”Subscribes to a shared consumer group over topics including:
| Topic | Role (typical) |
|---|---|
user.created.v1 | Risk profile / user cache |
user.updated.v1 | User cache |
user.verified.v1 | User cache |
user.kyc.updated.v1 | KYC / profile |
user.frozen.v1 / user.unfrozen.v1 | Account state |
position.updated.v1 | Position cache |
position.closed.v1 | Position cache |
risk.exposure.v2 | Aggregate per-user exposure (from position-service) |
order.command.v1 | Open orders cache |
engine.event.v1 | Fills / open orders |
wallet.balance_changed.v1 | Balance cache |
md.instrument.created.v1 | Instrument cache |
md.instrument.updated.v1 | Instrument cache |
Additionally, a dedicated reader consumes md.mark.v1 for mark prices (updates in-memory mark cache and Redis).
Published (risk-service)
Section titled “Published (risk-service)”| Topic | Description |
|---|---|
risk.pretrade_decision.v1 | Pre-trade decision audit (Avro) |
risk.exposure_snapshot.v1 | Per-check snapshot when notional math ran (analytics) |
risk.violation.v1 | Dynamic rule violation from pre-trade |
risk.margin_call.v1 | Margin call signal (e.g. ratio vs rule) |
risk.liquidation.v1 | Liquidation-related signal from pre-trade path |
The service includes schema helpers for risk.freeze_account.v1 (deserialize); it does not list that topic as a producer in bootstrap — do not assume risk-service publishes it without checking current code.
Lifecycle: PreTradeCheck
Section titled “Lifecycle: PreTradeCheck”- Order Service calls
PreTradeCheckwith user, account, order id, symbol, side, type, qty, price, leverage, flags,trace_id. - Service resolves risk profile (in-memory L1, optional Redis L2) and reads positions, balances, open orders from Redis (Kafka-fed), plus marks / instruments / rules / tiers from in-memory caches (Kafka, Metadata gRPC, or Postgres refresh, depending on domain).
- Runs phased validation and margin / exposure math; may consult margin tiers and risk rules from memory-backed caches.
- Returns gRPC response (approved/rejected + metrics + rule hits).
- On paths that publish to Kafka: emits
risk.pretrade_decision.v1; when projected notional is present, alsorisk.exposure_snapshot.v1; on specific outcomes,risk.violation.v1,risk.margin_call.v1, orrisk.liquidation.v1.
Latency: target low tens of ms p95 is typical for pre-trade; tune with your infra.
REST API
Section titled “REST API”Base path /v1 (Fiber). Swagger UI: /docs (serves docs/swagger.json). Health: /healthz, /readyz.
| Method | Endpoint | Description |
|---|---|---|
GET | /v1/risk-profiles | List risk profiles |
GET | /v1/risk-profiles/:user_id | Get profile by user id |
PATCH | /v1/risk-profiles/:user_id | Partial update profile |
POST | /v1/risk-rules | Create risk rule |
GET | /v1/risk-rules | List risk rules |
GET | /v1/risk-rules/:id | Get rule by id |
PATCH | /v1/risk-rules/:id | Partial update rule |
POST | /v1/margin-tiers | Create margin tier |
GET | /v1/margin-tiers | List margin tiers |
GET | /v1/margin-tiers/:id | Get tier by id |
PATCH | /v1/margin-tiers/:id | Partial update tier |
There is no public REST pretrade-check on this service; pre-trade is gRPC from Order Service.
Related documentation
Section titled “Related documentation”- Pre-trade risk concepts — reduce-only rules, PnL, equity, notional, leverage, margin ratio (and Kafka field meanings)
- Order Service — calls
PreTradeCheck - Position Service — portfolio exposure and liquidation trigger topics
- Kafka Events — event catalog
- Metadata Service — instruments and config