Market Maker Service
The Market Maker Service (MMS) is a production-grade Go microservice responsible for providing automated liquidity on TradeX through the Pool Engine architecture — a multi-participant, Avellaneda-Stoikov based quoting system with per-member revenue attribution and monthly settlement.
For step-by-step operational procedures see the Market Maker SOP.
Overview
Section titled “Overview”| Responsibility | Details |
|---|---|
| Quoting | A-S optimal spread, dynamically adjusted for inventory and volatility |
| Multi-participant | Multiple liquidity providers trade under one coordinated pool per symbol |
| Risk isolation | Per-member position limits; per-pool aggregate kill switch |
| Revenue sharing | Daily Q-score scoring, monthly settlement with configurable split |
| Shadow mode | MM_SHADOW_MODE=true computes quotes without placing orders |
| Admin panel | Full CRUD + lifecycle controls at http://localhost:3001 |
Architecture
Section titled “Architecture”Actor model
Section titled “Actor model”One PoolEngine goroutine runs per symbol. It owns an event channel and processes all events
single-threadedly — no locks required for the quoting loop itself. External API calls (GetState,
GetOpenOrderCount, etc.) use a small sync.RWMutex for read safety.
symbol BTCUSDT-PERP└── PoolEngine (single goroutine) ├── Aggregate member positions → scalar netPosition ├── A-S Engine computes reservation price + optimal spread ├── ProRata allocates order sizes to each member by capital weight └── Place orders under each member's own account identityCore components
Section titled “Core components”| Component | Package | Role |
|---|---|---|
PoolEngine | service/pool | Per-symbol event loop and lifecycle |
ASEngine | service/quoting | Avellaneda-Stoikov formula |
VolatilityTracker | service/quoting | Rolling σ from trade stream |
ProRataAllocator | service/pool | Capital-weight order splitting |
PoolRiskGuards | service/pool | Per-pool kill switch + reject storm |
MemberRuntime | service/pool | Live position/PnL/orders per participant |
Kafka Router | processor/kafka | Fan-out market events to pool engines |
OrderService client | infra/orderservice | HTTP: place/cancel per-member orders |
Avellaneda-Stoikov quoting
Section titled “Avellaneda-Stoikov quoting”Parameters
Section titled “Parameters”| Symbol | Config field | Meaning |
|---|---|---|
| γ (gamma) | gamma | Risk aversion — higher = tighter quotes pulled closer to mid |
| σ (sigma) | computed | Rolling volatility from recent trades (lookback = vol_lookback_seconds) |
| τ (tau) | tau | Time horizon in hours — how far ahead the model plans |
| κ (kappa) | kappa | Depth parameter — how quickly liquidity decays with price |
| q | aggregated | Net position across all pool members (positive = long) |
Formula
Section titled “Formula”Reservation price: r = microprice − q × γ × σ² × τ
Optimal half-spread: δ = (γ × σ² × τ) / 2 + (1/γ) × ln(1 + γ/κ)
Quotes (level 0): bid₀ = r − δ ask₀ = r + δ
Additional levels (i = 1 … num_levels−1): bid_i = bid₀ − i × level_spacing_bps ask_i = ask₀ + i × level_spacing_bpsSpread is clamped to [min_spread_bps, max_spread_bps] before placing orders.
Inventory effect
Section titled “Inventory effect”When the pool is long (positive q), the reservation price shifts below mid — bids are lower (discouraging more buying) and asks are lower (encouraging selling). When short, the opposite. This naturally mean-reverts inventory without widening the spread.
Order identity
Section titled “Order identity”Every order placed carries the member’s own account_id / user_id:
client_order_id = mm-{8-char-participant-id}-{symbol}-{level}-{side}-{nanoseconds}Fill events from the matching engine are attributed back to the originating member via an in-memory
orderOwner map keyed by order ID.
Pool lifecycle
Section titled “Pool lifecycle”State machine
Section titled “State machine” ┌─────────────────┐ startup │ │ POST /pools/{symbol}/enable ───────►│ PAUSED │◄─────────────────────────────────────────────┐ │ │ │ └────────┬────────┘ │ │ POST /pools/{symbol}/resume │ ▼ │ ┌─────────────────┐ risk breach / ┌───────────────┴─┐ │ ACTIVE │──► emergency kill ──────────►│ HALTED │ │ │ │ (manual reset) │ └────────┬────────┘ └─────────────────┘ │ POST /pools/{symbol}/pause ▼ ┌─────────────────┐ │ PAUSED │ └─────────────────┘- Service always starts all pools in PAUSED — quoting never auto-resumes after a restart.
- HALTED requires human review before calling
/resume. Investigate the risk event in logs / Grafana first.
Kill switch triggers (per pool)
Section titled “Kill switch triggers (per pool)”| Trigger | Condition |
|---|---|
| Position breach | Aggregate pool position > max_pool_position_usdt |
| Loss breach | Aggregate pool PnL < −max_pool_loss_usdt |
| Reject storm | Too many consecutive order rejections |
| Exchange halt | Instrument status = HALTED received from Kafka |
| Stale market data | No new order book update within staleness_threshold_ms (when enabled) |
Participant & revenue model
Section titled “Participant & revenue model”Each liquidity provider is a Participant with a global identity (user/account IDs, tier, margin). A participant can be a member of multiple symbol pools simultaneously.
| Tier | Intended use |
|---|---|
TIER_1 | Institutional — highest capital, tightest SLA |
TIER_2 | Professional |
TIER_3 | Retail / test |
Revenue models
Section titled “Revenue models”| Model | Behaviour |
|---|---|
KEEP_ALL | Participant keeps 100% of realised PnL |
PERCENTAGE_SPLIT | Platform retains revenue_share_pct% of gross PnL; remainder paid to participant |
Settlement lifecycle
Section titled “Settlement lifecycle”Monthly job creates record │ ▼ status = PENDING Admin reviews fills + Q-score │ ┌────┴────┐ │ │ APPROVE REJECT (with reason) │ ▼ status = APPROVED Finance executes wallet transfer │ ▼ status = SETTLED (or FAILED)Q-score performance metric
Section titled “Q-score performance metric”Every 10 seconds (configurable via PERFORMANCE_SNAPSHOT_INTERVAL) a snapshot captures each member’s:
- uptime % (ratio of active snapshots to total)
- avg spread bps
- avg depth (USDT size at best levels)
- trade count and volume
Q-score is a composite of these, weighted to reward tight spreads and high uptime. The leaderboard
at GET /v1/performance/leaderboard ranks participants across all symbols.
Kafka topics
Section titled “Kafka topics”Consumed
Section titled “Consumed”| Topic | Purpose |
|---|---|
md.orderbook.snap.v1 | Full order book snapshot on subscribe |
md.orderbook.delta.v1 | Incremental order book updates |
md.mark.v1 | Mark price for perpetuals |
engine.event.v1 | Fill / reject / cancel events from matching engine |
Published (outbox pattern)
Section titled “Published (outbox pattern)”| Topic | When |
|---|---|
mm.state.changed.v1 | Any pool state transition |
mm.risk.triggered.v1 | Kill switch activation |
mm.quote.simulated.v1 | Shadow mode only |
All events are Avro-encoded; schemas live in shared/kafka-schema/market-maker-service/.
Database schema
Section titled “Database schema”| Table | Purpose |
|---|---|
mm_pool_configs | A-S strategy config per symbol |
mm_participants | Enrolled liquidity providers |
mm_pool_members | Participant ↔ symbol assignments with weights |
mm_orders | Open and historical MM orders |
mm_fills | Fill history with per-member attribution |
mm_performance_daily | Aggregated daily Q-score metrics per participant × symbol |
mm_settlements | Monthly revenue-sharing payout records |
outbox | Transactional outbox for Kafka publishing |
Configuration reference
Section titled “Configuration reference”Service environment variables
Section titled “Service environment variables”| Variable | Default | Description |
|---|---|---|
POSTGRES_URL | — | Required — PostgreSQL connection string |
REDIS_URL | — | Required — Redis connection string |
KAFKA_BROKERS | localhost:9092 | Kafka broker list |
SCHEMA_REGISTRY_URL | — | Required — Confluent Schema Registry |
ORDER_SERVICE_URL | http://order-service:3000 | Order Service HTTP base URL |
METADATA_SERVICE_GRPC_URL | localhost:50051 | Metadata Service gRPC URL |
HTTP_PORT | 8095 | REST API port |
METRICS_PORT | 9090 | Prometheus metrics port |
MM_SHADOW_MODE | false | Compute quotes, place no orders |
PERFORMANCE_SNAPSHOT_INTERVAL | 10s | Q-score snapshot cadence |
SHUTDOWN_TIMEOUT | 30s | Graceful shutdown window |
AUTH_SERVICE_URL | http://auth-service:3000 | JWT validation endpoint |
DISABLE_AUTH | false | Dev only — skip JWT validation |
Pool config fields (per symbol, stored in DB)
Section titled “Pool config fields (per symbol, stored in DB)”| Field | Type | Description |
|---|---|---|
symbol | string | Trading symbol, e.g. BTCUSDT-PERP |
enabled | bool | Whether the pool should auto-start on service restart |
gamma | decimal | Risk-aversion coefficient (A-S γ) |
kappa | decimal | Market depth parameter (A-S κ) |
vol_lookback_seconds | int | Rolling window for σ estimation |
tau | decimal | Time horizon in hours (A-S τ) |
num_levels | int | Number of price levels per side |
level_spacing_bps | decimal | Basis-point gap between levels |
min_spread_bps | decimal | Floor for computed spread |
max_spread_bps | decimal | Ceiling for computed spread |
order_size_usdt | decimal | Total pool order size per level |
max_pool_position_usdt | decimal | Aggregate position limit (kill switch trigger) |
max_pool_loss_usdt | decimal | Aggregate loss limit (kill switch trigger) |
check_staleness | bool | Halt quoting when market data is stale |
staleness_threshold_ms | decimal | Max age of last order book update |
quote_interval_ms | decimal | Minimum re-quote cadence |
price_threshold_bps | decimal | Minimum price move to trigger a re-quote |
REST API quick reference
Section titled “REST API quick reference”All routes are prefixed /v1.
# Pool lifecycleGET /pools — list all pool configsPOST /pools — create or update (upsert) a pool configGET /pools/{symbol} — get config for one symbolPUT /pools/{symbol} — update configDELETE /pools/{symbol} — delete config (pool must be paused)POST /pools/{symbol}/enable — PAUSED → ACTIVEPOST /pools/{symbol}/pause — ACTIVE → PAUSEDPOST /pools/{symbol}/resume — PAUSED → ACTIVE (after manual review)GET /pools/status — all pool live statusesGET /pools/{symbol}/status — one pool live status + member breakdown
# Pool membersGET /pools/{symbol}/members — list membersPOST /pools/{symbol}/members — add member to poolPUT /pools/{symbol}/members/{id} — update weight / limitsDELETE /pools/{symbol}/members/{id} — remove member
# ParticipantsGET /participants — list allPOST /participants — enroll new participant (PENDING)GET /participants/{id} — get participantPUT /participants/{id} — update mutable fieldsPOST /participants/{id}/activate — PENDING → ACTIVEPOST /participants/{id}/suspend — ACTIVE → SUSPENDEDPOST /participants/{id}/terminate — permanent removalGET /participants/{id}/memberships — cross-symbol memberships
# PerformanceGET /performance/leaderboard — Q-score rankingGET /performance/symbol/{symbol} — per-symbol daily perfGET /performance/{participantId} — per-participant daily perf
# SettlementsGET /settlements — list all settlementsGET /settlements/pending — pending approval queueGET /settlements/{id} — get settlementPOST /settlements/{id}/approve — approve (body: {approved_by})POST /settlements/{id}/reject — reject (body: {rejected_by, reason})
# Orders & fills (read-only)GET /mm-orders — all MM ordersGET /mm-orders/{symbol} — orders for one symbolGET /mm-orders/{symbol}/breakdown — by side/statusGET /mm-fills — all fillsGET /mm-fills/{symbol} — fills for one symbolGET /mm-fills/{symbol}/summary — 24h aggregate
# AnalyticsGET /mm-analytics/overview — 24h totals across all symbolsGET /mm-analytics/hourly — hourly buckets (?symbol= to filter)GET /mm-analytics/{symbol}/metrics — full symbol metrics
# Emergency controlsPOST /mm/emergency/kill — pause ALL pool enginesPOST /mm/emergency/flatten — pause ALL pool engines + cancel ordersPOST /mm/reset — stub: directs to /pools/{symbol}/resumeGET /mm/status — summary across all enginesObservability
Section titled “Observability”Prometheus metrics (:9090/metrics)
Section titled “Prometheus metrics (:9090/metrics)”| Metric | Labels | Description |
|---|---|---|
mm_pool_member_count | symbol | Active member count per pool |
mm_pool_net_position | symbol, participant_id | Per-member net position (USDT) |
mm_pool_member_pnl | symbol, participant_id | Realised PnL per member |
mm_pool_member_inventory | symbol, participant_id | Inventory units |
mm_pool_member_open_orders | symbol, participant_id | Open order count |
mm_pool_q_score | symbol, participant_id | Latest Q-score |
mm_pool_quote_compute_latency_seconds | symbol | Histogram of A-S compute time |
mm_pool_allocation_latency_seconds | symbol | Histogram of pro-rata allocation time |
mm_settlement_count | status | Settlement count by status |
mm_performance_snapshot_total | symbol | Total snapshots taken |
mm_performance_rollup_total | symbol | Total daily rollups |
Tracing
Section titled “Tracing”OpenTelemetry traces exported via OTLP to OpenObserve (OPENOBSERVE_OTLP_ENDPOINT).
Logging
Section titled “Logging”Structured JSON via zap. Every log entry includes symbol, participant_id (where applicable),
and a trace_id for cross-service correlation.
Safety guarantees
Section titled “Safety guarantees”MMS never:
- ❌ Mutates order books directly
- ❌ Bypasses the Order Service
- ❌ Accesses user wallets or positions
- ❌ Uses privileged matching logic
- ❌ Places orders when state ≠ ACTIVE
- ❌ Auto-resumes after a kill switch
MMS always:
- ✅ Cancels all orders when transitioning to PAUSED or HALTED
- ✅ Routes all orders through Order Service REST API
- ✅ Tags orders with member’s own
account_type = SYSTEM_MARKET_MAKER - ✅ Starts in PAUSED state on every boot
- ✅ Validates tick size and lot size from Metadata Service before quoting
Related documentation
Section titled “Related documentation”- Market Maker SOP — Step-by-step operational runbooks
- Market Maker Engineering Guide — Conceptual deep-dive
- Market Maker API Reference — Swagger UI
- Kafka Integration Guide
- Order Service
- Architecture Overview