# Kronos X FIX 4.4 Integration Manual for Claude Code

Drop this file into the root of your integration repository and point Claude
Code at it. It is self-contained: everything needed to generate a correct FIX
4.4 client for the Kronos X MTF, in any language, including the venue's
deliberate deviations from a textbook FIX engine.

Suggested first prompt:

> Read KronosX-Claude-Code-FIX.md and generate a Kronos X FIX 4.4 client in
> <language>: tag=value codec with checksum framing, session layer with logon
> and heartbeats, NewOrderSingle/Cancel/Replace, an ExecutionReport-driven order
> state machine, and market data. Then write a conformance test against the
> sandbox using credentials from environment variables.

Companion manuals: `KronosX-Claude-Code-REST.md` and
`KronosX-Claude-Code-WebSocket.md`. **You need REST alongside FIX**: recovery
after a disconnect is a REST call, by design (section 4).

---

## 1. Connection

| Environment | Host | Port |
|---|---|---|
| Sandbox | `sandbox.mtf.perpetuals.net` | 9879 |
| Production | confirmed at onboarding | confirmed at onboarding |

- **The clustered sandbox.** The sandbox host is `sandbox.mtf.perpetuals.net`
  (FIX rules of engagement revision 2.30, member API revision 3.83):
  three nodes behind that one entry host, replacing the single-node
  `sandbox.mtf.perpetuals.com` from the date venue operations announce. Node
  addresses are not published and are not member endpoints: connect to the
  configured host and port only, never pin a node. A leader change therefore
  happens on the sandbox as it can on production (section 3).
- **FIX 4.4 only.** `BeginString` must be `FIX.4.4`; anything else is refused.
- **TLS is mandatory** (the venue's publicly trusted certificate, TLS 1.3). Wrap
  the socket before writing a single byte. There is no plaintext port.
- Venue `SenderCompID(49)` is `KRONOSX`. Your CompID is whatever you send as
  `SenderCompID(49)`; the venue echoes it as your `TargetCompID(56)`.
- Credentials are the same API key and secret as the other transports, sent on
  Logon. A `READ_ONLY` (drop copy) credential logs on successfully but cannot
  trade (section 8).

## 2. Wire format

Standard tag=value with SOH (`0x01`) separators:

```
8=FIX.4.4|9=<bodyLength>|35=<msgType>|49=<sender>|56=<target>|34=<seqNum>|52=<sendingTime>|...|10=<checksum>|
```

- `BodyLength(9)` counts the bytes AFTER the `9=...<SOH>` field up to and
  including the SOH before `10=`.
- `CheckSum(10)` is the sum of all bytes before it, modulo 256, as three digits
  with leading zeros.
- `SendingTime(52)` format: `yyyyMMdd-HH:mm:ss.fff` in UTC.
- The venue validates both. **Malformed framing closes the connection**: FIX
  cannot resynchronise mid-stream, so there is no recovery from a bad checksum.

Your codec must handle partial reads: TCP will hand you half a message. Buffer
until `BodyLength` plus the 7-byte trailer is satisfied.

## 3. Session layer

### Logon (35=A)

Required fields:

| Tag | Value |
|---|---|
| `EncryptMethod(98)` | `0` (transport TLS only) |
| `HeartBtInt(108)` | 5 to 60 seconds |
| `ResetSeqNumFlag(141)` | `Y` (**required on EVERY logon**) |
| `Username(553)` | your API key |
| `Password(554)` | your API secret |
| `MsgSeqNum(34)` | `1` |
| `DropCopyOnly(9003)` | optional `Y`, see section 8 |
| `9004` | optional `Y`: venue tag, arm cancel-on-disconnect for this session (trading sessions only; forced `N` on drop-copy), see below |
| `9005` | grace window in ms for `9004`, clamped 0 to 30000; absent means 0 |

The venue replies with a Logon echoing `98`, `108`, `141=Y`, `9003` and `9004`
(the state actually armed, so a drop-copy session that sent `9004=Y` reads `N`
back). Authentication failure gives a Logout naming code 1001, then a close.

Log on promptly: a connection that has not completed a valid Logon within 10
seconds of connecting is closed (best effort Logout with `Text(58)` "logon
timeout"; inbound bytes do not extend the deadline), and the TLS handshake
itself must complete within 5 seconds. The venue also caps concurrent FIX
connections (512 total, 64 not yet logged on, 8 not yet logged on per remote
address, all enforced at accept), so bound your reconnect storms.

### Cancel on disconnect (tags 9004/9005)

Armed PER SESSION, in memory only: re-arm on every logon. When an armed session
disconnects, the venue waits the `9005` grace window and then, if the member
has no live FIX TRADING session left, submits ONE member-scope cancel-all:
every open order of the member, every instrument, untriggered conditionals
included, regardless of the transport that placed them (orders belong to
members, not connections). The cancels arrive as ordinary `150=4` reports with
`Text(58)=USER_REQUESTED`, the member's own standing instruction, and no
`ExecRestatementReason(378)`: by their tags they cannot be told apart from a
member-wide cancel-all of your own or from the member portal's kill switch,
which report `USER_REQUESTED` too (`ADMIN_CANCEL` is reserved for cancels by
venue operations). What tells the protection apart: it follows an armed
session's disconnect and the grace window, it is ONE command (every report
carries the same engine sequence prefix in `ExecID(17)`), it covers every open
order of the member and it answers no request; the value stays
`USER_REQUESTED` by design (FIX rules of engagement revision 2.30). A FIX
TRADING re-logon inside the grace window disarms the pending timer; timers are
per transport, so only a FIX Logon clears a FIX timer (a WebSocket reconnect
does not, and vice versa; the WebSocket arms its own via the
`session.cancelOnDisconnect` op). A drop-copy session counts for NEITHER side:
it cannot arm, its presence does not satisfy the liveness probe, and a
drop-copy logon does not disarm a pending timer, so an always-connected
drop-copy feed can never silently void the protection a trading session
armed. The venue re-checks liveness immediately before submitting, so a
trading logon at the last instant still stops the cancel; a second armed
session dropping restarts the window best effort only (the restart can race a
timer already at expiry). A venue restart voids all in-memory arming
uniformly: nothing fires during or after it and every re-logon starts
unarmed. On a clustered deployment a leader change does the same (member API
revision 3.77, FIX rules of engagement 2.25): a session logged out for a
leader change, or closing on a node that is not the leader, arms nothing, and
resting orders survive on the new leader. On a drop-copy session `9004` is ignored and
echoed `N`: a read-only credential must not be able to trigger cancels.
ORDERS ONLY: the venue never closes or reduces a position on a disconnect, so
flattening after a drop remains your own reconnect logic.

### Heartbeats

- Send `Heartbeat(0)` per your `HeartBtInt`.
- `TestRequest(1)` from either side must be answered with a `Heartbeat(0)`
  echoing `TestReqID(112)`. Always send `112`: the venue cannot answer a
  TestRequest without it, so one that omits it gets `Reject(35=3)` with
  `373=1`, `371=112`, `372=1`, and no Heartbeat.
- The venue sends a TestRequest after 1.2x silence and, after 2x, sends a
  `Logout(35=5)` with `Text(58)` "heartbeat timeout" and disconnects.

### Sequence numbers

**The venue keeps no FIX message store.** This is deliberate:

- Sequence numbers reset on every logon (hence mandatory `141=Y`).
- An inbound gap ends the session: the venue sends Logout naming the backfill
  route and closes. Re-logon with `141=Y`.
- An outbound `ResendRequest(2)` is answered with a GapFill `SequenceReset(4)`,
  not with replayed messages. That reply takes the next number in the venue's
  outbound sequence and points `NewSeqNo(36)` at the one after it, so your own
  gap detection on `MsgSeqNum(34)` keeps working across it.
- `PossDupFlag(43)=Y` on an already-processed sequence is silently dropped.
- A venue restart ends every session deliberately: `Logout(35=5)` with
  `Text(58)="venue restart"`, then the TCP close. Normally it is the venue
  stopping for a rollout or a restart; the venue also sends it, while it keeps
  running, to every session that may have missed an ExecutionReport when a
  venue-side fault lost the reports of one command or batch (the command itself
  executed), and no other Logout text marks that case. Either way: reconnect
  with backoff, `Logon` with `141=Y`, re-arm `9004` and resubscribe to market
  data, then run the section 4 recovery, whose REST backfill of the `order` and
  `execution` families brings your order state back in step. Nothing survives
  the restart on the session (the WebSocket twin is close `1001` `venue restart`).
- On a clustered deployment a leader change ends sessions the same way with
  `Text(58)="leader change, reconnect"`: on every leader change, also on a node
  that regains the leadership at once, and, since member API revision 3.77, on
  the first order, cancel, replace or market data request that reaches a node
  which is no longer the leader (reconcile by `ClOrdID(11)` on the leader
  before you resubmit). Same handling as a
  restart: reconnect toward the leader, `Logon` with `141=Y`, re-arm,
  resubscribe, run the section 4 recovery. The clustered sandbox of FIX rules
  of engagement revision 2.30 (`sandbox.mtf.perpetuals.net`, from the date
  venue operations announce) sends this Logout at every leader change, the hand-over venue operations stage at an agreed time for
  Conformance Test Plan case I-05 included. Reconnect with backoff to the host
  and port you configured (never a node address); a reconnect just after the
  change can still fail, or be logged out with the same text: reconnect again
  after a short delay. An order whose outcome you did not see before the Logout
  may still execute (the former leader answers an order it had already
  appended with the Logout, not with a report): reconcile by `ClOrdID(11)` on
  the leader and never send it again under a new `ClOrdID(11)`. A cancel or a
  replace whose `150=4` or `150=5` you did not see did not necessarily take
  effect: send it again on the new session at once, so a resting order you
  meant to pull does not stay on the book. A leader change itself pulls no
  resting order.
- On a clustered deployment an order, cancel or replace whose commit the
  venue does not confirm within 10 seconds ends the session WITHOUT a Logout:
  the TCP connection closes and no report answers that message (FIX rules of
  engagement 2.30, section 1, leader change item 6, which states it; nothing
  changed). Unlike the leader-change Logout, that close counts as a disconnect
  for an armed cancel on disconnect, which cancels every open order of your
  member after its grace window unless a FIX trading Logon disarms it first:
  log on again at once, then reconcile the message by `ClOrdID(11)` on the
  leader, because it may still execute. REST and the WebSocket answer the same
  fault `5000`.

Do not build a resend/persistence layer. Recovery is section 4.

## 4. Recovery (the part that differs from other venues)

After ANY disconnect, recover over REST, not over FIX:

1. Reconnect, Logon with `141=Y`, resubscribe to market data if used.
2. **Buffer inbound ExecutionReports; do not process them yet.**
3. Take your resume point (below; on a first start with nothing persisted,
   `GET /v1/stream/cursor`).
4. Page `GET /v1/stream/events?afterCursor=<resume point>&channels=order,execution`
   (or with no `channels` filter) until `hasMore` is false and `store.status`
   is `COMPLETE`.
5. Deduplicate against buffered live reports using `ExecID(17)`, which is
   `{engineSeq}-{eventIndex}` and directly comparable with the REST cursor
   space.
6. Drain the buffer, discard anything at or below your cursor, resume live.

Your resume point is the ordinal maximum of the `nextCursor` of your last
backfill page and the cursor of the last event you processed; stop paging on
`hasMore` false or `count` 0, never on `nextCursor` equal to the `afterCursor`
you sent. A FIX recovery backfills the `order` and `execution` families
together, or with no `channels` filter at all: since member API revision 3.73
the `nextCursor` of a last page is a position at the store watermark, valid only
with the same filter, so discard buffered reports by your backfill cursor only
for the families that backfill read.

A backfill page can answer HTTP 429 with `1003` (more reads of yours in flight
on the node than it serves at once, or the node's shared budget of these reads
full) or HTTP 503 with `5007` (the venue's history store did not answer in
time, is held after a slow read, or is catching up):
sleep `data.retryAfterMs`, retry the same page with the same cursor, never
advance your cursor on a refusal, and narrow the read (a smaller `limit`) after
repeated `5007`.

This is stronger than FIX resend: the durable event stream is complete, totally
ordered, and shared across REST, WebSocket and FIX, so a member using several
transports reconciles them by a single key.

## 5. Order entry (35=D, NewOrderSingle)

| Tag | Required | Meaning |
|---|---|---|
| `ClOrdID(11)` | optional | Your reference, <= 36 chars, unique per member per 24 h, idempotency key. The `KX-` prefix is RESERVED (code 2020). NOT enforced, matching the native API: an order without one is accepted, has no duplicate protection, cannot be canceled or replaced by `OrigClOrdID(41)`, and its ExecutionReports OMIT tag 11 entirely. Send one anyway. |
| `Account(1)` | default on absence | The end-client reporting identifier (`clientUid`). On a broker member, missing gives 2010; a market-maker member may omit it and the configured house identifier is applied. Since Member API 3.72 (in force from the date venue operations announce) a `ClOrdID(11)`, an `Account(1)` or a Trader ID tag (`9012`, `9024`, `9025`) that begins or ends with whitespace is the business reject 2013 (`150=8`, `39=8`, the rule named in `Text(58)`), exactly as on REST and WebSocket, and so is a padded `TargetOpenClOrdID(9028)`, with its own `Text(58)`. A cancel naming an order by `OrigClOrdID(41)` is a lookup and is NOT refused: the reference is compared character for character as before, so an order you placed under a padded `ClOrdID(11)` before that date stays cancellable under exactly that reference, and by `OrderID(37)`; any other padded `OrigClOrdID(41)` answers OrderCancelReject (35=9). A replace (35=G) addresses its order by `OrderID(37)` and carries no reference. |
| `Symbol(55)` | named on absence | Venue symbol, for example `BTC-USD-PERP`. PRESENT but unknown is still 2001. |
| `Side(54)` | named on absence | `1`=Buy, `2`=Sell. PRESENT but unsupported is still 2013. |
| `OrderQty(38)` | named on absence | Quantity in decimal base-asset units, as REST `qty`. Must align to `qtyStep`; sub-`minNotional` orders reject 2028 in tag 9010. |
| `OrdType(40)` | named on absence | `1`=Market, `2`=Limit, `3`=StopMarket, `4`=StopLimit. PRESENT but unsupported is still 2004. |
| `Price(44)` | conditional | Limit and StopLimit. Must align to `tickSize`. |
| `StopPx(99)` | conditional | Trigger price for stop types. Absent on a stop type is 2013. |
| `TimeInForce(59)` | default on absence | `1`=GTC, `3`=IOC, `4`=FOK, `6`=GTD. ABSENT means GTC. `59=0` (the FIX DAY default) is REJECTED with 2013: the venue does not offer DAY, and it never substitutes a value for one you actually sent. |
| `ExpireTime(126)` | conditional | GTD only. UTC, `yyyyMMdd-HH:mm:ss` or `yyyyMMdd-HH:mm:ss.fff`. |
| `ExecInst(18)` contains `6` | optional | Post-only (Limit only). |
| `9001` | optional | Venue tag: `Y` = reduce-only (perps only). |
| `9002` | optional | Venue tag: trigger source `LAST` (default), `MARK`, `INDEX`. |
| `9012` | conditional | Venue tag: Trader ID of the Authorised User entering the order (the native API's `reg.traderId`). Optional until your member registers Authorised Users, then REQUIRED on every order (code 2021). See the staged-enforcement note below. |
| `9016` | optional | Venue tag: hedge-mode position side, `LONG` or `SHORT` (the native API's `positionSide`). Routes the order's fills to that sub-position bucket of the `Account(1)` end client; ABSENT means netting. Order-book perpetual markets only: 2016 on spot, 2013 on any other value. Echoed on the order's ExecutionReports, omitted on netting orders. |
| `9021` | optional | Venue tag: `reg.tradingCapacity`, `DEAL`, `MTCH` or `AOTC`. Recorded verbatim into the same per-order regulatory record as the REST/WS `reg` object; see the reg-tags note below. |
| `9022` | optional | Venue tag: `reg.clientIdType`, `LEI`, `NATIONAL_ID` or `INTERNAL`. |
| `9023` | optional | Venue tag: `reg.clientId`. |
| `9024` | optional | Venue tag: `reg.investmentDecisionId`. |
| `9025` | optional | Venue tag: `reg.executionDecisionId`. |
| `9026` | optional | Venue tag: `reg.dea`. `Y` means true, anything else reads as `N`, the 9001/9004 convention. |
| `9027` | optional | Venue tag: `reg.liquidityProvision`. `Y` means true, same convention as `9026`. |
| `9029` | optional | Venue tag: `reg.riskReducing` (Member API 3.70, RTS 22 field 64, commodity-derivative markets only). `Y` means true, anything else reads as false, absent stays unset (= false on the report). NewOrderSingle only. |

**Required fields on 35=D, and how they fail.** Two different answers, and your
client has to handle both:

- `55`, `54`, `38` or `40` ABSENT or empty: a session-level `Reject(35=3)` with
  `SessionRejectReason(373)=1`, `RefTagID(371)` = the tag, `RefMsgType(372)=D`
  and a `Text(58)` naming it. Decided before the order is built, so no
  ExecutionReport follows and the session stays up. It carries no `ClOrdID(11)`
  and no `ExecID(17)`: correlate it by `RefSeqNum(45)`.
- Any of those PRESENT with a bad value: the unchanged business reject, an
  `ExecutionReport 150=8` with the venue code in `9010`. The ONE exception is a
  `Side(54)` outside the FIX 4.4 enumeration (`1` to `9`, `A` to `G`), which is
  the only echoed tag whose value could make the venue's own `35=8` fail a client
  data dictionary: `54=X` answers `Reject(35=3)` with `373=5` and `371=54`, while
  `54=5` (in the enumeration, not traded here) keeps its unchanged 2013.
- A session-layer `Reject(35=3)` means the venue never accepted the message for
  processing, so it costs no order-budget token and is not counted in your RTS 9
  order-to-trade ratio. Anything answered with a business reject is charged and
  counted as usual.

The venue NEVER emits a zero-length value for any tag: a tag it cannot fill is
omitted, or carries a documented sentinel where FIX requires it (`KX-NONE` on an
`OrderCancelReject`, section 7), or the message is refused by name. That holds on
the standard header too: an absent or empty `SenderCompID(49)`, and anything the
venue must answer before your Logon arrives, comes back as
`TargetCompID(56)=CLIENT` rather than an empty `56=`. It also holds for a tag you
sent EMPTY, which the venue reads everywhere as one you did not send.

Semantics identical to the native API:

- Market orders execute inside a protection collar; margin is checked at the
  collar price. An empty book gives 2007.
- **MARKET + FOK is all-or-none** inside the collar: either a full fill set or a
  single Canceled with `Text(58)=FOK_UNFILLABLE`. Never a partial.
- Stop direction is derived from side (buy stops at-or-above, sell stops
  at-or-below).
- Prefer `9002=MARK` for stop-losses: `LAST` can be moved by one small print.
- There are **no iceberg and no trailing-stop orders**. There is no
  `MaxFloor(111)` support; a trailing parameter is rejected.

### Reusing a ClOrdID

`ClOrdID(11)` is the venue's idempotency key, unique per member for 24 hours.
Reusing one while the venue still holds it never creates a second order, and the
venue always answers the NewOrderSingle on the session that sent it:

- **Different payload:** `150=8`, `39=8`, `9010=2009`,
  `Text(58)=duplicate clOrdId with different payload`, `37=0`. This is the same
  registry rejection REST returns as HTTP 400 and the WebSocket as a
  `success: false` `order.place` ack.
- **Byte-identical payload:** the idempotency path. Nothing new is created and
  the ORIGINAL order is restated as `150=I` (order status) with venue tag
  `9015=Y`, that order's `OrderID(37)`, `OrdStatus(39)`, `LeavesQty(151)` and
  `CumQty(14)`. It is the FIX shape of the REST/WebSocket `duplicate: true`
  response.

The venue holds a reference for 24 h, or until it releases it to stay inside its
retention bound of the most recent 4,000,000 references venue-wide, whichever comes
first. A released reference is unknown, so a reuse is accepted as a NEW order
(`150=0`) rather than answered with either shape above. A reference whose order is
still working or pending trigger is never released, so `OrigClOrdID(41)` on a cancel
or replace resolves for the whole life of the order.

Neither answer is a lifecycle event: like a pre-engine rejection they are absent
from the durable event stream and from `GET /v1/stream/events`, and their
`ExecID(17)` is a gateway-local value outside the cursor space. Correlate both
by `ClOrdID(11)`. **Book neither as a new order**, and generate unique
references so you only ever meet these deliberately.

### Code 5005 and ExecType(150)=I

`5005 COMMAND_FAILED` means the venue accepted your command, made it durable,
and then could not finish processing it. Whatever the command achieved before it
stopped is KEPT, so an order it created is REAL. You receive exactly one further
report on the session that sent the order, and which one is decided by what the
command DID, in this order:

- **The engine rejected the order**, before accepting it OR after (a post-only
  order that would cross, and the `REJECT_TAKER` self-trade policy, are accepted
  and then rejected inside one command): its own rejection is the answer, with
  the real reject code in `9010`, and nothing further follows.
- **The reference was already taken by a different payload**: the ordinary 2009
  rejection above. Nothing was created and 2009 is the reason you need.
- **An order exists** (this command created it, or the byte-identical retry maps
  to one): the ordinary fan-out reports for it, then one `150=I` restatement
  carrying that order's real `OrderID(37)`, `OrdStatus(39)`, `LeavesQty(151)` and
  `CumQty(14)`, plus `9010=5005` and a reason in `Text(58)`. Tag `9015` means "a
  reused reference" and nothing else, so it is absent here unless the reference
  really was reused, in which case both tags ride the one report.
- **Nothing was created or rejected**: a single `150=8`, `39=8`, `9010=5005`.

On a cancel (`35=F`) or a replace (`35=G`), 5005 is not a verdict either. An
`OrderCancelReject(9)` means the request was REFUSED, so you get one carrying
`9010=5005` only when the cancel or replace did not take effect. When it did take
effect and the command failed afterwards, the answer is the ordinary `150=4` or
`150=5` and nothing else.

`150=I` is a RESTATEMENT, never a lifecycle transition, on both this path and the
reused-reference path above: it repeats the order's state at the moment it was
built and closes nothing. It therefore CAN carry a terminal `OrdStatus(39)`,
after the lifecycle report that really closed the order, whenever the order it
restates is already terminal (a canceled order behind a reused reference, an
`IOC` remainder canceled inside the same command). Exactly one report with a
lifecycle `ExecType(150)`, that is `150` other than `I`, ever carries a terminal
`39` for a given order. So assert that, not "one terminal `39` per order", or
your conformance run will flag the venue for behaviour it publishes. A client
that books `150=I` as terminal, or that treats any report carrying `9010` as "I
have no order", will abandon a live position or double-send. Do not resubmit an
identical request after a 5005: the venue is deterministic, so it fails
identically. Reconcile against `GET /v1/orders/open`.

### Trader ID (tag 9012) and staged enforcement

Tag `9012` carries the Trader ID of the Authorised User entering the order.
Trader IDs are issued by the venue when your member registers Authorised Users;
the default form is `TR-{memberId}-{8 hex}`, but treat the value as opaque, up
to 64 characters.

Enforcement is **staged per member**:

- A member with NO registered Authorised Users is not gated: tag 9012 is
  optional and orders flow exactly as before this release.
- From the FIRST registered user onward, EVERY NewOrderSingle must carry a
  registered, ACTIVE Trader ID in 9012, or it is rejected before reaching the
  engine: `150=8`, `37=0`, `9010=2021`,
  `Text(58)=reg.traderId missing or not a registered Authorised User`.

The check is one shared code path across REST (`reg.traderId` in the `reg`
object), WebSocket and FIX, so the gating state you observe on one transport is
exactly what the others enforce. Wire 9012 into your order mapper now even if
your member is not yet gated: the moment the first Authorised User is
registered, enforcement flips on for every subsequent order.

**The remaining reg fields ride tags 9021 to 9027 and 9029** (tradingCapacity,
clientIdType, clientId, investmentDecisionId, executionDecisionId, dea,
liquidityProvision, riskReducing). An absent tag leaves the field unset, exactly as omitting
the key in the REST `reg` object; values are recorded verbatim and validation
is shared with REST and WebSocket, including the 512 byte ceiling on the total
reg record (code 2013), so the journaled regulatory record for the same order
is byte-identical across all three transports. `35=G` (cancel/replace) accepts
none of these tags: the regulatory record is set at order entry and immutable.

### RFQ markets have NO FIX surface

`GET /v1/instruments` now carries `"mode"`: `ORDER_BOOK` or `RFQ` (and `kind`
gained `BARRIER`, knock-out contracts that exist only on RFQ markets).
**RFQ-mode markets, including all BARRIER markets, cannot be traded over FIX at
all.** They have no order book: a NewOrderSingle for one is rejected with
`150=8` and `9010=2022` (`Text(58)=RFQ market: no order book, trade via POST
/v1/rfq`). RFQ trading (create, quote, accept) is offered over REST and
WebSocket only; see the companion manuals. Read `mode` from reference data and
route accordingly: FIX order entry is for `ORDER_BOOK` markets only.

## 6. Cancel (35=F) and Cancel/Replace (35=G)

- Cancel by `OrderID(37)` or by `OrigClOrdID(41)`.
- Replace supports `Price(44)` and/or `OrderQty(38)`. Quantity-down keeps queue
  priority; a price change loses it.
- Failures answer `OrderCancelReject(9)`:
  `CxlRejReason(102)=1` unknown order, else `0` with the venue reason in
  `Text(58)`; `CxlRejResponseTo(434)` is `1` for cancel, `2` for replace.
- Success is NOT reported on the reject message: it arrives as an
  ExecutionReport with `ExecType(150)=4` (or `5` for replace).
- An `OrderCancelReject(9)` always means REFUSED and the order unchanged, code
  5005 included: you get one carrying `9010=5005` only when the cancel or replace
  did not take effect (section 5). One exception since FIX rules of engagement
  revision 2.31 (member API revision 3.86, from the matching engine's
  replay-contract line for that release): a `35=G` or a `35=F` for a GTD order at
  or past its `ExpireTime(126)` that the venue's expiry sweep has not yet reached
  is refused as the unknown order, `434=2` (replace) or `434=1` (cancel), `102=1`
  and `9010=2014`, and the same command expires the order, which arrives as its own
  ExecutionReport (`150=C`, `39=C`); once the sweep has reached the order the same
  request answers `102=1` again (a `35=G` then without `9010`). Either way the
  order has ended and nothing traded: drive the order to EXPIRED on the `150=C`,
  never on the reject.
- **References on the reject.** FIX 4.4 requires `OrderID(37)`, `ClOrdID(11)` and
  `OrigClOrdID(41)` on a `35=9`, so a reference you did not send is reported as
  the sentinel `KX-NONE`, never as a zero-length value; `37` keeps its `0`
  sentinel. `KX-` is venue reserved and refused with 2020 on order entry, so
  `KX-NONE` cannot collide with a real reference: do not match it against your
  order table. A `35=F` with NEITHER `37` nor `41` keeps `102=1` and its
  `Text(58)` names the two tags. All three treat a tag you sent EMPTY as one you
  did not send, so a client that always writes `37`, `41` and `11` and blanks the
  ones it lacks gets `37=0`, `11=KX-NONE`, `41=KX-NONE`, all three present.

## 7. ExecutionReports (35=8)

Your session is a **full-member drop copy**: reports cover all of your member's
order activity regardless of the transport that originated it (REST, WebSocket,
another FIX session), including venue-initiated closes.

| Tag | Content |
|---|---|
| `ExecType(150)` / `OrdStatus(39)` | `0`=New, `F`=Trade (with `39`=`1` while the order still has quantity open AFTER this fill, `2` on the fill that completes it), `4`=Canceled, `C`=Expired, `5`=Replaced, `8`=Rejected, `L`=Triggered (with `39`=`0`: a stop or take-profit order activated; carries `37`, `11`, `55`, `54`, `151`, `14`=`0` and the trigger price in `StopPx(99)`, never `Account(1)` or `9016`; a lifecycle transition from pending trigger to working, never a fill), `I`=order status (restatement of an order a reused `ClOrdID` maps to, section 5). |
| `OrderID(37)` | Venue order id. `0` on a pre-book rejection. |
| `ClOrdID(11)` | Your reference. On venue-initiated closes: `KX-LQ-{orderId}` or `KX-ADL-{tradeId}`. ABSENT, never empty, on every report for an order you placed without a reference. |
| `Account(1)` | The `clientUid`, on every report that has one. Omitted rather than sent empty where the venue has none: the 2010 rejection saying it was missing, and the `150=L` activation report (read it from the order's accept). |
| `ExecID(17)` | `{engineSeq}-{eventIndex}`: globally unique, totally ordered, comparable with the REST/WebSocket cursor. Use it as your dedupe key. |
| `LastQty(32)`, `LastPx(31)` | This fill only (trades). |
| `LeavesQty(151)`, `CumQty(14)` | The order's open quantity and its cumulative filled quantity after the event the report describes: on a fill, after THAT fill (this fill included, no later one); `151=0` on a cancel, expiry or reject, with `14` still the order's filled quantity (a post-only order partly filled and then repriced by a `35=G` to cross is rejected with its fills: `150=5` then `150=8`, both with that `14`). Every fill of the order counts, whichever transport placed it and whenever your session logged on. A fill with `OrderID(37)` `0` (RFQ, barrier, yield take, ADL, reversal) is complete in itself: `39=2`, `151=0`, `14` = `LastQty(32)`. Since rules of engagement revision 2.29 (Member API 3.82); before it a fill carried the order's state after the whole command (a partly filled IOC said `39=2`, `151=0`, then its remainder cancel `14=0`; a reject always said `14=0`) and `14` counted only the fills your session had seen. |
| `Text(58)` | Cancel reason enum, or the reject message. |
| `OrdRejReason(103)` | `99` (other). The precise code is in `9010`. |
| `9010` | The venue's numeric code (full registry: `KronosX-API-Codes`). |
| `9011` | `MEMBER` or `VENUE`. **Key unsolicited-close handling on this**, never on the shape of `ClOrdID`. |
| `9015` | `Y` on a `150=I` restatement answering a reused `ClOrdID` with an identical payload (section 5). Absent on every other report, including the `150=I` that answers a 5005 on a reference you did not reuse. |
| `9017` | Queue position at rest, quantity ahead (decimal base units, FIFO priority at the order's price level). ONLY on the accept report (`150=0`) of a placement that RESTED the order; `0` means front of the level, so presence is the signal that the order rested. Omitted on full fills, IOC/FOK/market remainders, pending-trigger accepts and rejects. A snapshot at rest time, immediately stale: the venue never updates it. |
| `9018` | Queue position at rest, orders ahead (integer). Beside `9017`, same presence rule: both tags appear together or not at all. |

`cancelReason` values in `Text(58)`: `USER_REQUESTED`, `IOC_REMAINDER`,
`FOK_UNFILLABLE`, `GTD_EXPIRED`, `STP_CANCEL`, `LIQUIDATION_CANCEL`,
`ADMIN_CANCEL`, `INSTRUMENT_HALT`, `MEMBER_SUSPENDED`,
`REDUCE_ONLY_VIOLATION`, `SPOT_UNFUNDED`, `POSITION_SIZE_LIMIT`, `MARKET_ACCESS`
(revision 3.65, venue-initiated: the venue cancelled the order on a `DENIED`
market access decision or an admission flip), `FUNDING_CATCH_UP` (revision 3.70,
venue-initiated: a conditional order that triggered while the market's GRID
funding catch-up was pending; reduce-only conditionals execute),
`POST_ONLY_WOULD_CROSS` (revision 3.70, venue-initiated, drop copy only since FIX
carries no post-only time in force: a post-only order that rested through a call
on a scheduled SECURITY market and would cross on the entry into continuous
trading).

`TransactTime(60)` on a report built from an engine event is the sequencing
time of that event, the same instant the native `order` frames carry in
`acceptedTs` (member API section 6, "`acceptedTs` per surface"): the cancel's
time on a `150=4`, the fill's on a `150=F`, never the time the order was first
accepted.

Gateway-level rejections (rate limit, bad mapping, unknown symbol) arrive
immediately as `150=8` with `37=0`. Engine rejections and all accepts, fills and
cancels arrive through the post-durable fan-out, in engine order: **no
ExecutionReport is ever sent before the journal made the event durable**.

## 8. Drop copy and credential scope

Two ways to get an observation-only session, and they compose:

1. **Credential level (recommended).** A `READ_ONLY` credential from venue
   operations logs on as drop-copy regardless of tag 9003, and the same key is
   refused on REST and WebSocket writes too (code 1005). The restriction is a
   property of the key, so a misconfigured client cannot undo it.
2. **Session level.** Send `9003=Y` on Logon with an ordinary trading
   credential when one connection should be observation-only.

On such a session `D`, `F` and `G` are refused with a `Reject(3)` naming the
message type, without reaching the engine. ExecutionReports and market data flow
normally. A member may hold a trading key and a read-only key at the same time.

## 9. Market data (35=V request, 35=W snapshot, 35=X incremental)

`MarketDataRequest(V)` with `SubscriptionRequestType(263)`:

| 263 | Behaviour |
|---|---|
| `0` | Snapshot once. One `MarketDataSnapshotFullRefresh(W)`, nothing further. |
| `1` | Snapshot then updates, in the shape `MDUpdateType(265)` selects. |
| `2` | Unsubscribe. No reply. A message already queued may still arrive, so treat the first quiet period as the boundary. Read from `263` and `55` alone, so `264`, `265` and `269` on a disable are ignored rather than validated: a disable echoing back a `264` the venue would refuse on a subscribe is still accepted. |

`MDUpdateType(265)` picks the update shape for `263=1`:

| 265 | Behaviour |
|---|---|
| `0` or absent | **Full refresh, the default.** A complete fresh `W` after every commit batch that changed the book or the last trade (conflated, never a partial delta). Absent means this, so a client written before revision 2.9 keeps working unchanged. A `269=2` subscriber sees a batch's trading only as the last trade PRICE in the next `W`: the print-by-print tape, with trade ids, sizes and timestamps, exists only on `X`. |
| `1` | **Incremental.** The initial `W` snapshot, then one `MarketDataIncrementalRefresh(X)` per commit batch carrying only what changed plus that batch's trade prints. |

Request: `MDReqID(262)` (echoed on `W`, `X` and rejects), `NoRelatedSym(146)=1`
with `Symbol(55)` (one symbol per request), `MarketDepth(264)` levels per side
(5 if you omit it, and if you SEND it, an integer from 1 to 25: anything else,
including `0` for full book, answers `281=5` and subscribes nothing), and an
optional `NoMDEntryTypes(267)` group of
`MDEntryType(269)` values selecting what you want: `0` bid, `1` offer, `2`
trade. Absent means all three. Up to 32 instrument subscriptions per session.

**Required fields, and how they fail.** `MDReqID(262)`, `SubscriptionRequestType(263)`
and `Symbol(55)` are REQUIRED and are rejected by name when absent or empty; the
venue never substitutes a value for them, on any `263` including the `2` disable.
`263` answers `281=4`, `55` answers `281=0`, and `262` answers a session-level
`Reject(35=3)` with `SessionRejectReason(373)=1`, `RefTagID(371)=262` and
`RefMsgType(372)=V`, because a `35=Y` must itself carry the `MDReqID` that is
missing. Nothing is subscribed and the session stays up. Everything else on the
request is optional and an ABSENT one takes the documented default: `264` absent
is 5 levels, `265` absent is the conflated full refresh, and an absent `267`
group is all three entry types. Generate `263` explicitly, always: it is the
field that decides whether you get a stream at all.

**A value you DID send is never replaced either.** That is the same rule from the
other side, and `MarketDepth(264)` is where it bites: a PRESENT `264` must be an
integer from 1 to 25, and `0` (full book in FIX 4.4), a larger value, a negative,
a non-numeric string or an empty value all answer `281=5` with a `Text(58)`
naming the tag and the range, and subscribe nothing. `264=0` used to be served as
5 levels and anything above 25 as 25, both without a reject. Do not send a depth
you have not validated and expect the venue to trim it.

**Body field order carries no meaning.** The venue reads every body field by tag,
so `264` before `265` and `265` before `264` are the same request. The only place
body order matters is inside a repeating group, and `269` is the only group the
venue reads inbound. The standard header and trailer are positional as FIX 4.4
requires (`8=FIX.4.4` first, `9=` immediately after it, `10=` exactly at the
`BodyLength` boundary), and getting one of those wrong is unframable rather than
rejectable, so the venue logs out instead of answering `35=3`. A tag sent twice
in one message keeps its first occurrence.

The W body: `NoMDEntries(268)` then repeating `MDEntryType(269)` /
`MDEntryPx(270)` / `MDEntrySize(271)`, where `269` is `0`=bid, `1`=offer,
`2`=last trade (price only). Values are exact decimal strings.

The X body: `MDReqID(262)`, `NoMDEntries(268)`, then entries each BEGINNING with
`MDUpdateAction(279)` (the group delimiter):

- **Book entries.** `279` `0` New, `1` Change, `2` Delete; `269` `0` bid or `1`
  offer; `55` symbol; `270` the price level; `271` the level's new AGGREGATE
  size (`0` on a Delete); `290` the level's 1-based rank on its side, in the
  view this message produces on a New or a Change, and in the view you are
  currently holding on a Delete.
- **Trade entries.** `279` always `0`; `269=2`; `278` the venue trade id (the
  same id `GET /v1/trades` returns); `55` symbol; `270` price; `271` quantity;
  `272`/`273` UTC date `YYYYMMDD` and time `HH:MM:SS.sss`; venue tag `9013`
  aggressor side (`1` buy, `2` sell); venue tag `9014` `NORMAL` /
  `LIQUIDATION` / `ADL` / `RFQ` / `BARRIER_CLOSE` / `REVERSAL` / `SWAP` (the
  same `flags` vocabulary as the WebSocket `trades` channel; `BARRIER_CLOSE` is
  the closing trade of a barrier contract closed early by RFQ; `SWAP` is a
  yield contract take whose `270` is the rate in basis points, not a price; the
  vocabulary is additive, treat unknown values as opaque).

Order inside one X: trades first, in engine order, then the book entries, bid
entries before offer entries, describing the state at the END of that commit
batch. Do not depend on that ordering to parse: split the group on `279` and
read `269` on each entry.

**Key the book by `MDEntryPx(270)`.** `MDEntryPositionNo(290)` is the rank at
message time and shifts whenever a level is added or removed above it without
the venue restating the levels below; it is for display and sanity checks, never
an identifier. `271` on a Change is absolute, so re-applying a Change is a no-op.

Building a correct consumer:

1. Send `263=1`, `265=1`. Wait for the `W`. Build the book from it, keyed by
   price per side.
2. Apply every following `X` in `MsgSeqNum` order. New inserts, Change
   overwrites the size at that price, Delete removes the price.
3. **Any `W` that arrives afterwards is a re-baseline**: the venue sends one when
   your session fell behind and it stopped sending you increments rather than
   racing your queue. Discard the book for that symbol and rebuild from it.
4. Detect loss with `MsgSeqNum(34)`: market data shares the session sequence with
   ExecutionReports, so a gap is visible the same way, and the recovery is to
   reconnect (section 4) and re-subscribe.
5. Re-issuing the `MarketDataRequest` at any time returns a fresh `W` and
   re-seeds the venue's baseline. That is the whole recovery procedure; there is
   no market-data resend. The replacement takes effect immediately: anything the
   venue had already built for the subscription you replaced is discarded rather
   than delivered after the new `W`, so you can safely narrow `264` or the `269`
   set on the re-issue without inheriting entries from the old window.
6. Trade entries are best effort. A session that falls behind loses prints, and
   the re-baseline `W` restores the book but not the missed prints. Reconcile
   against `GET /v1/trades` if you need a gap-free tape: it returns the prints
   for an instrument in engine order with nothing missing inside the window it
   covers, which is the most recent 1000 prints of that instrument. It is a
   recent-trades route, not an archive, so reconcile while you are still inside
   that window.

Failures answer `MarketDataRequestReject(Y)` with `MDReqRejReason(281)`: `0`
missing or unknown symbol, or bad request shape, `2` at the 32 subscription
limit, engine momentarily busy, or the snapshot could not be built (all three
retryable, and `Text(58)` says which), `4` missing or unsupported
`SubscriptionRequestType`, `5` unsupported `MarketDepth(264)` (present and
outside 1 to 25, or not an integer), `6` unsupported `MDUpdateType`, `8`
unsupported `MDEntryType`. A refusal is guaranteed-class and is never dropped to
save a slow consumer; only the refresh stream is best effort.
A missing `MDReqID(262)` answers the session-level `Reject(35=3)`
described above instead. `Text(58)` always names the tag at fault. A rejected
request subscribes nothing.

**What is still WebSocket only**: per-level book checksums, mark and index
prices, funding rates and the typed venue-events feed.

## 10. Limits and controls

- Two independent buckets per MEMBER, shared by every API key the member holds:
  6000 reads/min and a **40000/min order budget**. Order placements, batch
  entries (REST/WS only, below), and amendments all consume the order budget;
  cancels are never throttled. FIX order entry uses the order bucket. Both are
  full-capacity, so a quiet period leaves the whole minute available for a
  burst. FIX order entry shares the per-second ceiling your class publishes
  (STANDARD 1,000 orders/s on production, none on the sandbox; member reference
  section 4); a burst above it answers a report with Text(58) carrying
  retryAfterMs under one second. Holding several keys does not multiply the
  budget.
- Cancels consume neither bucket but DO count toward the order-to-trade regime
  (RTS 9), which blocks new orders and amendments with 1004 while never blocking
  cancels.
- Batch order entry (REST `POST /v1/orders/batch`, WS `orders.batch`) is not
  offered over FIX: loop NewOrderSingle messages instead. Nor are batch
  amendment (`POST /v1/orders/batch/amend`, WS `orders.batchAmend`) and batch
  cancellation (`POST /v1/orders/batch/cancel`, WS `orders.batchCancel`),
  member reference revision 3.74: FIX 4.4 has no message that amends or
  cancels a list of orders by id, so loop 35=G or 35=F, or use those routes on
  a native connection alongside.
- A session that cannot keep up with its own execution flow is disconnected
  (Logout, slow consumer), mirroring WebSocket close 4008. Read on a dedicated
  loop and hand off to a queue.
- **Clustered deployments and code 5030 `NOT_LEADER`**: over REST, any request
  that is not a `GET` under `/v1` sent to a node that is not the leader answers
  HTTP 503 with code 5030, and the body carries `leaderId` and `leader`, an
  `https` base URL that on every venue deployment is the environment's
  published entry host. That is a redirect, not a backoff and never a
  rejection: accept `leader` only when it equals the entry host you configured
  and resubmit there with the SAME `clOrdId`; if it names another host, send no
  credentials there and tell venue operations (member API section 10.7). FIX
  never surfaces 5030: since member API revision 3.77 (FIX
  rules of engagement 2.25) order flow that lands on a node which is not the
  leader is answered with `Logout(35=5)`, `Text(58)="leader change, reconnect"`
  and the close; reconnect toward the current leader and
  recover over the REST backfill. Before 3.77 it was rejected with the
  retryable shape (`150=8`, `9010=5001`, `Text(58)=engine busy, retry`), which
  now means a full intake on the leader only: retry it with backoff on the same
  session. An order sent just before a leader change may still execute, so
  reconcile by `ClOrdID(11)` on the leader before you resubmit. The clustered
  sandbox of member API revision 3.83 (section 1, from the date venue
  operations announce) answers 5030 on its REST and WebSocket surfaces, and
  its FIX sessions get the leader-change Logout there too.

## 11. What to build

1. **Codec**: framing with `BodyLength`/`CheckSum` generation and validation,
   partial-read buffering, and first-wins duplicate tag handling. Keep repeating
   groups (market data entries) in arrival order, and split them on the group
   delimiter (`MDUpdateAction(279)` in a 35=X, `MDEntryType(269)` in a 35=W)
   rather than on a fixed tag stride.
2. **Session layer**: logon with the required fields, heartbeat and TestRequest
   handling, strict inbound sequence checking, GapFill response to ResendRequest,
   clean Logout.
3. **Order mapper**: your model to/from the tags in sections 5 to 7, with
   decimal strings throughout and instrument-grid alignment from REST
   `GET /v1/instruments`. FIX carries no instrument reference or status
   message, and the grid of a listed market never changes in place: a
   `tickSize` or `qtyStep` change is a re-listing announced in advance (member
   API section 2a), which reaches a FIX session only as `150=4` cancels with
   `Text(58)=INSTRUMENT_HALT` and then `9010=3006` on new orders. Re-read the
   instruments after an announced date and on any unexpected `9010=2001`,
   `2002` or `3006`, and key on the symbol.
4. **Order state machine** driven by ExecutionReports, applied idempotently by
   `ExecID(17)` so replaying a backfill is harmless, and handling
   **unsolicited** reports (`9011=VENUE`) for orders you never sent. Take the
   order's open and filled quantities from `LeavesQty(151)` and `CumQty(14)` of
   each report (the order after that report, section 7) and close the order on
   the one lifecycle report with a terminal `OrdStatus(39)`.
5. **Recovery engine** implementing section 4 with a persisted cursor.
6. **Market data consumer** if used, keyed on `MDReqID`. For `265=1`, a
   price-keyed book per symbol seeded from the `W` and maintained by the `X`
   stream, treating any later `W` as a re-baseline, plus a tape consumer keyed on
   `MDEntryID(278)` so prints dedupe against `GET /v1/trades` (section 9).

## 12. Conformance tests

1. Logon with `141=Y` and valid credentials; assert the Logon echo. Bad
   credentials: assert a Logout mentioning 1001.
2. Omit `141`; assert the venue refuses the logon.
3. TestRequest answered by a Heartbeat echoing your `TestReqID`.
4. Place a far-from-market LIMIT GTC; assert `150=0` with your `ClOrdID` and
   `Account` echoed. Cancel it; assert `150=4` with `Text=USER_REQUESTED` and
   `LeavesQty=0`.
5. Cross with a second session; assert `150=F` on both with correct `LastPx`,
   `LastQty`, `LeavesQty` and `CumQty`, and `9011=MEMBER`. Rest a SELL of 1 and
   send an IOC BUY of 2 at its price; assert `150=F` with `39=1`, `151=1`,
   `14=1`, then `150=4` with `39=4`, `151=0`, `14=1`. Sweep two resting SELLs of
   1 with a BUY of 2; assert `39=1`, `151=1`, `14=1`, then `39=2`, `151=0`,
   `14=2`. Fill 1 of a post-only BUY of 3 and reprice it by a `35=G` across a
   resting SELL; assert `150=5` with `151=2`, `14=1`, then `150=8` with
   `151=0`, `14=1`.
6. Unknown symbol; assert `150=8` with `9010=2001`.
7. Cancel an unknown order; assert `OrderCancelReject(9)` with `102=1`.
8. Resend a working order's `ClOrdID` with a changed `Price(44)`; assert
   `150=8` with `9010=2009`. Resend it with the byte-identical payload; assert
   `150=I` with `9015=Y` and the ORIGINAL `OrderID(37)`. Assert your client
   booked no second order either time, and that the count of NewOrderSingles
   still awaiting an answer settles back at zero.
9. Assert your order state machine treats `150=I` as a restatement and never as
   a terminal state, and that a report carrying `9010` does not by itself close
   an order: on a 5005 the `39`, `151` and `14` of a `150=I` describe an order
   you still own. Then cancel that order and resend its `ClOrdID` with the
   identical payload: the restatement now carries the terminal `39=4` of the
   canceled order, AFTER the `150=4` that closed it. Assert your client books it
   as neither a second cancel nor a resurrection, and that any "one terminal
   `39` per order" check you run excludes `150=I` (section 5).
10. Send a deliberate `MsgSeqNum` gap; assert a Logout naming the backfill route,
    then re-logon cleanly.
11. Corrupt a checksum; assert the connection closes.
12. `MarketDataRequest` with `263=0`; assert one W with bid and offer entries.
    With `263=1`, assert an unsolicited W after a book change. With `263=2`,
    assert refreshes stop. Unknown symbol: assert `Y` with `281=0`.
13. `263=1` with `265=1`; assert a W, then rest an order at a new price from a
    second session and assert an `X` with `279=0` at that price, add size at the
    same price and assert `279=1` with the summed size, then remove it all and
    assert `279=2` with `271=0`. Rebuild the book from snapshot plus increments
    and reconcile against `GET /v1/orderbook?symbol=` while the book is quiet.
14. Subscribe with `269=2` and cross a trade; assert an `X` trade entry whose
    `278` matches the `GET /v1/trades` id for the same execution, and whose
    `270`/`271` match its price and quantity.
15. Send `265=9`; assert `Y` with `281=6`. Send `269=4`; assert `Y` with
    `281=8` and that no `W` follows. Send `264=0`, then `264=50`, then
    `264=abc`; assert `Y` with `281=5` and no `W` on each. Then subscribe, and
    disable with `263=2`, `55` and `264=0`; assert the disable is accepted
    silently and the stream stops.
15a. Omit `55`, then `54`, then `38`, then `40` from a `35=D`, one at a time;
    assert `Reject(35=3)` with `373=1`, the matching `371`, `372=D`, that NO
    ExecutionReport follows, and that the session stays up. Assert your client
    correlates it by `RefSeqNum(45)` and does not wait for a `150=8`. Then send
    a PRESENT but unknown `55` and assert the unchanged `150=8` with
    `9010=2001`. Send an order with no `11` and assert your parser accepts a
    report with no tag 11; send a `35=F` by `37` alone against an unknown order
    and assert it accepts `11=KX-NONE` and `41=KX-NONE`; send a `35=1` with no
    `112` and assert `Reject(35=3)` with `371=112` and no Heartbeat.
15b. Send `54=X`; assert `Reject(35=3)` with `373=5` and `371=54`. Send `54=5`;
    assert the unchanged `150=8` with `9010=2013`. Send a `35=F` with `37=`,
    `41=` and `11=` all EMPTY; assert an `OrderCancelReject(9)` carrying `37=0`,
    `11=KX-NONE` and `41=KX-NONE`, with all three tags PRESENT, and that your
    session layer does not reject it for a missing required field. Log on with
    `49` empty, and separately send anything before your Logon; assert both
    answers carry `56=CLIENT` and never an empty `56`.
16. Kill the connection mid-flow; recover per section 4 and prove the merged log
    is gap-free and duplicate-free by `ExecID`.
17. If you hold a READ_ONLY credential: assert Logon echoes `9003=Y` and that
    `D` is refused with a `Reject(3)`.
18. If your member has registered Authorised Users (ask venue operations to
    register one on your sandbox member first): send a NewOrderSingle without
    tag 9012; assert `150=8` with `9010=2021` and `37=0`. Repeat with a
    registered active Trader ID in 9012; assert `150=0`. A member with no
    registered users accepts both, so the negative case only proves anything
    once the member is gated.
19. At the leader hand-over venue operations stage on the clustered sandbox at
    an agreed time (Conformance Test Plan I-05): assert you handle
    `Logout(35=5)` with `Text(58)="leader change, reconnect"`, log on again to
    the configured host with `141=Y`, re-arm `9004`, recover per section 4,
    reconcile by `ClOrdID(11)` an order whose outcome you did not see, and send
    again at once a cancel whose `150=4` you did not see.

## 13. Common mistakes

- Building a resend/message-store layer. The venue has none by design; use the
  REST backfill (section 4).
- Pinning a node address, or resending an order whose outcome a leader-change
  Logout hid under a NEW `ClOrdID(11)`: connect to the configured host only and
  reconcile by the old reference first (section 3).
- Forgetting `141=Y`, or sending `MsgSeqNum` other than 1 on logon.
- Computing `BodyLength` or `CheckSum` over the wrong byte range. Test against a
  known-good message before going near the venue.
- Assuming one TCP read equals one FIX message.
- Treating `OrderCancelReject(9)` as the success path for a cancel. Success is
  an ExecutionReport with `150=4`.
- Waiting for an ExecutionReport after a NewOrderSingle that omitted a required
  tag. The answer is a session-level `Reject(35=3)` naming the tag; it carries no
  `ClOrdID(11)`, so correlate it by `RefSeqNum(45)` (section 5).
- Sending a `MarketDepth(264)` above 25, or `264=0` for "the whole book", and
  expecting the venue to trim it. Both answer `281=5` and subscribe nothing
  (section 9).
- Requiring `ClOrdID(11)` on inbound ExecutionReports. Reports for an order you
  placed without a reference have no tag 11 at all (section 7).
- Keying unsolicited-close handling on the text of `ClOrdID` instead of
  `9011=VENUE`.
- Ignoring `Account(1)` on reports, then being unable to attribute a liquidation
  fill to an end client.
- Using floats for `Price` or `OrderQty` and failing tick/lot alignment (2002).
- Processing live reports before the post-reconnect backfill completes.
- Rebuilding an order's `CumQty` as `OrderQty` minus `LeavesQty`. A canceled IOC
  remainder has `LeavesQty` 0 with `CumQty` below `OrderQty`. Read `CumQty(14)`,
  the order's own filled quantity on every report, or sum `LastQty(32)` over its
  fills (section 7).
- Leaving tag 9012 out of the order mapper. The moment your member registers
  its first Authorised User, every order without a registered active Trader ID
  rejects with 2021 (section 5).
- Sending a NewOrderSingle for an RFQ-mode or BARRIER market. There is no order
  book there and no FIX surface at all: 2022. Route those markets to REST or
  WebSocket (section 5).
- Reusing a `ClOrdID` across runs, for example by seeding references from a
  counter that restarts at 1. The venue answers, but with 2009 or a `150=I`
  restatement rather than an accept, so a client that only handles `150=0`
  leaves those requests open forever. Seed references from something that does
  not repeat within 24 hours (section 5).
- Keying an incremental book on `MDEntryPositionNo(290)` instead of
  `MDEntryPx(270)`, then drifting the first time a level is inserted or removed
  above the ones you are holding (section 9).
- Treating a mid-stream `W` on an incremental subscription as a duplicate
  snapshot and ignoring it. It is a re-baseline and your book is stale until you
  rebuild from it (section 9).
- Assuming a fixed tag stride inside a repeating group. Trade entries carry tags
  book entries do not, and the last-trade entry in a `W` has no `271`. Drive the
  parser off the delimiter (`279` in an X, `269` in a W) and off `268`.
