Configuration Schema and Process Blocks¶
This chapter is the field-by-field reference for engine_config.yaml: every
top-level key, which process reads it, and the settings of each section. For
how to write, check and deploy a configuration, read the Operator's Guide
chapter The Configuration Workflow;
for the formal grammar and validation rules, see
Engine Configuration Specification.
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 |
participants |
Yes | Engine | Participant identities allowed to connect, with per-participant settings |
participant_defaults |
No | Engine | Values a participants entry inherits when it omits them |
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 |
require_mm_seed_quotes |
No | Engine | Whether every symbol must seed market_maker_quotes when a MARKET_MAKER gateway is configured |
auction_indicative_interval_sec |
No | Engine | Throttle (seconds) for indicative-uncross republishing during auction call phases |
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 and order-limit 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 |
locale |
No | pm-scheduler, Engine |
Country (bank-holiday/weekend calendar) and session timezone (trading-day bucketing) |
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, participants, participant_defaults, sessions_enabled, enforce_collars, enforce_circuit_breakers, require_mm_seed_quotes, auction_indicative_interval_sec, engine_tuning, mm_obligation_defaults, risk_controls, circuit_breaker_defaults, market_maker_combos, schedule, indices |
Symbol universe, allowed order-entry gateways, session/collar/order-limit/circuit-breaker policy, runtime tuning, MM obligations, startup combo seeds, session schedule, and index definitions |
pm-alf-gwy |
alf_gwy/config.py |
alf_gateway, participants |
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, participants |
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_gateway/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, locale.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-alf-gwy¶
pm-alf-gwy is the TCP front door for the ALF text protocol. It reads an
optional top-level alf_gateway block from the same engine_config.yaml. The
block is not consumed by pm-engine; it only configures the gateway process
itself. Who may log in, and with which role, is not set here: the gateway
takes that from the participants list (see
Participants). Omit the block entirely to run
on the defaults shown below.
Minimal example (every value shown is the default):
alf_gateway:
enabled: true
name: alf-gwy01
bind_address: 0.0.0.0
port: 5565
heartbeat_interval_sec: 5
handshake_timeout_sec: 10
idle_timeout_sec: 30
max_connections: 1024
max_client_queue: 10000
max_commands_per_second: 100
max_errors_before_disconnect: 50
error_window_sec: 60
Fields¶
| Field | Default | Rule | What it does |
|---|---|---|---|
enabled |
true |
boolean | When false, pm-alf-gwy logs a warning and refuses to start |
name |
alf-gwy01 |
string | Reported to every client as GW= in the WELCOME line |
bind_address |
0.0.0.0 |
string | Interface the listener binds. Use 127.0.0.1 for loopback-only. The EDUMATCHER_GATEWAY_BIND_HOST environment variable overrides this value |
port |
5565 |
1..65535 |
TCP port clients connect to |
heartbeat_interval_sec |
5 |
integer > 0 |
How often the gateway sends HB to an authenticated client that has received nothing else in that time. Advertised as HBINT= in WELCOME |
handshake_timeout_sec |
10 |
integer > 0 |
How long a new connection has to complete the HELLO login |
idle_timeout_sec |
30 |
integer > 0 |
How long a client may stay silent before it is disconnected. Advertised as IDLE= in WELCOME |
max_connections |
1024 |
integer > 0 |
Cap on simultaneous client connections |
max_client_queue |
10000 |
integer > 0 |
Cap on lines queued for one client that is not reading |
max_commands_per_second |
100 |
integer > 0 |
Per-client command rate limit |
max_errors_before_disconnect |
50 |
integer > 0 |
Number of protocol errors tolerated within error_window_sec |
error_window_sec |
60 |
integer > 0 |
Length of the sliding window over which errors are counted |
How the settings work together¶
A connection goes through these stages, and each setting acts at one of them.
- Accept. A new TCP connection is accepted unless
max_connectionsclients are already connected; in that case the socket is closed immediately and a warning is logged. The client receives noERRline, only a closed connection. - Login. The first message must be
HELLO. If login is not complete withinhandshake_timeout_sec, the gateway sendsERR|CODE=AUTH_TIMEOUTand closes the connection. On success the client getsWELCOME|PROTO=ALF1|GW=<name>|ID=<gateway id>|HBINT=<n>|IDLE=<n>, so a client can read the timers it must respect instead of hard-coding them. - Keep-alive. Once logged in, the gateway sends
HBwheneverheartbeat_interval_sechas passed without any other line going out to that client. In the other direction, any bytes received from the client count as activity; if nothing arrives foridle_timeout_secthe gateway sendsERR|CODE=IDLE_TIMEOUTand closes. A client that is only watching and sending nothing must therefore send aPINGmore often than the idle timeout, so keepidle_timeout_seccomfortably above the interval at which your clients ping. - Commands. Each client has its own token bucket that holds up to
max_commands_per_secondtokens and refills at that rate; every command spends one token. A command arriving with the bucket empty is rejected withERR|CODE=RATE_LIMITEDand is not forwarded to the engine; the connection stays open.PING,EXITandQUITafter login do not spend tokens. - Errors. Every
ERRthe gateway sends for a client's mistake (bad syntax, unknown command, rejected request,RATE_LIMITED, and so on) is recorded with a timestamp. Timestamps older thanerror_window_secdrop out. Whenmax_errors_before_disconnecterrors are inside the window, the gateway sendsERR|CODE=MAX_ERRORSand disconnects. A client that is rate-limited all the time is therefore eventually disconnected; raisemax_commands_per_secondor lower the client's send rate. - Slow consumers. Lines for a client wait in an outbound queue until its
socket accepts them. If that queue reaches
max_client_queuelines, the queue is discarded, the gateway sendsERR|CODE=SLOW_CLIENTand closes the connection, so one stuck client cannot consume the gateway's memory.
Choosing values for a classroom: the defaults suit a few dozen desks. Raise
max_connections if more clients connect, raise max_commands_per_second for
bots that send quote refreshes in bursts, and lower idle_timeout_sec if you
want abandoned sessions released quickly.
Overrides and generation¶
pm-alf-gwy --bind ADDRand--port Noverridebind_addressandportfor one run;--engine-host HOSTpoints the gateway at an engine on another machine. The other fields can only be changed in the file.- Changing the block takes effect when
pm-alf-gwyis restarted;pm-enginedoes not need to be restarted, since it never reads this block. pm-config-genemits the block with--alf-gatewayand the--alf-*overrides listed under ALF gateway options.pm-cverifiervalidates every field above and reports an unrecognised key in the block (S121), which would otherwise be ignored and leave the default in force.
See ALF Gateway for operating the gateway and ALF Protocol for the message formats.
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_connections: 64
max_messages_per_second: 200
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_connectionscaps simultaneous TCP connectionsmax_messages_per_secondis the inbound per-client token-bucket rate limitmax_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
participants list — no separate credentials block is needed.
Minimal example:
balf_gateway:
enabled: true
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 |
|---|---|---|
enabled |
true |
When false, pm-balf-gwy logs a warning and refuses to start |
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 |
Accepted but currently unused by the gateway runtime — only heartbeat_timeout_sec drives idle-session disconnects. Reserved for future use. |
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¶
Configuring pm-dc-gwy¶
pm-dc-gwy reads an optional top-level dc_gateway block from the same
engine_config.yaml file. This block is not consumed by pm-engine; it is
consumed by the drop-copy TCP relay process itself. Gateway identities and
roles are read from the existing participants list — no separate
credentials block is needed. This block has no enabled key.
Minimal example (every value shown is the default):
dc_gateway:
name: dc-gwy01
bind_address: 0.0.0.0
port: 5590
heartbeat_interval_sec: 5
idle_timeout_sec: 30
max_client_queue: 10000
| Field | Default | Description |
|---|---|---|
name |
dc-gwy01 |
Gateway name echoed in WELCOME |
bind_address |
0.0.0.0 |
TCP listen interface (127.0.0.1 for loopback-only) |
port |
5590 |
TCP listen port |
heartbeat_interval_sec |
5 |
Seconds between server-initiated HB lines when no other outbound traffic |
idle_timeout_sec |
30 |
Disconnect a session if no inbound traffic arrives within this window |
max_client_queue |
10000 |
Per-client outbound buffer depth before a slow-client disconnect |
The drop-copy source address the gateway relays from (tcp://127.0.0.1:5557,
the engine's drop-copy PUB socket) is not configurable through this
block — use pm-dc-gwy --engine-dc-pub to point at a non-default engine
address for one run.
If you prefer to generate this block instead of writing it by hand,
pm-config-gen can emit it with --dc-gateway and optional --dc-*
overrides. See Drop-Copy 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
audit_db: data/audit_index.db
clearing_db: data/clearing.db
order_retention_sec: 3600
market_data_cache_sec: 60
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 databaseaudit_dbispm-audit's read-only index, used only byGET /admin/orders/{order_id}; optional, and that endpoint returns503when the file is absentclearing_dbispm-clearing's read-only ledger, used only for the session/MTD/YTD realized P&L onGET /positions; optional, and those three fields reportnullwhen the file is absentorder_retention_secbounds how long a terminal order stays in the in-memory order cache;0disables evictionmarket_data_cache_secis the TTL for the per-symbol market-data cache backing WebSocket snapshot/resumecredentials[].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.
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.
auction_indicative_interval_sec¶
Throttle, in seconds, between republications of the indicative uncross price
and quantity while a symbol is in an auction call phase (PRE_OPEN or
PRE_CLOSE). Must be numeric and greater than zero; defaults to 1.0 when
omitted.
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
Participant Defaults¶
The optional top-level participant_defaults block holds values that every
participants entry inherits when it omits the key. Use it to state a
uniform policy once instead of repeating it on each gateway.
participant_defaults:
smp_action: CANCEL_AGGRESSOR
disconnect_behaviour: CANCEL_ALL
participants:
- id: TRADER01 # inherits both defaults
- id: TRADER02
smp_action: NONE # explicit: allows self-trades on this gateway
- id: OPS01
role: ADMIN
disconnect_behaviour: LEAVE_ALL
| Field | Required | Accepted values | Default when omitted |
|---|---|---|---|
smp_action |
No | NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
NONE |
disconnect_behaviour |
No | CANCEL_QUOTES_ONLY, CANCEL_ALL, LEAVE_ALL |
CANCEL_QUOTES_ONLY |
Rules:
- a gateway's effective value is its own key, then
participant_defaults, then the built-in default shown above - an explicit gateway value always wins, including
smp_action: NONEwhen the default is something else - the block applies to every role; a
disconnect_behaviourdefault ofCANCEL_ALLalso reachesADMINandMARKET_MAKERentries that omit the key, so give those entries their own value when they need a different behaviour participant_defaultsmust be a mapping, values are case-insensitive, and any key other than the two above is rejected at load (a mistyped name is not silently ignored)- per-order
SMP=still outranks both (see thesmp_actionnote below)
pm-config-gen writes the block with --participant-default-smp and
--participant-default-disconnect, pm-cverifier checks it (S118–S120), and the
config GUI edits it under Basics → Participants.
Participants¶
Only gateway IDs listed under participants may connect and submit orders when
a config file exists.
participants:
- id: TRADER01
description: Student workstation 1
role: TRADER
disconnect_behaviour: CANCEL_ALL
Participant 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 |
participant_defaults, then 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 20 |
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 |
participant_defaults, then NONE |
smp_action is a fallback default, not an override
participants[].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(or the inheritedparticipant_defaults.smp_action), and finally toNONEif neither is configured.
In short: an explicit per-order SMP= always wins; participants[].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:
participants:
- 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 | 20 |
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:
participants[*].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 anyparticipantsentry hasrole: MARKET_MAKER, unless the top-levelrequire_mm_seed_quotesis explicitly set tofalse(defaulttrue). Withrequire_mm_seed_quotes: false, aMARKET_MAKERgateway may exist with nomarket_maker_quotesentries at all. If noMARKET_MAKERgateway is configured, this field can be omitted for every symbol regardless ofrequire_mm_seed_quotes.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 \
--participants 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 |
DAY |
Time in force (pm-cverifier rejects any other value, S016) |
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 | Display price for priced order types, on the leg symbol's tick grid |
stop_price |
Conditional | Display stop price for stop order types, on the leg symbol's tick grid |
smp_action |
No | NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH; if omitted, falls back to the seeding gateway's participants[].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. Every
weekday (mon..sun) resolves to its own five-time block, or CLOSED if
unconfigured; weekdays:/weekend: are shortcuts for repeating one block
across Monday-Friday or Saturday+Sunday, and holidays: gives bank holidays
their own schedule instead of defaulting to CLOSED. See
Session Scheduling → Configuring the schedule
for the full set of keys, precedence rules, and worked examples.
schedule:
weekdays:
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 | Applies to |
|---|---|---|
weekdays |
No | Monday-Friday, for any day not overridden below |
mon..fri |
No | One weekday, overriding weekdays for that day |
weekend |
No | Saturday and Sunday — mutually exclusive with sat/sun |
sat, sun |
No | One weekend day, overriding weekend for that day |
holidays |
No | Bank holidays for the configured locale.country |
Every block that is present MUST define all five transition times
(pre_open, opening_auction_start, continuous_start,
closing_auction_start, closing_auction_end) — there is no per-key
fallback for a partial block. A day with nothing resolved for it (not
weekdays/weekend and not individually specified) is CLOSED. If no usable
schedule section is present at all, pm-scheduler uses built-in defaults
(the times shown above, applied as weekdays; weekends and holidays CLOSED).
With pm-scheduler --now, the wall-clock values are ignored and transitions
are sent immediately with short delays.
The default session path is:
locale¶
locale is a top-level key, a sibling of schedule, grouping the two facts
about where this exchange is: its country and its session timezone. Both
sub-fields are optional and independent of each other — setting one does not
change the other's default or validation.
locale.country — pm-scheduler uses it to decide, for a given calendar
day, whether the holidays: block applies (see the table above), 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 | No longer forced CLOSED — weekend:/sat:/sun: can give them their own schedule, same as any weekday |
Under --daily, a CLOSED day is skipped and the scheduler sleeps through to
the next day that resolves to a schedule, rather than the next calendar day.
In single-shot mode (the default, no --daily), the scheduler simply sends
no transitions and exits if today is CLOSED. See
Session Scheduling → Bank holidays and weekends
for the full behavior breakdown by run mode.
locale.session_timezone — the IANA timezone (e.g. "Europe/Stockholm",
"UTC") used to decide which calendar date, in that timezone, a
UTC-timestamped event falls into. pm-stats and pm-clearing both read it
from the compiled config to bucket events into trading days; neither takes
a --timezone CLI flag any more.
| Aspect | Value |
|---|---|
| Accepted forms | "UTC" or any IANA timezone name recognized by zoneinfo |
| Default when omitted | "Europe/Stockholm" |
| Behavior on an unrecognized value | Rejected — the process refuses to start |
Both pm-stats and pm-clearing persist the timezone name they first run
with and refuse to start if the configured value later contradicts what is
already recorded in their database, since re-bucketing an existing
database's events under a different trading-day boundary would silently
change already-published figures.
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 |
participants |
list | Yes | — | — | Non-empty list of participant 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 |
require_mm_seed_quotes |
bool | No | true |
true, false |
If false, a MARKET_MAKER gateway may exist with no market_maker_quotes (see Mandatory Fields) |
locale |
mapping | No | — | — | Country and session timezone; see locale fields |
indices |
list | No | [] |
— | At most 5 entries; see Configuring pm-index for the per-index field reference |
auction_indicative_interval_sec |
float | No | 1.0 |
Any number | Must be > 0 |
participant_defaults |
mapping | No | — | smp_action, disconnect_behaviour |
Inherited by participants entries that omit them; see Participant Defaults |
alf_gateway |
mapping | No | — | — | pm-alf-gwy settings; see Configuring pm-alf-gwy |
balf_gateway |
mapping | No | — | — | pm-balf-gwy settings; see Configuring pm-balf-gwy |
market_data_gateway |
mapping | No | — | — | pm-md-gwy settings; see Configuring pm-md-gwy |
post_trade_gateway |
mapping | No | — | — | pm-ralf-gwy settings; see Configuring pm-ralf-gwy |
dc_gateway |
mapping | No | — | — | pm-dc-gwy settings; see Drop-Copy Gateway |
log_server |
mapping | No | — | — | pm-log-srv settings; see Configuring pm-log-srv |
api_gateways |
mapping | No | — | — | Named pm-api-gwy instances; see Configuring pm-api-gwy |
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 |
locale fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
country |
str | No | "Sweden" |
Country name or ISO 3166-1 alpha-2 code | Unrecognized value falls back to the default (see M026 in Config Verifier) |
session_timezone |
str | No | "Europe/Stockholm" |
"UTC" or an IANA timezone name |
Unrecognized value is rejected |
participants[] — 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 20 |
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= |
participants[].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 | 20 |
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, unless require_mm_seed_quotes: false (see Mandatory Fields) |
outstanding_shares |
int | No | null |
Positive integer | Required if the symbol is listed in an indices[].constituents entry (see Mandatory Fields) |
order_limits |
mapping | No | — | See below | Rejected at risk_controls.levels.<LEVEL> — set per symbol only |
symbols.<SYMBOL>.order_limits fields¶
| Field | Type | Required | Default | Allowed values / range | Constraint |
|---|---|---|---|---|---|
max_order_qty |
int | No | null (no limit) |
Positive integer | Must be > 0 |
max_order_value |
float | No | null (no limit) |
Any number | Must be > 0 |
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 |
Case-insensitive; pm-cverifier rejects any other value (S016) |
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 |
float | Conditional | null |
Display price | Required for LIMIT, STOP_LIMIT, FOK, ICEBERG (not enforced for IOC); must be a multiple of the leg symbol's tick size |
stop_price |
float | Optional | null |
Display price | Not currently validated as required for any order type, including STOP/STOP_LIMIT/TRAILING_STOP; must be a multiple of the leg symbol's tick size |
smp_action |
Enum | No | Seeding gateway's participants[].smp_action, else NONE |
NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
Case-insensitive |
Combo leg prices use the leg symbol's scale
The legs of one combo trade different instruments, which need not share a
tick size, so each leg's price is checked against its own symbol's
tick_decimals rather than the combo's.
schedule fields¶
Top level:
| Field | Type | Required | Applies to | Constraint |
|---|---|---|---|---|
weekdays |
day block | No | Mon-Fri, for any day not overridden below | mutually exclusive with nothing; see individual-day override rule below |
mon, tue, wed, thu, fri |
day block | No | one weekday | overrides weekdays for that day only |
weekend |
day block | No | Sat+Sun, for either day not overridden below | mutually exclusive with sat/sun |
sat, sun |
day block | No | one weekend day | overrides weekend for that day only; mutually exclusive with weekend |
holidays |
day block | No | bank holidays for locale.country |
applies instead of the calendar day's own entry |
Each day block (the value of weekdays, weekend, holidays, or any
individual mon..sun key) is:
| Field | Type | Required | Default | Allowed values / range |
|---|---|---|---|---|
pre_open |
str | Yes, if the block is present | — | "HH:MM" (local server time) |
opening_auction_start |
str | Yes, if the block is present | — | "HH:MM" (local server time) |
continuous_start |
str | Yes, if the block is present | — | "HH:MM" (local server time) |
closing_auction_start |
str | Yes, if the block is present | — | "HH:MM" (local server time) |
closing_auction_end |
str | Yes, if the block is present | — | "HH:MM" (local server time) |
A day block with any of the five keys missing is rejected — there is no
per-key default once a block is present at all. A day (or holidays) with no
block resolved for it is CLOSED. weekend and an individual sat/sun key
present together is also rejected.
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, unless top-levelrequire_mm_seed_quotesis set tofalse. - 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.risk_controls.levels.<LEVEL>.order_limitsis explicitly rejected with an error; setorder_limitson each symbol instead.