Skip to content

Configuring & Starting Up

Objective

By the end of this chapter you will have a working exchange with at least one gateway and three tradeable symbols, ready to accept orders. In addition you have become familiar with the three tools:

  • pm-config-gen used to automatically generate configuration file based on options and flags.
  • pm-cverifier used to verify an existing (possibly hand-crafterd) configuration file for errors or missing settings
  • pm-config-deploy used to compile a verified configuration file and install it as the artifact pm-engine/pm-scheduler actually read

Prerequisites

Background

EduMatcher requires two essential processes:

  1. pm-engine — the matching engine.
  2. pm-scheduler — drives session phase transitions.

A gateway (pm-alf-console) connects traders to the engine.

Neither process reads engine_config.yaml directly, and neither accepts a config path on its command line. engine_config.yaml is the file you author and edit; what they actually read is a compiled artifact at $EDUMATCHER_DATA_DIR/ref_data/engine_config.json, produced by pm-config-deploy (see 00 — Installation & Setup for where that directory comes from). This chapter's exercises follow that exact pipeline: author → verify → deploy → start.

The artifact carries its own tamper and staleness checks: every load recomputes a SHA-256 over the artifact's own content and refuses to start if it no longer matches what was recorded at compile time, and every process separately checks the artifact's recorded hash of the source YAML against the YAML currently on disk, warning (but still starting) if you edited the source and forgot to redeploy. Exercise 12 walks through both.

Exercise 1: Create a Minimal Configuration

Create a file called engine_config.yaml in your working directory:

symbols:
  AAPL:
    tick_decimals: 2
    last_buy_price: 150.00
    last_sell_price: 150.00
  MSFT:
    tick_decimals: 2
    last_buy_price: 420.00
    last_sell_price: 420.00
  TSLA:
    tick_decimals: 2
    last_buy_price: 250.00
    last_sell_price: 250.00

participants:
  - id: TRADER01
    description: "Alice — first trader"
    role: TRADER
  - id: TRADER02
    description: "Bob — second trader"
    role: TRADER
  - id: GW_ADMIN
    description: "Exchange operator"
    role: ADMIN

Why tick_decimals, not tick_size

A symbol's price granularity is given as a number of decimal places, not a tick size: tick_decimals: 2 means prices move in units of 0.01. The engine matches on integer ticks internally, which is why it is expressed this way.

last_buy_price / last_sell_price seed the reference price used by the price-collar checks. Without them a symbol has no reference and its collar is never enforced — which is exactly the trap Chapter 11 Exercise 2 depends on avoiding.

Keys the engine does not recognise — tick_size, last_price, description on a symbol — are silently ignored rather than rejected, so a typo here fails quietly. pm-cverifier in Exercise 3 is how you catch that.

Checkpoint: file saved, YAML is valid (no tabs!).

This file is only the authored source — nothing reads it yet. pm-engine will not see any of this until you compile and install it with pm-config-deploy in Exercise 5.

Exercise 2: Generate a Config with pm-config-gen

Instead of writing YAML by hand, you can use the pm-config-gen helper to scaffold a configuration. Try generating an equivalent config:

pm-config-gen \
  --symbols AAPL MSFT TSLA \
  --symbol-opts AAPL:tick_decimals=2 \
  --symbol-opts MSFT:tick_decimals=2 \
  --symbol-opts TSLA:tick_decimals=2 \
  --participants TRADER01:TRADER TRADER02:TRADER GW_ADMIN:ADMIN \
  --static-band 0.10 \
  --dynamic-band 0.05 \
  --sessions-enabled \
  --output generated.yaml --force

This writes generated.yaml beside your hand-written file — it does not replace it — with:

  • Three symbols (AAPL, MSFT, TSLA) with 2-decimal tick precision.
  • Two trader gateways and one admin gateway.
  • Static and dynamic price collars pre-configured.
  • Session schedule enabled (PRE_OPEN → CONTINUOUS → CLOSED).

Inspect the generated file:

cat generated.yaml

Compare it with your hand-written engine_config.yaml. Two differences matter. The generated file spells out many settings you left to their defaults, with a comment on each — a good way to discover what can be configured. And it has no reference prices (last_buy_price / last_sell_price): pm-config-gen cannot know what a stock is worth, and the verifier will warn about it (C001) in the next exercise. Price collars and several later exercises need those prices, so keep using your hand-written engine_config.yaml for the rest of the training; generated.yaml is for reference.

Dry-run mode

Add --dry-run to preview the output without writing a file:

pm-config-gen --symbols AAPL MSFT TSLA --participants TRADER01 --dry-run

Adding market-maker gateways

You can include MM gateways in the same command:

pm-config-gen \
  --symbols AAPL MSFT TSLA \
  --participants TRADER01 TRADER02 GW_ADMIN:ADMIN MM_AAPL_01:MARKET_MAKER \
  --enforce-mm-obligations \
  --output mm-example.yaml --force

Chapter 02 explains what a market maker needs in the configuration.

Checkpoint: generated.yaml has the same three symbols and three participants as your hand-written engine_config.yaml.

Exercise 3: Validate the Config with pm-cverifier

Before starting runtime processes, verify the file:

pm-cverifier engine_config.yaml

Check the verdict and exit code:

echo $?

Expected for this chapter config:

  • Verdict is OK or WARN (depending on optional sections you did or did not add).
  • Exit code is 0 when there are no warnings/errors, 1 when warnings exist, and 2 when hard errors exist.

CI-style validation

Treat warnings as failures and emit machine-readable output:

pm-cverifier --strict --format json engine_config.yaml

Focus only on actionable items

Hide info-level advisories while iterating:

pm-cverifier --level warn engine_config.yaml

Checkpoint: you can run pm-cverifier, read the verdict, and interpret its exit code.

Exercise 4: Practice Fixing Verifier Findings

Create a temporary broken config and use pm-cverifier to diagnose it:

cp engine_config.yaml engine_config.bad.yaml

Edit engine_config.bad.yaml and intentionally introduce two issues:

  1. Remove the GW_ADMIN gateway entry.
  2. Set one symbol's tick_decimals to an invalid value like 12.

Run verifier:

pm-cverifier engine_config.bad.yaml

You should see only one finding:

  • S010 error (invalid tick_decimals).

Where is the missing admin gateway? The verifier works in layers — YAML syntax, then the schema of every field (S codes), then the rules between fields (M codes), then completeness advice (C codes) — and it stops after the first layer that finds an error, because later checks would be judging a file it already knows is broken. Fix tick_decimals and run it again: now you see

  • M013 warning (no gateway with role ADMIN).

Put the GW_ADMIN entry back and rerun until the verdict is OK.

Checkpoint: you can reproduce a verifier finding, map it to a check code, and clear it by fixing the config.

Exercise 5: Compile and Deploy the Config with pm-config-deploy

Verified YAML is still just a file on disk — no process reads it until it is compiled and installed. Check that it compiles without installing anything:

pm-config-deploy --check engine_config.yaml

Expected output:

OK — engine_config.yaml compiles (3 symbol(s), 3 gateway(s))

Now actually deploy it:

pm-config-deploy engine_config.yaml

Expected output:

Compiled engine_config.yaml
      to /Users/you/.local/share/edumatcher/ref_data/engine_config.json
   3 symbol(s), 3 gateway(s).
   Restart any running processes to pick it up.

Confirm where it landed:

pm-config-deploy --show
ls -la "$EDUMATCHER_DATA_DIR/ref_data"

Expected behavior:

  • --check validates and reports symbol/gateway counts but writes nothing
  • deploying overwrites the previous ref_data/engine_config.json atomically — a process reading it mid-deploy sees either the old configuration or the new one, never a half-written file
  • if engine_config.yaml fails validation, deploy fails and prints [ERROR] ... followed by Nothing was deployed.; any previous deployment is left untouched
  • deploying does not restart any already-running pm-engine/pm-scheduler — they must be (re)started to pick up the new artifact, which Exercise 6 does next

Checkpoint: you can explain, in one sentence, the difference between engine_config.yaml and $EDUMATCHER_DATA_DIR/ref_data/engine_config.json.

Exercise 6: Start the Engine

Open a terminal and run:

pm-engine --verbose

pm-engine takes no config path on its command line — it always reads whatever is currently deployed at $EDUMATCHER_DATA_DIR/ref_data/engine_config.json, which is exactly the artifact Exercise 5 just installed.

--verbose shows the engine's informational log lines; without it the engine prints only warnings and errors. Expected output includes lines like these (timestamps and paths will differ):

... INFO edumatcher.engine.main - Loaded config from .../ref_data/engine_config.yaml  (3 symbol(s): AAPL, MSFT, TSLA; 3 gateway id(s))
... INFO edumatcher.engine.main - Session handling: enabled
... INFO edumatcher.engine.main - Listening on PULL=tcp://127.0.0.1:5555  PUB=tcp://127.0.0.1:5556

Session handling: enabled means the engine starts in the CLOSED phase and waits to be told when the day begins: your configuration does not set sessions_enabled, and its default is true.

The stable way to confirm the engine actually loaded your config, independent of log wording, is to query it from a gateway once connected (Exercise 8) with SYMBOLS — if it lists AAPL, MSFT, and TSLA, the engine started correctly regardless of what the startup banner said.

Checkpoint: engine is running without errors.

Exercise 7: Start the Scheduler

In a second terminal:

pm-scheduler

Expected output (illustrative):

[INFO] Session state: PRE_OPEN

The scheduler will transition through PRE_OPEN → OPENING_AUCTION → CONTINUOUS automatically, following the timetable in the deployed configuration. To skip the wait and drive the sequence immediately, restart it with --now:

pm-scheduler --now --delay 5

--now starts the day's sequence from this moment instead of the configured wall-clock times; --delay 5 gives you five seconds between phases.

--now runs the whole day

--now does not stop at continuous trading: it carries on through the closing auction to CLOSED, about half a minute later, and exits. That is fine for watching the phases go by, but afterwards the market is closed and orders are rejected with MARKET_CLOSED. Exercise 10 shows how to open it again by hand, which is how the following chapters keep it open.

If nothing was deployed

pm-scheduler refuses to guess a schedule the engine has never seen. If Exercise 5 was skipped, it exits immediately with a fatal error naming pm-config-deploy, instead of silently falling back to a built-in timetable.

Checkpoint: scheduler reports session state changes.

Exercise 8: Connect a Gateway

In a third terminal:

pm-alf-console --id TRADER01

You should see:

Gateway TRADER01 connected.   Type HELP for commands.  Tab=complete  ↑↓=history  Ctrl-A/E=line start/end

[TRADER01]>

Try typing ORDERS — it should report no resting orders for this gateway.

Checkpoint: gateway prompt is interactive and connected.

Exercise 9: Verify the Setup

From the gateway prompt, confirm the three symbols are available by attempting a tiny limit order:

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=1|PRICE=0.01|TIF=DAY

You should see an acknowledgement (the order rests since no matching ask exists).

If instead you see REJECTED ... code=MARKET_CLOSED Market is closed, the scheduler is outside its timetable or has finished its --now day. Do Exercise 10 first — it ends by opening the market — then come back.

Repeat for MSFT and TSLA to confirm all three books are active.

Checkpoint: all three symbols accept orders.

Exercise 10: Connect the Operator Console

Two different consoles — this trips everyone up once

EduMatcher has two interactive consoles, and they accept different commands. Mixing them up is the most common early mistake, because the error is just Unknown command.

pm-alf-console pm-admin
Who it is A trader The exchange operator
Prompt [TRADER01]> [GW_ADMIN|ADMIN]>
Commands NEW, AMEND, CANCEL, STATUS, ORDERS, POS, QUOTE, QLEGS, SYMBOLS, SESSION, INDEX BOOK, ORDERS|GW=, HALT, HALT_SYM, RESUME_SYM, CANCEL_SYM, KILL|GW=, KICK, QCANCEL, SESSION|STATE=, SESSION_STATUS, SCHEDULE, GATEWAYS, VOLUME
Sees the whole book? No — only its own orders Yes

A trader cannot inspect the order book, halt a symbol, or move the session. Those are operator powers, and they live in pm-admin.

Throughout this guide, a prompt of [GW_ADMIN|ADMIN]> means "type this in the pm-admin terminal"; anything else means the trader console.

In a fourth terminal, start the operator console. Note this is pm-admin, not pm-alf-console:

pm-admin --id GW_ADMIN

GW_ADMIN must be a gateway with role: ADMIN in your configuration — your hand-written file from Exercise 1 has one.

Now look at the order book, which no trader console can do:

[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL

You should see a book snapshot (possibly with the 1-lot bid from Exercise 9).

Try two more operator queries:

[GW_ADMIN|ADMIN]> GATEWAYS
[GW_ADMIN|ADMIN]> SESSION_STATUS

GATEWAYS lists every configured gateway and whether it is connected; SESSION_STATUS reports the current trading phase.

Open the market for the chapters ahead

Chapters 02–05 need the market in continuous trading, whatever the time of day. Stop pm-scheduler (Ctrl-C in its terminal) and move the phase yourself:

[GW_ADMIN|ADMIN]> SESSION|STATE=PRE_OPEN
[GW_ADMIN|ADMIN]> SESSION|STATE=CONTINUOUS
[GW_ADMIN|ADMIN]> SESSION_STATUS

SESSION_STATUS should now report CONTINUOUS. The engine only accepts the phase changes of a real trading day, in order — from CLOSED the only legal step is PRE_OPEN — so it takes two commands. Chapter 06 explains this "freeze and advance" technique properly; whenever a later exercise says the market must be open, this is how you open it.

Checkpoint: pm-admin is connected, BOOK|SYM=AAPL shows the book, and you can state which of the two consoles a given command belongs to.

Keep both terminals open from here on

Every later chapter assumes you have a trader console and an operator console available. Leaving both running saves restarting them constantly.

Exercise 11: Inspect Enriched SYMBOLS Metadata

From any connected gateway:

[TRADER01]> SYMBOLS

Besides the symbol names, the table shows each symbol's tick size and its market-maker obligation settings — whether they are enforced, the maximum spread in ticks and the minimum quoted quantity:

┃ #    ┃ Symbol     ┃ Tick ┃ MM Enforced ┃ Max Spread ┃ Min Qty ┃
│ 1    │ AAPL       │ 0.01 │     NO      │         20 │     100 │

Checkpoint: you can read the tick size and the market-maker settings of each symbol from the SYMBOLS output.

Exercise 12: Break — and Recognise — the Artifact's Hash Checks

The compiled artifact records two independent SHA-256 hashes in its meta block: content_sha256 (the artifact against itself) and source_sha256 (the artifact against the engine_config.yaml it was built from). This exercise reproduces both failures on purpose, so you recognise the real message the first time you see it instead of the first time it costs you an afternoon. See Configuration — Verifying the Deployed Artifact for the full explanation this exercise is drilling.

Part A — trip the content-digest check (an error).

In the engine's terminal, stop it with Ctrl-C. Then hand-edit the deployed artifact directly — something pm-config-deploy never does and nothing should:

python3 -c "
import json
p = '$EDUMATCHER_DATA_DIR/ref_data/engine_config.json'
data = json.load(open(p))
data['symbols']['AAPL']['tick_decimals'] = 4
json.dump(data, open(p, 'w'), indent=2)
"

Start the engine again in the same terminal:

pm-engine

Expected: the engine refuses to start, reporting that the compiled config's payload no longer matches its recorded content digest — naming pm-config-deploy, not manual editing, as the fix.

Recover by recompiling the (untouched) source that is still sitting next to the artifact, then start the engine again:

pm-config-deploy engine_config.yaml
pm-engine

A second, different message can come from the same digest check

The content-digest check actually distinguishes two separate causes for the same mismatch, and reports each with a different message. What you just triggered — editing the payload's values — is reported as compiled config has been modified since it was compiled (payload digest ... does not match the recorded ...). If instead the artifact's on-disk bytes are untouched but a newer/older build of EduMatcher declares a different set of configuration fields than the artifact was compiled with, you get a different message: compiled config is intact but was compiled against a different configuration schema ... Nothing has been edited — run pm-config-deploy to recompile it. Both are fixed the same way (pm-config-deploy), but only the second one means nobody touched anything — the check tells the two apart by separately hashing the payload exactly as it sits on disk, so don't go hunting for an edit that never happened if you see that second message after, say, upgrading EduMatcher itself without redeploying.

Part B — trip the source-staleness check (a warning).

Leave this engine running. In another terminal, edit the authored YAML and deliberately skip the deploy step — this only changes the file on disk, it does not affect the already-running engine:

sed -i.bak 's/tick_decimals: 2/tick_decimals: 3/' engine_config.yaml   # edits AAPL's line, the first match

Now stop the engine (Ctrl-C) and start it again without redeploying:

pm-engine

Expected: the engine starts anyway, but logs a warning that engine_config.yaml has changed since the running configuration was compiled and that the exchange is still serving the previous one. Confirm with SYMBOLS from a gateway that AAPL still reports the old tick_decimals — the warning told the truth, nothing was silently applied.

Restore the file and redeploy to clear the warning:

mv engine_config.yaml.bak engine_config.yaml
pm-config-deploy engine_config.yaml

Why the two checks fail differently

A hand-edited artifact is refused outright because a process cannot tell a malicious edit from a benign one, and either way the file no longer means what it claims to. A stale source file is only a warning because the deployed artifact is still perfectly valid — you just have an edit sitting uncommitted next to it. pm-config-deploy is the only fix for either case.

Checkpoint: you have seen both the content-digest error and the source-staleness warning fire for real, and can state which of the two blocks startup and which only warns.

Exercise 13: Weekday / Weekend / Holiday Scheduling

Exercise 2's --sessions-enabled config used the built-in default schedule — the same five times every day, Monday through Friday, with Saturday, Sunday and bank holidays all CLOSED. Real exchanges are rarely that uniform: some run a short Friday, some trade weekends, and all of them close (or shorten hours) on bank holidays that fall on an otherwise-ordinary weekday. pm-config-gen can generate all three variations, and this exercise walks through each.

Resolving which schedule applies to a given calendar day always happens in this order:

  1. If the day is a bank holiday in the configured locale.country, the holidays: block applies — or CLOSED, if no --holidays block was generated.
  2. Otherwise, the day's own entry applies: mon..fri (from weekdays:, unless overridden by --weekend) or sat/sun (from weekend:, only if --weekend was passed).

Any day left with no resolved entry — including Saturday and Sunday by default — is CLOSED all day, exactly like today's config already leaves them.

Add a weekend session.

By default pm-config-gen leaves Saturday and Sunday CLOSED. Pass --weekend to give them their own (shorter) session:

pm-config-gen \
  --symbols AAPL MSFT TSLA \
  --symbol-opts AAPL:tick_decimals=2 \
  --symbol-opts MSFT:tick_decimals=2 \
  --symbol-opts TSLA:tick_decimals=2 \
  --participants TRADER01:TRADER TRADER02:TRADER GW_ADMIN:ADMIN \
  --static-band 0.10 \
  --dynamic-band 0.05 \
  --sessions-enabled \
  --weekend \
  --weekend-pre-open 10:00 \
  --weekend-opening-auction 10:25 \
  --weekend-continuous 10:30 \
  --weekend-closing-auction 14:00 \
  --weekend-closing-end 14:05 \
  --output engine_config.yaml --force

Inspect the generated schedule: block — Saturday and Sunday now carry their own five transition times instead of being absent.

Add a holiday session, and set the country.

Pass --holidays to give bank holidays a schedule of their own instead of CLOSED (a common real-world case is a reduced half-day rather than a full close):

pm-config-gen \
  --symbols AAPL MSFT TSLA \
  --symbol-opts AAPL:tick_decimals=2 \
  --symbol-opts MSFT:tick_decimals=2 \
  --symbol-opts TSLA:tick_decimals=2 \
  --participants TRADER01:TRADER TRADER02:TRADER GW_ADMIN:ADMIN \
  --static-band 0.10 \
  --dynamic-band 0.05 \
  --sessions-enabled \
  --holidays \
  --holidays-pre-open 10:00 \
  --holidays-opening-auction 10:25 \
  --holidays-continuous 10:30 \
  --holidays-closing-auction 13:00 \
  --holidays-closing-end 13:05 \
  --country DE \
  --output engine_config.yaml --force

--country is what tells pm-scheduler which calendar of bank holidays to check a date against before it decides whether the holidays: block even applies — it generates the locale.country key in the config's locale: block. It accepts a country name ("Sweden") or an ISO 3166-1 alpha-2 code ("DE"), and it does only that — it has no effect on the session timezone (that is the sibling locale.session_timezone key, generated by --session-timezone, out of scope here). Get it wrong and the consequence is silent, not a crash: an unrecognised value makes pm-cverifier raise a M026 warning, and at runtime pm-scheduler logs a warning and falls back to "Sweden" — so a Germany-based deployment left at the default, or given a typo'd country name, will silently follow the Swedish holiday calendar instead, trading straight through a local bank holiday it should have been closed for (or vice versa). There is no separate validation step for this — run pm-cverifier (Exercise 3) after any change to locale.country or the schedule and read its output, don't just deploy.

Verify the country was picked up:

pm-cverifier engine_config.yaml

Deploy and confirm the resolved schedule end-to-end:

pm-config-deploy engine_config.yaml

Restart pm-scheduler (Exercise 7) and, from a gateway console (Exercise 8/10), query SESSION — or check the engine's own wire replies — to see the resolved weekly table, including today's entry and whether today is currently flagged as a holiday.

weekend: and sat:/sun: are mutually exclusive

--weekend generates a single shared block for both days. There is no pm-config-gen flag for giving Saturday and Sunday different hours — that requires hand-editing the generated sat:/sun: keys directly in engine_config.yaml (see Session Scheduling — Configuring the schedule). A config that sets both weekend: and an individual sat:/sun: block is rejected at load — they say two different things about the same day.

Checkpoint: generated config has a weekend: block, a holidays: block, and locale.country: DE; pm-cverifier reports no M026 warning; the deployed schedule reflects all three when queried from a running engine.

Summary

You now have:

  • A configuration file defining 3 symbols and 3 gateways.
  • A repeatable verifier workflow (pm-cverifier) to catch config problems before startup.
  • A compiled, deployed configuration (pm-config-deploy) that pm-engine and pm-scheduler actually read.
  • A running engine, scheduler, and at least one trader gateway.
  • Confirmation that all symbols accept orders.
  • Firsthand experience of the artifact's content-digest error and source-staleness warning, and which fix (pm-config-deploy) clears both.
  • A schedule with weekend trading, a bank-holiday session, and the correct locale.country for its holiday calendar — and the ability to tell, from pm-cverifier, when locale.country isn't one.

Reflection

Why does the engine, scheduler, and each gateway all run as separate processes connected over ZMQ sockets, instead of one monolithic program? What would you lose (or gain) operationally if the scheduler crashed while the engine kept running?

pm-engine and pm-scheduler never accept a config path, and neither reads engine_config.yaml directly. What problem does forcing every process through one compiled artifact (pm-config-deploy) solve that letting each process parse its own copy of the YAML would not?

Exercise 12's two checks fail at different severities — one refuses to start, the other only warns. Why is a hand-edited artifact treated as more serious than a stale source file, given that both mean the running configuration is not what engine_config.yaml currently says?

An unrecognised locale.country doesn't stop the config from deploying — it only warns, both from pm-cverifier (M026) and from pm-scheduler at runtime, then silently falls back to Sweden's holiday calendar. Why might a design choose "warn and fall back" here instead of refusing to deploy, the way an invalid or incomplete schedule block does? What's different about getting locale.country wrong versus getting the schedule times wrong?

Further Reading

Next: 02 — Setting Up Market-Maker Liquidity