Skip to content

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:

poetry run pm-engine --verbose

If you installed with pipx (end-user mode), the commands are on your PATH and need no prefix:

pm-engine --verbose

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:

  1. If EDUMATCHER_DATA_DIR is set, its expanded, absolute path wins — in a source checkout and in an installed deployment alike.
  2. Otherwise EduMatcher checks where edumatcher/config.py is installed. If its package parent is named src, the process is treated as running from a source checkout and uses <repo>/src/data/.
  3. Otherwise the package is treated as installed and uses ~/.local/share/edumatcher (for example /Users/<user>/.local/share/edumatcher on macOS). This path is fixed; it does not follow XDG_DATA_HOME.
  4. If the directory chosen above does not exist, but ./data exists 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:

<DATA_DIR>/ref_data/engine_config.json
<DATA_DIR>/ref_data/engine_config.yaml

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:

  1. Bootstrap session files:
    pm-setup
    
  2. Generate a config (or reuse/edit the sample config):
    pm-config-gen --symbols AAPL MSFT --participants TRADER01 TRADER02 OPS01:ADMIN --sessions-enabled --output engine_config.yaml
    
  3. Use the operational control pm-opctl-cli to start the system in the correct order with:
    pm-opctl-cli start
    
    Then use pm-opctl-cli list to list the status of each process, and pm-opctl-cli stop to shut the profile down again. See Running the Exchange.
  4. 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.

pm-alf-console --id <GW_ID>

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 (including LIMIT, 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, QUIT
  • KILL|... 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.

pm-alf-gwy [--bind 0.0.0.0] [--port 5565] [--engine-host HOST] [--log-level LEVEL] [-v|-vv] [-q]

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:

pm-viewer --symbol AAPL &
pm-viewer --symbol MSFT &
pm-viewer --symbol TSLA

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.

pm-orders [--gateway GW01] [--log-level LEVEL] [-v|-vv] [-q]

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-size messages (e.g., 100 messages), or
  • --flush-interval seconds 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: prints No rows found.
  • json: prints []
  • csv: prints only header row unless --no-header is 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_events
  • gateway_symbol_positions
  • gateway_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: prints No rows found. (except reconcile, which prints OK — no discrepancies found.)
  • json: prints []
  • csv: prints only header row unless --no-header is 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:

\[mid = \frac{best\_bid + best\_ask}{2}\]

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:

sqlite3 data/stats.db
.headers on
.mode column
SELECT * FROM daily_stats;

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: prints No rows found.
  • json: prints []
  • csv: prints only header row unless --no-header is 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's position_close_snapshot minus the same at the snapshot of the latest earlier settled date (0 if there is none, or if clearing.db no longer has that date's snapshot), each converted from ticks to currency with the symbol's own tick_decimals. Any unsettled snapshot dates in between are part of the one interval.
  • A date has closed when its latest session_events PHASE row is CLOSED and an earlier PHASE row of the same date is not, so the market was open that day. The CLOSED an 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.01 per 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 empty pm-clearing-cli reconcile for 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:

AAPL  415.00  +0.48%  H:418.00  L:412.00  Vol:52,400 (8T)  414.50/415.50  ◆  MSFT …

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.

pm-board [--rows 8] [--interval 10]

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 --rows symbols each.
  • The display auto-rotates to the next page every --interval seconds.
  • 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:

pm-board --rows 15 --interval 8
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.

pm-ai-trader --id AI001 [--preset NAME | --preset-file PATH] [options]
pm-ai-trader --list-presets

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.

pm-ai-swarm --swarm swarm.yaml [options]

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:

python -c "from edumatcher.compdb.store import SEED_DIR; print(SEED_DIR)"

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; run init yourself 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 competitors column 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- and BB+ is the most important one on the scale.
  • Effect. The rating sets the interest a company pays when it borrows: a AAA company pays little over the government's rate, a B company 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_musd against employees) 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 show or list) 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:

  1. Opens ZMQ PUSH and SUB sockets
  2. Sends system.gateway_connect and waits for system.gateway_auth ACK
  3. Requests the symbol list and verifies the assigned symbol exists
  4. Sends QBOOT (quote bootstrap) request — if an active quote already exists for (gateway_id, symbol), adopts it instead of creating a duplicate
  5. Sends QLEGS request to reconcile quote-leg mapping
  6. Waits for session.state (exits if not received within timeout)
  7. Resolves initial reference price and enters the quoting loop

Reference price resolution (in priority order):

  1. Active quote from QBOOT (restart recovery)
  2. Book mid-price (if other participants are already quoting)
  3. Last trade price from trade.executed events
  4. Bootstrap quote prices from QBOOT snapshot (inactive)
  5. 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):

  1. Sends quote.cancel to remove the resting quote
  2. Waits up to --shutdown-timeout-sec for cancel confirmation
  3. 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|...
  • PING
  • EXIT

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=..
  • PING
  • EXIT

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, RESUME
  • HALT_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>]
  • SYMBOLS
  • SESSION|STATE=<PRE_OPEN|OPENING_AUCTION|CONTINUOUS|CLOSING_AUCTION|CLOSED>
  • SESSION_STATUS, SCHEDULE, GATEWAYS, VOLUME
  • POSITION|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.

pm-admin-cli <command> [options]

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.

pm-cverifier [--format text|json] [--level info|warn|error] [--no-color] [--strict] CONFIG_FILE

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 --strict and 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 written
  • 2 — the config file does not exist or cannot be read
  • 3 — the file is not valid YAML (the message points at pm-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 (a config.py constant), env (an environment override), set (explicit in the file), or default (the section is present but omits port:). A port claimed twice is flagged in red, the same condition pm-cverifier reports as M018.
  • 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 --all never 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.

pm-setup [--data-dir PATH] [--show] [--config NAME] [--force] [--no-config]

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).

pm-index-cli [--config engine_config.yaml] [--format table|json|csv] COMMAND [options]

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.

pm-log-cli [--db PATH] [--format human|json] COMMAND [options]

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 (generate wrote or confirmed up-to-date output; check/lint found no problems)
  • 1 — check found drift between the spec and the committed generated output
  • 2 — spec error (invalid YAML/schema) or the --spec directory was not found (most often caused by running from the wrong working directory — run from the repository root, or pass --spec explicitly)

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:

Operator's Guide:

Protocols and Clients: