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) 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 P&L and position display — use ORDERS + SYMBOLS instead |
POS |
Positions are computed locally in pm-alf-console |
HELP |
Interactive terminal reference text |
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.yamlundergateways.alf. - 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 gateways.alf list — no
separate credentials block is needed. Any gateway ID listed in gateways.alf
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 |
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 gateways.alf in config |
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
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 |
Responses: ACK|ORDER_ID=...|ACCEPTED=TRUE|... or ACK|ORDER_ID=...|ACCEPTED=FALSE|REASON=...
followed asynchronously by FILL|..., CANCELLED|..., or EXPIRED|....
AMEND — amend resting order¶
AMEND|ID=<order-id>|PRICE=151.00
AMEND|ID=<order-id>|QTY=200
AMEND|ID=<order-id>|PRICE=151.00|QTY=200
At least one of PRICE or QTY is required.
Response: AMENDED|ORDER_ID=...|PRICE=...|QTY=...|REMAINING=...|PRIORITY_RESET=TRUE|FALSE
CANCEL — cancel order / OCO / combo¶
Response for single order: CANCELLED|ORDER_ID=...
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=...|ACCEPTED=TRUE|BID_ID=...|ASK_ID=...
QUOTE_CANCEL — cancel active quote (MARKET_MAKER role)¶
Response: QUOTE_ACK|QUOTE_ID=...|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 live
qty/remaining/status per leg, same as before. 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 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 full column semantics (shared with pm-alf-console's QLEGS).
Multi-line response:
QLEGS|COUNT=2|RECENT_COUNT=1|SHOW=ALL
LEG|QUOTE_ID=Q123|SYM=AAPL|SIDE=BUY|ORDER_ID=7c4a91e2|QTY=500|REMAINING=400|FILLED=100|STATUS=PARTIAL_FILL|QUOTE_STATUS=ACTIVE
LEG|QUOTE_ID=Q123|SYM=AAPL|SIDE=SELL|ORDER_ID=be2170fd|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|QTY=500|REMAINING=500|FILLED=0|STATUS=CANCELLED
RECENT_ASK_LEG|QUOTE_ID=Q100|SIDE=SELL|ORDER_ID=8d1c4e05|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.
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, MODE |
Circuit-breaker resume |
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, SYMBOL, SIDE, TYPE |
FILL |
ORDER_ID, FILL_QTY, FILL_PRICE, REMAINING, STATUS |
AMENDED |
ORDER_ID, PRICE, QTY, REMAINING, PRIORITY_RESET |
CANCELLED |
ORDER_ID |
EXPIRED |
ORDER_ID |
QUOTE_ACK |
QUOTE_ID, 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 |
Error codes¶
Every error arrives as ERR|CODE=<CODE>|DETAIL=<message>.
| 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 |
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) |
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 |
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")
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"})
# → "NEW|SYM=AAPL|SIDE=BUY|TYPE=LIMIT|QTY=100|PRICE=150.00\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
[09:30:01.234] ACK xxxxxxxx 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
[09:30:01.234] ACK xxxxxxxx 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", 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")
# 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 gateways.alf |
Add the ID under gateways.alf 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 andgateways.alfallowlist - Processes — process topology and ZMQ message tables
- External Protocols Overview — protocol comparison and selection guide