Skip to content

ALF Gateway - Order-Entry

Learning objectives

After reading this page you will understand:

  • what pm-alf-gwy does and why it exists alongside pm-alf-console
  • how to configure it in engine_config.yaml
  • how to start it and verify connectivity from a terminal
  • the session lifecycle: HELLO → WELCOME → commands → EXIT
  • what commands are accepted and what responses to expect
  • how multi-line responses (SYMBOLS, ORDERS, QBOOT, QLEGS, POS|GW=) are framed
  • which broadcast events arrive unsolicited on every authenticated session
  • how heartbeats, idle timeouts, and rate limiting work
  • the error codes your client must handle
  • how to write a minimal Python ALF client

What this process is

pm-alf-gwy is the ALF TCP gateway. The interactive pm-alf-console terminal is designed for a human at a keyboard — it reads stdin, prints to stdout, and connects to the engine ZMQ sockets directly. That architecture cannot serve an external bot or a process on another host.

pm-alf-gwy fills that gap. It binds a TCP port, accepts multiple simultaneous connections, validates each line defensively, and translates ALF commands into the same engine ZMQ messages pm-alf-console uses today. Engine responses are translated back to ALF-formatted lines and delivered over TCP.

flowchart LR
    subgraph External["External ALF clients"]
        B1["Trading bot\n(TRADER01)"]
        B2["Script\n(TRADER02)"]
        B3["MM bot\n(MM01)"]
    end

    subgraph GWY["pm-alf-gwy  (TCP :5565)"]
        direction TB
        ACC["TCP accept loop"]
        SES["Session manager\n(auth · heartbeat · idle)"]
        PAR["ALF line parser\n(defensive)"]
        XLT["Command translator\n(ALF → ZMQ)"]
        DMX["Event demux\n(ZMQ PUB → per-client)"]
        ACC --> SES --> PAR --> XLT
        DMX --> SES
    end

    B1 -->|TCP| ACC
    B2 -->|TCP| ACC
    B3 -->|TCP| ACC
    XLT -->|"PUSH :5555"| ENG["pm-engine"]
    ENG -->|"PUB :5556"| DMX

What this is not

pm-alf-gwy accepts the same ALF command vocabulary as pm-alf-console but does not support interactive-terminal features:

Unsupported command Reason
STATUS Console session summary display — use ORDERS + SYMBOLS instead
HELP Interactive terminal reference text

A bare POS (no GW=) is also rejected — pm-alf-console's bare POS renders from its own local, session-scoped fill ledger, which pm-alf-gwy has no equivalent of. POS|GW=<gateway_id> is supported (see the command reference below) since that form is always a genuine engine round trip, never a local ledger — asking about your own connected gateway_id or any other gateway's (such as a running pm-mm-bot's) works identically.

QLEGS is supported — see the command reference below. Unlike pm-alf-console's QLEGS (which renders from its own local, session-scoped cache), pm-alf-gwy forwards the request to the engine over system.quote_legs_request and renders the engine's reply, including real RECENT/ALL history.

For interactive operator use, pm-alf-console remains the right tool. pm-alf-gwy is for programmatic clients and remote bots.

Prerequisites

  • pm-engine running.
  • Gateway IDs that will connect must be configured in engine_config.yaml under participants.
  • Optional: add the alf_gateway: config section to customise port and limits.

Configuration

Add an alf_gateway: section to engine_config.yaml:

alf_gateway:
  enabled: true
  name: "alf-gwy01"
  bind_address: "0.0.0.0"
  port: 5565
  heartbeat_interval_sec: 5
  handshake_timeout_sec: 10
  idle_timeout_sec: 30
  max_connections: 64
  max_client_queue: 10000
  max_commands_per_second: 100
  max_errors_before_disconnect: 50
  error_window_sec: 60

The gateway reads gateway roles from the existing participants list — no separate credentials block is needed. Any gateway ID listed in participants can connect to pm-alf-gwy.

Field Default Description
enabled true Master switch
name alf-gwy01 Process name echoed in WELCOME
bind_address 0.0.0.0 Network interface to listen on (127.0.0.1 for local-only)
port 5565 TCP listen port
heartbeat_interval_sec 5 Seconds between HB lines when no other outbound traffic
handshake_timeout_sec 10 Disconnect a connection that hasn't sent HELLO within this many seconds
idle_timeout_sec 30 Disconnect after this many seconds of inbound silence. This default applies only when an alf_gateway: section is present but omits the key; if the section is absent entirely, the effective default is 3600
max_connections 64 Maximum simultaneous TCP connections
max_client_queue 10000 Per-client outbound line buffer capacity
max_commands_per_second 100 Token-bucket rate limit per client
max_errors_before_disconnect 50 Error threshold in a sliding window before forced disconnect
error_window_sec 60 Width of the sliding window used to count errors toward max_errors_before_disconnect

TLS

pm-alf-gwy does not terminate TLS. For remote deployments, put it behind a reverse proxy (nginx, stunnel, or similar).

Start the gateway

Installed mode:

pm-engine --verbose
pm-alf-gwy

Developer mode:

poetry run pm-engine --verbose
poetry run pm-alf-gwy

CLI override options:

Option Default Description
--bind ADDR from config / 0.0.0.0 Override TCP bind address
--port PORT from config / 5565 Override TCP listen port
--engine-host HOST from config Override engine host (sets tcp://HOST:5555, tcp://HOST:5556, and tcp://HOST:5557 for drop copy)
--log-level WARNING Explicit level: CRITICAL, ERROR, WARNING, INFO, DEBUG
-v / --verbose off Increase verbosity (-v → INFO, -vv → DEBUG)
-q / --quiet off Reduce output to warnings/errors

Config file location

The engine configuration is read from <EDUMATCHER_DATA_DIR>/ref_data/engine_config.json. There is no --config flag: every process reads that one file, so none of them can be started against a configuration the others have not seen. Install one with pm-config-deploy.

Quick connect test

Use nc or telnet to validate the session lifecycle before writing any code:

nc 127.0.0.1 5565

Type the following lines, pressing Enter after each:

HELLO|CLIENT=test|PROTO=ALF1|ID=TRADER01
SYMBOLS
EXIT

Expected response pattern:

  1. WELCOME|PROTO=ALF1|GW=alf-gwy01|ID=TRADER01|HBINT=5|IDLE=30
  2. SYMBOLS|COUNT=N followed by one SYMBOL|SYM=...|TICK=... per instrument
  3. END|TYPE=SYMBOLS
  4. HB|TS=... every 5 seconds of quiet

TCP is a byte stream

Never assume one recv() equals one line. Always buffer and split on \n as shown in the Python example below.

Session lifecycle

Every ALF gateway session follows this exact sequence:

sequenceDiagram
    participant C as ALF Client
    participant G as pm-alf-gwy
    participant E as pm-engine

    C->>G: TCP connect 5565
    C->>G: HELLO|CLIENT=mybot|PROTO=ALF1|ID=TRADER01
    G->>E: system.gateway_connect {gateway_id "TRADER01"}
    E-->>G: system.gateway_auth.TRADER01 {accepted true}
    G-->>C: WELCOME|PROTO=ALF1|GW=alf-gwy01|ID=TRADER01|HBINT=5|IDLE=30
    Note over C,G: Session active - commands accepted
    C->>G: NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00
    G->>E: order.new ...
    E-->>G: order.ack.TRADER01 {accepted: true, ...}
    G-->>C: ACK|ORDER_ID=...|ACCEPTED=TRUE|...
    C->>G: PING
    G-->>C: PONG|TS=...
    C->>G: EXIT
    G->>E: system.gateway_disconnect
    Note over G: TCP closed

Step 1 — Send HELLO

The first line must be a HELLO:

HELLO|CLIENT=mybot|PROTO=ALF1|ID=TRADER01
Field Required Notes
CLIENT Yes Free-text label for logging (max 32 chars)
PROTO Yes Must be exactly ALF1
ID Yes Gateway ID that must be in participants in config; max 32 characters (connection closed with INVALID_VALUE if exceeded)

On any other first line the gateway sends ERR|CODE=AUTH_REQUIRED|... and closes the connection.

Step 2 — Authentication round trip

The gateway sends system.gateway_connect to the engine and waits for the system.gateway_auth.<ID> reply. The engine is authoritative: if the ID is not in the allowlist the gateway sends ERR|CODE=AUTH_FAILED|DETAIL=... and closes.

Step 3 — Receive WELCOME

WELCOME|PROTO=ALF1|GW=alf-gwy01|ID=TRADER01|HBINT=5|IDLE=30

The gateway immediately follows the WELCOME with a SYMBOLS multi-line response so the client knows which instruments are configured.

Step 4 — Send commands

After WELCOME, any supported command may be sent. Responses and broadcast events arrive asynchronously.

Step 5 — Disconnect

Send EXIT (or QUIT) for a graceful close. The gateway notifies the engine and shuts down the TCP connection after flushing pending output.

Command reference

All commands follow the ALF line format: VERB|FIELD=VALUE|FIELD=VALUE\n. Field names are case-insensitive — the gateway normalises everything to uppercase before parsing.

NEW — submit order

Single-leg order:

NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TAG=ORDER-001
NEW|SYM=AAPL|SIDE=BUY|TYPE=MARKET|QTY=50
NEW|SYM=AAPL|SIDE=BUY|TYPE=STOP|QTY=100|STOP=148.00
NEW|SYM=AAPL|SIDE=BUY|TYPE=STOP_LIMIT|QTY=100|STOP=148.00|PRICE=147.50
NEW|SYM=AAPL|SIDE=BUY|TYPE=FOK|QTY=100|PRICE=150.00
NEW|SYM=AAPL|SIDE=BUY|TYPE=IOC|QTY=100|PRICE=150.00
NEW|SYM=AAPL|SIDE=BUY|TYPE=ICEBERG|QTY=1000|PRICE=150.00|VISIBLE=100
NEW|SYM=AAPL|SIDE=BUY|TYPE=TRAILING_STOP|QTY=100|TRAIL=0.50
NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TIF=GTC

OCO pair:

NEW|TYPE=OCO|OCO_ID=tp-sl|SYM=AAPL|QTY=100|TIF=DAY|LEG1_SIDE=SELL|LEG1_TYPE=LIMIT|LEG1_PRICE=152.00|LEG2_SIDE=SELL|LEG2_TYPE=STOP|LEG2_STOP=147.00

Multi-leg combo:

NEW|TYPE=COMBO|COMBO_ID=spread-1|COMBO_TYPE=AON|TIF=DAY|LEG_COUNT=2|LEG0.SYM=AAPL|LEG0.SIDE=BUY|LEG0.QTY=100|LEG0.PRICE=150.00|LEG1.SYM=MSFT|LEG1.SIDE=SELL|LEG1.QTY=50|LEG1.PRICE=400.00
Field NEW (single) Notes
SYM required Instrument symbol
SIDE required BUY or SELL
TYPE required LIMIT, MARKET, STOP, STOP_LIMIT, FOK, IOC, ICEBERG, TRAILING_STOP, OCO, COMBO
QTY required Positive integer
PRICE conditional Required for LIMIT, FOK, IOC, ICEBERG, STOP_LIMIT
STOP conditional Required for STOP, STOP_LIMIT
VISIBLE conditional Required for ICEBERG; must be < QTY
TRAIL conditional Required for TRAILING_STOP
TIF optional DAY (default), GTC, ATO, ATC
SMP optional NONE (default), CANCEL_AGGRESSOR, CANCEL_RESTING, CANCEL_BOTH
TAG optional Client order tag, max 64 chars, characters limited to A-Z0-9-_.; echoed on order lifecycle responses

Responses: ACK|ORDER_ID=...|ACCEPTED=TRUE|... or ACK|ORDER_ID=...|ACCEPTED=FALSE|REJECT_CODE=...|REASON=... followed asynchronously by FILL|..., CANCELLED|..., or EXPIRED|....

FILL includes TRADE_IDS, a comma-separated list of the durable public trade IDs that composed the fill. It normally contains one ID; a swept order can contain several in execution order. An empty value means no public trade ID was available for that fill.

FILL also includes LIQUIDITY=<MAKER|TAKER>: MAKER if this order was resting on the book and got matched into, TAKER if it crossed the spread and matched immediately. It is the same maker/taker attribution the drop-copy DC_FILL line has always carried, now on the ordinary session fill too — no need to enable DC or cross-reference the drop-copy feed just to know which side of a trade you were on.

On rejected order ACKs, REJECT_CODE is the stable machine-readable rejection classification. REASON remains the human-readable explanation. Gateway-local validation errors use ERR|CODE=...|REJECT_CODE=...|DETAIL=... and echo TAG=... when the rejected command carried a correlating tag.

PRICE, STOP and TRAIL must be on the tick grid

Every price must be an exact multiple of the instrument's tick size — SYMBOLS reports it per symbol as TICK. One that is not is rejected by the gateway before it reaches the engine:

ERR|CODE=INVALID_VALUE|REJECT_CODE=TICK_VIOLATION|DETAIL=PRICE: 100.005 is not a multiple of AAPL's tick size 0.01|TAG=ORDER-001

The gateway refuses rather than rounds, because rounding would rest your order at a price you never sent and never tell you. Ordinary floating-point arithmetic is safe: a price your code computed as 100.00 - 3 * 0.01 is accepted as 99.97.

SYMBOLS_NOT_READY covers the startup window — the gateway will not price an order for a symbol whose tick precision it has not yet received from the engine, because converting against a default precision would move the price. Retry once the symbol list has arrived.

AMEND — amend resting order

AMEND|ID=<order-id>|PRICE=151.00[|RTAG=<request-tag>]
AMEND|ID=<order-id>|QTY=200[|RTAG=<request-tag>]
AMEND|ID=<order-id>|PRICE=151.00|QTY=200[|RTAG=<request-tag>]

At least one of PRICE or QTY is required. RTAG is optional and identifies this amend request; when present, the gateway echoes it on AMENDED or the rejected ACK.

Response: AMENDED|ORDER_ID=...|PRICE=...|QTY=...|REMAINING=...|PRIORITY_RESET=TRUE|FALSE[|RTAG=...]

CANCEL — cancel order / OCO / combo

CANCEL|ID=<order-id>[|RTAG=<request-tag>]
CANCEL|OCO_ID=<oco-id>
CANCEL|COMBO_ID=<combo-id>

RTAG is accepted on single-order cancels only. It identifies this cancel request and is echoed on CANCELLED or the rejected ACK.

Response for single order: CANCELLED|ORDER_ID=...[|RTAG=...]
Response for OCO: OCO_CANCELLED|OCO_ID=...|CANCELLED_ID=...|REASON=...
Response for combo: COMBO_STATUS|COMBO_ID=...|STATUS=CANCELLED|REASON=...

QUOTE — submit/replace two-sided quote (MARKET_MAKER role)

QUOTE|SYM=AAPL|BID=150.00|ASK=150.10|BID_QTY=500|ASK_QTY=500
QUOTE|SYM=AAPL|BID=150.00|ASK=150.10|BID_QTY=500|ASK_QTY=500|QUOTE_ID=my-q1

Requires gateway role MARKET_MAKER. BID must be strictly less than ASK.

Response: QUOTE_ACK|QUOTE_ID=...|SYM=AAPL|ACCEPTED=TRUE|BID_ID=...|ASK_ID=...

QUOTE_CANCEL — cancel active quote (MARKET_MAKER role)

QUOTE_CANCEL|SYM=AAPL

Response: QUOTE_ACK|QUOTE_ID=...|SYM=AAPL|ACCEPTED=TRUE|...

KILL — gateway kill-switch

KILL
KILL|SYM=AAPL

Cancels all resting orders and active quotes for this gateway, optionally scoped to one symbol.

Response: KILL_ACK|ACCEPTED=TRUE|ORDERS=N|QUOTES=N

DC — toggle drop-copy relay

DC|STATE=ON
DC|STATE=OFF

Subscribes (or unsubscribes) this session to the engine's drop-copy feed (DropCopyPublisher, ZMQ PUB :5557 — a separate socket from the main event bus on :5556, see Drop Copy), scoped to this session's own gateway_id. Unlike CALF/RALF, the drop-copy feed has no handshake or role model of its own — pm-alf-gwy maintains a single refcounted subscription per gateway ID on its drop-copy SUB socket and fans it out only to the session(s) that requested it.

Disabled by default per session. There is no gateway-wide config flag to force it on — each connecting client opts in independently with DC|STATE=ON, mirroring how a real drop-copy relay is provisioned per participant, not per venue.

Response: DC_ACK|STATE=ON or DC_ACK|STATE=OFF

Live events (while enabled): DC_FILL|SEQ=..|ORDER_ID=..|SYMBOL=..|FILL_QTY=..|FILL_PRICE=..|LIQUIDITY=.. — one per fill, delivered asynchronously in addition to (not instead of) the ordinary FILL message. SEQ and LIQUIDITY (MAKER/TAKER) come from the drop-copy envelope and are not present on FILL; DC_FILL does not carry REMAINING/STATUS the way FILL does. See Drop Copy — order.fill event for the full source payload.

> DC|STATE=ON
DC_ACK|STATE=ON
...
DC_FILL|SEQ=42|ORDER_ID=ORD-001|SYMBOL=AAPL|FILL_QTY=100|FILL_PRICE=150.05|LIQUIDITY=TAKER

SYMBOLS — instrument list

SYMBOLS

Multi-line response:

SYMBOLS|COUNT=3
SYMBOL|SYM=AAPL|TICK=0.01
SYMBOL|SYM=MSFT|TICK=0.01
SYMBOL|SYM=TSLA|TICK=0.01
END|TYPE=SYMBOLS

ORDERS — resting order list

ORDERS

Multi-line response:

ORDERS|COUNT=2|GW=TRADER01
ORDER|ID=abc123|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|REMAINING=60|PRICE=150.00|STATUS=PARTIAL
ORDER|ID=def456|SYM=MSFT|SIDE=SELL|TYPE=LIMIT|QTY=200|REMAINING=200|PRICE=415.00|STATUS=NEW
END|TYPE=ORDERS

QBOOT — quote bootstrap state

QBOOT
QBOOT|SYM=AAPL

Multi-line response:

QBOOT|COUNT=1
QUOTE|QUOTE_ID=...|SYM=AAPL|BID=150.00|ASK=150.10|BID_QTY=500|ASK_QTY=500|STATUS=ACTIVE
END|TYPE=QBOOT

QLEGS — quote leg snapshot (active + recent)

QLEGS
QLEGS|SYM=AAPL
QLEGS|SHOW=RECENT
QLEGS|SYM=AAPL|SHOW=ALL
Field Required Default Description
SYM No all symbols Restrict output to one symbol
SHOW No ACTIVE ACTIVE = currently live legs, RECENT = recently-inactivated quotes, ALL = both

pm-alf-gwy forwards this straight to the engine's system.quote_legs_request message and renders the reply. ACTIVE legs (LEG lines) carry each leg's live price/qty/remaining/status. RECENT rows (RECENT_LEG lines) are quote-level summaries drawn from the engine's bounded, in-memory, per-gateway history of recently-inactivated quotes. Each RECENT_LEG line is optionally followed by RECENT_BID_LEG and/or RECENT_ASK_LEG lines carrying that leg's final price/qty/remaining/filled/status snapshot at the moment it was cancelled — these are emitted only when the engine had that leg's final order state available at removal time, which is the common case for every normal inactivation path (see system.quote_legs_request for when a leg's snapshot can be absent). See ALF Console → QLEGS for the column semantics, which pm-alf-console and pm-alf-gwy now share.

PRICE on leg lines

PRICE is display money — the leg's limit price, the same number the QUOTE command was given. A quote leg is always a priced limit order, so it is normally present; an empty value means the engine had no price for that order, not that the leg was unpriced.

On RECENT_BID_LEG/RECENT_ASK_LEG it is the price as it stood at removal. The engine records it at that moment because once an order leaves the book nothing in the engine can recover its price, which is the same reason QTY/REMAINING/FILLED on those lines are final values rather than live ones.

Multi-line response:

QLEGS|COUNT=2|RECENT_COUNT=1|SHOW=ALL
LEG|QUOTE_ID=Q123|SYM=AAPL|SIDE=BUY|ORDER_ID=7c4a91e2|PRICE=150.00|QTY=500|REMAINING=400|FILLED=100|STATUS=PARTIAL|QUOTE_STATUS=ACTIVE
LEG|QUOTE_ID=Q123|SYM=AAPL|SIDE=SELL|ORDER_ID=be2170fd|PRICE=150.10|QTY=500|REMAINING=500|FILLED=0|STATUS=RESTING|QUOTE_STATUS=ACTIVE
RECENT_LEG|QUOTE_ID=Q100|SYM=AAPL|QUOTE_STATUS=CANCELLED|REASON=Cancelled by participant|REMOVED_AT_NS=1784468999030221878
RECENT_BID_LEG|QUOTE_ID=Q100|SIDE=BUY|ORDER_ID=3f9a2b71|PRICE=149.90|QTY=500|REMAINING=500|FILLED=0|STATUS=CANCELLED
RECENT_ASK_LEG|QUOTE_ID=Q100|SIDE=SELL|ORDER_ID=8d1c4e05|PRICE=150.20|QTY=500|REMAINING=200|FILLED=300|STATUS=CANCELLED
END|TYPE=QLEGS

An unconnected/unknown gateway still gets a well-formed, empty reply (QLEGS|COUNT=0|RECENT_COUNT=0|SHOW=... followed immediately by END|TYPE=QLEGS) rather than an error — QLEGS never fails on a bad gateway ID, it simply has nothing to report.

POS|GW=<gateway_id> — query another gateway's position

POS|GW=MM_AAPL_01
Field Required Default Description
GW Yes — The gateway_id to ask about — any connected gateway, not just your own

A bare POS (no GW=) is rejected with ERR|CODE=MISSING_FIELD (POS requires GW=<gateway_id>).

POS|GW= forwards system.position_request for the given gateway_id to the engine and renders the engine's system.position_snapshot.{gateway_id} reply: net quantity and average cost, per symbol, for every symbol that gateway has a non-zero position in. Unlike every other command on this page, the reply's topic is scoped to the queried gateway_id, not to your own session's gateway_id, so pm-alf-gwy subscribes to it on demand for the duration of one outstanding query and drops the subscription again once the reply arrives (or the session disconnects) — this is invisible on the wire, but it is why POS|GW= is the one command whose reply topic differs from every other request/reply pair in this protocol.

Multi-line response:

POSITION|GW=MM_AAPL_01|COUNT=1
POS_ENTRY|SYM=AAPL|NET_QTY=-300|AVG_COST=150.05
END|TYPE=POSITION

A flat gateway (or one with no position in any symbol) still gets a well-formed, empty reply (POSITION|GW=<gateway_id>|COUNT=0 followed immediately by END|TYPE=POSITION) rather than an error — same convention as QLEGS and QBOOT for an unconnected/unknown/flat gateway.

SESSION — query current trading session state

SESSION

Requests the engine's current trading session state (PRE_OPEN, OPENING_AUCTION, CONTINUOUS, CLOSING_AUCTION, CLOSED) for this gateway. pm-alf-gwy forwards this straight to the engine's system.session_state_request message and routes the reply back to the querying session only.

Response:

SESSION|STATE=CONTINUOUS|PREV_STATE=|SESSIONS_ENABLED=TRUE

This reuses the same SESSION message type as the unsolicited session-state broadcast below — a queried response is simply a SESSION line with PREV_STATE left empty (a query has no "previous state" to report) and an additional SESSIONS_ENABLED field indicating whether session-state gating is active for this engine. Existing clients that already parse the broadcast SESSION line need no changes to also handle the queried response.

PING / EXIT

PING        → PONG|TS=2026-07-02T09:30:00.123Z
EXIT        → (connection closed)
QUIT        → (connection closed)

Broadcast events

These messages arrive unsolicited on every authenticated session.

Message type Key fields Trigger
SESSION STATE, PREV_STATE Session phase change (e.g. CONTINUOUS, CLOSED). Also sent (with PREV_STATE empty and an added SESSIONS_ENABLED field) as a direct reply to the SESSION command
HALT SYMBOL, LEVEL Circuit-breaker halt on a symbol
RESUME SYMBOL, SRC Circuit-breaker resume; SRC is the halt source that triggered the original halt
TRADE SYMBOL, PRICE, QTY, SIDE Any matched trade on any symbol
HB TS Periodic heartbeat when no other outbound activity

Your client does not need to subscribe to anything. Broadcast events are delivered automatically after WELCOME.

Engine-scoped events (per-gateway)

These messages are addressed to your gateway ID and arrive on your session only.

Message type Key fields
ACK ORDER_ID, ACCEPTED, REASON, REJECT_CODE, SYMBOL, SIDE, TYPE, TAG, RTAG
FILL ORDER_ID, FILL_QTY, FILL_PRICE, REMAINING, STATUS, TRADE_IDS, LIQUIDITY, TAG
AMENDED ORDER_ID, PRICE, QTY, REMAINING, PRIORITY_RESET, TAG, RTAG
CANCELLED ORDER_ID, TAG, RTAG, CANCEL_REASON — see Unsolicited cancels
EXPIRED ORDER_ID, TAG
QUOTE_ACK QUOTE_ID, SYM, ACCEPTED, REASON, BID_ID, ASK_ID
QUOTE_STATUS QUOTE_ID, STATUS, REASON
COMBO_ACK COMBO_ID, ACCEPTED, REASON
COMBO_STATUS COMBO_ID, STATUS, REASON
OCO_ACK OCO_ID, ACCEPTED, LEG1_ID, LEG2_ID, REASON
OCO_CANCELLED OCO_ID, CANCELLED_ID, REASON
KILL_ACK ACCEPTED, REASON, ORDERS, QUOTES
DC_ACK STATE (ON/OFF) — reply to DC, not unsolicited
DC_FILL SEQ, ORDER_ID, SYMBOL, FILL_QTY, FILL_PRICE, LIQUIDITY — only while DC\|STATE=ON is active, see DC

SYM on QUOTE_ACK

Quote events are addressed to a gateway, not to a symbol, so a client quoting several instruments would otherwise have to remember which quote it sent last and match replies by send order. That breaks the moment one reply is missed. SYM names the instrument on the ack itself, so no client-side bookkeeping is needed. It is empty only when the quote was rejected before its symbol was known — a malformed payload, or a missing SYM= on the request.

QUOTE_ID remains the correlation key for the specific quote: supply your own QUOTE_ID= on the QUOTE command and it is echoed here, on QUOTE_STATUS, and stamped on both leg orders. If you omit it the engine generates one.

Every one of these carries TAG when the order was submitted with one, so a client correlates an event to its own order without matching on arrival order. That includes events you did not ask for — an expiry hours after you last touched the order, or a cancel the exchange decided on.

Unsolicited cancels

A CANCELLED line does not always answer a CANCEL you sent. The exchange cancels orders on its own: self-match prevention, the unfillable remainder of a MARKET or IOC order, a kill switch, an OCO sibling, a DAY order at the close. Two fields tell you which happened:

Field Meaning
RTAG Present only when you asked, echoing the RTAG from your CANCEL. Absent means the exchange decided
CANCEL_REASON Why the exchange decided. Absent on your own cancels
CANCELLED|ORDER_ID=ORD-7f3a|TAG=ORDER-001|CANCEL_REASON=SELF_MATCH_PREVENTED
CANCEL_REASON Meaning
SELF_MATCH_PREVENTED The order would have traded against another order from your own gateway. Which order is cancelled — the aggressor, the resting one, or both — follows the SMP action in force
INSUFFICIENT_LIQUIDITY A MARKET or IOC order ran out of book. Whatever traded is reported by the preceding FILL lines; this cancels the remainder, which never rests
KILL_SWITCH A gateway kill switch cancelled this order
CIRCUIT_BREAKER_HALT Cancelled as part of a circuit-breaker halt
GATEWAY_DISCONNECT Cancelled because the owning gateway session disconnected
ADMIN_CANCEL_SYMBOL An administrator cancelled all resting orders on this symbol
QUOTE_REPLACED A market-maker quote leg was superseded by a newer quote
QUOTE_LEG_FILLED The sibling leg of a two-sided quote was auto-cancelled because this leg filled

The field is omitted, not empty, when you requested the cancel yourself. Treat a value you do not recognise the same way RTAG/CANCEL_REASON both absent is treated: new members may be added, but existing ones are never removed or renamed.

CANCEL_REASON deliberately does not share the REJECT_CODE vocabulary. A cancel is not a rejection — the order was accepted, and may well have traded.

Error codes

Every error arrives as ERR|CODE=<CODE>|REJECT_CODE=<canonical-code>|DETAIL=<message>. When the rejected command carried TAG or RTAG, the gateway also echoes it as TAG so the client can correlate the error with the submitted line.

Code When it occurs Connection kept?
AUTH_REQUIRED Any command before HELLO completes No — closed immediately
AUTH_TIMEOUT No HELLO received within handshake_timeout_sec (default 10s) of connecting No
AUTH_FAILED Engine rejected the gateway ID No
PROTO_MISMATCH HELLO with wrong PROTO value No
GATEWAY_ALREADY_CONNECTED Same gateway ID already has an active session No
HELLO_ALREADY_PENDING A second HELLO arrives while an earlier one is still awaiting the engine's auth result Yes
BAD_MESSAGE Empty line, non-UTF-8, or line > 4096 bytes Yes
UNKNOWN_COMMAND Unrecognised command verb Yes
MISSING_FIELD Required field absent Yes
INVALID_VALUE Field value fails validation (e.g. PRICE=NaN, or a price off the instrument's tick grid — the latter carries REJECT_CODE=TICK_VIOLATION) Yes
SYMBOLS_NOT_READY Any command naming a symbol before the engine's symbol list has reached the gateway (REJECT_CODE=SYMBOL_NOT_READY) — transient, retry Yes
SYMBOL_NOT_CONFIGURED Unknown symbol (after symbols are loaded) Yes
ROLE_DENIED Command not allowed for this gateway's role (e.g. QUOTE for non-MM) Yes
RATE_LIMITED Commands arriving faster than max_commands_per_second Yes
SLOW_CLIENT Outbound queue full No
IDLE_TIMEOUT No inbound traffic for idle_timeout_sec No
MAX_ERRORS Too many errors in the sliding error window No
ENGINE_UNAVAILABLE Command could not be forwarded to the engine (engine unreachable); retry shortly Yes
INTERNAL_ERROR Unexpected gateway-internal exception Yes

Error escalation

If a client accumulates max_errors_before_disconnect (default 50) errors in the error_window_sec sliding window, the gateway disconnects it. This protects the gateway from runaway or malicious clients.

Example libraries and interactive clients

The examples/alf/ directory contains ready-to-run Python and C libraries that replicate the workflow of pm-alf-console as an external TCP client — no ZeroMQ, no edumatcher package import, only a plain socket.

examples/alf/
├── python/
│   ├── alf_parser.py       # Protocol library: parse, build, AlfSession
│   └── alf_client.py       # Interactive client (tab-completion, event display, P&L)
└── c/
    ├── alf_parser.h / .c   # C library
    ├── alf_client.c        # Interactive C client (readline + select)
    └── Makefile

Python

Library — alf_parser.py

from alf_parser import parse_alf_line, build_alf_line, AlfSession, AlfMessage

# Parse one line received from the gateway
msg: AlfMessage = parse_alf_line("ACK|ORDER_ID=abc|ACCEPTED=TRUE|SYMBOL=AAPL|TAG=ORDER-001")
print(msg.msg_type)    # "ACK"
print(msg.fields)      # {"ORDER_ID": "ABC", "ACCEPTED": "TRUE", ...}

# Build a line to send
line = build_alf_line("NEW", {"SYM": "AAPL", "SIDE": "BUY",
                               "TYPE": "LIMIT", "QTY": "100", "PRICE": "150.00", "TAG": "ORDER-001"})
# → "NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TAG=ORDER-001\n"

# High-level session: connect, HELLO/WELCOME, send/recv
session = AlfSession.connect("127.0.0.1", 5565, "TRADER01")
print(session.welcome.gw_name)         # "alf-gwy01"
session.send("SYMBOLS")
msg = session.recv_msg()               # first line of SYMBOLS response
session.close()

Interactive client — alf_client.py

cd docs/examples/alf/python

# Connect to a local gateway
python3 alf_client.py --id TRADER01

# Connect to a remote gateway
python3 alf_client.py --host 10.0.0.5 --port 5565 --id TRADER01

At the prompt the client behaves like pm-alf-console: Tab completes command verbs, field names, and enum values. Background receive thread displays fills, acks, and broadcast events while you type. POS shows tracked positions. STATUS shows session info. History is saved to ~/.alf_client_history.

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TAG=ORDER-001
[09:30:01.234] ACK      xxxxxxxx tag=ORDER-001  order accepted
[TRADER01]> ORDERS
[TRADER01]> POS
[TRADER01]> HELP
[TRADER01]> EXIT

C

Build:

# macOS: brew install readline (Homebrew readline for full callback support)
# Linux: sudo apt install libreadline-dev

cd docs/examples/alf/c
make

Run:

./alf_client --id TRADER01
./alf_client --host 10.0.0.5 --port 5565 --id TRADER01
./alf_client --id TRADER01 --no-color

The C client uses select() to multiplex the TCP socket and stdin, so gateway events display immediately while you are typing. Readline provides tab completion and history.

[TRADER01]> NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TAG=ORDER-002
[09:30:01.234] ACK      xxxxxxxx tag=ORDER-002  order accepted
[TRADER01]> ORDERS
[TRADER01]> POS
[TRADER01]> HELP
[TRADER01]> EXIT

Library usage:

#include "alf_parser.h"

/* Parse */
char line[] = "ACK|ORDER_ID=abc|ACCEPTED=TRUE";
alf_message_t msg;
alf_parse_line(line, &msg);
puts(alf_get_field(&msg, "ACCEPTED"));   /* "TRUE" */

/* Build */
const char *kv[] = {"SYM", "AAPL", "SIDE", "BUY",
                    "TYPE", "LIMIT", "QTY", "100", "PRICE", "150.00", "TAG", "ORDER-003", NULL};
char buf[4096];
alf_build_line(buf, sizeof(buf), "NEW", kv);
write(sockfd, buf, strlen(buf));

Minimal zero-dependency client (Python)

For scripts that cannot import anything outside the standard library:

import socket

def alf_connect(host: str, port: int, gateway_id: str, client_name: str = "bot"):
    sock = socket.create_connection((host, port), timeout=5)
    buf = bytearray()

    def send(line: str) -> None:
        sock.sendall((line + "\n").encode("utf-8"))

    def recv_line() -> str:
        while True:
            nl = buf.find(b"\n")
            if nl >= 0:
                line = bytes(buf[:nl]).decode("utf-8", errors="replace")
                del buf[:nl + 1]
                return line
            chunk = sock.recv(4096)
            if not chunk:
                raise RuntimeError("gateway closed connection")
            buf.extend(chunk)

    send(f"HELLO|CLIENT={client_name}|PROTO=ALF1|ID={gateway_id}")
    while True:
        line = recv_line()
        if line.startswith("WELCOME"):
            break
        if line.startswith("ERR"):
            raise RuntimeError(f"Auth failed: {line}")

    return sock, send, recv_line


sock, send, recv_line = alf_connect("127.0.0.1", 5565, "TRADER01")
send("NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00|TAG=ORDER-004")

# Read events until ACK arrives — HB/SESSION/TRADE may arrive first
while True:
    line = recv_line()
    print(line)
    if line.startswith("ACK"):
        break

send("EXIT")
sock.close()

TCP is a byte stream

Never assume one recv() equals one line. Always buffer and split on \n.

When to use pm-alf-gwy vs. the alternatives

Scenario Best choice
Human operator on the same machine pm-alf-console (tab completion, history, P&L display)
External bot in Python / any language on the same or a remote host pm-alf-gwy
Browser UI / REST-native stack pm-api-gwy
Read-only market-data consumer pm-md-gwy (CALF)
Post-trade / clearing / audit consumer pm-ralf-gwy (RALF)

Troubleshooting

Check whether the port is in use

Before starting pm-alf-gwy, or when a client cannot connect, verify that something is actually listening on port 5565.

macOS:

# lsof — shows the process name and PID holding the port
sudo lsof -iTCP:5565 -sTCP:LISTEN

# BSD netstat (ships with macOS)
netstat -an | grep LISTEN | grep 5565

Linux:

# ss — preferred on modern Linux
ss -tlnp 'sport = :5565'

# lsof
sudo lsof -iTCP:5565 -sTCP:LISTEN

# netstat (older distributions)
netstat -tlnp | grep 5565

If no output appears, the gateway is not running or is bound to a different port. Check the alf_gateway.port value in engine_config.yaml.

Test the TCP connection from the command line

Use nc (netcat) to open a raw TCP connection and type ALF lines by hand. This bypasses any client library and proves the gateway is reachable end-to-end.

macOS / Linux:

nc 127.0.0.1 5565

For a remote host:

nc 10.0.0.5 5565

Type the following lines (press Enter after each):

HELLO|CLIENT=test|PROTO=ALF1|ID=TRADER01
SYMBOLS
EXIT

Expected output: WELCOME|..., then a SYMBOLS|COUNT=N block, then END|TYPE=SYMBOLS, then the connection closes.

telnet (macOS / Linux):

telnet 127.0.0.1 5565

Type HELLO|CLIENT=test|PROTO=ALF1|ID=TRADER01 and press Enter. telnet echoes characters locally so the line appears duplicated in the terminal — the WELCOME response confirms the gateway accepted it. Press Ctrl-], then type quit to close.

Non-interactive test (useful in scripts or CI):

printf 'HELLO|CLIENT=test|PROTO=ALF1|ID=TRADER01\nEXIT\n' | nc 127.0.0.1 5565

Expected output ends with BYE or a clean connection close immediately after WELCOME.

Common problems

Symptom Likely cause Fix
Connection refused Gateway not started or wrong port Confirm pm-alf-gwy is running; check alf_gateway.port in config
Connection hangs with no output Firewall blocking port 5565 Test on loopback (127.0.0.1) first; open port in firewall for remote access
ERR\|CODE=AUTH_REQUIRED immediately First line was not HELLO Ensure the very first line is a valid HELLO
ERR\|CODE=AUTH_FAILED Gateway ID not in participants Add the ID under participants in engine_config.yaml and restart engine
ERR\|CODE=PROTO_MISMATCH PROTO field value is not ALF1 Fix the HELLO line: HELLO\|CLIENT=...\|PROTO=ALF1\|ID=...
ERR\|CODE=GATEWAY_ALREADY_CONNECTED Same gateway ID connected elsewhere Disconnect the other session, or use a different gateway ID
WELCOME arrives but then silence Engine not running or ZMQ link lost Start pm-engine; check gateway logs for ZMQ errors
Gateway closes after ~30 s of silence idle_timeout_sec elapsed Send PING periodically; reduce idle_timeout_sec in config if needed
ERR\|CODE=RATE_LIMITED Commands arriving faster than max_commands_per_second Throttle the client; increase max_commands_per_second in config
Gateway not reachable from another host bind_address: 127.0.0.1 Change bind_address to 0.0.0.0 (or the specific interface IP)

See also