The Position Service is a Go microservice responsible for tracking open positions, computing mark-to-market P&L, aggregating exposure across accounts, and monitoring positions for liquidation triggers.
Position Tracking : Maintain per-user, per-symbol position records updated in real-time from trade executions.
P&L Computation : Calculate unrealized P&L using mark price, realized P&L from closed trades.
Exposure Aggregation : Aggregate long/short exposure across all symbols per account in USDT terms.
Margin Calculations : Compute initial margin, maintenance margin, and margin utilization.
Liquidation Monitoring : Monitor positions against maintenance margin and emit liquidation events when thresholds are breached.
Event Publishing : Emit position.update.v1, position.closed.v1, and risk.exposure.v1 events.
The Position Service does not execute liquidations — it detects the need for liquidation and emits events. The Settlement Service orchestrates the actual liquidation flow.
graph TB
ME[Matching Engine] -->|engine.event.v1| Kafka1[Kafka]
MDS[Market Data Service] -->|md.mark.v1| Kafka1
FundingSvc[Funding Service] -->|funding.payment.v1| Kafka1
Kafka1 --> PosSvc[Position Service]
PosSvc -->|Stores| PG[(PostgreSQL)]
PosSvc -->|Events| Kafka2[Kafka]
Kafka2 --> Settlement[Settlement Service]
Kafka2 --> WalletSvc[Wallet Service]
PosSvc -->|gRPC| OrderSvc[Order Service]
PosSvc -->|gRPC| RiskSvc[Risk Service]
Field Type Description idUUID (PK) Position ID user_idUUID Position owner account_idUUID Trading account symbolVARCHAR(32) Instrument symbol sideENUM(long, short) Position direction quantityNUMERIC(24,12) Current position size entry_priceNUMERIC(24,12) Volume-weighted average entry price mark_priceNUMERIC(24,12) Latest mark price liquidation_priceNUMERIC(24,12) Computed liquidation price unrealized_pnlNUMERIC(24,12) Current unrealized P&L realized_pnlNUMERIC(24,12) Cumulative realized P&L leverageINT Applied leverage initial_marginNUMERIC(24,12) Initial margin requirement maintenance_marginNUMERIC(24,12) Maintenance margin requirement margin_ratioNUMERIC(10,6) Current margin ratio close_reasonENUM(none, user_close, liquidation, adl, expiry) Close reason opened_atTIMESTAMPTZ Position open time closed_atTIMESTAMPTZ Position close time (NULL if open) updated_atTIMESTAMPTZ Last update time
Field Type Description idUUID (PK) History entry ID position_idUUID (FK) References positions trade_idUUID Trade that caused the change prev_quantityNUMERIC(24,12) Quantity before trade new_quantityNUMERIC(24,12) Quantity after trade fill_priceNUMERIC(24,12) Trade fill price fill_qtyNUMERIC(24,12) Trade fill quantity realized_pnl_deltaNUMERIC(24,12) P&L realized by this trade created_atTIMESTAMPTZ Entry timestamp
Field Type Description idUUID (PK) Exposure record ID user_idUUID Account owner account_idUUID Trading account total_usdt_exposureNUMERIC(24,12) Total exposure in USDT long_exposureNUMERIC(24,12) Sum of long position notional values short_exposureNUMERIC(24,12) Sum of short position notional values margin_usedNUMERIC(24,12) Total margin allocated margin_availableNUMERIC(24,12) Remaining available margin updated_atTIMESTAMPTZ Last computation time
Method Endpoint Description GET/v1/positionsList all open positions for user GET/v1/positions/:symbolGet position for specific symbol GET/v1/positions/historyPosition history (paginated) GET/v1/positions/aggregateAggregated exposure across all symbols
RPC Description Called By GetPosition(user_id, symbol)Get current position Order Service, Risk Service GetAllPositions(user_id)Get all open positions Risk Service, Settlement Service GetExposure(user_id)Get aggregated exposure Risk Service CheckPositionLimit(user_id, symbol, additional_qty)Check if position limit allows order Order Service
Topic Description engine.event.v1 (TRADE events)Trade executions — primary trigger for position updates md.mark.v1Mark price updates — triggers P&L recomputation funding.payment.settled.v1Funding payments — adjusts position P&L
Topic Description position.update.v1Position changed (quantity, P&L, margin) position.closed.v1Position fully closed risk.exposure.v1Aggregated exposure update risk.liquidation.trigger.v1Position breached maintenance margin — liquidation needed
Consume TRADE event from engine.event.v1
Load existing position for (user_id, symbol) — create if first trade
Position Netting :
Same direction trade: increase quantity, update entry_price (volume-weighted average)
Opposite direction trade: reduce quantity, realize P&L
If quantity reaches zero: close position, set close_reason = user_close
If trade flips direction: close old position, open new one in opposite direction
Insert position_history row with the delta
Recompute: unrealized_pnl, initial_margin, maintenance_margin, margin_ratio, liquidation_price
Update exposures table
Emit position.update.v1
If position closed: emit position.closed.v1
Consume md.mark.v1 for a symbol
Load all open positions for that symbol
Recompute unrealized_pnl:
Long: (mark_price - entry_price) * quantity
Short: (entry_price - mark_price) * quantity
Recompute margin_ratio = maintenance_margin / (initial_margin + unrealized_pnl)
If margin_ratio >= 1.0: emit risk.liquidation.trigger.v1
Update position and exposure records
Emit position.update.v1 and risk.exposure.v1
Consume funding.payment.settled.v1
Load position for (user_id, symbol)
Adjust realized_pnl by funding amount
Emit position.update.v1
When a position’s maintenance margin is breached:
Compute margin_ratio = maintenance_margin / account_equity
If margin_ratio >= liquidation_threshold (e.g., 1.0):
Emit risk.liquidation.trigger.v1 with position details
The Settlement Service picks this up and orchestrates liquidation
Position Service does NOT directly close the position — it waits for the Settlement Service to route a liquidation order through the normal Order → Matching pipeline
Per-User + Per-Symbol Locking : PostgreSQL advisory locks keyed by hash(user_id, symbol) prevent concurrent position updates for the same position
Idempotent by trade_id : Position updates check if trade_id already exists in position_history — prevents double-application of fills
Ordered Processing : Kafka partition key is symbol, ensuring all trades for a symbol arrive in order
Long: unrealized_pnl = (mark_price - entry_price) * quantity
Short: unrealized_pnl = (entry_price - mark_price) * quantity
Long close: realized_pnl = (fill_price - entry_price) * fill_quantity
Short close: realized_pnl = (entry_price - fill_price) * fill_quantity
Long: liquidation_price = entry_price * (1 - 1/leverage + maintenance_margin_rate)
Short: liquidation_price = entry_price * (1 + 1/leverage - maintenance_margin_rate)
position_update_total{symbol} — Position updates processed
position_opened_total{symbol,side} — New positions opened
position_closed_total{symbol,reason} — Positions closed by reason
position_liquidation_trigger_total{symbol} — Liquidation triggers emitted
exposure_usdt_gauge{user_id} — Current USDT exposure per user
position_processing_latency_ms — Processing latency histogram
Spans: position.process_trade, position.update_mark, position.compute_pnl, position.check_liquidation, position.update_exposure
Structured JSON via zap: user_id, symbol, position_id, trade_id, quantity, entry_price, mark_price, unrealized_pnl, trace_id.
Scenario Handling Duplicate trade event Idempotent: position_history.trade_id uniqueness check Mark price stale Continue with last known price; alert if staleness exceeds threshold DB write failure Kafka consumer pauses; retries with exponential backoff Exposure calculation error Log error, skip exposure update, alert ops
Metric Target Position update latency (p95) < 50 ms Liquidation detection latency (p95) < 500 ms after mark price change Exposure aggregation freshness < 2 seconds Position update success rate >= 99.99%
Variable Description Default POSTGRES_URLPostgreSQL connection string Required KAFKA_BROKERSKafka broker addresses Required GRPC_PORTgRPC server port 50051 HTTP_PORTREST API port 8080 MARK_PRICE_STALENESS_MSMax mark price age 5000 LIQUIDATION_THRESHOLDMargin ratio trigger 1.0
Language : Go 1.21+
Database : PostgreSQL with SQLC code generation
Migrations : Atlas
Kafka : confluent-kafka-go with Avro
gRPC : google.golang.org/grpc
Metrics : Prometheus client_golang
Logging : zap structured logging
Hot Reload : air (development)