Skip to main content

Market-Making Bot Overview

The Rocky market-making bot (rocky-bot) is a long-running Python service on EC2. It uses 30 independent accounts to continuously quote and actively trade BTC-PERP / ETH-PERP, so that demo.rocky.exchange shows multi-level real depth and continuous fills.

Current deployment: systemd-user service rocky-bot, single process running ~61 asyncio tasks. Repo: /Users/ubuntu/Desktop/Rocky/rocky-bot.


1. Why a market-making bot

Rocky is an early-stage demo with no real user volume. Without continuous quoting:

  • The order book is empty, looks like a dead market
  • No fills, the frontend kline / trade tape is forever blank
  • Even if a user submits an order, there's no counterparty — matching and settlement can't be exercised

The bot fills that gap:

  1. Quote (Maker) — limit orders on both sides, forming multi-level depth
  2. Active fill (Taker) — periodic aggressive orders, guaranteed fill events
  3. Trigger on-chain reward — every fill triggers a MiningReward contract creation, demonstrating the MTC mining mechanism (see MTC Mining Reward)
  4. Populate kline / 24h stats — fill data lands in ledger.trades, feeding the kline chart, 24h volume, recent-trades feed

2. Funnel geometry

30 accounts split by role:

┌─────────────────────────────┐
Anchor (1 account) │ mm-anchor │ best bid ± 1 bps
│ qty BTC: 0.0003 / ETH: 0.006
└─────────────────────────────┘

Ladder L1 (12 accts)│ mm-l1-{buy|sell}-{05..10}bps
│ qty BTC: 0.0005 / ETH: 0.01

Ladder L2 (8 accts) │ mm-l2-{buy|sell}-{15,20,25,30}bps
│ qty BTC: 0.001 / ETH: 0.02

Ladder L3 (4 accts) │ mm-l3-{buy|sell}-{50,100}bps
│ qty BTC: 0.002 / ETH: 0.04

Taker (5 accounts) │ taker-{1..5}
│ qty BTC: 0.0005 / ETH: 0.01
│ cross 50 bps past mid, every 30s±10s

Total: 1 anchor + 24 ladder + 5 taker = 30.

Each account is a separate registered user on demo.rocky.exchange, minted in one shot by scripts/mint-30.sh along with $100 USDC seed each.

2.1 Why the "funnel" shape

  • Dense in the center, sparse at the edges — anchor + L1 ladders quote in ±1–10 bps, keeping the inside spread very narrow (looks like good liquidity)
  • L2/L3 provide depth — orders at 50–100 bps show "the market can absorb large orders," making the depth chart look healthy
  • Takers drive fills — pure quoting wouldn't produce fills; takers actively cross 50 bps so a fill happens roughly every 30s
  • Multiple accounts vs single account — each account has its own risk budget ($100 wallet), so a blow-up on one doesn't propagate; also visually showcases "many real users trading"

2.2 Capacity math

$100 USDC seed per account. 30 accounts = $3000 total liquidity.

  • Each ladder account carries at most ~$15 position margin ($150 notional × 10x leverage / 10 = $15)
  • Each taker, same
  • Total locked-margin ceiling ≈ 30 × $30 = $900 (enough to show depth without being swept by the market)

Risk configuration details: see Risk Controls.


3. Architecture

rocky-bot is single-process, multi-asyncio-task:

rocky-bot (systemd-user service on EC2)

├── BinanceFeed (1 task)
│ └── WebSocket subscribes btcusdt + ethusdt bookTicker, caches mid

├── For each account in .keys.json (30 total):
│ │
│ ├── If role=ladder:
│ │ LadderMakerLoop × 2 symbols = 48 tasks
│ │ └── Every 3s ± 1s: read mid → compute target → diff
│ │ against existing open order → cancel/place
│ │
│ ├── If role=anchor:
│ │ AnchorMakerLoop × 2 symbols = 2 tasks
│ │ └── Every 2s ± 0.5s: read mid → quote both ±1 bps
│ │
│ └── If role=taker:
│ TakerLoop × 2 symbols = 10 tasks
│ └── Every 30s ± 10s: choose side (rebalance from position)
│ → 50 bps aggressive cross

└── Per-account CircuitBreaker (30 instances)
└── Tracks wallet PnL, API errors; trips if thresholds breached

Total tasks: 1 feed + 48 ladder + 2 anchor + 10 taker = 61 asyncio tasks.


4. Tech stack

PartChoiceWhy
LanguagePython 3.12Rapid iteration, mature ecosystem
Concurrencyasyncio60+ I/O-bound tasks; asyncio is far lighter than threading
HTTPhttpx (async)Async, connection reuse, doesn't block the event loop
Configpydantic-settings + custom .keys.json30 API key pairs in .env would be ugly
Market datawebsockets → wss://fstream.binance.comFree, low latency, doesn't conflict with the demo backend's matcher
Deployrsync + uv + systemctl --userSimple, low-dependency, observable

5. Relationship to rocky-backend

rocky-bot rocky-backend (EC2)
├── calls /fapi/v1/order ─────────────► api-gateway → trading-api → internal-ledger
├── calls /fapi/v1/order DELETE ──────► ...
├── calls /fapi/v1/positionRisk ──────► api-gateway → matching-engine query
└── calls /fapi/v1/balance ───────────► api-gateway → internal-ledger query

Auth: HMAC-SHA256 signing on every request; keys from .keys.json

The bot doesn't go through the /api/perp/* web BFF; it hits /fapi/v1/* directly (the Binance-compatible path) with API key + HMAC auth. These endpoints are designed for programmatic traders and correspond to the backend's auth.api_keys table (fully independent from the User Account API auth.users table).


6. Next