Configuration¶
Learning objectives
After reading this page you will understand:
- Which
engine_config.yamlsections are required when a config file exists - How to generate a starter config with
pm-config-gen - Which fields the current engine and scheduler parsers recognize
- How to configure the optional
pm-ralf-gwy,pm-md-gwy,pm-balf-gwy,pm-dc-gwy,pm-index,pm-log-srv, andpm-api-gwyblocks - How to configure symbols, gateways, risk controls, market-maker seeds, combo seeds, and schedules
- How to inspect a deployed config at a glance with
pm-config-show - How to choose between minimal, medium, and fully featured configurations
- Which checks to perform before using a config in a class, demo, or test
Configuration Workflow¶
engine_config.yaml is compiled, not read directly. The file you author is
never the file any process runs — it always passes through the same
create → verify → deploy pipeline before it becomes the ref-data artifact the
exchange actually loads:
flowchart LR
A["Create config<br/>pm-config-gen / config-gui"] --> B{"Verify<br/>pm-cverifier"}
B -- issues found --> A
B -- clean --> C["Deploy<br/>pm-config-deploy"]
C --> D[("Compiled ref-data artifact<br/>ref_data/engine_config.json")]
D --> E["pm-engine, pm-scheduler,<br/>and gateway processes"]
D -.-> F["Inspect<br/>pm-config-show"]
The rest of this page documents each stage: generating a starter file, verifying it, deploying it as the compiled artifact every process reads, and inspecting what was deployed.
Configuring the Exchange¶
The matching engine and session scheduler both read engine_config.yaml. The
engine uses it to define the symbol universe, authenticated ALF gateway IDs,
session mode, risk controls, market-maker policy, per-symbol outstanding shares,
and startup seeds. The scheduler uses only the optional schedule section.
The optional post_trade_gateway, market_data_gateway, dc_gateway,
log_server, and api_gateways sections are read by pm-ralf-gwy,
pm-md-gwy, pm-dc-gwy, pm-log-srv, and pm-api-gwy respectively.
If the config file is absent, pm-engine starts in unrestricted mode: any symbol
and gateway can be used, and no startup seeds are loaded. If the config file is
present, the parser requires two sections:
symbols- a mapping of accepted symbols; generated examples include a positive integeroutstanding_sharesfield for each symbolgateways.alf- a list with at least one accepted ALF gateway
The sample engine_config.yaml intentionally keeps the
live configuration minimal and places the full parser-recognized shape in
comments. This page explains that shape in operational terms.
Prefer a form over the CLI? Use the Config Builder GUI
If you would rather build or edit engine_config.yaml visually — with live
validation, per-field help, and import of an existing file — see the
Configuration GUI (config-gui) chapter. It targets the
same file format as pm-config-gen described below.
gateways.alf is the only sub-key under gateways:
The gateways: mapping only contains alf. BALF, CALF, DC, and the
centralized log server are configured via separate top-level keys
(balf_gateway, market_data_gateway, dc_gateway, and log_server)
and are read by their own processes, not by pm-engine.
Each protocol's configuration lives in a different part of engine_config.yaml:
- ALF — configured under
gateways.alf; used bypm-engineto authenticate order-entry connections frompm-alf-consoleandpm-alf-gwy, as well aspm-balf-gwy(the gateway id used in the BALF configurations must exist undergateways.alf). Uses a pipe-delimited text format (FIELD=VALUE|FIELD=VALUE). - BALF — configured under the top-level
balf_gatewaykey; used bypm-balf-gwy. Uses fixed-width binary frames with sequence numbers and integer-scaled prices, targeting programmatic clients where text-parsing overhead is undesirable. See BALF Gateway for more usage and BALF Protocol for the full specification. - CALF — configured under the top-level
market_data_gatewaykey; used bypm-md-gwy. Provides a subscribe/unsubscribe market-data feed delivering order-book snapshots, trade prints, and session-state changes over a persistent TCP connection with sequence-based gap detection. See Market Data Feed for usage and CALF Protocol for the full protocol specification. - RALF — configured under the top-level
post_trade_gatewaykey; used bypm-ralf-gwy. Provides a replayable audit feed of all executed trades, including the original order details, over a persistent TCP connection with sequence-based gap detection. See Post Trade for usage and RALF Protocol for the full protocol specification. - DC1 — configured under the top-level
dc_gatewaykey; used bypm-dc-gwy. Relays the engine's internal drop-copy feed to plain TCP clients that cannot speak ZeroMQ, using the lightweight DC1 text protocol. See Drop-Copy Gateway for full usage and protocol details. - LALF — configured under the top-level
log_serverkey; used bypm-log-srv. Collects operationallogging-module output from every otherpm-*process over a persistent TCP connection into a queryable SQLite database. The same key also configures LALF-PS, the ZeroMQPUB/PULLinterface that distributes those rows back out to live log viewers. See Centralized Log Server for usage and LALF Protocol Reference for the full protocol specification. - A Full overview of all protocol and their intended usage can be found in Protocols Overview.
File Location¶
EduMatcher separates the configuration you author from the one the exchange runs.
The authored engine_config.yaml lives wherever suits you — normally under
version control alongside the rest of your course material. Edit it, review it,
diff it.
What the exchange runs is a compiled artifact at
<EDUMATCHER_DATA_DIR>/ref_data/engine_config.json. That is the only file any
running process reads. No process accepts a config path, so it is not possible
to start two of them against different files.
<EDUMATCHER_DATA_DIR> is resolved centrally by the runtime configuration
module. EDUMATCHER_DATA_DIR takes precedence; without it, a source checkout
uses <repo>/src/data/, while an installed production package uses
~/.local/share/edumatcher. Source mode is determined from the installed
location of edumatcher/config.py (its package parent is named src), not from
the current working directory. See Getting Started — how the default is
selected for the exact
precedence rules.
The same resolver places configured relative runtime paths, such as
data/stats.db and data/log.db, under this shared data directory. This keeps
pm-engine, pm-stats, pm-log-srv, and pm-api-gwy on the same files even
when they are launched from different directories.
See Compile Configs with pm-config-deploy
for how the authored file becomes that artifact.
Generate Configs with pm-config-gen¶
pm-config-gen creates a parser-compatible engine_config.yaml from concise
CLI inputs. It is designed for operators and instructors who want to bootstrap
new sessions without manually writing large YAML blocks.
Use it when:
- you are creating a new class/demo config from scratch
- you want consistent defaults and validation hints
- you need repeatable config generation in scripts
Generate Configs with config-gui¶
A more user friendly way to create a configuration file (also known as
reference data) is to spin up the Web-App. The easiest way to run it is by running the
container image. You can use either docker or podman but in the examples we use
podman
- Download latest
edumatcher-config-gui-<VERSION>.tar.gzand unzip podman load --input dist/edumatcher-config-gui-<VERSION>tar.gz- Then run:
podman run -p 8080:8080 edumatcher-config-gui:<VERSION>
You can then access the UI at http://localhost:8080/
More details of the Web app and how to use it can be found in Configuration GUI
Verify Configs with pm-cverifier¶
Before starting the engine with a hand-written or generated config, run
pm-cverifier to get a deep, actionable report on every problem the config
contains — not just the first one the engine would encounter.
# human-readable report
pm-cverifier engine_config.yaml
# CI-friendly: fail on any warning, output JSON
pm-cverifier --strict --format json engine_config.yaml
pm-cverifier is read-only and safe to run at any time. It reports:
- every YAML syntax or schema error that would prevent the engine from starting
- semantic inconsistencies (e.g. sessions enabled but no schedule, MM gateway
without seed quotes, index constituent not in
symbols) - completeness advisories (e.g. no reference prices, MM obligations not enforced)
- a plain-English Risk Summary showing what collars, circuit breakers, and gateways are actually active
For a full description of all check codes and CLI options, see Config Verifier (pm-cverifier).
Quick start¶
Installed mode:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--sessions-enabled \
--output engine_config.yaml
Poetry/source mode:
poetry run pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--sessions-enabled \
--output engine_config.yaml
Print to stdout only (no file write):
pm-config-gen \
--symbols AAPL \
--gateways TRADER01 \
--outstanding-shares AAPL:15400000000 \
--dry-run
Important behavior¶
- If
--outputis omitted, YAML is printed to stdout. - If
--outputexists, generation fails unless--forceis set. - If any gateway is
MARKET_MAKERand you do not pass--seed-mm-mid-range, MM quote stubs are emitted withbid_price: nullandask_price: null. Fill these values before startingpm-engine. - If you pass
--seed-mm-mid-range, MM quotes are emitted with concrete prices on the configured tick grid. - Loader validation is skipped only in the MM-stub case above. It runs automatically for non-MM configs and MM configs with seeded midpoints.
MM quote generation decision matrix:
| Gateway/flags state | Generated market_maker_quotes |
Generated last_buy_price / last_sell_price |
|---|---|---|
No MARKET_MAKER gateway configured |
No MM quote section emitted | Only emitted if --seed-last-prices is set (as null placeholders) |
MARKET_MAKER present, no --seed-mm-mid-range |
Stub quotes with bid_price: null, ask_price: null |
null placeholders only if --seed-last-prices is set |
MARKET_MAKER present, with --seed-mm-mid-range MIN:MAX |
Concrete bid/ask quote prices generated on tick grid | If --seed-last-prices-from-mm is set, both are set to the same midpoint used for seeded quotes |
In this guide, "MM stub" means a quote row exists but prices are null and must
be filled manually. "Full MM setup" means concrete bid/ask prices are generated
for each MM quote seed at generation time.
Option reference¶
Required inputs:
| Option | Type | Description |
|---|---|---|
--symbols SYM [SYM ...] |
Repeatable tokens | Symbol universe (uppercased on parse) |
--gateways GW_SPEC [GW_SPEC ...] |
Repeatable tokens | Gateway specs as ID[:ROLE[:DISCONNECT[:DESCRIPTION]]] |
--gateway-smp GW_ID:SMP_ACTION |
Repeatable | Sets gateways.alf[<GW_ID>].smp_action (NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH); GW_ID must be one of --gateways; omitted gateways stay at NONE. See Risk Controls — Self-Match Prevention |
Session and schedule options:
| Option | Type | Default | Description |
|---|---|---|---|
--sessions-enabled / --no-sessions-enabled |
Flag pair | false |
Enable/disable scheduler-driven sessions |
--schedule / --no-schedule |
Flag pair | auto | Force include/exclude schedule; auto emits when sessions are enabled |
--pre-open HH:MM |
String | 09:00 |
Schedule pre-open time |
--opening-auction HH:MM |
String | 09:25 |
Opening auction start |
--continuous HH:MM |
String | 09:30 |
Continuous start |
--closing-auction HH:MM |
String | 16:00 |
Closing auction start |
--closing-end HH:MM |
String | 16:05 |
Closing auction end |
Core engine and risk options:
| Option | Type | Default | Description |
|---|---|---|---|
--snapshot-interval SECS |
float (> 0) |
0.5 |
engine_tuning.snapshot_interval_sec |
--quote-history-maxlen N |
int (> 0) |
30 |
engine_tuning.quote_history_maxlen |
--drop-copy-buffer-size N |
int (> 0) |
10000 |
engine_tuning.drop_copy_buffer_size |
--recent-trades-maxlen N |
int (> 0) |
20 |
engine_tuning.recent_trades_maxlen |
--depth-snapshot-tolerance-ticks N |
int (> 0) |
100 |
engine_tuning.depth_snapshot_tolerance_ticks |
--no-collars |
Flag | off | Emit enforce_collars: false |
--no-circuit-breakers |
Flag | off | Emit enforce_circuit_breakers: false |
--static-band PCT |
float in (0,1) |
unset | Default risk-control static band (DEFAULT level) |
--dynamic-band PCT |
float in (0,1) |
unset | Default risk-control dynamic band (DEFAULT level) |
--symbol-static-band SYM:PCT |
Repeatable | none | Per-symbol collar.static_band_pct override |
--symbol-dynamic-band SYM:PCT |
Repeatable | none | Per-symbol collar.dynamic_band_pct override |
--symbol-risk-level SYM:LEVEL |
Repeatable | none | Per-symbol symbols.<SYM>.level override |
--risk-level NAME:STATIC[:DYNAMIC] |
Repeatable | none | Add named risk levels under risk_controls.levels |
--cb-levels NAME:SHIFT[:HALT_MINS[:RESUMPTION_MODE]] ... |
List | built-in ladder | Circuit-breaker level specs; RESUMPTION_MODE is AUCTION (default) or CONTINUOUS |
--cb-window-ns NS |
int (> 0) |
300000000000 |
Circuit-breaker reference window |
Market-maker and symbol defaults:
| Option | Type | Default | Description |
|---|---|---|---|
--mm-spread-ticks N |
int (> 0) |
20 |
Global MM spread threshold |
--mm-min-qty N |
int (> 0) |
100 |
Global MM min quantity |
--enforce-mm-obligations / --no-enforce-mm-obligations |
Flag pair | false |
Global MM obligation toggle |
--tick-decimals N |
int 0..8 |
2 |
Default tick_decimals for symbols |
--outstanding-shares SYM:N |
Repeatable | none | Per-symbol outstanding shares in the generated config |
--seed-last-prices |
Flag | off | Emit last_buy_price/last_sell_price placeholders |
--seed N |
int | random source default | Deterministic RNG seed for generated training values |
--seed-mm-mid-range MIN:MAX |
string | none | Seed MM quotes from a random midpoint in the inclusive price range |
--seed-last-prices-from-mm |
Flag | off | Set last_buy_price/last_sell_price to the same midpoint used for seeded MM quotes |
Output and safety options:
| Option | Type | Default | Description |
|---|---|---|---|
--output FILE |
Path | none | Write YAML to file |
--force |
Flag | off | Overwrite existing output file |
--dry-run |
Flag | off | Print YAML only; do not write file |
--comment-default-config-fields |
Flag | off | Add a header comment block listing defaultable engine_config.yaml fields currently omitted from the generated file |
Post-trade gateway options:
| Option | Type | Default | Description |
|---|---|---|---|
--post-trade-gateway |
Flag | off | Emit top-level post_trade_gateway block for pm-ralf-gwy |
--post-trade-name |
string | ralf-gwy01 |
post_trade_gateway.name |
--post-trade-bind-address |
string | 0.0.0.0 |
post_trade_gateway.bind_address |
--post-trade-port |
int (> 0) |
5580 |
post_trade_gateway.port |
--post-trade-replay-retention-sec |
int (> 0) |
86400 |
post_trade_gateway.replay_retention_sec |
--post-trade-heartbeat-interval-sec |
int (> 0) |
1 |
post_trade_gateway.heartbeat_interval_sec |
--post-trade-idle-timeout-sec |
int (> 0) |
5 |
post_trade_gateway.idle_timeout_sec |
--post-trade-max-client-queue |
int (> 0) |
10000 |
post_trade_gateway.max_client_queue |
--post-trade-allowed-roles ROLE [ROLE ...] |
list | CLEARING DROP_COPY AUDIT |
post_trade_gateway.allowed_roles |
Market-data gateway options:
| Option | Type | Default | Description |
|---|---|---|---|
--market-data-gateway |
Flag | off | Emit top-level market_data_gateway block for pm-md-gwy |
--market-data-enabled / --market-data-disabled |
Flag pair | unset (true when emitted) |
Set market_data_gateway.enabled |
--market-data-name |
string | md-gwy01 |
market_data_gateway.name |
--market-data-bind-address |
string | 0.0.0.0 |
market_data_gateway.bind_address |
--market-data-port |
int (> 0) |
5570 |
market_data_gateway.port |
--market-data-heartbeat-interval-sec |
int (> 0) |
1 |
market_data_gateway.heartbeat_interval_sec |
--market-data-idle-timeout-sec |
int (> 0) |
5 |
market_data_gateway.idle_timeout_sec |
--market-data-replay-window-sec |
int (> 0) |
30 |
market_data_gateway.replay_window_sec |
--market-data-max-symbols-per-client |
int (> 0) |
200 |
market_data_gateway.max_symbols_per_client |
--market-data-max-client-queue |
int (> 0) |
10000 |
market_data_gateway.max_client_queue |
--market-data-depth-levels |
int (> 0) |
10 |
market_data_gateway.depth_levels |
BALF gateway options:
| Option | Type | Default | Description |
|---|---|---|---|
--balf-gateway |
Flag | off | Emit top-level balf_gateway block for pm-balf-gwy |
--balf-name |
string | balf-gwy01 |
balf_gateway.name |
--balf-bind-address |
string | 0.0.0.0 |
balf_gateway.bind_address |
--balf-port |
int (> 0) |
5560 |
balf_gateway.port |
--balf-heartbeat-interval-sec |
int (> 0) |
1 |
balf_gateway.heartbeat_interval_sec |
--balf-heartbeat-timeout-sec |
int (> 0) |
5 |
balf_gateway.heartbeat_timeout_sec |
--balf-idle-timeout-sec |
int (> 0) |
30 |
balf_gateway.idle_timeout_sec |
--balf-auth-timeout-sec |
int (> 0) |
10 |
balf_gateway.auth_timeout_sec |
--balf-max-connections |
int (> 0) |
64 |
balf_gateway.max_connections |
--balf-max-client-queue |
int (> 0) |
10000 |
balf_gateway.max_client_queue |
--balf-max-messages-per-second |
int (> 0) |
100 |
balf_gateway.max_messages_per_second |
--balf-max-errors-before-disconnect |
int (> 0) |
10 |
balf_gateway.max_errors_before_disconnect |
--balf-error-window-sec |
int (> 0) |
60 |
balf_gateway.error_window_sec |
--balf-duplicate-session-policy |
enum | REJECT_NEW |
balf_gateway.duplicate_session_policy; REJECT_NEW or EVICT_OLD |
Drop-copy gateway options:
| Option | Type | Default | Description |
|---|---|---|---|
--dc-gateway |
Flag | off | Emit top-level dc_gateway block for pm-dc-gwy |
--dc-name |
string | dc-gwy01 |
dc_gateway.name |
--dc-bind-address |
string | 0.0.0.0 |
dc_gateway.bind_address |
--dc-port |
int (> 0) |
5590 |
dc_gateway.port |
--dc-heartbeat-interval-sec |
int (> 0) |
5 |
dc_gateway.heartbeat_interval_sec |
--dc-idle-timeout-sec |
int (> 0) |
30 |
dc_gateway.idle_timeout_sec |
--dc-max-client-queue |
int (> 0) |
10000 |
dc_gateway.max_client_queue |
Log server options:
| Option | Type | Default | Description |
|---|---|---|---|
--log-server |
Flag | off | Emit top-level log_server block for pm-log-srv |
--log-server-enabled / --log-server-disabled |
Flag pair | unset (true when emitted) |
Set log_server.enabled |
--log-server-name |
string | log-srv01 |
log_server.name |
--log-server-bind-address |
string | 0.0.0.0 |
log_server.bind_address |
--log-server-port |
int (> 0) |
5600 |
log_server.port |
--log-server-db-path |
path | data/log.db |
log_server.db_path |
--log-server-retention-days |
int (>= 0) |
30 |
log_server.retention_days; 0 means unbounded retention |
--log-server-max-message-bytes |
int (> 0) |
65536 |
log_server.max_message_bytes |
--log-server-max-client-queue |
int (> 0) |
10000 |
log_server.max_client_queue |
--log-server-write-batch-size |
int (> 0) |
50 |
log_server.write_batch_size |
--log-server-write-batch-interval-ms |
int (> 0) |
100 |
log_server.write_batch_interval_ms |
--log-server-heartbeat-interval-sec |
int (> 0) |
5 |
log_server.heartbeat_interval_sec |
Log server LALF-PS options — the ZeroMQ log-distribution interface, see LALF-PS:
| Option | Type | Default | Description |
|---|---|---|---|
--log-server-pubsub-enabled / --log-server-pubsub-disabled |
Flag pair | unset (true when emitted) |
Set log_server.pubsub_enabled — the master switch for LALF-PS |
--log-server-pub-port |
int (> 0) |
5601 |
log_server.pub_port — ZeroMQ PUB socket carrying rows, ticks, backfill chunks and acks |
--log-server-pull-port |
int (> 0) |
5602 |
log_server.pull_port — ZeroMQ PULL socket receiving subscriber control requests |
--log-server-lease-sec |
int (> 0) |
30 |
log_server.lease_sec — subscription lease TTL |
--log-server-max-lease-sec |
int (>= lease_sec) |
300 |
log_server.max_lease_sec — ceiling on a subscriber's requested lease |
--log-server-max-subscribers |
int (> 0) |
32 |
log_server.max_subscribers |
--log-server-notify-interval-ms |
int (> 0) |
250 |
log_server.notify_interval_ms — NOTIFY-mode coalescing window |
--log-server-backfill-chunk-rows |
int (> 0) |
500 |
log_server.backfill_chunk_rows |
--log-server-max-backfill-minutes |
int (> 0) |
1440 |
log_server.max_backfill_minutes |
--log-server-max-backfill-rows |
int (> 0) |
100000 |
log_server.max_backfill_rows |
--log-server-max-pending-rows |
int (> 0) |
20000 |
log_server.max_pending_rows |
--log-server-pub-sndhwm |
int (> 0) |
10000 |
log_server.pub_sndhwm |
Passing any of these implies the log_server block, so --log-server is
not additionally required. pm-config-gen refuses to write a file that
pm-log-srv would then refuse to start on: port, pub_port and
pull_port must resolve to three different numbers (compared against each
other's defaults, not only against explicitly-set values), and
max-lease-sec must be at least lease-sec.
# Two log servers on one host — move the whole three-port block
pm-config-gen --symbols AAPL --gateways GW01 \
--log-server-port 5700 --log-server-pub-port 5701 --log-server-pull-port 5702
# Collect logs but publish nothing: no ZeroMQ socket is bound
pm-config-gen --symbols AAPL --gateways GW01 --log-server-pubsub-disabled
# Reap dead viewers within 10 s, and cap concurrent viewers at 8
pm-config-gen --symbols AAPL --gateways GW01 \
--log-server-lease-sec 10 --log-server-max-subscribers 8
API gateway options:
| Option | Type | Default | Description |
|---|---|---|---|
--api-gateway |
Flag | off | Emit top-level api_gateways block for pm-api-gwy |
--api-gateway-name NAME |
string | default |
Name of the generated api_gateways.<NAME> entry for single-process generation |
--api-gateway-instance NAME:GATEWAY[,GATEWAY...][:PORT] |
Repeatable | none | Emit one named API gateway process per option; use NAME::PORT for an identity-free read-only process |
--api-gateway-enabled / --api-gateway-disabled |
Flag pair | unset (true when emitted) |
Set each generated API gateway enabled field |
--api-gateway-host ADDR |
string | 127.0.0.1 |
HTTP bind address |
--api-gateway-port N |
int (> 0) |
8080 |
HTTP listen port |
--api-gateway-swagger-enabled / --api-gateway-swagger-disabled |
Flag pair | unset (true when emitted) |
Enable or disable /docs and /openapi.json |
--api-gateway-log-level LEVEL |
enum | info |
debug, info, warning, or error |
--api-gateway-stats-db PATH |
path | data/stats.db |
SQLite database used by /history/* endpoints |
--api-key KEY:GATEWAY_ID[:DESCRIPTION] |
Repeatable | none | Add an explicit bearer-token credential; use GATEWAY_ID=null for read-only access |
--api-gateway-generate-keys / --no-api-gateway-generate-keys |
Flag pair | generated when emitted | Generate one key for each gateways.alf entry |
--api-gateway-readonly-key |
Flag | off | Generate an additional read-only key with gateway_id: null |
--api-gateway-rate-limit-writes-per-second N |
int (> 0) |
10 |
Per-key write rate limit |
--api-gateway-rate-limit-burst N |
int (> 0) |
20 |
Per-key write burst capacity |
--api-gateway-engine-auth-sec SECS |
float (> 0) |
3.0 |
Engine auth timeout field |
--api-gateway-engine-reply-sec SECS |
float (> 0) |
3.0 |
Engine request/reply timeout |
--api-gateway-wait-ack-sec SECS |
float (> 0) |
3.0 |
?wait=ack timeout |
--api-gateway-order-retention-sec SECS |
int (>= 0) |
3600 |
Seconds a terminal order stays in the gateway's in-memory cache. 0 disables eviction |
Combo seed options:
| Option | Type | Default | Description |
|---|---|---|---|
--combo COMBO_SPEC |
Repeatable | none | Seed a market_maker_combos entry; format described in --combo format |
Index options:
| Option | Type | Default | Description |
|---|---|---|---|
--index ID[:DESCRIPTION] |
Repeatable | none | Define an index; ID is alphanumeric, DESCRIPTION is optional text after the first colon |
--index-constituents ID:SYM[,SYM,...] |
Repeatable | none | Set constituent symbols for the named index |
--index-base-value ID:VALUE |
Repeatable | 1000.0 |
Override base_value for the named index |
--index-interval ID:SECS |
Repeatable | 1.0 |
Override publish_interval_sec for the named index |
--index-history-file ID:PATH |
Repeatable | derived from ID | Override history_file path; default is data/indexes/<ID>_history.jsonl |
--index-state-file ID:PATH |
Repeatable | derived from ID | Override state_file path; default is data/indexes/<ID>_state.json |
Up to 5 indices may be defined. Every constituent symbol must appear in --symbols. Every index must have at least one constituent. Each ID must be alphanumeric. When history_file and state_file are omitted, pm-config-gen derives them from the index ID under data/indexes/.
When --api-gateway is enabled, pm-config-gen emits sensible local defaults
and automatically generates one bearer token for each configured ALF gateway.
Pass --seed N to make those generated keys reproducible. Use
--no-api-gateway-generate-keys when you only want manually supplied
--api-key entries.
Use repeated --api-gateway-instance options when you want separate API
gateway processes for logical separation. A non-null gateway_id can belong to
only one generated API gateway entry. An instance with no gateway list uses the
NAME::PORT form and is suitable for a dashboard or other read-only service.
For example, this gives the desk process credentials for every participant and
the dashboard process one read-only credential:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 MM01:MARKET_MAKER OPS01:ADMIN \
--api-gateway-instance desk:TRADER01,TRADER02,MM01,OPS01:8080 \
--api-gateway-instance dashboards::8081 \
--api-gateway-readonly-key \
--seed 20260814 \
--output engine_config.yaml
With named instances, --api-gateway-readonly-key is generated only for
identity-free instances such as dashboards::8081; it is not added to the
identity-bound desk instance. Read-only gateway_id: null credentials may
be repeated across named instances.
Typical CLI example for a local lab with RALF enabled:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--sessions-enabled \
--post-trade-gateway \
--post-trade-bind-address 127.0.0.1 \
--post-trade-port 5580 \
--post-trade-replay-retention-sec 3600 \
--post-trade-heartbeat-interval-sec 1 \
--post-trade-idle-timeout-sec 10 \
--post-trade-max-client-queue 2000 \
--post-trade-allowed-roles CLEARING AUDIT \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--output engine_config.yaml
This generates a standard engine config plus a top-level post_trade_gateway
block for pm-ralf-gwy. Use 127.0.0.1 for a single-host lab; switch to a
controlled network bind such as 0.0.0.0 only when external clients must
connect from other machines.
--gateways format¶
Each gateway token is:
Examples:
TRADER01MM01:MARKET_MAKEROPS01:ADMIN:LEAVE_ALLMM01:MARKET_MAKER:CANCEL_QUOTES_ONLY:Primary market maker
The optional fourth field sets description on the generated gateway entry.
It may contain spaces; the entire string after the third colon is used as-is.
Role defaults for disconnect behavior:
| Role | Default disconnect behavior |
|---|---|
TRADER |
CANCEL_ALL |
MARKET_MAKER |
CANCEL_QUOTES_ONLY |
ADMIN |
LEAVE_ALL |
--symbol-opts format¶
Use --symbol-opts for per-symbol overrides:
Example:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 MM01:MARKET_MAKER \
--symbol-opts AAPL:tick_decimals=2,level=L1,mm_spread_ticks=8 \
--symbol-opts MSFT:dynamic_band=0.03,cb_halt_l1=10,ace_initial_band=0.05 \
--symbol-opts AAPL:enforce_mm_obligation=true
Supported KEY values:
| Key | Value type | Effect |
|---|---|---|
tick_decimals |
int 0..8 |
Override symbol tick precision |
static_band |
float (0,1) |
Symbol collar static band |
dynamic_band |
float (0,1) |
Symbol collar dynamic band |
cb_shift_l1 / cb_shift_l2 / cb_shift_l3 |
float (0,1) |
Override CB level shift pct |
cb_halt_l1 / cb_halt_l2 / cb_halt_l3 |
int >= 0 minutes |
Override CB halt duration (0 means rest-of-day) |
ace_enabled |
true / false |
Enable or disable Automated Corridor Expansion for that symbol |
ace_initial_band |
float (0,1) |
Symbol reopening corridor half-width |
ace_random_end_ns |
int >= 0 ns |
Symbol random-end bound (0 = predictable reopen times) |
level |
string | Symbol risk level key |
mm_spread_ticks |
int > 0 |
Symbol MM spread threshold |
mm_min_qty |
int > 0 |
Symbol MM minimum quantity |
enforce_mm_obligation |
true or false |
Override per-symbol enforce_mm_obligation in mm_obligation_defaults.symbols |
For the two most common collar overrides, you can also use explicit flags:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 \
--symbol-static-band AAPL:0.18 \
--symbol-dynamic-band AAPL:0.03
Per-symbol risk-level assignment can also use an explicit flag:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 \
--risk-level CORE:0.18:0.02 \
--risk-level HIGH_BETA:0.12:0.04 \
--symbol-risk-level AAPL:CORE \
--symbol-risk-level TSLA:HIGH_BETA
--symbol-risk-level is a convenience alias for --symbol-opts SYM:level=....
It writes symbols.<SYM>.level and uses the same runtime validation rules.
Unknown symbols/keys or invalid values in --symbol-opts are reported as
warnings and ignored.
The generated symbols: section also includes an outstanding_shares field
for every symbol. Use that as the slow-changing input for statistics and future
index-style consumers; market capitalization can then be derived from it and
the latest price instead of being stored as a separate static field.
--combo format¶
--combo seeds one market_maker_combos entry per flag:
Where each LEG is:
| Part | Required | Accepted values | Default |
|---|---|---|---|
ID |
Yes | Non-empty string; becomes combo_id |
— |
TYPE |
Yes | AON |
— |
TIF |
Yes | DAY, GTC, ATO, ATC |
— |
SYM |
Yes | Symbol in --symbols; unique within the combo |
— |
SIDE |
Yes | BUY, SELL |
— |
ORDER_TYPE |
Yes | LIMIT, MARKET, STOP, STOP_LIMIT, FOK, ICEBERG, IOC, TRAILING_STOP |
— |
QTY |
Yes | Positive integer | — |
PRICE |
No | Integer tick count or decimal display price (see note below); omit or use null for market orders |
null |
STOP_PRICE |
No | Integer tick count or decimal display price for stop orders; omit or use null otherwise |
null |
SMP_ACTION |
No | NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
NONE |
Constraints: at least 2 and at most 10 legs; each SYM must appear in --symbols; duplicate leg symbols within one combo are rejected.
--combo PRICE/STOP_PRICE accept ticks or a decimal price
Unlike hand-written market_maker_combos YAML — where legs[].price/legs[].stop_price
are always integer ticks — the --combo flag's PRICE/STOP_PRICE parts accept either
a plain integer tick count (e.g. 20950) or a decimal display price containing a .
(e.g. 209.50), which is converted to ticks using the leg symbol's tick_decimals. With
tick_decimals: 2, both 20950 and 209.50 produce the same stored value: $209.50.
Minimal two-leg example:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 \
--combo "SEED-PAIR:AON:DAY:AAPL/BUY/LIMIT/100/20950,MSFT/SELL/LIMIT/50/41550" \
--output engine_config.yaml
Generated market_maker_combos section:
market_maker_combos:
- combo_id: SEED-PAIR
combo_type: AON
tif: DAY
legs:
- symbol: AAPL
side: BUY
order_type: LIMIT
quantity: 100
price: 20950
stop_price: null
smp_action: NONE
- symbol: MSFT
side: SELL
order_type: LIMIT
quantity: 50
price: 41550
stop_price: null
smp_action: NONE
Multiple combos use repeated --combo flags:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 \
--combo "PAIR-AM:AON:DAY:AAPL/BUY/LIMIT/100/20950,MSFT/SELL/LIMIT/50/41550" \
--combo "PAIR-AT:AON:DAY:AAPL/BUY/LIMIT/100/20950,TSLA/SELL/LIMIT/20/24800" \
--output engine_config.yaml
Practical recipes¶
Minimal classroom config:
pm-config-gen \
--symbols AAPL \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--no-sessions-enabled \
--output engine_config.yaml
Session-driven day with risk levels and CB ladder:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--outstanding-shares TSLA:3200000000 \
--sessions-enabled \
--risk-level L1:0.30:0.05 \
--risk-level L2:0.20:0.02 \
--cb-levels L1:0.07:5 L2:0.13:15 L3:0.20 \
--output engine_config.yaml
Market-maker session with seeded startup quotes:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 MM01:MARKET_MAKER OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--sessions-enabled \
--enforce-mm-obligations \
--seed 20260621 \
--seed-mm-mid-range 20:300 \
--seed-last-prices-from-mm \
--output engine_config.yaml
After generation, validate manually:
poetry run python -c 'from pathlib import Path; from edumatcher.engine.config_loader import load_engine_config; print(load_engine_config(Path("engine_config.yaml")))'
If MM gateways are present and you do not use --seed-mm-mid-range, fill all market_maker_quotes prices first, then
run the validation command.
Post-trade gateway config with explicit RALF listener settings:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--post-trade-gateway \
--post-trade-bind-address 127.0.0.1 \
--post-trade-port 5580 \
--post-trade-allowed-roles CLEARING AUDIT \
--output engine_config.yaml
Expected emitted section:
post_trade_gateway:
name: ralf-gwy01
bind_address: 127.0.0.1
port: 5580
replay_retention_sec: 3600
heartbeat_interval_sec: 1
idle_timeout_sec: 10
max_client_queue: 2000
allowed_roles:
- CLEARING
- AUDIT
This is the quickest path when you want one command that prepares both:
- the engine symbol and ALF gateway config used by
pm-engine - the optional RALF listener settings used by
pm-ralf-gwy
REST/WebSocket API gateway config with generated keys:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--api-gateway \
--api-gateway-readonly-key \
--api-gateway-host 0.0.0.0 \
--api-gateway-port 8080 \
--seed 20260624 \
--output engine_config.yaml
Expected emitted section shape:
api_gateways:
default:
enabled: true
host: 0.0.0.0
port: 8080
swagger_enabled: true
log_level: info
stats_db: data/stats.db
credentials:
- api_key: key-trader01-...
gateway_id: TRADER01
description: Generated key for TRADER01
- api_key: key-trader02-...
gateway_id: TRADER02
description: Generated key for TRADER02
- api_key: key-ops01-...
gateway_id: OPS01
description: Generated key for OPS01
- api_key: key-readonly-...
gateway_id: null
description: Generated read-only market-data key
rate_limit:
writes_per_second: 10
burst: 20
timeouts:
engine_auth_sec: 3.0
engine_reply_sec: 3.0
wait_ack_sec: 3.0
Explicit API-key config:
pm-config-gen \
--symbols AAPL \
--gateways TRADER01 \
--api-key trader-secret:TRADER01:"Desk app" \
--api-key dashboard-secret:null:"Read-only dashboard" \
--no-api-gateway-generate-keys \
--output engine_config.yaml
gateway_id values in API credentials must either be null for read-only
market-data access or match an ID from gateways.alf. Generated keys are plain
YAML bearer tokens for local labs and teaching setups; production deployments
should manage secrets with the surrounding platform and terminate TLS in front
of pm-api-gwy.
For multiple generated processes, start a specific named entry with
pm-api-gwy --instance NAME.
BALF gateway config with explicit settings:
pm-config-gen \
--symbols AAPL MSFT \
--gateways TRADER01 TRADER02 \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--balf-gateway \
--balf-bind-address 127.0.0.1 \
--balf-port 5560 \
--balf-duplicate-session-policy EVICT_OLD \
--output engine_config.yaml
Expected emitted section:
balf_gateway:
name: balf-gwy01
bind_address: 127.0.0.1
port: 5560
heartbeat_interval_sec: 1
heartbeat_timeout_sec: 5
idle_timeout_sec: 30
auth_timeout_sec: 10
max_connections: 64
max_client_queue: 10000
max_messages_per_second: 100
max_errors_before_disconnect: 10
error_window_sec: 60
duplicate_session_policy: EVICT_OLD
Index calculation config with pm-index:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--outstanding-shares TSLA:3200000000 \
--sessions-enabled \
--index EDU100:"EduMatcher broad index" \
--index-constituents EDU100:AAPL,MSFT,TSLA \
--output engine_config.yaml
Expected emitted section shape:
indices:
- id: EDU100
description: EduMatcher broad index
base_value: 1000.0
publish_interval_sec: 1.0
history_file: data/indexes/EDU100_history.jsonl
state_file: data/indexes/EDU100_state.json
constituents:
- AAPL
- MSFT
- TSLA
With multiple indices and custom settings:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--outstanding-shares TSLA:3200000000 \
--index TECH2:"Technology pair" \
--index-constituents TECH2:AAPL,MSFT \
--index-base-value TECH2:500.0 \
--index-interval TECH2:2.0 \
--index VOLAT1:"High-beta watch" \
--index-constituents VOLAT1:TSLA \
--output engine_config.yaml
Startup combo seeds with two pairs:
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--outstanding-shares TSLA:3200000000 \
--sessions-enabled \
--combo "SEED-AM:AON:DAY:AAPL/BUY/LIMIT/100/20950,MSFT/SELL/LIMIT/50/41550" \
--combo "SEED-AT:AON:DAY:AAPL/BUY/LIMIT/100/20950,TSLA/SELL/LIMIT/20/24800" \
--output engine_config.yaml
Circuit-breaker ladder with CONTINUOUS resumption on level 2 (no auction on L2 halt recovery):
pm-config-gen \
--symbols AAPL MSFT TSLA \
--gateways TRADER01 OPS01:ADMIN \
--sessions-enabled \
--cb-levels L1:0.07:5:AUCTION L2:0.13:15:CONTINUOUS L3:0.20 \
--output engine_config.yaml
Per-symbol CONTINUOUS resumption override while using global defaults for the other levels:
pm-config-gen \
--symbols AAPL TSLA \
--gateways TRADER01 OPS01:ADMIN \
--cb-levels L1:0.07:5 L2:0.13:15 L3:0.20 \
--symbol-opts TSLA:ace_initial_band=0.05 \
--output engine_config.yaml
Gateway description labels and per-symbol MM obligation override:
pm-config-gen \
--symbols AAPL MSFT \
--gateways \
"TRADER01:TRADER:CANCEL_ALL:Student desk 1" \
"TRADER02:TRADER:CANCEL_ALL:Student desk 2" \
"MM01:MARKET_MAKER:CANCEL_QUOTES_ONLY:Primary market maker" \
"OPS01:ADMIN:LEAVE_ALL:Instructor console" \
--enforce-mm-obligations \
--symbol-opts AAPL:enforce_mm_obligation=true,mm_spread_ticks=8 \
--symbol-opts MSFT:enforce_mm_obligation=false \
--seed-mm-mid-range 20:300 \
--seed 20260706 \
--sessions-enabled \
--output engine_config.yaml
This uses enforce_mm_obligation=false on MSFT to disable the check for that
symbol only, while leaving it enabled globally. Gateway descriptions appear in
the generated YAML as the description field on each gateways.alf entry.
Compile Configs with pm-config-deploy¶
pm-config-deploy is the bridge between the file you author and the compiled
artifact the exchange runs (see File Location). It:
- validates the authored file with all four
pm-cverifierlayers, so a configuration nobody checked can no longer reach a running exchange; - resolves every default exactly once, rather than in the eight loaders that used to hold their own copies and could drift apart;
- installs the result atomically, alongside a copy of the source it was built from.
pm-config-deploy my_config.yaml # validate, compile and install
pm-config-deploy --check my_config.yaml # validate only, install nothing
pm-config-deploy --show # where do the deployed files live?
pm-engine --verbose
pm-scheduler
The artifact is not a reformatted copy of your YAML. A source naming two keys
compiles to nine fully-resolved sections: a market_data_gateway block you
never wrote still arrives with all of its fields, which is what lets each
process deserialise rather than decide.
Deployment replaces the running configuration but does not disturb live processes; restart them to pick it up.
Deploying example configurations¶
By using the option --example it is possible to deploy one of the example configurations
supplied as examples in an easy way.
The available examples are all located in the directory docs/examples/ref_data/<SPEC>/engine_config.yaml and are as follows
| Directory | --example shorthand |
Profile | Number of symbols | Session enabled |
|---|---|---|---|---|
one-book-basic-setup |
one-basic |
basic | 1 | no |
one-book-nominal-setup |
one-nominal |
nominal | 1 | yes |
one-book-complex-setup |
one-complex |
complex | 1 | yes |
three-books-basic-setup |
three-basic |
basic | 3 | no |
three-books-nominal-setup |
three-nominal |
nominal | 3 | yes |
three-books-complex-setup |
three-complex |
complex | 3 | yes |
ten-books-basic-setup |
ten-basic |
basic | 10 | no |
ten-books-nominal-setup |
ten-nominal |
nominal | 10 | yes |
ten-books-complex-setup |
ten-complex |
complex | 10 | yes |
thirty-books-basic-setup |
thirty-basic |
basic | 30 | no |
thirty-books-nominal-setup |
thirty-nominal |
nominal | 30 | yes |
thirty-books-complex-setup |
thirty-complex |
complex | 30 | yes |
The shorthand is always <count>-<profile>, where <count> is one of one,
three, ten, or thirty and <profile> is one of basic, nominal, or
complex. The three profiles differ as follows:
| Profile | Gateways | Sessions and schedule | Auxiliary blocks | Risk controls |
|---|---|---|---|---|
| basic | 4 (2 TRADER, 1 MARKET_MAKER, 1 ADMIN) |
disabled — starts in CONTINUOUS |
none | engine defaults only |
| nominal | 4 (2 TRADER, 1 MARKET_MAKER, 1 ADMIN) |
enabled, with a full schedule |
post_trade_gateway, market_data_gateway, api_gateways (desk + dashboards) |
engine defaults only |
| complex | 8 (5 TRADER, 2 MARKET_MAKER, 1 ADMIN) |
enabled, with a full schedule |
same as nominal, plus market_maker_combos |
named risk_controls levels and a circuit_breaker_defaults ladder |
Pick basic for a quick matching demo with no scheduler, nominal for a
realistic single-desk session with the market-data, post-trade, and REST
gateways available, and complex when you need multiple desks, two market
makers, startup combo seeds, and explicit collar/circuit-breaker policy.
For example
will validate, compile, and install
docs/examples/ref_data/three-books-basic-setup/engine_config.yaml as the
deployed ref_data/engine_config.json artifact, exactly as if you had passed
that path as SOURCE.
What the artifact records about itself¶
"meta": {
"schema_version": 3,
"compiler_version": "0.17.0",
"compiled_at": "2026-07-30T16:43:18.000Z",
"source_path": "/Users/you/course/engine_config.yaml",
"source_sha256": "e3bc2f14cf5c10da…",
"content_sha256": "87be4dc0b9b645cb…"
}
The two digests answer different questions, and both are checked:
source_sha256— has the authored file changed since this was built? Each process warns at startup when it has, so an edit you forgot to deploy is visible rather than silently ignored.content_sha256— has this file changed since it was built? Recomputed on every load. Editing the deployed artifact by hand is refused, namingpm-config-deployas the way to make the change properly.
The payload digest detects modification, not malice: it travels inside the file it protects, so anyone who edits the payload can recompute it. Proving provenance rather than integrity would need a signature.
schema_version guards against a build reading an artifact shaped for another;
an unknown version is refused with a message telling you to recompile.
If you are running from a Poetry checkout, prefix commands with poetry run.
Missing-file Behavior¶
The engine and scheduler handle missing config differently:
| Process | Missing default config | Missing explicit --config |
|---|---|---|
pm-engine |
Starts unrestricted | Starts unrestricted for that path |
pm-scheduler |
Uses built-in schedule | Fatal error |
Unrestricted engine mode means there is no symbol allowlist, no gateway allowlist, no configured risk levels, no seeded last prices, no seeded market-maker quotes, no configured startup combos, and no outstanding share metadata.
Inspect Configs with pm-config-show¶
pm-config-show prints the effective configuration as a terminal dashboard and
exits. Where pm-cverifier answers is this file correct, pm-config-show
answers what does this file actually say — a question that is surprisingly
hard to answer by reading the YAML, because a deployed config is 800–1500 lines
of which perhaps 150 are data.
pm-config-show # the deployed config, essentials only
pm-config-show -m # denser: risk, breakers, gateway tuning
pm-config-show -a # everything, API keys unmasked
pm-config-show -f my_config.yaml # an authored file, before deploying it
pm-config-show --format pdf -o exchange.pdf
With no arguments it reads <DATA_DIR>/ref_data/engine_config.yaml — the same
file File Location describes — so what you see is what the
exchange was configured from. pm-config-show is read-only: it never writes
to the configuration or the data directory, and --output is the only path it
ever creates.
Three questions dominate day-to-day use, and the layout is built around them.
Which ports are in use, and what binds each one? This is the one thing that
cannot be read off the file at all. Three engine sockets and two index sockets
are compiled into config.py and appear nowhere in the YAML; and a gateway
section present without a port: key still binds, on its runtime default. The
ports panel shows all of them together with the process, the function, and where
the value came from — fixed, env, set or default — and flags any port
claimed twice in red, the same condition pm-cverifier reports as M018.
What are the API keys? They exist to be copied, so a key is never wrapped or
truncated at any width, and no styling is applied inside the token, which lets a
terminal double-click select the whole thing. Keys are masked by default;
--all reveals them. Masked and revealed keys are the same length, so revealing
never moves the layout.
What instruments are configured? The symbol list reflows into as many side-by-side sub-tables as the width allows, reading alphabetically down each column like a printed index.
╭─ ENGINE CONFIGURATION ─────────────────────────────────────────────────────────────────────────╮
│ docs/examples/ref_data/ten-books-nominal-setup/engine_config.yaml │
│ 33.7 kB · 2026-08-20 17:32 · via --file │
│ ● on sessions ● on collars ● on breakers ○ off mm-oblig │
│ 10 symbols 4 participants 2 API gateways 5 keys 9 listeners │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ PORTS & LISTENERS ────────────────────────────────────────────────────────────────────────────╮
│ PORT PROTO PROCESS FUNCTION BIND │
│ ──────────────────────────────────────────────────────────────────────────────────────────────── │
│ 5555 ZMQ PULL pm-engine Order intake (CALF) 127.0.0.1 fixed │
│ 5556 ZMQ PUB pm-engine Event + book feed 127.0.0.1 fixed │
│ 5557 ZMQ PUB pm-engine Drop-copy feed 127.0.0.1 fixed │
│ 5558 ZMQ PUB pm-index Index value publish 127.0.0.1 env │
│ 5559 ZMQ PULL pm-index Index command intake 127.0.0.1 env │
│ 5570 TCP pm-md-gwy Market data (MDLF) 127.0.0.1 set │
│ 5580 TCP pm-ralf-gwy Post-trade (RALF) 127.0.0.1 set │
│ 8080 HTTP pm-api-gwy REST API — desk 0.0.0.0 set │
│ 8081 HTTP pm-api-gwy REST API — dashboards 0.0.0.0 set │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ API KEYS ─────────────────────────────────────────────────────────────────────────────────────╮
│ GATEWAY ID API GW ROLE API KEY │
│ ──────────────────────────────────────────────────────────────────────────────────────────────── │
│ TRADER01 desk TRADER key-trader01-••••••••••••••••••••••••••••g6u1 │
│ TRADER02 desk TRADER key-trader02-••••••••••••••••••••••••••••y09z │
│ OPS01 desk ADMIN key-ops01-••••••••••••••••••••••••••••oes2 │
│ MM01 desk MARKET_MAKER key-mm01-••••••••••••••••••••••••••••1o3s │
│ — dashboards READ-ONLY key-readonly-••••••••••••••••••••••••••••nrjd │
│ masked — run with -a/--all to reveal │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Adapting to the terminal¶
The layout is computed from the terminal you actually have; there is no fixed column count. Panels are packed side by side where they fit, short panels are stacked beside tall ones so no gutter is left empty, and individual tables shed optional columns as they narrow. In round terms:
| Terminal | What you get |
|---|---|
| below 72 columns or 18 rows | A plain summary: filename, counts, flags, and the port map. No boxes. |
| 72–99 columns | Single column; wide tables drop their optional columns. |
| 100–169 columns | Two columns, sometimes three where panels are narrow. |
| 170 columns and up | Three columns; symbols reflow to four or five sub-tables. |
At the default density only, the output is also trimmed to fit the window
height: optional panels are dropped first, then the symbol list is shortened.
Both trims say so, and name the flag that brings the content back. Passing -m
or -a disables height trimming entirely — asking for more information is taken
as accepting that you will scroll.
Options¶
| Option | Meaning |
|---|---|
-f, --file YAML |
Config file to read. Default: <DATA_DIR>/ref_data/engine_config.yaml. |
-m, --density [1\|2] |
Pack more in. Bare -m means 1. Adds collars, circuit breakers, gateway tuning and combo seeds at 1; engine tuning, indices, the reopening ladder and per-symbol override markers at 2. |
-a, --all |
Everything: implies -m 2, unmasks API keys, and lists unrecognised top-level keys. |
--format {terminal,pdf} |
Output format. Default terminal. |
-o, --output FILE |
Destination for --format pdf. Defaults to engine-config-<stem>.pdf. |
--no-color |
Suppress ANSI colour. Also implied when stdout is not a TTY, or when NO_COLOR is set. |
--ascii |
ASCII box drawing. Auto-enabled on non-UTF-8 terminals. |
--width N, --height N |
Force render dimensions, for piping or scripted capture. |
Density is a layout control and --all is a disclosure control, which is
why they are separate flags: -m 2 shows every setting but keeps keys masked,
so it stays safe to run on a projector.
Exit codes are 0 on success, 2 when the file is missing or unreadable, and
3 when the YAML does not parse. A parse failure prints the parser's message
and points at pm-cverifier rather than attempting partial recovery.
PDF output¶
--format pdf renders the same content to A4 landscape across several pages:
an overview with the full port table and the session schedule, an access page
with participants and credentials, a risk and market-making page, then as many
symbol pages as the universe needs, and an appendix of tuning and indices. Every
page repeats the four global enforcement flags in its header and carries
page N of M, so a page printed on its own still says whether collars were on.
pm-config-show --format pdf -o handout.pdf # keys masked
pm-config-show --format pdf --all -o operations.pdf # keys in full
A PDF made with --all contains live credentials
Masking is per-run, not per-file. Prefer the masked form for anything you hand out or print for a class.
Current Schema¶
The current parser recognizes these top-level keys:
| Key | Required when file exists? | Used by | Purpose |
|---|---|---|---|
symbols |
Yes | Engine | Accepted symbols and per-symbol settings |
gateways |
Yes | Engine | Gateway configuration container |
gateways.alf |
Yes | Engine | Accepted ALF order-entry gateways |
alf_gateway |
No | pm-alf-gwy |
External ALF text TCP gateway settings |
sessions_enabled |
No | Engine | Enable scheduler-driven session states |
enforce_collars |
No | Engine | Global collar enforcement toggle |
enforce_circuit_breakers |
No | Engine | Global circuit-breaker enforcement toggle |
engine_tuning |
No | Engine | Runtime retention and throttling knobs |
mm_obligation_defaults |
No | Engine | Default market-maker quote obligation policy |
risk_controls |
No | Engine | Named collar profiles |
circuit_breaker_defaults |
No | Engine | Default circuit-breaker ladder |
market_maker_combos |
No | Engine | Startup multi-symbol combo seeds |
schedule |
No | Scheduler, parsed by engine too | Session transition times |
country |
No | pm-scheduler |
Country used for the scheduler's bank-holiday/weekend calendar |
post_trade_gateway |
No | pm-ralf-gwy |
External RALF dissemination gateway settings |
market_data_gateway |
No | pm-md-gwy |
External CALF dissemination gateway settings |
balf_gateway |
No | pm-balf-gwy |
External BALF binary TCP gateway settings |
dc_gateway |
No | pm-dc-gwy |
Drop-copy TCP relay gateway settings |
log_server |
No | pm-log-srv |
Centralized LALF log-collector settings |
api_gateways |
No | pm-api-gwy |
Named REST/WebSocket order-entry and market-data gateway process settings |
indices |
No | pm-index |
Index calculation process configurations |
The nested sections below document every field currently parsed under these
top-level keys. Unknown keys in a mapping are generally ignored by the loader,
but they should not be relied on for runtime behavior — pm-config-show -a
lists any it finds, which is the cheapest way to catch a mistyped section name.
Which Process Reads What¶
engine_config.yaml is one shared ref-data file, but no single process reads
all of it. Each auxiliary gateway or service opens the file independently at
its own startup, ignores every top-level key it doesn't recognize, and parses
only the section(s) it owns. pm-engine is the only process that reads most
of the file — the gateway-specific blocks (market_data_gateway,
balf_gateway, post_trade_gateway, dc_gateway, log_server,
alf_gateway, api_gateways) are never touched by the engine itself.
| Process | Loader module | Top-level section(s) read | What it needs it for |
|---|---|---|---|
pm-engine |
engine/config_loader.py |
symbols, gateways.alf, sessions_enabled, enforce_collars, enforce_circuit_breakers, engine_tuning, mm_obligation_defaults, risk_controls, circuit_breaker_defaults, market_maker_combos, schedule, indices |
Symbol universe, allowed order-entry gateways, session/collar/circuit-breaker policy, runtime tuning, MM obligations, startup combo seeds, session schedule, and index definitions |
pm-alf-gwy |
alf_gwy/config.py |
alf_gateway, gateways.alf |
Own bind address/port/timeouts, plus the gateway ID allowlist and roles for ALF client sessions |
pm-balf-gwy |
balf_gwy/config.py |
balf_gateway, gateways.alf |
Own bind address/port/timeouts, plus the gateway ID allowlist, roles, and disconnect_behaviour for BALF sessions |
pm-ralf-gwy |
ralf_gateway/config.py |
post_trade_gateway |
Own bind address/port/timeouts and allowed_roles for RALF (post-trade) subscribers |
pm-md-gwy |
md_gateway/config.py |
market_data_gateway |
Own bind address/port/timeouts, replay window, and depth_levels for CALF subscribers |
pm-dc-gwy |
dc_gwy/config.py |
dc_gateway |
Own bind address/port/timeouts and per-client queue limit for the drop-copy TCP relay |
pm-log-srv |
log_srv/config.py |
log_server |
Own bind address/port/retention/throughput knobs for the centralized LALF log collector, plus the LALF-PS ZeroMQ log-distribution ports and subscription limits |
pm-api-gwy |
api_gateway/config.py |
api_gateways |
Named REST/WebSocket gateway instances, API credentials, rate limits, and timeouts |
pm-index |
index/config_loader.py (wraps engine/config_loader.py) |
indices, symbols.<SYM>.outstanding_shares, symbols.<SYM>.last_buy_price / last_sell_price |
Index definitions (constituents, base value, publish interval) and the per-constituent share counts / reference prices needed to seed each index at startup |
pm-scheduler |
scheduler/main.py |
schedule, country |
Session-phase transition times — re-reads the same block pm-engine reads, but as an independent process so the schedule can be driven or tested externally — plus the country used to skip weekends and bank holidays |
Tooling reads everything
pm-cverifier and pm-config-show are the exceptions: the linter parses
and cross-validates the whole file, and the viewer displays all of it,
including sections no runtime process consumes on its own. Neither is a
"reader" in the operational sense above — see
Verify Configs with pm-cverifier and
Inspect Configs with pm-config-show.
Two practical consequences follow from this split:
- A typo in, say,
balf_gatewaywill not be caught bypm-engineat all — onlypm-balf-gwy(orpm-cverifier) will reject it. Runpm-cverifierbefore starting a full stack to catch cross-section mistakes early. - Each gateway process can be restarted independently with a changed config
section (for example, bumping
market_data_gateway.depth_levels) without restartingpm-engine, since the engine never reads that section.
Configuring pm-ralf-gwy¶
pm-ralf-gwy reads an optional top-level post_trade_gateway block from the
same engine_config.yaml file used by the engine. This block is not consumed by
pm-engine; it is consumed by the RALF dissemination gateway process itself.
Minimal example:
post_trade_gateway:
name: ralf-gwy01
bind_address: 0.0.0.0
port: 5580
replay_retention_sec: 86400
heartbeat_interval_sec: 1
idle_timeout_sec: 5
max_client_queue: 10000
allowed_roles:
- CLEARING
- DROP_COPY
- AUDIT
Use this block to control where the RALF gateway listens and which external client roles it will accept. In the current implementation:
nameis the gateway id reported inWELCOMEbind_addressandportdefine the TCP listener for external subscribersreplay_retention_seccontrols the in-memory replay windowheartbeat_interval_seccontrolsHBcadenceidle_timeout_seccontrols inactive-session disconnect timingmax_client_queuecaps slow-client buffering beforeSLOW_CLIENTallowed_roleslimits acceptedHELLO|ROLE=...values
If you prefer to generate this block instead of writing it by hand, pm-config-gen
can emit it with --post-trade-gateway and optional --post-trade-* overrides.
Configuring pm-md-gwy¶
pm-md-gwy reads an optional top-level market_data_gateway block from the
same engine_config.yaml file. This block is not consumed by pm-engine; it
is consumed by the CALF market-data gateway process itself.
Minimal example:
market_data_gateway:
enabled: true
name: md-gwy01
bind_address: 0.0.0.0
port: 5570
heartbeat_interval_sec: 1
idle_timeout_sec: 5
replay_window_sec: 30
max_symbols_per_client: 200
max_client_queue: 10000
depth_levels: 10
Use this block to control whether the CALF gateway starts and how it serves subscribers. In the current implementation:
enabledcontrols whetherpm-md-gwystarts serving clientsnameis the gateway id reported in welcome/session payloadsbind_addressandportdefine the TCP listener for CALF subscribersheartbeat_interval_seccontrols heartbeat cadenceidle_timeout_seccontrols inactive-session disconnect timingreplay_window_seccontrols the in-memory replay history windowmax_symbols_per_clientcaps per-client subscription fanoutmax_client_queuecaps slow-client bufferingdepth_levelssets how many aggregated price levels per side are sent on the CALFDEPTHchannel (see 920-app-calf-protocol.md)
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --market-data-gateway and optional
--market-data-* overrides.
Configuring pm-balf-gwy¶
pm-balf-gwy reads an optional top-level balf_gateway block from the same
engine_config.yaml file used by the engine. This block is not consumed by
pm-engine; it is consumed by the BALF binary TCP gateway process.
Gateway identities and disconnect behaviour are read from the existing
gateways.alf list — no separate credentials block is needed.
Minimal example:
balf_gateway:
name: balf-gwy01
bind_address: 0.0.0.0
port: 5560
heartbeat_interval_sec: 1
heartbeat_timeout_sec: 5
idle_timeout_sec: 30
auth_timeout_sec: 10
max_connections: 64
max_client_queue: 10000
max_messages_per_second: 100
max_errors_before_disconnect: 10
error_window_sec: 60
duplicate_session_policy: REJECT_NEW
| Field | Default | Description |
|---|---|---|
name |
balf-gwy01 |
Gateway name echoed in the LOGON_ACK message field |
bind_address |
0.0.0.0 |
TCP listen interface (127.0.0.1 for loopback-only) |
port |
5560 |
TCP listen port |
heartbeat_interval_sec |
1 |
Seconds between server-initiated HEARTBEAT frames when no other outbound traffic |
heartbeat_timeout_sec |
5 |
Disconnect session if no inbound traffic arrives within this window |
idle_timeout_sec |
30 |
Additional idle-session cleanup guard |
auth_timeout_sec |
10 |
Hard-close unauthenticated connections if LOGON is not completed within this window |
max_connections |
64 |
Maximum simultaneous TCP connections |
max_client_queue |
10000 |
Per-client outbound frame buffer depth before SLOW_CLIENT disconnect |
max_messages_per_second |
100 |
Token-bucket inbound rate limit per client |
max_errors_before_disconnect |
10 |
Error threshold in the sliding error_window_sec before forced disconnect |
error_window_sec |
60 |
Sliding window length (seconds) for the error counter |
duplicate_session_policy |
REJECT_NEW |
What to do when a second LOGON arrives for an already-connected gateway ID: REJECT_NEW or EVICT_OLD |
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --balf-gateway and optional --balf-*
overrides. See BALF TCP Gateway for the full
client-facing documentation.
Configuring pm-api-gwy¶
pm-api-gwy reads an optional top-level api_gateways block from the same
engine_config.yaml file. This block is not consumed by pm-engine; it is
consumed by the REST/WebSocket API gateway process.
Minimal generated example:
api_gateways:
desk:
enabled: true
host: 0.0.0.0
port: 8080
swagger_enabled: true
log_level: info
stats_db: data/stats.db
credentials:
- api_key: key-trader01-example
gateway_id: TRADER01
description: Generated key for TRADER01
- api_key: key-dashboard-example
gateway_id: null
description: Read-only dashboard client
rate_limit:
writes_per_second: 10
burst: 20
timeouts:
engine_auth_sec: 3.0
engine_reply_sec: 3.0
wait_ack_sec: 3.0
Use this block to control where the REST API listens, whether Swagger is available, which bearer tokens are accepted, and how write rate limits and engine reply waits are applied. In the current implementation:
enabledletspm-api-gwyrefuse startup when set tofalsehostandportdefine the uvicorn HTTP listenerswagger_enabledcontrols/docsand/openapi.jsonstats_dbpoints history endpoints at thepm-statsSQLite databasecredentials[].api_keyis the bearer token used by REST and WebSocket clientscredentials[].gateway_idmaps a key to an ALF gateway;nullis read-only- a non-null
credentials[].gateway_idmay appear in only oneapi_gatewaysentry rate_limitapplies per API key to write endpoints onlytimeouts.engine_reply_secandtimeouts.wait_ack_seccontrol request/reply and?wait=ackwaits
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --api-gateway. By default it generates one
credential per configured ALF gateway. Add --api-gateway-readonly-key for a
dashboard-style key with gateway_id: null, or pass explicit --api-key
entries when you need known token values.
When more than one named API gateway is configured, start each process with its
entry name, for example pm-api-gwy --instance desk.
Configuring pm-index¶
pm-index reads an optional top-level indices block from the same
engine_config.yaml file. This block is not consumed by pm-engine; it is
consumed by the index calculation process.
Example with two indices:
indices:
- id: EDU100
description: EduMatcher broad index
base_value: 1000.0
publish_interval_sec: 1.0
history_file: data/indexes/EDU100_history.jsonl
state_file: data/indexes/EDU100_state.json
constituents:
- AAPL
- MSFT
- TSLA
- id: TECH2
description: Technology pair
base_value: 500.0
publish_interval_sec: 2.0
history_file: data/indexes/TECH2_history.jsonl
state_file: data/indexes/TECH2_state.json
constituents:
- AAPL
- MSFT
Use this block to define one or more weighted-average price indices that
pm-index tracks as trades print through the engine. In the current
implementation:
idis the unique index identifier used in events, history records, and derived file namesdescriptionis a human-readable label emitted inINDEX_OPENandINDEX_UPDATEDeventsbase_valueis the divisor-normalized starting level;1000.0is the standard conventionpublish_interval_secthrottles how frequently index updates are published to subscribershistory_fileis a line-delimited JSON file where corporate-action and constituent events are persistedstate_fileis a JSON file where the current divisor and constituent shares are checkpointedconstituentsis an ordered list of symbols; each must haveoutstanding_sharesset insymbols:
Constraints enforced at startup:
- Maximum 5 indices per config file
- Every constituent symbol must appear in
symbols:with a positiveoutstanding_shares idvalues must be unique across theindiceslist
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --index and the associated --index-*
options. File paths are automatically derived from the index ID when not
specified.
Configuring pm-log-srv¶
pm-log-srv reads an optional top-level log_server block from the same
engine_config.yaml file. This block is not consumed by pm-engine; it is
consumed by the centralized LALF log-collector process. See
Centralized Log Server for the operational guide (starting
the server, using pm-log-cli) and LALF Protocol Reference
for the normative wire specification.
Minimal example:
log_server:
enabled: true
name: log-srv01
bind_address: 0.0.0.0
port: 5600
db_path: data/log.db
retention_days: 30
max_message_bytes: 65536
max_client_queue: 10000
write_batch_size: 50
write_batch_interval_ms: 100
heartbeat_interval_sec: 5
| Field | Default | Description |
|---|---|---|
enabled |
true |
Master switch — lets pm-log-srv refuse to accept connections when set to false |
name |
log-srv01 |
Server name echoed in the LALF WELCOME.SRV field |
bind_address |
0.0.0.0 |
TCP listen interface (127.0.0.1 for loopback-only) |
port |
5600 |
TCP listen port for LALF clients |
db_path |
data/log.db |
SQLite database path where log_events/processes/server_stats are stored |
retention_days |
30 |
Prune log_events rows older than this many days, once per hour; null (or --retention-days 0) means unbounded retention |
max_message_bytes |
65536 |
Maximum LOG payload size before truncation — oversized messages are truncated and stored, never dropped |
max_client_queue |
10000 |
Per-connection outbound backlog limit before backpressure is applied |
write_batch_size |
50 |
Maximum rows per SQLite transaction in the background writer thread |
write_batch_interval_ms |
100 |
Maximum time between writer-thread flushes, whichever comes first with write_batch_size |
heartbeat_interval_sec |
5 |
How often a connected client must send something (LOG or HB) to stay alive; the server disconnects after 2× this interval of silence and advertises the value to clients in WELCOME.HBINT (the server itself never sends HB). Doubles as the publish interval for the LALF-PS log.server_state tick |
LALF-PS fields — the ZeroMQ log-distribution interface¶
Everything above configures how logging gets in to pm-log-srv. The
fields below configure how it gets back out: pm-log-srv also binds a
ZeroMQ PUB/PULL pair so live log viewers can be pushed rows as they are
committed, rather than polling log.db on a timer. See
LALF-PS for
the full interface.
log_server:
# ... the collector fields above ...
pubsub_enabled: true
pub_port: 5601
pull_port: 5602
lease_sec: 30
max_lease_sec: 300
max_subscribers: 32
notify_interval_ms: 250
backfill_chunk_rows: 500
max_backfill_minutes: 1440
max_backfill_rows: 100000
max_pending_rows: 20000
pub_sndhwm: 10000
| Field | Default | Description |
|---|---|---|
pubsub_enabled |
true |
Master switch for LALF-PS. When false, no ZeroMQ socket is bound at all and pm-log-srv runs as a pure TCP collector |
pub_port |
5601 |
ZeroMQ PUB port carrying live rows, notification ticks, backfill chunks, control acks and errors |
pull_port |
5602 |
ZeroMQ PULL port receiving subscriber control requests |
lease_sec |
30 |
Subscription lease TTL. A PUB socket cannot see that a peer died, so every subscription carries a TTL the subscriber must refresh with log.renew; one that goes silent is reaped and its buffers discarded |
max_lease_sec |
300 |
Ceiling on a subscriber's requested lease. A request above it is clamped, not rejected. Must be >= lease_sec |
max_subscribers |
32 |
Maximum concurrent leased subscriptions; further log.subscribe requests are answered with TOO_MANY_SUBS |
notify_interval_ms |
250 |
Coalescing window for NOTIFY-mode ticks, and the floor on a subscriber's own requested interval |
backfill_chunk_rows |
500 |
Rows per backfill chunk, and the maximum rows per live stream message |
max_backfill_minutes |
1440 |
Largest "last n minutes" window a subscriber may request (24 h) |
max_backfill_rows |
100000 |
Hard cap on rows returned by one backfill; the final chunk sets truncated when it bites |
max_pending_rows |
20000 |
Per-subscription stream buffer cap. A subscriber that is alive but too slow loses its oldest buffered rows rather than growing the server without bound |
pub_sndhwm |
10000 |
ZeroMQ send high-water mark on the PUB socket |
pm-log-srv therefore occupies a contiguous three-port block —
5600/5601/5602 by default — and all three must be different. It
refuses to start otherwise, and pm-cverifier reports the condition as
S102 before you ever get there. pm-cverifier also cross-checks the two
LALF-PS ports against every other configured listener (M018) and rejects
a max_lease_sec below lease_sec (S103).
Every CLI flag on pm-log-srv (--host, --port, --db,
--retention-days, --max-message-bytes, --pub-port, --pull-port,
--lease-sec, --no-pubsub) overrides the corresponding config field for
that invocation only — the same CLI-flag-over-config precedence every other
pm-* process uses.
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --log-server and the associated
--log-server-* options — see "Log server options" and "Log server LALF-PS
options" in
Generate Configs with pm-config-gen
above.
Minimal Example¶
Use this when you want the smallest fully working configured exchange. It starts
in continuous matching mode, accepts only AAPL, and allows two trader gateways.
This mirrors the live sample engine_config.yaml.
sessions_enabled: false
enforce_collars: true
enforce_circuit_breakers: true
engine_tuning:
snapshot_interval_sec: 0.5
symbols:
AAPL:
tick_decimals: 2
last_buy_price: 209.50
last_sell_price: 210.50
gateways:
alf:
- id: TRADER01
description: Student workstation 1
role: TRADER
disconnect_behaviour: CANCEL_ALL
- id: TRADER02
description: Student workstation 2
role: TRADER
disconnect_behaviour: CANCEL_ALL
This config does not define a MARKET_MAKER gateway, so no
market_maker_quotes are required.
Medium Example¶
Use this for a classroom session with scheduled phases, multiple symbols, an operator gateway, reusable collar levels, and a normal continuous trading day.
sessions_enabled: true
enforce_collars: true
enforce_circuit_breakers: true
engine_tuning:
snapshot_interval_sec: 0.5
risk_controls:
default_level: L2
levels:
L1:
collar:
static_band_pct: 0.30
dynamic_band_pct: 0.05
L2:
collar:
static_band_pct: 0.20
dynamic_band_pct: 0.02
symbols:
AAPL:
tick_decimals: 2
last_buy_price: 209.50
last_sell_price: 210.50
MSFT:
tick_decimals: 2
level: L1
last_buy_price: 415.00
last_sell_price: 415.50
TSLA:
tick_decimals: 2
collar:
dynamic_band_pct: 0.04
gateways:
alf:
- id: TRADER01
description: Student workstation 1
role: TRADER
disconnect_behaviour: CANCEL_ALL
- id: TRADER02
description: Student workstation 2
role: TRADER
disconnect_behaviour: CANCEL_ALL
- id: OPS01
description: Instructor console
role: ADMIN
disconnect_behaviour: LEAVE_ALL
schedule:
pre_open: "09:00"
opening_auction_start: "09:25"
continuous_start: "09:30"
closing_auction_start: "16:00"
closing_auction_end: "16:05"
This still avoids market-maker seed quotes. Students can supply liquidity manually, and the operator can manage session phases and exchange-wide circuit-breaker controls.
Fully Complex Example¶
Use this as a reference for every major parser-supported feature: market-maker roles, quote seeds, obligation policy, collar profiles, circuit-breaker defaults, symbol overrides, startup combo seeds, and scheduler times.
sessions_enabled: true
enforce_collars: true
enforce_circuit_breakers: true
engine_tuning:
snapshot_interval_sec: 0.5
quote_history_maxlen: 30
drop_copy_buffer_size: 10000
recent_trades_maxlen: 20
depth_snapshot_tolerance_ticks: 100
mm_obligation_defaults:
enforce_mm_obligation: true
mm_max_spread_ticks: 20
mm_min_qty: 100
symbols:
AAPL:
enforce_mm_obligation: true
mm_max_spread_ticks: 8
mm_min_qty: 200
TSLA:
enforce_mm_obligation: true
mm_max_spread_ticks: 40
mm_min_qty: 50
risk_controls:
default_level: L2
levels:
L1:
collar:
static_band_pct: 0.30
dynamic_band_pct: 0.05
L2:
collar:
static_band_pct: 0.20
dynamic_band_pct: 0.02
L3:
collar:
static_band_pct: 0.12
dynamic_band_pct: 0.01
circuit_breaker_defaults:
reference_window_ns: 300000000000
levels:
L1:
price_shift_pct: 0.07
halt_duration_ns: 300000000000
L2:
price_shift_pct: 0.13
halt_duration_ns: 900000000000
L3:
price_shift_pct: 0.20
halt_duration_ns:
gateways:
alf:
- id: TRADER01
description: Student workstation 1
role: TRADER
disconnect_behaviour: CANCEL_ALL
- id: TRADER02
description: Student workstation 2
role: TRADER
disconnect_behaviour: CANCEL_ALL
- id: MM01
description: Primary market maker
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_ANY_FILL
enforce_mm_obligation: true
mm_max_spread_ticks: 20
mm_min_qty: 100
mm_obligations:
AAPL:
enforce_mm_obligation: true
max_spread_ticks: 6
min_qty: 300
TSLA:
enforce_mm_obligation: true
max_spread_ticks: 50
min_qty: 50
- id: MM02
description: Backup market maker
role: MARKET_MAKER
disconnect_behaviour: CANCEL_QUOTES_ONLY
quote_refresh_policy: INACTIVATE_ON_FULL_FILL
enforce_mm_obligation: true
mm_max_spread_ticks: 30
mm_min_qty: 50
- id: OPS01
description: Instructor console
role: ADMIN
disconnect_behaviour: LEAVE_ALL
symbols:
AAPL:
tick_decimals: 2
last_buy_price: 209.50
last_sell_price: 210.50
collar:
dynamic_band_pct: 0.015
circuit_breaker:
levels:
L1:
halt_duration_ns: 180000000000
market_maker_quotes:
- gateway_id: MM01
quote_id: SEED-MM01-AAPL
bid_price: 209.00
ask_price: 211.00
bid_qty: 2000
ask_qty: 2000
tif: DAY
seed_once: true
- gateway_id: MM02
quote_id: SEED-MM02-AAPL
bid_price: 208.50
ask_price: 211.50
bid_qty: 1000
ask_qty: 1000
tif: DAY
seed_once: true
MSFT:
tick_decimals: 2
level: L1
last_buy_price: 415.00
last_sell_price: 415.50
market_maker_quotes:
- gateway_id: MM01
quote_id: SEED-MM01-MSFT
bid_price: 414.00
ask_price: 416.00
bid_qty: 1000
ask_qty: 1000
tif: DAY
seed_once: true
TSLA:
tick_decimals: 2
level: L3
last_buy_price: 248.00
last_sell_price: 249.00
collar:
dynamic_band_pct: 0.04
circuit_breaker:
levels:
L1:
halt_duration_ns: 600000000000
L2:
halt_duration_ns: 1800000000000
market_maker_quotes:
- gateway_id: MM01
quote_id: SEED-MM01-TSLA
bid_price: 247.00
ask_price: 250.00
bid_qty: 500
ask_qty: 500
tif: DAY
seed_once: false
market_maker_combos:
- combo_id: SEED-PAIR-AAPL-MSFT
combo_type: AON
tif: DAY
legs:
- symbol: AAPL
side: BUY
order_type: LIMIT
quantity: 100
price: 20950
smp_action: NONE
- symbol: MSFT
side: SELL
order_type: LIMIT
quantity: 50
price: 41550
smp_action: NONE
schedule:
pre_open: "09:00"
opening_auction_start: "09:25"
continuous_start: "09:30"
closing_auction_start: "16:00"
closing_auction_end: "16:05"
Combo seed prices are ticks
market_maker_quotes use display prices such as 209.00. Startup combo
legs are parsed through the combo model and expect integer tick prices. With
tick_decimals: 2, price: 20950 represents 209.50.
Configuration Checklist¶
Use this checklist when creating a new engine configuration.
-
Decide session mode. Use
sessions_enabled: falsefor simple demos and tests. Usesessions_enabled: truewhenpm-schedulershould drive phases. -
Define the symbol universe. Add every tradable symbol under
symbols, settick_decimals, and addlast_buy_price/last_sell_priceif viewers should start with references. -
Define ALF gateways. Add every expected
pm-alf-console --id ...undergateways.alf. ChooseTRADER,MARKET_MAKER, orADMIN, then choose disconnect behavior. -
Decide whether market makers exist. If no gateway has
role: MARKET_MAKER,market_maker_quotesare optional. If any gateway hasrole: MARKET_MAKER, every symbol needs at least one quote seed. Quote seedgateway_idvalues must reference configuredMARKET_MAKERgateways. -
Add risk controls only as needed. Use
risk_controls.levelsfor reusable collar profiles,circuit_breaker_defaultsfor the global breaker ladder, and symbol-level overrides only for exceptions. -
Add market-maker obligation policy if quote quality matters. Start with
mm_obligation_defaults, override by symbol undermm_obligation_defaults.symbols, and usegateways.alf[*].mm_obligationsonly for gateway-specific exceptions. -
Add startup combos only after symbols are stable. Keep combo leg symbols unique within one combo, use 2 to 10 legs, and remember combo leg prices are integer ticks.
-
Add index calculations if needed. Define each
pm-indexprocess in theindicesblock. Every constituent must appear insymbols:with a positiveoutstanding_shares. Runpm-indexfor each configured index;pm-config-gen --indexgenerates the block automatically. -
Add a schedule if sessions are enabled. Provide all five schedule keys for readability and confirm times are local server
HH:MMstrings. -
Check persistence before first run. Remove stale state when changing seed behavior or symbol universe, especially
book_stats.json,gtc_orders.json, andgtc_combos.jsonin the data directory (src/data/in a source checkout,~/.local/share/edumatcherwhen installed, or$EDUMATCHER_DATA_DIRif set). -
Validate before class or demo. Start
pm-engine --verbose, connect each gateway ID you expect to use, and runSYMBOLSfrom a gateway.
Engine Behavior Flags¶
sessions_enabled¶
When true, the engine starts in CLOSED and accepts scheduler transitions.
When false, the engine starts in CONTINUOUS and ignores scheduler
transitions.
| Scenario | Effective value |
|---|---|
| Config file present, field absent | true |
| No config file (unrestricted mode) | false |
enforce_collars¶
Controls whether configured price collars reject incoming orders. This defaults
to true and should normally remain enabled outside tests.
enforce_circuit_breakers¶
Controls whether configured circuit breakers can halt symbols. This defaults to
true and should normally remain enabled outside tests.
engine_tuning¶
engine_tuning:
snapshot_interval_sec: 0.5
quote_history_maxlen: 30
drop_copy_buffer_size: 10000
recent_trades_maxlen: 20
depth_snapshot_tolerance_ticks: 100
engine_tuning groups low-level runtime retention and throttling knobs that
affect memory usage, snapshot cost, and replay depth. All of them are optional;
omitted fields fall back to built-in defaults.
engine_tuning.snapshot_interval_sec¶
Controls the per-symbol throttle window for book.<SYMBOL> publications from
dirty books.
Rules:
- must be numeric
- must be greater than zero
- defaults to
0.5seconds when omitted
engine_tuning.quote_history_maxlen¶
Controls how many recently inactivated quotes per gateway are retained in
memory for QLEGS SHOW=RECENT / SHOW=ALL.
Rules:
- must be an integer
- must be greater than zero
- defaults to
30when omitted
engine_tuning.drop_copy_buffer_size¶
Controls how many drop-copy events are retained in memory for replay after a subscriber reconnects.
Rules:
- must be an integer
- must be greater than zero
- defaults to
10000when omitted
engine_tuning.recent_trades_maxlen¶
Controls how many recent trade rows each order book keeps for snapshots and diagnostics.
Rules:
- must be an integer
- must be greater than zero
- defaults to
20when omitted
engine_tuning.depth_snapshot_tolerance_ticks¶
Controls the depth window around the last trade, measured in ticks, when the engine publishes aggregated depth snapshots.
Rules:
- must be an integer
- must be greater than zero
- defaults to
100when omitted
ALF Gateway Allowlist¶
Only gateway IDs listed under gateways.alf may connect and submit orders when
a config file exists.
gateways:
alf:
- id: TRADER01
description: Student workstation 1
role: TRADER
disconnect_behaviour: CANCEL_ALL
Gateway Fields¶
| Field | Required | Accepted values / type | Default |
|---|---|---|---|
id |
Yes | Non-empty string, uppercased by parser | None |
description |
No | String or null | Empty string |
role |
No | TRADER, MARKET_MAKER, ADMIN |
TRADER |
disconnect_behaviour |
No | CANCEL_QUOTES_ONLY, CANCEL_ALL, LEAVE_ALL |
CANCEL_QUOTES_ONLY |
quote_refresh_policy |
No | INACTIVATE_ON_ANY_FILL, INACTIVATE_ON_FULL_FILL, NEVER_INACTIVATE |
INACTIVATE_ON_ANY_FILL |
enforce_mm_obligation |
No | Boolean | Global MM default |
mm_max_spread_ticks |
No | Positive integer | Global MM default, then 10 |
mm_min_qty |
No | Positive integer | Global MM default, then 100 |
mm_obligations |
No | Per-symbol mapping | Empty mapping |
smp_action |
No | NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
NONE |
smp_action is a fallback default, not an override
gateways.alf[].smp_action is the self-match-prevention action the
engine applies to this gateway's orders when the order itself doesn't
specify one:
QUOTElegs (the bid/ask orders aQUOTEcommand generates viapm-alf-gwyor the ALF console) have no per-request SMP concept of their own, so they always use this gateway default.NEWorders and combo orders/legs submitted throughpm-alf-gwy, the ALF console, or the REST API gateway each carry their own optional per-orderSMP=field (see theNEWcommand in ALF Protocol). If the client sends an explicitSMP=— includingSMP=NONE, a deliberate request to allow self-trades — that value is always honoured as-is. Only when the client omitsSMP=entirely does the engine fall back to this gateway'ssmp_action, and finally toNONEif the gateway has none configured.
In short: an explicit per-order SMP= always wins; gateways.alf[].smp_action
only fills the gap when the client didn't say anything.
Nested mm_obligations.<SYMBOL> entries support these fields:
| Field | Required | Accepted values / type | Default |
|---|---|---|---|
enforce_mm_obligation |
No | Boolean | Gateway enforce_mm_obligation |
max_spread_ticks |
No | Integer; use positive values for a valid spread limit | Gateway mm_max_spread_ticks |
min_qty |
No | Integer; use positive values for a valid quantity floor | Gateway mm_min_qty |
Inside mm_obligations, use this shape:
gateways:
alf:
- id: MM01
role: MARKET_MAKER
mm_obligations:
AAPL:
enforce_mm_obligation: true
max_spread_ticks: 6
min_qty: 300
Use max_spread_ticks and min_qty inside mm_obligations; do not use the
flat-field names mm_max_spread_ticks and mm_min_qty there.
Role Privileges¶
| Role | Regular orders | Quotes | Admin circuit-breaker halt/resume | Typical use |
|---|---|---|---|---|
TRADER |
Yes | No | No | Students, manual participants, AI traders |
MARKET_MAKER |
Yes | Yes | No | Quote providers |
ADMIN |
Yes | No | Yes | Instructor/operator console |
MARKET_MAKER gateways are the only gateways allowed to submit quotes. ADMIN
gateways can send exchange-wide circuit-breaker halt/resume commands.
Market-Maker Obligation Defaults¶
mm_obligation_defaults defines quote-quality policy inherited by market-maker
gateways.
mm_obligation_defaults:
enforce_mm_obligation: true
mm_max_spread_ticks: 20
mm_min_qty: 100
symbols:
AAPL:
enforce_mm_obligation: true
mm_max_spread_ticks: 8
mm_min_qty: 200
| Field | Required | Description |
|---|---|---|
enforce_mm_obligation |
No | Enable quote obligation checks |
mm_max_spread_ticks |
No | Maximum allowed bid/ask spread in ticks |
mm_min_qty |
No | Minimum bid and ask quantity |
symbols |
No | Per-symbol overrides using the same three fields |
Defaults and validation:
| Field | Accepted values / type | Default |
|---|---|---|
enforce_mm_obligation |
Boolean | false |
mm_max_spread_ticks |
Positive integer | 10 |
mm_min_qty |
Positive integer | 100 |
symbols.<SYMBOL>.enforce_mm_obligation |
Boolean | Top-level enforce_mm_obligation |
symbols.<SYMBOL>.mm_max_spread_ticks |
Positive integer | Top-level mm_max_spread_ticks |
symbols.<SYMBOL>.mm_min_qty |
Positive integer | Top-level mm_min_qty |
The effective policy is resolved from most specific to least specific:
gateways.alf[*].mm_obligations.<SYMBOL>mm_obligation_defaults.symbols.<SYMBOL>- Gateway flat fields
mm_obligation_defaultsflat fields- Built-in defaults
Symbol Universe¶
Only symbols declared under symbols are accepted by the configured engine.
Adding a symbol is an IPO
Treat each symbol as an initial listing (IPO): you set its opening reference price, issued shares, and (when a market maker is configured) its opening quote up front, and those values seed the book and both risk-control references. The symbol universe is fixed at startup — the engine does not support adding symbols intra-day. See Adding or Removing Symbols and Risk Controls - Day one (IPO) behaviour.
Symbol keys are uppercased. The value for a symbol may be a mapping, {}, or
null.
Symbol Fields¶
| Field | Required | Type / accepted values | Description |
|---|---|---|---|
tick_decimals |
No | Integer 0..8 |
Decimal places used to convert display prices to ticks |
level |
No | Key from risk_controls.levels |
Named collar profile |
outstanding_shares |
Conditional | Positive integer | Required only if the symbol is an index constituent |
last_buy_price |
No | Number | Initial last-buy reference when no persisted stat exists |
last_sell_price |
No | Number | Initial last-sell reference when no persisted stat exists |
collar |
No | Mapping | Symbol-level collar override |
circuit_breaker |
No | Mapping | Symbol-level circuit-breaker override |
market_maker_quotes |
Conditional | List of mappings | Required (non-empty) only if a MARKET_MAKER gateway exists |
Mandatory Fields¶
None of tick_decimals, level, last_buy_price, last_sell_price, collar,
or circuit_breaker are ever mandatory — each has a built-in default or is
simply left inactive when omitted. Two fields become mandatory, but only under
specific conditions:
market_maker_quotes— becomes mandatory (must be a non-empty list) for every symbol as soon as anygateways.alfentry hasrole: MARKET_MAKER. If noMARKET_MAKERgateway is configured, this field can be omitted for every symbol.outstanding_shares— becomes mandatory (must be a positive integer) only for symbols listed in anindices[].constituentsentry (see Configuringpm-index). A symbol not referenced by any index does not needoutstanding_shares.
Collar Reference Price Selection¶
When a symbol ends up with an active collar (via symbols.<SYMBOL>.collar,
its level, or risk_controls.default_level — see
Risk Controls and Collars), the engine derives
the collar's static-band reference_price at startup as follows:
- Persisted
<DATA_DIR>/book_stats.jsonvalues are restored first (see Startup and Persistence Order). If the symbol has a persistedlast_buy_price, it is used. - Otherwise, if the symbol has a persisted
last_sell_price, it is used. - Otherwise, fall back to the config file's
last_buy_price. - Otherwise, fall back to the config file's
last_sell_price. - If none of the above are set, the collar is still parsed but is not
activated for that symbol — no collar check runs for it at all, even
though
enforce_collars: trueand acollar/levelsection are present.
In short: persisted book_stats.json prices always take precedence over the
last_buy_price / last_sell_price seed values in engine_config.yaml, so
the collar tracks the most recently known trading price instead of a
config value that may go stale as the session progresses. This uses the same
resolved last-buy/last-sell prices that seed the order book itself, so the
collar reference and the book's displayed last prices never disagree.
Orders for unknown symbols are rejected with:
Risk Controls and Collars¶
risk_controls defines reusable collar levels.
risk_controls:
default_level: L2
levels:
L1:
collar:
static_band_pct: 0.30
dynamic_band_pct: 0.05
L2:
collar:
static_band_pct: 0.20
dynamic_band_pct: 0.02
Per-symbol risk-level assignment¶
Use per-symbol risk levels when different symbols should inherit different
named collar profiles from risk_controls.levels.
You can assign the symbol level directly in YAML:
risk_controls:
default_level: DEFAULT
levels:
DEFAULT:
collar:
static_band_pct: 0.20
dynamic_band_pct: 0.02
CORE:
collar:
static_band_pct: 0.18
dynamic_band_pct: 0.02
HIGH_BETA:
collar:
static_band_pct: 0.12
dynamic_band_pct: 0.04
symbols:
AAPL:
level: CORE
TSLA:
level: HIGH_BETA
Or generate the same structure from CLI:
pm-config-gen \
--symbols AAPL TSLA \
--gateways TRADER01 \
--risk-level CORE:0.18:0.02 \
--risk-level HIGH_BETA:0.12:0.04 \
--symbol-risk-level AAPL:CORE \
--symbol-risk-level TSLA:HIGH_BETA
Semantics:
symbols.<SYMBOL>.levelselects one named profile fromrisk_controls.levels.- If
levelis omitted, the symbol usesrisk_controls.default_levelwhen present. - If neither a symbol level nor
default_levelapplies, the symbol has no collar unlesssymbols.<SYMBOL>.collaris defined directly. symbols.<SYMBOL>.collarremains the highest-priority per-field override over any selected level.
Risk-control Fields¶
| Field | Required | Accepted values / type | Default |
|---|---|---|---|
default_level |
No | Non-empty string matching a key in levels |
None |
levels |
No | Mapping of named level configs | Empty mapping |
levels.<LEVEL> |
No | Mapping; level name is uppercased | None |
levels.<LEVEL>.collar |
No | Mapping | Empty mapping |
Collar Fields¶
Collars may appear under risk_controls.levels.<LEVEL>.collar or under
symbols.<SYMBOL>.collar.
| Field | Required | Accepted values / type | Default when a collar is active |
|---|---|---|---|
static_band_pct |
No | Number in (0, 1) |
0.20 |
dynamic_band_pct |
No | Number in (0, 1) |
0.02 |
Meaning of collar values:
static_band_pctis an absolute guard around the symbol reference price (for example prior close or seeded last price). A value of0.20means allow prices within ±20% of that reference.dynamic_band_pctis an incremental guard around the latest trade price. A value of0.02means allow prices within ±2% of the latest fill.
This is the same behavior described in Risk Controls
and implemented in src/edumatcher/engine/collar.py.
Validation rules:
risk_controlsmust be a mappingrisk_controls.default_levelmust reference a key underrisk_controls.levels- each
levels.<LEVEL>.collarmust be a mapping when present risk_controls.levels.<LEVEL>.circuit_breakeris not supported; use top-levelcircuit_breaker_defaults- collar percentages must be in
(0, 1)after level and symbol overrides are merged
A symbol only gets a collar if at least one of these is present:
symbols.<SYMBOL>.collar, the symbol's level collar, or the
risk_controls.default_level collar. If none of them apply, the symbol has
no collar at all, even when enforce_collars: true.
When a collar is active, its two fields are resolved most-specific first:
symbols.<SYMBOL>.collar(per-field override)symbols.<SYMBOL>.levelcollarrisk_controls.default_levelcollar- built-in field defaults (
static_band_pct: 0.20,dynamic_band_pct: 0.02)
The built-in defaults in step 4 only fill in fields that none of the higher tiers set; they never create a collar on their own.
Circuit Breakers¶
circuit_breaker_defaults defines the default threshold ladder. Symbol-level
circuit_breaker sections merge over it field by field.
For an operational comparison of symbol-level circuit breakers versus symbol price collars, see Risk Controls - Price collars vs circuit breakers.
circuit_breaker_defaults:
reference_window_ns: 300000000000
levels:
L1:
price_shift_pct: 0.07
halt_duration_ns: 300000000000
L2:
price_shift_pct: 0.13
halt_duration_ns: 900000000000
L3:
price_shift_pct: 0.20
halt_duration_ns:
symbols:
TSLA:
circuit_breaker:
levels:
L1:
halt_duration_ns: 600000000000
Validation rules:
circuit_breaker_defaultsmust be a mapping when presentlevelsmust be a non-empty mapping after defaults and symbol overrides merge- each level requires
price_shift_pctin(0, 1) halt_duration_nsmust be a positive integer or nullreference_window_nsis converted to integer nanoseconds
A halt has no resumption setting, and deliberately so. The halt period is a reopening auction's call phase — LIMIT orders are accepted and rest, market and immediate-or-cancel orders are rejected, and no matching runs — so every halt ends in an uncross at the equilibrium price. Resuming without one would restart continuous matching on a book that had been accumulating crossed interest for the whole halt.
A symbol only gets a circuit breaker if circuit_breaker_defaults or its own
symbols.<SYMBOL>.circuit_breaker section is present. If neither exists, the
symbol has no circuit breaker at all, even when
enforce_circuit_breakers: true.
When a breaker is active, configuration is resolved as follows:
symbols.<SYMBOL>.circuit_breaker(per-level, per-field override)circuit_breaker_defaults- built-in ladder fallback (L1 7%/5m, L2 13%/15m, L3 20%/rest-of-day),
used only when a circuit-breaker section is present but supplies no
levels
Circuit-breaker Fields¶
Circuit breakers may appear under circuit_breaker_defaults or under
symbols.<SYMBOL>.circuit_breaker. Symbol-level fields merge over defaults.
| Field | Required | Accepted values / type | Default |
|---|---|---|---|
reference_window_ns |
No | Integer nanoseconds | 300000000000 |
levels |
Yes when a breaker is active | Non-empty mapping after merging | Built-in L1/L2/L3 only when no levels are supplied |
levels.<LEVEL>.price_shift_pct |
Yes | Number in (0, 1) |
None |
levels.<LEVEL>.halt_duration_ns |
No | Positive integer nanoseconds or null | Null |
Market-Maker Quote Seeds¶
market_maker_quotes create linked bid/ask quote legs at engine startup.
symbols:
AAPL:
market_maker_quotes:
- gateway_id: MM01
quote_id: SEED-MM01-AAPL
bid_price: 209.00
ask_price: 211.00
bid_qty: 2000
ask_qty: 2000
tif: DAY
seed_once: true
| Field | Required | Accepted values / type | Default | Description |
|---|---|---|---|---|
gateway_id |
Yes | Non-empty string, uppercased | None | Configured MARKET_MAKER gateway that owns the quote |
quote_id |
No | String; empty string is treated as omitted | Generated | Explicit quote label |
bid_price |
Yes | Number | None | Display price converted to ticks by the engine |
ask_price |
Yes | Number greater than bid_price |
None | Display price converted to ticks by the engine |
bid_qty |
Yes | Positive integer | None | Bid-side quantity |
ask_qty |
Yes | Positive integer | None | Ask-side quantity |
tif |
No | DAY, GTC, ATO, ATC |
DAY |
Time in force |
seed_once |
No | Boolean-like value; use YAML true/false |
true |
Skip injection after book_stats.json has symbol history |
Validation rules:
- quote seeds must be mappings inside a list
gateway_idmust reference a configured gateway withrole: MARKET_MAKERbid_pricemust be lower thanask_price- quantities must be positive
- if any configured gateway has
role: MARKET_MAKER, every symbol must define at least one quote seed
Quote legs are not persisted to gtc_orders.json; config seeds remain the source
of truth. seed_once: true skips injection after book_stats.json has history
for the symbol. seed_once: false injects on every startup.
Startup Market-Maker Combo Seeds¶
market_maker_combos inject startup combo orders through the same combo path used
by live combo entry.
market_maker_combos:
- combo_id: SEED-PAIR-AAPL-MSFT
combo_type: AON
tif: DAY
legs:
- symbol: AAPL
side: BUY
order_type: LIMIT
quantity: 100
price: 20950
smp_action: NONE
- symbol: MSFT
side: SELL
order_type: LIMIT
quantity: 50
price: 41550
smp_action: NONE
Combo fields:
| Field | Required | Accepted values / type |
|---|---|---|
combo_id |
Yes | Non-empty string |
combo_type |
No | AON; defaults to AON |
tif |
No | DAY, GTC, ATO, ATC; defaults to DAY |
legs |
Yes | List with 2 to 10 entries |
Leg fields:
| Field | Required | Accepted values / type |
|---|---|---|
symbol |
Yes | Configured symbol, unique inside the combo |
side |
Yes | BUY, SELL |
order_type |
Yes | MARKET, LIMIT, STOP, STOP_LIMIT, FOK, ICEBERG, IOC, TRAILING_STOP |
quantity |
Yes | Integer quantity |
price |
Conditional | Integer tick price for priced order types |
stop_price |
Conditional | Integer tick stop price for stop order types |
smp_action |
No | NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH; if omitted, falls back to the seeding gateway's gateways.alf[].smp_action (§below), then NONE |
Combo leg values are passed to ComboLeg.from_dict(), so these are the only leg
fields used by current config parsing. Unlike quote seeds, combo legs do not
include a gateway_id; startup combo ownership is assigned by the engine's combo
seed path — that combo-level gateway_id is what an omitted smp_action falls
back through.
Prefer tif: DAY for repeatable demo seeds. GTC combo seeds can interact with
restored gtc_combos.json state and duplicate intended startup liquidity if you
are not managing persistence deliberately.
Session Schedule¶
The scheduler reads schedule and sends transitions to the engine.
schedule:
pre_open: "09:00"
opening_auction_start: "09:25"
continuous_start: "09:30"
closing_auction_start: "16:00"
closing_auction_end: "16:05"
| Key | Required | Default |
|---|---|---|
pre_open |
No | 09:00 |
opening_auction_start |
No | 09:25 |
continuous_start |
No | 09:30 |
closing_auction_start |
No | 16:00 |
closing_auction_end |
No | 16:05 |
Schedule values are read as strings and should be local server HH:MM values.
The scheduler uses any provided subset in trading-day order. If no usable
schedule is present, it uses built-in defaults. With pm-scheduler --now, the
wall-clock values are ignored and transitions are sent immediately with short
delays.
The default session path is:
country¶
country is a top-level key — a sibling of schedule, not nested under it.
pm-scheduler uses it to decide which calendar days are trading days: it
will not run the daily schedule on a weekend or on that country's bank
holidays, using the python-holidays
package to resolve the holiday calendar.
| Aspect | Value |
|---|---|
| Accepted forms | Country name ("Sweden") or ISO 3166-1 alpha-2 code ("SE") |
| Default when omitted | "Sweden" |
| Behavior on an unrecognized value | Falls back to "Sweden" and logs a warning |
| Weekends | Always treated as non-working days, regardless of the holiday calendar |
Under --daily, a non-working day is skipped and the scheduler sleeps
through to the next working day rather than the next calendar day. In
single-shot mode (the default, no --daily), the scheduler simply sends no
transitions and exits if started on a non-working day. See
Session Scheduling → Bank holidays and weekends
for the full behavior breakdown by run mode.
Startup and Persistence Order¶
The effective engine startup sequence is:
Engine startup
|
+-- 1. Parse config if present
+-- 2. Bind main PULL/PUB sockets
+-- 3. Load persisted book stats from <DATA_DIR>/book_stats.json
+-- 4. Restore persisted GTC orders from <DATA_DIR>/gtc_orders.json
+-- 5. Restore persisted GTC combos from <DATA_DIR>/gtc_combos.json
+-- 6. Inject market_maker_quotes
+-- 7. Inject market_maker_combos
+-- 8. Bind drop-copy PUB :5557 if available
+-- 9. Publish initial book snapshots
<DATA_DIR> resolves to src/data/ in a source checkout, ~/.local/share/edumatcher
when installed, or $EDUMATCHER_DATA_DIR if that environment variable is set.
This ordering means persisted GTC state comes back before config seed liquidity,
and persisted book stats override last_buy_price / last_sell_price seeds.
When changing seed behavior or symbol definitions, consider removing stale data:
# from a source checkout:
rm -f src/data/gtc_orders.json src/data/book_stats.json src/data/gtc_combos.json
# or, if EDUMATCHER_DATA_DIR is set:
rm -f "$EDUMATCHER_DATA_DIR"/gtc_orders.json "$EDUMATCHER_DATA_DIR"/book_stats.json "$EDUMATCHER_DATA_DIR"/gtc_combos.json
Adding or Removing Symbols¶
Adding a symbol is the configuration equivalent of an IPO (initial listing):
you define the instrument together with its opening reference price
(last_buy_price / last_sell_price), its issued share count
(outstanding_shares), and — when a market maker is configured — its opening
quote. Those opening values seed the book's last prices and both the collar and
circuit-breaker references, so the symbol is priced and protected from its very
first order (see
Risk Controls - Day one (IPO) behaviour).
The symbol universe is fixed at startup
pm-engine reads symbols once, at startup. There is no command to
list a new symbol intra-day. Introducing a symbol always means editing
engine_config.yaml and restarting the engine — plan the full instrument
set before the session, or restart during a maintenance window to add a new
listing.
Edit engine_config.yaml and restart the engine.
- adding a symbol makes it tradable on next startup
- removing a symbol causes future orders for it to be rejected
- persisted GTC orders for removed symbols are skipped during restore
- startup combo seeds referencing removed symbols make config loading fail
mm_obligation_defaults.symbols.<SYMBOL>entries must reference configured symbols
Validation Commands¶
For a quick parser check from a source checkout:
poetry run python -c 'from pathlib import Path; from edumatcher.engine.config_loader import load_engine_config; print(load_engine_config(Path("engine_config.yaml")))'
If the file is valid, this prints the parsed EngineConfig object. On error
you get a traceback ending with a descriptive message:
For installed (pipx) users who do not have access to the poetry run environment,
pass the config file to the engine directly — it validates on startup:
For the focused config parser test suite:
These commands answer does it load. To see what a file that loads actually
says — ports, keys, participants, symbols — use
pm-config-show.
Formal Specification¶
This section is a complete machine-readable-style reference for every field
parsed from engine_config.yaml. Types follow Python conventions: bool,
int, float, str. "Enum" means the field must match one of the listed
string values exactly (case-insensitive during loading; stored in uppercase).
For the auxiliary gateway processes (pm-alf-gwy, pm-balf-gwy, pm-md-gwy,
pm-ralf-gwy, pm-api-gwy) and the full normative schema, see
App Config Spec, which governs if the two documents
ever disagree.
Ranges use mathematical interval notation: (a, b) is open (exclusive),
[a, b] is closed (inclusive).
Top-level fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
symbols |
mapping | Yes | — | — | Must contain at least one entry |
gateways |
mapping | Yes | — | — | Must contain key alf |
gateways.alf |
list | Yes | — | — | Non-empty list of gateway mappings |
sessions_enabled |
bool | No | true when file exists, false in unrestricted mode |
true, false |
Must be a YAML boolean |
enforce_collars |
bool | No | true |
true, false |
Must be a YAML boolean |
enforce_circuit_breakers |
bool | No | true |
true, false |
Must be a YAML boolean |
engine_tuning |
mapping | No | — | — | Runtime tuning block |
mm_obligation_defaults |
mapping | No | — | — | — |
risk_controls |
mapping | No | — | — | — |
circuit_breaker_defaults |
mapping | No | — | — | — |
market_maker_combos |
list | No | [] |
— | Each entry must be a mapping |
schedule |
mapping | No | — | — | Parsed by scheduler and stored by engine |
engine_tuning fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
snapshot_interval_sec |
float | No | 0.5 |
Any number | Must be > 0 |
quote_history_maxlen |
int | No | 30 |
Positive integer | Must be > 0 |
drop_copy_buffer_size |
int | No | 10000 |
Positive integer | Must be > 0 |
recent_trades_maxlen |
int | No | 20 |
Positive integer | Must be > 0 |
depth_snapshot_tolerance_ticks |
int | No | 100 |
Positive integer | Must be > 0 |
gateways.alf[] — gateway entry fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
id |
str | Yes | — | Any non-empty string | Uppercased; must be unique within the list |
description |
str or null | No | "" |
Any string or null | Null is coerced to "" |
role |
Enum | No | TRADER |
TRADER, MARKET_MAKER, ADMIN |
Case-insensitive |
disconnect_behaviour |
Enum | No | CANCEL_QUOTES_ONLY |
CANCEL_QUOTES_ONLY, CANCEL_ALL, LEAVE_ALL |
Case-insensitive |
quote_refresh_policy |
Enum | No | INACTIVATE_ON_ANY_FILL |
INACTIVATE_ON_ANY_FILL, INACTIVATE_ON_FULL_FILL, NEVER_INACTIVATE |
Case-insensitive |
enforce_mm_obligation |
bool | No | From mm_obligation_defaults.enforce_mm_obligation, else false |
true, false |
Must be a YAML boolean |
mm_max_spread_ticks |
int | No | From mm_obligation_defaults.mm_max_spread_ticks, else 10 |
Integer | Must be > 0 |
mm_min_qty |
int | No | From mm_obligation_defaults.mm_min_qty, else 100 |
Integer | Must be > 0 |
mm_obligations |
mapping | No | {} |
Mapping of symbol → obligation entry | Symbol keys are uppercased |
smp_action |
Enum | No | NONE |
NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
Case-insensitive; fallback default used when an order (NEW, combo, or QUOTE) doesn't specify its own SMP= |
gateways.alf[].mm_obligations.<SYMBOL> fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
enforce_mm_obligation |
bool | No | Gateway enforce_mm_obligation |
true, false |
Must be a YAML boolean |
max_spread_ticks |
int | No | Gateway mm_max_spread_ticks |
Integer | Must be > 0 |
min_qty |
int | No | Gateway mm_min_qty |
Integer | Must be > 0 |
mm_obligation_defaults fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
enforce_mm_obligation |
bool | No | false |
true, false |
Must be a YAML boolean |
mm_max_spread_ticks |
int | No | 10 |
Integer | Must be > 0 |
mm_min_qty |
int | No | 100 |
Integer | Must be > 0 |
symbols |
mapping | No | {} |
Symbol name → override mapping | Symbol keys are uppercased; each must reference a configured symbol |
mm_obligation_defaults.symbols.<SYMBOL> fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
enforce_mm_obligation |
bool | No | Top-level enforce_mm_obligation |
true, false |
Must be a YAML boolean |
mm_max_spread_ticks |
int | No | Top-level mm_max_spread_ticks |
Integer | Must be > 0 |
mm_min_qty |
int | No | Top-level mm_min_qty |
Integer | Must be > 0 |
risk_controls fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
default_level |
str | No | null |
Any non-empty string | Must match a key in risk_controls.levels if set |
levels |
mapping | No | {} |
Level name → level config mapping | Level names are uppercased |
risk_controls.levels.<LEVEL> fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
collar |
mapping | No | {} |
See collar fields | Must be a mapping; circuit_breaker sub-key is rejected |
Collar fields — in risk_controls.levels.<LEVEL>.collar or symbols.<SYMBOL>.collar¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
static_band_pct |
float | No | 0.20 |
(0, 1) exclusive |
Band truncates toward zero; makes range slightly tighter than exact |
dynamic_band_pct |
float | No | 0.02 |
(0, 1) exclusive |
Same truncation rule |
circuit_breaker_defaults fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
reference_window_ns |
int | No | 300000000000 (5 min) |
Positive integer nanoseconds | Coerced to int |
levels |
mapping | No | Built-in L1/L2/L3 ladder only when a CB section is present but omits levels |
Level name → level config mapping | Values must be mappings |
reopening |
mapping | No | Built-in ACE defaults | Automated Corridor Expansion settings | Merges field-by-field over defaults |
circuit_breaker_defaults.reopening and symbols.<SYMBOL>.circuit_breaker.reopening fields¶
Governs how a circuit-breaker halt ends — see Risk Controls - Automated Corridor Expansion.
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
enabled |
bool | No | true |
true / false |
When false a halt reopens at the equilibrium price with no corridor |
initial_band_pct |
float | No | 0.10 |
(0, 1) exclusive |
Corridor half-width as a fraction of the CB reference price |
expansions |
list | No | [{0.10, 2 min}, {0.20, 5 min}] |
Non-empty list of mappings | The final entry repeats indefinitely. Only valid under circuit_breaker_defaults; per-symbol is an error (S112) |
expansions[].widen_pct |
float | Yes within an entry | — | (0, 1) exclusive |
Added to the half-width; additive on the reference, not compounding |
expansions[].min_duration_ns |
int | Yes within an entry | — | Positive integer nanoseconds | Minimum length of that extension's call phase |
random_end_max_ns |
int | No | 30000000000 (30 s) |
>= 0 nanoseconds |
Uniform random tail added to every call phase; 0 disables it |
random_seed |
int or null | No | null |
Integer or null |
Engine-wide. Only valid under circuit_breaker_defaults; per-symbol is an error |
circuit_breaker_defaults.levels.<LEVEL> and symbols.<SYMBOL>.circuit_breaker.levels.<LEVEL> fields¶
Symbol-level entries merge over the defaults: only the fields you specify are overridden.
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
price_shift_pct |
float | Yes when creating a level | — | (0, 1) exclusive |
Required in any level that originates from config; inherited from defaults for symbol overrides |
halt_duration_ns |
int or null | No | null |
Positive integer nanoseconds, or null/omitted |
null means rest-of-day halt; must be > 0 when provided |
Built-in default CB ladder (used only when a circuit-breaker section exists
but supplies no levels; if no circuit-breaker section is present at all, the
symbol has no breaker):
| Level | price_shift_pct |
halt_duration_ns |
|---|---|---|
| L1 | 0.07 |
300000000000 (5 min) |
| L2 | 0.13 |
900000000000 (15 min) |
| L3 | 0.20 |
null (rest-of-day) |
symbols.<SYMBOL> fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
tick_decimals |
int | No | 2 |
[0, 8] inclusive |
Must be an integer |
level |
str | No | risk_controls.default_level |
Any non-empty string | Must reference a key in risk_controls.levels |
last_buy_price |
float | No | null |
Any number | Overridden by persisted book_stats.json |
last_sell_price |
float | No | null |
Any number | Overridden by persisted book_stats.json |
collar |
mapping | No | — | See collar fields | Merged over the level's collar; symbol wins on conflicting keys |
circuit_breaker |
mapping | No | — | See circuit-breaker fields | levels subkey merged over defaults; other keys replace |
market_maker_quotes |
list | No | [] |
List of quote seed mappings | Required (non-empty) if any MARKET_MAKER gateway is configured |
symbols.<SYMBOL>.market_maker_quotes[] fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
gateway_id |
str | Yes | — | Any non-empty string | Uppercased; must reference a MARKET_MAKER gateway |
quote_id |
str | No | Auto-generated | Any string | Empty string treated as absent |
bid_price |
float | Yes | — | Any number | Must be < ask_price |
ask_price |
float | Yes | — | Any number | Must be > bid_price |
bid_qty |
int | Yes | — | Positive integer | Must be > 0 |
ask_qty |
int | Yes | — | Positive integer | Must be > 0 |
tif |
Enum | No | DAY |
DAY, GTC, ATO, ATC |
Case-insensitive |
seed_once |
bool | No | true |
true, false |
When true, skips injection if book_stats.json has history for this symbol |
market_maker_combos[] fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
combo_id |
str | Yes | — | Any non-empty string | Must not be empty after stripping whitespace |
combo_type |
Enum | No | AON |
AON |
Case-insensitive |
tif |
Enum | No | DAY |
DAY, GTC, ATO, ATC |
Case-insensitive |
legs |
list | Yes | — | List of leg mappings | Must contain 2 to 10 entries |
market_maker_combos[].legs[] fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
symbol |
str | Yes | — | Configured symbol | Uppercased; must be unique within the combo; must be in symbols |
side |
Enum | Yes | — | BUY, SELL |
Case-insensitive |
order_type |
Enum | Yes | — | MARKET, LIMIT, STOP, STOP_LIMIT, FOK, ICEBERG, IOC, TRAILING_STOP |
Case-insensitive |
quantity |
int | Yes | — | Positive integer | — |
price |
int | Conditional | null |
Integer tick price | Required for LIMIT, STOP_LIMIT, FOK, ICEBERG (not enforced for IOC) |
stop_price |
int | Optional | null |
Integer tick price | Not currently validated as required for any order type, including STOP/STOP_LIMIT/TRAILING_STOP |
smp_action |
Enum | No | Seeding gateway's gateways.alf[].smp_action, else NONE |
NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
Case-insensitive |
Combo leg prices are integer ticks
All combo leg price fields (price, stop_price) are integer tick values,
not display floats. For a symbol with tick_decimals: 2, the display price
209.50 is stored as 20950 ticks.
schedule fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
pre_open |
str | No | "09:00" |
"HH:MM" (local server time) |
Any provided subset is used in order |
opening_auction_start |
str | No | "09:25" |
"HH:MM" (local server time) |
— |
continuous_start |
str | No | "09:30" |
"HH:MM" (local server time) |
— |
closing_auction_start |
str | No | "16:00" |
"HH:MM" (local server time) |
— |
closing_auction_end |
str | No | "16:05" |
"HH:MM" (local server time) |
— |
Cross-field validation rules¶
These constraints span multiple sections and are checked after all fields are parsed:
- If any gateway has
role: MARKET_MAKER, every symbol insymbolsmust have at least onemarket_maker_quotesentry. - Every
market_maker_quotes[].gateway_idmust reference a configured gateway withrole: MARKET_MAKER. - Every
symbols.<SYMBOL>.levelmust reference a key inrisk_controls.levels. risk_controls.default_levelmust reference a key inrisk_controls.levels.- Every
mm_obligation_defaults.symbols.<SYMBOL>key must reference a symbol insymbols. - Every
market_maker_combos[].legs[].symbolmust reference a symbol insymbols. - Symbols within one combo must be unique.
risk_controls.levels.<LEVEL>.circuit_breakeris explicitly rejected with an error; use top-levelcircuit_breaker_defaultsinstead.
See Also¶
- Running the Engine - startup order and common runtime workflows
- ALF Console - ALF commands and gateway behavior
- Risk Controls - collar and circuit-breaker behavior in depth
- Persistence - how GTC orders, book stats, and combos are saved and restored
- Processes - which process reads which config section