Skip to content

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.v2 and risk.liquidation.trigger.v1 are produced by other services (e.g. position-service). This page describes risk-service only.

  • Pre-trade validation: Order Service calls PreTradeCheck before 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.v1 updates 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, and risk.liquidation.v1 when configured rules or outcomes apply.
  • Audit & analytics Kafka: risk.pretrade_decision.v1 (every published decision) and risk.exposure_snapshot.v1 (when projected notional math ran).
  • Admin REST (Fiber): CRUD-style APIs for risk profiles, risk rules, and margin tiers (see below).
graph TB OrderSvc[Order Service] -->|gRPC PreTradeCheck| Risk[Risk Service] KafkaIn[Kafka] -->|cache consumers| Risk MarkKafka[md.mark.v1] -->|mark consumer| Risk Risk -->|optional gRPC| Wallet[Wallet Service] Risk -->|optional gRPC| Position[Position Service] Risk -->|optional gRPC| Metadata[Metadata Service] Risk -->|Avro producers| KafkaOut[Kafka] Risk --> PG[(PostgreSQL)] Risk --> Redis[(Redis)] Risk --> Mem["In-memory L1: marks, instruments, rules, tiers, profiles"]

Defined in shared/protos/risk/v1/main.proto.

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.

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/.

FieldTypeDescription
idbigserial (PK)Row id
user_idUUID (unique)User
kyc_levelvarchar(20)L0 / L1 / L2
exposure_limit_usdtnumeric(30,8)Exposure cap
frozenbooleanAccount frozen
risk_scorenumeric(10,4)Stored risk score
max_leveragenumeric(30,8)Per-user max leverage cap
last_margin_calltimestamptzLast margin call timestamp
last_event_idtextIdempotency / last event
last_event_tstimestamptzLast event time
created_at / updated_attimestamptzAudit timestamps

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/.

Per-symbol (or default symbol bucket) tier ladder: tier, max_notional, maintenance_margin_pct, optional max leverage, active, timestamps.

Topic names are typically environment-suffixed via BuildTopicName (e.g. dev/staging/prod).

Subscribes to a shared consumer group over topics including:

TopicRole (typical)
user.created.v1Risk profile / user cache
user.updated.v1User cache
user.verified.v1User cache
user.kyc.updated.v1KYC / profile
user.frozen.v1 / user.unfrozen.v1Account state
position.updated.v1Position cache
position.closed.v1Position cache
risk.exposure.v2Aggregate per-user exposure (from position-service)
order.command.v1Open orders cache
engine.event.v1Fills / open orders
wallet.balance_changed.v1Balance cache
md.instrument.created.v1Instrument cache
md.instrument.updated.v1Instrument cache

Additionally, a dedicated reader consumes md.mark.v1 for mark prices (updates in-memory mark cache and Redis).

TopicDescription
risk.pretrade_decision.v1Pre-trade decision audit (Avro)
risk.exposure_snapshot.v1Per-check snapshot when notional math ran (analytics)
risk.violation.v1Dynamic rule violation from pre-trade
risk.margin_call.v1Margin call signal (e.g. ratio vs rule)
risk.liquidation.v1Liquidation-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.

  1. Order Service calls PreTradeCheck with user, account, order id, symbol, side, type, qty, price, leverage, flags, trace_id.
  2. 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).
  3. Runs phased validation and margin / exposure math; may consult margin tiers and risk rules from memory-backed caches.
  4. Returns gRPC response (approved/rejected + metrics + rule hits).
  5. On paths that publish to Kafka: emits risk.pretrade_decision.v1; when projected notional is present, also risk.exposure_snapshot.v1; on specific outcomes, risk.violation.v1, risk.margin_call.v1, or risk.liquidation.v1.

Latency: target low tens of ms p95 is typical for pre-trade; tune with your infra.

Base path /v1 (Fiber). Swagger UI: /docs (serves docs/swagger.json). Health: /healthz, /readyz.

MethodEndpointDescription
GET/v1/risk-profilesList risk profiles
GET/v1/risk-profiles/:user_idGet profile by user id
PATCH/v1/risk-profiles/:user_idPartial update profile
POST/v1/risk-rulesCreate risk rule
GET/v1/risk-rulesList risk rules
GET/v1/risk-rules/:idGet rule by id
PATCH/v1/risk-rules/:idPartial update rule
POST/v1/margin-tiersCreate margin tier
GET/v1/margin-tiersList margin tiers
GET/v1/margin-tiers/:idGet tier by id
PATCH/v1/margin-tiers/:idPartial update tier

There is no public REST pretrade-check on this service; pre-trade is gRPC from Order Service.