Appendix: Engine Configuration Specification¶
Status: Normative. This appendix defines the formal structure of
engine_config.yaml, the single reference-data file for an EduMatcher exchange.
It is the authoritative schema; where it and any tutorial disagree, this document
governs. For worked examples, recipes, and rationale, see
Configuration (informative).
The schema described here is derived from and MUST match the runtime loaders:
engine/config_loader.py, alf_gwy/config.py, balf_gwy/config.py,
ralf_gateway/config.py, md_gateway/config.py, api_gateway/config.py,
dc_gateway/config.py, log_srv/config.py, and scheduler/main.py
(schedule and country only). pm-cverifier is the reference validator.
1. Conventions¶
1.1 Requirement keywords¶
The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as described in RFC 2119.
1.2 The file¶
The configuration is a single YAML 1.1 document whose root MUST be a mapping.
A loader given a non-mapping root MUST reject the file. load_engine_config()
itself MUST raise FileNotFoundError for a missing path — it does not implement
a fallback. The caller, pm-engine (engine/main.py), checks for the file's
existence before calling the loader: if the deployed config
(compiled to <EDUMATCHER_DATA_DIR>/ref_data/engine_config.json) does not
exist, pm-engine skips loading entirely and starts in unrestricted mode (no
symbol/gateway allowlist, sessions disabled) rather than failing. That fallback
is implemented by pm-engine, not by the loader, and is out of scope here —
this document specifies the content of a present file.
1.3 Scalar type notation¶
Field tables and the schema tree (§3) use these type names:
| Type | YAML form | Definition |
|---|---|---|
Bool |
boolean | true / false |
Int |
integer | whole number |
Float |
number | integer or decimal; parsed as floating point |
Str |
string | UTF-8 text |
Path |
string | filesystem path (~ is expanded where noted) |
Symbol |
string | instrument id; normalised to upper-case on load |
GatewayId |
string | participant id; non-empty; normalised to upper-case |
IndexId |
string | index id; alphanumeric, upper-case, unique |
Price |
number | price in display units (e.g. 150.10); MUST be a whole multiple of the owning symbol's tick size 10^-tick_decimals (see CV18) |
Qty |
integer | quantity; > 0 unless stated |
Ticks |
integer | count of minimum price increments; > 0 |
Pct01 |
number | fraction in the open interval (0, 1) |
Nanos |
integer | duration in nanoseconds; > 0 (or null where noted) |
Secs |
number | duration in seconds; > 0 |
Port |
integer | TCP port; > 0. The ALF, BALF, CALF, RALF and drop-copy gateway ports MUST also be <= 65535 (§6) |
HHMM |
string | wall-clock local time "HH:MM" |
Country |
string | country name (e.g. "Sweden") or ISO 3166-1 alpha-2 code (e.g. "SE"); MUST be a country recognised by the python-holidays package |
Enum<E> |
string | one member of enum E (§2); case-insensitive, stored upper-case |
List<T> |
sequence | ordered list of T |
Map<K,V> |
mapping | keyed collection; keys of type K, values of type V |
1.4 Field-table columns¶
Each section table uses: Field, Type, Req (✔ required / –
optional), Default (value applied when the key is omitted from a present
file), and Constraints. A Default of — means the field has no default and,
if optional, its absence leaves the feature disabled.
1.5 Unknown keys¶
Loaders are permissive: a key not defined in this specification is ignored
and MUST NOT be relied upon for behaviour. Conforming producers SHOULD NOT emit
unknown keys. pm-cverifier reports an unrecognised key inside a gateway or
service process block (§6) as an error (S121), because the loader's
silent fallback to a default would otherwise hide the typo; it does not check
top-level keys, which pm-config-show --all lists — a mistyped section name is otherwise
indistinguishable from an absent one.
1.6 Case normalisation¶
Symbol, GatewayId, IndexId, and every Enum<E> value are upper-cased during
load. Producers MAY write any case; consumers compare upper-case.
2. Enumerations¶
| Enum | Members | Used by |
|---|---|---|
Role |
TRADER, MARKET_MAKER, ADMIN |
participants[].role |
DisconnectBehaviour |
CANCEL_QUOTES_ONLY, CANCEL_ALL, LEAVE_ALL |
participants[].disconnect_behaviour, participant_defaults.disconnect_behaviour |
QuoteRefreshPolicy |
INACTIVATE_ON_ANY_FILL, INACTIVATE_ON_FULL_FILL, NEVER_INACTIVATE |
participants[].quote_refresh_policy |
TIF |
DAY, GTC, ATO, ATC |
quote/combo seeds |
ComboType |
AON |
market_maker_combos[].combo_type |
Side |
BUY, SELL |
combo legs |
OrderType |
MARKET, LIMIT, STOP, STOP_LIMIT, FOK, ICEBERG, IOC, TRAILING_STOP |
combo legs |
SmpAction |
NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH |
combo legs, participants[].smp_action, participant_defaults.smp_action |
DuplicateSessionPolicy |
REJECT_NEW, EVICT_OLD |
balf_gateway.duplicate_session_policy |
An Enum<E> value outside its member set MUST be rejected.
3. Formal schema tree¶
The complete structure at a glance. Annotations: ! = REQUIRED key, ? =
OPTIONAL key, = = default, ∈ = domain/constraint. Types are from §1.3–§2.
This tree is normative for shape; §4–§6 are normative for field law.
# ── ENGINE (read by pm-engine) ──────────────────────────────────────────────
symbols: ! Map<Symbol, SymbolSpec> # ≥0 entries; key required
participants: ! List<ParticipantSpec> # ≥1 entry
participant_defaults: ? ParticipantDefaultsSpec # values a participants entry inherits when it omits them
sessions_enabled: ? Bool = true
enforce_collars: ? Bool = true
enforce_circuit_breakers: ? Bool = true
require_mm_seed_quotes: ? Bool = true
auction_indicative_interval_sec: ? Float = 1.0 ∈ > 0
engine_tuning: ? EngineTuningSpec
mm_obligation_defaults: ? MMObligationDefaultsSpec
risk_controls: ? RiskControlsSpec
circuit_breaker_defaults: ? CircuitBreakerSpec
market_maker_combos: ? List<ComboSeedSpec> # each: 2..10 legs
indices: ? List<IndexSpec> # ≤ 5
schedule: ? WeeklyScheduleSpec
country: ? Country = "Sweden" # read by pm-scheduler for holiday gating; pm-engine reads it only for an outbound wire field
# ── AUXILIARY GATEWAY BLOCKS (each read by its own process) ─────────────────
alf_gateway: ? AlfGwyProcSpec # pm-alf-gwy
balf_gateway: ? BalfGwyProcSpec # pm-balf-gwy
market_data_gateway: ? MdGwyProcSpec # pm-md-gwy (CALF)
post_trade_gateway: ? RalfGwyProcSpec # pm-ralf-gwy (RALF)
dc_gateway: ? DcGwyProcSpec # pm-dc-gwy (drop-copy TCP relay)
api_gateways: ? Map<Str, ApiGwyProcSpec> # pm-api-gwy (named instances)
log_server: ? LogSrvProcSpec # pm-log-srv (centralized LALF log collector)
SymbolSpec:
level: ? Str ∈ key of risk_controls.levels
tick_decimals: ? Int = 2 ∈ 0..8
outstanding_shares: ? Int ∈ > 0 # REQUIRED for index constituents
last_buy_price: ? Price
last_sell_price: ? Price
market_maker_quotes: ? List<MMQuoteSeedSpec>
collar: ? CollarSpec
order_limits: ? OrderLimitsSpec # per-symbol override
circuit_breaker: ? CircuitBreakerSpec # per-symbol override
ParticipantSpec: # one entry of participants
id: ! GatewayId
description: ? Str = ""
role: ? Enum<Role> = TRADER
disconnect_behaviour: ? Enum<DisconnectBehaviour> = <participant_defaults.disconnect_behaviour | CANCEL_QUOTES_ONLY>
quote_refresh_policy: ? Enum<QuoteRefreshPolicy> = INACTIVATE_ON_ANY_FILL
enforce_mm_obligation: ? Bool = <mm_obligation_defaults.enforce_mm_obligation | false>
mm_max_spread_ticks: ? Ticks = <mm_obligation_defaults.mm_max_spread_ticks | 10>
mm_min_qty: ? Qty = <mm_obligation_defaults.mm_min_qty | 100>
mm_obligations: ? Map<Symbol, MMObligationSpec>
smp_action: ? Enum<SmpAction> = <participant_defaults.smp_action | NONE>
ParticipantDefaultsSpec: # top-level participant_defaults
disconnect_behaviour: ? Enum<DisconnectBehaviour>
smp_action: ? Enum<SmpAction>
# ── PROCESS SPECS (field law in §6) ─────────────────────────────────────────
# bind_address / host: the EDUMATCHER_GATEWAY_BIND_HOST environment variable,
# when set, overrides both the YAML value and the "0.0.0.0" default (§6).
AlfGwyProcSpec: # alf_gateway (pm-alf-gwy)
enabled: ? Bool = true
name: ? Str = "alf-gwy01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5565 ∈ 1..65535
heartbeat_interval_sec: ? Int = 5 ∈ > 0
handshake_timeout_sec: ? Int = 10 ∈ > 0
idle_timeout_sec: ? Int = 30 ∈ > 0
max_connections: ? Int = 64 ∈ > 0
max_client_queue: ? Int = 10000 ∈ > 0
max_commands_per_second: ? Int = 100 ∈ > 0
max_errors_before_disconnect: ? Int = 50 ∈ > 0
error_window_sec: ? Int = 60 ∈ > 0
BalfGwyProcSpec: # balf_gateway (pm-balf-gwy)
enabled: ? Bool = true
name: ? Str = "balf-gwy01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5560 ∈ 1..65535
heartbeat_interval_sec: ? Secs = 1.0
heartbeat_timeout_sec: ? Secs = 5.0
idle_timeout_sec: ? Secs = 30.0
auth_timeout_sec: ? Secs = 10.0
max_connections: ? Int = 64 ∈ > 0
max_client_queue: ? Int = 10000 ∈ > 0
max_messages_per_second: ? Int = 100 ∈ > 0
max_errors_before_disconnect: ? Int = 10 ∈ > 0
error_window_sec: ? Secs = 60.0
duplicate_session_policy: ? Enum<DuplicateSessionPolicy> = REJECT_NEW
MdGwyProcSpec: # market_data_gateway (pm-md-gwy, CALF)
enabled: ? Bool = true
name: ? Str = "md-gwy01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5570 ∈ 1..65535
heartbeat_interval_sec: ? Int = 1 ∈ > 0
idle_timeout_sec: ? Int = 5 ∈ > 0
replay_window_sec: ? Int = 30 ∈ > 0
max_connections: ? Int = 64 ∈ > 0
max_messages_per_second: ? Int = 200 ∈ > 0
max_symbols_per_client: ? Int = 200 ∈ > 0
max_client_queue: ? Int = 10000 ∈ > 0
depth_levels: ? Int = 10 ∈ > 0
RalfGwyProcSpec: # post_trade_gateway (pm-ralf-gwy, RALF); no `enabled`
name: ? Str = "ralf-gwy01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5580 ∈ 1..65535
replay_retention_sec: ? Int = 86400 ∈ > 0
heartbeat_interval_sec: ? Int = 1 ∈ > 0
idle_timeout_sec: ? Int = 5 ∈ > 0
max_client_queue: ? Int = 10000 ∈ > 0
allowed_roles: ? List<Str> = [CLEARING, DROP_COPY, AUDIT]
DcGwyProcSpec: # dc_gateway (pm-dc-gwy); no `enabled`
name: ? Str = "dc-gwy01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5590 ∈ 1..65535
heartbeat_interval_sec: ? Secs = 5
idle_timeout_sec: ? Secs = 30
max_client_queue: ? Int = 10000 ∈ > 0
ApiGwyProcSpec: # one value of api_gateways (pm-api-gwy)
enabled: ? Bool = true
host: ? Str = "0.0.0.0"
port: ? Port = 8080
log_level: ? Str = "info"
swagger_enabled: ? Bool = true
stats_db: ? Path = <data dir>/stats.db
audit_db: ? Path = <data dir>/audit_index.db
order_retention_sec: ? Int = 3600 ∈ >= 0
market_data_cache_sec: ? Int = 60 ∈ >= 0
session_timezone: ? Str | null = null ∈ IANA timezone name
engine_pull_addr: ? Str = tcp://<engine host>:5555
engine_pub_addr: ? Str = tcp://<engine host>:5556
index_pull_addr: ? Str = tcp://<index bind host>:5559
index_pub_addr: ? Str = tcp://<index bind host>:5558
credentials: ? List<ApiCredentialSpec> = []
rate_limit: ? RateLimitSpec
timeouts: ? TimeoutSpec
ApiCredentialSpec:
api_key: ! Str # non-empty, unique within the instance
gateway_id: ? GatewayId | null = null # null = read-only key
description: ? Str = ""
RateLimitSpec:
writes_per_second: ? Int = 10 ∈ > 0
burst: ? Int = 20 ∈ > 0
TimeoutSpec:
engine_auth_sec: ? Secs = 3.0
engine_reply_sec: ? Secs = 3.0
wait_ack_sec: ? Secs = 3.0
LogSrvProcSpec: # log_server (pm-log-srv, LALF / LALF-PS)
enabled: ? Bool = true
name: ? Str = "log-srv01"
bind_address: ? Str = "0.0.0.0"
port: ? Port = 5600
db_path: ? Path = <data dir>/log.db
retention_days: ? Int | null = 30 ∈ >= 0; 0 ≡ null ≡ unbounded
max_message_bytes: ? Int = 65536 ∈ > 0
max_client_queue: ? Int = 10000 ∈ > 0
write_batch_size: ? Int = 50 ∈ > 0
write_batch_interval_ms: ? Int = 100 ∈ > 0
heartbeat_interval_sec: ? Int = 5 ∈ > 0
pubsub_enabled: ? Bool = true
pub_port: ? Port = 5601 # port, pub_port, pull_port pairwise distinct
pull_port: ? Port = 5602
lease_sec: ? Int = 30 ∈ > 0
max_lease_sec: ? Int = 300 ∈ >= lease_sec
max_subscribers: ? Int = 32 ∈ > 0
notify_interval_ms: ? Int = 250 ∈ > 0
backfill_chunk_rows: ? Int = 500 ∈ > 0
max_backfill_minutes: ? Int = 1440 ∈ > 0
max_backfill_rows: ? Int = 100000 ∈ > 0
max_pending_rows: ? Int = 20000 ∈ > 0
pub_sndhwm: ? Int = 10000 ∈ > 0
client: ? LogClientSpec
LogClientSpec: # log_server.client; read by every pm-* process
connect_timeout_sec: ? Secs = 0.5
failover_timeout_sec: ? Float = 30.0 ∈ >= 0
failover_dir: ? Path = <data dir>/logs
4. Domain sub-structures¶
4.1 MMQuoteSeedSpec — symbols.<S>.market_maker_quotes[]¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
gateway_id |
GatewayId |
✔ | — | MUST reference a configured gateway whose role is MARKET_MAKER |
bid_price |
Price |
✔ | — | bid_price < ask_price |
ask_price |
Price |
✔ | — | > bid_price |
bid_qty |
Qty |
✔ | — | > 0 |
ask_qty |
Qty |
✔ | — | > 0 |
tif |
Enum<TIF> |
– | DAY |
|
quote_id |
Str |
– | null |
empty string treated as null |
seed_once |
Bool |
– | true |
when true, skip injection if book_stats already has this symbol |
4.2 CollarSpec — symbols.<S>.collar and risk_controls.levels.<L>.collar¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
static_band_pct |
Pct01 |
– | 0.20 |
0 < x < 1 |
dynamic_band_pct |
Pct01 |
– | 0.02 |
0 < x < 1 |
A per-symbol collar is merged over the symbol's resolved level collar
(symbol keys win). When neither is present, no collar applies to the symbol.
4.3 OrderLimitsSpec — symbols.<S>.order_limits¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
max_order_qty |
Qty |
– | — | > 0 |
max_order_value |
Float |
– | — | > 0; display money |
Pre-trade caps on a single order, configured per symbol and nowhere
else. Unlike collar, order_limits has no risk-level tier and no global
default: an appropriate cap depends on the individual instrument's price and
typical size, so there is nothing sensible for a shared profile to say.
risk_controls.levels.<L>.order_limits is NOT supported and MUST be
rejected (§5.5).
Each cap is independently optional and has no default: a cap the symbol does not set is not enforced. There is deliberately no built-in fallback number — a limit that exists only in code and not in the configuration file is not auditable as a requirement.
An order breaching max_order_qty is rejected with reject code
MAX_ORDER_QTY; one breaching max_order_value (quantity × price, in
display money) with MAX_ORDER_VALUE. max_order_value is not evaluated
for an order that carries no price on the wire (MARKET, IOC) — the same orders
the collar's price bands already skip. Both caps are re-checked on order.amend
against the amended quantity and price, so the control cannot be bypassed by
entering small and amending up.
4.4 MMObligationSpec — participants[].mm_obligations.<S>¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enforce_mm_obligation |
Bool |
– | gateway's enforce_mm_obligation |
|
max_spread_ticks |
Ticks |
– | gateway's mm_max_spread_ticks |
> 0 |
min_qty |
Qty |
– | gateway's mm_min_qty |
> 0 |
4.5 ComboSeedSpec — market_maker_combos[]¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
combo_id |
Str |
✔ | — | non-empty |
combo_type |
Enum<ComboType> |
– | AON |
|
tif |
Enum<TIF> |
– | DAY |
|
legs |
List<ComboLegSpec> |
✔ | — | length 2..10; symbols unique within the combo |
ComboLegSpec:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
symbol |
Symbol |
✔ | — | MUST exist in symbols |
side |
Enum<Side> |
✔ | — | |
order_type |
Enum<OrderType> |
✔ | — | |
quantity |
Qty |
✔ | — | |
price |
Price |
– | null |
required by LIMIT, FOK, STOP_LIMIT, ICEBERG legs (not enforced for IOC despite carrying a limit price); on this leg's symbol's tick grid, which need not be the grid of the combo's other legs; positivity is not validated anywhere in code |
stop_price |
Price |
– | null |
stop price; currently unvalidated for any order type, including STOP/STOP_LIMIT/TRAILING_STOP; on this leg's symbol's tick grid |
smp_action |
Enum<SmpAction> |
– | seeding gateway's participants[].smp_action, else NONE |
4.6 IndexSpec — indices[]¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
id |
IndexId |
✔ | — | alphanumeric; unique across indices |
description |
Str |
✔ | — | non-empty |
base_value |
Float |
– | 1000.0 |
> 0 |
publish_interval_sec |
Secs |
– | 1.0 |
> 0 |
history_file |
Path |
– | data/indexes/<id>_history.jsonl |
non-empty |
state_file |
Path |
– | data/indexes/<id>_state.json |
non-empty |
constituents |
List<Symbol> |
✔ | — | non-empty; each MUST exist in symbols and define outstanding_shares; no duplicates |
4.7 WeeklyScheduleSpec — schedule, and DayScheduleSpec — one day block¶
WeeklyScheduleSpec is a mapping of shortcut and day keys, each an optional
DayScheduleSpec:
| Field | Type | Req | Applies to |
|---|---|---|---|
weekdays |
DayScheduleSpec |
– | mon..fri, for any not given its own key below |
mon, tue, wed, thu, fri |
DayScheduleSpec |
– | one weekday; overrides weekdays for that day |
weekend |
DayScheduleSpec |
– | sat+sun, for either not given its own key below; ∈ mutually exclusive with sat/sun (CV20) |
sat, sun |
DayScheduleSpec |
– | one weekend day; overrides weekend for that day; ∈ mutually exclusive with weekend (CV20) |
holidays |
DayScheduleSpec |
– | days the configured country observes as a bank holiday |
A key absent from schedule (and, for mon..sun, not covered by
weekdays/weekend either) resolves to CLOSED for that day/holiday-set — no
transitions are sent and the engine never leaves CLOSED on it. No other
keys are permitted under schedule (CV21).
DayScheduleSpec — the value of any key above:
| Field | Type | Req | Default |
|---|---|---|---|
pre_open |
HHMM |
✔, if the block is present | — |
opening_auction_start |
HHMM |
✔, if the block is present | — |
continuous_start |
HHMM |
✔, if the block is present | — |
closing_auction_start |
HHMM |
✔, if the block is present | — |
closing_auction_end |
HHMM |
✔, if the block is present | — |
Unlike every other REQUIRED marking in this document, "required" here is
conditional on the block appearing at all: a DayScheduleSpec that is
present but omits any of the five fields MUST be rejected (CV21) — there is
no per-field default once a block exists.
5. Engine sections¶
5.1 symbols (REQUIRED)¶
Map<Symbol, SymbolSpec>. The mapping key is the symbol id. A value of null/{}
is a valid empty spec. Symbol fields: see the schema tree (§3, SymbolSpec);
market_maker_quotes §4.1, collar §4.2, order_limits §4.3,
circuit_breaker §5.6.
5.2 participants (REQUIRED)¶
List<ParticipantSpec> with at least one entry (§3, ParticipantSpec). Participant
ids MUST be unique after upper-casing. This list is the participant allowlist and
is also consumed by pm-alf-gwy and pm-balf-gwy for identity and role.
smp_action sets the self-match-prevention action the engine applies to this
gateway's orders when the order itself doesn't specify one:
- Live
QUOTElegs (the bid/ask orders generated from aquote.newrequest — see Market-Maker Bot and the ALFQUOTEcommand) have no per-request SMP concept of their own, so they always use this default. NEW/combo order entry carries its own optional per-orderSMP=field (§4.5,ComboLegSpec.smp_action; ALF protocolNEW|SMP=). An explicitSMP=from the client — includingSMP=NONE— always takes precedence over this gateway default; only an omittedSMP=falls back to it.
5.2a participant_defaults (OPTIONAL) — ParticipantDefaultsSpec¶
A mapping of values that every participants entry inherits when it omits the
key. It exists so a uniform policy (for example, CANCEL_AGGRESSOR on every
gateway) is written once instead of on each entry.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
smp_action |
Enum<SmpAction> |
– | NONE |
inherited by an entry that omits smp_action |
disconnect_behaviour |
Enum<DisconnectBehaviour> |
– | CANCEL_QUOTES_ONLY |
inherited by an entry that omits disconnect_behaviour |
Resolution order for a gateway's effective value is: the entry's own key, then
participant_defaults, then the built-in default. An explicit entry value always
wins, including smp_action: NONE when the default is something else. The block
applies to every role: a participant_defaults.disconnect_behaviour of
CANCEL_ALL also reaches ADMIN and MARKET_MAKER entries that omit the key,
so entries that need a different behaviour MUST set it explicitly.
Unlike most sections, an unrecognised key under participant_defaults is rejected
(CV22) rather than ignored, so that a mistyped name is not silently dropped.
5.3 Engine behaviour flags¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
sessions_enabled |
Bool |
– | true |
when true, engine starts CLOSED and honours schedule/session transitions; when false, starts and stays CONTINUOUS |
enforce_collars |
Bool |
– | true |
global collar enforcement toggle |
enforce_circuit_breakers |
Bool |
– | true |
global circuit-breaker enforcement toggle |
require_mm_seed_quotes |
Bool |
– | true |
when true, CV3 applies; when false, a MARKET_MAKER gateway may exist with no market_maker_quotes entries, for a genuinely empty book at startup |
NOTE —
sessions_enableddefault: the engine loader appliestruewhen the key is omitted from a present file. (A completely absent config file runs unrestricted with sessions disabled — see Auctions & Scheduling.)
5.3a auction_indicative_interval_sec and engine_tuning (OPTIONAL)¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
auction_indicative_interval_sec |
Float |
– | 1.0 |
> 0; throttle for indicative-uncross republishing during auction call phases |
engine_tuning (EngineTuningSpec) groups low-level runtime tuning knobs not
expected to need adjustment in normal use:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
snapshot_interval_sec |
Float |
– | 0.5 |
> 0; per-symbol book snapshot throttle |
quote_history_maxlen |
Int |
– | 30 |
> 0 |
drop_copy_buffer_size |
Int |
– | 10000 |
> 0 |
recent_trades_maxlen |
Int |
– | 20 |
> 0 |
depth_snapshot_tolerance_ticks |
Int |
– | 100 |
> 0 |
5.4 mm_obligation_defaults (OPTIONAL) — MMObligationDefaultsSpec¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enforce_mm_obligation |
Bool |
– | false |
|
mm_max_spread_ticks |
Ticks |
– | 10 |
> 0 |
mm_min_qty |
Qty |
– | 100 |
> 0 |
symbols |
Map<Symbol, {enforce_mm_obligation, mm_max_spread_ticks, mm_min_qty}> |
– | {} |
each key MUST exist in symbols; per-symbol fields default to the block-level values above |
These values supply the defaults inherited by participants[] obligation fields.
5.5 risk_controls (OPTIONAL) — RiskControlsSpec¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
default_level |
Str |
– | null |
non-empty; MUST be a key of levels |
levels |
Map<Str, {collar: CollarSpec}> |
– | {} |
level names upper-cased |
risk_controls.levels.<L>.circuit_breaker is NOT supported and MUST be
rejected; define circuit breakers under circuit_breaker_defaults (§5.6) instead.
risk_controls.levels.<L>.order_limits is likewise NOT supported and MUST
be rejected; set order_limits on each symbol that needs a cap (§4.3).
5.6 circuit_breaker_defaults and symbols.<S>.circuit_breaker — CircuitBreakerSpec¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
reference_window_ns |
Nanos |
– | 300000000000 (5 min) |
rolling reference window |
levels |
Map<Str, CBLevelSpec> |
– | built-in L1/L2/L3 |
non-empty after merge |
reopening |
ReopeningSpec |
– | built-in ACE defaults | merges field-by-field |
CBLevelSpec:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
price_shift_pct |
Pct01 |
✔ | — | 0 < x < 1 |
halt_duration_ns |
Nanos | null |
– | — | > 0 when set; null = halt for the rest of the trading day |
ReopeningSpec (Automated Corridor Expansion):
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
– |
initial_band_pct |
Pct01 |
– | 0.10 |
0 < x < 1 |
expansions |
List<ExpansionSpec> |
– | [{0.10, 120e9}, {0.20, 300e9}] |
non-empty; final entry repeats indefinitely; only valid under circuit_breaker_defaults |
random_end_max_ns |
Nanos |
– | 30000000000 |
>= 0; 0 disables the random end |
random_seed |
Int | null |
– | null |
engine-wide; only valid under circuit_breaker_defaults |
ExpansionSpec:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
widen_pct |
Pct01 |
✔ | — | 0 < x < 1; additive on the reference price |
min_duration_ns |
Nanos |
✔ | — | > 0 |
Merge order: circuit_breaker_defaults supplies defaults; a symbol's
circuit_breaker.levels.<L> overrides by level key. If no levels result from the
merge, the built-in ladder applies: L1 = 0.07 / 5 min, L2 = 0.13 / 15 min,
L3 = 0.20 / rest-of-day, all AUCTION.
5.7 market_maker_combos (OPTIONAL)¶
List<ComboSeedSpec> — see §4.5. Leg prices are display money on the leg
symbol's own tick grid: the legs of one combo trade different instruments,
which need not share a tick size.
5.8 indices (OPTIONAL)¶
List<IndexSpec>, at most 5 entries — see §4.6.
5.9 schedule (OPTIONAL)¶
WeeklyScheduleSpec — see §4.7. Resolved once at load time into a plain
per-day table (mon..sun, plus holidays) that every consumer reads
without re-implementing the weekdays/weekend shortcut rules: pm-engine
(when sessions_enabled, to publish it on system.reference and
system.session_schedule) and, independently, pm-scheduler (to decide
which transitions to send today).
5.10 country (OPTIONAL)¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
country |
Country |
– | "Sweden" |
MUST be a country name or ISO 3166-1 alpha-2 code recognised by python-holidays; an unrecognised value falls back to the default rather than being rejected |
country is a top-level key, a sibling of schedule rather than nested under
it. Unlike every other field in this document, an invalid country value does
not abort loading (contrast §1.4's general rule and CV16 below). The two
consumers handle an invalid value differently: engine/config_loader.py
substitutes "Sweden" silently, with no python-holidays recognition check
and no log message; pm-scheduler (scheduler/main.py) separately validates
the value against python-holidays, logs a warning, and substitutes
"Sweden" if it is unrecognised (CV16).
pm-engine reads this key for two purposes: it derives the ISO alpha-2
country code reported in outbound reference messages, and it uses the same
python-holidays lookup pm-scheduler does to resolve the today/
today_is_holiday convenience fields on system.reference and
system.session_schedule (§4.7, §5.9) — which of holidays or the
calendar day's own entry applies right now. pm-scheduler uses country
for the same bank-holiday lookup to decide which block of the resolved
schedule (§5.9) — if any — to run today — see
Session Scheduling → Bank holidays and weekends.
6. Auxiliary gateway blocks¶
Each block below is read only by its own process; pm-engine ignores them.
The one exception is log_server.client (§6.7), which every pm-* process reads.
See Configuration → Which Process Reads What.
The named spec type of each block (AlfGwyProcSpec, …) is defined in the schema
tree (§3); the tables below give the field law.
bind_address (host for api_gateways) defaults to "0.0.0.0". When the
EDUMATCHER_GATEWAY_BIND_HOST environment variable is set it overrides both the
YAML value and the default, for every block at once. The ZeroMQ addresses of
the engine and index buses are not YAML-configurable, with the exception of the
four *_addr fields of ApiGwyProcSpec (§6.5).
6.1 alf_gateway — pm-alf-gwy¶
Spec type: AlfGwyProcSpec.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
|
name |
Str |
– | "alf-gwy01" |
|
bind_address |
Str |
– | "0.0.0.0" |
|
port |
Port |
– | 5565 |
1..65535 |
heartbeat_interval_sec |
Int |
– | 5 |
> 0 |
handshake_timeout_sec |
Int |
– | 10 |
> 0 |
idle_timeout_sec |
Int |
– | 30 |
> 0 |
max_connections |
Int |
– | 64 |
> 0 |
max_client_queue |
Int |
– | 10000 |
> 0 |
max_commands_per_second |
Int |
– | 100 |
> 0 |
max_errors_before_disconnect |
Int |
– | 50 |
> 0 |
error_window_sec |
Int |
– | 60 |
> 0 |
Also consumes participants for identity/role.
6.2 balf_gateway — pm-balf-gwy¶
Spec type: BalfGwyProcSpec.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
|
name |
Str |
– | "balf-gwy01" |
|
bind_address |
Str |
– | "0.0.0.0" |
|
port |
Port |
– | 5560 |
1..65535 |
heartbeat_interval_sec |
Secs |
– | 1.0 |
> 0 |
heartbeat_timeout_sec |
Secs |
– | 5.0 |
> 0 |
idle_timeout_sec |
Secs |
– | 30.0 |
> 0 |
auth_timeout_sec |
Secs |
– | 10.0 |
> 0 |
max_connections |
Int |
– | 64 |
> 0 |
max_client_queue |
Int |
– | 10000 |
> 0 |
max_messages_per_second |
Int |
– | 100 |
> 0 |
max_errors_before_disconnect |
Int |
– | 10 |
> 0 |
error_window_sec |
Secs |
– | 60.0 |
> 0 |
duplicate_session_policy |
Enum<DuplicateSessionPolicy> |
– | REJECT_NEW |
Also consumes participants for identity, role, and disconnect_behaviour.
6.3 market_data_gateway — pm-md-gwy (CALF)¶
Spec type: MdGwyProcSpec.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
|
name |
Str |
– | "md-gwy01" |
reported as WELCOME\|GW= |
bind_address |
Str |
– | "0.0.0.0" |
|
port |
Port |
– | 5570 |
1..65535 |
heartbeat_interval_sec |
Int |
– | 1 |
> 0; advertised as WELCOME\|HBINT= |
idle_timeout_sec |
Int |
– | 5 |
> 0 |
replay_window_sec |
Int |
– | 30 |
> 0; advertised as WELCOME\|REPLAY= |
max_connections |
Int |
– | 64 |
> 0 |
max_messages_per_second |
Int |
– | 200 |
> 0 |
max_symbols_per_client |
Int |
– | 200 |
> 0 |
max_client_queue |
Int |
– | 10000 |
> 0 |
depth_levels |
Int |
– | 10 |
> 0 |
6.4 post_trade_gateway — pm-ralf-gwy (RALF)¶
Spec type: RalfGwyProcSpec.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
name |
Str |
– | "ralf-gwy01" |
|
bind_address |
Str |
– | "0.0.0.0" |
|
port |
Port |
– | 5580 |
1..65535 |
replay_retention_sec |
Int |
– | 86400 |
> 0 |
heartbeat_interval_sec |
Int |
– | 1 |
> 0 |
idle_timeout_sec |
Int |
– | 5 |
> 0 |
max_client_queue |
Int |
– | 10000 |
> 0 |
allowed_roles |
List<Str> |
– | [CLEARING, DROP_COPY, AUDIT] |
values upper-cased |
This block has no enabled key.
6.5 api_gateways — pm-api-gwy (REST / WebSocket)¶
api_gateways is a Map<Str, ApiGwyProcSpec> of named instances (the map key
is the instance name, non-empty). The singular key api_gateway is NOT supported and MUST
be rejected. ApiCredentialSpec, RateLimitSpec and TimeoutSpec are defined in the
schema tree (§3).
ApiGwyProcSpec:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
|
host |
Str |
– | "0.0.0.0" |
use 127.0.0.1 for loopback-only deployments |
port |
Port |
– | 8080 |
> 0 |
log_level |
Str |
– | "info" |
|
swagger_enabled |
Bool |
– | true |
|
stats_db |
Path |
– | resolved stats.db |
~ expanded |
audit_db |
Path |
– | resolved audit_index.db |
~ expanded. Read-only; only GET /admin/orders/{order_id} uses it, and that endpoint returns 503 when the file is absent |
order_retention_sec |
Int |
– | 3600 |
>= 0. Seconds a terminal order stays in the in-memory cache; 0 disables eviction |
market_data_cache_sec |
Int |
– | 60 |
>= 0. TTL for cached market-data reads served by this instance |
engine_pull_addr |
Str |
– | tcp://<engine host>:5555 |
ZeroMQ endpoint the gateway sends orders to; <engine host> is EDUMATCHER_ENGINE_HOST (default 127.0.0.1). Not validated; normally left unset |
engine_pub_addr |
Str |
– | tcp://<engine host>:5556 |
ZeroMQ endpoint of the engine event feed. Not validated; normally left unset |
index_pull_addr |
Str |
– | tcp://<index bind host>:5559 |
ZeroMQ endpoint of pm-index control; <index bind host> is EDUMATCHER_INDEX_BIND_HOST (default 127.0.0.1). Not validated; normally left unset |
index_pub_addr |
Str |
– | tcp://<index bind host>:5558 |
ZeroMQ endpoint of the pm-index feed. Not validated; normally left unset |
session_timezone |
Str |
– | null |
IANA timezone name (e.g. "Europe/Stockholm") used to resolve which trading day a date-only query refers to; overrides the timezone recorded in the stats database. An unrecognised value is rejected. |
credentials |
List<ApiCredentialSpec> |
– | [] |
api keys unique within instance |
rate_limit |
RateLimitSpec |
– | see below | |
timeouts |
TimeoutSpec |
– | see below |
ApiCredentialSpec:
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
api_key |
Str |
✔ | — | non-empty; unique within the instance |
gateway_id |
GatewayId |
– | null |
a gateway_id MUST NOT map to two different instances |
description |
Str |
– | "" |
RateLimitSpec: writes_per_second Int > 0 = 10, burst Int > 0 = 20.
TimeoutSpec: engine_auth_sec Secs > 0 = 3.0, engine_reply_sec Secs > 0 = 3.0,
wait_ack_sec Secs > 0 = 3.0.
6.6 dc_gateway — pm-dc-gwy (drop-copy TCP relay)¶
Spec type: DcGwyProcSpec.
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
name |
Str |
– | "dc-gwy01" |
non-empty; echoed in WELCOME |
bind_address |
Str |
– | "0.0.0.0" |
non-empty |
port |
Port |
– | 5590 |
1..65535 |
heartbeat_interval_sec |
Secs |
– | 5 |
> 0; interval between HB lines |
idle_timeout_sec |
Secs |
– | 30 |
> 0; inbound silence disconnect threshold |
max_client_queue |
Int |
– | 10000 |
> 0; per-client outbound buffer before slow-client disconnect |
This block has no enabled key. The drop-copy source address (tcp://127.0.0.1:5557)
is not configurable via YAML — use the --engine-dc-pub CLI flag on pm-dc-gwy
to point at a non-default engine address.
6.7 log_server — pm-log-srv (centralized LALF log collector) — LogSrvProcSpec¶
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
enabled |
Bool |
– | true |
master switch; pm-log-srv refuses to accept connections when false |
name |
Str |
– | "log-srv01" |
echoed in the LALF WELCOME\|SRV= field |
bind_address |
Str |
– | "0.0.0.0" |
TCP listen interface (127.0.0.1 for loopback-only) |
port |
Port |
– | 5600 |
> 0 |
db_path |
Path |
– | resolved data/log.db |
SQLite database path where log_events/processes/server_stats are stored |
retention_days |
Int | null |
– | 30 |
>= 0 or null; 0 is normalised to null (both mean unbounded retention — pruning disabled); pruning otherwise runs once per hour |
max_message_bytes |
Int |
– | 65536 |
> 0; maximum LOG payload size before truncation — oversized messages are truncated and stored, never dropped |
max_client_queue |
Int |
– | 10000 |
> 0; per-connection outbound backlog limit before backpressure is applied |
write_batch_size |
Int |
– | 50 |
> 0; maximum rows per SQLite transaction in the background writer thread |
write_batch_interval_ms |
Int |
– | 100 |
> 0; maximum time between writer-thread flushes, whichever comes first with write_batch_size |
heartbeat_interval_sec |
Int |
– | 5 |
> 0; 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 in WELCOME\|HBINT= (the server itself never sends HB). Doubles as the publish interval for the LALF-PS log.server_state tick |
pubsub_enabled |
Bool |
– | true |
master switch for LALF-PS, the ZeroMQ log-distribution interface; when false no ZeroMQ socket is bound and pm-log-srv runs as a pure TCP collector |
pub_port |
Port |
– | 5601 |
> 0; must differ from port and pull_port; ZeroMQ PUB port carrying live rows, notify ticks, backfill chunks and control acks |
pull_port |
Port |
– | 5602 |
> 0; must differ from port and pub_port; ZeroMQ PULL port receiving subscriber control requests |
lease_sec |
Int |
– | 30 |
> 0; default subscription lease TTL — a subscriber that stops sending log.renew within this window is reaped and its buffers discarded |
max_lease_sec |
Int |
– | 300 |
>= lease_sec; upper bound on a subscriber's requested lease_sec, which is clamped rather than rejected |
max_subscribers |
Int |
– | 32 |
> 0; maximum concurrent leased subscriptions before log.subscribe is answered with TOO_MANY_SUBS |
notify_interval_ms |
Int |
– | 250 |
> 0; coalescing window for NOTIFY-mode ticks, and the floor on a subscriber's requested notify_interval_ms |
backfill_chunk_rows |
Int |
– | 500 |
> 0; rows per log.backfill chunk, and the maximum rows per live log.event message |
max_backfill_minutes |
Int |
– | 1440 |
> 0; largest "last n minutes" window a subscriber may request; a larger request is rejected with INVALID_WINDOW |
max_backfill_rows |
Int |
– | 100000 |
> 0; hard cap on the rows returned by one backfill; the final chunk sets truncated: true when it bites |
max_pending_rows |
Int |
– | 20000 |
> 0; per-subscription STREAM buffer cap — a subscriber that is alive but too slow loses its oldest buffered rows, reported back to it as dropped |
pub_sndhwm |
Int |
– | 10000 |
> 0; ZeroMQ send high-water mark on the PUB socket |
client |
LogClientSpec |
– | see below | read by every pm-* process (not by pm-log-srv) to configure its TcpLogHandler; must be a mapping |
LogClientSpec (log_server.client):
| Field | Type | Req | Default | Constraints |
|---|---|---|---|---|
connect_timeout_sec |
Secs |
– | 0.5 |
> 0; TCP connect timeout for each attempt to reach the log server |
failover_timeout_sec |
Float |
– | 30.0 |
>= 0; how long a process keeps retrying a dropped log-server connection before it switches, permanently for that process, to a local log file |
failover_dir |
Path |
– | resolved <data dir>/logs |
directory for the local failover log files; ~ expanded |
The pubsub_*/pub_*/pull_*/lease_*/backfill_* fields configure LALF-PS,
described in full in Centralized Log Server.
This block has no interaction with participants or any other engine section —
pm-log-srv is a standalone LALF collector, unrelated to the ZeroMQ bus, and does
not consume any engine-section fields. The client sub-block is the only part
read by other processes. See Configuring pm-log-srv
for worked examples and Centralized Log Server for the operational
guide; the wire protocol it serves is normatively specified in the
LALF Protocol Reference.
7. Cross-field and semantic constraints¶
A conforming document MUST satisfy all of the following. Violations MUST be rejected at load.
| # | Rule |
|---|---|
| CV1 | symbols is present and a mapping; participants is present and a list with ≥ 1 entry. |
| CV2 | participants[].id values are unique (after upper-casing). |
| CV3 | If any gateway has role: MARKET_MAKER, then every symbol MUST define at least one market_maker_quotes entry, unless require_mm_seed_quotes: false. |
| CV4 | Every market_maker_quotes[].gateway_id references a configured gateway whose role is MARKET_MAKER. |
| CV5 | For each MM quote seed, bid_price < ask_price and bid_qty, ask_qty > 0. |
| CV6 | A symbol's level (explicit, else risk_controls.default_level) MUST be a key of risk_controls.levels, when any level is in effect. |
| CV7 | risk_controls.default_level, if set, MUST be a key of risk_controls.levels. |
| CV8 | risk_controls.levels.<L>.circuit_breaker MUST NOT appear. |
| CV9 | Each market_maker_combos[] has 2–10 legs; leg symbols are unique within the combo; every leg symbol exists in symbols. |
| CV10 | indices has ≤ 5 entries; each id is alphanumeric and unique; each constituent exists in symbols and defines outstanding_shares; constituents are non-empty and duplicate-free. |
| CV11 | Every key of mm_obligation_defaults.symbols references a symbol that exists in symbols. |
| CV12 | collar.*_band_pct ∈ (0,1); circuit_breaker.levels.<L>.price_shift_pct ∈ (0,1); halt_duration_ns is > 0 or null. |
| CV13 | circuit_breaker.reopening.initial_band_pct and every expansions[].widen_pct ∈ (0,1); expansions is non-empty; expansions[].min_duration_ns is > 0; random_end_max_ns is >= 0; random_seed and expansions appear only under circuit_breaker_defaults. |
| CV14 | (pm-alf-gwy, pm-balf-gwy) No participants id may be a prefix of another id. |
| CV15 | (pm-api-gwy) The singular api_gateway key is not supported; a gateway_id credential MUST NOT be shared across two api_gateways instances. |
| CV16 | (pm-scheduler) An unrecognised country value is the sole exception to the "MUST be rejected" rule in this section — the loader substitutes the default ("Sweden") and logs a warning instead of aborting. pm-scheduler sends no schedule transitions on a calendar day whose resolved entry (§4.7 — a country bank holiday resolves to holidays, otherwise the day's own mon..sun entry) is CLOSED; weekends are not treated specially, only whatever their resolved entry says. |
| CV17 | (pm-log-srv) log_server.retention_days, when present, MUST be >= 0 or null; port, max_message_bytes, max_client_queue, write_batch_size, write_batch_interval_ms, and heartbeat_interval_sec MUST each be > 0. |
| CV18 | Every Price in the file is a whole multiple of its symbol's tick size — symbols.<S>.last_buy_price/last_sell_price, market_maker_quotes[].bid_price/ask_price, and market_maker_combos[].legs[].price/stop_price. The scale is the owning symbol's tick_decimals; for a combo leg that is the leg's symbol, not the combo's first. An off-grid price is rejected, not rounded. |
| CV19 | symbols.<S>.tick_decimals ∈ 0..8; outstanding_shares, when present, > 0. |
| CV20 | schedule.weekend and an individual schedule.sat/schedule.sun key MUST NOT both be present. |
| CV21 | Every DayScheduleSpec present anywhere under schedule (weekdays, weekend, holidays, or an individual mon..sun key) MUST define all five of pre_open, opening_auction_start, continuous_start, closing_auction_start, closing_auction_end; a partial block is rejected, not filled from a default. |
| CV22 | participant_defaults, when present, is a mapping whose only keys are smp_action and disconnect_behaviour, each a valid member of its enumeration. |
8. Processing model¶
- Parse. Load the document as YAML; a non-mapping root is rejected (§1.2).
- Section ownership.
pm-enginereads the engine sections (§5) plus theSymbolSpec/nested structures — with one exception,country(§5.10), which is read only bypm-scheduler; each auxiliary process reads only its own block (§6). No single process reads the whole file. - Normalisation. Symbols, gateway ids, index ids, and enum values are upper-cased (§1.6).
- Validation. Field-level constraints (§4–§6) are checked, then the cross-field rules (§7). The first violation MAY abort loading.
- Unknown keys are ignored (§1.5).
- Whole-file validation.
pm-cverifiervalidates every section — including blocks no runtime process consumes on its own — across four layers (YAML, schema, semantic, completeness). It is the reference conformance checker. - Whole-file presentation.
pm-config-showreads every section for display only. It is read-only and non-normative: it neither validates nor resolves defaults, and it MUST NOT be treated as a conformance signal — a document it renders without complaint may still be rejected by a loader. Where it reports an effective value the specification does define — most notably the port a section binds whenportis omitted (§6) — that value MUST agree with this document.
9. Minimal conformant document¶
The smallest document that loads and supports trading (informative):
participants:
- id: TRADER01
symbols:
AAPL:
tick_decimals: 2
last_buy_price: 149.90
last_sell_price: 150.10
If a MARKET_MAKER gateway is added, CV3 then requires a market_maker_quotes
block on every symbol, unless require_mm_seed_quotes: false is also set.
See also¶
- Configuration — informative reference, generator, recipes
- Config Verifier (
pm-cverifier) — the reference validator - Inspect Configs (
pm-config-show) — read-only viewer for a deployed document, including the effective port map - Risk Controls — collar and circuit-breaker behaviour
- Market Index (
pm-index) — index calculation usingindices - External Protocols Overview — the gateways that read the auxiliary blocks
- Centralized Log Server —
pm-log-srv/pm-log-clioperational guide - LALF Protocol Reference — the wire protocol
pm-log-srvserves