Setting Up Market-Maker Liquidity¶
Objective¶
Configure market-maker gateways and use manual QUOTE commands to provide
two-sided liquidity for all three symbols. You will also compare this manual
workflow with pm-mm-bot, which automates the same lifecycle — including how
to quote several symbols from one process on different terms each, and how to
drive the whole desk from a committed config file instead of a long CLI
invocation.
Background reading
Prerequisites¶
- Chapters 00–01 completed.
pm-enginerunning and the market open inCONTINUOUS(chapter 01, "Open the market for the chapters ahead").- At least one trader gateway connected (for book checks).
Background¶
A market maker posts simultaneous buy (bid) and sell (ask) prices. Without one, the order book is empty and no trader can get an immediate fill.
Manual first, automation second
pm-mm-bot is available, but this chapter starts with manual
pm-alf-console + QUOTE so you can see quote lifecycle and operator tools
directly before using automation.
Exercise 1: Add MM Gateways to Configuration¶
Extend your engine_config.yaml participants list:
participants:
# ... existing TRADER01, TRADER02, GW_ADMIN entries ...
- id: MM_AAPL_01
description: "AAPL market-maker"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
- id: MM_MSFT_01
description: "MSFT market-maker"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
- id: MM_TSLA_01
description: "TSLA market-maker"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
Declaring a MARKET_MAKER gateway obliges you to seed a quote for every
symbol it makes a market in. Add a market_maker_quotes entry under each
symbol naming its market maker:
symbols:
AAPL:
tick_decimals: 2
last_buy_price: 150.00
last_sell_price: 150.00
market_maker_quotes:
- gateway_id: MM_AAPL_01
bid_price: 149.95
ask_price: 150.05
bid_qty: 500
ask_qty: 500
# ... the same shape for MSFT/MM_MSFT_01 and TSLA/MM_TSLA_01
Skip the seeds and the deploy is refused
Without them the verifier raises M001 (ERROR) — "Symbol 'AAPL' has no
market_maker_quotes entry for MARKET_MAKER gateway(s) MM_AAPL_01" — and
pm-config-deploy refuses to install a configuration with any error. This
is the verifier doing its job: a market maker with nothing to quote is a
configuration mistake, not a runtime one.
pm-config-gen can generate the seeds instead of hand-writing them
pm-config-gen (Chapter 01) will emit a market_maker_quotes stub for
every MARKET_MAKER gateway automatically — you don't have to hand-write
the block above. Two ways to use it:
# Stub only: emits gateway_id/bid_qty/ask_qty/tif, but bid_price and
# ask_price are left as `null` for you to fill in by hand
pm-config-gen \
--symbols AAPL MSFT TSLA \
--participants TRADER01 TRADER02 GW_ADMIN:ADMIN \
MM_AAPL_01:MARKET_MAKER MM_MSFT_01:MARKET_MAKER MM_TSLA_01:MARKET_MAKER \
--enforce-mm-obligations \
--output engine_config.yaml --force
# Fully seeded: also fills bid_price/ask_price from a random midpoint in
# the given range, rounded to each symbol's tick grid
pm-config-gen \
--symbols AAPL MSFT TSLA \
--participants TRADER01 TRADER02 GW_ADMIN:ADMIN \
MM_AAPL_01:MARKET_MAKER MM_MSFT_01:MARKET_MAKER MM_TSLA_01:MARKET_MAKER \
--enforce-mm-obligations \
--seed-mm-mid-range 100:400 \
--output engine_config.yaml --force
The first form is a trap worth knowing about: pm-cverifier will report
0 errors on the stub — market_maker_quotes entries exist, so M001
doesn't fire, and the required keys are all present (just null), so the
schema check doesn't fire either. The verifier cannot see that null is
unusable. It's pm-config-deploy that catches it, at compile time, with
Symbol 'AAPL': market_maker_quotes[0] is invalid — because loading the
artifact tries float(None). pm-config-gen itself warns you about this
up front with [HINT] Fill all market_maker_quotes bid_price/ask_price
values before starting pm-engine, right after it writes the file — that
hint is the moment to either fill the two prices in by hand or rerun with
--seed-mm-mid-range instead.
If you never want seeded quotes at all (a deliberately empty book for a
symbol), --no-mm-seed-quotes sets require_mm_seed_quotes: false and
suppresses M001 for that config entirely — no stub is emitted and no
price is expected.
Check, deploy, then restart the engine — editing the YAML alone changes nothing, because every process reads the compiled artifact:
Then restart pm-engine to pick up the new gateways. A restarted engine
starts in CLOSED again, so reopen the market from the operator console
(SESSION|STATE=PRE_OPEN, then SESSION|STATE=CONTINUOUS).
Checkpoint: pm-cverifier reports 0 errors,
the deploy succeeds, and the engine logs show 6 gateways loaded.
Exercise 2: Connect the AAPL Market Maker¶
In a new terminal:
At the prompt, submit a two-sided quote:
[MM_AAPL_01]> QUOTE|SYM=AAPL|BID=149.95|ASK=150.05|BID_QTY=500|ASK_QTY=500|TIF=DAY|QUOTE_ID=AAPL-MM-001
Expected output should include a quote acknowledgement and active status.
Checkpoint: AAPL quote acknowledged and active.
Exercise 3: Quote MSFT and TSLA¶
Open one terminal per MM gateway:
Submit quotes:
[MM_MSFT_01]> QUOTE|SYM=MSFT|BID=419.90|ASK=420.10|BID_QTY=300|ASK_QTY=300|TIF=DAY|QUOTE_ID=MSFT-MM-001
[MM_TSLA_01]> QUOTE|SYM=TSLA|BID=249.75|ASK=250.25|BID_QTY=200|ASK_QTY=200|TIF=DAY|QUOTE_ID=TSLA-MM-001
Checkpoint: all three market makers report active quotes.
Exercise 4: Verify Liquidity from the Operator Console¶
From the operator console (pm-admin) — a trader cannot inspect the whole book:
You should see a two-sided book with the MM's bid and ask. Repeat for MSFT and TSLA.
Checkpoint: all three books show two-sided liquidity.
Exercise 5: Inspect Quote State with QLEGS¶
From each market-maker gateway, inspect quote legs:
[MM_AAPL_01]> QLEGS|SYM=AAPL|SHOW=ALL
[MM_MSFT_01]> QLEGS|SYM=MSFT|SHOW=ALL
[MM_TSLA_01]> QLEGS|SYM=TSLA|SHOW=ALL
QLEGS shows the bid and ask leg order IDs, prices, remaining quantities, and
fill flags. This is the operator view that helps you reconcile fills after
restart or partial execution.
Interpretation guide:
Remis open quantity still resting in the book.Filledis already executed quantity on that leg.Leg statusis one ofNEW,PARTIAL,FILLED,CANCELLED,EXPIRED, orPENDING— a resting, untouched leg showsNEW, notRESTING.
Checkpoint: QLEGS shows both quote legs for each symbol.
Exercise 6: Run the Equivalent Bot Workflow (Optional)¶
The manual quote sequence above can be automated with one bot per symbol —
independent processes, each free to crash, restart, or be tuned without
touching the others. (Exercise 8 below shows the alternative: one process
covering several symbols at once, each on its own terms.) Stop your manual
quotes first (QUOTE_CANCEL|SYM=<symbol> from each MM console, or just leave
them — the bot's own startup reconciliation handles either case, see the note
below) and run:
pm-mm-bot --symbol AAPL --gap 0.10 --qty 500
pm-mm-bot --symbol MSFT --gap 0.20 --qty 300
pm-mm-bot --symbol TSLA --gap 0.50 --qty 200
The bot connects using the gateway ID MM_<SYMBOL>_<id-suffix> (default
suffix 01), quotes symmetrically around the current mid-price at --gap,
reissues after fills, and reprices when the mid drifts. It also runs a
QBOOT request at startup so it can adopt an already-active quote instead of
creating a duplicate, and periodically re-runs QLEGS to reconcile leg state
in case an engine reply was ever dropped.
Gateway ID must already exist in your config
pm-mm-bot does not create a gateway — it connects under the ID it
computes (MM_AAPL_01, MM_MSFT_01, MM_TSLA_01 by default) and expects
that ID to already be present in engine_config.yaml from Exercise 1. If
your config used different gateway IDs, either rename them to match this
pattern or pass --id-suffix to the bot so the computed ID lines up. A
mismatch here causes the engine to reject the bot's connection.
Quick primer:
QBOOTasks the engine whether a gateway+symbol already has an active quote slot (for example after a crash/restart).QLEGSreconciles leg-level state (order IDs, remaining, fills) so the bot can adopt or replace safely instead of duplicating quotes.
See the detailed walkthrough in 09 — Market Making, and 20 — Automation with CommandClient & MM Bot Tuning for the advanced runtime flags (bootstrap timeout, QLEGS reconciliation interval, and similar) that this chapter deliberately leaves out.
Checkpoint: explain what the bot automates compared with your manual QUOTE workflow.
Exercise 7: Drive the Bot from a Config File¶
A bot's whole configuration can live in a version-controlled YAML file
instead of a long command line. Create mm_aapl.yaml:
version: 1
gateway:
label: AAPL
id_suffix: "01"
defaults:
strategy: symmetric
gap: 0.10
qty: 500
tif: DAY
drift_ticks: 3
symbols:
AAPL: {}
Run the bot from it instead of typing the flags:
The file has four blocks, and the split between them is the thing worth learning:
| Block | Holds | Can it differ per symbol? |
|---|---|---|
gateway: |
Gateway identity and the two engine socket addresses | No — one process, one gateway, one pair of sockets |
logging: |
Log level and destination | No — one process, one log stream |
defaults: |
Quoting settings applied to every symbol | These are the defaults |
symbols: |
Which symbols to quote, and each one's overrides | Yes — see Exercise 8 |
AAPL: {} means "quote AAPL, on the house defaults". The empty braces are
required YAML, not decoration: the value of a symbol key is its override
block, and this one overrides nothing.
An explicit CLI flag still overrides the file, so you can keep one committed file per desk for a class and override a single value for a one-off run:
No symbol needs to appear on the CLI at all as long as the file's symbols:
block names one. The bot fails fast with a usage error if neither the CLI
nor the file provides a symbol, rather than silently picking a default.
Two pricing strategies ship today
strategy: symmetric (the default, so the line above is not strictly
needed) quotes symmetrically around the tracked mid at gap.
strategy: inventory_skew instead shifts both quotes to work an
accumulated position back toward flat, and requires a max_position:
alongside it — the net position at which the skew saturates. Naming any
other strategy is a startup failure, not a silent fallback.
A typo in the file fails fast, and tells you what you meant
An unknown key is rejected at startup rather than silently ignored, with a suggestion where there is an obvious one:
invalid config file: config file mm_aapl.yaml: unknown key 'gapp' under symbols.AAPL (did you mean 'gap'?)
Putting a real key in the wrong block gets its own message rather than
"unknown key" — try moving gap: into gateway: and see what it says.
Quote the id_suffix
id_suffix: 01 without quotes is the number 1 to YAML, which would
make the gateway MM_AAPL_1 rather than MM_AAPL_01. The loader refuses
it outright rather than guessing; "01" is correct.
Checkpoint: you have started a bot from a config file alone, then overridden one value from the CLI and confirmed the CLI value won.
Exercise 8: One Bot, Three Symbols, Three Sets of Terms¶
Exercise 6 ran three separate pm-mm-bot processes, one per symbol. But
nothing on the engine side requires a MARKET_MAKER gateway to quote only
one symbol: the engine's QuoteIndex keys every active quote by
(gateway_id, symbol) and tracks a set of such keys per gateway, so one
gateway can hold AAPL's quote and MSFT's quote and TSLA's quote at the same
time — and each of those symbols can be quoted on its own terms.
Stop the three bots from Exercise 6 (Ctrl+C each, or leave them — startup reconciliation handles either case) and register one gateway that will quote all three symbols instead:
participants:
- id: MM_TECH_01
description: "AAPL+MSFT+TSLA market-maker"
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
The market_maker_quotes seed requirement still applies to every symbol
pm-cverifier's M001 check fires whenever any MARKET_MAKER
gateway exists and a symbol has no market_maker_quotes entry — it does
not check which gateway is meant to quote which symbol. So MM_TECH_01
still needs a seed under symbols.AAPL, symbols.MSFT, and
symbols.TSLA, exactly as if it were three separate single-symbol
gateways. If you regenerate with pm-config-gen, point
--participants ... MM_TECH_01:MARKET_MAKER at the one new ID and it will
seed all three symbols against it automatically.
Check and deploy as before, then start one bot covering all three symbols on identical terms:
pm-cverifier engine_config.yaml # expect 0 errors
pm-config-deploy engine_config.yaml
pm-mm-bot --symbols AAPL,MSFT,TSLA --label TECH --gap 0.10 --qty 500
--label TECH is what makes the gateway ID MM_TECH_01 instead of the
default MM_AAPL_MSFT_TSLA_01 the bot would otherwise derive by joining
every symbol — useful once the symbol list is longer than a couple of
entries.
Watch the log output. The startup lines report the gateway, the symbol list, and then one line per symbol with that symbol's resolved settings:
[MM_TECH_01] starting: gateway=MM_TECH_01 symbols=AAPL,MSFT,TSLA
[MM_TECH_01] [AAPL] strategy=symmetric gap=0.1 qty=500 tif=DAY drift_ticks=3
[MM_TECH_01] [MSFT] strategy=symmetric gap=0.1 qty=500 tif=DAY drift_ticks=3
[MM_TECH_01] [TSLA] strategy=symmetric gap=0.1 qty=500 tif=DAY drift_ticks=3
Every per-symbol decision afterwards (a quote sent, a fill, a reprice) is
tagged [AAPL], [MSFT], or [TSLA] — one process, three independent
quoting lifecycles.
Giving each symbol its own terms¶
Quoting a volatile name and a liquid one with the same spread and size is
rarely what you want. --symbol may be repeated, and every quoting flag
after it applies to that symbol until the next --symbol:
pm-mm-bot --label TECH --qty 500 \
--symbol AAPL --gap 0.10 \
--symbol MSFT --gap 0.20 --qty 300 \
--symbol TSLA --gap 0.50 --qty 200 --drift-ticks 5
Flags before the first --symbol (here --label and --qty 500) are the
defaults for everything; flags after one apply to that symbol alone. The
startup lines now differ per symbol — check them against what you typed:
[MM_TECH_01] [AAPL] strategy=symmetric gap=0.1 qty=500 tif=DAY drift_ticks=3
[MM_TECH_01] [MSFT] strategy=symmetric gap=0.2 qty=300 tif=DAY drift_ticks=3
[MM_TECH_01] [TSLA] strategy=symmetric gap=0.5 qty=200 tif=DAY drift_ticks=5
That command line is already at the edge of readable, which is exactly what
the config file is for. The same desk as mm_tech.yaml:
version: 1
gateway:
label: TECH
id_suffix: "01"
defaults:
strategy: symmetric
gap: 0.10
qty: 500
tif: DAY
drift_ticks: 3
symbols:
AAPL: {} # house defaults
MSFT: # wider, smaller
gap: 0.20
qty: 300
TSLA: # wider still, smaller still, less twitchy,
gap: 0.50 # and working its inventory back toward flat
qty: 200
drift_ticks: 5
strategy: inventory_skew
max_position: 5000
Compare the startup lines with the CLI version above: the same three spreads
and sizes, expressed as a file instead of a command line — plus a per-symbol
strategy for TSLA that would have made the command line longer still.
Worked examples ship with the docs
docs/examples/mm-bot/ contains single-symbol.yaml, tech-desk.yaml
(the file above, with every option commented), and uniform-desk.yaml
showing the symbols: [AAPL, MSFT, TSLA] list shorthand for when no
symbol needs its own block.
Which setting wins?¶
With both a file and a command line in play, each symbol's settings are resolved from the most specific source that supplies one:
| Rank | Source | Example |
|---|---|---|
| 1 | The symbol's own CLI scope | --symbol MSFT --gap 0.20 |
| 2 | The symbol's block in the config file | symbols.MSFT.gap |
| 3 | The global CLI scope | --gap 0.20 before the first --symbol |
| 4 | The file's defaults: block |
defaults.gap |
| 5 | The built-in default | 0.10 |
Rank 2 sitting above rank 3 is the one to think about, and it is deliberate. Try both and read the startup lines:
# Widens only AAPL — the one symbol whose block asks for no gap of its own.
# MSFT keeps 0.20 and TSLA keeps 0.50, because their blocks named a gap.
pm-mm-bot --config mm_tech.yaml --gap 0.30
# Widens TSLA too, because this names it
pm-mm-bot --config mm_tech.yaml --symbol TSLA --gap 0.30
A value someone deliberately wrote into a reviewed file for one symbol is not un-tuned by a throwaway gateway-wide flag. Overriding it is still one short command — you just have to say which symbol you mean.
Confirm all three books are still served from TRADER01:
The spreads should now differ visibly between the three symbols — that is the
per-symbol gap reaching the book.
A struggling symbol doesn't take the others down with it
If one symbol's gap were to violate its own mm_max_spread_ticks
obligation, that symbol alone would be excluded from quoting at startup
(logged as [SYM] excluded from quoting: ...) while the rest of
MM_TECH_01's symbols keep quoting normally — the whole process only
exits if every symbol fails. Try it: add
mm_max_spread_ticks: 2 under one symbol's config, redeploy, and
restart the bot to see it happen.
Checkpoint: one pm-mm-bot process is
quoting all three symbols with three different spreads and sizes, its startup
lines report each symbol's own settings, its log lines are tagged per symbol,
and all three books show two-sided liquidity at visibly different spreads.
Summary¶
You now have:
- Market-maker gateways configured for all symbols, seeded either by hand or
with
pm-config-gen --seed-mm-mid-range. - Manual
QUOTEliquidity in AAPL, MSFT, and TSLA. - Familiarity with
QLEGSas the quote-leg inspection tool. - A clear picture of what
pm-mm-botautomates, and how to drive it from either the CLI or a committed config file. - Experience running one
pm-mm-botprocess across multiple symbols, giving each symbol its own spread, size and strategy, and seeing per-symbol failure isolation in action. - A working understanding of which setting wins when a config file and a command line disagree.
Reflection¶
If no market maker were quoting a symbol, what would happen to a marketable order sent by a regular trader in Chapter 03? Why does the training guide insist you set up liquidity before any trading exercises rather than letting students discover an empty book on their own?
pm-config-gen's stub market_maker_quotes entry (no --seed-mm-mid-range)
passes pm-cverifier with 0 errors, yet still fails at pm-config-deploy.
Why do you think the verifier and the compile-time loader disagree here —
what would it take for the verifier to catch a null bid/ask price itself,
and can you think of a reason the two checks are allowed to diverge like
this rather than the verifier being made strict enough to catch everything
pm-config-deploy would?
Exercise 8 put three symbols behind one gateway ID and one process, each with
its own spread and size. Given that the engine places no limit on how many
symbols a MARKET_MAKER gateway may quote, and that per-symbol tuning no
longer requires per-symbol processes, what's actually left driving the choice
between "one process, many symbols" and "one process per symbol" in a real
deployment? Think about failure blast radius, whether the scenario needs the
symbols to be independently attributable to different market makers, and
process/operational overhead — is there a symbol count where you'd expect to
switch from one pattern to the other?
The precedence table in Exercise 8 puts a per-symbol file value above a
gateway-wide CLI flag, which reverses the usual "the command line always
wins" rule. Argue the other side: what would break, or become surprising, if
--gap 0.30 did override every symbol's gap including the ones a reviewed
file had deliberately tuned? Which behaviour would you rather explain to
someone at 09:29 on a trading morning?
Further Reading¶
- Market Making
- The Market-Maker Bot (
pm-mm-bot) - Market-Maker Bot — Per-symbol settings
- Market-Maker Bot CLI Reference
- Market-Maker Bot — Config File
- Market-Maker Bot — Precedence
- Market-Maker Bot — Per-symbol failure isolation
- ALF Console (pm-alf-console)
- ALF Protocol Reference
- Config Verifier (
pm-cverifier) - 20 — Automation with CommandClient & MM Bot Tuning
Next: 03 — The First Trade