Processes, Environment and Ports¶
Learning objectives
After reading this page you will understand:
- Why EduMatcher is built as a set of independent processes connected by a message bus rather than a single monolithic program
- The trade-offs that architecture introduces: scalability and observability
vs. deployment complexity and latency
- The role and responsibilities of each of the core runtime processes plus the optional AI trader, market-maker bot, and admin tools
- Which processes are mandatory and which are optional observers
- How to read the message-flow tables to trace an order from submission to fill
Background — Why Separate Processes?¶
The monolith approach¶
The simplest trading system is a single program: the user types an order, the matching logic runs, and the result is printed — all in the same process. This is easy to build, easy to debug, and fast. It is also a dead end as soon as you need any of the following:
- More than one trader connected simultaneously
- Observers (audit, statistics, P&L) that see every trade without modifying the matching logic
- The ability to restart one component without restarting everything
- A viewer that can run on a different machine from the engine
The message-bus approach¶
EduMatcher distributes work across processes connected by a ZeroMQ message bus. The engine owns three sockets:
flowchart LR
subgraph producers["Producers (PUSH → 5555)"]
GW["pm-alf-console\n(ALF order entry)"]
ALFGWY["pm-alf-gwy\n(ALF TCP gateway)"]
SCH["pm-scheduler\n(session transitions)"]
ADM["pm-admin / pm-admin-cli\n(admin commands)"]
AI["pm-ai-trader / pm-ai-swarm\n(AI bots)"]
MMB["pm-mm-bot\n(MM bot)"]
end
subgraph engine["pm-engine"]
PULL["PULL :5555"]
PUB["PUB :5556"]
DC["PUB :5557\n(drop-copy)"]
end
subgraph subscribers["Subscribers (SUB from 5556)"]
VW["pm-viewer"]
ORD["pm-orders"]
AUD["pm-audit"]
CLR["pm-clearing"]
STS["pm-stats"]
TKR["pm-ticker"]
BRD["pm-board"]
end
GW -->|PUSH| PULL
ALFGWY -->|PUSH| PULL
SCH -->|PUSH| PULL
ADM -->|PUSH| PULL
AI -->|PUSH| PULL
MMB -->|PUSH| PULL
PUB -->|SUB| GW
PUB -->|SUB| ALFGWY
PUB -->|SUB| VW
PUB -->|SUB| ORD
PUB -->|SUB| AUD
PUB -->|SUB| CLR
PUB -->|SUB| STS
PUB -->|SUB| TKR
PUB -->|SUB| BRD
PUB -->|SUB| AI
PUB -->|SUB| MMB
DC -.->|drop-copy SUB| EXT["External risk /\ncompliance systems\n(direct ZMQ)"]
DC -.->|drop-copy SUB| DCGWY["pm-dc-gwy\n(TCP :5590)"]
DCGWY -->|TCP| EXT2["External risk /\ncompliance systems\n(no ZeroMQ)"]
The engine is the only process that writes to the order book. Every other process is either a producer (sends commands via PUSH) or a subscriber (reads events via SUB), or both. No process shares memory with any other.
| Port | Socket type | Bound by | Purpose |
|---|---|---|---|
| 5555 | PULL | pm-engine | Receives all inbound commands (orders, cancels, admin) |
| 5556 | PUB | pm-engine | Broadcasts all market events (fills, book updates, session changes) |
| 5557 | PUB | pm-engine | Drop-copy feed, fill events tagged per gateway, with sequence numbers and replay support |
This design has concrete advantages:
| Advantage | How it helps |
|---|---|
| Isolation | A crashing viewer cannot corrupt the order book |
| Observability | Any subscriber can see every event without changing the engine |
| Horizontal scale | Multiple gateways connect simultaneously; each is independent |
| Language freedom | A subscriber can be written in any language that speaks ZeroMQ |
| Testability | The engine can be tested with a fake gateway; subscribers can be tested with recorded message streams |
And honest disadvantages:
| Disadvantage | Consequence |
|---|---|
| Latency | Each message crosses a socket; a round-trip (order → engine → ACK) takes microseconds rather than nanoseconds on localhost |
| Startup order | The engine must bind before any other process connects |
| Partial failure | If the engine crashes mid-session, subscribers hold stale state until it restarts |
| No shared clock | Timestamps are set by the sender; two processes on different machines can disagree by milliseconds |
| Message loss | ZeroMQ PUB/SUB drops messages if a subscriber is slow; fast publishers can outrun slow consumers |
For an educational system running on a laptop, none of the disadvantages are serious. For a production exchange, each one would require careful engineering (persistent queues, hardware timestamping, consensus protocols).
The two bus patterns used here¶
| Pattern | Socket pair | Direction | Used for |
|---|---|---|---|
| PUSH/PULL | Gateway PUSH → Engine PULL | One-to-one, reliable delivery | Sending commands (orders, cancel requests) to the engine |
| PUB/SUB | Engine PUB → many SUBs | One-to-many, topic-filtered | Broadcasting events (fills, book updates, session changes) |
PUSH/PULL guarantees delivery to exactly one receiver (the engine). PUB/SUB does not guarantee delivery — a subscriber that is not yet connected will miss messages sent before it subscribes. This is why the engine publishes an initial book snapshot on startup: late-joining viewers can request the current state rather than waiting for the next change.
Process Overview¶
A complete EduMatcher session is built from the processes below: the core engine, its recorders, and the external protocol gateways (tabulated as Core processes); a set of query, admin and spy tools used as needed (tabulated as Monitoring & Admin tools); and a handful of AI-trader, setup and config entrypoints that each get their own section further down instead of a table row.
Protocol landscape
EduMatcher supports multiple external protocol families with different roles: ALF for order entry, BALF for binary order entry, CALF for market-data dissemination, and RALF for post-trade dissemination. For a single map of purpose, runtime status, and links to detailed chapters and formal specs, see External Protocols Overview.
Why pm-?
All CLI commands share the pm- prefix, short for Process for Matching.
The prefix avoids name collisions with system utilities and makes it easy to
identify EduMatcher processes in a process list (ps aux | grep pm-).
Installation modes
Commands are shown without a prefix. If you are running from a source
checkout (developer mode), prepend poetry run:
If you installed with pipx (end-user mode), the commands are on your PATH and need no prefix:
See the Quick Start's Install and Start
for a first installation, and the Operator's Guide
Installation
chapter for every installation mode and the pm-setup bootstrap command.
Environment variables¶
One variable, EDUMATCHER_DATA_DIR, controls where EduMatcher finds and
stores everything at runtime. Set it once in your shell profile (~/.zshrc or
~/.bashrc) and every pm-* command picks it up automatically. A second
family of variables, listed under Network variables
below, moves the network endpoints; all of them are optional and the defaults
keep everything on the loopback interface.
| Variable | Default (installed) | Default (source checkout) | Purpose |
|---|---|---|---|
EDUMATCHER_DATA_DIR |
~/.local/share/edumatcher |
<repo>/src/data/ |
Directory for all persistent data files (gtc_orders.json, stats.db, audit.log, etc.) and the deployed engine configuration |
The engine configuration is not separately configurable. Every process
reads <EDUMATCHER_DATA_DIR>/ref_data/engine_config.json — there is no
--config flag and no EDUMATCHER_CONFIG variable. This is deliberate: while
each process took its own path, starting one of them against the wrong file
produced an exchange whose parts silently disagreed, and the symptom (a
gateway reporting no instruments) pointed nowhere near the cause.
One data directory is therefore one exchange instance. Getting it wrong now detaches a process from its statistics and logs as well, so the mistake announces itself immediately.
# Example: per-session isolation
export EDUMATCHER_DATA_DIR="$HOME/sessions/morning"
pm-config-deploy ~/configs/morning.yaml
pm-engine --verbose
See pm-config-deploy for installing a configuration.
Network variables¶
These are read once, when a process starts, by edumatcher/config.py (the
scheduler pair by pm-scheduler itself). Unset, every default keeps the core
bus on 127.0.0.1, so a plain install is not reachable from other machines.
"Bind" variables say where a server listens; EDUMATCHER_ENGINE_HOST says
where every client connects.
| Variable | Default | Read by | Purpose |
|---|---|---|---|
EDUMATCHER_ENGINE_BIND_HOST |
127.0.0.1 |
pm-engine |
Interface the engine binds 5555, 5556 and 5557 on. Set to 0.0.0.0 (or a private address) to let processes on other hosts reach the bus |
EDUMATCHER_ENGINE_HOST |
127.0.0.1 |
every process that connects | Host to connect to for the engine ports and for the index, sim and health endpoints below. Also the default for --alf-host on the AI bots |
EDUMATCHER_INDEX_BIND_HOST |
127.0.0.1 |
pm-index |
Interface for 5558, 5559 and the index health socket |
EDUMATCHER_INDEX_PUB_PORT |
5558 |
pm-index and its clients |
Index PUB port (index.update) |
EDUMATCHER_INDEX_PULL_PORT |
5559 |
pm-index and its clients |
Index PULL port (operator commands, history requests) |
EDUMATCHER_INDEX_HEALTH_PORT |
5564 |
pm-index, pm-index-cli health |
Health REP socket |
EDUMATCHER_SIM_BIND_HOST |
127.0.0.1 |
pm-market-sim |
Interface for the sim ports. Loopback by default on purpose: nothing a student's client can reach carries a true value |
EDUMATCHER_SIM_PUB_PORT |
5553 |
pm-market-sim and its clients |
sim.value, sim.state, news.event |
EDUMATCHER_SIM_PULL_PORT |
5554 |
pm-market-sim and its clients |
sim.command from ADMIN participants |
EDUMATCHER_STATS_BIND_HOST / EDUMATCHER_STATS_HEALTH_PORT |
127.0.0.1 / 5561 |
pm-stats, pm-stats-cli health |
Health REP socket |
EDUMATCHER_AUDIT_BIND_HOST / EDUMATCHER_AUDIT_HEALTH_PORT |
127.0.0.1 / 5562 |
pm-audit, pm-audit-cli health |
Health REP socket |
EDUMATCHER_CLEARING_BIND_HOST / EDUMATCHER_CLEARING_HEALTH_PORT |
127.0.0.1 / 5563 |
pm-clearing, pm-clearing-cli health |
Health REP socket |
EDUMATCHER_GATEWAY_BIND_HOST |
unset | pm-alf-gwy, pm-balf-gwy, pm-md-gwy, pm-ralf-gwy, pm-dc-gwy, pm-log-srv, pm-api-gwy, pm-monitor |
Overrides the bind address of every service-layer listener at once. Precedence: an explicit --host flag, then this variable, then the section's bind_address in engine_config.yaml, then 0.0.0.0 (127.0.0.1 for pm-monitor) |
EDUMATCHER_SCHEDULER_SPEED |
unset | pm-scheduler |
Default for --speed: run the --daily clock F times faster than the wall clock |
EDUMATCHER_SCHEDULER_START |
unset | pm-scheduler |
Default for --start: the session-local time (YYYY-MM-DDTHH:MM) the compressed clock starts at |
Two planes, two ways to steer them
The core plane (5553–5559, and the health sockets) is steered only
by environment variables, because those ports are fixed source constants or
env-overridable and appear nowhere in engine_config.yaml. The service
layer (the TCP gateways, pm-log-srv, pm-monitor) is steered by its own
section of the YAML, and EDUMATCHER_GATEWAY_BIND_HOST is the single
override on top. pm-config-show prints both, with the source of each
value.
How the default is selected¶
The data directory is selected when the EduMatcher Python package is imported, not from the process's current working directory:
- If
EDUMATCHER_DATA_DIRis set, its expanded, absolute path wins — in a source checkout and in an installed deployment alike. - Otherwise EduMatcher checks where
edumatcher/config.pyis installed. If its package parent is namedsrc, the process is treated as running from a source checkout and uses<repo>/src/data/. - Otherwise the package is treated as installed and uses
~/.local/share/edumatcher(for example/Users/<user>/.local/share/edumatcheron macOS). This path is fixed; it does not followXDG_DATA_HOME. - If the directory chosen above does not exist, but
./dataexists in the current working directory, that is used instead, with a warning on stderr. The chosen directory is used anyway, also with a warning, when neither exists.
So running an installed command from inside a repository does not turn it
into a source checkout, and running a Poetry command from another directory
does not move the source-checkout data location. When several processes must
share one exchange, give them all the same explicit EDUMATCHER_DATA_DIR.
The authored YAML may live anywhere, but deployment always installs the compiled artifact, with a copy of its source, under the selected directory:
Relative runtime paths in the configuration, such as data/stats.db, are
resolved under <DATA_DIR> too, so they name the same files whatever the
command's working directory. Absolute paths remain explicit overrides.
A container sets the variable for you: EDUMATCHER_DATA_DIR is /data
inside it, bind-mounted from ~/.edumatcher/data (or
deployment/docker/data in a checkout), so the databases and logs are
ordinary files on your disk.
The container's data directory is deliberately not the installed default
~/.local/share/edumatcher. Keeping it inside the install directory makes an
install self-contained (one folder to back up or delete, separate data for
each --dir install). It also stops a container and a pipx install from
overwriting each other's deployed configuration and databases, and keeps the
container's root-owned files (Docker on Linux) out of your own data
directory. A host-side pm-* command therefore needs
EDUMATCHER_DATA_DIR=~/.edumatcher/data to read the container's data, or
must be run from ~/.edumatcher, where rule 4 finds ./data.
Core processes:
| Process | Command | Role | Required? |
|---|---|---|---|
| pm-engine | pm-engine |
Matching engine — the single writer | Yes |
| pm-alf-console | pm-alf-console --id GW01 |
ALF order entry terminal (one per trader) | At least one |
| pm-scheduler | pm-scheduler |
Drives session phase transitions | No |
| pm-viewer | pm-viewer --symbol AAPL |
Live order book display | No |
| pm-orders | pm-orders |
Cross-gateway order status monitor | No |
| pm-board | pm-board |
Full-screen multi-symbol display | No |
| pm-ticker | pm-ticker |
Scrolling market data ticker | No (needs pm-stats) |
| pm-stats | pm-stats |
OHLCV, trade, and index-level statistics to SQLite | No — but recommended immediately after pm-engine (see below) |
| pm-clearing | pm-clearing |
P&L and trade settlement | No |
| pm-audit | pm-audit |
Full event log to disk | No — but recommended immediately after pm-engine (see below) |
| pm-alf-gwy | pm-alf-gwy |
ALF TCP gateway — order entry for external bots over TCP :5565 | No |
| pm-ralf-gwy | pm-ralf-gwy |
External post-trade dissemination gateway (RALF) | No |
| pm-md-gwy | pm-md-gwy |
External market-data gateway (CALF) over TCP :5570 | No |
| pm-balf-gwy | pm-balf-gwy |
External binary order-entry gateway (BALF) over TCP :5560 | No |
| pm-dc-gwy | pm-dc-gwy |
External drop-copy gateway (DC1) — relays :5557 fills over TCP :5590 for non-ZeroMQ clients | No |
| pm-api-gwy | pm-api-gwy |
REST/WebSocket order-entry and market-data API gateway | No |
| pm-index | pm-index |
Real-time cap-weighted index calculation and dissemination | No |
| pm-log-srv | pm-log-srv |
Centralized LALF log collector — logging over TCP :5600 to log.db |
No |
Monitoring & Admin tools:
| Process | Command | Role | Required? |
|---|---|---|---|
| pm-admin | pm-admin |
Interactive admin console | No |
| pm-admin-cli | pm-admin-cli <command> |
One-shot CLI admin commands | No |
| pm-opctl-cli | pm-opctl-cli <command> |
Start/stop/monitor named process profiles (recommended way to run the stack) | Recommended |
| pm-help / pm-man | pm-help [pm-command] |
Command index and man-page reference for every pm-* command | No |
| pm-cverifier | pm-cverifier [options] <config> |
Validate engine_config.yaml (YAML, schema, semantic, completeness checks) |
No |
| pm-clearing-cli | pm-clearing-cli <command> [options] |
Read/query interface for clearing.db (includes prune) |
Optional |
| pm-eod | pm-eod run \| status \| balances |
Close a trading day: settlement, statements, housekeeping | Optional |
| pm-stats-cli | pm-stats-cli <command> [options] |
Read-only query interface for stats.db |
Optional |
| pm-audit-cli | pm-audit-cli <command> [options] |
Read-only query interface for audit JSONL log files | Optional |
| pm-audit-replay | pm-audit-replay [options] <command> |
Read-only reconstruction of the audit trail into causal episodes and stories | Optional |
| pm-index-cli | pm-index-cli <command> [options] |
Read-only query interface for index history JSONL files | Optional |
| pm-index-admin-cli | pm-index-admin-cli --id <GW_ID> <command> [options] |
One-shot CLI for index corporate actions and constituent changes | Optional |
| pm-log-cli | pm-log-cli <command> [options] |
Read-only query/troubleshooting interface for log.db (includes diagnose, prune) |
Optional |
| pm-calf-spy | pm-calf-spy [--channels CH] [--symbols SYM] [--ping-interval SEC] [--format human\|json] |
Read-only CALF protocol spy — connects to pm-md-gwy and prints every line it sends |
Optional |
| pm-ralf-spy | pm-ralf-spy --role ROLE [--channels CH] [--symbols SYM] [--ping-interval SEC] [--format human\|json] |
Read-only RALF protocol spy — connects to pm-ralf-gwy and prints every line it sends |
Optional |
| pm-dc-spy | pm-dc-spy [--gateway GW_ID] [--replay-of ID] [--format human\|json] |
Read-only drop-copy spy — connects to the engine's drop-copy PUB socket (:5557) and prints every fill event |
Optional |
Port Reference¶
Port numbers are scattered across the protocol, gateway, and configuration
chapters (for example Protocols Overview and
Examples). The table below is the single place that
collects every default port for every long-running process in one view, split
into the internal ZeroMQ bus (bound only on 127.0.0.1 by default, spoken
only between EduMatcher's own processes) and the external TCP gateways
(bound on 0.0.0.0 by default, spoken by outside clients). All ports are
configurable in engine_config.yaml; the values below are the shipped
defaults, cross-checked against src/edumatcher/config.py and each
process's own config.py.
Core processes:
| Process | Internal ZMQ port(s) | External port(s) | Protocol | Purpose |
|---|---|---|---|---|
pm-engine |
5555 PULL (bind) · 5556 PUB (bind) · 5557 PUB (bind) |
– | ZeroMQ (PULL/PUB) | Matching engine. 5555 receives all inbound commands; 5556 broadcasts all market/session events; 5557 is the dedicated drop-copy fill feed. |
pm-log-srv |
5601 PUB (bind) · 5602 PULL (bind) |
5600 |
LALF over TCP (collection) · LALF-PS over ZeroMQ (distribution) | Centralized log collector. 5600 accepts LALF log lines from every other pm-* process; 5601/5602 are the LALF-PS PUB/PULL pair that pushes live rows to log viewers/subscribers without polling log.db. |
pm-audit |
5562 REP (bind); connects out to 5556 |
– | ZeroMQ (SUB/REP) | Subscribes to every engine event and writes the full audit trail to disk. 5562 only answers pm-audit-cli health pings (EDUMATCHER_AUDIT_BIND_HOST, EDUMATCHER_AUDIT_HEALTH_PORT). |
pm-stats |
5561 REP (bind); connects out to 5555, 5556, 5558 |
– | ZeroMQ (SUB/PUSH/REP) | Records market and index statistics to stats.db. 5561 only answers pm-stats-cli health pings (EDUMATCHER_STATS_BIND_HOST, EDUMATCHER_STATS_HEALTH_PORT). |
pm-clearing |
5563 REP (bind); connects out to 5556 |
– | ZeroMQ (SUB/REP) | Subscribes to trade events for P&L and settlement. 5563 only answers pm-clearing-cli health pings (EDUMATCHER_CLEARING_BIND_HOST, EDUMATCHER_CLEARING_HEALTH_PORT). |
pm-scheduler |
connects out to 5555 |
– | ZeroMQ (PUSH) | Drives session-phase transitions by pushing commands to the engine. Binds no port of its own. |
pm-md-gwy |
connects out to 5556, 5558 |
5570 |
CALF over TCP | External market-data gateway. Consumes engine and index PUB feeds internally and republishes top/book/trade/state channels to external CALF clients on 5570. |
pm-ralf-gwy |
connects out to 5556 |
5580 |
RALF over TCP | External post-trade dissemination gateway for clearing, drop-copy, and audit consumers (role-gated). |
pm-dc-gwy |
connects out to 5557 |
5590 |
DC1 over TCP | Relays the engine's 5557 drop-copy feed as DC1 text lines to plain-TCP clients that don't speak ZeroMQ. |
pm-api-gwy |
connects out to 5555, 5556 |
8080 (code default; example configs may assign additional instances other ports, e.g. 8081) |
HTTP/REST + WebSocket | REST/WebSocket gateway; translates HTTP/WS requests into the same ZMQ order flow used by pm-alf-console. Each named api_gateways instance in config gets its own port. |
pm-alf-gwy |
connects out to 5555, 5556, 5557 |
5565 |
ALF over TCP | External ALF order-entry gateway for bots/remote clients (same protocol as pm-alf-console, over TCP instead of stdin/stdout). |
pm-balf-gwy |
connects out to 5555, 5556 |
5560 |
BALF (binary) over TCP | External binary order-entry gateway for low-latency programmatic clients. |
pm-index |
5558 PUB (bind) · 5559 PULL (bind) · 5564 REP (bind); connects out to 5556 |
– | ZeroMQ (PUB/PULL/REP) | Real-time cap-weighted index calculation. 5558 broadcasts index.update; 5559 receives operator commands and history requests; 5564 only answers pm-index-cli health pings (EDUMATCHER_INDEX_BIND_HOST, EDUMATCHER_INDEX_HEALTH_PORT). No external protocol of its own — external consumers reach index data via pm-md-gwy/CALF. |
pm-monitor |
5610 PULL (bind) · 5611 PUB (bind); connects out to 5556 |
– | ZeroMQ (PULL/PUB/SUB) | Health collector. 5610 receives the health.beacon every long-running process pushes every 2 s; 5611 publishes health.snapshot (1 Hz) and health.event to pm-monitor-cli and the Operator Dashboard. Loopback by default (monitor.bind_address, EDUMATCHER_GATEWAY_BIND_HOST). See Health Monitoring. |
pm-market-sim |
5553 PUB (bind) · 5554 PULL (bind); connects out to 5555, 5556 |
– | ZeroMQ (PUB/PULL) | The market model: every symbol's true value, and news. 5553 publishes sim.value, the sim.state heartbeat and news.event; 5554 takes sim.command from ADMIN participants. Loopback only by default (EDUMATCHER_SIM_BIND_HOST). See The Market Model. |
Besides the connections listed, every process in the table (and the bots
pm-mm-bot and pm-ai-swarm) connects a ZeroMQ PUSH socket to pm-monitor
on 5610 (monitor.host/pull_port) and sends its health beacon there
every two seconds. The send never blocks: when pm-monitor is not running
the beacons are dropped and counted.
Internal-only utilities (ZeroMQ clients; no listening port of their own):
| Process | Internal ZMQ port(s) used | Protocol | Purpose |
|---|---|---|---|
pm-alf-console |
connects out to 5555, 5556 |
ZeroMQ (PUSH/SUB) | Interactive ALF order-entry terminal for a human trader; the same PUSH/SUB pattern as a gateway, without a TCP front end. |
pm-viewer |
connects out to 5556 |
ZeroMQ (SUB) | Live single/multi-symbol order book display. |
pm-board |
connects out to 5555, 5556 |
ZeroMQ (PUSH/SUB) | Full-screen multi-symbol market board. |
pm-ticker |
connects out to 5556, 5553 |
ZeroMQ (SUB) | Scrolling market-data ticker (reads recent trades from pm-stats's stats.db as well); headlines from pm-market-sim. |
Reading the table
"Internal ZMQ port(s)" are ZeroMQ endpoints intended for EduMatcher's own
processes on the same host (or a trusted internal network) — by default
bound to 127.0.0.1. "External port(s)" are TCP/HTTP listeners meant for
outside clients — by default bound to 0.0.0.0. Every port shown is a
default; production or multi-host deployments should bind internal ports
to a private interface and firewall external ports as appropriate. See
The Configuration Workflow for how to override any of these.
The same table, for the config you actually deployed
This table lists the shipped defaults. To see the ports one particular
configuration resolves to — including which were set explicitly and which
fell back to a default — run
pm-config-show. It derives its port map
from edumatcher/gateway_ports.py, the same table pm-cverifier uses for
its M018 collision check.
Web applications (web-apps/; each a Node bridge serving a browser app,
none part of engine_config.yaml). The container port is what the stacks
publish (deployment/docker make up-all, deployment/curl); make dev
uses the web and bridge ports. They follow one scheme: 809N, 819N, 519N.
| Application | Container port | Dev web / bridge | Talks to | Guide |
|---|---|---|---|---|
TapeDeck (pm-terminal) |
8090 |
8190 / 5190 |
pm-md-gwy 5570, pm-api-gwy 8081 |
TapeDeck |
Log Operator Console (pm-log-ui) |
8091 |
8191 / 5191 |
pm-log-srv 5601/5602, log.db read-only |
Log Operator Console |
Configuration GUI (config-gui) |
8092 |
8192 / 5192 |
nothing | Configuration GUI |
Trading GUI (pm-trading-ui) |
8093 |
8193 / – |
pm-api-gwy 8080 |
Trading Platform GUI |
Order Book Viewer (pm-book) |
8094 |
8194 / 5194 |
pm-api-gwy 8081 |
Order Book Viewer |
Operator Dashboard (pm-dash) |
8095 |
8195 / 5195 |
pm-monitor 5611, metrics.db read-only |
Operator Dashboard |
Setup and configuration tools:
| Process | Command | Role | Required? |
|---|---|---|---|
| pm-setup | pm-setup |
Bootstrap working directory and runtime files | Recommended first run |
| pm-config-gen | pm-config-gen [options] |
Generate engine_config.yaml from CLI options |
Optional |
| pm-cverifier | pm-cverifier CONFIG_FILE |
Validate a config across four layers | Recommended |
| pm-config-deploy | pm-config-deploy SOURCE |
Compile and install the config every process reads | Required to run |
| pm-config-show | pm-config-show [options] |
Display the deployed config, or render it as a PDF | Optional |
| pm-new-symbol / pm-ipo | pm-new-symbol --symbol SYM ... |
List a new symbol (an IPO) in the configuration and redeploy it | Optional |
| pm-valuation | pm-valuation [options] |
Interview-driven DCF valuation and IPO simulation for a fictive company | Optional |
Optional AI trader tools:
| Process | Command | Role | Required? |
|---|---|---|---|
| pm-ai-trader | pm-ai-trader |
Single AI trading bot gateway | No |
| pm-ai-swarm | pm-ai-swarm |
Coordinated multi-agent AI trading swarm | No |
| pm-market-sim | pm-market-sim |
Market model: true values and news for the AI traders | No |
| pm-news | pm-news --id ADMIN_ID … |
Instructor's news desk for pm-market-sim |
No |
| pm-mm-bot | pm-mm-bot --symbol SYM |
Autonomous market-maker bot for a single symbol | No |
Start the engine first
The engine binds the ZeroMQ sockets. All other processes connect to those sockets. If a process starts before the engine is ready, it will either fail immediately or silently lose its first messages.
Recommended startup sequence
For a deterministic first run, use this order:
- Bootstrap session files:
- Generate a config (or reuse/edit the sample config):
- Use the operational control
pm-opctl-clito start the system in the correct order with: Then usepm-opctl-cli listto list the status of each process, andpm-opctl-cli stopto shut the profile down again. See Running the Exchange. - Start remaining optional observers/tools as needed (
pm-board,pm-viewer,pm-ticker).
pm-engine — Matching Engine¶
The heart of the system — receives orders, matches them, publishes events.
pm-engine [-v|-vv] [--log-level LEVEL] [-q] [--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase log verbosity (-v → INFO: startup/lifecycle messages; -vv → DEBUG: per-order/per-trade detail) |
-q / --quiet |
off | Explicit WARNING (same as the default) |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
See Running the Engine → Logging levels for the full flag reference and example output.
Expected runtime input arguments:
None. pm-engine is a long-running background process after startup.
Startup behaviour:
1. Reads the deployed <DATA_DIR>/ref_data/engine_config.json — registers the symbol and gateway allowlists, session settings, collars, circuit breakers and tuning
2. Binds the main ZMQ PULL :5555 and PUB :5556 sockets during engine initialisation
3. On run(), bumps the run sequence (engine_run_seq.json) so trade ids are unique across restarts
4. Publishes system.startup first: engine version, git commit, the SHA-256 and full text of the configuration, and the persisted files it is about to load. An audit reader takes it as the start of a run
5. Restores persisted resting orders (gtc_orders.json), combos (gtc_combos.json) and book statistics (book_stats.json). Both GTC and DAY orders are restored, in each book's saved priority order; a DAY order whose business day has passed is discarded. Publishes system.startup_recovery with the counts
6. Applies config-driven seeds (book-statistics gaps, market-maker quotes, market-maker combos)
7. Publishes what restoring left in the books' trace, the collar bands (risk.band.<SYM>) and a system.book_checkpoint.<SYM> for every book (reason STARTUP)
8. Binds the dedicated drop-copy PUB :5557 socket (if that fails, it logs a warning and runs without drop copy), then enters the poll loop
The loop waits up to 200 ms for a message, handles it completely, then runs a
maintenance tick (snapshots, timers, persistence checkpoint every 5 s). Every
decoded inbound command (not the *_request queries) is re-published once,
unchanged, so that pm-audit records what the exchange was asked to do, not just
what it did, including a cancel the engine ignored.
Shutdown (Ctrl-C / SIGTERM):
1. Serialises every resting GTC and DAY order to <DATA_DIR>/gtc_orders.json. A process exit is not a day boundary, so DAY orders are not expired here; real end-of-day expiry happens when the scheduler moves the session to CLOSED
2. Saves GTC combos (gtc_combos.json) and book statistics (book_stats.json)
3. Publishes a system.book_checkpoint.<SYM> for every book (reason EOD)
4. Publishes system.eod with the final book snapshot for every symbol, so subscribers record closing prices
5. Closes the sockets
A message that cannot be decoded at all is discarded and counted rather than ending the loop, so a malformed packet cannot skip the shutdown persistence.
Messages received (PULL :5555). The engine dispatches 38 inbound topics; a
topic it does not recognise is dropped, counted and answered with a
system.diagnostic (UNROUTED_TOPIC). A handler that raises is answered with a
system.diagnostic (DISPATCH_ERROR) and a reject to the submitting gateway.
| Group | Topics | Source |
|---|---|---|
| Orders | order.new, order.cancel, order.amend, order.oco, order.oco_cancel, order.combo, order.combo_cancel |
Gateways |
| Quotes | quote.new, quote.cancel |
Market-maker gateways |
| Session of a gateway | system.gateway_connect, system.gateway_disconnect, system.gateway_heartbeat |
Gateways |
| Queries | system.symbols_request, system.reference_request, book.snapshot_request, order.orders_request, order.price_level_orders_request, system.quote_bootstrap_request, system.quote_legs_request, system.position_request |
Gateways, viewers, pm-stats |
| Admin queries | system.session_state_request, system.session_schedule_request, system.gateways_request, system.volume_request, system.halt_status_request, system.risk_state_request |
pm-admin, pm-admin-cli, pm-api-gwy |
| Risk controls | risk.kill_switch, risk.kill_switch_gateway, risk.kill_switch_global, risk.circuit_breaker_halt_all, risk.circuit_breaker_resume_all, risk.symbol_halt, risk.symbol_resume, risk.cancel_symbol, risk.force_uncross |
Admin gateways |
| Session control | session.transition, session.next |
Scheduler, admin |
| Reference data | system.reference_reload |
pm-api-gwy |
Messages published (PUB :5556). Per-gateway topics end in .{GW_ID}; the
payload of every topic is defined in the Message
Reference.
| Group | Topics | Purpose |
|---|---|---|
| Order events | order.ack, order.fill, order.cancelled, order.expired, order.amended, order.triggered, order.orders, order.price_level_orders (all .{GW_ID}) |
Private to the owning gateway. order.fill carries one VWAP fill per order per dispatch; order.triggered reports a stop that fired |
| Combo, OCO, quote | combo.ack/status/cancel_ack, oco.ack/cancelled/cancel_ack, quote.ack/status/cancel_ack (all .{GW_ID}) |
Lifecycle of structured orders |
| Market data | trade.executed, book.{SYMBOL}, depth.{SYMBOL}, auction.indicative.{SYMBOL}, auction.result.{SYMBOL} |
Public tape and book. Snapshots are throttled to snapshot_interval_sec |
| Session and risk | session.state, circuit_breaker.halt/extend/resume.{SYMBOL}, risk.band.{SYMBOL}, acknowledgements risk.*_ack.{GW_ID}, session.transition_ack.{GW_ID} |
Phase changes, halts, collars, admin acknowledgements |
| Admin | admin.action.{GW_ID} |
What one admin command acted on |
| System replies | system.gateway_auth, system.gateway_bye, system.symbols, system.reference, system.gateways, system.volume, system.halt_status, system.risk_state, system.session_status, system.session_schedule, system.position_snapshot, system.quote_bootstrap, system.quote_legs, system.reference_reload_ack (all .{GW_ID}) |
Replies to the queries above |
| Lifecycle and audit | system.startup, system.startup_recovery, system.recovery_item, system.book_checkpoint.{SYMBOL}, engine.trace.{SYMBOL}, system.diagnostic, system.eod |
Run start and recovery, book checkpoints, queue-position trace, engine faults, end of day |
Messages published (drop-copy PUB :5557):
| Topic | Purpose |
|---|---|
drop_copy.event.{GW_ID} |
Fill event with sequence number and nanosecond timestamp, filtered per gateway |
drop_copy.replay.{RECIPIENT_ID} |
Replay of buffered events to a consumer that asks for them |
The drop-copy socket is bound after startup (see step 8). See Drop-Copy Feed for the subscription protocol and replay support.
Warning
Start the engine first — gateways and subscribers will fail to connect otherwise.
pm-alf-console — User Gateway¶
One instance per user. Accepts ALF commands on stdin. See ALF Protocol Reference.
Startup options:
| Flag | Required | Description |
|---|---|---|
--id |
Yes | Unique gateway identifier (e.g. GW01, ALICE) |
--drop-copy |
No | Enable the drop-copy relay on startup (equivalent to sending DC\|STATE=ON immediately after connecting); default off — see ALF Console → DC |
--log-level |
No | Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
No | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
No | Reduce output to warnings/errors |
--log-target |
No | Where operational log records go: server (auto-detected pm-log-srv), stdout, or file (default server) |
--log-file |
No | Operational log file path — required when --log-target file |
--log-failover-timeout |
No | Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable (default 30 from config) |
--version |
No | Print version and exit |
Expected runtime input arguments:
NEW|...order-entry commands (includingLIMIT,MARKET,STOP,STOP_LIMIT,FOK,ICEBERG,IOC,TRAILING_STOP)AMEND|...,CANCEL|...QUOTE|...,QUOTE_CANCEL|...QLEGS|...(market-maker quote-leg monitor with fill flags)ORDERS,SYMBOLS,HELP,EXIT,QUITKILL|...for gateway-scoped kill-switch actions
Connection behaviour:
1. Sends system.gateway_connect to the engine
2. Waits for system.gateway_auth.<GW_ID>
3. Enters command loop only if accepted
If the ID is not listed in engine_config.yaml under participants, connection
is refused and the gateway exits.
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
system.gateway_connect |
Authentication request |
order.new |
Submit order |
order.cancel |
Cancel order |
order.combo |
Submit combo |
order.combo_cancel |
Cancel combo |
order.orders_request |
Request order list |
system.symbols_request |
Request symbol list |
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
system.gateway_auth.{own GW_ID} |
Authentication reply |
order.ack.{own GW_ID} |
Order acknowledgements |
order.fill.{own GW_ID} |
Fill notifications |
order.amended.{own GW_ID} |
Successful amend notifications |
order.cancelled.{own GW_ID} |
Cancel confirmations |
order.expired.{own GW_ID} |
Expiry notifications |
order.orders.{own GW_ID} |
Order list replies |
combo.ack.{own GW_ID} |
Combo acknowledgements |
combo.status.{own GW_ID} |
Combo status updates |
oco.ack.{own GW_ID} |
OCO creation acknowledgements |
oco.cancelled.{own GW_ID} |
OCO sibling-cancel notifications |
quote.ack.{own GW_ID} |
Quote acknowledgements |
quote.status.{own GW_ID} |
Quote lifecycle updates |
risk.kill_switch_ack.{own GW_ID} |
Kill-switch acknowledgement |
system.symbols.{own GW_ID} |
Symbol list reply |
trade.executed |
Global trade feed for last-price / P&L display |
See the ALF Console for the full command list.
pm-alf-gwy — ALF TCP Gateway¶
Accepts ALF order-entry commands from external bots and remote processes over a
plain TCP connection. Uses the same ALF command vocabulary as pm-alf-console
but is designed for programmatic clients, not interactive terminals. One
connection per gateway ID; all configured participants IDs may connect.
Startup options:
| Flag | Default | Description |
|---|---|---|
--bind |
from config / 0.0.0.0 |
TCP bind address for external clients |
--port |
from config / 5565 |
TCP listen port for ALF clients |
--engine-host |
from config | Override engine host for ZMQ ports 5555 / 5556 |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
No terminal input. External clients connect over TCP and send ALF lines:
HELLO|CLIENT=...|PROTO=ALF1|ID=<gateway-id>(must be first)NEW|...,AMEND|...,CANCEL|...QUOTE|...,QUOTE_CANCEL|...KILL[|SYM=...]SYMBOLS,ORDERS,QBOOT[|SYM=...]PING,EXIT/QUIT
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
system.gateway_connect |
Authentication request |
system.gateway_disconnect |
Graceful disconnect notice |
order.new |
Submit order |
order.cancel |
Cancel order |
order.amend |
Amend order |
order.combo |
Submit combo |
order.combo_cancel |
Cancel combo |
order.oco |
Submit OCO pair |
order.oco_cancel |
Cancel OCO pair |
order.orders_request |
Request order list |
quote.new |
Submit / replace MM quote |
quote.cancel |
Cancel MM quote |
risk.kill_switch |
Gateway kill-switch |
system.symbols_request |
Request symbol list |
system.quote_bootstrap_request |
Request quote bootstrap state |
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
system.gateway_auth.{GW_ID} |
Engine authentication reply |
order.ack.{GW_ID} |
Order accepted or rejected |
order.fill.{GW_ID} |
Partial or full fill |
order.amended.{GW_ID} |
Amend confirmation |
order.cancelled.{GW_ID} |
Cancel confirmation |
order.expired.{GW_ID} |
TIF expiry |
order.orders.{GW_ID} |
Order list reply |
combo.ack.{GW_ID} |
Combo accepted or rejected |
combo.status.{GW_ID} |
Combo lifecycle change |
oco.ack.{GW_ID} |
OCO accepted or rejected |
oco.cancelled.{GW_ID} |
OCO sibling-cancel notification |
quote.ack.{GW_ID} |
Quote accepted or rejected |
quote.status.{GW_ID} |
Quote lifecycle change |
risk.kill_switch_ack.{GW_ID} |
Kill-switch acknowledgement |
system.symbols.{GW_ID} |
Symbol list reply |
system.quote_bootstrap.{GW_ID} |
Quote bootstrap state reply |
session.state |
Session phase changes (broadcast) |
trade.executed |
Trade events (broadcast) |
circuit_breaker.halt.* |
Symbol halt events (broadcast) |
circuit_breaker.resume.* |
Symbol resume events (broadcast) |
See ALF TCP Gateway for operational usage, command reference, and client examples.
pm-viewer — Order Book Viewer¶
Live terminal view of a single symbol's order book. Draws a full-screen bordered box on the alternate terminal buffer that repaints cleanly on resize.
pm-viewer --symbol AAPL [--depth N] [--db data/stats.db] \
[--text-color COLOR] [--zebra-lines] [--zebra-lines-color COLOR]
Startup options:
| Flag | Default | Description |
|---|---|---|
--symbol / -s |
required | Symbol to watch |
--depth / -d |
fit to terminal | Max price levels to display per side (default: fills available height) |
--db |
data/stats.db |
Stats SQLite DB — seeds session OHLC and previous-close at startup |
--text-color |
white |
Body text color for the order book tables — #rrggbb hex or a Rich color name |
--zebra-lines |
off | Shade every second row of the Bids/Asks/Trades tables with a background tint |
--zebra-lines-color |
grey19 |
Background tint used by --zebra-lines — #rrggbb hex or a Rich color name; has no effect unless --zebra-lines is given |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
| Key | Action |
|---|---|
s or F1 |
Open the symbol picker and switch to another symbol |
Ctrl-C |
Quit |
Inside the picker: type to narrow the list by prefix, ↑/↓ to move,
Enter to switch, Esc to go back to the book unchanged. Both keys are
advertised in the footer that crosses the bottom border.
Display:
- Two-line header: last price with directional colour/arrow, change / %, session OHLC seeded from pm-stats DB, previous-close, bid/ask, spread, volume, and a live clock
- Three equal-height columns filling the terminal: Bids (with proportional depth bars), Asks, and Trades (newest first, uptick/downtick coloured)
- Bids and asks show proportional depth micro-bars per price level
- The frame border colour reflects the day's direction (green/red)
- Repaints the full frame on every terminal resize — no leftover garbage
Iceberg orders
Iceberg orders only show their displayed_qty (the visible peak) in the viewer.
The hidden quantity is completely invisible — this is by design and demonstrates the
privacy feature of iceberg orders.
Colors on a dark terminal
Body text in the Bids/Asks/Trades tables defaults to white; override it with
--text-color if that doesn't fit your terminal's theme (e.g. a light-background
profile). Alternating-row shading is off by default and opt-in via --zebra-lines,
which tints every second row with a background color (default grey19, a dark grey)
without changing that row's text color — customize it with --zebra-lines-color.
Both color options accept a #rrggbb hex code or any
Rich color name.
Switching symbol without restarting
Press s (or F1) to pick a different symbol from a popup list. The
viewer asks the engine for the current symbol universe each time the
picker opens, resubscribes to the chosen symbol's book, and reloads that
symbol's session statistics — so one viewer can follow a whole class
around the market. The picker needs a real terminal; when stdin is piped
or redirected the viewer behaves exactly as it always did.
Run multiple viewers simultaneously for different symbols:
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
book.{SYMBOL} |
Book updates for the watched symbol |
system.symbols.{GW_ID} |
Symbol list backing the interactive picker |
session.state |
Session phase changes (displayed in header) |
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
book.snapshot_request |
Requests initial book state on startup, and again after a symbol switch |
system.symbols_request |
Requests the symbol list when the picker is opened |
pm-orders — Order Status Monitor¶
Live cross-gateway view of all orders in the system.
Startup options:
| Flag | Default | Description |
|---|---|---|
--gateway / -g |
(all) | Filter to a single gateway |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Displays a live table with columns:
ID | Gateway | Symbol | Side | Type | TIF | Qty | Remaining | Price | Status | Updated
Status colours: green=NEW, yellow=PARTIAL, bright green=FILLED, red=REJECTED/CANCELLED, dim=EXPIRED.
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
order. (prefix) |
All order events (ack, fill, cancelled, expired) |
combo. (prefix) |
Combo ack and status events |
session.state |
Session phase changes |
pm-audit - Event Logger¶
Records every message on the bus to a day-closing log file; closed days are compressed to one zstd archive each.
pm-audit [--audit-log-file data/audit.log] [--terminal] [--buffer-size 100] [--flush-interval 10] [--max-file-mb 10] [--keep-days 0] [--log-level LEVEL] [-v|-vv] [-q]
Startup options:
| Flag | Default | Description |
|---|---|---|
--audit-log-file |
data/audit.log |
Audit-trail output log file path |
--terminal / -t |
off | Also print each entry to stdout |
--buffer-size |
100 | Number of messages to buffer in memory before writing to disk |
--flush-interval |
10.0 | Maximum seconds to wait before flushing buffer to disk |
--max-file-mb |
10 | Close the audit log as a day part when it reaches this size in MB |
--keep-days |
0 | Delete daily archives older than N days (0 keeps all) |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) | Seconds to wait before falling back to a local log file if pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Buffering behaviour:
To reduce disk wear, pm-audit buffers messages in memory before writing them
to disk in batches. The buffer is flushed to disk when either:
- The buffer reaches
--buffer-sizemessages (e.g., 100 messages), or --flush-intervalseconds have elapsed since the last flush (e.g., 10 seconds)
On shutdown (Ctrl-C, SIGINT, or SIGTERM), any remaining buffered messages are automatically flushed to disk before the process exits, ensuring no data is lost.
Log format (one entry per line):
[2026-04-29T14:32:01.123] [trade.executed] {"id": "...", "symbol": "AAPL", ...}
[2026-04-29T14:32:01.125] [book.AAPL] {"bids": [...], "asks": [...], ...}
Log files rotate at 10 MB with 5 backups kept.
Use --terminal during demos so the class can see every event in real time.
Use -vv or --log-level DEBUG when you want aggregated operational summaries
about buffering, flushes, decode errors, and broad topic-family mix without
changing the raw event log format.
Examples:
# Default: buffer 100 messages, flush every 10 seconds
pm-audit
# High-traffic scenario: larger buffer, longer flush interval
pm-audit --buffer-size 500 --flush-interval 30
# Low-latency: smaller buffer, aggressive flushing
pm-audit --buffer-size 10 --flush-interval 1
# Disable buffering entirely: flush every message immediately
pm-audit --buffer-size 1 --flush-interval 0.1
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
| (empty prefix — receives all messages) | Records everything published on the PUB socket |
pm-audit-cli — Audit Log Query CLI¶
pm-audit-cli is a read-only offline tool: it reads JSONL files directly from disk, runs one query, prints output, and exits. No ZeroMQ connection required — except for health, which pings the running pm-audit on port 5562.
pm-audit-cli [--log-file data/audit.log] [--log-dir PATH] [--format table|json|csv] [--no-header] [--use-index PATH] COMMAND [options]
Global options:
| Flag | Default | Description |
|---|---|---|
--log-file PATH |
data/audit.log |
Primary audit log file; reads the live trail (unarchived day parts, then this file). An archive or day part named here is read alone |
--log-dir PATH |
directory of --log-file |
Directory holding the audit/ day parts and archives |
--format |
table |
Output format: table, json, or csv |
--no-header |
off | Suppress header row in CSV output |
--use-index PATH |
auto-detected | Path to SQLite index file (auto-detected from --log-dir when not specified; ignored for commands that don't support it) |
--version |
— | Print version and exit |
Subcommands:
| Subcommand | Default limit | Purpose | Key filters |
|---|---|---|---|
events |
100 | Search log entries by topic, gateway, symbol, and time range | --topic, --gateway, --symbol, --date, --from, --to, --reverse |
orders |
100 | Order lifecycle events for specific order IDs or filters | --id (repeatable), --gateway, --symbol, --date, --from, --to |
trades |
100 | Trade executions | --symbol, --gateway, --buy-gateway, --sell-gateway, --min-price, --max-price, --min-qty, --date, --from, --to, --reverse |
topics |
— | List topics present in logs with event counts | --date, --from, --to, --prefix, --sort (count|alpha) |
gateways |
— | Gateway activity summary | --date, --from, --to, --min-events |
timeline |
500 | Raw chronological event stream for session replay | --from, --to, --topic, --gateway, --symbol |
stats |
— | Summary statistics about audit log files | --verbose |
index |
— | Build or update the optional SQLite index for faster queries | --output PATH |
archive |
— | Compress closed days' parts into one verified audit-D.log.zst per day |
--date D or --pending, --level N (default 9), --keep-days N |
health |
— | Check the pm-audit process and ping its health socket; --extended adds the audit log read/write and last-entry check |
--extended, -v/--verbose, -q/--quiet (print nothing; exit 0 when healthy, -1 otherwise) |
No-row behaviour:
table: printsNo rows found.json: prints[]csv: prints only header row unless--no-headeris set
pm-audit-replay — Audit Replay¶
Reconstructs the causal structure of the audit trail and narrates it: which
order caused which trade, which kill switch cancelled what, where the log
disagrees with itself. It reads the same JSONL files as pm-audit-cli,
read-only, and keeps a derived episode index in its own SQLite file. It joins
no ZeroMQ socket.
pm-audit-replay [global options] COMMAND [command options]
pm-audit-replay --last 15m stream
pm-audit-replay story --order 7f3a2c
pm-audit-replay anomalies --severity warn
Commands:
| Command | Purpose | Command options |
|---|---|---|
stream |
Narrate a window chronologically (the default view) | — |
story |
One entity and everything causally connected to it (needs the index) | exactly one selector: --order, --trade, --quote, --oco, --combo, --command, --client-tag, --chain ULID, --msg ULID; --depth N (default 2; not with --chain), --strict-causality |
digest |
One paragraph per episode, most significant first | --top N, --significance {anomalies,notional,qty,duration} |
episodes |
The index as a table, one row per episode | --outcome OUTCOME (repeatable) |
anomalies |
Every finding, worst first, with the story command to run next |
--severity {error,warn,info} (a floor; default info) |
stats |
Link confidence and envelope coverage over the window | — |
index |
Build or refresh the episode index | --stats |
Global options (accepted before or after the command):
| Flag | Default | Description |
|---|---|---|
--log-file PATH |
<DATA_DIR>/audit.log |
Audit log to read: the live trail (unarchived day parts, then this file). An archive or day part named here is read alone |
--db PATH |
<DATA_DIR>/audit_replay.db |
Episode index |
--no-index |
off | Stream without building or reading an index |
--rebuild |
off | Rebuild the index before rendering. There is no incremental mode; the index rebuilds itself whenever the reconstruction rules, the detection settings or the source logs change |
--from ISO_TS, --to ISO_TS |
— | Window bounds (ISO-8601 or YYYY-MM-DD) |
--date YYYY-MM-DD |
— | One trading day, in the session timezone. Cannot be combined with --from/--to or with an archive named by --log-file |
--last DURATION |
— | Relative window, e.g. 15m, 2h, 1d. Cannot be combined with --from, --to or --date |
--symbol, --gateway, --kind |
— | Restrict to symbols, gateways or episode kinds; each repeatable |
-q / -v / -vv / -vvv |
level 1 | Detail level: -q is level 0 (episode outcomes only); each -v adds one level, up to -vvv |
--format {text,ndjson,json,markdown,csv} |
text |
Output format (a command that cannot produce a format exits 2) |
--show-source |
off | Append audit.log:LINE to every narrated line |
--show-units |
off | Append unit provenance to every price |
--explain |
off | Show link evidence and confidence inline |
--id-len N\|full |
6 |
Order-id abbreviation |
--actor-style {id,descriptive} |
id |
Actor naming |
--tz TZ |
UTC |
Zone for rendered timestamps |
--reorder-window SPEC |
2000/5s |
Reorder buffer, a message count or a duration |
--no-color / --force-color |
auto | Never / always emit ANSI colour (--force-color for ... \| less -R) |
--strict |
off | Also report findings that are expected at a window edge (ARRIVAL_SEQ_GAP) |
--clock-skew-warn SECONDS |
0.1 |
Engine-versus-receipt clock difference worth reporting |
--version |
— | Print version and exit |
Exit codes: 0 success; 1 nothing to narrate, or no audit log at the
given path; 2 argument error.
See Audit Replay for how causality is recovered, what each anomaly means and worked examples.
pm-clearing — Clearing & P&L¶
SQLite-backed clearing writer for P&L and position state.
pm-clearing [--datapath PATH] [--db-name NAME] [--flush-size N] [--flush-interval SEC] [--print-every N] [--retention-days N] [--sql-trace] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--datapath |
data dir from EDUMATCHER_DATA_DIR |
Data directory or explicit .db path |
--db-name |
clearing.db |
SQLite filename when --datapath is a directory |
--flush-size |
100 |
Flush immediately when buffered trades reaches N |
--flush-interval |
5.0 |
Flush interval in seconds when buffer is non-empty |
--print-every |
100 |
Print in-memory P&L snapshot every N trades (0 disables) |
--retention-days |
90 |
Prune trade_events rows older than N days on startup and at every close of the market (0 disables pruning) |
--sql-trace |
off | Log executed SQLite SQL statements from the clearing writer connection |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
pm-clearing-cli global options include --raw-output to disable display
normalization and print raw tick-unit values for price-derived fields.
Expected runtime input arguments:
None.
On startup, pm-clearing applies schema migrations and prunes trade_events
older than the retention window (default: 90 days). Writes are batched into
single SQLite transactions to update:
trade_eventsgateway_symbol_positionsgateway_daily_summary
trade.executed.tick_decimals is persisted so downstream output can normalize
price-derived values for table/JSON/CSV rendering.
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
trade.executed |
Every matched trade pair (including tick_decimals) — drives P&L calculations |
system.eod |
End-of-day marks and the session-close session_events row |
system.gateway_auth. |
Gateway connect (accepted) — opens a gateway_sessions row |
system.gateway_bye. |
Gateway disconnect — closes the matching gateway_sessions row |
See Positions and P&L for the full accounting model.
pm-clearing-cli - Clearing Query CLI¶
pm-clearing-cli is a command-line interface for querying clearing.db
without writing SQL manually. It supports both read-style reporting commands
and a maintenance command (prune) that deletes old raw trade rows.
pm-clearing-cli [--datapath PATH] [--db-name clearing.db] [--format table|json|csv] [--no-header] [--raw-output] COMMAND [options]
Unlike pm-clearing, this is a one-shot tool: it runs one command, prints
output, and exits. Every verb works on clearing.db directly except health,
which also pings the running pm-clearing on port 5563.
Global options:
| Flag | Default | Description |
|---|---|---|
--datapath PATH |
resolved from EDUMATCHER_DATA_DIR |
Data directory or explicit .db file path |
--db-name NAME |
clearing.db |
SQLite filename when --datapath is a directory |
--format |
table |
Output format: table, json, or csv |
--no-header |
off | Suppress header row in CSV output |
--raw-output |
off | Disable tick-decimal normalization and emit raw tick-unit values |
--version |
— | Print version and exit |
Subcommands:
| Subcommand | Default limit | Purpose | Typical filters |
|---|---|---|---|
gateways |
1000 | Gateway-level realized/unrealized/total P&L totals | --gateway |
positions |
10000 | Current open position state by gateway and symbol | --gateway, --symbol |
pnl |
10000 | Realized/unrealized/total P&L rows per gateway and symbol | --gateway, --symbol |
daily |
1000 | Daily rollup summary rows | --gateway, --symbol, --date, --from, --to |
trades |
200 | Raw trade-event rows | --gateway, --symbol, --date, --from, --to |
exposure |
1000 | Net/gross notional exposure and P&L | --gateway, --symbol, --sort |
symbols |
1000 | Symbol-level totals and open exposure snapshot | --date, --from, --to, --sort |
dates |
1000 | Available trade dates (optionally with totals) | --gateway, --symbol, --from, --to, --with-totals |
health |
n/a | Check the pm-clearing process and ping its health socket; --extended adds the clearing DB integrity and read/write check, and --extended -v the row counts, flush metadata and journal mode |
--extended, -v/--verbose, -q/--quiet (print nothing; exit 0 when healthy, -1 otherwise) |
reconcile |
n/a | Compares raw trade_events vs daily summary (both buy and sell sides; also reports summary-only keys) |
--gateway, --symbol, --from, --to, --retention-days |
sessions |
— | Gateway connection and disconnection history | --gateway, --from, --to, --connected-only, --limit (default 500) |
eod |
100 | End-of-day sentinel events written by pm-clearing on system.eod |
--from, --to, --limit |
prune |
n/a | Deletes old trade_events rows by retention window |
--days, --dry-run |
daily rows are per gateway, while symbols sums both counterparties'
participant-side activity. A single 800-share trade therefore contributes 800
to each gateway's daily row and 1,600 to the symbol's traded_qty; use
pm-stats-cli daily for market volume counted once per executed trade.
Sort options:
| Command | Allowed --sort values |
|---|---|
exposure |
gross_notional, net_notional, realized_pnl, unrealized_pnl, total_pnl |
symbols |
symbol, traded_qty, traded_notional, realized_pnl, open_net_qty |
Examples:
# Gateway totals (table)
pm-clearing-cli gateways
# One gateway in JSON
pm-clearing-cli --format json gateways --gateway GW_A
# Current positions as CSV (normalized display values)
pm-clearing-cli --format csv positions --gateway MM01
# Emit raw tick-unit values instead of normalized display values
pm-clearing-cli --format json --raw-output positions --gateway MM01
# Daily summary for one date range
pm-clearing-cli daily --from 2026-07-01 --to 2026-07-05
# Raw trades for one symbol
pm-clearing-cli trades --symbol AAPL --limit 50
# Risk view by total P&L
pm-clearing-cli exposure --sort total_pnl
# Date discovery with aggregate totals
pm-clearing-cli dates --with-totals
# Data-consistency check
pm-clearing-cli reconcile --from 2026-07-01 --to 2026-07-05
# Retention maintenance (dry run then execute)
pm-clearing-cli prune --days 90 --dry-run
pm-clearing-cli prune --days 90
No-row behaviour:
table: printsNo rows found.(exceptreconcile, which printsOK — no discrepancies found.)json: prints[]csv: prints only header row unless--no-headeris set
See Positions and P&L for the accounting model and schema-level details.
pm-stats — Statistics Recorder¶
Records market statistics for every symbol, plus index level history, to a
SQLite database (data/stats.db). This is the queryable time-series home for
both trading statistics and index levels — pm-index's own JSONL file only
retains structural/corporate-action audit records, not level ticks.
pm-stats [--db data/stats.db] [--snapshot-interval SEC] \
[--keep-days N] [--keep-order-events-days N] \
[--human-gateways GLOB,...] [--keep-human-order-events-days N] \
[--sql-trace] \
[--log-level LEVEL] [-v|-vv] [-q]
Startup options:
| Flag | Default | Description |
|---|---|---|
--db |
data/stats.db |
SQLite database file path |
--snapshot-interval |
900 (15 min) |
Seconds between price_snapshots rows per symbol. Use a smaller value for finer intraday resolution, e.g. 60 for one-minute snapshots. |
--keep-days |
0 (keep all) |
Keep N days of trade_log, order_events, price_snapshots and feed_gaps; older rows are deleted at startup and once an hour. Daily rollups are always kept. The file stops growing at N days (SQLite reuses the freed pages) |
--keep-order-events-days |
as --keep-days |
Keep N days of order_events alone, overriding --keep-days for them. Fractions are fine (0.05 is about an hour); under an AI swarm order events are most of the file |
--human-gateways |
none | Comma-separated gateway id globs, e.g. TRADER*, whose order_events prune on --keep-human-order-events-days instead. A person places a handful of orders a day and should not lose them on a clock set for a hundred bots |
--keep-human-order-events-days |
as --keep-order-events-days |
Keep N days of order_events for gateways matching --human-gateways; 0 keeps them for good. Requires --human-gateways — on its own it would match nobody and change nothing |
--sql-trace |
off | Log executed SQLite SQL statements from the stats writer connection |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Subscriptions:
pm-stats opens two independent PUB/SUB connections: one to pm-engine
(ENGINE_PUB_ADDR, port 5556) and one to pm-index (INDEX_PUB_CONNECT_ADDR,
port 5558).
| Source | Topic | Purpose |
|---|---|---|
| pm-engine | trade.* |
Updates OHLCV, VWAP, min/max, volume, trade log |
| pm-engine | book.* |
Records opening bid/ask prices; drives 15-minute snapshots |
| pm-engine | system.eod |
Records end-of-day closing bid/ask prices |
| pm-index | index.update |
Records every index level tick to index_level_snapshots and rolls up daily OHLC into index_daily_stats |
On engine shutdown, the engine broadcasts system.eod before closing sockets.
pm-stats receives it and immediately flushes the closing bid/ask for every active symbol
into daily_stats. The index.update subscription is independent of the engine
socket and keeps working even if pm-index starts or stops at a different time
than pm-engine.
How statistics are computed¶
OHLCV and VWAP accumulate in memory, one SymbolStats object per symbol, and are
flushed to daily_stats after each trade.
| Statistic | Trigger | Rule |
|---|---|---|
open_price |
First trade.* of the day |
Set once; never overwritten |
close_price |
Every trade.* |
Always overwritten with the latest price |
high_price |
Every trade.* |
Running max(current, trade_price) |
low_price |
Every trade.* |
Running min(current, trade_price) |
volume |
Every trade.* |
Cumulative sum of matched quantities |
trade_count |
Every trade.* |
Incremented by 1 |
vwap |
Every trade.* |
\(\sum(price \times qty) / \sum(qty)\); maintained as two running accumulators |
largest_trade_qty/price |
Every trade.* |
Replaced when qty > current_largest |
open_bid/ask |
First book.* update of the day |
Set once from the book snapshot |
close_bid/ask |
system.eod |
Overwritten with the final bid/ask from the book |
15-minute price snapshots are written to price_snapshots when a book.* message
arrives and at least --snapshot-interval seconds have elapsed since the last snapshot for that symbol
(default: SNAPSHOT_INTERVAL_SEC = 900, i.e. 15 minutes). The mid-price is computed as:
If one side of the book is empty the available side is used as the mid-price; if both sides are empty the last known trade price is used instead.
Trade log rows are appended on every trade.* event — no aggregation, one row per
matched trade pair, using the trade UUID from the engine as the primary key.
Startup behaviour — on startup pm-stats sends a book.snapshot_request via PUSH
:5555 for every symbol it discovers from system.symbols.STATS. This primes the
open_bid/open_ask values and the first snapshot interval even before the first trade.
Statistics Database Schema¶
The database lives at data/stats.db (SQLite 3). Nine tables are maintained
in total: the three below (the ones queried directly by pm-stats-cli for
day-to-day reporting), plus instruments and stats_meta (reference/meta),
feed_gaps and order_events (private-event and gap tracking), and
index_daily_stats / index_level_snapshots (index history, fed by
pm-index). See
Statistics and Reporting — The Statistics Database Schema
for the complete reference, including those five tables.
All three tables below store prices as integer ticks, with a per-row
tick_decimals column giving the conversion: display price = price /
10^tick_decimals. Always use the row's own tick_decimals — symbols do not
share a tick size.
daily_stats¶
One row per (date, symbol). Upserted on every trade and again when system.eod arrives.
| Column | Type | Description |
|---|---|---|
date |
TEXT (PK) | Calendar date YYYY-MM-DD |
symbol |
TEXT (PK) | Instrument ticker |
open_price |
INTEGER | Price of the first trade of the day, in ticks |
high_price |
INTEGER | Highest trade price of the day, in ticks |
low_price |
INTEGER | Lowest trade price of the day, in ticks |
close_price |
INTEGER | Price of the last trade of the day, in ticks |
open_bid |
INTEGER | Best bid at the time of the first book update, in ticks |
open_ask |
INTEGER | Best ask at the time of the first book update, in ticks |
close_bid |
INTEGER | Best bid recorded at engine shutdown (system.eod), in ticks |
close_ask |
INTEGER | Best ask recorded at engine shutdown (system.eod), in ticks |
volume |
INTEGER | Total traded quantity for the day |
trade_count |
INTEGER | Number of individual trades |
turnover |
INTEGER | \(\sum(price\_ticks \times qty)\) — exact; the VWAP numerator |
vwap |
REAL | turnover / volume, in ticks. Derived, so REAL — divide by 10^tick_decimals for display money |
largest_trade_qty |
INTEGER | Quantity of the single largest trade |
largest_trade_price |
INTEGER | Price of the single largest trade, in ticks |
tick_decimals |
INTEGER | Decimal scale for this row's tick columns |
Example query — end-of-day summary, converted to display money:
SELECT date, symbol,
open_price / POWER(10, tick_decimals) AS open_price,
high_price / POWER(10, tick_decimals) AS high_price,
low_price / POWER(10, tick_decimals) AS low_price,
close_price / POWER(10, tick_decimals) AS close_price,
volume, trade_count,
ROUND(vwap / POWER(10, tick_decimals), 4) AS vwap
FROM daily_stats
ORDER BY date DESC, symbol;
price_snapshots¶
One row per (ts, symbol) written every --snapshot-interval seconds (default: 15 minutes) when a book update arrives.
| Column | Type | Description |
|---|---|---|
ts |
TEXT (PK) | ISO-8601 timestamp (UTC, second precision) |
symbol |
TEXT (PK) | Instrument ticker |
mid_price |
REAL | (best_bid + best_ask) / 2, in ticks; falls back to whichever side is present, then the last trade price. Derived, so REAL — a midpoint between two ticks can land on a half-tick |
best_bid |
INTEGER | Best bid price at snapshot time, in ticks (null if empty book) |
best_ask |
INTEGER | Best ask price at snapshot time, in ticks (null if empty book) |
pct_change |
REAL | Percentage change of mid_price vs. previous snapshot: \(100 \times (mid_{t} - mid_{t-1}) / mid_{t-1}\) (a ratio, not a price — never converted) |
tick_decimals |
INTEGER | Decimal scale for this row's tick columns |
Example query — intraday price path for MSFT, converted to display money:
SELECT ts,
mid_price / POWER(10, tick_decimals) AS mid_price,
best_bid / POWER(10, tick_decimals) AS best_bid,
best_ask / POWER(10, tick_decimals) AS best_ask,
ROUND(pct_change, 4) || '%' AS change
FROM price_snapshots
WHERE symbol = 'MSFT'
ORDER BY ts;
trade_log¶
Append-only record of every individual matched trade.
| Column | Type | Description |
|---|---|---|
ts |
TEXT | ISO-8601 timestamp (UTC, millisecond precision) |
trade_id |
TEXT (PK) | Durable engine trade id (run_seq-counter); duplicate delivery is idempotent on this key |
symbol |
TEXT | Instrument ticker |
price |
INTEGER | Execution price, in ticks |
quantity |
INTEGER | Matched quantity |
tick_decimals |
INTEGER | Decimal scale for price |
buy_gateway_id |
TEXT | Gateway that submitted the buy order |
sell_gateway_id |
TEXT | Gateway that submitted the sell order |
aggressor_side |
TEXT | BUY or SELL for a continuous match; AUCTION for an uncross print where both sides were resting |
Example query — all trades for a symbol sorted by time, converted to display money:
SELECT ts, price / POWER(10, tick_decimals) AS price,
quantity, aggressor_side, buy_gateway_id, sell_gateway_id
FROM trade_log
WHERE symbol = 'AAPL'
ORDER BY ts;
Querying the database
You can open data/stats.db with any SQLite client:
pm-stats-cli — Statistics Query CLI¶
pm-stats-cli is a read-only command-line interface for querying
data/stats.db without writing SQL manually.
pm-stats-cli [--db data/stats.db] [--format table|json|csv] [--no-header] [--timezone TZ] COMMAND [options]
Unlike pm-stats, this is not a subscriber process. It runs one query,
prints output, and exits.
Global options:
| Flag | Default | Description |
|---|---|---|
--db |
data/stats.db |
SQLite database file path |
--format |
table |
Output format: table, json, or csv |
--no-header |
off | Suppress header row in csv output |
--timezone |
DB-recorded timezone | Override the session timezone that --date resolves in; rarely needed, and a mismatched value triggers a warning since --date would then select a different trading day than pm-stats used. pm-stats itself takes no --timezone flag — it reads locale.session_timezone from the deployed config |
--version |
— | Print version and exit |
Subcommands:
| Subcommand | Purpose | Typical filters |
|---|---|---|
daily |
Daily OHLCV summary from daily_stats |
--date, --symbol, --limit, --after, --wide |
snapshots |
Intraday snapshots from price_snapshots |
--symbol (required), --date, --from, --to, --limit, --after |
trades |
Trade history from trade_log |
--symbol, --date, --from, --to, --limit, --after |
order-events |
Private order lifecycle events from the order_events table | --gateway (required), --symbol, --event-type, --date, --from, --to, --limit (default 500), --after |
order-lifecycle |
All events for a single order ID | --gateway (required), --order-id (required) |
symbols |
Discover symbols available in stats data | --date |
dates |
Discover trading dates available in daily_stats |
--symbol |
index-daily |
Daily index OHLC rollup from index_daily_stats |
--date, --index-id, --limit, --after, --wide |
index-snapshots |
Every recorded index level tick from index_level_snapshots |
--index-id (required), --date, --from, --to, --limit, --after |
index-ids |
Discover index IDs with recorded data | --date |
instruments |
Show instrument reference data (tick scale per symbol) | --symbol |
gaps |
Show detected feed gaps (trades the recorder never received) | --date, --from, --to, --limit (default 500) |
health |
Check the pm-stats process and ping its health socket; --extended adds the stats DB integrity and read/write check |
--extended, -v/--verbose, -q/--quiet (print nothing; exit 0 when healthy, -1 otherwise) |
--after CURSOR pages through results returned by a previous call to the same
subcommand — pass the cursor value from that call's last row to fetch the next page.
index-daily, index-snapshots, and index-ids query the level/EOD history
that pm-index's own JSONL file no longer stores — see
Statistics and Reporting
for the full reference and Market Index for
why this data moved here.
Examples:
# Latest available daily summary date
pm-stats-cli daily
# Daily summary for one date and one symbol
pm-stats-cli daily --date 2026-06-14 --symbol AAPL
# Include bid/ask and largest-trade fields
pm-stats-cli daily --date 2026-06-14 --wide
# Intraday snapshots for one symbol in a time window
pm-stats-cli snapshots --symbol MSFT --from 2026-06-14T09:00:00+00:00 --to 2026-06-14T16:30:00+00:00
# Trades as CSV for scripting/export
pm-stats-cli --format csv trades --symbol AAPL --date 2026-06-14
# Trades as JSON for automation
pm-stats-cli --format json trades --symbol AAPL --limit 50
# Discovery helpers
pm-stats-cli symbols
pm-stats-cli dates --symbol AAPL
No-row behaviour:
table: printsNo rows found.json: prints[]csv: prints only header row unless--no-headeris set
For schema details, see the pm-stats section above and
Statistics and Reporting.
pm-scheduler — Session Scheduler¶
Drives session-phase transitions (PRE_OPEN → OPENING_AUCTION → CONTINUOUS →
CLOSING_AUCTION → CLOSED) by sending session.transition messages to the
engine at configured wall-clock times.
pm-scheduler [--now] [--delay 3] [--daily] [--speed F] [--start YYYY-MM-DDTHH:MM] [--no-confirm] [--on-close COMMAND] [--on-close-delay SEC] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--now |
off | Skip wall-clock waiting; send all transitions immediately with a delay between each |
--delay |
None (effective default 3.0, only used with --now) |
Seconds between transitions in --now mode (ignored, with a warning, outside --now) |
--daily |
off | Run continuously, repeating the schedule every working day (weekends and the configured country's bank holidays are skipped) |
--speed F |
$EDUMATCHER_SCHEDULER_SPEED, else off |
Compressed mode (with --daily): simulated time runs F times faster than the wall clock, so a whole trading day fits in minutes |
--start YYYY-MM-DDTHH:MM |
$EDUMATCHER_SCHEDULER_START, else now |
Compressed mode: the session-local time the simulated clock starts at |
--no-confirm |
off | Do not query/confirm session state via the engine before sending a transition |
--on-close COMMAND |
off | Start this command after each day's CLOSED transition and once at start-up (for example pm-eod run --pending). It runs detached, one at a time, with its output in the scheduler's log; ignored in --now mode. See Closing the Day |
--on-close-delay SEC |
30 |
Seconds to wait after CLOSED before starting --on-close, so pm-clearing can save its close snapshot first |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
--verbose / -v |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
session.transition |
Requests the engine to move to the next session phase |
The scheduler does not subscribe to any PUB messages — it is fire-and-forget.
See Auctions & Scheduling for the full schedule configuration and session-phase documentation.
pm-eod — End-of-Day Settlement¶
Settles a closed trading day and tidies up after it. A one-shot command: it runs, prints what it did and exits. For each trading date it checks that the day really closed and the books reconcile (gate), turns each trader's change in mark-to-market equity into a cash amount against the exchange (settle), writes statements and a summary (report), and runs the housekeeping steps (audit, logs, backup). The concepts, setup and troubleshooting are in Closing the Day.
pm-eod [--datapath DIR] [--format table|json|csv] [--no-header] run \
(--date YYYY-MM-DD | --pending) [--only STEP] [--force] [--redo] [--dry-run] \
[--log-keep-days N] [--backup-keep-days N] [--backup-skip NAME ...]
pm-eod [--datapath DIR] [--format table|json|csv] status [-q] [--limit N]
pm-eod [--datapath DIR] [--format table|json|csv] balances [--date YYYY-MM-DD] [--gateway GW_ID] [--check]
Global options (must come before the subcommand):
| Flag | Default | Description |
|---|---|---|
--datapath DIR |
the configured data directory | Data directory holding clearing.db, eod.db, ref_data/roster.yaml and the output folders |
--format |
table |
Output format for status and balances: table, json or csv |
--no-header |
off | Omit the header row (table and csv) |
--version |
— | Print version and exit |
run options:
| Flag | Default | Description |
|---|---|---|
--date YYYY-MM-DD |
— | The trading date to close. Exactly one of --date and --pending is required. |
--pending |
off | Every closed date that has a close snapshot and no ok run, oldest first. Stops at the first failing date. With more than one date, each date's backup step is deferred and one backup is taken at the end, under the last date that got through. |
--only STEP |
all steps | Re-run just report, audit, logs or backup for an already settled date. Not recorded in the run history. |
--force |
off | Do not require the CLOSED session event (for exchanges without a scheduler). The close snapshot and reconcile checks still apply. |
--redo |
off | Recompute the date: deletes its settlement and the settlement of every later date, then settles it again, all in one transaction (if the recomputation fails, nothing is deleted). |
--dry-run |
off | Run the gate and compute the amounts, print them, write nothing. Cannot be combined with --only. |
--log-keep-days N |
14 |
Delete compressed fallback logs older than N days |
--backup-keep-days N |
7 |
Delete backup folders older than N days |
--backup-skip NAME |
none | Leave one database out of the backup: clearing.db, stats.db or eod.db. Repeatable. The backup step's message says what was skipped. |
status options: -q / --quiet prints nothing and exits 0 when no closed
date is waiting (a closed date whose run failed counts as waiting) and 1
otherwise; --limit N (default 30) caps the number of runs shown.
balances options: --date shows balances as of that date; --gateway
filters to one trader (case-insensitive); --check verifies that every settled
date sums to exactly zero and exits 1 if one does not.
Exit codes of run: 0 clean · 1 the gate, the settlement or the report
failed · 2 only housekeeping warnings. A second pm-eod started while one is
running exits 1 (lock file <data>/.pm-eod.lock).
Reads: clearing.db (trades, positions, position_close_snapshot,
session_events), stats.db (optional, for the summary's market table),
ref_data/roster.yaml (optional).
Writes: eod.db (the ledger), settlement/<date>/, backup/<date>/,
compressed audit days under audit/, and compressed fallback logs under logs/.
It binds no port and sends no message.
roster.yaml¶
| Key | Required | Description |
|---|---|---|
traders |
yes | List of trader entries |
traders[].gateway |
yes | Gateway identifier; upper-cased on load; unique; not EXCHANGE |
traders[].name |
no | Display name (default: the gateway identifier) |
traders[].email |
no | Printed on the statement and in instructions.csv; never used to send mail |
traders[].opening_balance |
no | Starting balance, at most 2 decimals (default 0) |
A missing file is an empty roster. Errors name the line (roster.yaml: line 7: …).
Files written to settlement/<date>/¶
| File | Content |
|---|---|
statements/<gateway>.md |
The trader's statement (the headline, result by symbol, open positions at the close, trades). Result by symbol lists a symbol only if it is held at the close, traded that day or has a non-zero result |
statements/<gateway>.json |
The same, with amount_minor / balance_minor as integers in minor units |
statements/<gateway>.csv |
One row per symbol: symbol,day_pnl,net_qty,mark |
instructions.csv |
gateway,name,email,amount,direction,balance; direction is receive, pay or none |
leaderboard.csv |
rank,gateway,name,balance; equal balances share a rank |
summary.md, summary.json |
Accounts settled, the zero-sum proof, rounding, biggest winners and losers, market table, warnings |
The settlement ledger eod.db¶
| Table | Columns | Description |
|---|---|---|
eod_settlement |
trade_date, account, amount_minor, detail_json |
One row per account per settled date. account is a gateway identifier or EXCHANGE (the rounding line). The amounts of a date sum to exactly 0. |
eod_runs |
trade_date, status, started_at, finished_at, steps_json |
One row per date run. status is ok, failed or report-failed. |
Balances are not stored: they are the sum of amount_minor plus the roster's
opening balance. The full DDL is in the generated
SQLite schema reference.
Settlement rules (reference)¶
- A date's amount for a gateway is the sum over its symbols of
(realized_pnl + unrealized_pnl)at this date'sposition_close_snapshotminus the same at the snapshot of the latest earlier settled date (0 if there is none, or ifclearing.dbno longer has that date's snapshot), each converted from ticks to currency with the symbol's owntick_decimals. Any unsettled snapshot dates in between are part of the one interval. - A date has closed when its latest
session_eventsPHASErow isCLOSEDand an earlierPHASErow of the same date is not, so the market was open that day. TheCLOSEDan engine announces when it starts, before the open, is therefore not a close. - Dates settle in order: a date is refused while an earlier date has closed (with a snapshot) but is not settled.
- Rounded to 2 decimals with the largest-remainder method; the exact total must
be within
0.01per position of zero, otherwise the settlement fails. - A position present in the earlier snapshot but missing from this date's is ignored.
- The gate requires: the date has closed (skipped by
--force); a close snapshot; and an emptypm-clearing-cli reconcilefor the date.
See Closing the Day for the worked example, the troubleshooting table and the housekeeping details.
pm-ticker — Scrolling Market Ticker¶
Draws a bordered ticker box across the top rows of the terminal. The box has a
header (the EduMatcher brand, today's total trade volume, and the current
date/time), a ticker line listing all active symbols with live prices, OHLCV,
and bid/ask spreads, and a news line. The symbol line scrolls leftward like a
classic ticker tape. The news line shows the latest pm-market-sim headline:
its time, [RUMOUR 60%] (with the credibility) or [CONFIRMED]/[RETRACTED]
when a rumour resolves, and the headline in green (good news) or red (bad),
struck through when retracted; no news yet until the first.
pm-ticker [--db data/stats.db] [--db-interval 900] [--timezone TZ] \
[--log-level LEVEL] [-v|-vv] [-q] [--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--db |
data/stats.db |
Path to the statistics SQLite database |
--db-interval |
900 | Seconds between daily_stats DB re-queries (15 min) |
--timezone |
DB-recorded timezone | Exchange session timezone (IANA name) used to bucket data into a trading day |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Display behaviour:
- The box redraws to the terminal width on resize.
- If every symbol fits within the current width the line stays static; scrolling only starts once the symbols are wider than the box.
- Below a minimum width the ticker line is truncated with an ellipsis (
…).
Ticker line format:
Each symbol segment shows:
| Field | Description |
|---|---|
| Symbol | Instrument name (bold cyan) |
| Last price | Most recent trade price or closing price from DB |
| Change % | Percentage change vs. today's open price (green if up, red if down) |
| H: / L: | Intraday high (green) and low (red) from the daily_stats table |
| Vol | Cumulative traded volume for the day |
| (nT) | Number of trades today |
| Bid/Ask | Current best bid (green) / best ask (red) from live book |
The ticker combines live ZMQ data (last price, bid/ask from book.* messages)
with historical DB data (OHLCV, trade count from pm-stats's SQLite database).
The DB is re-queried every --db-interval seconds to pick up updated daily statistics.
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
book. (prefix) |
Live last price, best bid/ask per symbol |
Depends on pm-stats
The ticker reads from data/stats.db which is populated by pm-stats.
Without pm-stats running, the ticker still works but only shows live bid/ask
and last price — OHLCV, volume, and trade count will be missing.
pm-board — Market Board¶
Full-screen multi-symbol display designed for large monitors or projection screens. Shows all active symbols in a single paged table with exchange-style colouring.
Startup options:
| Flag | Default | Description |
|---|---|---|
--rows / -r |
8 | Maximum number of symbols (rows) displayed per page |
--interval / -i |
10 | Seconds before auto-rotating to the next page |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
- ++enter++ to advance to the next page immediately
- ++ctrl+c++ to exit
Controls:
| Key | Action |
|---|---|
| ++enter++ | Advance to next page immediately |
| ++ctrl+c++ | Exit |
Display columns:
| Column | Description |
|---|---|
| Symbol | Instrument ticker |
| Last | Last traded price (coloured green if up from first trade, red if down) |
| Chg % | Percentage change from the first trade of the session: \(100 \times (last - first) / first\) |
| Bid | Best (highest) bid price currently in the book |
| Ask | Best (lowest) ask price currently in the book |
| Spread | Difference between best ask and best bid: \(ask - bid\) |
| Last Buy | Last trade price where this symbol was bought (green) |
| Last Sell | Last trade price where this symbol was sold (red) |
| Vol | Cumulative traded volume for the session |
| Updated | Timestamp of the most recent book update for this symbol |
The header bar shows: page number, total pages, number of active symbols, the auto-rotate interval, and the current clock time.
Paging behaviour:
- Symbols are sorted alphabetically and divided into pages of
--rowssymbols each. - The display auto-rotates to the next page every
--intervalseconds. - When the last page is reached, rotation wraps back to page 1.
- Pressing ENTER advances immediately and resets the auto-rotate timer.
Colour conventions (matching standard exchange displays):
- Green — price increase, buy trades, positive change %
- Red — price decrease, sell trades, negative change %
- White — unchanged or neutral values
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
book. (prefix) |
Book updates for all symbols — discovers symbols automatically |
trade.executed |
Every matched trade — drives volume and last-price tracking |
Large-screen demo
For classroom or conference demos, maximise the terminal and use large rows:
The board auto-discovers symbols as they become active — no configuration needed.pm-ai-trader — Autonomous Trader Bot¶
Runs one AI trader: a participant driven by a preset (strategy × execution
style × tempo × risk limits). It logs on to pm-alf-gwy as its own
participant, sends its orders over that ALF session and reads market data
from the engine's PUB socket (and the true values and news from
pm-market-sim's). See AI Traders.
Startup options:
| Flag | Default | Description |
|---|---|---|
--id |
required | Participant ID to log on as (e.g. AI001); must be a TRADER in the deployed configuration |
--preset |
noise-retail |
Built-in preset: auction-player, block-taker, combo-trader, contrarian, iceberg-seller, institutional, market-taker, news-trader, noise-retail, patient-liquidity, scalper, trend-follower, value-investor |
--preset-file |
none | Preset YAML file instead of a built-in |
--list-presets |
— | Print the built-in presets (strategy, execution, protection, description) and exit |
--symbols |
every deployed symbol | Comma-separated symbols to trade |
--seed |
1 |
Random seed; same seed and same market, same decisions |
--alf-host |
EDUMATCHER_ENGINE_HOST or 127.0.0.1 |
Host of pm-alf-gwy |
--alf-port |
alf_gateway.port of the deployed config |
Port of pm-alf-gwy |
--duration |
0 |
Runtime in seconds; 0 means run until stopped |
--log-level |
WARNING |
Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | -v → INFO (a status line a minute, session changes), -vv → DEBUG (every reject) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Connect and reconnect:
On logon, and after every reconnect, the trader reads the symbols, the session, the halts and its own positions from the engine before it sends an order. If the ALF session drops it retries with jittered back-off (at most 10 s apart); when the engine restarts, it logs on again and re-reads everything.
pm-ai-swarm — Multi-Agent Trading Swarm¶
Runs many AI traders: a supervisor process splits them into contiguous ID
blocks and runs each block in one worker process, restarting a worker that
dies. Each trader is its own ALF session. Settings come from a swarm.yaml
(--swarm), overridden by flags. See
Running the AI-Trader Swarm.
Startup options:
| Flag | Default | Description |
|---|---|---|
--swarm |
none | swarm.yaml file; flags override it |
--count |
10 |
Number of traders |
--prefix |
AI |
Participant ID prefix |
--start-index |
1 |
First ID number (AI001) |
--presets |
every built-in, equal weights | Composition: a,b (equal weights) or a:3,b:1 |
--symbols |
every deployed symbol | Comma-separated symbol universe |
--symbols-per-agent |
just enough to cover the universe | Consecutive symbols each trader trades |
--seed-base |
1000 |
Trader i gets seed seed-base + i |
--duration |
0 |
Runtime in seconds; 0 means run until stopped |
--workers |
min(cpus − 1, ceil(count / 100)) |
Worker processes |
--budget |
0 |
Order actions per second for the whole swarm; 0 lets the presets decide |
--alf-host |
EDUMATCHER_ENGINE_HOST or 127.0.0.1 |
Host of pm-alf-gwy |
--alf-port |
alf_gateway.port of the deployed config |
Port of pm-alf-gwy |
--log-level |
WARNING |
Logging level of the supervisor and the workers |
-v / --verbose |
off | -v → INFO, -vv → DEBUG; applies to the workers too |
-q / --quiet |
off | Reduce output to warnings/errors; applies to the workers too |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. SIGTERM or Ctrl+C stops every worker; workers get 10 s before they
are killed.
Output files:
<DATA_DIR>/ai_swarm/<date>-<worker>.jsonl — one JSON line per trader, five
seconds after each close.
pm-market-sim — Market Model¶
Publishes every symbol's true value from a seeded market model, and news. See The Market Model.
pm-market-sim [options]
pm-market-sim --init [--seed N] > market_sim.yaml
pm-market-sim --status --id ADMIN_ID [--top N]
Startup options:
| Flag | Default | Description |
|---|---|---|
--init |
off | Print a market_sim.yaml for the deployed configuration's symbols and exit |
--seed |
1 |
Seed written by --init |
--status |
off | Ask the running process for its state and the widest market-vs-value gaps, and exit |
--id |
none | ADMIN participant ID that --status asks as |
--top |
10 |
Gaps --status lists |
--log-level, -v, -q, --log-target, --log-file, --log-failover-timeout, --version |
— | As for the other processes |
Expected runtime input arguments:
None. It reads market_sim.yaml from the deployed configuration directory
and keeps its state in <DATA_DIR>/market_sim_state.json.
pm-news — News Desk¶
The instructor's news desk: sends commands to the running pm-market-sim as
an ADMIN participant. See
The Market Model — News.
pm-news --id ADMIN_ID inject (--symbol SYM | --sector SECTOR | --market) \
--kind KIND --sentiment S --impact I [--headline TEXT] [--rumour [--credibility C]]
pm-news --id ADMIN_ID confirm NEWS_ID
pm-news --id ADMIN_ID retract NEWS_ID
pm-news --id ADMIN_ID play scenario.yaml
pm-news list [--duration SECONDS]
| Subcommand | Does |
|---|---|
inject |
Publish a headline; prints its ID. --symbol and --sector repeat; --kind is one of CONTRACT, EARNINGS, ESG, GUIDANCE, LEGAL, MACRO, MANAGEMENT, MNA, OPERATIONS, PRODUCT, REGULATORY, RESEARCH; --sentiment −1 to 1 is public; --impact (log move of the value when confirmed) is not |
confirm |
Confirm a rumour: the values move now |
retract |
Retract a rumour: nothing moves |
play |
Run a timed scenario YAML |
list |
Print headlines as they are published; --duration 0 (default) until Ctrl+C |
Expected runtime input arguments:
None.
pm-compdb-cli — Company Database CLI¶
A SQLite database of company profiles for the 300 tickers of the s300
simulation (docs/examples/ref_data/s300-sim-setup), and the tool to query and
edit it. It also holds 100 Swedish and other Nordic companies whose tickers
pm-market-sim does not trade. The companies are made up: each ticker keeps its
sector and industry from pm-market-sim (and so the news it gets), but every
name, person and number is invented. No running process is needed.
pm-compdb-cli [--db PATH] [--format table|grid|markdown|json|csv] [--no-header] COMMAND [options]
pm-compdb-cli list [-c COLS|all] [-w FILTER]... [-s COL [--desc]] [-n N]
pm-compdb-cli add|update SYMBOL FIELD=VALUE...
| Subcommand | Does |
|---|---|
init |
Build the database of the 300 bundled companies; --force replaces an existing one |
columns |
List the columns, their types and what each holds |
show SYMBOL... |
Everything about one or more companies |
list |
List companies: -c comma-separated columns or all; -w COLUMN OP VALUE with OP one of = != < <= > >= ~ (contains, any case), repeat to narrow; -s sort column, --desc; -n at most N rows |
add SYMBOL FIELD=VALUE... |
Add a company; name, sector and industry are required |
import FILE... |
Add the companies of YAML files (a mapping of SYMBOL: to fields, the format of the bundled ones); all or nothing |
update SYMBOL FIELD=VALUE... |
Change fields; an empty value clears one |
delete SYMBOL |
Remove a company |
Output formats. --format applies to show and list:
| Format | Output |
|---|---|
table (default) |
Plain text columns separated by |; long cells are cut short with ... |
grid |
A table drawn with UTF-8 box characters (┌─┬─┐ │ ├─┼─┤ └─┴─┘); numbers right-aligned, long cells wrapped over several lines instead of cut. show gives a two-column table of column and value |
markdown |
Markdown for notes, wikis and pull requests. list gives a pipe table with column names and tickers in code and numbers right-aligned. show gives a heading with the name and ticker, then a table of column, value and meaning for each of Identity, Business, History, Management and Financials. A | in a value is escaped. --no-header is ignored, as a Markdown table needs its header |
json, csv |
For programs; --no-header leaves out the CSV header |
pm-compdb-cli --format grid list -w sector=TECH -s market_cap_musd --desc -n 5
pm-compdb-cli --format markdown show NVDA > nvda.md
Where the companies come from. The bundled companies are hand-written
YAML files, one per sector, installed as part of the edumatcher package in
its compdb/companies/ folder. init always reads them from the installed
package, and no option points it elsewhere. To load your own files, use
import. Where the folder is depends on how EduMatcher was installed:
| Install | Source YAML (init reads) |
Database (default --db) |
|---|---|---|
Source checkout (poetry install, editable) |
src/edumatcher/compdb/companies/*.yaml in the checkout; edits take effect at the next init |
<DATA_DIR>/companies.db |
pip / pipx install of the wheel |
<site-packages>/edumatcher/compdb/companies/*.yaml inside that environment (pipx: ~/.local/share/pipx/venvs/edumatcher/lib/python3.*/site-packages/…); replaced by every upgrade |
<DATA_DIR>/companies.db |
Simulation container (edumatcher-ai-sim) |
/opt/edumatcher/.venv/lib/python3.14/site-packages/edumatcher/compdb/companies/*.yaml in the image |
/data/companies.db, which is ~/.edumatcher-sim/data/companies.db (installed) or ./data-sim/companies.db (checkout) on the host |
To see the folder of the installation you are running:
init never changes the YAML files. Adding, changing or deleting a company
changes only the database. The source checkout also has a prebuilt copy,
docs/examples/ref_data/s300-sim-setup/companies.db. A test keeps it equal to
the YAML, so after editing the YAML rebuild it with
pm-compdb-cli --db docs/examples/ref_data/s300-sim-setup/companies.db init --force.
The prebuilt copy is not in the wheel or the container image.
In the simulation container. The container's entrypoint
(deployment/docker/entrypoint.sh) builds the database when it prepares the
data directory: if the bundled s300-sim example is configured
(EM_CONFIG=s300-sim, the image's default) and /data/companies.db does not
exist, it runs pm-compdb-cli --db /data/companies.db init. It does nothing
in the following cases:
- the database already exists, so companies you added survive restarts and upgrades. Delete the file and restart to get the bundled set of the installed version back.
- another example is configured, or your own configuration is given with
EM_CONFIG_FILE. Its tickers need not be the s300 ones; runinityourself if you want the database anyway.
A failed build is logged (warning: could not build /data/companies.db) and
the exchange starts regardless.
Columns. Identity: symbol, name, sector, industry, headquarters.
Business: purpose, key_market, market_position, competitors (tickers),
key_risk. History: origin (FOUNDER, CO_FOUNDERS, FAMILY, MERGER,
SPIN_OFF, PRIVATISATION, COOPERATIVE), founders, founder_story,
founded_year, ipo_year, ipo_valuation_musd. Management: ceo_name,
ceo_since, ceo_industry_years, employees. Financials for fiscal_year
(money in USD millions): revenue_musd, net_income_musd,
revenue_growth_pct, operating_margin_pct, free_cash_flow_musd,
market_cap_musd, dividend_yield_pct, debt_to_equity, credit_rating.
Derived by the database: years_in_business, ceo_tenure_years, pe_ratio
(market cap / net income, empty for a loss), ps_ratio, net_margin_pct.
Checks on every write: a ticker the simulation knows must keep its sector; the
industries the simulation gives news of their own (Semiconductors,
Software, Pharmaceuticals, Automobiles, Airlines,
Aerospace & Defense) belong to exactly their members; years must run in order
(founded, IPO, fiscal year; the CEO no earlier than the founding).
pm-compdb-cli show NVDA
pm-compdb-cli list -c symbol,name,pe_ratio,dividend_yield_pct -w sector=UTILITIES -s dividend_yield_pct --desc
pm-compdb-cli list -w "origin=MERGER" -w "founded_year<1950" -c symbol,name,founders
pm-compdb-cli list -w "net_income_musd<0" -c symbol,name,revenue_growth_pct,key_risk
pm-compdb-cli --format csv list -c all > companies.csv
Expected runtime input arguments:
None.
Reading a company profile¶
The Company tab of pm-trader and pm-terminal, and pm-compdb-cli show,
present the same profile. This section explains every value in it, in the
order the tab shows them: what the value is, how it is calculated, how to read
it, and how it affects the company, its valuation and its market position. The
companies and figures are invented, but they are built to behave as the real
ones do, so the habits learned here carry over. Column names are in code.
Three ideas run through everything below.
- A figure means little alone. Compare it with the company's own past, with
its competitors (the
competitorscolumn lists their tickers), and with its industry. A 2 % net margin is excellent for a supermarket and a disaster for a software company. - A price is a view of the future. What the market pays for a company
(
market_cap_musd) depends less on what it earned last year than on what investors expect it to earn from now on. Most of the figures below are either a measure of that expectation or evidence for it. - Everything is connected. Growth needs investment, investment uses cash or debt, debt costs interest, interest lowers profit, and profit and cash decide what the company is worth. Reading a profile is following these links.
Money is in millions of US dollars (shown as M$), for the fiscal year in
fiscal_year. A blank value means the database has no figure, or, for a
derived ratio, that the ratio has no meaning (a P/E for a company making a
loss).
Valuation¶
Market cap (market_cap_musd). The market capitalisation: what the market
says the whole company is worth, equal to the share price times the number of
shares. A company with 100 million shares at 50 $ has a market cap of 5,000 M$.
It is the price of the equity, the owners' part, and does not count the
company's debts.
- Reading it. It is a measure of size. Large companies are called large-caps, and are usually older, more stable and easier to trade. Small-caps can grow faster but are more volatile and have wider spreads.
- Effect. It is the base of every ratio in this group: P/E and P/S divide it by profit and sales. A high market cap gives a company cheap access to money (it can issue shares at a good price, or pay for acquisitions with them) and often makes it a member of the indices that funds buy by rule, which adds steady demand for its shares. A falling market cap makes everything more expensive for it, and can make it a takeover target.
P/E (pe_ratio). Price / earnings: market_cap_musd divided by
net_income_musd. It says how many years of the current profit you pay for
the company if the profit stayed the same. A P/E of 20 means 20 years of
earnings. It is left empty when the net income is zero or negative, because a
price over a loss has no meaning.
- Reading it. A high P/E means investors expect the profit to grow, or find the company safe, or both. A low P/E means the company is cheap, or that investors expect the profit to fall (a "value trap"). Compare it with the industry: utilities trade on low single digits to the low teens, fast-growing software companies on several times that.
- Effect. The P/E is the market's report card on the company's future. A high one is an asset: the company can raise money cheaply and buy others with its shares. It is also a burden, because a company with a high P/E must keep delivering, and a disappointing quarter can cut the share price by a large fraction in a day, since the multiple and the profit both fall.
P/S (ps_ratio). Price / sales: market_cap_musd divided by
revenue_musd. It says what you pay for each dollar of yearly sales.
- Reading it. It works where P/E does not: for a young company that spends its profit on growth, or for one that has a loss. It does not see costs, so a low P/S is only attractive if the company can turn sales into profit. Compare within an industry, where margins are alike: a P/S of 0.3 is normal for a retailer, 8 for a software company.
- Effect. A high P/S is a bet on future margins: the company must one day
earn a lot of profit on each dollar of sales to justify the price. P/S and
net margin together give the P/E:
P/E = P/S / net margin.
Dividend yield (dividend_yield_pct). The yearly dividend per share as a
percentage of the share price. A company paying 2 $ a year on a 50 $ share
yields 4 %. A dividend is a part of the profit paid out to owners in cash.
- Reading it. A yield of 0 means the company keeps its profit to reinvest, which is normal for companies that grow fast. A steady, moderate yield is the sign of a mature business with more cash than it can use. A very high yield is a warning: it is often the result of a share price that has fallen because the market expects the dividend to be cut.
- Effect. The yield draws a certain kind of owner, pension funds and income investors, who hold shares for years and so make the price steadier. A company that cuts its dividend sends a strong negative signal, and the price usually falls. A dividend also uses cash that could have gone to growth or to paying down debt, so the choice is a statement about the company's prospects.
Debt / equity (debt_to_equity). Total debt divided by shareholders'
equity (the owners' part of the balance sheet). A value of 1.5 means that for
each dollar the owners have put in, the company has borrowed 1.5.
- Reading it. Above 1 the company is financed mostly by lenders. That is normal for banks, utilities and real estate companies, whose income is stable and can carry debt, and a warning for a company whose sales rise and fall.
- Effect. Debt is leverage. It makes the owners' return larger in good years, since the profit is shared by fewer owners' dollars, and the loss larger in bad ones. Interest must be paid whatever the sales are, so a highly indebted company has little room for a bad year, and may have to sell assets or issue shares at a poor price to survive. A heavy debt also weakens the credit rating, and lenders then demand more interest. A company with little debt has the opposite strength: it can buy a weakened rival or invest through a downturn.
Credit rating (credit_rating). A rating agency's opinion of the chance
that the company repays its debt in full and on time. The scale runs from AAA
(safest) down through AA, A, BBB, BB, B and CCC to default, with
+ and - for fine steps. BBB- and above is investment grade; BB+ and
below is speculative grade, or "high yield".
- Reading it. Many pension funds and insurers are only allowed to hold
investment-grade debt, so the line between
BBB-andBB+is the most important one on the scale. - Effect. The rating sets the interest a company pays when it borrows: a
AAAcompany pays little over the government's rate, aBcompany several percentage points more. That cost reaches the profit and so the P/E. A downgrade, especially below investment grade, forces some holders to sell, raises borrowing costs and can lower the share price at once. A strong rating is a competitive weapon: the company can borrow cheaply to build, buy or wait out a price war that its weaker rivals cannot.
Financials¶
The figures are for the last fiscal year, the year named in the section's title. They follow the order of the income statement: sales at the top, profit at the bottom, and the cash that really comes out of it.
Revenue (revenue_musd). The money the company took in from customers
during the year, before any cost is deducted. Also called sales or turnover.
It is the "top line" of the income statement.
- Reading it. It measures the size of the business, independent of how well
it is run. Its value is in its trend and in what it is compared with:
revenue per employee (
revenue_musdagainstemployees) shows how productive the staff are. - Effect. Revenue is the ceiling for everything else: no cost cutting makes a profit bigger than the sales. Large revenue gives scale, which spreads fixed costs over more units, and buying power over suppliers. It is a main part of a company's market position: a leader is usually the largest seller in its market.
Revenue growth (revenue_growth_pct). This year's revenue against last
year's, in percent. Growth of 8 means sales are 8 % higher.
- Reading it. Compare it with the industry's growth. Faster than the market means the company is taking market share; slower means it is losing it, even if its sales still rise. Negative means the business is shrinking.
- Effect. Growth is the main reason one company deserves a higher P/E than another: the same profit is worth more if it is growing. It is also the most important driver of the share price, since the market prices the change. A fast-growing company, however, spends cash on staff, stock and equipment before the sales come, so it often has low free cash flow or needs to borrow. Growth bought by selling at a loss is not worth the same as growth that earns a profit.
Net income (net_income_musd). The profit: what remains of revenue after
every cost, interest and tax. The "bottom line", and the E in P/E. A negative
value is a loss.
- Reading it. It is the number the owners can ultimately claim. It is shaped by accounting choices, such as how fast equipment is written off, so it can be flattered. This is why it is read together with free cash flow.
- Effect. Profit funds dividends, buy-backs and the investment that makes growth, so it feeds the future. Losses use up the company's reserves; a company in loss with a high debt is in the most danger. The valuation follows it directly: with the market cap fixed, a profit that rises by a tenth lowers the P/E by a tenth, and the share price tends to rise to restore the usual multiple.
Net margin (net_margin_pct). Net income divided by revenue, in percent:
how many cents of profit each dollar of sales leaves.
- Reading it. Margins are set by the structure of an industry. Selling food in a shop earns a few percent, software and drugs earn 20 % or more because a copy costs little to make. Compare only like with like.
- Effect. A higher margin than competitors usually means a real advantage: a brand that lets the company ask more, a cost advantage from scale, or a protected product. It also gives a cushion: a company with a 25 % margin survives a price war that would bankrupt one with 3 %. A margin that is falling while sales rise is a warning of rising costs or falling prices.
Operating margin (operating_margin_pct). Operating income divided by
revenue, in percent. Operating income is the profit from running the business
itself, before interest and tax.
- Reading it. It isolates the quality of the operation from the way the company is financed and taxed. Compare it with the net margin: if the gap is large, interest or tax takes much of the profit. A highly indebted company can have a good operating margin and a poor net margin.
- Effect. It shows pricing power and cost control. Management can improve it by raising prices, cutting costs or selling more of the high-margin products. A company with a rising operating margin is getting more out of each dollar of sales, and the market usually rewards that with a higher multiple. Because it comes before interest, it also shows how much room there is to pay the debt: operating income that is a few times the interest is safe.
Free cash flow (free_cash_flow_musd). The cash the business generates
after it has paid for the investment needed to keep it going: equipment,
buildings, software. Roughly, the cash from operations less capital spending.
- Reading it. Profit is an accounting figure and cash is a fact. A company can show a profit and run out of cash, if customers pay late or if it must keep buying equipment. Free cash flow well below net income, year after year, is a warning. A negative value is normal for a young company that invests ahead of growth, and a concern for a mature one.
- Effect. Free cash flow is what can actually be paid to the owners or the lenders: dividends, buy-backs of shares, debt repayment, or the purchase of another company. Many analysts value a company by the cash it will produce in the future, so it is at the heart of the valuation. A company with strong free cash flow is independent: it does not need the markets to survive a bad period, which is a real strength against rivals that do.
Position¶
These four lines are text. They describe the competitive situation that the figures above are the result of, and the one that will decide them next.
Key market (key_market). Where most of the revenue comes from: a country,
a customer group or a kind of product.
- Reading it. It tells you what the company's results depend on. A company with one market is exposed to that market's cycle, regulation and competition; one that sells in many markets is steadier.
- Effect. Concentration is a risk that lowers the multiple investors will pay, and also an advantage when the market is large and growing, since the company can specialise. News about the market, such as a new law or a change in demand, moves every company that depends on it, which is why the news of the simulation is often given by industry.
Market position (market_position). What sets the company apart from its
rivals: its unique position.
- Reading it. Look for a lasting advantage that others cannot easily copy: a leading brand, the lowest cost, a patent, a network of users that grows more valuable with each new one, or high costs for a customer to switch. Economists call such an advantage a moat.
- Effect. A wide moat is what makes high margins last, and so what justifies a high P/E. A company without one competes on price and its margin tends to fall towards the industry's average. Leaders can set prices; followers must accept them.
Competitors (competitors). The tickers of the main listed rivals,
separated by commas.
- Reading it. Use it to find the group to compare with. Look the tickers up
(
pm-compdb-cli showorlist) and put the margins, growth and P/E side by side. A company that is better on all three than its rivals either deserves a higher P/E or is already priced for it. - Effect. A competitor's news is the company's news. A rival's price cut, new product or bankruptcy changes the company's own prospects, and in the simulation the shares of an industry tend to move together for this reason.
Key risk (key_risk). The biggest single threat to the business.
- Reading it. It names what to watch. Typical risks are the loss of a large customer, a change in regulation, a new technology, a dependence on a single supplier or country, or a heavy debt that falls due.
- Effect. The market prices risk. When the risk is realised, the share price falls at once and often more than the direct damage, because the market also doubts everything else. A company that is open about a risk and has a plan for it is valued higher than one that is surprised by it.
Management¶
CEO (ceo_name, ceo_since; the tenure is ceo_tenure_years). The chief
executive, who sets the strategy and runs the company day to day, and the year
they took office.
- Reading it. A new CEO often brings a change of direction: new products, cost cuts, a sale of parts of the business. A long tenure means stability and a leader who is trusted, or one who has stayed past the point of being useful.
- Effect. A change of CEO is one of the events that most often moves a share price, up when the market thought the old one was failing, down when the departing one was highly regarded. For a company whose value depends on a founder's vision, the succession is a risk in itself.
Years in the industry (ceo_industry_years). How long the CEO has worked
in this industry, in this company or others.
- Reading it. A long career means the CEO has lived through several cycles of the industry and knows its customers, suppliers and rivals. A short one means a fresh view, and a risk of mistakes that experience would avoid.
- Effect. It is a soft figure, and no guarantee: it is one input to the market's trust in management. It matters most in industries with long cycles (energy, mining, shipbuilding), where decisions made today pay off or fail years later.
Employees (employees). The number of people who work for the company.
- Reading it. It is a second measure of size, and, set against revenue, of efficiency. Revenue per employee varies enormously: it is high in software and trading, and low in retail and services.
- Effect. Wages are usually a company's largest cost, so the head count decides how easily it can cut costs, and how exposed it is to rising wages. A company that grows its revenue faster than its head count is gaining productivity, which feeds directly into the operating margin.
History¶
Founded (founded_year; the age is years_in_business). The year the
company was started.
- Reading it. Age is a rough guide to maturity. An old company has survived several cycles and often holds a large share of a slow market. A young one has less proof, and more room to grow.
- Effect. Longevity builds a brand and relations with customers and suppliers, which are parts of the moat. It also brings habits and old products that a young rival does not have, and the young rival can attack with a new technology.
IPO (ipo_year, ipo_valuation_musd). The initial public offering: the
year the company first sold its shares to the public on an exchange, and the
market value put on it then.
- Reading it. Set the value at the IPO against today's
market_cap_musd: it shows how much value the company has created since it went public. A company that has grown many times over has delivered on its promise. One that is worth less than at the IPO has destroyed value for the investors who bought then. - Effect. An IPO turns a private company into one with a price that anyone can see, and gives it a way to raise money by selling more shares and to pay its staff and acquisitions with them. In return it must publish its accounts and answer to the market every quarter.
Origin (origin, with founders and founder_story). How the company
began: FOUNDER, CO_FOUNDERS, FAMILY, MERGER, SPIN_OFF,
PRIVATISATION or COOPERATIVE.
- Reading it. The start shapes the company's character. A founder's company tends to be driven by one vision. A family firm thinks in generations, and may be slow to take outside advice. A merger must join two cultures and cut the overlap. A spin-off starts with a clear business but has to stand without its parent. A privatised company comes from a state monopoly and often carries its habits. A cooperative is owned by its members, who may care more about price than profit.
- Effect. Origin does not change the figures directly, but it explains them and warns of the risks: control that stays with a family or founder can protect the company from short-term pressure and also from change, and a merger is only worth what the integration achieves.
Putting the figures together¶
The same company can be read from three sides, and the profile gives you a figure for each.
| Question | Where to look | A strong company shows |
|---|---|---|
| What is it worth, and is that fair? | Market cap, P/E, P/S, dividend yield | A multiple that its growth and margins justify |
| Can it carry on and pay its way? | Debt / equity, credit rating, free cash flow, net income | Moderate debt, a good rating, cash above its needs |
| Does it have an edge? | Revenue growth, margins, market position, competitors | Faster growth and higher margins than its rivals, for a reason that lasts |
A few combinations tell more than any single figure:
- High P/E with low growth and falling margins: the price is not supported. A fall in the share price is likely when the market notices.
- Low P/E with high debt: it may be cheap for a reason. Check the credit rating and free cash flow before calling it a bargain.
- Profit but a negative free cash flow: the profit is not turning into cash. Look for growing receivables, stock or heavy investment.
- High margins, falling growth: the moat is at work but the market is saturated. Look for a rising dividend as the company has less to invest.
- Falling margins, rising sales: the company is buying growth, or competitors are forcing the price down.
To try these on the simulation's companies, use list with filters and
sorting:
# Cheap and profitable: low P/E, high net margin
pm-compdb-cli list -c symbol,name,pe_ratio,net_margin_pct -w "pe_ratio<12" -w "net_margin_pct>15" -s pe_ratio
# Highly leveraged companies and what they are rated
pm-compdb-cli list -c symbol,name,debt_to_equity,credit_rating,free_cash_flow_musd -w "debt_to_equity>2" -s debt_to_equity --desc
# Growth at a price: fast growers ranked by P/S
pm-compdb-cli list -c symbol,name,revenue_growth_pct,ps_ratio -w "revenue_growth_pct>20" -s ps_ratio --desc
# A company against its competitors
pm-compdb-cli show NVDA
pm-compdb-cli list -c symbol,pe_ratio,operating_margin_pct,revenue_growth_pct -w "industry=Semiconductors" -s pe_ratio --desc
pm-new-symbol / pm-ipo — List a New Symbol¶
Lists a new symbol (an IPO) in an engine configuration: appends it to
symbols:, gives it its reference price and, normally, a seed quote from the
market maker, validates the whole edited file and, when that file is the
deployed source, redeploys it. pm-ipo is an alias for the same command.
pm-new-symbol --symbol NEWCO --ipo-price 25.00 --outstanding-shares 40000000
pm-new-symbol --config my_config.yaml --symbol NEWCO --ipo-price 25.00 --outstanding-shares 40000000
Startup options:
| Flag | Default | Description |
|---|---|---|
--config PATH |
the deployed configuration's source | Authored YAML to edit. A file that is not the deployed source is edited only; run pm-config-deploy on it yourself |
--symbol NAME |
required | New symbol, 1–8 of A-Z 0-9 . _ |
--ipo-price PRICE |
required | Offer price; the book's reference price for collars and circuit breakers |
--outstanding-shares N |
required | Shares outstanding (for index market caps) |
--tick-decimals N |
2 |
Price grid, 0–8 |
--mm-gateway-id ID |
the only MARKET_MAKER gateway |
Gateway that posts the seed quote |
--mm-bid-price, --mm-ask-price |
maximum allowed spread around the price | Explicit seed prices; give both or neither |
--mm-bid-qty, --mm-ask-qty |
1000 |
Seed quote sizes |
--mm-tif {DAY,GTC} |
DAY |
Time-in-force of the seed quote |
--mm-seed-once / --no-mm-seed-once |
on | Skip the seed when a restored quote already exists |
--field KEY=YAML_VALUE |
none | Optional symbol section: level, collar, order_limits or circuit_breaker; repeatable, e.g. --field 'collar={static_band_pct: 0.2}' |
--version |
— | Print version and exit |
Nothing is written until the edited file passes the same four verification
layers as pm-config-deploy; a refusal leaves both the YAML and the deployed
artifact untouched. It refuses while pm-engine runs (when editing the
deployed source), when the deployed source has undeployed edits, when saved
engine state already exists for the symbol, and when the file is only a copy
or a bundled example.
See Listing a New Symbol for the full list of refusals, how the seed quote is priced and the IPO-day checklist.
pm-mm-bot — Autonomous Market-Maker Bot¶
Runs one autonomous market-maker gateway. Connects as a MARKET_MAKER
participant, posts a two-sided quote per symbol, and automatically reprices on
fills, mid-price drift, and session transitions.
One process is one gateway and can quote several symbols: repeat --symbol,
give --symbols A,B,C, or use --all-symbols. Each symbol is isolated from
the others' failures. To compete on the same symbol, or to keep symbols under
separate gateway IDs, run several instances with different --id-suffix
values.
pm-mm-bot --symbol AAPL [options]
pm-mm-bot --symbol AAPL --gap 0.10 --symbol TSLA --gap 0.50 --strategy inventory_skew --max-position 5000
pm-mm-bot --all-symbols
Per-symbol flags. --symbol may be repeated. A per-symbol flag
(--strategy, --gap, --qty, --max-position, --drift-ticks, --tif,
--anchor-sim, the passive-strategy flags, ...) applies to the symbol named
before it, until the next --symbol. Flags given before the first --symbol
are shared defaults for all symbols. A flag on the command line overrides the
same key in a --config file in the same scope.
Startup options:
| Flag | Default | Description |
|---|---|---|
--symbol |
one of --symbol, --symbols, --all-symbols or a config symbols: block is required |
Instrument to make a market in (e.g. AAPL). Repeatable; per-symbol flags after it apply to it (see above) |
--symbols |
unset | Comma-separated symbols (e.g. AAPL,MSFT) that all share the same settings; shorthand for repeating --symbol with no per-symbol flags; mutually exclusive with --symbol |
--all-symbols |
off | Quote every symbol of the currently deployed configuration with the same settings; gateway ID defaults to MM_ALL_<nn>. Mutually exclusive with --symbol, --symbols and a config symbols: block |
--label |
derived from symbol(s) | Override the gateway-ID symbol segment (default: the single symbol, or SYM1_SYM2_...); mainly to keep a multi-symbol gateway ID short |
--config |
unset | YAML file with gateway-wide settings, shared defaults and per-symbol overrides (bot-level, not the engine config). See Config file below |
--strategy |
symmetric |
Pricing strategy: symmetric, inventory_skew or passive |
--max-position |
unset | Net position (either direction) at which inventory skewing saturates; required when --strategy inventory_skew, an error with any other strategy |
--retreat-ticks |
5 |
passive only: how many ticks beyond its home price each side may step back to stay behind other traders |
--behind-ticks |
1 |
passive only: how many ticks behind other traders' best price a covered side quotes (must be ≥ 1) |
--min-cover-qty |
1 |
passive only: quantity other traders must show inside the retreat band before a side counts as covered |
--fade-ticks |
2 |
passive only: extra ticks a side steps back after it is filled; 0 disables fading |
--fade-sec |
3.0 |
passive only: seconds a fade lasts after a fill; 0 disables fading |
--gap |
0.10 |
Total spread in price units (bid at mid−gap/2, ask at mid+gap/2) |
--qty |
500 |
Quote size on each leg |
--id-suffix |
01 |
Running number for gateway ID (MM_AAPL_01) |
--gateway-id |
derived | Exact gateway ID (e.g. MM01); overrides MM_<symbols>_<id-suffix> |
--drift-ticks |
3 |
Reprice when mid moves by more than this many ticks (passive: when a side's target price does) |
--anchor-sim |
0 (off) |
Quote around pm-market-sim's true value; share of the gap closed per model step (0–1) |
--reissue-delay-ms |
200 |
Milliseconds to wait after fill before re-issuing |
--tif |
DAY |
Time-in-force for quote legs (DAY or GTC) |
--heartbeat-interval-sec |
5.0 |
Periodic live-quote check interval |
--startup-session-timeout-sec |
5.0 |
Max wait to learn the session phase (asked for, or a session.state) |
--bootstrap-timeout-sec |
1.0 |
Max wait for QBOOT reply |
--cancel-timeout-sec |
1.0 |
Max wait for cancel confirmation |
--shutdown-timeout-sec |
2.0 |
Max wait for cancel on SIGINT/SIGTERM |
--qlegs-reconcile-interval-sec |
15.0 |
Periodic QLEGS reconciliation interval |
--initial_min |
unset | Lower bound for random bootstrap price |
--initial_max |
unset | Upper bound for random bootstrap price |
--engine-pull |
tcp://127.0.0.1:5555 |
Engine PUSH/PULL address |
--engine-pub |
tcp://127.0.0.1:5556 |
Engine PUB address |
--log-level |
WARNING |
Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v enables bot debug prints, -vv sets DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. The bot is fully autonomous after startup.
Gateway ID convention:
The gateway ID is constructed as MM_<label>_<suffix>, where the label is the
single symbol, SYM1_SYM2_... for several, or ALL with --all-symbols — for
example, pm-mm-bot --symbol AAPL --id-suffix 02 connects as MM_AAPL_02.
--gateway-id overrides the whole name. Whichever way it is built, the ID must
be pre-registered in engine_config.yaml with role: MARKET_MAKER.
Strategies:
| Strategy | Quotes | Parameters |
|---|---|---|
symmetric |
bid at mid − gap/2, ask at mid + gap/2 | gap, drift_ticks |
inventory_skew |
like symmetric, but shifts both prices against the current position so a long bot sells more eagerly; skew saturates at max_position |
max_position (required) |
passive |
a backstop: steps behind_ticks behind other traders' best price, never tighter than its home price nor outside the retreat_ticks band, and fades fade_ticks for fade_sec after a fill. The mid comes from other traders' book only, so the bot never chases its own quote. If the symbol has a market-maker spread obligation, the band is narrowed so the widest quote still meets it |
the five passive flags |
Config file (--config): a YAML file with version: 1 separates
gateway-wide settings from per-symbol ones:
version: 1
gateway:
label: TECH
id_suffix: "01"
defaults:
gap: 0.10
qty: 500
symbols:
AAPL: {}
TSLA:
gap: 0.50
strategy: inventory_skew
max_position: 5000
Unknown keys are rejected with a suggestion for the likely typo. A
logging: block sets level, target, file and failover_timeout_sec.
Startup behaviour:
- Opens ZMQ PUSH and SUB sockets
- Sends
system.gateway_connectand waits forsystem.gateway_authACK - Requests the symbol list and verifies the assigned symbol exists
- Sends
QBOOT(quote bootstrap) request — if an active quote already exists for(gateway_id, symbol), adopts it instead of creating a duplicate - Sends
QLEGSrequest to reconcile quote-leg mapping - Waits for
session.state(exits if not received within timeout) - Resolves initial reference price and enters the quoting loop
Reference price resolution (in priority order):
- Active quote from
QBOOT(restart recovery) - Book mid-price (if other participants are already quoting)
- Last trade price from
trade.executedevents - Bootstrap quote prices from
QBOOTsnapshot (inactive) - Random price from
[--initial_min, --initial_max]range
State machine:
| State | Meaning |
|---|---|
CONNECTING |
Opening ZMQ sockets |
AUTHENTICATING |
Waiting for gateway auth reply |
WAITING_FOR_SESSION |
Waiting for session to enter CONTINUOUS |
QUOTING |
Active two-sided quote is resting in the book |
REPRICING |
Mid-price drift detected; preparing new quote |
REISSUING |
Sending replacement quote after fill or drift |
PAUSED |
Session is not CONTINUOUS (auction, halt, closed) |
Shutdown (Ctrl-C or SIGTERM):
- Sends
quote.cancelto remove the resting quote - Waits up to
--shutdown-timeout-secfor cancel confirmation - Closes ZMQ sockets and exits
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
system.gateway_connect |
Authentication request |
system.symbols_request |
Request symbol list |
system.quote_bootstrap_request |
Request active quote state for adopt-or-create (QBOOT) |
system.quote_legs_request |
Request quote-leg mapping for reconciliation (QLEGS) |
quote.new |
Submit or replace a two-sided quote |
quote.cancel |
Cancel the active quote (shutdown or session change) |
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
system.gateway_auth.{GW_ID} |
Authentication reply |
system.symbols.{GW_ID} |
Symbol list reply |
system.quote_bootstrap.{GW_ID} |
Bootstrap snapshot with active quote state |
system.quote_legs.{GW_ID} |
Quote-leg reconciliation reply |
book.{SYMBOL} |
Book updates — drives mid-price tracking and drift detection |
depth.{SYMBOL} |
Depth / imbalance metrics (microprice, mid_price) for the assigned symbol |
trade.executed |
Trade events — fallback reference price source |
order.fill.{GW_ID} |
Fill notifications on quote legs |
order.cancelled.{GW_ID} |
Cancel confirmations for quote legs |
quote.ack.{GW_ID} |
Quote accepted/rejected acknowledgements |
quote.status.{GW_ID} |
Quote lifecycle changes (inactivated, cancelled) |
session.state |
Session phase transitions (pause/resume quoting) |
circuit_breaker.halt.{SYMBOL} |
Circuit breaker halt for the assigned symbol |
circuit_breaker.resume.{SYMBOL} |
Circuit breaker resume for the assigned symbol |
See Market-Maker Bot for usage examples, configuration guide, bootstrap details, and troubleshooting.
pm-valuation — IPO Valuation¶
Interviews the student about a fictive company, values it with a two-stage
discounted cash flow, and simulates the IPO that prices it. The result can be
listed with pm-new-symbol. It is a full-screen terminal application and joins
no ZeroMQ socket.
pm-valuation
pm-valuation --case halvard --level intermediate
pm-valuation --load my_scenario.yaml --no-tui --export report.md
Startup options:
| Flag | Default | Description |
|---|---|---|
--load FILE |
— | Scenario file to start from (mutually exclusive with --case) |
--case {halvard,tornfalk} |
— | Classroom case to start from |
--no-tui |
off | Print the report instead of interviewing |
--level {beginner,intermediate,advanced,expert} |
beginner |
Interview detail; F3 changes it in the application |
--market {se,us} |
scenario's, else se |
Where the company lists: Sweden or the US; overrides the scenario |
--mode {deterministic,montecarlo,both} |
scenario's, else both |
Scenarios, Monte Carlo, or both |
--draws N |
— | Monte Carlo draws |
--seed N |
— | Monte Carlo seed |
--save FILE |
— | Write the scenario to this file |
--with-defaults |
off | With --save: write every value, commented with its source |
--export FILE |
— | Write the report as Markdown |
--pdf FILE |
— | Write the report as a printable PDF (also p in the application) |
--paper {a4,letter} |
— | PDF page size |
--presets FILE |
bundled | Alternative presets file |
--list |
off | With --no-tui: list the priced IPO with pm-new-symbol |
--config FILE |
deployed source | With --list: the engine YAML that pm-new-symbol edits |
--version |
— | Print version and exit |
See IPO Valuation for the finance behind the model and the interview, and the Training Guide's IPO chapter for the classroom exercise.
pm-ralf-gwy — Post-Trade Dissemination Gateway¶
Runs the external machine-facing post-trade dissemination gateway that publishes RALF over TCP for clearing, drop-copy, and audit consumers.
pm-ralf-gwy [--bind 0.0.0.0] [--port 5580] [--engine-pub tcp://127.0.0.1:5556] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--bind |
from config / 0.0.0.0 |
TCP bind address for external clients |
--port |
from config / 5580 |
TCP listen port for RALF clients |
--engine-pub |
tcp://127.0.0.1:5556 |
Engine PUB address consumed by the gateway |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
No terminal input. External parties connect over TCP and send RALF lines:
HELLO|...SUB|...UNSUB|...PINGEXIT
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
trade.executed |
Source for EXEC post-trade events |
system.eod |
Source for EOD summary markers |
Messages published (TCP RALF feed):
| Message type | Purpose |
|---|---|
WELCOME |
Session acceptance and capabilities |
SNAP |
Subscription/recovery baseline |
EXEC |
Live execution dissemination |
EOD |
End-of-day summary marker |
HB |
Heartbeat |
PONG |
Ping reply |
ERR |
Protocol/entitlement/replay errors |
EXIT |
Session termination reason |
For external usage and examples see Post-Trade Dissemination and RALF Protocol Reference.
pm-ralf-spy (RALF Protocol Spy)¶
pm-ralf-spy is a read-only CLI client for pm-ralf-gwy: it opens a RALF
TCP session under a chosen --role (CLEARING, DROP_COPY, or AUDIT),
subscribes to whatever channels/symbols you ask for, and prints every line
it receives — human-readable or as JSON — so you can see exactly what the
protocol sends without writing a client. It never mutates exchange state,
and any number of instances can run at once (e.g. one per terminal, each
with a different --role/--channels/--symbols filter). Since it never
sends anything on its own after the initial subscription, it sends a
periodic PING (--ping-interval, default 60s) to keep the gateway from
dropping it as an idle client.
Startup options:
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
pm-ralf-gwy TCP host |
--port |
5580 |
pm-ralf-gwy TCP port |
--client-name |
ralf-spy-<pid> |
HELLO\|CLIENT= identifier reported in gateway logs |
--role |
AUDIT |
HELLO\|ROLE= to authenticate as: CLEARING, DROP_COPY, or AUDIT |
--channels |
* |
Comma-separated channels; * = every channel --role is entitled to |
--symbols |
* |
Comma-separated symbols; * = every symbol |
--lastseq |
0 |
Requests replay on connect via HELLO\|LASTSEQ=N (0 = no replay) |
--ping-interval |
60 |
Seconds between PING keepalives sent to the gateway; 0 disables |
--format |
human |
human or json output |
--count |
0 |
Exit after N data-carrying lines (0 = run until Ctrl-C) |
--raw |
off | Print raw protocol bytes instead of decoded fields |
--no-color |
off | Disable ANSI colour in output |
--show-heartbeats |
off | Include HB heartbeat lines in output (hidden by default) |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
See RALF Protocol Spy (pm-ralf-spy) for full usage.
pm-dc-gwy — Drop-Copy TCP Gateway¶
Runs a small external-facing TCP gateway that relays the engine's drop-copy
feed (DropCopyPublisher, PUB :5557) to plain-TCP clients using the DC1
text protocol — no ZeroMQ dependency required on the client side. It is the
TCP counterpart to pm-dc-spy: both subscribe internally to
drop_copy.event.<gateway_id>, but pm-dc-gwy fans events out to any number
of concurrently connected external clients instead of printing to a
terminal. There is no authentication, entitlement model, or replay-by-sequence
— any client may HELLO with any gateway ID and receive that gateway's
live fills for as long as it stays connected.
pm-dc-gwy [--bind 0.0.0.0] [--port 5590] [--engine-dc-pub tcp://127.0.0.1:5557] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--bind |
from config / 0.0.0.0 |
TCP bind address for external clients |
--port |
from config / 5590 |
TCP listen port for DC1 clients |
--engine-dc-pub |
tcp://127.0.0.1:5557 |
Engine drop-copy PUB address consumed by the gateway |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
No terminal input. External parties connect over TCP and send DC1 lines:
HELLO|CLIENT=..|PROTO=DC1|ID=..PINGEXIT
Messages subscribed (internal SUB from :5557, refcounted per gateway ID requested by connected clients):
| Topic | Purpose |
|---|---|
drop_copy.event.<gateway_id> |
Source for DC_FILL relay lines |
Messages published (TCP DC1 feed):
| Message type | Purpose |
|---|---|
WELCOME |
Session acceptance |
DC_FILL |
Unsolicited fill relay |
HB |
Heartbeat |
PONG |
Ping reply |
ERR |
Protocol errors |
For external usage and examples see Drop-Copy TCP Gateway.
pm-dc-spy (Drop-Copy Spy)¶
pm-dc-spy is a read-only CLI client for the engine's drop-copy feed
(DropCopyPublisher, PUB :5557): it opens a plain zmq.SUB connection —
no handshake, no heartbeat, unlike CALF/RALF — subscribes to fill events
for one gateway or all gateways, and prints every message it receives,
human-readable or as JSON. It never mutates exchange state, and any number
of instances can run at once (e.g. one per terminal, each with a different
--gateway filter).
Startup options:
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Drop-copy PUB socket host |
--port |
5557 |
Drop-copy PUB socket port |
--gateway |
(none) | Only show fills for this gateway; default subscribes to all gateways |
--replay-of |
(none) | Also subscribe to drop_copy.replay.<RECIPIENT_ID> to observe DropCopyPublisher.replay() calls |
--format |
human |
human or json output |
--count |
0 |
Exit after N messages (0 = run until Ctrl-C) |
--raw |
off | Print raw bytes instead of decoded fields |
--no-color |
off | Disable ANSI colour in output |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
See Drop-Copy Spy (pm-dc-spy) for full usage.
pm-index — Index Calculation Process¶
Subscribes to trade events from the engine and maintains one or more configurable
cap-weighted market indices in real time. Publishes live index values on a
dedicated ZMQ PUB socket so pm-md-gwy can forward them to external subscribers
over the CALF INDEX channel.
pm-index [--reset] [--log-level LEVEL] [-v|-vv] [-q] [--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--reset |
off | Delete persisted state files and reinitialise all indices from config |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. pm-index is a long-running background process after startup.
Startup behaviour:
1. Reads all indices: entries from engine_config.yaml
2. For each index: loads divisor and last prices from the state file if present, or initialises fresh from constituent reference prices and base_value
3. Validates that every constituent symbol appears in symbols: with a positive outstanding_shares
4. Writes an INIT structural/audit history record for each index that starts without prior state
5. Binds ZMQ PULL :5559 for operator commands and history requests
6. Binds ZMQ PUB :5558 for broadcasting index.update messages
7. Subscribes to engine PUB :5556 for trade.executed, session.state, and system.eod
8. Enters the poll loop
Shutdown (Ctrl-C): 1. Flushes all pending structural/audit JSONL history writes 2. Closes ZMQ sockets
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
trade.executed |
Drives index recalculation for constituent symbols |
session.state |
Tracks intraday OHLC reset points and triggers EOD finalisation on CLOSED |
system.eod |
Alternate EOD trigger — publishes the final index.update for the day |
Messages received (PULL :5559):
| Topic | Purpose |
|---|---|
index.history_request |
Returns structural/audit records (INIT, CORP_ACTION, ADD_CONSTITUENT, DELIST) from the JSONL history file — not level/EOD data, which lives in pm-stats |
index.corp_action |
Applies a stock split, cash dividend, or shares-issuance adjustment |
index.constituent_change |
Adds or removes a constituent symbol from an index basket |
Messages published (PUB :5558):
| Topic | Purpose |
|---|---|
index.update |
Current index level, OHLC, aggregate cap, divisor, and session state — also consumed by pm-stats for level/EOD history |
index.history.{GW_ID} |
Structural/audit history query response addressed to the requesting gateway |
index.corp_action_ack.{GW_ID} |
Corporate action applied / rejected acknowledgement |
index.constituent_change_ack.{GW_ID} |
Constituent add or delist accepted / rejected acknowledgement |
index.error.{GW_ID} |
Generic error for malformed or rejected requests |
Persistence:
| File | Purpose |
|---|---|
data/indexes/<ID>_state.json |
Divisor, last prices, and intraday OHLC checkpoint — rewritten after each EOD and corporate action |
data/indexes/<ID>_history.jsonl |
Append-only JSONL structural/audit trail with INIT, CORP_ACTION, ADD_CONSTITUENT, and DELIST records only — level and EOD history is recorded by pm-stats in data/stats.db instead (see the pm-stats section) |
See Market Index (pm-index) for configuration details, calculation explanation, and corporate action procedures.
pm-api-gwy — REST/WebSocket API Gateway¶
Exposes EduMatcher order entry, order management, reference data, history, and
market data over REST/JSON and WebSocket. It is not a second matching engine —
it translates HTTP and WebSocket requests into the same ZMQ messages used by the
interactive pm-alf-console terminal. Reads configuration from the api_gateways:
section of engine_config.yaml.
pm-api-gwy [--instance NAME] [--host ADDR] [--port PORT] [--engine-host HOST] [--stats-db PATH] \
[--clearing-db PATH] \
[--log-level LEVEL] [-v|-vv] [-q] [--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--instance NAME |
auto-selected when only one entry exists | Named api_gateways entry to run; required when more than one entry is configured |
--host ADDR |
config value | Override HTTP bind address |
--port PORT |
config value | Override HTTP listen port |
--engine-host HOST |
config value | Override engine host for ZMQ connections (cross-host deployments) |
--stats-db PATH |
config value | Path to data/stats.db for /history/* endpoints |
--clearing-db PATH |
config value | Path to data/clearing.db, opened read-only for the session/MTD/YTD realized P&L on /positions. Optional: without it those three fields report null |
--log-level LEVEL |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
Expected runtime input arguments:
No terminal input. External clients connect via HTTP or WebSocket.
Startup behaviour:
1. Reads the named api_gateways.<NAME> entry from engine_config.yaml
2. Exits immediately if enabled: false
3. Sends system.gateway_connect to the engine and waits for system.gateway_auth ACK
4. Starts the uvicorn HTTP server and enters the request-serving loop
REST endpoints (base path /api/v1):
| Method | Path | Key type | Purpose |
|---|---|---|---|
POST |
/orders |
trading | Submit an order |
DELETE |
/orders/{id} |
trading | Cancel an order |
PATCH |
/orders/{id} |
trading | Amend price and/or quantity |
GET |
/orders |
trading | List live orders |
POST |
/oco |
trading | Submit OCO pair |
POST |
/combos |
trading | Submit combo order |
POST |
/quotes |
trading | Submit two-sided quote |
DELETE |
/quotes/{symbol} |
trading | Cancel active quote |
POST |
/mass-cancel |
trading | Cancel all or symbol-scoped exposure |
GET |
/symbols |
trading | Instrument metadata for the caller |
GET |
/session |
any | Session phase, timezone, trading date |
GET |
/positions |
trading | Net positions by symbol |
GET |
/history/orders |
trading | Historical order lifecycle events |
GET |
/history/fills |
trading | Historical fills |
GET |
/history/trades |
any | Public trade log from pm-stats |
GET |
/history/daily |
any | Daily OHLCV from pm-stats |
WebSocket endpoints:
| Path | Key type | Stream |
|---|---|---|
/ws/private |
trading | Private fills, acks, expiries — filtered to the connected gateway |
/ws/market-data |
any | Public order book snapshots and trade events |
/ws/admin/monitor |
ADMIN | Cross-gateway monitor feed |
/ws/admin/sim |
ADMIN | pm-market-sim true values and heartbeat |
This is a summary. The full contract is the REST API Reference and the WebSocket API Reference.
Authentication:
- REST:
Authorization: Bearer <api_key>header - WebSocket: send
{"api_key": "<key>"}as the first JSON message
Trading keys (gateway_id set to a gateway ID) can reach order-entry endpoints
and private event streams. Read-only keys (gateway_id: null) are limited to
the public endpoints: reference data, session, halts, public history and market
data.
Tip
When swagger_enabled: true in config, browse http://127.0.0.1:<PORT>/docs
for interactive endpoint documentation and a live try-it UI.
Messages sent (PUSH → :5555, the engine):
| Group | Topics |
|---|---|
| Session | system.gateway_connect, system.gateway_heartbeat, system.gateway_disconnect |
| Order entry | order.new, order.cancel, order.amend, order.oco, order.oco_cancel, order.combo, order.combo_cancel, quote.new, quote.cancel, risk.kill_switch (/mass-cancel) |
| Queries | order.orders_request, system.symbols_request, system.reference_request, system.session_state_request, system.session_schedule_request, system.position_request, system.halt_status_request, system.quote_bootstrap_request, system.quote_legs_request, system.gateways_request, system.risk_state_request, system.volume_request, book.snapshot_request, order.price_level_orders_request |
| Admin | session.transition, system.reference_reload, risk.symbol_halt, risk.symbol_resume, risk.cancel_symbol, risk.kill_switch_gateway, risk.kill_switch_global, risk.circuit_breaker_halt_all, risk.circuit_breaker_resume_all, risk.force_uncross |
To pm-index (its own PULL socket): index.rebalance, index.history_request.
Messages subscribed: every topic the engine publishes (SUB from :5556 with
an empty filter). The gateway routes each event itself: one addressed to a
gateway goes to that gateway's private sockets, the rest to market data, and
all of it to the admin monitor (see the
WebSocket API Reference).
It also subscribes to pm-index and pm-market-sim for index history and the
simulator's state, values and news.
See API Gateway (REST/WebSocket) for configuration, keys, caches and operation, and Protocol Support Library Examples for client code.
pm-admin — Interactive Admin Console¶
An interactive REPL for sending operational commands to a running engine without needing a full gateway session.
pm-admin --id <ADMIN_GW_ID> [--log-level LEVEL] [-v|-vv] [-q] [--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Required | Description |
|---|---|---|
--id |
Yes | ADMIN gateway ID configured in engine_config.yaml (e.g. GW_ADMIN) |
--log-level |
No | Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG (default WARNING) |
-v / --verbose |
No | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
No | Reduce output to warnings/errors |
--log-target |
No | Where operational log records go: server (auto-detected pm-log-srv), stdout, or file (default server) |
--log-file |
No | Operational log file path — required when --log-target file |
--log-failover-timeout |
No | Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable (default 30 from config) |
--version |
No | Print version and exit |
Expected runtime input arguments:
HALT,RESUMEHALT_SYM|SYM=<sym>,RESUME_SYM|SYM=<sym>,CANCEL_SYM|SYM=<sym>REOPEN|SYM=<sym>[|PRICE=<px>][|DRY_RUN=1][|NOTE=<text>]KILL|GW=<gw>[|SYM=<sym>]KICK|GW=<gw>[|REASON=<text>]QCANCEL|GW=<gw>|SYM=<sym>BOOK|SYM=<sym>ORDERS|GW=<gw>LEVEL|SYM=<sym>[|PRICE=<px>]SYMBOLSSESSION|STATE=<PRE_OPEN|OPENING_AUCTION|CONTINUOUS|CLOSING_AUCTION|CLOSED>SESSION_STATUS,SCHEDULE,GATEWAYS,VOLUMEPOSITION|GW=<gw>[|SYM=<sym>,..]HELP,EXIT,QUIT
Messages sent (PUSH → :5555):
| Topic | Purpose |
|---|---|
system.gateway_connect |
Authentication request (once at startup) |
risk.circuit_breaker_halt_all |
HALT — halt all symbols (requires ADMIN gateway role) |
risk.circuit_breaker_resume_all |
RESUME — resume all halted symbols (requires ADMIN gateway role) |
risk.symbol_halt |
HALT_SYM — halt one symbol |
risk.symbol_resume |
RESUME_SYM — resume one symbol |
risk.cancel_symbol |
CANCEL_SYM — cancel all resting orders on one symbol |
risk.force_uncross |
REOPEN — force a symbol to uncross now and clear any halt |
risk.kill_switch |
KILL — cancel all (or symbol-scoped) orders/quotes for a gateway |
system.gateway_disconnect |
KICK — forcefully disconnect a gateway (no ack published) |
quote.cancel |
QCANCEL — cancel a gateway's active two-sided quote |
book.snapshot_request |
BOOK — request a book snapshot |
order.orders_request |
ORDERS — request a gateway's resting orders |
order.price_level_orders_request |
LEVEL — request every resting order at a symbol (optionally one price level) |
system.symbols_request |
SYMBOLS — request the instrument list |
session.transition |
SESSION — request a session-phase transition |
system.session_state_request |
SESSION_STATUS — request the current session state |
system.session_schedule_request |
SCHEDULE — request the configured schedule |
system.gateways_request |
GATEWAYS — request gateway connection states |
system.volume_request |
VOLUME — request the daily volume summary |
system.position_request |
POSITION — request a gateway's net position snapshot |
Messages subscribed (SUB from :5556):
| Topic | Purpose |
|---|---|
system.gateway_auth.{GW_ID} |
Authentication reply |
risk.circuit_breaker_halt_all_ack.{GW_ID} |
HALT acknowledgement |
risk.circuit_breaker_resume_all_ack.{GW_ID} |
RESUME acknowledgement |
risk.symbol_halt_ack.{GW_ID} |
HALT_SYM acknowledgement |
risk.symbol_resume_ack.{GW_ID} |
RESUME_SYM acknowledgement |
risk.cancel_symbol_ack.{GW_ID} |
CANCEL_SYM acknowledgement |
risk.force_uncross_ack.{GW_ID} |
REOPEN acknowledgement, with indicative/printed price |
risk.kill_switch_ack.{GW_ID} |
KILL acknowledgement |
quote.ack.{GW_ID} |
QCANCEL acknowledgement |
book.{SYMBOL} |
BOOK reply — book snapshot |
order.orders.{GW_ID} |
ORDERS reply — resting order list |
order.price_level_orders.{GW_ID} |
LEVEL reply — per-order book composition |
system.symbols.{GW_ID} |
SYMBOLS reply |
system.session_status.{GW_ID} |
SESSION_STATUS reply |
system.session_schedule.{GW_ID} |
SCHEDULE reply |
system.gateways.{GW_ID} |
GATEWAYS reply |
system.volume.{GW_ID} |
VOLUME reply |
system.position_snapshot.{GW_ID} |
POSITION reply |
session.state |
Session phase changes (broadcast; also the SESSION reply) |
pm-admin-cli — CLI Admin Commands¶
A non-interactive alternative to pm-admin for scripting or single-shot
operational commands.
Each invocation sends one command to the engine, waits for an acknowledgement, prints the result, and exits. Suitable for shell scripts and CI automation.
Startup options:
| Flag | Required | Description |
|---|---|---|
--id <GW_ID> |
Yes | ADMIN gateway ID |
--push <ADDR> |
No | Engine PULL address (default from config) |
--sub <ADDR> |
No | Engine PUB address (default from config) |
--timeout <MS> |
No | Ack timeout in milliseconds (default: 3000) |
--format {text,json} |
No | text (default) prints a human-readable result; json prints it machine-readable to stdout, for scripts |
--version |
No | Print version and exit |
Expected runtime input arguments:
Subcommands and required arguments:
| Command | Required arguments | Optional arguments | Purpose |
|---|---|---|---|
halt |
none | none | Exchange-wide halt |
resume |
none | none | Exchange-wide resume |
halt-sym |
--sym <SYMBOL> |
none | Halt one symbol |
resume-sym |
--sym <SYMBOL> |
none | Resume one symbol |
cancel-sym |
--sym <SYMBOL> |
none | Cancel all resting orders on one symbol |
reopen |
--sym <SYMBOL> |
--price <PRICE>, --dry-run, --note <TEXT> |
Force a symbol to uncross now and clear any halt |
kill |
--gw <GW_ID> |
--sym <SYMBOL> |
Cancel all (or symbol-scoped) orders/quotes for gateway |
kick |
--gw <GW_ID> |
--reason <TEXT> |
Disconnect a gateway |
qcancel |
--gw <GW_ID> --sym <SYMBOL> |
none | Cancel active quote for gateway on symbol |
book |
--sym <SYMBOL> |
none | Fetch book snapshot |
orders |
--gw <GW_ID> |
none | List resting orders for gateway |
level |
--sym <SYMBOL> |
--price <PRICE> |
Show every resting order for a symbol (or one price level), across all gateways |
symbols |
none | none | List configured instruments |
session |
--state <STATE> |
none | Request session transition |
session-status |
none | none | Read current session state |
schedule |
none | none | Read configured schedule |
gateways |
none | none | List gateway states |
volume |
none | none | Show daily volume summary |
position |
--gw <GW_ID> |
none | Show net position, avg cost, and live bid/ask/spread/mid per symbol for gateway |
pm-cverifier — Config Verifier¶
Validates engine_config.yaml across four layers: YAML syntax, JSON schema, semantic checks, and completeness checks. Produces a human-readable or JSON report and exits with a non-zero code if any findings meet or exceed the minimum severity threshold.
Startup options:
| Flag | Default | Description |
|---|---|---|
CONFIG_FILE |
required | Path to engine_config.yaml |
--format |
text |
Output format: text or json |
--level |
info |
Minimum severity to show: info, warn, or error |
--no-color |
off | Disable ANSI colour in text output |
--strict |
off | Treat warnings as errors for CI exit-code purposes |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. pm-cverifier reads the config file, prints its report, and exits.
Exit codes:
0— no findings at or above the threshold (or--strictand no warnings/errors)1— one or more findings at or above the threshold
pm-config-show — Config Viewer¶
Prints the effective configuration as a terminal dashboard, or renders it as a
multi-page A4 PDF, and exits. Where pm-cverifier answers is this file
correct, pm-config-show answers what does this file say — including the
port map, which cannot be read off the YAML at all because the engine and index
sockets live in config.py and a gateway section without a port: key still
binds on its runtime default.
pm-config-show
pm-config-show -m 2
pm-config-show --all
pm-config-show -f my_config.yaml
pm-config-show --format pdf -o exchange.pdf
Startup options:
| Flag | Default | Description |
|---|---|---|
-f, --file <YAML> |
<DATA_DIR>/ref_data/engine_config.yaml |
Config file to read |
-m, --density [1\|2] |
0 (bare -m means 1) |
Pack more information in; 1 adds risk and gateway detail, 2 adds every knob |
-a, --all |
off | Show everything: implies -m 2, unmasks API keys, lists unrecognised keys |
--format <FMT> |
terminal |
Output format: terminal or pdf |
-o, --output <FILE> |
engine-config-<stem>.pdf |
Destination file for --format pdf |
--no-color |
off | Disable ANSI colour; also implied when stdout is not a TTY or NO_COLOR is set |
--ascii |
off | ASCII box drawing; auto-enabled on non-UTF-8 terminals |
--width <N> |
terminal width (100 when piped) |
Force render width — for scripted capture and piping |
--height <N> |
terminal height | Force render height — affects default-density height trimming only |
--version |
off | Print version and exit |
Expected runtime input arguments:
None. pm-config-show reads the config file, prints its dashboard (or writes
the PDF), and exits.
Exit codes:
0— the configuration was displayed, or the PDF was written2— the config file does not exist or cannot be read3— the file is not valid YAML (the message points atpm-cverifier)
With no --file it reads the deployed configuration from
<DATA_DIR>/ref_data/engine_config.yaml, so what it shows is what the exchange
was configured from. It is strictly read-only: it never writes to the
configuration or the data directory, and --output is the only path it creates.
The layout is computed from the terminal actually in use rather than a fixed
column count — panels pack side by side where they fit, short panels stack
beside tall ones, and tables shed optional columns as they narrow. Below 72
columns or 18 rows it falls back to a plain summary of counts, flags, and the
port map. At the default density the output is also trimmed to the window
height, dropping optional panels and then shortening the symbol list; both
trims say so and name the flag that restores the content. Passing -m or -a
disables height trimming.
Two details matter operationally:
- Ports. Every listener appears in one table with its process, function,
bind address, and where the value came from —
fixed(aconfig.pyconstant),env(an environment override),set(explicit in the file), ordefault(the section is present but omitsport:). A port claimed twice is flagged in red, the same conditionpm-cverifierreports asM018. - API keys. Masked by default and revealed with
--all. A key is never wrapped or truncated at any width, and no styling is applied inside the token, so a terminal double-click selects the whole thing. Masked and revealed keys are the same length, so--allnever moves the layout.
Density is a layout control and --all is a disclosure control, which is
why they are separate flags: -m 2 shows every setting while keeping keys
masked, so it stays safe to run on a projector. Note that a PDF produced with
--all contains live credentials — masking is per-run, not per-file.
For the panel-by-panel walkthrough and a worked example, see
Inspect Configs with pm-config-show.
This tool is local inspection logic and does not participate in the ZeroMQ runtime message bus.
pm-setup — Session Bootstrap Tool¶
Bootstraps a runnable EduMatcher session directory with sensible defaults.
Startup options:
| Flag | Default | Description |
|---|---|---|
--data-dir <PATH> |
$EDUMATCHER_DATA_DIR, else <repo>/src/data in a source checkout, else ~/.local/share/edumatcher |
Data directory for persistent files |
--show |
off | Print the data directory that would be used and exit |
--config <NAME> |
s10-basic |
Bundled example to deploy, e.g. s1-basic, s3-nominal, s10-complex, s150-basic. Resolved under docs/examples/ref_data/; append -nomm for the variant without market-maker quotes (s1-basic-nomm) |
--force |
off | Replace an already-deployed config |
--no-config |
off | Create the data dir only; deploy nothing |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
It creates the data directory and deploys a bundled sample configuration
(s10-basic unless --config names another) to
<DATA_DIR>/ref_data/engine_config.json, so a fresh environment can start the
exchange immediately.
Use this once per new environment (VM, container volume, or fresh local workspace) before starting runtime processes.
This tool is local bootstrap logic and does not participate in the ZeroMQ runtime message bus.
pm-config-deploy — Compile and Install a Configuration¶
Validates an authored engine_config.yaml, compiles it, and installs the
result as the one file every process reads.
pm-config-deploy my_config.yaml
pm-config-deploy --check my_config.yaml
pm-config-deploy --example s3-basic
pm-config-deploy --show
Startup options:
| Flag | Default | Description |
|---|---|---|
SOURCE |
— | Authored engine_config.yaml to validate, compile and install |
--example NAME |
— | Use a bundled example instead of SOURCE, e.g. s3-basic (from docs/examples/ref_data/s3-basic-setup/engine_config.yaml); append -nomm for the variant without market-maker quotes |
--check |
off | Validate and compile, but install nothing — for CI |
--show |
off | Print the deployed paths and exit |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. It reads SOURCE, writes
<DATA_DIR>/ref_data/engine_config.json plus a copy of the source beside it,
and exits.
Validation runs all four pm-cverifier layers. That is stricter than starting
used to be: a configuration with, say, a MARKET_MAKER gateway and no
market_maker_quotes (M001), or an API credential naming a gateway_id
absent from participants (M022), will now refuse to deploy where it
previously ran. Warnings do not block — a command that refused on advice would
push people back towards editing the deployed copy by hand.
Compiling also resolves every default exactly once. That is what allows the runtime loaders to deserialise rather than each keep its own copy of the defaults for its section.
A configuration that fails is not installed and the previous artifact is left untouched — the write is staged and renamed, so a process starting during a deploy sees either the old file or the new one, never half of either.
The command prints the symbol and gateway counts it installed. Those numbers are worth reading: an unexpected zero is the difference between an exchange and a gateway that accepts connections but has no instruments to offer.
Deploying does not disturb running processes. Restart them to pick up the new configuration; until you do, each will warn at startup that its source has moved on.
This tool is local bootstrap logic and does not participate in the ZeroMQ runtime message bus.
pm-config-gen — Engine Config Generator¶
Generates an engine_config.yaml from explicit CLI parameters so environments
can be recreated without manual YAML editing.
pm-config-gen --symbols AAPL MSFT --participants TRADER01 TRADER02 OPS01:ADMIN --sessions-enabled --output engine_config.yaml
Startup options:
Required¶
| Flag | Description |
|---|---|
--symbols SYM [SYM ...] |
One or more symbols |
--participants GW_SPEC [...] |
One or more gateway specs (ID[:ROLE[:DISCONNECT]]); ID is A-Z, 0-9, _, starting with a letter |
Gateway / symbol overrides¶
| Flag | Description |
|---|---|
--participant-smp GW_ID:SMP_ACTION |
Per-gateway self-match-prevention default (NONE, CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH); repeatable |
--participant-desc GW_ID:TEXT |
Per-gateway description; quote it when TEXT has spaces; repeatable |
--participant-default-smp SMP_ACTION |
Writes participant_defaults.smp_action, inherited by gateways without their own |
--participant-default-disconnect DISCONNECT |
Writes participant_defaults.disconnect_behaviour, inherited by gateways whose spec names no disconnect behaviour |
--symbol-opts SYMBOL:KEY=VALUE[,...] |
Per-symbol overrides; repeatable |
--symbol-static-band SYM:PCT |
Per-symbol collar static band in (0,1); repeatable |
--symbol-dynamic-band SYM:PCT |
Per-symbol collar dynamic band in (0,1); repeatable |
--symbol-risk-level SYM:LEVEL |
Per-symbol risk level key; repeatable |
--symbol-max-order-qty SYM:N |
Per-symbol order_limits.max_order_qty in shares (> 0); repeatable |
--symbol-max-order-value SYM:AMOUNT |
Per-symbol order_limits.max_order_value as notional (> 0); repeatable |
--outstanding-shares SYM:N |
Per-symbol outstanding shares; repeatable |
Session & engine tuning¶
| Flag | Default | Description |
|---|---|---|
--sessions-enabled / --no-sessions-enabled |
disabled | Enable/disable scheduler-driven sessions |
--snapshot-interval SECS |
0.5 |
Snapshot interval in seconds |
--quote-history-maxlen N |
30 |
Quote history max length |
--drop-copy-buffer-size N |
10000 |
Drop-copy buffer size |
--recent-trades-maxlen N |
20 |
Recent trades max length |
--depth-snapshot-tolerance-ticks N |
100 |
Depth snapshot tolerance in ticks |
Collars & circuit breakers¶
| Flag | Default | Description |
|---|---|---|
--no-collars |
off | Set enforce_collars: false |
--no-circuit-breakers |
off | Set enforce_circuit_breakers: false |
--static-band PCT |
unset | Default static collar band |
--dynamic-band PCT |
unset | Default dynamic collar band |
--risk-level NAME:STATIC_PCT[:DYNAMIC_PCT] |
— | Risk level definition; repeatable |
--cb-levels NAME:SHIFT_PCT[:HALT_MINS[:RESUMPTION_MODE]] |
— | Circuit-breaker level definition; repeatable |
--cb-window-ns NS |
300000000000 |
Circuit-breaker reference window in nanoseconds |
Reopening corridor (ACE)¶
Automated Corridor Expansion governs how a halted symbol reopens. It is on by default; these flags tune or disable it.
| Flag | Default | Description |
|---|---|---|
--no-ace |
off | Disable ACE: a halt then reopens at the equilibrium price whatever it is, with no corridor and no extensions |
--ace-initial-band PCT |
0.1 |
Reopening corridor half-width as a fraction of the reference price |
--ace-expansions WIDEN_PCT:MINUTES [...] |
0.10:2 0.20:5 |
Rungs of the expansion ladder, in order. The last rung repeats indefinitely, which is what lets the corridor eventually contain any price (the default is Nasdaq's ladder) |
--ace-random-end-ns NS |
30000000000 (30 s) |
Upper bound of the uniform random tail added to every call phase; 0 disables the random end and makes reopen times predictable |
--ace-random-seed N |
OS entropy | Seed the random-end generator for reproducible demos. Omit it on a real venue |
Market-maker obligations¶
| Flag | Default | Description |
|---|---|---|
--mm-spread-ticks N |
20 |
Global MM max spread ticks |
--mm-min-qty N |
100 |
Global MM minimum quote quantity |
--enforce-mm-obligations / --no-enforce-mm-obligations |
disabled | Enable/disable MM obligation enforcement |
Symbols / seeding¶
| Flag | Default | Description |
|---|---|---|
--tick-decimals N |
2 |
Default tick decimals (0..8) |
--seed-last-prices |
off | Emit null last-price placeholders |
--seed N |
— | RNG seed for generated training values |
--seed-mm-mid-range MIN:MAX |
— | Seed MM quotes from a random midpoint |
--mm-seed-spread-ticks N |
10 |
Half-spread, in ticks, either side of the seeded MM midpoint |
--seed-last-prices-from-mm |
off | Set last prices from the seeded MM midpoint |
--no-mm-seed-quotes |
off | Write require_mm_seed_quotes: false and no market_maker_quotes, so the book starts genuinely empty even though a MARKET_MAKER gateway exists. --seed-mm-mid-range then only seeds last prices (with --seed-last-prices-from-mm) |
Gateway sections¶
Each flag below enables a top-level YAML section for the corresponding gateway process.
| Flag | Emits | Key sub-flags |
|---|---|---|
--post-trade-gateway |
post_trade_gateway: for pm-ralf-gwy |
--post-trade-name, --post-trade-bind-address, --post-trade-port, --post-trade-replay-retention-sec, --post-trade-heartbeat-interval-sec, --post-trade-idle-timeout-sec, --post-trade-max-connections, --post-trade-max-client-queue, --post-trade-allowed-roles |
--market-data-gateway |
market_data_gateway: for pm-md-gwy |
--market-data-name, --market-data-bind-address, --market-data-port, --market-data-heartbeat-interval-sec, --market-data-idle-timeout-sec, --market-data-replay-window-sec, --market-data-max-connections, --market-data-max-symbols-per-client, --market-data-max-client-queue, --market-data-depth-levels, --market-data-enabled / --market-data-disabled |
--alf-gateway |
alf_gateway: for pm-alf-gwy |
--alf-enabled, --alf-disabled, --alf-name, --alf-bind-address, --alf-port, --alf-heartbeat-interval-sec, --alf-handshake-timeout-sec, --alf-idle-timeout-sec, --alf-max-connections, --alf-max-client-queue, --alf-max-commands-per-second, --alf-max-errors-before-disconnect, --alf-error-window-sec |
--balf-gateway |
balf_gateway: for pm-balf-gwy |
--balf-name, --balf-bind-address, --balf-port, --balf-heartbeat-interval-sec, --balf-heartbeat-timeout-sec, --balf-idle-timeout-sec, --balf-auth-timeout-sec, --balf-max-connections, --balf-max-client-queue, --balf-max-messages-per-second, --balf-max-errors-before-disconnect, --balf-error-window-sec, --balf-duplicate-session-policy |
--api-gateway |
api_gateways: for pm-api-gwy |
--api-gateway-name, --api-gateway-instance NAME:GATEWAY[,GATEWAY...][:PORT] (repeatable), --api-gateway-host, --api-gateway-port, --api-gateway-log-level, --api-gateway-stats-db, --api-gateway-swagger-enabled / --api-gateway-swagger-disabled, --api-gateway-enabled / --api-gateway-disabled, --api-gateway-rate-limit-writes-per-second, --api-gateway-rate-limit-burst, --api-gateway-engine-auth-sec, --api-gateway-engine-reply-sec, --api-gateway-wait-ack-sec, --api-gateway-order-retention-sec; credential flags: --api-key KEY:GATEWAY_ID[:DESCRIPTION] (repeatable), --api-gateway-generate-keys / --no-api-gateway-generate-keys, --api-gateway-readonly-key |
--dc-gateway |
dc_gateway: for pm-dc-gwy |
--dc-name, --dc-bind-address, --dc-port, --dc-heartbeat-interval-sec, --dc-idle-timeout-sec, --dc-max-connections, --dc-max-client-queue |
--log-server |
log_server: for pm-log-srv |
--log-server-enabled/-disabled, --log-server-name, -bind-address, -port, -db-path, -retention-days, -max-message-bytes, -max-client-queue, -write-batch-size, -write-batch-interval-ms, -heartbeat-interval-sec, -pubsub-enabled/-disabled, -pub-port, -pull-port, -lease-sec, -max-lease-sec, -max-subscribers, -notify-interval-ms, -backfill-chunk-rows, -max-backfill-minutes, -max-backfill-rows, -max-pending-rows, -pub-sndhwm (each prefixed --log-server) |
--monitor |
monitor: for pm-monitor and every health beacon, with every field |
--monitor-enabled/-disabled, --monitor-bind-address, --monitor-host, --monitor-pull-port, --monitor-pub-port, --monitor-beacon-interval-sec, --monitor-hung-after-sec, --monitor-silent-after-beacons, --monitor-book-metrics-interval-sec, --monitor-raw-hours, --monitor-minute-days, --monitor-threshold RULE:WARN:CRIT (repeatable; rules host_cpu_pct, host_mem_pct, disk_free_pct (warn above crit), engine_handle_p99_us, reject_pct_5m, rss_growth_pct_1h, heap_bloat_ratio) |
Index sections¶
| Flag | Description |
|---|---|
--index ID[:DESCRIPTION] |
Add an index (up to 5); ID is letters and digits only |
--index-constituents ID:SYM[,SYM,...] |
Constituents of index ID |
--index-base-value ID:VALUE |
Base value of index ID (default 1000.0) |
--index-interval ID:SECS |
Publish interval of index ID (default 1.0) |
--index-history-file ID:PATH |
History file of index ID (default data/indexes/<ID>_history.jsonl) |
--index-state-file ID:PATH |
State file of index ID (default data/indexes/<ID>_state.json) |
Each --index-* flag takes one value per index: give it at most once per ID, and only for an ID defined with --index. Every constituent needs --outstanding-shares and a reference price, which pm-config-gen only writes with a MARKET_MAKER participant and --seed-mm-mid-range MIN:MAX --seed-last-prices-from-mm; otherwise it refuses with an error.
Schedule¶
| Flag | Default | Description |
|---|---|---|
--schedule / --no-schedule |
auto | Force include/suppress schedule section |
--pre-open HH:MM |
09:00 |
Pre-open schedule time |
--opening-auction HH:MM |
09:25 |
Opening auction start time |
--continuous HH:MM |
09:30 |
Continuous trading start time |
--closing-auction HH:MM |
16:00 |
Closing auction start time |
--closing-end HH:MM |
16:05 |
Closing auction end time |
--weekend |
off | Also emit a weekend: block (Sat+Sun) using the --weekend-* times below; without this flag weekends are CLOSED |
--weekend-pre-open HH:MM |
10:00 |
Weekend pre-open schedule time |
--weekend-opening-auction HH:MM |
10:25 |
Weekend opening auction start time |
--weekend-continuous HH:MM |
10:30 |
Weekend continuous trading start time |
--weekend-closing-auction HH:MM |
14:00 |
Weekend closing auction start time |
--weekend-closing-end HH:MM |
14:05 |
Weekend closing auction end time |
--holidays / --no-holidays |
off | Also emit a holidays: block using the --holidays-* times below; without this flag bank holidays are CLOSED |
--holidays-pre-open HH:MM |
10:00 |
Holiday pre-open schedule time |
--holidays-opening-auction HH:MM |
10:25 |
Holiday opening auction start time |
--holidays-continuous HH:MM |
10:30 |
Holiday continuous trading start time |
--holidays-closing-auction HH:MM |
14:00 |
Holiday closing auction start time |
--holidays-closing-end HH:MM |
14:05 |
Holiday closing auction end time |
--country CODE |
— | Holiday calendar country code (ISO 3166-1 alpha-2) — which dates the holidays: block applies to |
--session-timezone TZ |
— | Session timezone, an IANA name (e.g. Europe/Stockholm) |
Combos¶
| Flag | Description |
|---|---|
--combo SPEC |
Add a pre-configured combo definition; repeatable |
Output¶
| Flag | Default | Description |
|---|---|---|
--output FILE |
stdout | Output YAML file path |
--force |
off | Overwrite existing output file |
--dry-run |
off | Print generated YAML; do not write file |
--comment-default-config-fields |
off | Add a commented-out header with every optional section (engine settings, gateways, api_gateways, log_server, monitor, indices, schedule) at its default values, ready to uncomment |
Expected runtime input arguments:
None.
Use cases:
- Generate consistent configs across multiple machines.
- Create deterministic demo/test environments quickly.
- Avoid hand-editing errors in gateway/symbol/session configuration.
Like pm-setup, this is a local tooling command and does not subscribe/publish
on the runtime ZeroMQ sockets.
For more details on the options see Configuration Chapter
pm-index-cli — Index Structural/Audit History Query Tool¶
Reads the structural/corporate-action audit JSONL files written by pm-index
directly from disk and renders them as a table, JSON, or CSV. No running
process is required, except for health. It does not expose level or EOD history — use
pm-stats-cli index-daily / index-snapshots for that instead (see the
pm-stats-cli section above).
Startup options:
| Flag | Default | Description |
|---|---|---|
--config PATH / -c PATH |
unset | Path to engine_config.yaml; auto-discovers history file paths and index IDs |
--data-dir DIR |
data/indexes |
Directory containing history files; used when --config is absent or an index is not in config |
--format table\|json\|csv |
table |
Output format |
--no-header |
off | Suppress header row (CSV only) |
--version |
— | Print version and exit |
Expected runtime input arguments:
None.
Subcommands:
| Subcommand | Default limit | Purpose |
|---|---|---|
events |
1,000 | Structural events: INIT, CORP_ACTION, ADD_CONSTITUENT, DELIST |
indices |
— | List configured indices from engine_config.yaml |
health |
— | Check the pm-index process and ping its health socket; --extended adds every index's state/history file check. Options: --extended, -v/--verbose, -q/--quiet (print nothing; exit 0 when healthy, -1 otherwise) |
The events subcommand accepts --index ID (repeatable), --days N,
--from DATE_OR_TS, --to DATE_OR_TS, --limit N, and --type TYPE to
filter to a specific structural event kind.
Like pm-stats-cli, this is a read-only tool that works offline. The only
ZeroMQ connection it ever makes is health's ping to pm-index on port
5564.
See pm-index-cli in the commands reference for the full option reference, output column descriptions, and examples including CSV export and Python plotting.
pm-index-admin-cli — Index Corporate Action / Constituent Change CLI¶
One-shot CLI for applying corporate actions (splits, cash dividends, share
issuances and buy-backs) and constituent changes (add/delist) to a running
pm-index process. Unlike pm-index-cli above, this tool is a write
tool and talks directly to pm-index's live PUSH/PULL socket pair — it does
not read JSONL files and does not require --config.
pm-index-admin-cli --id <GW_ID> [--push ADDR] [--sub ADDR] [--timeout MS] [--dry-run] [-y] [--format table|json] COMMAND [options]
Startup options:
| Flag | Default | Description |
|---|---|---|
--id |
required | Ack-routing label — not authenticated by pm-index (its PULL socket has no auth of any kind) |
--push |
tcp://127.0.0.1:5559 |
pm-index PULL socket address |
--sub |
tcp://127.0.0.1:5558 |
pm-index PUB socket address |
--timeout |
3000 |
Ack timeout in milliseconds |
--dry-run |
off | Print the outbound payload instead of sending it |
-y / --yes |
off | Skip the confirmation prompt |
--format |
table |
Output format: table or json |
--version |
— | Print version and exit |
Expected runtime input arguments:
None (one-shot; runs a single subcommand and exits).
Subcommands:
| Subcommand | Purpose |
|---|---|
split |
Apply a stock split or reverse split |
dividend |
Apply a cash dividend adjustment |
shares |
Set shares outstanding — covers both issuances and buy-backs |
add |
Add a new constituent |
delist |
Remove a constituent |
history |
Show recent structural/corp-action history for an index |
Subcommand arguments (all take --index INDEX_ID, which is required):
| Subcommand | Arguments | Notes |
|---|---|---|
split |
--sym SYMBOL, --ratio N:M |
4:1 is a 4-for-1 split, 1:10 a 1-for-10 reverse split |
dividend |
--sym SYMBOL, --amount AMOUNT |
Dividend per share in price units; must be > 0 |
shares |
--sym SYMBOL, and either --new-shares N or --delta N |
--new-shares is the new absolute total; --delta is a signed change to the last known count (negative for a buy-back), resolved via history and shown in the prompt |
add |
--sym SYMBOL, --shares N, --price PRICE |
Initial shares outstanding and reference price; both must be > 0 |
delist |
--sym SYMBOL |
|
history |
--from UNIX_TS, --to UNIX_TS, --types T,T,..., --limit N |
Defaults: last 24 h, all types (INIT, CORP_ACTION, DELIST, ADD_CONSTITUENT), 50 rows |
Every mutating subcommand (split, dividend, shares, add, delist)
prints the action and prompts Continue? [y/N] before sending, unless
-y/--yes is given. pm-index has no dedicated buy-back action type — the
shares subcommand's --delta flag models a buy-back as a negative delta
against the constituent's last known share count, resolved via history
before sending an absolute SHARES_ISSUANCE value.
This tool has no connect()/authentication step, unlike pm-admin-cli:
pm-index's PULL socket accepts any non-empty gateway_id, using it only as
an ack-routing key.
See Index Administration (pm-index-admin-cli) for the full subcommand
reference, confirmation-prompt behaviour, and worked examples.
pm-opctl-cli — Operational Process Control¶
Start, stop, and monitor a named group of pm-* processes together. This is
the recommended way to bring up (or tear down) a full EduMatcher stack,
instead of launching each process by hand in its own terminal — see
Running the Exchange → Starting the stack with pm-opctl-cli.
pm-opctl-cli start [PROFILE] [-d]
pm-opctl-cli list [-y | --no-restart]
pm-opctl-cli health [-q]
pm-opctl-cli stop
pm-opctl-cli kill
pm-opctl-cli init
pm-opctl-cli show [--json]
pm-opctl-cli clear (--state | --all) [--yes]
Profiles:
A profile is a named list of processes to run together, defined in
<DATA_DIR>/emo-config.yaml. Four profiles are built in and used as-is until
that file exists:
| Profile | Contents |
|---|---|
micro |
Centralized logging plus the matching engine only |
mini |
A trading-capable subset: logging, stats, engine, scheduler, market data, the desk API gateway, and the ALF/post-trade/drop-copy gateways |
default |
The full nominal exchange stack, including audit, clearing, both API gateway instances, the BALF gateway, and the index calculation process (pm-index) |
mm-demo |
default plus one pm-mm-bot: passive strategy, quoting as gateway MM01 on every symbol of the deployed configuration (--all-symbols), deliberately slow (--gap 0.20 --qty 200 --reissue-delay-ms 5000 --fade-ticks 3 --fade-sec 20 --drift-ticks 15) so students can follow what a market maker does. All settings are command-line arguments; no bot config file is used. Needs a config that registers MM01 and those symbols, such as s10-basic |
ai-swarm |
The default set, mm-demo's market-maker bot with --anchor-sim 0.3 (quoting around the true values; an unanchored passive maker pins prices), pm-market-sim --verbose (the market model and its news) and pm-ai-swarm --count 20 --budget 40 --symbols-per-agent 1000 --verbose: twenty AI traders (AI001–AI020, every built-in preset in equal shares, each on every symbol) in one worker process. Needs a config that registers them and ships a market_sim.yaml, such as the s150 examples or s300-load |
ai-sim |
ai-swarm set up to run for weeks on the s300-sim example: pm-ai-swarm --swarm <examples>/s300-sim-setup/swarm.yaml --verbose (AI001–AI100, 120 order actions a second), the market maker with the symmetric strategy (the passive one re-quotes on every change in other traders' orders, which with a hundred traders was most of the exchange's load), pm-engine without --verbose, pm-stats --keep-days 7 --keep-order-events-days 0.5 --human-gateways 'TRADER*' --keep-human-order-events-days 7 and pm-clearing --print-every 0 --retention-days 7. What the edumatcher-ai-sim image runs; needs s300-sim deployed. See AI Traders and Market Simulation |
Run pm-opctl-cli init to write the built-ins to emo-config.yaml for editing;
once that file exists its profiles replace the built-ins entirely (a missing
default profile is backfilled from the built-in nominal stack).
Subcommands:
| Subcommand | Aliases | Options | Purpose |
|---|---|---|---|
start [PROFILE] |
up |
-d/--debug/-vv (append --log-level DEBUG to every process's command) |
Start a profile (default when omitted), skipping entries already running |
list |
— | -y/--restart (restart dead entries without asking), --no-restart (never offer) |
Status table for the active profile: uptime (HH:MM) and resident memory (MiB) per process |
health |
— | -q/--quiet (print nothing) |
Same checks as list; exits 0 only when every process is running — for monitoring scripts |
stop |
down |
— | Send SIGTERM to processes this tool started (recorded in the PID directory) |
kill |
— | — | Emergency stop: pkill -15 -f -i -l pm- — signals every process whose command line contains pm-, including ones this tool did not start |
init |
— | — | Write the built-in profiles to <DATA_DIR>/emo-config.yaml; refuses to overwrite an existing file |
show |
— | --json |
Print version, resolved data directory, and deployed config paths — same info as pm-config-deploy --show |
clear |
— | --state (engine/session state only) or --all (also logs and audit trail), --yes (skip prompt) |
Delete persisted data under the data directory; ref_data/ (configuration) is never touched |
Exactly one of --state/--all must be given to clear.
start --debug appends --log-level DEBUG to every process's command line
before it is launched, overriding whatever level a profile's --verbose (or
lack of one) would otherwise select — see
Developer → Logging levels and pm-log-srv
for what that changes and why every process still needs to be started this
way (an already-running process must be stopped and restarted with --debug
to pick up DEBUG logging; there is no way to raise a running process's log
level in place).
Expected runtime input arguments:
None. Each subcommand runs once and exits (list/health print a snapshot,
they do not stay running).
Health checking:
There is no general way to prove an arbitrary process is not internally hung,
so health is reported at three levels of confidence: dead (no live PID
found), not responding (the PID is alive but its healthcheck failed or its
tcp probe refused/timed out), or running (the PID is alive and any
configured check passed). A healthcheck command takes precedence over a
tcp probe because it exercises the application itself, not just its socket.
Process tracking:
Started processes are detached into their own session and recorded as
<DATA_DIR>/emo/<name>.pid, with combined stdout/stderr appended to
<DATA_DIR>/emo/<name>.log. The active profile name is remembered in
<DATA_DIR>/emo/active-profile. A process restarted outside this tool is
re-adopted on the next list/health/stop by matching its command line via
pgrep, so a stale PID file is never treated as final.
This tool is local orchestration logic — it starts other pm-* processes as
subprocesses and reads their PID/log files, but binds no ZeroMQ or TCP port
of its own and does not participate in the runtime message bus.
See Running the Exchange and the Quick Start's Install and Start for the recommended startup sequence.
pm-help / pm-man — Command Index and Man-Page Reference¶
Lists every pm-* command with a one-sentence explanation, or prints a full
man page for one of them. pm-man is a plain alias for the exact same tool --
use whichever name is more familiar.
pm-help [--format table|text] [-v]
pm-help [--format table|text] [-v] <pm-command>
pm-help --completion bash|zsh
pm-man <pm-command>
With no argument, prints the version and data-file locations (the same
information as pm-opctl-cli show), then a table of every pm-* command
grouped by category: Core Runtime, External Gateways, Protocol Spies, AI &
Bots, Index, Query & Reporting CLIs, Admin & Operations, Setup &
Configuration, Logging, Developer Tools, and Help.
With a command name, prints a full man page: NAME, SYNOPSIS, DESCRIPTION,
OPTIONS, SUBCOMMANDS (where applicable), PORTS, MESSAGE BUS involvement,
RELATED COMMANDS, EXAMPLES, NOTES, and SEE ALSO. The name is looked up with or
without its pm- prefix, so pm-help viewer finds pm-viewer.
Startup options:
| Flag | Default | Description |
|---|---|---|
--format table\|text |
table |
table uses UTF-8 box-drawing characters; text is plain aligned columns with no box-drawing -- friendly to grep, redirects, and non-UTF-8 terminals |
-v, --verbose |
off | In the command-index view, show extra detail (and a worked example, where one exists) under each command |
--no-color |
off | Disable ANSI colour |
--completion bash\|zsh |
— | Print the tab-completion script for every pm-* command and exit -- see Shell completion |
--version |
— | Print version and exit |
Expected runtime input arguments:
None. Each invocation prints its output once and exits.
This tool is local reference data -- hand-curated, not scraped from each
command's own --help output -- so it can show information no argparse
parser exposes on its own, such as ports and ZeroMQ message-bus involvement.
It does not participate in the runtime message bus.
Examples:
pm-help # version, data dirs, and the full command table
pm-help --format text -v # plain-text table with per-command examples
pm-help pm-viewer # full man page for pm-viewer
pm-man pm-alf-console # pm-man is an alias for pm-help
pm-help --completion bash # print the bash tab-completion script and exit
Order Lifecycle Message Flow¶
The following diagram traces a single limit order from submission to full fill, showing which process sends each message and on which socket:
sequenceDiagram
participant GW as pm-alf-console
participant ENG as pm-engine
participant VW as pm-viewer
participant CLR as pm-clearing
participant AUD as pm-audit
GW->>ENG: order.new (PUSH :5555)
ENG-->>GW: order.ack.GW_ID (PUB :5556)
Note over ENG: order rests in book
ENG-->>VW: book.SYMBOL (PUB :5556)
ENG-->>AUD: order.ack.GW_ID (PUB :5556)
GW->>ENG: order.new — aggressive order (PUSH :5555)
ENG-->>GW: order.ack.GW_ID (PUB :5556)
ENG-->>GW: order.fill.GW_ID (PUB :5556)
ENG-->>CLR: trade.executed (PUB :5556)
ENG-->>VW: book.SYMBOL (PUB :5556)
ENG-->>AUD: trade.executed (PUB :5556)
All subscribers (pm-viewer, pm-clearing, pm-audit, pm-stats, pm-board,
etc.) receive the same trade.executed and book.{SYMBOL} events concurrently
from the single PUB socket — none of them coordinate with each other.
pm-balf-gwy — BALF TCP Gateway¶
pm-balf-gwy is implemented and provides external binary order-entry over TCP.
It accepts BALF binary frames from low-latency programmatic clients and
translates them into the same engine order flow as the ALF gateways.
See BALF TCP Gateway for operational usage and BALF Protocol Reference for the wire-level contract.
pm-balf-gwy [--bind 0.0.0.0] [--port 5560] [--engine-host HOST] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--bind |
from config / 0.0.0.0 |
TCP bind address for BALF clients |
--port |
from config / 5560 |
TCP listen port for BALF clients |
--engine-host |
from config | Override engine host for ZMQ ports 5555 / 5556 |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
pm-md-gwy (CALF Market-Data Gateway)¶
pm-md-gwy is implemented and provides external market-data distribution over
CALF/TCP. It consumes engine (and index) PUB topics and exposes seven
sequence-aware channels: TOP (top-of-book), TRADE (executed trades),
STATE (session and halt/resume state), INDEX (index level updates),
DEPTH (full book depth), AUCTION (auction uncross results), and CB
(circuit-breaker halt/resume events). A client discovers which channels a
given gateway build supports via WELCOME|CH_SUPPORTED=; only TOP,
TRADE, STATE, and AUCTION accept the SYM=* wildcard.
pm-md-gwy [--bind 0.0.0.0] [--port 5570] [--engine-pub tcp://127.0.0.1:5556] [--index-pub tcp://127.0.0.1:5558] [--log-level LEVEL] [-v|-vv] [-q] \
[--log-target TARGET] [--log-file PATH] [--log-failover-timeout SEC]
Startup options:
| Flag | Default | Description |
|---|---|---|
--bind |
from config / 0.0.0.0 |
TCP bind address for external clients |
--port |
from config / 5570 |
TCP listen port for CALF clients |
--engine-pub |
tcp://127.0.0.1:5556 |
Engine PUB address consumed by the gateway |
--index-pub |
tcp://127.0.0.1:5558 |
Index PUB socket address (overrides config) |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
See Market Data Feed (CALF) for operational usage and CALF Protocol Reference for the wire-level contract.
pm-calf-spy (CALF Protocol Spy)¶
pm-calf-spy is a read-only CLI client for pm-md-gwy: it opens a CALF TCP
session, subscribes to whatever channels/symbols you ask for, and prints
every line it receives — human-readable or as JSON — so you can see exactly
what the protocol sends without writing a client. It never mutates exchange
state, and any number of instances can run at once (e.g. one per terminal,
each with a different --channels/--symbols filter) since pm-md-gwy
accepts unlimited concurrent connections up to max_connections. Since it
never sends anything on its own after the initial subscription, it sends a
periodic PING (--ping-interval, default 60s) to keep the gateway from
dropping it as an idle client.
Startup options:
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
pm-md-gwy TCP host |
--port |
5570 |
pm-md-gwy TCP port |
--client-name |
calf-spy-<pid> |
HELLO\|CLIENT= identifier reported in gateway logs |
--channels |
* |
Comma-separated channels; * = every channel in WELCOME\|CH_SUPPORTED= |
--symbols |
* |
Comma-separated symbols; * = wildcard for channels that allow it |
--resume |
(none) | One-shot CH:SYM:LASTSEQ — requests single-stream replay on connect |
--ping-interval |
60 |
Seconds between PING keepalives sent to the gateway; 0 disables |
--format |
human |
human or json output |
--count |
0 |
Exit after N data-carrying lines (0 = run until Ctrl-C) |
--raw |
off | Print raw protocol bytes instead of decoded fields |
--no-color |
off | Disable ANSI colour in output |
--show-heartbeats |
off | Include HB heartbeat lines in output (hidden by default) |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
server |
Where operational log records go: server (auto-detected pm-log-srv), stdout, or file |
--log-file |
none | Operational log file path — required when --log-target file |
--log-failover-timeout |
30 (from config) |
Grace window in seconds before falling back to a local log file once pm-log-srv becomes unreachable |
--version |
— | Print version and exit |
See CALF Protocol Spy (pm-calf-spy) for full usage.
pm-log-srv — Centralized Log Server¶
pm-log-srv is a dedicated collector process, unrelated to the ZeroMQ bus:
it accepts LALF ("Logging ALF") TCP connections from any number of pm-*
processes and appends every received log record to a queryable SQLite
database (log.db), the same "dedicated collector + SQLite + read-only
query CLI" shape pm-stats/pm-audit already use for market data and
trading events, applied here to operational logging-module output.
Phased rollout
pm-log-srv/pm-log-cli are fully implemented and usable today. Wiring
every existing pm-* process's own logging into a LALF client that
auto-detects a running pm-log-srv at startup is a follow-up phase and
has not yet been rolled out — until then, --log-level/-v/-q still
control each process's own stdout logging as before.
pm-log-srv [--host ADDR] [--port PORT] [--db PATH]
[--retention-days N] [--max-message-bytes N]
[--pub-port PORT] [--pull-port PORT] [--no-pubsub] [--lease-sec N]
[--log-level LEVEL] [-v|-vv] [-q] [--log-target stdout|file] [--log-file PATH]
| Flag | Default | Description |
|---|---|---|
--host |
0.0.0.0 (from config) |
TCP bind address |
--port |
5600 |
TCP listen port for LALF clients |
--db |
data/log.db |
SQLite database path |
--retention-days |
30 |
Prune log_events rows older than N days; 0 = unbounded |
--max-message-bytes |
65536 |
Truncation ceiling per LOG payload (never dropped, only truncated) |
--pub-port |
5601 |
LALF-PS ZeroMQ PUB bind port for log distribution |
--pull-port |
5602 |
LALF-PS ZeroMQ PULL bind port for subscriber control |
--no-pubsub |
off | Disable the LALF-PS interface entirely (bind no ZeroMQ sockets) |
--lease-sec |
30 |
Subscription lease TTL in seconds; a subscriber that stops sending log.renew is reaped after this long |
--log-level |
WARNING |
Explicit log level: CRITICAL, ERROR, WARNING, INFO, DEBUG |
-v / --verbose |
off | Increase verbosity (-v → INFO, -vv → DEBUG) |
-q / --quiet |
off | Reduce output to warnings/errors |
--log-target |
stdout |
Where this process's own operational log records go: stdout or file — never server, since pm-log-srv must not depend on itself over the network |
--log-file |
none | Operational log file path — required when --log-target file |
--version |
— | Print version and exit |
pm-log-cli — Log Server Query/Troubleshooting CLI¶
pm-log-cli is a read-only offline tool: it queries log.db directly, never
over the network, so a busy or even-stopped pm-log-srv never blocks
troubleshooting with data already collected.
Subcommands: tail, query, processes, stats, diagnose (rule-based
troubleshooting heuristics with concrete recommendations), and prune
(manual retention maintenance).
See Centralized Log Server for the full operational guide
— starting the server, every pm-log-cli subcommand, and a workflow
cookbook — and LALF Protocol Reference for the
normative wire specification.
pm-monitor — Health Collector¶
pm-monitor collects the health beacon every long-running pm-* process
sends, judges each process (HEALTHY, DEGRADED, HUNG, SILENT, DEAD, MISSING,
STOPPED) and the exchange as a whole (OPERATIONAL, DEGRADED, IMPAIRED,
DOWN), turns counters into rates, evaluates thresholds, keeps history in
metrics.db, and publishes health.snapshot once a second and
health.event as things happen. Every built-in pm-opctl-cli profile except
micro starts it.
pm-monitor [--log-level LEVEL] [-v|-vv] [-q]
[--log-target server|stdout|file] [--log-file PATH]
[--log-failover-timeout SEC] [--version]
All its settings come from the monitor section of the deployed
configuration (see Configuring pm-monitor);
the flags are the common logging options.
| Port | Socket | Purpose |
|---|---|---|
5610 |
PULL (bind) | health.beacon from every process |
5611 |
PUB (bind) | health.snapshot (1 Hz), health.event |
5556 |
SUB (connect) | Engine lifecycle events: connects, disconnects, diagnostics, session, halts |
| File | Purpose |
|---|---|
<data dir>/metrics.db |
Samples, 1-minute roll-ups, daily totals, counter state, events (WAL) |
<data dir>/metrics.db.lock |
Held while running: one pm-monitor per data directory |
Exit codes: 0 stopped normally (also after idling with monitor.enabled:
false); 1 cannot bind its ports or open metrics.db (held by another
pm-monitor, or recorded with another session timezone); 2 usage error or
an unknown session timezone.
See Health Monitoring for how health is judged,
the thresholds, metrics.db and troubleshooting.
pm-monitor-cli — Health Query CLI¶
pm-monitor-cli is read-only: it subscribes to pm-monitor's PUB port for
the live view and reads metrics.db for the event log, never writing either.
pm-monitor-cli [--host HOST] [--port PORT] [--db PATH]
[--format table|json] [--timeout SEC] COMMAND [options]
| Command | Purpose |
|---|---|
health [-q] [--extended] [-v] |
pm-monitor publishing snapshots (its loop runs); --extended also checks metrics.db integrity, schema, pinned timezone and write access. -q: exit 0 / 255. Used as the profile's healthcheck |
status |
Exchange status, trading summary, every process, active breaches. Exit 0 OPERATIONAL, 1 DEGRADED, 3 IMPAIRED, 4 DOWN, 5 pm-monitor unreachable |
events [--since TS] [--kind K] [--subject TEXT] [--limit N] [--follow [--count N]] |
The event log, then live events with --follow |
See pm-monitor-cli for every option and a cookbook.
pm-msgen — Message Binding Generator¶
Developer/build tool that generates the Python message-binding modules
(src/edumatcher/models/generated/), C header artifacts, and the
Message Reference documentation table from the
canonical specification in spec/ (spec/transports.yaml and
spec/messages/). It is not part of the running exchange — no pm-* runtime
process imports or invokes it — it exists so that the spec is the single
source of truth for every topic name and payload shape, and so a spec change
without regeneration (or a hand-edit to a generated file) cannot silently
drift from what the engine and gateways actually send.
pm-msgen generate [--spec DIR] [--out-python DIR] [--out-c DIR]
pm-msgen check [--spec DIR] [--out-python DIR] [--out-c DIR]
pm-msgen lint [--spec DIR]
pm-msgen grep-literals [--spec DIR] [--src DIR]
Subcommands:
| Subcommand | Purpose |
|---|---|
generate |
Render Python bindings, C artifacts, and docs from the spec and write them to disk |
check |
Re-render from the spec and fail (exit 1) if the committed output differs — the CI gate; run via make msgen-check |
lint |
Validate the spec only (YAML structure, message/family consistency); prints a family/message count |
grep-literals |
Scan a source tree for topic string literals that a generated constant should replace instead |
Startup options:
| Flag | Default | Applies to | Description |
|---|---|---|---|
--spec DIR |
spec |
all | Spec root holding transports.yaml and messages/ |
--out-python DIR |
src/edumatcher/models/generated |
generate, check |
Python output directory |
--out-c DIR |
docs/examples/generated |
generate, check |
C output directory |
--src DIR |
src |
grep-literals |
Source tree to scan |
--version |
— | all | Print version and exit |
Expected runtime input arguments:
None. Each subcommand runs once and exits.
Exit codes:
0— success (generatewrote or confirmed up-to-date output;check/lintfound no problems)1—checkfound drift between the spec and the committed generated output2— spec error (invalid YAML/schema) or the--specdirectory was not found (most often caused by running from the wrong working directory — run from the repository root, or pass--specexplicitly)
Regenerate after editing a spec file with make msgen; make msgen-check
(part of make check/make pre-commit) fails the build if generated output
was not committed alongside a spec change.
This tool is local build tooling — it edits source files under version control and does not participate in the ZeroMQ runtime message bus.
See Messages for the generated message catalog itself.
See also¶
Participant Guide:
- Market-Maker Bot — autonomous quoting process for a single symbol
Operator's Guide:
- Running the Exchange — startup order, launch scripts, and verification
- The Configuration Workflow — how each process is configured via
engine_config.yaml - Inspect Configs (pm-config-show) — read-only viewer for the deployed config and its port map
- Persistence — which process writes which data file
- RALF Gateway — Post-Trade Dissemination — external RALF gateway usage
- CALF Gateway — Market Data — external CALF gateway usage
- Centralized Log Server — pm-log-srv/pm-log-cli operational guide
- ALF Gateway — Order Entry — external ALF order-entry gateway usage
- API Gateway (REST/WebSocket) — running and operating the REST/WebSocket gateway; the endpoint contract is the REST API Reference and the WebSocket API Reference
- Market Index (pm-index) — index configuration, calculation, and corporate actions
Protocols and Clients:
- Message Reference — the full ZeroMQ message catalog all processes share
- Drop Copy — the engine's built-in :5557 drop-copy feed
- RALF Protocol Reference — official protocol appendix
- CALF Protocol Spy (pm-calf-spy) — read-only CALF inspection CLI
- RALF Protocol Spy (pm-ralf-spy) — read-only RALF inspection CLI
- LALF Protocol Reference — official protocol appendix