The trading bots service is a standalone Python process that connects to the exchange engine as a collection of autonomous market participants.
4 distinct bot strategies are implemented. Each strategy registers one shared account and then runs one task per symbol under that account. With 3 default symbols this means 4 × 3 = 12 concurrent tasks, but only 4 accounts appear on the leaderboard — one per strategy, not one per stock.
Every bot account starts with $100,000 simulated capital (seeded by the exchange on first registration).
trading-bots/
├── bots/
│ ├── __init__.py
│ ├── base_bot.py ← abstract base: auth, REST, WS, metrics
│ ├── market_maker.py ← Strategy 1: spread-quoting market maker
│ ├── alpha_bot.py ← Strategy 2: RSI + EMA crossover
│ ├── momentum_bot.py ← Strategy 3: MACD + EMA trend filter
│ └── mean_reversion_bot.py ← Strategy 4: Bollinger Bands fade
├── indicators.py ← pure-Python technical indicator library
├── metrics.py ← aiohttp JSON metrics server (port 9090)
├── main.py ← entry point: fetch symbols, spawn bots
├── requirements.txt ← aiohttp>=3.9.0 (covers HTTP + WebSocket)
└── Dockerfile ← Python 3.12-slim, exposes 9090
| Environment Variable | Default | Description |
|---|---|---|
ENGINE_URL |
http://localhost:8080 |
Exchange engine base URL |
BOT_PASSWORD |
bot-secret-2026 |
Shared password for all bot users |
METRICS_PORT |
9090 |
Port for the metrics HTTP server |
-
Calls
GET /api/symbolswith retry + 3 s back-off (up to 10 attempts) to discover all symbols dynamically — no hardcoded symbol list. -
For each symbol (e.g.
BTC-USD) derives a short key (sym.split("-")[0].lower()→"btc"). -
Spawns four bot instances per symbol:
Key pattern Class Username example mm_{short}MarketMakerBotmm_btcalpha_{short}AlphaBotalpha_btcmom_{short}MomentumBotmom_btcmrv_{short}MeanReversionBotmrv_btc -
Registers every bot in the global metrics registry.
-
Starts the metrics HTTP server.
-
Runs all bots as concurrent
asyncio.Taskinstances. -
Installs
SIGINT/SIGTERMhandlers that cancel all tasks for graceful shutdown.
- On
run(), attemptsPOST /api/auth/login. - If login fails with 401, falls back to
POST /api/auth/register(creating the account) then logs in again. - Uses exponential back-off (up to 5 retries) for network errors.
- Stores the JWT and attaches it to every subsequent REST call and WS connection.
| Method | Exchange Endpoint | Description |
|---|---|---|
place_limit(side, p, q) |
POST /api/orders |
Submit a LIMIT order |
place_market(side, q) |
POST /api/orders |
Submit a MARKET order |
cancel_order(id) |
DELETE /api/orders/:id |
Cancel a single order |
cancel_all_symbol_orders() |
GET + DELETE /api/orders |
Cancel all open orders for symbol |
get_portfolio() |
GET /api/portfolio |
Fetch cash + positions |
-
Connects to
ws://<engine>/ws?token=<jwt>&symbol=<sym>. -
Auto-reconnects with 3 s back-off on disconnect.
-
Handles the following server-sent event types:
WS type Action tickerUpdates self.ticker(last_price, bid, ask, …)orderbookUpdates self.orderbook(bids/asks snapshot)ohlcvUpdates candle buffer ( is_closed=false→ patch live candle;is_closed=true→ append closed candle, fireon_candle_close)portfolioSyncs cash,positions,realized_pnl; tracks win/loss countsorder_ackLogs acknowledgement order_cancelFires on_order_cancelhook (used by MarketMaker)errorLogs the error payload
On startup, seeds the candle buffer from GET /api/history/ohlcv/:symbol before
the WS connection starts delivering live candles. Falls back through intervals
[CANDLE_INTERVAL, "5s", "1s"] so bots start even on a freshly launched engine
with no 1-minute history yet. Guards against candles: null API responses with
raw.get("candles") or [].
| Attribute | Type | Description |
|---|---|---|
cash |
float | Current cash balance (USD) |
positions |
dict | {symbol: qty} map |
realized_pnl |
float | Cumulative realised P&L |
trade_count |
int | Total fills received |
win_count |
int | Fills with positive P&L delta |
loss_count |
int | Fills with negative P&L delta |
pnl_history |
list | Time-series of (ts, total_value) |
_peak_value |
float | Running peak for drawdown tracking |
Returns a dict with: username, symbol, strategy, cash,
position_qty, last_price, unrealized_pnl, realized_pnl,
total_value, trade_count, win_count, loss_count, win_rate,
sharpe_ratio, max_drawdown, current_drawdown, ws_connected,
candle_count, pnl_history.
Hypothesis: Continuously quoting a two-sided market captures the bid-ask spread as profit.
| Parameter | Value | Description |
|---|---|---|
SPREAD_PCT |
0.0015 | Half-spread as fraction of mid-price (0.15%) |
QUOTE_INTERVAL |
0.5 s | Quote refresh frequency |
MAX_INVENTORY |
1.0 | Max net position in base asset units |
MIN_CASH_RATIO |
0.10 | Reserve 10% of cash; never deploy all of it |
QUOTE_VALUE_USD |
$500 | Target USD notional per side per quote |
Quantity is computed live as QUOTE_VALUE_USD / mid_price, rounded to an
appropriate precision (2 dp for qty ≥ 1, 4 dp for qty ≥ 0.01, 6 dp otherwise).
This makes the bot symbol-agnostic regardless of price range.
Uses Bollinger Band width as a volatility proxy:
multiplier = 1.0 + max(0, (bb_width - 0.01) / 0.01) * 0.5
spread = SPREAD_PCT × multiplier (capped at 3×)
Widens automatically in high-volatility periods to protect against adverse selection.
skew = net_position / MAX_INVENTORY ∈ [-1, 1]
bid_price = mid - half_spread × (1 + 0.5 × skew)
ask_price = mid + half_spread × (1 - 0.5 × skew)
When long (skew > 0): bid is pushed lower (less aggressive buy), ask is pulled tighter (more aggressive sell) — encourages inventory reduction.
- Wait up to 20 s for first ticker/orderbook data.
- Cancel any stale orders left by a previous run.
- Every
QUOTE_INTERVAL: cancel existing quotes → compute mid → place fresh bid + ask. - On shutdown: cancel all outstanding quotes.
Hypothesis: EMA crossovers combined with RSI filtering identify high-probability trend-entry points.
| Indicator | Parameter | Value |
|---|---|---|
| RSI | period | 14 |
| EMA fast | period | 9 |
| EMA slow | period | 21 |
| Candle interval | — | 1 minute |
| Minimum candles | — | 40 (EMA_SLOW + RSI_PERIOD + 5) |
| Direction | Condition |
|---|---|
| LONG | EMA(9) crosses above EMA(21) AND RSI < 70 |
| SHORT | EMA(9) crosses below EMA(21) AND RSI > 30 |
| Trigger | Condition |
|---|---|
| Stop-loss | −2% adverse move from entry |
| Take-profit | +4% favourable move from entry |
| Signal reversal | Opposite EMA crossover while in position |
Fixed TRADE_USD = $3,000 per trade, converted to qty at current price.
Short selling fully supported.
- Triggered on every
on_candle_closeWS hook. - Periodic fallback evaluation every 30 s to handle missed candle events.
Hypothesis: MACD crossovers in the direction of the prevailing EMA(50) trend capture sustained momentum moves.
| Indicator | Parameter | Value |
|---|---|---|
| MACD fast EMA | period | 12 |
| MACD slow EMA | period | 26 |
| MACD signal | period | 9 |
| Trend EMA | period | 50 |
| Candle interval | — | 5 seconds (fast warm-up: ~7 min vs 35 min for 1m) |
| Minimum candles | — | 87 (EMA_TREND + MACD_SLOW + MACD_SIGNAL + 2) |
| Direction | Condition |
|---|---|
| LONG | MACD histogram crosses from negative to positive AND price > EMA(50) |
| SHORT | MACD histogram crosses from positive to negative AND price < EMA(50) |
Crossover detected by comparing histogram sign between prices[:-1] and
prices (the _prev_histogram helper).
| Trigger | Condition |
|---|---|
| Stop-loss | −1.5% adverse move |
| Take-profit | +3.0% favourable move |
| Trend flip | Price crosses to the other side of EMA(50) |
Fixed TRADE_USD = $2,500 per trade.
- Triggered on every
on_candle_closeWS hook (5s bars). - Periodic fallback every 15 s.
Hypothesis: Price tends to revert to the mean (middle Bollinger Band) after extreme standard-deviation excursions.
| Indicator | Parameter | Value |
|---|---|---|
| Bollinger Bands | period | 20 |
| Bollinger Bands | std dev | 2.0 |
| RSI confirmation | period | 14 |
| Candle interval | — | 1 minute |
| Minimum candles | — | 36 (BB_PERIOD + RSI_PERIOD + 2) |
| Direction | Condition |
|---|---|
| LONG | close < lower Bollinger Band AND RSI < 40 (oversold) |
| SHORT | close > upper Bollinger Band AND RSI > 60 (overbought) |
| Trigger | Condition |
|---|---|
| Target | Price returns to middle Bollinger Band (mean) |
| Stop-loss | −1.5% adverse move from entry |
| Time-stop | 30 bars (~30 minutes) elapsed without exit |
Fixed TRADE_USD = $2,500 per trade.
- Triggered on every
on_candle_closeWS hook. - Periodic fallback every 20 s.
Pure Python, zero external dependencies. All functions operate on
List[float] and return Optional values (or lists of Optional) so callers
can guard against insufficient data.
| Function | Signature | Description |
|---|---|---|
ema |
(prices, period) → List[Optional[float]] |
Exponential Moving Average (EMA) across full series |
rsi |
(prices, period=14) → Optional[float] |
Wilder's RSI for most recent point |
bollinger |
(prices, period=20, num_std=2.0) → Optional[Tuple[float,float,float]] |
Bollinger Bands: (upper, middle, lower) |
bollinger_width |
(prices, period=20) → Optional[float] |
Normalised width = (upper−lower)/middle, volatility proxy |
macd |
(prices, fast=12, slow=26, signal=9) → Tuple[Optional[float],Optional[float],Optional[float]] |
MACD line, signal line, histogram |
crossover |
(fast, slow) → Optional[bool] |
True=bullish cross, False=bearish, None=no cross |
sharpe_ratio |
(values) → float |
Mean/std of period returns; 0.0 if insufficient data |
max_drawdown |
(values) → float |
Peak-to-trough as positive fraction |
Runs an aiohttp.web server on port 9090. All responses include
Access-Control-Allow-Origin: *.
| Endpoint | Method | Response |
|---|---|---|
/health |
GET | {"status": "ok", "bots": <count>} |
/metrics |
GET | JSON object — all bots keyed by username |
/metrics/{key} |
GET | JSON object — single bot snapshot |
Each bot snapshot (from BaseBot.get_metrics()) includes:
{
"username": "mm_btc",
"symbol": "BTC-USD",
"strategy": "MarketMakerBot",
"cash": 98456.12,
"position_qty": 0.0023,
"last_price": 67234.50,
"unrealized_pnl": 154.71,
"realized_pnl": 902.43,
"total_value": 99513.26,
"trade_count": 412,
"win_count": 238,
"loss_count": 174,
"win_rate": 0.578,
"sharpe_ratio": 1.34,
"max_drawdown": 0.024,
"current_drawdown": 0.003,
"ws_connected": true,
"candle_count": 287,
"pnl_history": [[1743590400000, 100000.0], ["...", "..."]]
}Base image : python:3.12-slim
Workdir : /app
Install : requirements.txt (aiohttp>=3.9.0)
Expose : 9090
CMD : python main.py
The service depends on exchange-engine in docker-compose.yml and waits for
the engine to be healthy before the bots attempt to register or connect.
| Requirement | Status |
|---|---|
| Market Maker bot — spread quoting | MarketMakerBot |
| Directional / Alpha bot — RSI + EMA | AlphaBot |
| $100,000 starting capital per bot | Exchange engine seeds on register |
| WebSocket client connections to engine | Each bot maintains its own persistent WS |
| REST order placement (limit, market, cancel) | All three order types used |
| Short selling | All strategies place SELL market orders |
| Metrics endpoint for bot dashboard | /metrics on port 9090 |
| Dockerfile | Python 3.12-slim |
docker-compose up launches everything |
trading-bots service in compose |
| Feature | Detail |
|---|---|
| MomentumBot — MACD(12,26,9) + EMA(50) | 4th strategy, 5s candles for fast warm-up |
| MeanReversionBot — Bollinger Bands fade | 5th strategy, time-stop after 30 bars |
| Dynamic symbol discovery | Calls /api/symbols at startup; no hardcoded list |
| Volatility-adaptive spread (MarketMaker) | BB-width multiplier, capped at 3× |
| Inventory skew (MarketMaker) | Linear skew pushes quotes to reduce position risk |
| Candle bootstrap with interval fallback | 1m → 5s → 1s so bots start on fresh engines |
| Sharpe ratio, max drawdown, win rate in metrics | Computed from live pnl_history |
| Graceful SIGINT/SIGTERM shutdown | Cancels all 32 asyncio tasks cleanly |