ALF Gateway - Order-Entry¶
Learning objectives
After reading this page you will understand:
- what
pm-alf-gwydoes and why it exists alongsidepm-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-enginerunning.- Gateway IDs that will connect must be configured in
engine_config.yamlunderparticipants. - 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:
Developer mode:
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:
Type the following lines, pressing Enter after each:
Expected response pattern:
WELCOME|PROTO=ALF1|GW=alf-gwy01|ID=TRADER01|HBINT=5|IDLE=30SYMBOLS|COUNT=Nfollowed by oneSYMBOL|SYM=...|TICK=...per instrumentEND|TYPE=SYMBOLSHB|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:
| 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¶
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¶
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)¶
Response: QUOTE_ACK|QUOTE_ID=...|SYM=AAPL|ACCEPTED=TRUE|...
KILL — gateway kill-switch¶
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¶
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¶
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¶
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¶
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)¶
| 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¶
| 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:
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¶
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:
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¶
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 |
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:
For a remote host:
Type the following lines (press Enter after each):
Expected output: WELCOME|..., then a SYMBOLS|COUNT=N block, then
END|TYPE=SYMBOLS, then the connection closes.
telnet (macOS / Linux):
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):
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¶
- ALF Protocol Reference — formal wire syntax and full field/enum definitions
- ALF Console — interactive command reference for
pm-alf-console - Drop Copy — the engine's drop-copy feed (
:5557) thatDC|STATE=ONrelays - Configuration —
alf_gateway:section andparticipantsallowlist - Processes — process topology and ZMQ message tables
- External Protocols Overview — protocol comparison and selection guide