Market-Maker Bot (pm-mm-bot)¶
Learning objectives
After reading this page you will understand:
- What
pm-mm-botis and how it differs from manualQUOTEcommands - How to launch one or more autonomous market-maker bot instances
- The bot's lifecycle: startup handshake, quoting, repricing, and shutdown
- How quote refresh works after fills and mid-price drift
- How to configure gateway entries in
engine_config.yamlfor MM bots - How to bootstrap a fresh exchange with no existing book data
Prerequisites: Market Making — understand the QUOTE command,
quote_refresh_policy, and disconnect_behaviour before using the bot.
Configuration — each bot instance needs a pre-registered
MARKET_MAKER gateway in the engine config.
What is pm-mm-bot?¶
pm-mm-bot is an autonomous market-maker process that keeps a single
symbol liquid without human intervention. It connects to the engine as a
MARKET_MAKER gateway, posts a two-sided quote (bid and ask), and automatically
reprices when:
- One side of the quote is filled
- The mid-price drifts beyond a configurable threshold
- The session state changes (e.g. entering or leaving an auction phase)
Each instance handles exactly one symbol — this is a deliberate design constraint that keeps the bot simple, stateless per-book, and easy to reason about. To provide liquidity for multiple symbols, run one instance per symbol:
Multiple instances can also compete on the same symbol — they appear as
independent market makers in the book (use --id-suffix to distinguish them).
| Feature | Manual QUOTE |
pm-mm-bot |
|---|---|---|
| Requires human operator | Yes | No |
| Automatic reissue after fill | No | Yes |
| Drift-based repricing | No | Yes |
| Session-aware (pause in auctions) | Manual | Automatic |
| Multiple instances per symbol | Possible | Built-in (--id-suffix) |
Quick start¶
# Start a market maker for AAPL with default settings
pm-mm-bot --symbol AAPL
# With explicit spread and quantity
pm-mm-bot --symbol AAPL --gap 0.10 --qty 500
# In Poetry development mode
poetry run pm-mm-bot --symbol AAPL --gap 0.10 --qty 500 -v
Before launching, ensure the gateway ID is registered in engine_config.yaml:
gateways:
alf:
- id: MM_AAPL_01
description: "AAPL market-maker bot"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
Gateway identity convention¶
Each bot instance uses the gateway ID format:
Where <SYMBOL> is the symbol in uppercase and <nn> is the two-digit suffix
from --id-suffix (default 01).
--symbol |
--id-suffix |
Gateway ID |
|---|---|---|
AAPL |
01 (default) |
MM_AAPL_01 |
AAPL |
02 |
MM_AAPL_02 |
MSFT |
01 |
MM_MSFT_01 |
This convention makes bot gateways immediately identifiable in logs, the admin
console (pm-admin), and the order book viewer (pm-board).
Architecture¶
Each pm-mm-bot instance is a standalone process that communicates with the
engine using the same ZMQ PUSH/SUB pattern as all other participants:
flowchart LR
E["pm-engine\nPULL :5555 / PUB :5556"]
B1["pm-mm-bot\nMM_AAPL_01"]
B2["pm-mm-bot\nMM_MSFT_01"]
GW["pm-alf-console\nTrader"]
B1 -- "QUOTE / CANCEL → PUSH" --> E
B2 -- "QUOTE / CANCEL → PUSH" --> E
GW -- "order.new → PUSH" --> E
E -- "book, fills, status → SUB" --> B1
E -- "book, fills, status → SUB" --> B2
Bot lifecycle¶
State machine¶
The bot progresses through a well-defined set of states:
stateDiagram-v2
[*] --> CONNECTING : process start
CONNECTING --> AUTHENTICATING : ZMQ sockets open
AUTHENTICATING --> WAITING_FOR_SESSION : auth ACK received
AUTHENTICATING --> [*] : auth rejected or timeout
WAITING_FOR_SESSION --> REISSUING : session=CONTINUOUS + reference available
QUOTING --> REPRICING : mid drift exceeds threshold
QUOTING --> REISSUING : quote inactivated (fill)
QUOTING --> PAUSED : session != CONTINUOUS or HALTED
REPRICING --> REISSUING : new quote required
REISSUING --> QUOTING : quote.ack received
PAUSED --> WAITING_FOR_SESSION : resume trigger
QUOTING --> [*] : SIGINT / SIGTERM
PAUSED --> [*] : SIGINT / SIGTERM
Startup sequence¶
- Open ZMQ sockets (PUSH and SUB)
- Send
gateway_connectand wait forgateway_authACK - Request symbol list and verify the assigned symbol exists
- Send
QBOOTrequest — if an active quote already exists for this(gateway_id, symbol)pair, adopt it instead of creating a duplicate - Send
QLEGSrequest to reconcile quote-leg mapping - Wait for
session.stateevent (fail fast if not received within timeout) - Resolve initial reference price and begin quoting
Graceful shutdown¶
On SIGINT (Ctrl+C) or SIGTERM, the bot:
- Sends
quote.cancelto remove its resting quote - Waits up to
--shutdown-timeout-secfor cancel confirmation - Closes ZMQ sockets and exits
Pricing logic¶
Mid-price tracking¶
The bot tracks the current mid-price from the order book:
- Both sides present:
mid = (best_bid + best_ask) / 2 - Ask only:
mid = best_ask - Bid only:
mid = best_bid - No data: keep previous mid
Quote placement¶
Given the mid-price and --gap (total spread), the bot places:
- Bid at
mid − gap/2, rounded to the nearest tick - Ask at
mid + gap/2, rounded to the nearest tick
A minimum spread of 2 ticks is always guaranteed, even after rounding.
Drift detection¶
After posting a quote, the bot records the mid at the time of posting. On each
book update, it checks whether the mid has moved by more than --drift-ticks
ticks. If so, it cancels and reissues at the new mid.
Quote refresh logic¶
Refresh triggers¶
| Trigger | Action |
|---|---|
| Quote inactivated (one side filled) | Reissue after --reissue-delay-ms |
| Mid-price drift exceeds threshold | Cancel active quote, then reissue at new mid |
| Quote rejected | Retry after delay |
| Periodic heartbeat (no active quote) | Reissue |
| Periodic QLEGS reconciliation mismatch | Clear local state and reissue |
Reissue delay¶
After a fill, the bot waits --reissue-delay-ms (default: 200 ms) before
reissuing. If multiple fills arrive in quick succession, the timer resets on
each fill — resulting in exactly one reissue after the burst settles.
Cancel timeout guard¶
When the bot is replacing an active quote, it first sends quote.cancel and
waits up to --cancel-timeout-sec for lifecycle confirmation. If no
confirmation arrives within that window, it forces a safe replacement by
clearing local quote IDs and sending a fresh quote.new.
Self-healing against dropped messages¶
ZMQ delivery is best-effort, so the bot is built to recover if an engine reply is ever lost:
- Dropped
quote.ack— after sending a quote the bot waits for the ack to confirm it. If the ack never arrives, the heartbeat guard notices it holds no live quote and, once a full--heartbeat-interval-sechas elapsed since the last quote was sent (so an in-flight ack is never pre-empted), it reissues. - Periodic QLEGS reconciliation — every
--qlegs-reconcile-interval-secthe bot requests a fresh quote-leg snapshot (QLEGS). The request is non-blocking: the reply is processed in the normal event loop, so fills and status updates are never missed while the snapshot is outstanding. If the snapshot shows no legs, a differentquote_id, or different leg order IDs than the bot is tracking, it clears its local state and reissues to converge.
Session state handling¶
The bot respects the exchange session lifecycle:
| Session State | Bot Behaviour |
|---|---|
PRE_OPEN |
Wait — do not quote |
OPENING_AUCTION |
Cancel any live quote; wait |
CONTINUOUS |
Post and maintain a two-sided quote |
CLOSING_AUCTION |
Cancel any live quote; wait |
CLOSED |
Cancel any live quote; wait |
HALTED (circuit breaker) |
Cancel and pause immediately |
When the session transitions to CONTINUOUS, the bot resumes quoting
automatically.
Bootstrap: starting with an empty book¶
When the exchange starts fresh (no existing book or trades), the bot needs an initial reference price. It resolves one using this priority:
- QBOOT — active quote from a previous session (restart recovery)
- Book mid / last trade — the current book mid if another participant has
posted orders, otherwise the most recent
trade.executedprice - Bootstrap quote — inactive quote prices from QBOOT
- Random range —
--initial_minto--initial_max(configurable)
If no source is available and no random range is configured, the bot exits with a clear error message.
# Bootstrap from random price range when the book is empty
pm-mm-bot --symbol AAPL --initial_min 95.00 --initial_max 105.00
CLI reference¶
| Argument | Default | Description |
|---|---|---|
--symbol SYM |
required | Instrument to make a market in |
--gap PRICE |
0.10 |
Total spread (bid at mid−gap/2, ask at mid+gap/2) |
--qty N |
500 |
Quote size on each leg |
--id-suffix NN |
01 |
Running number for gateway ID (MM_AAPL_01) |
--drift-ticks N |
3 |
Reprice when mid moves by this many ticks |
--reissue-delay-ms N |
200 |
Wait after fill before re-issuing |
--tif {DAY,GTC} |
DAY |
Time-in-force for quote legs |
--heartbeat-interval-sec F |
5.0 |
Periodic live-quote check interval |
--startup-session-timeout-sec F |
5.0 |
Max wait for first session.state |
--bootstrap-timeout-sec F |
1.0 |
Max wait for QBOOT reply |
--cancel-timeout-sec F |
1.0 |
Max wait for cancel confirmation before forced replacement reissue |
--shutdown-timeout-sec F |
2.0 |
Max wait for cancel on SIGINT/SIGTERM |
--qlegs-reconcile-interval-sec F |
15.0 |
Periodic QLEGS reconciliation interval |
--initial_min PRICE |
unset | Lower bound for random bootstrap price |
--initial_max PRICE |
unset | Upper bound for random bootstrap price |
--engine-pull ADDR |
tcp://127.0.0.1:5555 |
Engine PUSH/PULL address |
--engine-pub ADDR |
tcp://127.0.0.1:5556 |
Engine PUB address |
--log-level |
WARNING |
Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v, --verbose |
false |
Increase verbosity (-v enables bot debug prints, -vv sets DEBUG) |
-q, --quiet |
false |
Reduce output to warnings/errors |
Engine configuration¶
Gateway registration¶
Each bot instance must be pre-registered in engine_config.yaml:
gateways:
alf:
- id: MM_AAPL_01
description: "AAPL market-maker bot instance 1"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
enforce_mm_obligation: true
mm_max_spread_ticks: 10
mm_min_qty: 100
smp_action: CANCEL_RESTING
- id: MM_AAPL_02
description: "AAPL market-maker bot instance 2"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
Recommended settings¶
disconnect_behaviour: CANCEL_QUOTES_ONLY— ensures stale quotes are removed if the bot crashes or restartsquote_refresh_policy: INACTIVATE_ON_ANY_FILL— the engine cancels the remaining leg when either side fills, triggering an immediate reissuesmp_action— self-match-prevention default for this gateway (NONEif unset). Sincemm_botonly ever submitsQUOTEs (no plainNEW/combo orders), this is the only SMP control it has — quote legs have no per-requestSMP=field of their own, so a bid/ask leg sweeping into a resting order from the same gateway id (e.g. a stale leg left over from a prior quote) is always handled per this gateway-level setting instead of self-trading. See Configuration Spec §5.2 for the fullSmpActionvalue list and how this default also applies toNEW/combo orders from other gateway roles when they omitSMP=. See Risk Controls — Self-Match Prevention for the full conceptual explanation, including a worked example of exactly this stale-quote-leg scenario
Gap validation¶
The bot enforces pricing validity at startup:
gap >= 2 * tick_size(via pricer validation)- if
mm_max_spread_ticksis available in symbol metadata, the bot validatesgap <= mm_max_spread_ticks * tick_size
Defaulting rules:
- An explicitly supplied
--gap(in either the--gap 0.10or--gap=0.10form) is always respected — it is only validated against the obligation, never overridden. - If
--gapis not provided andmm_max_spread_ticksis available, the bot defaults to half the max spread:(mm_max_spread_ticks / 2) * tick_size. - If no MM spread metadata is available, the bot uses the standard
0.10default.
Usage examples¶
Single symbol, default settings¶
Two competing MMs on the same symbol¶
pm-mm-bot --symbol AAPL --gap 0.08 --qty 500 &
pm-mm-bot --symbol AAPL --gap 0.12 --qty 300 --id-suffix 02 &
Faster repricing for volatile sessions¶
Fresh exchange with no book data¶
Verbose mode for troubleshooting¶
Understanding bot output¶
Each bot prefixes every log line with its gateway ID and timestamp. At default verbosity the bot is quiet — it only logs milestones and problems, not routine quoting activity:
[MM:MM_AAPL_01 09:30:00] starting: symbol=AAPL gap=0.1 qty=500 tif=DAY drift_ticks=3
[MM:MM_AAPL_01 09:30:01] authenticated
[MM:MM_AAPL_01 09:30:01] bootstrap from random range: 150.00
[MM:MM_AAPL_01 09:30:14] quote REJECTED: gap exceeds mm_max_spread_ticks
[MM:MM_AAPL_01 09:30:20] circuit breaker HALT
[MM:MM_AAPL_01 09:30:45] circuit breaker RESUME
[MM:MM_AAPL_01 09:31:02] heartbeat: no active quote — reissuing
[MM:MM_AAPL_01 09:31:10] QLEGS mismatch: quote_id divergence — reissuing
[MM:MM_AAPL_01 09:35:00] shutdown complete
The starting: line reports the bot's resolved configuration and is always
visible — you don't need -v to confirm what a running bot is actually
configured to quote.
With --verbose (-v), the bot additionally logs routine quoting activity —
symbol/session updates, every quote sent, every fill, and every repricing
decision:
[MM:MM_AAPL_01 09:30:01] symbols received: ['AAPL', 'MSFT', 'TSLA']
[MM:MM_AAPL_01 09:30:01] reference from book/trade: 150.00
[MM:MM_AAPL_01 09:30:01] QUOTE sent bid=149.95 ask=150.05
[MM:MM_AAPL_01 09:30:01] quote ACK id=q-001
[MM:MM_AAPL_01 09:30:14] fill: ASK 200@150.05
[MM:MM_AAPL_01 09:30:15] QUOTE sent bid=149.95 ask=150.05
[MM:MM_AAPL_01 09:31:02] drift detected — repricing
[MM:MM_AAPL_01 09:31:02] state: QUOTING -> REPRICING
[MM:MM_AAPL_01 09:31:02] QUOTE sent bid=149.99 ask=150.09
-v also raises the underlying log level and enables the bot's own debug
prints together — it is not just a "print more" switch, it also turns on
low-level flow tracing (e.g. book mid=..., session: OLD -> NEW, and
state: OLD -> NEW transitions of the bot's own state machine).
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
auth rejected |
Gateway ID not in engine_config.yaml |
Add the MM_<SYM>_<nn> entry with role: MARKET_MAKER |
startup failed: no reference price |
Empty book + no --initial_min/--initial_max |
Add bootstrap range flags |
startup failed: no session.state |
Engine not running or scheduler not started | Start the engine and scheduler |
quote REJECTED |
Gap or qty violates MM obligation policy | Reduce --gap / increase --qty or adjust gateway MM settings |
| Bot quotes but prices look wrong | Tick size mismatch | Check symbol tick_size in engine config |
| Bot stops quoting for a while, then resumes on its own | An engine reply (quote.ack/quote.status) was dropped |
Expected self-healing; the heartbeat and QLEGS reconciliation recover automatically. Lower --heartbeat-interval-sec for faster recovery |
See also¶
- Market Making — the
QUOTEcommand, the MM obligations framework, and Market-Maker Protection (MMP) - AI Traders — the
pm-ai-traderandpm-ai-swarmprocesses - Configuration — engine and gateway configuration
- Processes — architecture overview of all EduMatcher processes
- Risk Controls — the kill switch