Appendix: CALF Protocol Reference¶
Status: Normative. This appendix is the single source of truth for the CALF
1.0.0wire contract as implemented bypm-md-gwy(md_gateway/). For an operational, tutorial-style guide see Market Data Feed (CALF); for the gateway's configuration block see Engine Config Specification §6.3. The key words MUST, MUST NOT, SHOULD, and MAY are used per RFC 2119.
What CALF is¶
CALF stands for Channel ALF.
CALF is EduMatcher's text market-data protocol. It is designed for educational clarity and bot usability: human-readable on the wire, easy to debug in a terminal, and strict enough to support deterministic clients.
CALF complements the other application protocols:
| Protocol | Purpose |
|---|---|
| ALF | Text order entry (interactive) |
| BALF | Binary order entry (low-latency programmatic) |
| CALF | Channelized text market data |
This appendix is the normative reference for CALF 1.0.0 semantics.
Scope & conformance¶
CALF is the external market-data protocol exposed by pm-md-gwy. The gateway
subscribes to internal engine PUB topics and translates them into CALF lines
for TCP clients.
This appendix specifies the client-visible CALF protocol. It does not specify internal engine message schemas beyond what is needed to explain CALF behavior.
Supported in CALF 1.0.0¶
- top-of-book updates (
MD) by symbol - trade prints (
TRADE) by symbol - state transitions (
STATE) for session-wide and symbol-level changes - index level updates (
IDX) frompm-index - aggregated multi-level order book updates (
DEPTH) — Level 2, not order-by-order - auction uncross results (
AUCTION) — equilibrium price, matched quantity, and imbalance for open/close auctions and circuit-breaker reopening auctions - full circuit-breaker halt/resume detail (
CB) — trigger price, reference price, ladder level, auto-resume time, and halt cause, alongside the coarseSTATEtransition - point-in-time stream baselines (
SNAP) forTOP,STATE,INDEX,DEPTH, andCB - per-stream sequence numbers on
(CH, SYM) SYM=*wildcard subscriptions forSTATE,TOP,TRADE, andAUCTION- bounded replay on reconnect (
RESUME+LASTSEQ) - heartbeat and liveness signaling
- gateway capability advertisement via
WELCOME|CH_SUPPORTED=
Out of scope in CALF 1.0.0¶
- full order-by-order (Level 3) market data —
DEPTHis Level 2 (aggregated per price level), never per-order SYM=*forINDEX,DEPTH, orCB- multicast / UDP transport
- entitlement matrix per field
- durable historical replay from disk
- protocol-layer authentication token
Transport and session model¶
| Property | Value |
|---|---|
| Transport | TCP |
| Default port | 5570 |
| Encoding | UTF-8 line protocol |
| Delimiter | \n |
| Max line length | 4096 bytes including newline |
A CALF client connection is long-lived.
- Client must send
HELLOwithin 5 seconds of TCP connect. - Gateway replies with
WELCOMEon success. - Client may then send
SUB,RESUME,UNSUB,SYMBOLS,PING, andEXIT. - Gateway streams
SNAP,MD,TRADE,STATE,IDX,DEPTH,AUCTION,CB,HB, andERR.
If no HELLO is received within 5 seconds, the gateway closes the socket.
Wire format¶
Line structure¶
Every CALF message is one line:
MSGTYPE is the first token and is always uppercase ASCII.
Examples:
HELLO|CLIENT=bot01|PROTO=CALF1
TRADE|CH=TRADE|SYM=AAPL|SEQ=809|TS=2026-06-07T10:16:00.141Z|PX=150.12|QTY=200|SIDE=BUY
Parsing behavior¶
- Messages are delimited by newline (
\n). \r\nfrom clients is accepted for robustness.- Field order after
MSGTYPEis not significant. - Unknown keys are ignored unless needed for validating a specific message.
- Duplicate keys: last occurrence wins.
- Empty lines are invalid and may result in
ERR|CODE=BAD_MESSAGE.
TCP stream requirement¶
TCP is a byte stream, not a message queue.
A receiver must buffer bytes and split by newline. A single recv() may
contain half a line, one full line, or many lines.
Field conventions¶
Reserved keys¶
| Key | Meaning |
|---|---|
CH |
Logical channel (TOP, TRADE, STATE, INDEX, DEPTH, AUCTION, CB) |
SYM |
Symbol, index id, or * where allowed |
SEQ |
Sequence number for one (CH, SYM) stream |
TS |
UTC ISO-8601 timestamp with milliseconds |
Wire value types¶
| Type | Wire representation | Example |
|---|---|---|
| Decimal price | Text decimal | 150.25 |
| Integer | Base-10 text integer | 1200 |
| Boolean flag | 0 or 1 |
EXC=1 |
| Timestamp | UTC ISO-8601 ms | 2026-06-07T10:15:23.411Z |
Optional fields are omitted when not present. Empty required values are invalid.
One optional field pair carries meaning when empty rather than being malformed:
MD's BID/ASK, where an empty value marks that book side as withdrawn —
see Withdrawal of a book side under MD.
Channel model¶
CALF groups market data into logical channels.
| Channel | Description | SYM=* allowed? |
|---|---|---|
TOP |
Best bid/ask updates and snapshots | Yes |
TRADE |
Trade prints | Yes |
STATE |
Session or symbol state transitions | Yes |
INDEX |
Index level updates | No |
DEPTH |
Aggregated multi-level order book (Level 2) | No |
AUCTION |
Auction uncross results (equilibrium price, imbalance) | Yes |
CB |
Circuit-breaker halt/resume detail (trigger price, level, ...) | No |
SNAP is a message type, not a channel.
A client does not subscribe to SNAP directly. The gateway auto-sends SNAP
for new SUB requests on TOP, STATE, INDEX, DEPTH, and CB. TRADE
and AUCTION have no baseline SNAP — only future events are delivered for
those two channels, since neither has a persistent "current value" to
snapshot.
SYM=* for TOP does not produce a single SNAP|SYM=*. Top-of-book has no
meaningful "wildcard" value, so the gateway instead sends one real SNAP per
currently known symbol, then live MD for any symbol — including ones that
become known only after the SUB — via the same wildcard subscription entry.
Subscription rules¶
SUBmay include multiple channels and symbols separated by commas.- A multi-value
SUBapplies to the Cartesian product of channels and symbols. SYM=*is valid when the channel set is a subset of{STATE, TOP, TRADE, AUCTION}, in any combination.SYM=*combined withINDEX,DEPTH, orCB— alone or mixed with any other channel in the sameSUB— is invalid.- A wildcard subscription (
SYM=*) counts as exactly one entry towardmax_symbols_per_client, not one entry per known symbol. - Re-subscribing an already active pair is idempotent.
- Maximum symbols per client are enforced by gateway config.
- If any requested
(CH,SYM)pair is invalid, the gateway rejects theSUBrequest withERRand leaves existing subscriptions unchanged. - For non-
INDEXchannels, a non-wildcardSYMis validated against the gateway's known instrument list (populated from the engine's configured symbols at startup); an unrecognized symbol returnsERR|CODE=INVALID_SYMBOL.INDEXis exempt from this check — index ids live in a separate namespace from tradable instrument symbols, so any non-empty id is accepted atSUBtime regardless of whether a matching index is actually configured.AUCTIONandCBare not exempt: a non-wildcard symbol for either is checked against the same known-instrument list asTOP/TRADE/STATE/DEPTH. - If the gateway's known-symbol list itself is empty (for example, engine config failed to load additional symbol metadata at gateway startup), known-symbol validation is skipped entirely and any non-wildcard symbol is accepted; this is a permissive fallback, not a documented steady-state mode, and operators should treat an empty known-symbol list as a configuration problem to fix rather than relied-upon behavior.
DEPTH and CB disallow SYM=* deliberately, not as an oversight: DEPTH
messages carry up to 2 x depth_levels price levels each, so a wildcard
subscription could multiply one client's outbound bandwidth by the entire
symbol count; CB halts/resumes are rare, per-symbol operator-relevant
events rather than a firehose use case. AUCTION, by contrast, allows
SYM=*: auction events are extremely low frequency (at most a handful per
symbol per trading day), so a wildcard subscription poses none of DEPTH's
bandwidth risk, and — like TRADE — AUCTION has no baseline SNAP to
burst per symbol on subscribe.
SUB|CH=TOP,TRADE|SYM=AAPL,MSFT
SUB|CH=STATE|SYM=*
SUB|CH=TRADE|SYM=*
SUB|CH=TOP,TRADE,STATE|SYM=*
SUB|CH=DEPTH|SYM=AAPL
SUB|CH=AUCTION|SYM=*
SUB|CH=CB|SYM=AAPL
Message catalog¶
Session control messages¶
| Message | Direction | Purpose |
|---|---|---|
HELLO |
Client -> Gateway | Start session |
WELCOME |
Gateway -> Client | Confirm session and advertise parameters |
SUB |
Client -> Gateway | Add subscriptions |
RESUME |
Client -> Gateway | Replay one stream from a known sequence |
UNSUB |
Client -> Gateway | Remove subscriptions |
SYMBOLS |
Both directions | Request, and reply with, the instrument universe |
PING |
Client -> Gateway | Liveness probe |
PONG |
Gateway -> Client | Probe reply |
HB |
Gateway -> Client | Heartbeat when quiet |
ERR |
Gateway -> Client | Protocol or flow error |
EXIT |
Client -> Gateway | Clean disconnect |
Market-data messages¶
| Message | Direction | Purpose |
|---|---|---|
SNAP |
Gateway -> Client | Point-in-time baseline for one stream |
MD |
Gateway -> Client | Incremental top-of-book update |
INDIC |
Gateway -> Client | Indicative auction uncross, during a call phase |
TRADE |
Gateway -> Client | Trade print |
STATE |
Gateway -> Client | Session/symbol state transition |
IDX |
Gateway -> Client | Index level update |
DEPTH |
Gateway -> Client | Incremental multi-level order book update |
AUCTION |
Gateway -> Client | Auction uncross result |
CB |
Gateway -> Client | Circuit-breaker halt/resume detail |
Message definitions¶
HELLO¶
Direction: Client -> Gateway
Purpose: Session handshake.
Response: WELCOME on successful handshake, or ERR|CODE=PROTO_MISMATCH
(connection closed) if CLIENT/PROTO fail validation.
| Field | Req | Description |
|---|---|---|
CLIENT |
Yes | Client ID (ASCII, max 32 chars) |
PROTO |
Yes | Must be CALF1 |
Validation rules:
- Messages other than
HELLOsent before successful handshake receiveERR|CODE=AUTH_REQUIRED. HELLOis only accepted while a session is unauthenticated. A secondHELLOon an established session receivesERR|CODE=BAD_MESSAGE.
Changed: replay is no longer requested through a
RESUME=1flag onHELLO. BecauseHELLOis only ever processed once per connection, that form could resume a single stream and no more — leaving any client that follows several streams, which is most of them, unable to recover the rest after a reconnect. Replay now has its own repeatableRESUMEcommand, sent after the handshake.
WELCOME¶
Direction: Gateway -> Client
| Field | Req | Description |
|---|---|---|
PROTO |
Yes | Echoes negotiated protocol |
GW |
Yes | Gateway instance name |
HBINT |
Yes | Heartbeat interval in seconds |
REPLAY |
Yes | Replay window in seconds |
SYMBOLS |
No | Comma-separated snapshot of instrument symbols known to the gateway at connect time. Omitted when the gateway has no known symbols yet. The known-symbol set can grow after WELCOME is sent, as new book.{SYMBOL}/trade events arrive from the engine — this field is a point-in-time snapshot, not a fixed universe, and a symbol absent here may still become subscribable later without a new WELCOME. |
REF |
No | Per-symbol reference data as SYM:DEC tuples, where DEC is the instrument's display precision (tick_decimals). Covers exactly the symbols listed in SYMBOLS=, and is omitted whenever that field is. Absent entirely from gateways predating CALF 1.1.0 — a client detects support by its presence, not by PROTO, and falls back to 2 decimals when it is missing. |
CH_SUPPORTED |
No | Comma-separated list of channels this gateway build supports. Present on every CALF 1.0.0+ gateway; omitted entirely by earlier gateways. A client uses its presence — not the PROTO value, which does not change — to detect whether DEPTH/INDEX and the SYM=* wildcard extension are available. |
WELCOME|PROTO=CALF1|GW=md-gwy01|HBINT=1|REPLAY=30|SYMBOLS=AAPL,MSFT|CH_SUPPORTED=AUCTION,CB,DEPTH,INDEX,STATE,TOP,TRADE|REF=AAPL:2,MSFT:4
A client that receives no CH_SUPPORTED field must assume only TOP,
TRADE, and STATE are available and must not rely on SYM=* for TOP/
TRADE, INDEX, or DEPTH without first probing with SUB and handling a
possible ERR|CODE=INVALID_CHANNEL/INVALID_SYMBOL response.
SUB¶
Direction: Client -> Gateway
| Field | Req | Description |
|---|---|---|
CH |
Yes | Comma-separated channels |
SYM |
Yes | Comma-separated symbols (* only when CH is a subset of {STATE, TOP, TRADE, AUCTION}) |
Response semantics:
- No explicit ACK.
- New
TOP/STATE/INDEX/DEPTH/CBsubscriptions triggerSNAP. A wildcardTOPsubscription (SYM=*) triggers one realSNAPper currently known symbol, never a singleSNAPwith a literalSYM=*. TRADEandAUCTIONsubscriptions do not have a baselineSNAP; only future events are sent for those two channels.- Invalid requests return
ERR. - Existing successful subscriptions remain active when a later
SUBrequest is invalid.
SUB|CH=TOP,TRADE|SYM=AAPL,MSFT
SUB|CH=STATE|SYM=*
SUB|CH=TRADE|SYM=*
SUB|CH=DEPTH|SYM=AAPL
SUB|CH=AUCTION|SYM=*
SUB|CH=CB|SYM=AAPL
RESUME¶
Direction: Client -> Gateway
Purpose: Subscribe to one stream and replay what was missed since
LASTSEQ, instead of starting from a SNAP.
Response: replayed events in sequence order, or ERR|CODE=REPLAY_MISS
when the gap is wider than the replay window. On the snapshot-backed channels
(TOP, STATE, INDEX, DEPTH, CB) a fresh SNAP follows that error; on
TRADE and AUCTION it does not, because a past print has no current state to
snapshot. See "Reconnect behavior" below.
| Field | Req | Description |
|---|---|---|
CH |
Yes | Exactly one channel |
SYM |
Yes | Exactly one concrete symbol; SYM=* is invalid |
LASTSEQ |
Yes | Last sequence the client received on that stream |
Send one RESUME per stream being recovered. Unlike SUB, this message
takes no comma-separated lists: LASTSEQ describes a single stream's
position, so a multi-stream RESUME would have no coherent meaning.
RESUME|CH=TOP|SYM=AAPL|LASTSEQ=1042
RESUME|CH=TRADE|SYM=AAPL|LASTSEQ=88
RESUME|CH=DEPTH|SYM=MSFT|LASTSEQ=310
Validation rules:
CHorSYMcarrying more than one value isERR|CODE=BAD_MESSAGE.- A missing or non-positive
LASTSEQisERR|CODE=BAD_MESSAGE. - An unknown channel is
ERR|CODE=INVALID_CHANNEL. SYM=*isERR|CODE=INVALID_SYMBOLon every channel — see "Reconnect behavior".
All of these leave the session open. A client recovering several streams
sends several RESUME messages, and one bad request must not cost it the
others; only handshake-level failures close a connection.
SYMBOLS¶
Direction: Client -> Gateway (request), Gateway -> Client (reply)
Purpose: Ask which instruments this gateway knows about.
The request carries no fields:
The reply:
| Field | Req | Description |
|---|---|---|
COUNT |
Yes | How many symbols are known; 0 is a valid answer |
SYMBOLS |
No | Comma-separated symbols, sorted; omitted when COUNT=0 |
REF |
No | Per-symbol SYM:DEC display precision; same set as SYMBOLS, omitted alongside it |
Read COUNT, not the presence of SYMBOLS. An empty universe omits the field
rather than sending it empty, so the two must not be conflated with a
malformed reply.
Why this exists, given WELCOME|SYMBOLS=. That field is optional, sent
once, and omitted entirely when the gateway was started without a readable
engine config — a misconfiguration that otherwise looks, from the client side,
exactly like an exchange with no instruments. The gateway's set also grows
as symbols first appear on the engine bus, so a client that connected before a
given instrument traded could not learn of it without reconnecting. SYMBOLS
makes the universe both askable and refreshable.
Repeatable at any time. A client that needs the universe up front should send
it immediately after WELCOME rather than depending on the handshake field.
REF — per-symbol display precision¶
REF answers "how many decimal places does this instrument quote in?", which
a client otherwise has no way to discover. The only other source is
GET /api/symbols on the API gateway, which requires a trading credential —
something a market data consumer has no business holding — so before this field
existed every CALF client had to assume the default of 2 and was quietly
wrong about any instrument configured otherwise.
Three properties are deliberate:
- It is reference data, not market data.
tick_decimalsnever changes for a symbol, so it rides the handshake and this reply rather thanTOPorTRADE. Repeating a constant on every tick would be exactly whatMD's delta encoding exists to avoid. - Its presence is the capability signal. Like
CH_SUPPORTED, and for the same reason:PROTOstaysCALF1. A client seeing noREFfalls back to2knowingly rather than by accident. - The tuple has room to grow.
SYM:DECreuses the colon-delimited grammarDEPTHalready uses forprice:qty:count, and extends toSYM:DEC:MULT:CCYas further reference fields are defined — without another protocol change. Clients must ignore trailing components they do not recognise rather than treating the entry as malformed.
Every symbol in SYMBOLS appears in REF, so a client never has to reason
about one being listed in one field and missing from the other. A symbol first
seen on the engine bus, which has no configured precision, is reported at the
default rather than omitted.
UNSUB¶
Direction: Client -> Gateway
| Field | Req | Description |
|---|---|---|
CH |
Yes | Comma-separated channels |
SYM |
Yes | Comma-separated symbols |
UNSUB is idempotent. Removing a non-existent (CH,SYM) pair has no effect.
SNAP¶
Direction: Gateway -> Client
Purpose: Baseline for one stream.
SNAP uses channel-specific payload fields.
Common fields:
| Field | Req | Description |
|---|---|---|
CH |
Yes | TOP, STATE, INDEX, DEPTH, or CB |
SYM |
Yes | Symbol, index id, or * for session state |
SEQ |
Yes | Current stream sequence |
TS |
Yes | Snapshot timestamp |
CH=TOP fields:
| Field | Req | Description |
|---|---|---|
BID |
No | Best bid price |
BIDSZ |
No | Best bid size |
ASK |
No | Best ask price |
ASKSZ |
No | Best ask size |
LAST |
No | Last trade price |
LASTSZ |
No | Last trade size |
A SUB|CH=TOP|SYM=* never produces a SNAP with a literal SYM=* — see
"Channel model" above. Each SNAP in the resulting burst has a real SYM
and uses that symbol's own (TOP, SYM) sequence.
CH=STATE fields:
| Field | Req | Description |
|---|---|---|
SESSION |
Yes | Current state value |
CH=INDEX fields: identical field set to the IDX message — see that
section below. Since CALF 1.0.0, SUB|CH=INDEX sends a baseline SNAP
before any live IDX updates, the same as TOP/STATE/DEPTH.
CH=DEPTH fields: identical field set to the DEPTH message — see that
section below. BIDS/ASKS are omitted entirely (not sent as empty
strings) when a symbol's book has no resting orders on that side yet.
CH=CB fields: identical field set to the CB message — see that section
below. Reflects the last known circuit-breaker status for the symbol —
STATUS=ACTIVE with no further fields if the symbol has never halted.
TRADE/AUCTION stream note:
- Neither
CH=TRADEnorCH=AUCTIONhas aSNAPvariant in CALF1.0.0— both are pure event streams with no persistent "current value." - Delivery for both starts from events that occur after the subscription is
active (plus any replay via
RESUME, see "Sequence and recovery semantics").
SNAP|CH=TOP|SYM=AAPL|SEQ=100|TS=2026-06-07T10:16:00.000Z|BID=150.10|BIDSZ=1200|ASK=150.12|ASKSZ=900|LAST=150.11|LASTSZ=300
SNAP|CH=STATE|SYM=*|SEQ=5|TS=2026-06-07T10:16:00.000Z|SESSION=CONTINUOUS
SNAP|CH=INDEX|SYM=EDU100|SEQ=42|TS=2026-06-12T10:15:23.000Z|LEVEL=1048.73|OPEN=1042.10|HIGH=1056.30|LOW=1040.05|SESSION=CONTINUOUS
SNAP|CH=DEPTH|SYM=AAPL|SEQ=1|TS=2026-07-11T14:32:00.000Z|LEVELS=10|BIDS=150.10:1200:3,150.09:800:2|ASKS=150.12:900:2,150.13:600:1
SNAP|CH=CB|SYM=AAPL|SEQ=3|TS=2026-07-20T14:05:00.000Z|STATUS=HALTED|LEVEL=L2|TRIGGERPX=148.20|REFPX=150.10|RESUMEAT=2026-07-20T15:20:00.000Z|SRC=CB
SNAP|CH=CB|SYM=MSFT|SEQ=1|TS=2026-07-20T14:05:00.000Z|STATUS=ACTIVE
MD¶
Direction: Gateway -> Client
Purpose: Incremental TOP update. Unchanged sides may be omitted.
| Field | Req | Description |
|---|---|---|
CH |
Yes | TOP |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence |
TS |
Yes | Event timestamp |
BID |
No | Updated bid; empty value means the bid side is now empty |
BIDSZ |
No | Updated bid size |
ASK |
No | Updated ask; empty value means the ask side is now empty |
ASKSZ |
No | Updated ask size |
LAST |
No | Updated last trade price |
LASTSZ |
No | Updated last trade size |
Withdrawal of a book side¶
BID and ASK have three distinct states on an MD, and a client must treat
them differently:
| On the wire | Meaning |
|---|---|
BID=150.11 |
New value for this side |
BID= |
This side is now empty — discard the price |
| (absent) | Unchanged since the previous message |
An empty value is the only way the gateway can say "there is no bid". This
is the one documented exception to Wire value types above: BID/ASK are
optional fields, and for these two an empty value is meaningful rather than
malformed.
A client that treats a withdrawal as "unchanged" will display the last known
price indefinitely, and will disagree with any client that reconnects and
receives a fresh SNAP — which omits the side correctly. Merging code must
therefore remove the field on an empty value, not overwrite it.
LAST/LASTSZ are never withdrawn: once a symbol has traded, its last price
persists for the session, so an empty value is not valid for those fields.
LAST after a trade¶
A trade updates LAST/LASTSZ on the TOP channel as well as producing a
TRADE message. The two arrive at different times and neither replaces the
other:
| Channel | Carries | When |
|---|---|---|
TRADE |
Every individual print (PX, QTY, SIDE) |
Immediately, one message per trade |
TOP |
The latest price only (LAST, LASTSZ) |
With the next book republish, throttled by the engine's snapshot_interval_sec |
A client that wants every print subscribes to TRADE; a client that only wants
"what did this last trade at" can rely on TOP alone, including its SNAP
baseline. Several trades inside one throttle window collapse to the latest
price on TOP — that is the intended behaviour, not a dropped update.
A SNAP reports a trade immediately, without waiting for the next book
republish, so a client subscribing between the two is not handed a stale
price. Gateway builds before this was fixed suppressed LAST from the
following MD entirely, leaving a continuously-connected client on the price
baked into its original SNAP while a reconnecting client saw the true value.
TRADE¶
Direction: Gateway -> Client
| Field | Req | Description |
|---|---|---|
CH |
Yes | TRADE |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence |
TS |
Yes | Trade timestamp |
PX |
Yes | Trade price |
QTY |
Yes | Trade quantity |
SIDE |
Yes | Aggressor side (BUY or SELL) |
STATE¶
Direction: Gateway -> Client
| Field | Req | Description |
|---|---|---|
CH |
Yes | STATE |
SYM |
Yes | Symbol or * |
SEQ |
Yes | Stream sequence |
TS |
Yes | Transition timestamp |
SESSION |
Yes | New state value |
PREV |
No | Previous state when known |
NEXTPHASE |
No | Phase the session moves to next; SYM=* only |
NEXTAT |
No | When that transition is scheduled, UTC ISO-8601; SYM=* only |
Valid SESSION values:
PRE_OPENOPENING_AUCTIONCONTINUOUSCLOSING_AUCTIONCLOSEDHALTED(symbol-level)
STATE|CH=STATE|SYM=*|SEQ=14|TS=2026-06-07T10:30:00.000Z|SESSION=CONTINUOUS|PREV=OPENING_AUCTION|NEXTPHASE=CLOSING_AUCTION|NEXTAT=2026-06-07T16:25:00.000Z
STATE|CH=STATE|SYM=AAPL|SEQ=8|TS=2026-06-07T10:30:00.000Z|SESSION=CONTINUOUS|PREV=OPENING_AUCTION
STATE|CH=STATE|SYM=AAPL|SEQ=9|TS=2026-06-07T11:02:17.330Z|SESSION=HALTED|PREV=CONTINUOUS
NEXTPHASE/NEXTAT: the next scheduled transition.
Both appear on the SYM=* stream only, and only when the transition that
produced this state was driven by the session scheduler — the one component
that knows the day's timetable. They are what lets a client show a countdown
to the open, the closing auction, or the close, which is otherwise the
most-glanced number on a trading screen and the one CALF could not answer.
They are sent together or not at all. A phase with no time cannot be counted down to, and a time with no phase does not say what happens when it arrives.
Their absence is information. A manual or admin-driven transition carries no timetable, and the engine clears whatever the scheduler last advertised rather than leaving it in place: it has just moved somewhere the schedule did not predict, so the old target has stopped being a fact about anything. A client must render that as silence, not as a countdown to zero — and must not substitute a schedule it read from configuration, which describes what should happen rather than what the engine is actually going to do.
NEXTAT may pass without the transition arriving, if the scheduler is late or
has stopped. A client should say so rather than run a negative clock or freeze
at zero, since a late scheduler, a wedged one, and an absent one otherwise
look identical.
An exchange transition is published twice: once as SYM=*, and once per
symbol. A subscription matches on SYM=* or an exact symbol, so a client
that subscribed to one instrument would otherwise see its halts and resumes
but never the session around them — it would not learn the exchange had
opened or closed. Wildcard subscribers receive both forms; that is
deliberate, and the SYM=* line remains the authoritative exchange-level
event.
A symbol that is halted is not moved by an exchange transition. Its halt outlives the phase it began in, and the engine publishes an explicit resume when it ends.
A resume returns the symbol to whatever the exchange is doing at that
moment, which is not necessarily CONTINUOUS. Circuit-breaker halts expire
on elapsed time with no session check, so an L2 halt — 15 minutes by default
— triggered shortly before the close resumes into CLOSING_AUCTION or
CLOSED.
IDX¶
Direction: Gateway -> Client
Purpose: Index level update for one INDEX stream.
| Field | Req | Description |
|---|---|---|
CH |
Yes | INDEX |
SYM |
Yes | Index identifier (e.g. EDU50) |
SEQ |
Yes | Stream-local sequence number |
TS |
Yes | Event timestamp |
LEVEL |
Yes | Current index level, decimal string |
SESSION |
Yes | Index session state |
OPEN |
No | Day open level, decimal string |
HIGH |
No | Day high, decimal string |
LOW |
No | Day low, decimal string |
CHG |
No | Change from open, signed decimal e.g. +1.23 |
PCTCHG |
No | Percent change from open, signed e.g. +0.45 |
AGGCAP |
No | Aggregate market cap, integer string |
IDX has no baseline SNAP variant in CALF 1.0.0. Delivery starts from
events that occur after the subscription becomes active.
IDX|CH=INDEX|SYM=EDU50|SEQ=12|TS=2026-06-07T10:16:00.000Z|LEVEL=5123.45|SESSION=CONTINUOUS|OPEN=5100.00|CHG=+23.45|PCTCHG=+0.46
DEPTH¶
Direction: Gateway -> Client
Purpose: Aggregated, multi-level order book update — CALF's Level 2 view. Each level is an aggregate of every resting order at that price; individual order identity is never exposed (Level 3 is explicitly out of scope, see "Out of scope in CALF 1.0.0" above).
Response: No reply required. A gap in SEQ should trigger replay or
resync, exactly as for MD.
Unlike MD, which omits individual unchanged BID/ASK fields, DEPTH is
a full-ladder replace per message: whenever the top-LEVELS price
levels on either side change, the message carries that side's complete
current ladder, not a per-level diff. DEPTH is only sent when the tracked
levels actually changed since the previous DEPTH/SNAP for the symbol.
| Field | Req | Description |
|---|---|---|
CH |
Yes | DEPTH |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence for (DEPTH, SYM) |
TS |
Yes | Event timestamp |
LEVELS |
Yes | Number of price levels per side configured on this gateway (market_data_gateway.depth_levels, default 10) |
BIDS |
No | Bid-side ladder, best price first; omitted if no resting bids |
ASKS |
No | Ask-side ladder, best price first; omitted if no resting asks |
Level encoding grammar (applies to both BIDS and ASKS):
PRICE is decimal text, QTY is the aggregated resting quantity at that
price, COUNT is the number of individual resting orders aggregated into
it. : and , are ordinary field-value characters in CALF — the only
reserved wire character is |.
DEPTH|CH=DEPTH|SYM=AAPL|SEQ=2|TS=2026-07-11T14:32:00.512Z|LEVELS=10|BIDS=150.10:1400:4,150.09:800:2,150.08:400:1|ASKS=150.12:900:2,150.13:600:1,150.14:250:1
SYM=* is invalid for SUB|CH=DEPTH — see "Subscription rules" above.
sequenceDiagram
participant C as Client
participant G as pm-md-gwy
C->>G: SUB|CH=DEPTH|SYM=AAPL
Note over G: current DEPTH SEQ for AAPL is 1
G-->>C: SNAP|CH=DEPTH|SYM=AAPL|SEQ=1|LEVELS=10|BIDS=150.10:1200:3|ASKS=150.12:900:2
Note over G: book.AAPL changes at a tracked level
G-->>C: DEPTH|CH=DEPTH|SYM=AAPL|SEQ=2|LEVELS=10|BIDS=150.10:1400:4|ASKS=150.12:900:2
Note over G: book.AAPL changes but not within the top 10 levels
Note over G: no DEPTH message emitted — unchanged ladder is not resent
AUCTION¶
Direction: Gateway -> Client
Purpose: Result of one auction uncross for a symbol — a scheduled opening/closing auction, a circuit-breaker reopening auction, or the pass over restored GTC orders at engine startup. Published exactly once per uncross, even when there was no crossable interest at all.
| Field | Req | Description |
|---|---|---|
CH |
Yes | AUCTION |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence for (AUCTION, SYM) |
TS |
Yes | Event timestamp |
EQPX |
No | Equilibrium price; omitted when there was no crossable interest |
EQQTY |
Yes | Total executable quantity matched at EQPX (0 when no cross) |
TRADES |
Yes | Number of trades produced by the uncross (0 when no cross) |
IMBSIDE |
No | Residual imbalance side, BUY or SELL; omitted when balanced or no cross |
IMBQTY |
Yes | Residual imbalance quantity at EQPX (0 when balanced or no cross) |
REASON |
No | Which uncross this was: SCHEDULED (leaving an auction or other non-matching phase), REOPEN (a halted symbol reopening) or RECOVERY (restored GTC orders at engine startup) |
Without REASON the three are indistinguishable — the fields are otherwise
identical — so a client cannot tell a circuit-breaker reopening from the
closing auction. Treat an unrecognised value as absent rather than as an
error: it means a gateway newer than the client.
AUCTION has no baseline SNAP (see "Channel model" above) — a new
subscriber only receives auction results from the next uncross onward,
unless it also uses RESUME to replay recent history.
AUCTION|CH=AUCTION|SYM=AAPL|SEQ=1|TS=2026-07-20T13:30:00.012Z|EQPX=150.10|EQQTY=48200|TRADES=37|IMBSIDE=BUY|IMBQTY=1400
AUCTION|CH=AUCTION|SYM=TSLA|SEQ=4|TS=2026-07-20T20:00:00.004Z|EQQTY=0|TRADES=0|IMBQTY=0
AUCTION|CH=AUCTION|SYM=MSFT|SEQ=2|TS=2026-07-20T13:30:00.031Z|EQPX=421.00|EQQTY=15000|TRADES=12|IMBQTY=0
INDIC¶
Direction: Gateway -> Client
Purpose: Where a symbol would uncross if the call phase ended now.
Published repeatedly, on an interval, for the whole of an OPENING_AUCTION
or CLOSING_AUCTION.
This is the same channel as AUCTION and a different statement. AUCTION
reports what happened at an uncross; INDIC reports what would happen while
there is still time to act on it. A client must not treat one as the other:
an INDIC price is not a trade and nothing has printed at it.
| Field | Req | Description |
|---|---|---|
CH |
Yes | AUCTION |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence for (AUCTION, SYM) |
TS |
Yes | Event timestamp |
INDICPX |
No | Indicative uncross price; omitted when the book would not cross at all |
INDICQTY |
Yes | Quantity that would match at INDICPX (0 when there is no cross) |
IMB |
No | Which side the surplus runs, BUY or SELL; omitted when balanced |
IMBQTY |
Yes | Surplus quantity that would go unmatched (0 when balanced) |
PHASE |
No | The call phase running, so a client need not infer it from STATE |
INDICPX absent is a reading, not a gap. It means the bids and offers
collected so far do not overlap, so nothing would trade if the phase ended
now. Rendering it as a price of zero is wrong in the way that matters: it
asserts a clearing level the book does not have.
Why publish it at all. An imbalance nobody can see before the uncross is an imbalance nobody can offset. Disseminating it is what lets participants supply the offsetting interest that resolves it — the same reasoning the circuit-breaker path already applies to a reopening, and it holds with more force at the open and the close, where the largest volume of the day prints. The field names are shared with that path deliberately: a reopening auction and a scheduled one are the same mechanism, and a client that learned to read one should not have to learn the other.
Cadence. A fixed interval, configurable engine-side
(auction_indicative_interval_sec, default 1s), not one message per book
change: bounded cost regardless of how heavy order entry gets. Every symbol
is republished every interval, including ones whose reading has not changed —
otherwise a client cannot tell a stable indicative from a stalled feed.
Halted symbols are excluded. A halt is its own reopening auction with its
own corridor, and the CB channel already carries an indicative for it. Two
sources describing one symbol would eventually disagree.
Like AUCTION, INDIC has no baseline SNAP. A client joining mid-auction
waits at most one interval for the next reading.
INDIC|CH=AUCTION|SYM=AAPL|SEQ=12|TS=2026-07-20T13:29:45.000Z|INDICPX=150.10|INDICQTY=48200|IMB=BUY|IMBQTY=1400|PHASE=OPENING_AUCTION
INDIC|CH=AUCTION|SYM=TSLA|SEQ=12|TS=2026-07-20T13:29:45.000Z|INDICQTY=0|IMBQTY=0|PHASE=OPENING_AUCTION
The second example is a no-cross auction (EQPX/IMBSIDE both omitted, all
counts 0); the third is a perfectly balanced cross (IMBSIDE omitted,
IMBQTY=0, but EQPX/EQQTY/TRADES all present).
CB¶
Direction: Gateway -> Client
Purpose: Full circuit-breaker halt/resume detail for one symbol —
trigger price, reference price, ladder level, scheduled auto-resume time,
and what caused the halt. STATE (above) still carries the coarse
SESSION=HALTED/SESSION=CONTINUOUS transition unchanged; CB is emitted
alongside STATE, from the same underlying engine event, for clients
that also want the detail.
| Field | Req | Description |
|---|---|---|
CH |
Yes | CB |
SYM |
Yes | Symbol |
SEQ |
Yes | Stream sequence for (CB, SYM) |
TS |
Yes | Event timestamp |
STATUS |
Yes | ACTIVE (not halted) or HALTED |
LEVEL |
No | Ladder level (e.g. L1/L2/L3, config-defined) or ADMIN_ALL/ADMIN_SYMBOL for an operator-initiated halt; present only when STATUS=HALTED |
TRIGGERPX |
No | Trigger price; present only for an automatic (non-ADMIN_*) halt currently in effect |
REFPX |
No | Reference price at trigger time; present only for an automatic halt currently in effect |
RESUMEAT |
No | Scheduled auto-resume time, UTC ISO-8601 with ms (same format as TS); present only for a timed halt currently in effect — absent for rest-of-day or manual/ADMIN_* halts |
SRC |
No | What halted the symbol: CB for an automatic breaker trigger, ADMIN for an operator halt |
CORRLO |
No | Lower bound of the ACE reopening corridor; present only while STATUS=HALTED and ACE is enabled for the symbol |
CORRHI |
No | Upper bound of the same corridor |
EXP |
No | Number of ACE extensions consumed so far. 0 on the initial halt |
INDICPX |
No | Indicative uncross price observed at the end of a call phase. Extension events only |
INDICQTY |
No | Quantity that would have executed at INDICPX. Extension events only |
IMB |
No | BUY or SELL — which side the imbalance ran. Extension events only |
REASON |
No | CLOSING_BACKSTOP when the resume was forced by the end of the trading day. Resume events only |
CLAMPED |
No | 1 when the backstop printed at the corridor boundary rather than at the equilibrium. Resume events only |
PRINTPX |
No | The price the backstop printed at. Resume events only |
LEVEL/TRIGGERPX/REFPX/RESUMEAT/CORRLO/CORRHI/EXP describe the
halt that just ended and are always omitted on a resume event
(STATUS=ACTIVE) — only SRC carries over, since what caused the halt is
meaningful on the way out as well as in.
ACE corridor expansions¶
A halt does not necessarily end when RESUMEAT arrives. If the indicative
uncross price falls outside [CORRLO, CORRHI], the symbol stays halted, the
corridor widens by one ladder rung and a fresh call phase begins — see
Risk Controls - Automated Corridor Expansion.
The gateway publishes this as a further CB event with STATUS=HALTED and an
updated RESUMEAT, CORRLO, CORRHI and EXP. A client that ignores these
will show a RESUMEAT that has already passed and report the symbol as overdue
to reopen. No STATE event accompanies an extension: the symbol was halted
before and is halted after, so the coarse session state has not changed.
INDICPX/INDICQTY/IMB are carried on extension events only. They are
computed once, at the instant the call phase ends, so they are deliberately
absent from the SNAP baseline — replaying them later would assert a stale
price for a book that has kept moving. Disseminating them at all mirrors the
order-imbalance indicator real venues publish during a reopening, which is
what lets participants supply the offsetting interest that resolves the halt.
CORRLO/CORRHI/EXP are part of the SNAP baseline, because they
describe the halt still in force: a client subscribing mid-halt otherwise
cannot tell where the symbol is permitted to reopen.
A halt is a reopening auction's call phase. While a symbol is halted the
engine accepts LIMIT orders and rests them, rejects MARKET/FOK/IOC, and runs
no matching; when the halt ends it uncrosses at the equilibrium price and
publishes an AUCTION with REASON=REOPEN before the STATE and CB
resume events. There is no separate "resumption mode" to choose, and CALF no
longer carries one: crossed interest accumulates during the call, so
restarting continuous matching without an uncross would begin on a crossed
book. RESUMEAT says whether the halt ends by itself; SRC says who
started it. The two are independent.
CB has a baseline SNAP (see the SNAP section above) reflecting the
last known status for the symbol.
CB|CH=CB|SYM=AAPL|SEQ=4|TS=2026-07-20T14:05:00.010Z|STATUS=HALTED|LEVEL=L2|TRIGGERPX=148.20|REFPX=150.10|RESUMEAT=2026-07-20T15:20:00.000Z|SRC=CB
CB|CH=CB|SYM=TSLA|SEQ=1|TS=2026-07-20T15:00:00.000Z|STATUS=HALTED|LEVEL=ADMIN_ALL|SRC=ADMIN
CB|CH=CB|SYM=AAPL|SEQ=5|TS=2026-07-20T14:20:00.010Z|STATUS=ACTIVE|SRC=CB
An ACE sequence — halt, one extension, then a reopen inside the widened corridor:
CB|CH=CB|SYM=AAPL|SEQ=4|TS=2026-07-20T13:30:00.010Z|STATUS=HALTED|LEVEL=L1|TRIGGERPX=122.00|REFPX=100.00|RESUMEAT=2026-07-20T13:35:00.000Z|CORRLO=90.00|CORRHI=110.00|EXP=0|SRC=CB
CB|CH=CB|SYM=AAPL|SEQ=5|TS=2026-07-20T13:35:00.010Z|STATUS=HALTED|LEVEL=L1|TRIGGERPX=122.00|REFPX=100.00|RESUMEAT=2026-07-20T13:37:00.000Z|CORRLO=80.00|CORRHI=120.00|EXP=1|SRC=CB|INDICPX=122.00|INDICQTY=500|IMB=BUY
CB|CH=CB|SYM=AAPL|SEQ=6|TS=2026-07-20T13:37:00.010Z|STATUS=ACTIVE|SRC=CB
And a halt the trading day ended before ACE could resolve, printed at the corridor boundary rather than at the equilibrium:
CB|CH=CB|SYM=AAPL|SEQ=9|TS=2026-07-20T16:05:00.010Z|STATUS=ACTIVE|SRC=CB|REASON=CLOSING_BACKSTOP|CLAMPED=1|PRINTPX=120.00
The first example is an automatic threshold-breach halt (all detail fields
present); the second is an ADMIN exchange-wide halt (trigger/reference/
resume all omitted, matching the engine's None values for that path);
the third is the resume that follows the first halt (STATUS=ACTIVE, only
SRC retained).
SYM=* is invalid for SUB|CH=CB — see "Subscription rules" above.
sequenceDiagram
participant E as pm-engine
participant G as pm-md-gwy
participant C as Client
C->>G: SUB|CH=STATE,CB|SYM=AAPL
G-->>C: SNAP|CH=STATE|SYM=AAPL|SEQ=9|SESSION=CONTINUOUS
G-->>C: SNAP|CH=CB|SYM=AAPL|SEQ=2|STATUS=ACTIVE
Note over E: large trade shifts price beyond the L2 threshold
E-->>G: circuit_breaker.halt.AAPL
G-->>C: STATE|CH=STATE|SYM=AAPL|SEQ=10|SESSION=HALTED|PREV=CONTINUOUS
G-->>C: CB|CH=CB|SYM=AAPL|SEQ=3|STATUS=HALTED|LEVEL=L2|TRIGGERPX=148.20|REFPX=150.10|RESUMEAT=...|SRC=CB
Note over E: halt duration elapses engine resumes and re-auctions AAPL
E-->>G: circuit_breaker.resume.AAPL
G-->>C: STATE|CH=STATE|SYM=AAPL|SEQ=11|SESSION=CONTINUOUS|PREV=HALTED
G-->>C: CB|CH=CB|SYM=AAPL|SEQ=4|STATUS=ACTIVE|SRC=CB
E-->>G: auction.result.AAPL
G-->>C: AUCTION|CH=AUCTION|SYM=AAPL|SEQ=1|EQPX=149.80|EQQTY=6200|TRADES=5|IMBSIDE=SELL|IMBQTY=300
HB¶
Direction: Gateway -> Client
Sent when no outbound market-data line was emitted during the heartbeat interval.
| Field | Req | Description |
|---|---|---|
TS |
Yes | Gateway timestamp |
PING / PONG¶
PING is client-initiated liveness check. PONG is immediate reply.
ERR¶
Direction: Gateway -> Client
| Field | Req | Description |
|---|---|---|
CODE |
Yes | Machine-readable error code |
MSG |
No | Human-readable context |
CH |
No | Channel context when relevant |
SYM |
No | Symbol context when relevant |
Normative error codes:
| Code | Meaning |
|---|---|
PROTO_MISMATCH |
HELLO missing CLIENT or PROTO != CALF1 |
AUTH_REQUIRED |
Non-HELLO message sent before successful handshake |
INVALID_CHANNEL |
CH not in {TOP, TRADE, STATE, INDEX, DEPTH, AUCTION, CB} |
INVALID_SYMBOL |
Symbol not in the gateway's known instrument list (does not apply to INDEX, which has no known-id check — see "Subscription rules"), SYM=* used with INDEX/DEPTH/CB on SUB (alone or mixed with any other channel), SYM=* used at all on RESUME (every channel, including TOP/TRADE/STATE/AUCTION), a SUB with no SYM at all, or CH=INDEX combined with an empty symbol |
SUB_LIMIT |
Subscription would exceed max_symbols_per_client |
REPLAY_MISS |
LASTSEQ is older than the replay window; a SNAP follows on TOP/STATE/INDEX/DEPTH/CB, but not on TRADE/AUCTION |
SLOW_CLIENT |
Outbound queue exceeded max_client_queue; connection closed |
BAD_MESSAGE |
Parse failure, oversized line (> 4096 bytes), or unsupported message type |
RATE_LIMITED |
Client exceeded max_messages_per_second (inbound token-bucket); connection stays open |
Terminal behavior:
SLOW_CLIENTis terminal for the current TCP session; gateway disconnects.BAD_MESSAGEmay be terminal when parsing cannot continue safely.RATE_LIMITEDis non-terminal; the offending message is dropped and the connection remains open. The client may retry once its send rate is back undermax_messages_per_second.
EXIT¶
Direction: Client -> Gateway
Requests clean disconnect.
Sequence and recovery semantics¶
Stream identity¶
Sequence numbers are maintained per (CH, SYM) stream.
Examples:
(TOP, AAPL)has its own counter(TRADE, AAPL)has a different counter(STATE, *)and(STATE, AAPL)are distinct counters
Sequence rules¶
- Start value is
1for each stream. - Increment by
1per emitted message in that stream. - Sequence appears in
SNAP,MD,TRADE,STATE,IDX,DEPTH,AUCTION, andCB. - A client-detected gap means one or more missed messages.
First connect behavior¶
On first subscribe to a TOP, STATE, INDEX, DEPTH, or CB stream:
- Gateway sends
SNAPwith current streamSEQ. - Client stores
last_seq[(CH,SYM)] = SNAP.SEQ. - Next incremental event for that stream must be
SEQ + 1.
TRADE and AUCTION have no step 1 — see the SNAP section above.
Reconnect behavior (RESUME)¶
RESUME applies to one stream per message, and may be sent as many times as
there are streams to recover. Reconnect therefore looks like: HELLO, then
one RESUME per stream the client was following, then a SUB for anything
it wants that it has no sequence position for.
- Client supplies
CH,SYM, andLASTSEQ. CHandSYMmust each contain exactly one value.LASTSEQmust be a positive base-10 integer.- If missing events are inside replay window, gateway replays in order then continues live.
- If missing range is outside window, gateway sends
ERR|CODE=REPLAY_MISS. A freshSNAPfollows onTOP/STATE/INDEX/DEPTH/CB. It does not onTRADEorAUCTION: those carry discrete events, and there is no snapshot of a print that already happened, so the missed events are simply gone. Do not wait for a baseline that will not arrive — and if you are talking to an older gateway that sends one anyway, discard it: it is an envelope with no payload, and a decoder keyed onCHalone will read it as a print of zero shares at zero price. - A
SNAPre-baselines the stream; it is never a gap. WhateverSEQit carries becomes your newlast_seqfor that stream, with no gap check. Gap checking aSNAPwould ask to replay history it just superseded, and on the replay-miss path — whose answer is aSNAP— loopsRESUMEagainst a window already known to be too old. - A replay is not disjoint from live delivery.
RESUME|LASTSEQ=nreturns every buffered message withSEQ > n, andnis your position from before the gap — so the reply re-sends the message that revealed the gap, plus anything delivered live while your request was in flight. Replayed and live lines share one ordered connection, so duplicates always arrive after their originals. Discard any message at or below theSEQyou have already recorded, and never let one lower yourlast_seq. Track which sequence ranges you are actually missing: that is the only thing distinguishing the backfill you asked for from a print you already have. - If the stream has no retained replay history at all yet (nothing has been
emitted for that
(CH,SYM)since the gateway started or the buffer last pruned it), the gateway returns zero replay lines and does not sendERR|CODE=REPLAY_MISSor aSNAP— the client resumes live from whatever the next emitted event turns out to be. This differs from the replay-miss case above and is easy to mistake for a silently dropped resume; clients that need a guaranteed baseline afterRESUMEshould also send an explicitSUBfor the same stream, which always triggers aSNAPforTOP/STATE/INDEX/DEPTH/CBregardless of replay state. SYM=*is always invalid forRESUME, for every channel, even forTOP/TRADE/STATE/AUCTIONwhereSYM=*is otherwise allowed onSUB.RESUMEhas no equivalent ofSUB's per-symbol snapshot burst, so a wildcard resume cannot be served a meaningful baseline on a replay miss.RESUME|CH=TOP|SYM=*returnsERR|CODE=INVALID_SYMBOLand, unlike an ineligible wildcard onHELLOpreviously, leaves the session open. Clients must always resume a single concrete symbol and, if they also want an "everything" subscription, add it separately viaSUB|SYM=*after reconnecting.- Beyond the wildcard rule above,
RESUME'sSYMvalue is otherwise not checked against the gateway's known-symbol list the waySUB's is — a resume for a symbol the gateway doesn't currently know about is still accepted and added to the session's subscriptions; it simply won't match any live event until the gateway learns about that symbol.
sequenceDiagram
participant C as Client
participant G as pm-md-gwy
Note over C,G: Last seen on (TOP,AAPL): SEQ=1042, (TRADE,AAPL): SEQ=88
C->>G: HELLO|CLIENT=bot01|PROTO=CALF1
G-->>C: WELCOME|PROTO=CALF1|GW=md-gwy01|HBINT=1|REPLAY=30
C->>G: RESUME|CH=TOP|SYM=AAPL|LASTSEQ=1042
C->>G: RESUME|CH=TRADE|SYM=AAPL|LASTSEQ=88
alt Replay hit
G-->>C: MD|CH=TOP|SYM=AAPL|SEQ=1043|...
G-->>C: MD|CH=TOP|SYM=AAPL|SEQ=1044|...
G-->>C: ...
G-->>C: MD|CH=TOP|SYM=AAPL|SEQ=1050|...
G-->>C: MD|CH=TOP|SYM=AAPL|SEQ=1051|... (live)
else Replay miss
G-->>C: ERR|CODE=REPLAY_MISS|CH=TOP|SYM=AAPL
G-->>C: SNAP|CH=TOP|SYM=AAPL|SEQ=1105|...
end
Liveness and timeout rules¶
- Gateway emits
HBeveryheartbeat_interval_secwhen no outbound market-data line has been sent in that interval. - Client may issue
PINGanytime; gateway must respond withPONG. - If no inbound or outbound traffic occurs for
idle_timeout_sec, gateway closes the connection. HB,PING, andPONGare liveness messages and do not participate in(CH,SYM)sequence counters.
Session lifecycle¶
sequenceDiagram
participant C as CALF Client
participant G as pm-md-gwy
C->>G: TCP connect
C->>G: HELLO|CLIENT=bot01|PROTO=CALF1
G-->>C: WELCOME|PROTO=CALF1|GW=md-gwy01|HBINT=1|REPLAY=30
C->>G: SUB|CH=TOP,TRADE|SYM=AAPL
G-->>C: SNAP|CH=TOP|SYM=AAPL|SEQ=100|...
G-->>C: MD|CH=TOP|SYM=AAPL|SEQ=101|...
G-->>C: TRADE|CH=TRADE|SYM=AAPL|SEQ=44|...
G-->>C: HB|TS=...
C->>G: PING
G-->>C: PONG
C->>G: EXIT
Note over G: Close TCP cleanly
Gateway behavior requirements¶
For CALF 1.0.0 interoperability, pm-md-gwy must:
- Accept TCP clients and enforce HELLO-before-use semantics.
- Normalize internal engine events into CALF lines.
- Maintain independent sequence counters per
(CH, SYM). - Keep bounded replay buffers per
(CH, SYM)stream. - Auto-send
SNAPon newTOP/STATE/INDEX/DEPTH/CBsubscriptions; for a wildcardTOPsubscription, auto-send one real per-symbolSNAPfor every currently known symbol rather than a singleSYM=*snapshot. - Enforce channel and symbol rules deterministically, including which
channels accept
SYM=*(STATE,TOP,TRADE,AUCTIONonly). - Advertise supported channels in
WELCOME|CH_SUPPORTED=. - Disconnect slow clients when queue limits are exceeded.
Configuration reference¶
CALF gateway settings are part of the main engine configuration file
(engine_config.yaml) as a top-level market_data_gateway block.
Path location:
engine_config.yaml->market_data_gateway
All supported CALF 1.0.0 configuration fields are listed below.
| Field | Type / allowed range | Default | Description |
|---|---|---|---|
market_data_gateway.enabled |
Boolean (true/false) |
true (recommended when CALF is used) |
Enables/disables the CALF gateway process configuration. |
market_data_gateway.name |
Non-empty string | Implementation-defined | Gateway instance name advertised in WELCOME|GW=.... |
market_data_gateway.bind_address |
IP/host bind string | 0.0.0.0 (common) |
Local interface address to bind for incoming TCP clients. |
market_data_gateway.port |
Integer, 1..65535 |
5570 |
TCP listen port for CALF clients. |
market_data_gateway.heartbeat_interval_sec |
Integer, > 0 |
1 |
Interval used to emit HB when no outbound market-data line was sent. |
market_data_gateway.idle_timeout_sec |
Integer, > 0 |
5 |
Maximum silent period (no inbound and no outbound traffic) before disconnect. |
market_data_gateway.replay_window_sec |
Integer, > 0 |
30 |
Time-bounded replay retention per (CH,SYM) stream for resume/gap recovery. |
market_data_gateway.max_connections |
Integer, > 0 |
64 |
Maximum concurrent TCP client connections accepted by the gateway. |
market_data_gateway.max_messages_per_second |
Integer, > 0 |
200 |
Per-client inbound token-bucket rate limit; excess messages receive ERR|CODE=RATE_LIMITED and are dropped without disconnecting the client. |
market_data_gateway.max_symbols_per_client |
Integer, > 0 |
200 |
Per-client subscription symbol limit across active subscriptions. A wildcard subscription counts as one entry. |
market_data_gateway.max_client_queue |
Integer, > 0 |
10000 |
Per-client outbound queue cap; overflow triggers ERR|CODE=SLOW_CLIENT and disconnect. |
market_data_gateway.depth_levels |
Integer, > 0 |
10 |
Number of price levels per side included in DEPTH/SNAP(CH=DEPTH) messages. There is no separate enable/disable flag — DEPTH is on by default in CALF 1.0.0; this only tunes ladder depth. |
Operational notes:
HBINTinWELCOMEmust reflectheartbeat_interval_sec.REPLAYinWELCOMEmust reflectreplay_window_sec.max_connectionsbounds concurrent TCP clients; connections beyond this limit are rejected at accept time.max_messages_per_secondaffects inboundERR|CODE=RATE_LIMITEDbehavior; it is a non-terminal, per-client token bucket, unlikemax_client_queue.max_symbols_per_clientaffectsSUBvalidation andERR|CODE=SUB_LIMIT.max_client_queuecontrols slow-client backpressure behavior.depth_levelsaffectsDEPTHmessage size and bandwidth; lower it on bandwidth-constrained deployments rather than expecting clients to request fewer levels — there is no per-clientLEVELS=override in CALF1.0.0.
Example:
market_data_gateway:
enabled: true
name: "md-gwy01"
bind_address: "0.0.0.0"
port: 5570
heartbeat_interval_sec: 1
idle_timeout_sec: 5
replay_window_sec: 30
max_connections: 64
max_messages_per_second: 200
max_symbols_per_client: 200
max_client_queue: 10000
depth_levels: 10
What to watch out for during implementation¶
- Implement line buffering correctly for TCP streams (
recv()may return partial lines or multiple lines at once). - Enforce HELLO-before-use strictly; all non-HELLO pre-auth messages must
receive
ERR|CODE=AUTH_REQUIRED. - Keep
SNAPsemantics explicit: it is a message type, not a subscribable channel;TOP,STATE,INDEX,DEPTH, andCBsubscriptions auto-triggerSNAP—TRADEandAUCTIONnever do. - For a wildcard
TOPsubscription, do not call the per-symbol snapshot builder with a literalSYM="*"— it has no meaningful per-symbol state and will silently produce an empty snapshot. Iterate known symbols and send one realSNAPeach. - Enforce
SYM=*constraints exactly (STATE,TOP,TRADE,AUCTIONonly — neverINDEX,DEPTH, orCB, and never when mixed with any of those three in the sameSUB) and validate multi-valueSUBas Cartesian stream requests. DEPTHis a full-ladder replace per message, not a per-level diff likeMD. Do not attempt incremental per-level patching on either the gateway or client side.CBandSTATEare emitted from the same underlyingcircuit_breaker.halt.*/circuit_breaker.resume.*engine event — do not letCB's richer detail leak intoSTATE's field set, and do not let aCBnormaliser failure suppress theSTATEemission (or vice versa); both should be independent_emit_stream_eventcalls from the same handler.- Carry the halt's cause on a single wire key,
SRC, inCB— do not propagate the internal inconsistency to clients. - Track sequence numbers independently per
(CH,SYM)stream; never use a single global counter. - Treat
RESUMEas single-stream only and validateCH,SYM, andLASTSEQstrictly. - Bound replay by configured window and emit deterministic
REPLAY_MISSbehavior when outside window — with a freshSNAPonly on the channels that have one (TOP/STATE/INDEX/DEPTH/CB), never onTRADE/AUCTION. - Apply slow-client backpressure deterministically: queue overflow must produce
SLOW_CLIENTand disconnect. - Keep liveness signals (
HB,PING,PONG) outside market-data sequencing; they do not consume(CH,SYM)sequence numbers.
Conformance notes¶
If you are implementing a CALF client, the most important protocol truths are:
- CALF is line-based text over TCP, not message-framed datagrams.
HELLOis mandatory before any subscription command.SNAPis a message type, not a subscribable channel.- Sequence tracking is per
(CH, SYM)stream. SYM=*is valid forSTATE,TOP,TRADE, andAUCTIONsubscriptions — never forINDEX,DEPTH, orCB.- A wildcard
TOPsubscription never yields aSNAPwith a literalSYM=*; it yields one realSNAPper known symbol. - Replay resume is single-stream per
RESUME; send one per stream. - On replay miss, client must accept fresh
SNAPand reset local baseline. DEPTHmessages replace a side's entire tracked ladder, never a single price level in isolation.WELCOME|CH_SUPPORTED=, notPROTO, is how a client detects whether a gateway build supportsDEPTH,INDEX,AUCTION,CB, or theSYM=*wildcard extension —PROTO=CALF1does not change across CALF1.0.0.- Heartbeats and ping/pong are separate liveness mechanisms.
- A
SLOW_CLIENTerror indicates disconnect and reconnect is required. - Protocol values and keys are uppercase by convention and should be emitted uppercase for interoperability.
CBis always emitted alongsideSTATEfor the same halt/resume engine event, never instead of it — a client that only wants the coarse transition can ignoreCBentirely and keep usingSTATEexactly as before this extension.AUCTIONfires exactly once per uncross, including when there was no crossable interest — absence ofEQPX/IMBSIDEsignals "no cross" or "balanced," not a suppressed/missing event.
See also¶
- Market Data Feed (CALF) — operational guide and client examples
- CALF Protocol Spy (pm-calf-spy) — read-only CLI for inspecting the live wire format
- Processes — where
pm-md-gwysits in the process model - Engine Config Specification —
market_data_gatewayfield law - External Protocols Overview — ALF/BALF/CALF/RALF at a glance
- Risk Controls — circuit-breaker engine behavior behind the
CBchannel - Market Index — auction uncross mechanics behind the
AUCTIONchannel