Skip to content

Position Service

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]
FieldTypeDescription
idUUID (PK)Position ID
user_idUUIDPosition owner
account_idUUIDTrading 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
leverageINTApplied 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_atTIMESTAMPTZPosition open time
closed_atTIMESTAMPTZPosition close time (NULL if open)
updated_atTIMESTAMPTZLast update time
FieldTypeDescription
idUUID (PK)History entry ID
position_idUUID (FK)References positions
trade_idUUIDTrade 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_atTIMESTAMPTZEntry timestamp
FieldTypeDescription
idUUID (PK)Exposure record ID
user_idUUIDAccount owner
account_idUUIDTrading 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_atTIMESTAMPTZLast computation time
MethodEndpointDescription
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
RPCDescriptionCalled By
GetPosition(user_id, symbol)Get current positionOrder Service, Risk Service
GetAllPositions(user_id)Get all open positionsRisk Service, Settlement Service
GetExposure(user_id)Get aggregated exposureRisk Service
CheckPositionLimit(user_id, symbol, additional_qty)Check if position limit allows orderOrder Service
TopicDescription
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
TopicDescription
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
  1. Consume TRADE event from engine.event.v1
  2. Load existing position for (user_id, symbol) — create if first trade
  3. 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
  4. Insert position_history row with the delta
  5. Recompute: unrealized_pnl, initial_margin, maintenance_margin, margin_ratio, liquidation_price
  6. Update exposures table
  7. Emit position.update.v1
  8. If position closed: emit position.closed.v1
  1. Consume md.mark.v1 for a symbol
  2. Load all open positions for that symbol
  3. Recompute unrealized_pnl:
    • Long: (mark_price - entry_price) * quantity
    • Short: (entry_price - mark_price) * quantity
  4. Recompute margin_ratio = maintenance_margin / (initial_margin + unrealized_pnl)
  5. If margin_ratio >= 1.0: emit risk.liquidation.trigger.v1
  6. Update position and exposure records
  7. Emit position.update.v1 and risk.exposure.v1
  1. Consume funding.payment.settled.v1
  2. Load position for (user_id, symbol)
  3. Adjust realized_pnl by funding amount
  4. Emit position.update.v1

When a position’s maintenance margin is breached:

  1. Compute margin_ratio = maintenance_margin / account_equity
  2. 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
  3. 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.

ScenarioHandling
Duplicate trade eventIdempotent: position_history.trade_id uniqueness check
Mark price staleContinue with last known price; alert if staleness exceeds threshold
DB write failureKafka consumer pauses; retries with exponential backoff
Exposure calculation errorLog error, skip exposure update, alert ops
MetricTarget
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%
VariableDescriptionDefault
POSTGRES_URLPostgreSQL connection stringRequired
KAFKA_BROKERSKafka broker addressesRequired
GRPC_PORTgRPC server port50051
HTTP_PORTREST API port8080
MARK_PRICE_STALENESS_MSMax mark price age5000
LIQUIDATION_THRESHOLDMargin ratio trigger1.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)