Skip to content

Exchange Commands

Learning objectives

After reading this page you will understand:

  • What Exchange Commands are and how they differ from raw ZeroMQ messages
  • How to build an operator client that wraps the message layer
  • The full catalogue of commands available to an ADMIN-role gateway
  • Which commands are ADMIN-restricted and which any connected gateway may send
  • How to extend the framework with new commands as the exchange evolves
  • How to actively manage an exchange

Prerequisites: Messages for the raw two-frame format. Risk Controls for the halt/resume operational flow.

Concept

Every engine interaction is ultimately a two-frame ZeroMQ multipart message:

frame[0]  b"<topic>"
frame[1]  b'{"field": "value", ...}'

Exchange Commands are a thin Python wrapper layer that turns those frames into named method calls. They exist so that operators can control the exchange without writing raw socket code, and so that the growing set of operator interactions has a single, consistent home that can be extended with new commands as the system evolves.

Operator script
    └─ ExchangeCommandClient.halt_all()
            └─ PUSH  b"risk.circuit_breaker_halt_all"  {"gateway_id": "GW_ADMIN"}
            └─ SUB   b"risk.circuit_breaker_halt_all_ack.GW_ADMIN"  ← ack

The client holds a PUSH socket (port 5555) for commands and a SUB socket (port 5556) for receiving acks and events.

How to send ADMIN commands

There are three ways to issue exchange commands, suited to different workflows:

Tool What it is Best for
pm-admin Interactive REPL with tab completion and history Human operator — incident response, market supervision
pm-admin-cli Single-shot CLI command, exits with code 0/1 Scripting, CI/CD, automation pipelines
ExchangeCommandClient Python API class Custom operator tooling, integration tests

All three share the same command execution logic (execute_command in src/edumatcher/commands/console.py), so adding a new exchange command only requires changes in one place.

ADMIN console (pm-admin)

For operators who want to control the exchange without writing Python code, EduMatcher ships a dedicated interactive console:

poetry run pm-admin --id GW_ADMIN

The gateway ID must match an entry in engine_config.yaml with role: ADMIN.

What you see at startup

ADMIN console — GW_ADMIN connected  Exchange operator (read/write)
Type HELP for commands.  Tab=complete  ↑↓=history  Ctrl-C=exit

[GW_ADMIN|ADMIN]>

The prompt is red to distinguish it from the regular pm-alf-console prompt. Tab completion and arrow-key history work exactly as in pm-alf-console.

Console command syntax

Commands follow the same CMD|KEY=VALUE|KEY=VALUE pipe syntax as the ALF gateway command language (see ALF Protocol Reference). Single-word commands (no arguments) need no pipes.

Command Syntax Notes
HALT HALT Exchange-wide circuit-breaker halt — requires ADMIN
RESUME RESUME Lift the exchange-wide halt — requires ADMIN
HALT_SYM HALT_SYM\|SYM=AAPL Halt trading on a single symbol — requires ADMIN
RESUME_SYM RESUME_SYM\|SYM=AAPL Resume a single halted symbol — requires ADMIN
CANCEL_SYM CANCEL_SYM\|SYM=AAPL Cancel all resting orders for a symbol across every gateway — requires ADMIN
KILL KILL\|GW=TRADER01 or KILL\|GW=TRADER01\|SYM=AAPL Cancel all orders/quotes for a gateway (optional: scope to one symbol)
KICK KICK\|GW=TRADER01 or KICK\|GW=TRADER01\|REASON=Compliance hold Forcefully disconnect a gateway
QCANCEL QCANCEL\|GW=MM01\|SYM=AAPL Cancel an MM's active quote on one symbol
BOOK BOOK\|SYM=AAPL Print L1/L2 order-book snapshot
ORDERS ORDERS\|GW=TRADER01 List resting orders for a gateway
SYMBOLS SYMBOLS List all instruments configured in the engine
SESSION SESSION\|STATE=CONTINUOUS Advance session phase
SESSION_STATUS SESSION_STATUS Show the current session state (read-only)
SCHEDULE SCHEDULE Show the automatic session-transition schedule
GATEWAYS GATEWAYS List all configured gateways and connection status
VOLUME VOLUME Show daily traded volume per symbol and in total
HELP HELP Show the command reference
EXIT / QUIT EXIT Disconnect and exit

Example session

[GW_ADMIN|ADMIN]> SYMBOLS
┌─────────────────────────────┐
│   Configured instruments    │
├────┬────────────────────────┤
│ #  │ Symbol                 │
├────┼────────────────────────┤
│ 1  │ AAPL                   │
│ 2  │ MSFT                   │
│ 3  │ TSLA                   │
└────┴────────────────────────┘

[GW_ADMIN|ADMIN]> BOOK|SYM=AAPL
┌──────────────────────────────────────────────────────┐
│               Order Book — AAPL                      │
├────────────┬────────────┬────────────┬───────────────┤
│   Bid Qty  │  Bid Price │  Ask Price │       Ask Qty │
├────────────┼────────────┼────────────┼───────────────┤
│        300 │     149.50 │     150.00 │           100 │
│        200 │     149.25 │     150.25 │           250 │
└────────────┴────────────┴────────────┴───────────────┘
  Last trade: 149.75 × 200

[GW_ADMIN|ADMIN]> HALT
HALTED  3 symbol(s), 6 quote leg(s) cancelled

[GW_ADMIN|ADMIN]> KILL|GW=TRADER01
KILL OK  TRADER01  orders=4  quotes=0

[GW_ADMIN|ADMIN]> ORDERS|GW=TRADER01
No resting orders for TRADER01

[GW_ADMIN|ADMIN]> RESUME
RESUMED  3 symbol(s)

[GW_ADMIN|ADMIN]> SESSION_STATUS
  Session state     : CONTINUOUS
  Auto-scheduling   : ON

[GW_ADMIN|ADMIN]> VOLUME
┌────────────┬──────────────┬──────────────────┬────────┐
│ Symbol     │          Qty │            Value │ Trades │
├────────────┼──────────────┼──────────────────┼────────┤
│ AAPL       │        5,000 │      750,000.00  │     12 │
│ MSFT       │        3,200 │      576,000.00  │      8 │
├────────────┼──────────────┼──────────────────┼────────┤
│ TOTAL      │        8,200 │    1,326,000.00  │     20 │
└────────────┴──────────────┴──────────────────┴────────┘

[GW_ADMIN|ADMIN]> EXIT
ADMIN console GW_ADMIN disconnected.

Relationship to the other operator tools

pm-admin, pm-admin-cli, and ExchangeCommandClient all share the same execute_command() function. Every command maps 1-to-1:

Console (pm-admin) CLI (pm-admin-cli) Python API
HALT halt client.halt_all()
RESUME resume client.resume_all()
HALT_SYM\|SYM=X halt-sym --sym X client.symbol_halt("X")
RESUME_SYM\|SYM=X resume-sym --sym X client.symbol_resume("X")
CANCEL_SYM\|SYM=X cancel-sym --sym X client.cancel_symbol("X")
KILL\|GW=X\|SYM=Y kill --gw X --sym Y client.kill_switch("X", symbol="Y")
KICK\|GW=X\|REASON=Z kick --gw X --reason Z client.gateway_kick("X", reason="Z")
QCANCEL\|GW=X\|SYM=Y qcancel --gw X --sym Y client.quote_cancel("X", "Y")
BOOK\|SYM=X book --sym X client.book_depth("X")
ORDERS\|GW=X orders --gw X client.order_list("X")
SYMBOLS symbols client.symbol_list()
SESSION\|STATE=X session --state X client.session_advance("X")
SESSION_STATUS session-status client.session_status()
SCHEDULE schedule client.session_schedule()
GATEWAYS gateways client.gateway_list()
VOLUME volume client.volume()

Use pm-admin for interactive human-driven operations, pm-admin-cli for scripting and automation, and ExchangeCommandClient for custom Python tooling.

CLI tool (pm-admin-cli)

pm-admin-cli sends one command per invocation and exits. It is designed for shell scripts, CI/CD pipelines, and automation where you want the exchange to be driven non-interactively.

poetry run pm-admin-cli --id GW_ADMIN <subcommand> [options]

Exit code is 0 on success and 1 on failure (auth refused, engine rejection, or timeout). This makes it safe to use in set -e shell scripts.

Global flags

Flag Default Description
--id GW_ID (required) ADMIN gateway ID (must match engine_config.yaml)
--push ADDR tcp://127.0.0.1:5555 Engine PULL socket address
--sub ADDR tcp://127.0.0.1:5556 Engine PUB socket address
--timeout MS 3000 Ack wait timeout in milliseconds

Subcommands

halt — Exchange-wide halt

pm-admin-cli --id GW_ADMIN halt
HALTED  3 symbol(s), 6 quote leg(s) cancelled
Requires role: ADMIN. Exit code 0 if accepted, 1 if rejected.

resume — Lift the exchange-wide halt

pm-admin-cli --id GW_ADMIN resume
RESUMED  3 symbol(s)
Requires role: ADMIN.

halt-sym — Halt trading on a single symbol

pm-admin-cli --id GW_ADMIN halt-sym --sym AAPL
HALTED  AAPL  0 quote leg(s) cancelled
Halts only the specified symbol. All other symbols continue trading. Any active MM quote legs for that symbol are cancelled. The symbol remains halted until resume-sym is called. Requires role: ADMIN.

Flag Required Description
--sym SYMBOL yes Symbol to halt

resume-sym — Resume a single halted symbol

pm-admin-cli --id GW_ADMIN resume-sym --sym AAPL
RESUMED  AAPL
Resumes a symbol halted by halt-sym or by an automatic circuit-breaker trigger. Requires role: ADMIN.

Flag Required Description
--sym SYMBOL yes Symbol to resume

cancel-sym — Cancel all resting orders for a symbol

pm-admin-cli --id GW_ADMIN cancel-sym --sym AAPL
CANCEL_SYM OK  AAPL  orders=12  quotes=2
Cancels every resting order and active quote for AAPL across all connected gateways. This is an emergency book-clearing command — unlike kill, which targets a single gateway, cancel-sym clears the entire order book for one symbol regardless of who placed the orders. The symbol remains in its current halt state; no halt or resume is triggered. Requires role: ADMIN.

Flag Required Description
--sym SYMBOL yes Symbol whose orders to cancel

kill — Cancel all orders/quotes for a gateway

# All symbols
pm-admin-cli --id GW_ADMIN kill --gw TRADER01

# Scoped to one symbol
pm-admin-cli --id GW_ADMIN kill --gw TRADER01 --sym AAPL
KILL OK  TRADER01  orders=4  quotes=0

Flag Required Description
--gw GW_ID yes Target gateway to cancel for
--sym SYMBOL no Scope to one symbol (omit for all)

kick — Forcefully disconnect a gateway

pm-admin-cli --id GW_ADMIN kick --gw TRADER01
pm-admin-cli --id GW_ADMIN kick --gw TRADER01 --reason "Compliance hold"
No ack is published. Exit code is always 0 if the engine is reachable. Verify with orders --gw TRADER01.

Flag Required Description
--gw GW_ID yes Target gateway to disconnect
--reason TEXT no Reason string recorded in the engine log

qcancel — Cancel a market-maker's active quote on one symbol

pm-admin-cli --id GW_ADMIN qcancel --gw MM01 --sym AAPL
QCANCEL OK  MM01  AAPL
Cancels both bid and ask legs of the active quote. Resting limit orders are unaffected. Use kill to also remove those.

book — Print the order-book snapshot for a symbol

pm-admin-cli --id GW_ADMIN book --sym AAPL
┌──────────────────────────────────────────────────────┐
│               Order Book — AAPL                      │
├────────────┬────────────┬────────────┬───────────────┤
│   Bid Qty  │  Bid Price │  Ask Price │       Ask Qty │
├────────────┼────────────┼────────────┼───────────────┤
│        300 │     149.50 │     150.00 │           100 │
│        200 │     149.25 │     150.25 │           250 │
└────────────┴────────────┴────────────┴───────────────┘
  Last trade: 149.75 × 200

orders — List resting orders for a gateway

pm-admin-cli --id GW_ADMIN orders --gw TRADER01

Useful to confirm a kill or kick took effect:

pm-admin-cli --id GW_ADMIN kill --gw TRADER01
pm-admin-cli --id GW_ADMIN orders --gw TRADER01   # should print 'No resting orders'

symbols — List all configured instruments

pm-admin-cli --id GW_ADMIN symbols
┌─────────────────────────────┐
│   Configured instruments    │
├────┬────────────────────────┤
│ #  │ Symbol                 │
├────┼────────────────────────┤
│ 1  │ AAPL                   │
│ 2  │ MSFT                   │
│ 3  │ TSLA                   │
└────┴────────────────────────┘

session — Request a session-phase transition

pm-admin-cli --id GW_ADMIN session --state CONTINUOUS
SESSION  OPENING_AUCTION → CONTINUOUS

Flag Required Description
--state STATE yes Target state (case-insensitive): PRE_OPEN OPENING_AUCTION CONTINUOUS CLOSING_AUCTION CLOSED

Invalid transitions are silently rejected by the engine. Check the printed result to verify the transition was applied.

session-status — Show current session state (read-only)

pm-admin-cli --id GW_ADMIN session-status
  Session state     : CONTINUOUS
  Auto-scheduling   : ON
Returns the current phase without triggering any transition. Useful for monitoring scripts that need to know the exchange state before taking action.

schedule — Show the session-transition schedule

pm-admin-cli --id GW_ADMIN schedule
┌─────────────────────────────────────────┬────────────────┐
│   Session schedule                        │ Time (HH:MM)  │
├─────────────────────────────────────────┼────────────────┤
│ Pre-Open                                │ 08:30         │
│ Opening Auction Start                   │ 09:00         │
│ Continuous Trading Start                │ 09:30         │
│ Closing Auction Start                   │ 16:00         │
│ Closing Auction End                     │ 16:15         │
└─────────────────────────────────────────┴────────────────┘
If sessions_enabled is false in the engine config, a message is printed before the table explaining that automatic scheduling is disabled.

gateways — List all configured gateways

pm-admin-cli --id GW_ADMIN gateways
┌────────────┬──────────┬──────────────────────┬────────────┐
│ ID         │ Role     │ Description          │ Connected  │
├────────────┼──────────┼──────────────────────┼────────────┤
│ GW_ADMIN   │ ADMIN    │ Operator console     │    YES     │
│ TRADER01   │ TRADER   │ Proprietary desk 1   │    YES     │
│ MM01       │ MARKET_MAKER │ Market maker 1   │    no      │
└────────────┴──────────┴──────────────────────┴────────────┘
Shows every gateway entry from engine_config.yaml with its role and current connection status. A gateway not listed in the config but that somehow connected will not appear (it would have been rejected during auth).

volume — Show daily traded volume

pm-admin-cli --id GW_ADMIN volume
┌────────────┬──────────────┬───────────────────┬────────┐
│ Symbol     │        Qty   │             Value │ Trades │
├────────────┼──────────────┼───────────────────┼────────┤
│ AAPL       │        5,000 │      750,000.00   │     12 │
│ MSFT       │        3,200 │      576,000.00   │      8 │
│ TSLA       │        1,100 │      275,000.00   │      5 │
├────────────┼──────────────┼───────────────────┼────────┤
│ TOTAL      │        9,300 │    1,601,000.00   │     25 │
└────────────┴──────────────┴───────────────────┴────────┘
Counters reset when the engine restarts. There is currently no automatic end-of-day reset; daily volume accumulates across the entire engine session.

Shell scripting example

#!/bin/bash
set -e
ID="--id GW_ADMIN"

# Halt the entire exchange while investigating
pm-admin-cli $ID halt

# Cancel all exposure and disconnect the offending participant
pm-admin-cli $ID kill --gw ROGUE01
pm-admin-cli $ID kick --gw ROGUE01 --reason "Automated risk breach"

# Confirm no orders remain
pm-admin-cli $ID orders --gw ROGUE01

# Resume when clear
pm-admin-cli $ID resume
#!/bin/bash
# Targeted: suspend one symbol and clear its book, keep everything else trading
set -e
ID="--id GW_ADMIN"

# Halt only AAPL — MSFT and TSLA keep trading
pm-admin-cli $ID halt-sym --sym AAPL

# Clear all resting orders on AAPL across every participant
pm-admin-cli $ID cancel-sym --sym AAPL

# ... investigate ...

# Reopen AAPL when satisfied
pm-admin-cli $ID resume-sym --sym AAPL

pm-index-cli — Index History Query Tool

pm-index-cli is a read-only command-line tool for querying index history files written by pm-index. It reads JSONL files directly from disk — no running pm-index process is required.

pm-index-cli [--config PATH] [--data-dir DIR] [--format table|json|csv] [--no-header] COMMAND [options]

Global options

Option Default Description
--config PATH / -c PATH unset Path to engine_config.yaml; used to discover history file paths and index IDs automatically
--data-dir DIR data/indexes Directory containing history JSONL files; used when --config is not given or an index is not in config
--format table\|json\|csv table Output format
--no-header off Suppress header row (CSV only)

Subcommands

Subcommand Purpose
level Throttled LEVEL records — index value snapshots captured during trading
eod EOD records — one row per trading day with open/high/low/close
events Structural events: INIT, CORP_ACTION, ADD_CONSTITUENT, DELIST
indices List configured indices from engine_config.yaml

All history subcommands (level, eod, events) share the same set of per-index and time-range options:

Option Description
--index ID / -i ID Index ID to query (repeatable); defaults to all configured indices when --config is given
--days N Return records from the last N days (mutually exclusive with --from)
--from DATE_OR_TS Start of time range: YYYY-MM-DD or ISO-8601 (mutually exclusive with --days)
--to DATE_OR_TS End of time range: YYYY-MM-DD or ISO-8601 (default: now)
--limit N Maximum rows per index

The events subcommand also accepts:

Option Description
--type TYPE / -t TYPE Filter to one event type (repeatable): INIT, CORP_ACTION, ADD_CONSTITUENT, DELIST

Output columns

level subcommand:

Column Description
ts UTC timestamp (YYYY-MM-DDTHH:MM:SS)
index_id Index identifier
level Index level at this snapshot
session_state Session state at time of snapshot
aggregate_cap Aggregate market capitalisation (sum of price × shares for all constituents)
divisor Current divisor

eod subcommand:

Column Description
date Calendar date (YYYY-MM-DD)
index_id Index identifier
open Day-open index level
high Day-high index level
low Day-low index level
close Closing index level
level Final level at EOD write time (same as close)
aggregate_cap Aggregate market cap at session close
divisor Divisor at session close

events subcommand:

Column Description
ts UTC timestamp
index_id Index identifier
type Event type: INIT, CORP_ACTION, ADD_CONSTITUENT, or DELIST
symbol Affected symbol (empty for INIT)
detail Human-readable summary (e.g. SPLIT 2:1, ref_price=195.0)
old_divisor Divisor before adjustment (empty for INIT)
new_divisor Divisor after adjustment (empty for INIT)
level Index level after the event

Examples

Current day's live index snapshots:

pm-index-cli --config engine_config.yaml level --index EDU100 --days 1

All EOD records for two indices (table format):

pm-index-cli --config engine_config.yaml eod --index EDU100 --index TECH2

EOD records as CSV for spreadsheet import:

pm-index-cli --config engine_config.yaml eod --format csv > index_history.csv

EOD records as JSON for scripting:

pm-index-cli --config engine_config.yaml eod --index EDU100 --format json \
  | python3 -c "import json,sys; [print(r['date'], r['close']) for r in json.load(sys.stdin)]"

Corporate actions and constituent changes in the last 30 days:

pm-index-cli --config engine_config.yaml events --days 30

Only stock-split events, all time:

pm-index-cli --config engine_config.yaml events --type CORP_ACTION --index EDU100

60-day intraday snapshots in a date range for analysis:

pm-index-cli \
  --config engine_config.yaml \
  --format csv \
  level \
  --index EDU100 \
  --from 2026-05-01 \
  --to 2026-06-25 \
  --limit 50000 \
  > idu100_intraday.csv

List all configured indices:

pm-index-cli --config engine_config.yaml indices
id     | description              | history_file                            | state_file                           | constituents
-------+--------------------------+-----------------------------------------+--------------------------------------+----------------
EDU100 | EduMatcher broad index   | data/indexes/EDU100_history.jsonl       | data/indexes/EDU100_state.json       | AAPL,MSFT,TSLA
TECH2  | Technology pair          | data/indexes/TECH2_history.jsonl        | data/indexes/TECH2_state.json        | AAPL,MSFT

Without a config file (direct path):

pm-index-cli --format table level \
  --index EDU100 \
  --data-dir /var/edumatcher/indexes \
  --days 7

No-row behaviour

Format Output when no rows match
table No rows found.
json []
csv Header row only (unless --no-header)

Plotting example (Python)

import subprocess, json, matplotlib.pyplot as plt

data = json.loads(
    subprocess.check_output([
        "pm-index-cli", "--config", "engine_config.yaml",
        "--format", "json",
        "eod", "--index", "EDU100",
    ])
)

dates  = [r["date"]  for r in data]
closes = [r["close"] for r in data]

plt.figure(figsize=(10, 4))
plt.plot(dates, closes, marker="o")
plt.title("EDU100 — daily closing level")
plt.ylabel("Index level")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("edu100_history.png", dpi=150)

ExchangeCommandClient

ExchangeCommandClient lives in src/edumatcher/commands/client.py and is importable as:

from edumatcher.commands import ExchangeCommandClient, CommandTimeoutError

Each command method sends a two-frame ZMQ multipart message over the PUSH socket and blocks on the SUB socket until the matching ack topic arrives or the timeout elapses. On timeout the method raises CommandTimeoutError.

ExchangeCommandClient is a context manager — use with to ensure sockets are closed on exit.

Constructor parameters

Parameter Default Description
gw_id (required) Gateway ID to authenticate as — must be configured in engine_config.yaml
push_addr tcp://127.0.0.1:5555 Engine PULL socket address
pub_addr tcp://127.0.0.1:5556 Engine PUB socket address
timeout_ms 3000 Milliseconds to wait for an ack before raising CommandTimeoutError

Usage example

from edumatcher.commands import ExchangeCommandClient, CommandTimeoutError

with ExchangeCommandClient("GW_ADMIN") as client:
    auth = client.connect()
    if not auth["accepted"]:
        raise RuntimeError(f"Auth failed: {auth['reason']}")

    # ADMIN: halt, inspect, then resume
    try:
        result = client.halt_all()
        print(f"Halted {result['halted_symbols']} symbols, "
              f"cancelled {result['cancelled_quotes']} quote legs")

        orders = client.order_list("TRADER01")
        for o in orders:
            print(f"  {o['id'][:8]}  {o['symbol']}  {o['side']}  qty={o['remaining_qty']}")

        client.resume_all()
    except CommandTimeoutError as e:
        print(f"Engine did not respond: {e}")
# Sockets are closed automatically when the with-block exits.

Command reference

Command Arguments Auth required Underlying message Ack topic
connect() system.gateway_connect system.gateway_auth.{GW}
disconnect() system.gateway_disconnect (none)
halt_all() ADMIN risk.circuit_breaker_halt_all risk.circuit_breaker_halt_all_ack.{GW}
resume_all() ADMIN risk.circuit_breaker_resume_all risk.circuit_breaker_resume_all_ack.{GW}
kill_switch(target, symbol?) target GW ID, optional symbol Any connected GW risk.kill_switch risk.kill_switch_ack.{target}
mass_cancel(target, symbol) target GW ID, symbol Any connected GW risk.kill_switch risk.kill_switch_ack.{target}
quote_cancel(target, symbol) target GW ID, symbol Any connected GW quote.cancel quote.ack.{target}
gateway_kick(target, reason?) target GW ID, optional reason Any connected GW system.gateway_disconnect (none)
book_depth(symbol) symbol Any connected GW book.snapshot_request book.{SYMBOL}
order_list(target) target GW ID Any connected GW order.orders_request order.orders.{target}
symbol_list() Any connected GW system.symbols_request system.symbols.{GW}
session_advance(state) target state string Any connected GW session.transition session.state
session_status() Any connected GW system.session_state_request system.session_status.{GW}
session_schedule() Any connected GW system.session_schedule_request system.session_schedule.{GW}
gateway_list() Any connected GW system.gateways_request system.gateways.{GW}
volume() Any connected GW system.volume_request system.volume.{GW}
symbol_halt(symbol) symbol ADMIN risk.symbol_halt risk.symbol_halt_ack.{GW}
symbol_resume(symbol) symbol ADMIN risk.symbol_resume risk.symbol_resume_ack.{GW}
cancel_symbol(symbol) symbol ADMIN risk.cancel_symbol risk.cancel_symbol_ack.{GW}

PUSH socket has no authentication

The engine does not verify who placed a message on the PUSH socket — it only checks the gateway_id field inside the payload. ADMIN enforcement (halt/resume) is a role check inside the engine handler, not a transport- level control. This is appropriate for a learning system running on localhost; a production venue would add TLS mutual authentication and signing.

Command details

halt_all — Exchange-wide circuit-breaker halt

Frame 0:  b"risk.circuit_breaker_halt_all"
Frame 1:  {"gateway_id": "GW_ADMIN"}
client = ExchangeCommandClient("GW_ADMIN")
client.connect()

result = client.halt_all()
# result = {
#   "accepted": True,
#   "reason": "",
#   "halted_symbols": 4,
#   "cancelled_quotes": 12
# }
print(f"Halted {result['halted_symbols']} symbols, cancelled {result['cancelled_quotes']} quote legs")

The engine sets every known symbol to HALTED with resumption_mode = MANUAL. No timer is set — the halt is permanent until resume_all() is called or the session transitions to CLOSED.

While halted:

  • MARKET / FOK / IOC orders are rejected with SYMBOL_HALTED.
  • LIMIT / ICEBERG orders are accepted and rest without matching.
  • Quote submission is rejected.

resume_all — Lift the exchange-wide halt

Frame 0:  b"risk.circuit_breaker_resume_all"
Frame 1:  {"gateway_id": "GW_ADMIN"}
result = client.resume_all()
# result = {
#   "accepted": True,
#   "reason": "",
#   "resumed_symbols": 4
# }
print(f"Resumed {result['resumed_symbols']} symbols")

For each previously halted symbol the engine publishes circuit_breaker.resume.<SYMBOL> with mode = "MANUAL". Normal order flow and MM quote obligations resume immediately after the ack is received.

kill_switch — Cancel all exposure for a gateway

Frame 0:  b"risk.kill_switch"
Frame 1:  {"gateway_id": "TRADER01", "symbol": ""}
# Cancel everything for TRADER01
result = client.kill_switch("TRADER01")
# result = {
#   "accepted": True,
#   "reason": "",
#   "cancelled_orders": 7,
#   "cancelled_quotes": 0
# }

Kill switch does not halt the gateway

Resting orders and quotes are cancelled but the gateway remains connected and can submit fresh orders immediately. There is no resume message because nothing is halted. To prevent the gateway from submitting new orders, follow up with gateway_kick().

mass_cancel — Cancel exposure for one symbol

Frame 0:  b"risk.kill_switch"
Frame 1:  {"gateway_id": "TRADER01", "symbol": "AAPL"}
result = client.mass_cancel("TRADER01", "AAPL")
# result = {"accepted": True, "cancelled_orders": 3, "cancelled_quotes": 2}

Identical to kill_switch with a symbol argument. Only orders and quotes for TRADER01 on AAPL are affected.

quote_cancel — Cancel a market-maker's quote for one symbol

Frame 0:  b"quote.cancel"
Frame 1:  {"gateway_id": "MM01", "symbol": "AAPL"}
result = client.quote_cancel("MM01", "AAPL")
# result = {"accepted": True, "quote_id": "MM01-AAPL-..."}

Cancels both the bid and ask legs of the active quote. Resting limit orders submitted outside the quote mechanism are unaffected. Use mass_cancel to also remove non-quote limit orders.

gateway_kick — Forcefully disconnect a gateway

Frame 0:  b"system.gateway_disconnect"
Frame 1:  {"gateway_id": "TRADER01", "reason": "Compliance hold"}
client.gateway_kick("TRADER01", reason="Compliance hold")

The engine applies the gateway's configured disconnect_behaviour:

Behaviour Effect
LEAVE_ALL Session marked disconnected, all orders and quotes left resting
CANCEL_QUOTES_ONLY Quotes cancelled, limit orders left resting
CANCEL_ALL All quotes and orders cancelled

No ack is published. Verify the effect with order_list("TRADER01").

book_depth — L1 / L2 order-book snapshot

Frame 0:  b"book.snapshot_request"
Frame 1:  {"symbol": "AAPL"}
book = client.book_depth("AAPL")
# book = {
#   "symbol": "AAPL",
#   "bids": [{"price": 149.50, "qty": 300, "count": 2}, ...],
#   "asks": [{"price": 150.00, "qty": 100, "count": 1}, ...],
#   "last_price": 149.75,
#   "last_qty": 200,
#   "recent_trades": [...]
# }

best_bid = book["bids"][0] if book["bids"] else None
best_ask = book["asks"][0] if book["asks"] else None
print(f"AAPL  {best_bid['price']} x {best_bid['qty']}  /  {best_ask['price']} x {best_ask['qty']}")

Returns the same payload as the live book.{SYMBOL} subscription but on demand rather than waiting for the next change. Useful for a one-shot dashboard or a pre-trade sanity check.

Real-time L1/L2 feed

For a continuous feed, subscribe directly to book.AAPL on the PUB socket (port 5556) rather than polling with book_depth. The engine publishes a new snapshot after every state-changing event.

order_list — Inspect a gateway's resting orders

Frame 0:  b"order.orders_request"
Frame 1:  {"gateway_id": "TRADER01"}
orders = client.order_list("TRADER01")
for o in orders:
    print(f"  {o['id'][:8]}  {o['symbol']}  {o['side']}  {o['order_type']}  "
          f"qty={o['remaining_qty']}/{o['quantity']}  price={o['price']}")

Returns all resting (unfilled, non-cancelled) orders across all symbols for the target gateway. Useful for confirming that a kill_switch or mass_cancel took effect.

symbol_list — Discover configured instruments

Frame 0:  b"system.symbols_request"
Frame 1:  {"gateway_id": "GW_ADMIN"}
symbols = client.symbol_list()
print("Configured symbols:", symbols)
# ["AAPL", "MSFT", "TSLA", "GOOG"]

Returns the list of symbols the engine was started with. Useful at startup to drive iteration over all instruments (e.g. request a book snapshot for each).

session_advance — Manually drive the trading day

Frame 0:  b"session.transition"
Frame 1:  {"to_state": "CONTINUOUS"}
result = client.session_advance("CONTINUOUS")
# result = {"state": "CONTINUOUS", "prev_state": "OPENING_AUCTION"}

Valid state transitions:

stateDiagram-v2
    [*] --> PRE_OPEN
    PRE_OPEN --> OPENING_AUCTION
    OPENING_AUCTION --> CONTINUOUS
    CONTINUOUS --> CLOSING_AUCTION
    CLOSING_AUCTION --> CLOSED
    CLOSED --> [*]

Invalid transitions are silently rejected by the engine. The ack is the session.state broadcast — it carries the actual new state, so you can verify the transition succeeded by checking result["state"].

Full ADMIN operator workflow

The example below shows a complete emergency halt-and-resume sequence using the command client:

from edumatcher.commands import ExchangeCommandClient, CommandTimeoutError

client = ExchangeCommandClient("GW_ADMIN")

# 1. Connect
auth = client.connect()
assert auth["accepted"], f"Auth failed: {auth['reason']}"
print("Connected as ADMIN")

# 2. Inspect the market before halting
symbols = client.symbol_list()
for sym in symbols:
    book = client.book_depth(sym)
    bids = book.get("bids", [])
    asks = book.get("asks", [])
    bid_str = f"{bids[0]['price']} x {bids[0]['qty']}" if bids else "—"
    ask_str = f"{asks[0]['price']} x {asks[0]['qty']}" if asks else "—"
    print(f"  {sym:6}  bid={bid_str}  ask={ask_str}")

# 3. Halt all trading (e.g. technology incident detected)
result = client.halt_all()
print(f"HALT: {result['halted_symbols']} symbols halted, "
      f"{result['cancelled_quotes']} quotes cancelled")

# 4. Cancel all exposure for the affected participant
result = client.kill_switch("TRADER01")
print(f"Kill switch TRADER01: {result['cancelled_orders']} orders cancelled")

# 5. Kick the affected gateway
client.gateway_kick("TRADER01", reason="Compliance review")

# 6. Verify TRADER01 has no remaining orders
remaining = client.order_list("TRADER01")
assert not remaining, f"Unexpected orders: {remaining}"

# 7. Resume when the all-clear is given
result = client.resume_all()
print(f"RESUME: {result['resumed_symbols']} symbols resumed")

Extending the framework

To add a new command:

  1. Define the raw message in src/edumatcher/models/message.py (a make_<command>_msg helper and an ack helper).
  2. Handle it in the engine (src/edumatcher/engine/main.py) — add a _handle_<command> method and wire it into the dispatch elif chain.
  3. Add the ack topic prefix to _ACK_SUB_PREFIXES at the top of src/edumatcher/commands/client.py.
  4. Add the method that calls self._send(frames) and self._recv(ack_prefix).
  5. Add tests in tests/test_commands.py.
  6. Document it here with a command-reference row and a detail section.

See also