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) 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-engine running.
  • Gateway IDs that will connect must be configured in engine_config.yaml under gateways.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:

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 (-vINFO, -vvDEBUG)
-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 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

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

CANCEL|ID=<order-id>
CANCEL|OCO_ID=<oco-id>
CANCEL|COMBO_ID=<combo-id>

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)

QUOTE_CANCEL|SYM=AAPL

Response: QUOTE_ACK|QUOTE_ID=...|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 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

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, 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:

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