Skip to content

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-engine running and the market open in CONTINUOUS (chapter 01, "Open the market for the chapters ahead").
  • The operator console connected (for book checks), and your working folder as your current directory (cd /data/training on the container route).

Which exercises chapter 03 needs

Chapter 03 trades against the manual quotes you create in Exercises 1–5. Exercises 6–8 replace them with market-maker bots and change the configuration, so they are optional for now: do them after chapter 03, or use "Returning to the manual-quote state" at the end of this chapter to get back.

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
  MSFT:
    tick_decimals: 2
    last_buy_price: 420.00
    last_sell_price: 420.00
    market_maker_quotes:
      - gateway_id: MM_MSFT_01
        bid_price: 419.90
        ask_price: 420.10
        bid_qty: 300
        ask_qty: 300
  TSLA:
    tick_decimals: 2
    last_buy_price: 250.00
    last_sell_price: 250.00
    market_maker_quotes:
      - gateway_id: MM_TSLA_01
        bid_price: 249.75
        ask_price: 250.25
        bid_qty: 200
        ask_qty: 200

These are the prices and sizes the manual QUOTE commands below use too, so the quote you send replaces a seed that looks exactly the same.

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, MM_MSFT_01, MM_TSLA_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 ADMIN_CLI:ADMIN \
    MM_AAPL_01:MARKET_MAKER MM_MSFT_01:MARKET_MAKER MM_TSLA_01:MARKET_MAKER \
  --enforce-mm-obligations \
  --output mm-stub.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 ADMIN_CLI: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 mm-seeded.yaml --force

These write to their own files on purpose: generated files have no reference prices, so writing them over your hand-written engine_config.yaml would silently lose what later chapters need.

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:

pm-cverifier engine_config.yaml       # expect 0 errors (the M032 warning is fine)
pm-config-deploy engine_config.yaml
cp engine_config.yaml engine_config.ex1.yaml   # keep a copy of this state

The copy is for the optional Exercise 8, which changes engine_config.yaml.

Then restart pm-engine to pick up the new gateways (Ctrl-C, then pm-engine --verbose). A restarted engine starts in CLOSED again. Your consoles log out when they detect the lost connection; start pm-admin and your trader console again, then reopen the market from the operator console (SESSION|STATE=PRE_OPEN, then SESSION|STATE=CONTINUOUS).

Now look at the books before any market maker has connected:

[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL

They are already two-sided: at start-up the engine places each seed quote from market_maker_quotes on the market maker's behalf (the engine log shows MM quote SEED-MM_AAPL_01-AAPL-1 …). The seeds are what keeps the books from being empty the moment the exchange starts, and the exercises below take them over.

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:

pm-alf-console --id MM_AAPL_01

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. Because the engine already placed a seed quote for this market maker, your QUOTE replaces it, and you see the old one go first:

[<time>] CANCELLED <seed bid leg id> reason=QUOTE_REPLACED
[<time>] CANCELLED <seed ask leg id> reason=QUOTE_REPLACED
[<time>] QUOTE CANCELLED  SEED-MM_AAPL_01-AAPL-1  Replaced by new quote
[<time>] QUOTE ACK  AAPL-MM-001  bid=<order id> ask=<order id>
[<time>] QUOTE ACTIVE  AAPL-MM-001

A market maker holds at most one active quote per symbol; sending another always replaces the previous one.

Checkpoint: AAPL quote acknowledged and active.

Exercise 3: Quote MSFT and TSLA

Open one terminal per MM gateway:

pm-alf-console --id MM_MSFT_01
pm-alf-console --id MM_TSLA_01

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:

[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL

You should see a two-sided book with the MM's bid and ask (the prices are the same as the seed's, so the book looks the same as before — what changed is who owns the quote). Repeat for MSFT and TSLA.

[GW_ADMIN|ADMIN]> BOOK|SYM=MSFT
[GW_ADMIN|ADMIN]> BOOK|SYM=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. With SHOW=ALL you also see the legs of the seed quote that your QUOTE replaced, marked CANCELLED; SHOW=ACTIVE hides them. This is the operator view that helps you reconcile fills after restart or partial execution.

Interpretation guide:

  • Rem is open quantity still resting in the book.
  • Filled is already executed quantity on that leg.
  • Leg status is one of NEW, PARTIAL, FILLED, CANCELLED, EXPIRED, or PENDING — a resting, untouched leg shows NEW, not RESTING.

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.)

First, exit the three market-maker consoles (EXIT in each). A bot connects under the same gateway ID as the console (MM_AAPL_01, …) and the engine allows only one connection per ID, so while a console is open the bot is refused with auth rejected: Gateway already connected: MM_AAPL_01. Leaving the consoles also cancels their quotes (CANCEL_QUOTES_ONLY in the configuration), so the books are left without a market maker and the bots need a starting price. Give each one a range to start in with --initial_min and --initial_max; it draws its first mid-price at random inside it. Then, in three terminals:

pm-mm-bot --symbol AAPL --gap 0.10 --qty 500 --initial_min 149 --initial_max 151
pm-mm-bot --symbol MSFT --gap 0.16 --qty 300 --initial_min 419 --initial_max 421
pm-mm-bot --symbol TSLA --gap 0.20 --qty 200 --initial_min 249 --initial_max 251

Add --verbose to any of these to follow what the bot does; by default it prints only warnings and errors. The quotes now sit around the random mid-price the bot chose, not at the prices of Exercises 2–3: check with BOOK|SYM=AAPL in the operator console.

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 of its own gateway — the engine's seed quote, for instance, when no console is holding the ID — instead of creating a duplicate, and periodically re-runs QLEGS to reconcile leg state in case an engine reply was ever dropped.

The 20-tick limit on --gap

The gaps above are not arbitrary. Every symbol has a maximum quoted spread, mm_max_spread_ticks, which is 20 ticks unless you configure otherwise — the Max Spread column of SYMBOLS in Chapter 01. At tick_decimals: 2 that is 0.20, and a bot started with a wider --gap refuses to quote that symbol: excluded from quoting: --gap exceeds mm_max_spread_ticks obligation.

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:

  • QBOOT asks the engine whether a gateway+symbol already has an active quote slot (for example after a crash/restart).
  • QLEGS reconciles 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 22 — 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 (Optional)

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:
    initial_min: 149
    initial_max: 151

The symbols: block names AAPL and gives it a starting range (the bot needs a reference price on an empty book, as in Exercise 6). Run the bot from the file instead of typing the flags (exit any other process connected as MM_AAPL_01 first):

pm-mm-bot --config mm_aapl.yaml --verbose

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

A symbol's block holds its overrides: here only the start range, so AAPL is quoted on the house defaults. A symbol with nothing to override is written AAPL: {} — the empty braces are required YAML, not decoration, because the value of a symbol key is its override block.

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:

pm-mm-bot --config mm_aapl.yaml --gap 0.15

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.

Three 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. strategy: passive keeps its quotes behind other traders' orders instead of in front of them (see the passive strategy). 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 (Optional)

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 if you ran them (Ctrl+C each — a bot cancels its own quote when it exits) 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 seed rule is per symbol, not per gateway

pm-cverifier's M001 check fires for a symbol that has no market_maker_quotes entry at all; it does not check which gateway is meant to quote it. The seeds from Exercise 1 therefore already satisfy it, and the configuration verifies clean without a seed for MM_TECH_01. Add one anyway — a second item in each symbol's market_maker_quotes list, naming MM_TECH_01 — because a seed is also what gives a bot something to start from (next paragraph). If you regenerate with pm-config-gen, pass --participants ... MM_TECH_01:MARKET_MAKER and it seeds all three symbols against it.

For AAPL the list becomes (and the same for MSFT and TSLA, with their own prices and sizes):

    market_maker_quotes:
      - gateway_id: MM_AAPL_01
        bid_price: 149.95
        ask_price: 150.05
        bid_qty: 500
        ask_qty: 500
      - gateway_id: MM_TECH_01
        bid_price: 149.95
        ask_price: 150.05
        bid_qty: 500
        ask_qty: 500

Check and deploy as before, restart the engine, restart pm-admin (the engine restart disconnected it, and KILL would answer Gateway not connected: GW_ADMIN) and reopen the market. The old market makers' seed quotes are still in the books, so clear them from the operator console and leave MM_TECH_01 the only quoter:

[GW_ADMIN|ADMIN]> KILL|GW=MM_AAPL_01
[GW_ADMIN|ADMIN]> KILL|GW=MM_MSFT_01
[GW_ADMIN|ADMIN]> KILL|GW=MM_TSLA_01

Now start one bot covering all three symbols on identical terms:

pm-mm-bot --verbose --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

A little further down, each symbol reports adopted existing quote SEED-MM_TECH_01-AAPL-2 … (and the same for MSFT and TSLA): the bot found its own seed quote through QBOOT and took it over instead of quoting afresh. Every per-symbol decision afterwards (a quote sent, a fill, a reprice) is tagged [AAPL], [MSFT], or [TSLA] — one process, three independent quoting lifecycles.

A bot needs a reference price to start

Stop the bot with Ctrl+C and its quotes are cancelled — the books are empty again. Start it once more and it exits with startup failed: no reference price available. A bot prices around a reference: the book's mid, the last trade, a quote of its own gateway that QBOOT finds, or, failing all of those, a starting range you give it with initial_min / initial_max (--initial_min / --initial_max on the command line). The variants below all state a range per symbol so that they start on an empty book; see Bootstrap: starting with an empty book. The bot then draws its first mid at random inside the range, so the exact prices in your books will differ from run to run.

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 --verbose --label TECH --qty 500 \
  --symbol AAPL --gap 0.10 --initial_min 149 --initial_max 151 \
  --symbol MSFT --gap 0.16 --qty 300 --initial_min 419 --initial_max 421 \
  --symbol TSLA --gap 0.20 --qty 200 --drift-ticks 5 --initial_min 249 --initial_max 251

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.16 qty=300 tif=DAY drift_ticks=3
[MM_TECH_01] [TSLA] strategy=symmetric gap=0.2 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, plus a starting range
    initial_min: 149
    initial_max: 151

  MSFT:                    # wider, smaller
    gap: 0.16
    qty: 300
    initial_min: 419
    initial_max: 421

  TSLA:                    # wider still, smaller still, less twitchy,
    gap: 0.20              # and working its inventory back toward flat
    qty: 200
    drift_ticks: 5
    strategy: inventory_skew
    max_position: 5000
    initial_min: 249
    initial_max: 251
pm-mm-bot --verbose --config mm_tech.yaml

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 (a similar desk 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.16
2 The symbol's block in the config file symbols.MSFT.gap
3 The global CLI scope --gap 0.18 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.16 and TSLA keeps 0.20, because their blocks named a gap.
pm-mm-bot --verbose --config mm_tech.yaml --gap 0.18

# Changes TSLA too, because this names it. (0.18 is narrower than its 0.20, but
# the point is that the symbol's own CLI scope wins.)
pm-mm-bot --verbose --config mm_tech.yaml --symbol TSLA --gap 0.18

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 the books from the operator console:

[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL
[GW_ADMIN|ADMIN]> BOOK|SYM=MSFT
[GW_ADMIN|ADMIN]> BOOK|SYM=TSLA

The spreads should now differ visibly between the three symbols — 0.10, 0.16 and 0.20 — that is the per-symbol gap reaching the book. (If you adopted the seeds instead of starting from a range, the first spreads are the seeds' own; force the bot to quote on its own terms with QCANCEL|GW=MM_TECH_01|SYM=AAPL — the bot notices the cancel and reissues at its configured gap.)

A struggling symbol doesn't take the others down with it

If one symbol's gap violates its own mm_max_spread_ticks obligation (20 ticks unless configured), that symbol alone is excluded from quoting at startup while the rest of MM_TECH_01's symbols keep quoting normally — the whole process only exits if every symbol fails. Try it: change TSLA's gap in mm_tech.yaml to 0.50 and restart the bot. Its log says

[MM_TECH_01] [TSLA] excluded from quoting: --gap exceeds mm_max_spread_ticks obligation (0.500000 > 20 * 0.010000)

and AAPL and MSFT quote as before.

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.

Returning to the manual-quote state

Chapter 03 needs the state of Exercises 1–5: MM_AAPL_01 connected with the manual quote AAPL-MM-001. If you have since run the bots of Exercises 6–8 (or simply want a clean start), get back to it:

  1. Stop every pm-mm-bot (Ctrl-C; a bot cancels its quotes on exit).
  2. Restore the configuration from Exercise 1 and deploy it:
cp engine_config.ex1.yaml engine_config.yaml
pm-config-deploy engine_config.yaml
  1. Restart pm-engine, then restart pm-admin, and reopen the market with SESSION|STATE=PRE_OPEN and SESSION|STATE=CONTINUOUS.
  2. Connect pm-alf-console --id MM_AAPL_01 (and the MSFT and TSLA ones if you want them) and send the QUOTE commands of Exercises 2 and 3 again.
  3. In pm-admin, BOOK|SYM=AAPL shows 500 bid at 149.95 and 500 ask at 150.05. Orders that a trader placed earlier (in chapter 03, say) survive the engine restart and are still in the book; clear them with KILL|GW=TRADER01 (and KILL|GW=TRADER02) if you want the clean state.

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 QUOTE liquidity in AAPL, MSFT, and TSLA.
  • Familiarity with QLEGS as the quote-leg inspection tool.
  • A clear picture of what pm-mm-bot automates, and how to drive it from either the CLI or a committed config file.
  • Experience running one pm-mm-bot process 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

Next: 03 — The First Trade