# Kronos X FIX 4.4 Gateway, Rules of Engagement

Revision 2.31, 21 October 2026, covering Kronos X v2.0 "Pacifica".
FIX order entry, execution-report drop copy and streaming market data for
members that integrate over FIX rather than the native WebSocket/REST API. The
FIX surface maps onto exactly the same engine command path, risk checks, rate
limits and post-durable delivery as every other transport: an ExecutionReport is
never sent before the journal made the event durable.

Status: the sandbox acceptor is `sandbox.mtf.perpetuals.net:9879` over TLS
from the date venue operations announce (revision 2.30: the clustered sandbox
of the member reference revision 3.83, section 1a, which replaces
`sandbox.mtf.perpetuals.com` on that date). Production connectivity details (host, port, TLS)
are confirmed by venue operations at onboarding.

## 1. Session layer

- FIX 4.4, tag=value, SOH delimited. `BeginString` must be `FIX.4.4`.
- Transport is TLS (same publicly trusted certificate as the REST API). The
  TLS handshake must complete within 5 seconds of the TCP connect or the
  connection is closed.
- **Logon deadline.** A connection that has not completed a valid Logon within
  10 seconds of connecting is closed; the venue first attempts, best effort, a
  Logout (35=5) with `Text(58)` "logon timeout". The deadline is measured from
  TCP accept and inbound bytes do NOT extend it, so connect only when you are
  ready to log on.
- **Connection ceilings**, enforced at accept with the surplus connection
  closed immediately and no FIX frame sent: 512 FIX connections in total
  (authenticated sessions plus not-yet-logged-on connections plus TLS
  handshakes in progress), 64 connections that have not yet completed a Logon
  across all addresses, and 8 such not-yet-logged-on connections per remote
  address. Keep reconnect storms bounded on your side; an orderly client never
  approaches these.
- Venue `SenderCompID(49)` is `KRONOSX`. Your CompID is whatever you send as
  `SenderCompID(49)` on Logon; the venue echoes it as `TargetCompID(56)`.
  `SenderCompID(49)` is not enforced: if you send it absent or empty, or if the
  venue has to answer you before your Logon arrives at all, it addresses you as
  `CLIENT`. `49` and `56` are standard header fields that FIX 4.4 requires on
  every message and that can therefore be neither omitted nor left empty, so a
  documented placeholder is the only valid answer there. Send a real CompID.
- **Logon (35=A)** must carry:
  - `EncryptMethod(98)=0`
  - `HeartBtInt(108)` between 5 and 60 seconds
  - `ResetSeqNumFlag(141)=Y` (required on EVERY logon)
  - `Username(553)` = your API key, `Password(554)` = your API secret
  - `MsgSeqNum(34)=1`

  Every one of those is checked and a failure closes the session with a Logout
  naming the reason, so a Logon is never partially honoured. An absent `553` or
  `554` is a failed authentication like any other: Logout mentioning code 1001.
- **Cancel on disconnect (Logon tags `9004`/`9005`).** A trading session may
  arm the venue's cancel-on-disconnect protection at Logon: `9004=Y` arms it,
  `9005` carries the grace window in milliseconds, clamped to 0 to 30000,
  absent meaning 0. Armed PER SESSION, in memory only: it does not survive the
  session and a re-logon must re-arm. When an armed session disconnects, for
  any reason, the venue waits the 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, on every instrument, untriggered conditionals
  included, regardless of the transport that placed them (orders belong to
  members, not connections, so no per-connection cancel exists). The canceled
  orders report `Text(58)=USER_REQUESTED`, because the armed protection is the
  member's own standing instruction, and the cancels arrive as ordinary
  `150=4` reports with no `ExecRestatementReason(378)`: by their tags they
  cannot be told apart from a member-wide cancel-all of the member's 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 is that it follows an armed session's disconnect, is ONE
  command (every report carries the same engine sequence prefix in
  `ExecID(17)`), covers every open order of the member and answers no request
  (revision 2.30 states it; the value stays `USER_REQUESTED` by design). A FIX TRADING Logon within 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). A DROP-COPY session
  counts for NEITHER side of the protection: it cannot arm (below), its
  presence does not satisfy the liveness probe, and a drop-copy Logon does not
  disarm a pending timer, because a credential that cannot manage orders must
  neither trigger cancels nor silently veto them. The venue re-checks liveness
  immediately before submitting the cancel-all, so a trading Logon that lands
  even at the last instant still stops it. A second armed session of the same
  member dropping restarts the grace window on a BEST EFFORT basis (the
  restart can race a timer already at expiry; the final liveness re-check
  applies either way). A venue restart voids all in-memory arming uniformly:
  nothing fires during or after the restart and every re-logon starts
  unarmed. Since revision 2.25 a leader change on a clustered deployment does
  the same: a session the venue logged out for a leader change arms nothing,
  no session that closes on a node which is not the leader arms anything, and
  a pending timer whose node lost the leadership is voided; your resting orders
  survive on the new leader. On a DROP-COPY session, `9003=Y` or a `READ_ONLY`
  credential, `9004` is ignored and forced to `N`: a read-only credential must
  not be able to trigger cancels. **The Logon reply echoes `9004`** with the
  state actually armed, so a drop-copy session that sent `Y` reads `N` back.
  This protection cancels ORDERS ONLY: the venue never closes, reduces or
  hedges a position on a disconnect. The WebSocket twin is the
  `session.cancelOnDisconnect` op (member reference section 10.5a).
- **Sequence numbers reset on every logon.** The venue keeps no FIX message
  store. On an inbound gap the venue sends Logout with the reason and closes;
  an outbound `ResendRequest(2)` is answered with a GapFill
  `SequenceReset(4)` that takes the next number in the venue's own outbound
  sequence and points `NewSeqNo(36)` at the one after it. The venue's
  `MsgSeqNum(34)` is strictly increasing for the life of a session and never
  renumbers, on this path as on every other. **Recovery after any disconnect is the durable REST
  backfill** (`GET /v1/stream/events`, see the member reference section 11),
  which is strictly stronger than FIX resend: it is complete, ordered and
  deduplicable across all transports. 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`. 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.
- Heartbeats per `HeartBtInt`; the venue sends `TestRequest(1)` after 1.2x
  silence and ends the session after 2x: a `Logout(35=5)` with `Text(58)`
  "heartbeat timeout", then the disconnect (revision 2.26; before it the
  connection closed with no frame). Malformed framing (bad checksum, bad
  BeginString) closes the connection: FIX cannot resynchronise mid-stream.
- Nothing follows a Logout the venue sends (revision 2.26): until the
  disconnect the session acts on no message it receives, so a Logon that
  crosses the "logon timeout" Logout is not answered and disarms no pending
  cancel-on-disconnect, and it receives no further ExecutionReport. Reports of
  that window reach your other sessions and the event stream as before;
  recover them through `GET /v1/stream/events`.
- A `TestRequest(1)` must carry `TestReqID(112)`. The answering Heartbeat has to
  echo it, so there is nothing legal for the venue to send back when it is
  absent or empty: such a request answers a session-level `Reject(35=3)` with
  `SessionRejectReason(373)=1`, `RefTagID(371)=112` and `RefMsgType(372)=1`, and
  no Heartbeat. The session stays up.
- **The venue never emits a zero-length value for any tag on any message.** A tag
  the venue cannot fill is either omitted, or carries a documented sentinel where
  FIX requires it to be present (section 3), or the message is refused with a
  session-level `Reject(35=3)` naming the tag. So `11=`, `55=`, `54=`, `41=`,
  `37=`, `56=` and `112=` with nothing after the equals sign never appear. That
  holds on the standard header too, which is why an absent or empty
  `SenderCompID(49)` is answered as `CLIENT` rather than echoed back empty, and
  it holds for a tag you sent EMPTY as well as for one you omitted: the venue
  reads a present-but-empty field as one you did not send, on every message.
- **Venue restarts.** When the venue stops for a rollout or a restart, every
  FIX session receives `Logout(35=5)` with `Text(58)="venue restart"` before
  the TCP connection is closed (best effort, a session whose outbound queue is
  full is closed at the same bounded deadline). The venue sends the same
  Logout (revision 2.27), 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); no other Logout text marks that case,
  and the REST backfill of the `order` and `execution` families below brings
  your order state back in step. Before revision 2.27 this paragraph said that
  nothing is dropped for cause; that holds for a venue stop only. In both cases:
  reconnect with backoff, `Logon` again with `141=Y`, re-arm `9004` if you use
  it, re-subscribe to market data, and recover over the durable REST backfill
  as after any other disconnect (section 4 of the Claude Code FIX integration guide, or the member reference section 11). Neither
  the session nor its subscriptions survive a restart. The WebSocket twin is
  close status `1001` with reason `venue restart` (member reference section
  10.7).
- **Leader change (clustered venue).** When the node your session is on stops
  accepting FIX because it handed venue leadership to another node, the venue is
  NOT restarting: every FIX session on that node receives `Logout(35=5)` with
  `Text(58)="leader change, reconnect"` before the TCP close. The action is the
  same as after a restart (reconnect with backoff, `Logon` with `141=Y`,
  re-arm, re-subscribe, recover over the REST backfill), only the reason on the
  wire differs, so alerting keyed on `venue restart` does not fire on a
  leader flap. A single-node deployment never sends this text. The clustered
  sandbox of revision 2.30 (three nodes, `sandbox.mtf.perpetuals.net`, from
  the date venue operations announce) sends it at every leader change there, including the leader hand-over
  venue operations stage at an agreed time for Conformance Test Plan case
  I-05.
- **Leader change (clustered deployments, revision 2.25).** A single-node
  deployment is unchanged. On a clustered deployment:
  1. A `NewOrderSingle(35=D)`, `OrderCancelRequest(35=F)`,
     `OrderCancelReplaceRequest(35=G)` or `MarketDataRequest(35=V)` received by
     a node that is not the leader is answered with `Logout(35=5)`, `Text(58)`
     "leader change, reconnect", then the close; reconcile by `ClOrdID(11)` on
     the leader before you resubmit.
     Session-level messages (Heartbeat, TestRequest) are answered as before. A
     drop-copy session's order entry stays refused with `BusinessMessageReject(35=3)`,
     `373=99` on every node, as before.
     Reconnect to the leader and recover through the event stream, as member
     reference section 10.7 describes for a leader change. Before revision 2.25
     such a session was answered `9010=5001` "engine busy, retry" (or a `35=9`
     "engine busy") for as long as the node's demotion was delayed.
  2. `150=8`, `39=8`, `9010=5001`, `Text(58)` "engine busy, retry" on an order,
     and `OrderCancelReject(35=9)` with `Text(58)` "engine busy" on a cancel or a
     replace, now mean that the leader's intake is full; a node that no longer
     leads sends the leader-change Logout instead. An order appended just before
     a leader change may still execute: reconcile by `ClOrdID(11)` on the leader
     (the event stream, or `GET /v1/orders/open`) before you resubmit.
  3. A session the leader change logged out arms no cancel-on-disconnect, like a
     restart (the cancel-on-disconnect paragraph above); your resting orders
     survive on the new leader.
  4. A FIX session receives the Logout on every leader change, also on a node
     that regains the leadership at once: every new leadership term starts a
     fresh acceptor, so log on again whenever you read the text.
  5. (Revision 2.30 states it; nothing changed.) Reconnect with backoff to the
     FIX host and port you configured for the environment: node addresses are
     not published and are not member endpoints, so never pin a node. A
     reconnect just after a leader change can still fail, or be logged out with
     the same text: reconnect again after a short delay. FIX
     never answers `5030`; the REST and WebSocket `5030`, its leader hint and
     what to do with it are the member reference section 10.7. 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.
  6. (Revision 2.30 states it; nothing changed.) An order, cancel or replace
     (`35=D`, `35=F`, `35=G`) whose commit the cluster does not confirm within
     10 seconds (its pending-acknowledgement window, which a stalling disk on
     a follower can exhaust) ends the session: the venue closes the TCP
     connection without a Logout and without a report for that message.
     Unlike the leader-change Logout (item 3), that close counts as a
     disconnect for the cancel on disconnect: on a session that armed it
     (`9004=Y`) the protection cancels every open order of your member after
     its grace window unless a FIX TRADING Logon disarms it first. The outcome
     of the message is unknown and it may still execute: log on again at
     once, reconcile by `ClOrdID(11)` on the leader before you resubmit, and
     never send it again under a new `ClOrdID(11)`. REST and, since member
     reference revision 3.83, the WebSocket answer the same fault `5000`
     (member reference section 10.7).
- A member whose FIX session cannot keep up with its execution flow is
  disconnected (Logout, reason "slow consumer"), mirroring WebSocket close 4008.
  Execution reporting is the guaranteed stream and is never dropped to save a
  laggard; market data is best effort and is held back first, so a session that
  falls briefly behind loses market-data messages before it loses the connection
  (section 5.4).

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

"Named on absence" in the Required column means the tag is refused by name with a
session-level `Reject(35=3)` when it is absent or empty, as described under the
table. "Default on absence" means its absence selects the documented default and
changes nothing else.

| FIX tag | Required | Venue meaning |
|---|---|---|
| `ClOrdID(11)` | optional | Your order reference (same semantics as `clOrdId`: <= 36 chars, unique per 24 h, idempotency key, `KX-` prefix reserved). NOT enforced, matching the native API: an order sent without one is accepted and simply has no reference, so it has no duplicate protection and cannot be canceled or replaced by `OrigClOrdID(41)`. Its ExecutionReports OMIT tag 11 entirely. Send one on every order. |
| `Account(1)` | default on absence | The end-client reporting identifier (`clientUid`). On a broker member it is effectively mandatory and its absence is the business reject 2010; on a market-maker member it may be omitted, in which case 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`. A PRESENT but unknown symbol keeps its business reject 2001. |
| `Side(54)` | named on absence | 1=Buy, 2=Sell. A PRESENT value that FIX 4.4 defines but the venue does not trade (`5` sell short, say) keeps its business reject 2013. A value OUTSIDE the FIX 4.4 `Side` enumeration (`1` to `9`, `A` to `G`) is refused by name instead, see the notes. |
| `OrderQty(38)` | named on absence | Quantity in decimal base-asset units, exactly as REST `qty` (member reference section 2a): must be an exact multiple of the instrument `qtyStep` (reject 2002 in `9010`; a value that is not a decimal numeral at all, an exponent, a blank, letters, is 2013 since revision 2.19), integer strings remain valid. |
| `OrdType(40)` | named on absence | 1=Market, 2=Limit, 3=StopMarket, 4=StopLimit. A PRESENT but unsupported value keeps its business reject 2004. |
| `Price(44)` | conditional | Limit price (Limit and StopLimit), quoted per base-asset unit on the instrument tick grid, exactly as REST. Conditional on `OrdType(40)`, so it keeps its business reject rather than being named at the session layer. |
| `StopPx(99)` | conditional | Trigger price (stop types). Absent on a stop type is the business reject 2013. |
| `TimeInForce(59)` | default on absence | 1=GTC, 3=IOC, 4=FOK, 6=GTD (+`ExpireTime(126)` UTC). A GTD order never trades at or after its `ExpireTime(126)` (revision 2.31, member reference 3.86), measured against the time the venue sequences each command, never your clock: an order, a replace, a cancel, a trigger or a call auction's uncross that reaches it from that instant expires it (`150=C`, `39=C`; a replace or a cancel is refused, section 3), and the venue's expiry sweep, about once a second, expires one nothing reaches. These rules were published at revision 2.31 by the operator's decision of 2026-10-20, the venue being before production go-live, the 30-day notice of the versioning policy of the member reference waived for them, as for item (3) of member reference 3.73, member reference 3.81 and member reference 3.82. The matching engine applies them from its replay-contract line for member reference revision 3.86, the first command of a venue started on the production contract; venue operations announce the date on any environment where that line lies ahead. Before that line the sweep alone expired a GTD order, so it could still fill for up to about a second after `ExpireTime(126)`. ABSENT means GTC. The venue does not offer DAY, so `59=0` is rejected with 2013 rather than accepted as the FIX default: send `59` explicitly. Both halves follow the same rule, an absent field takes its documented default and a field you DID send is never replaced. |
| `ExecInst(18)` contains `6` | optional | Post-only (Limit only). |
| `9001=Y` | optional | Venue tag: reduce-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`, member reference section 6). Enforcement is staged per member: optional while your member has no registered Authorised Users, then REQUIRED on every order, else the business reject 2021 in `9010`, decided before the order reaches the engine. Treat the value as opaque, up to 64 characters; the venue-issued default form is `TR-{memberId}-{8 hex}`. |
| `9016` | optional | Venue tag: hedge-mode position side, `LONG` or `SHORT` (Member API reference, section 6). Routes the order's fills to that sub-position bucket of the `Account(1)` end client, so one client can hold a long and a short sub-position in the same instrument at once. ABSENT means netting, exactly as before the tag existed. Order-book perpetual markets only: on a spot instrument it gives 2016, any other PRESENT value gives 2013. Echoed on the order's ExecutionReports and omitted on netting orders (section 4). |
| `9020` | optional | Venue tag: TargetLotID, the FIX carrier of the native targeted lot close (`POST /v1/lots/{lotId}/close`, member reference section 6e). Names ONE open lot id of the `Account(1)` end client, as `GET /v1/lots` reports it. The order itself is a normal order in every other respect: give it its own unique `ClOrdID(11)`, the correct closing `Side(54)` and `OrderQty(38)` yourself, and `9016` when the lot sits in a hedge bucket (the venue does not derive these from the lot on FIX). On acceptance the venue records the target and the lot fold consumes that lot ahead of FIFO; `GET /v1/lots/closures` then pairs your open and close references exactly as after a REST targeted close. A target naming a lot that is unknown, already fully closed, or that this order does not in fact reduce does NOT reject: that close follows FIFO, and the closures feed shows which lot was actually consumed, so verify there. A value that is not a positive integer answers the business reject 2013 in `9010` before the order reaches the engine, as does any 9020 on a venue running without the lot ledger. One target per order; an identical-payload duplicate (`9015=Y`) restates the original order, whose own recorded target governs. |
| `9028` | optional | Venue tag: TargetOpenClOrdID, the targeted close addressed by YOUR OWN opening order reference instead of the venue lot id, so a FIX-only integration never reads `GET /v1/lots` to close what it opened. Value: the `ClOrdID(11)` you sent on the OPENING order, up to 36 characters. The venue resolves it to the lot or lots that order opened (an order filled in several prints opens one lot per fill; they share your reference) and directs this close at them oldest fill first, up to this order's quantity, exactly as if each had been named by tag `9020`. Each of those lots is targeted at its full opening size and the fold takes only what is left of it, so consecutive partial closes of the same order walk its lots oldest first and only quantity beyond its last open lot follows FIFO (revision 2.28, Member API 3.80; before it the close was allocated over the fills' original sizes, so a second partial close of an order that filled in several prints could send part of its quantity FIFO to the client's oldest lot). To land a partial close on one specific lot, send `9020` with an `OrderQty(38)` no larger than that lot's current `qty`. Everything else matches `9020`: the order is a normal order (own unique tag 11, correct closing `Side(54)`, `OrderQty(38)` and `9016` supplied by you), an unknown or already-closed reference degrades that close to FIFO rather than rejecting, and the closures feed pairs your open and close references for verification. Resolution rule, because tag 11 uniqueness is a 24 hour window: when you have reused the reference, the MOST RECENT order bearing it wins; never reuse references you intend to close by. One target per order: `9020` and `9028` together answer the business reject 2013 in `9010`, as does a value over 36 characters, a value that begins or ends with whitespace (since Member API 3.72: the reference is resolved against the venue's history records, which must never take a padded reference for the unpadded one; a lot you opened under a padded reference before that date is targeted by its lot id, tag `9020`) or any `9028` on a venue running without the lot ledger. |
| `9021` | optional | Venue tag: `tradingCapacity` of the REST/WS `reg` object, `DEAL` (own account), `MTCH` (matched principal) or `AOTC` (any other capacity). Recorded verbatim into the same per-order regulatory record as the native API's `reg.tradingCapacity`; see the reg-tags note below. When capacity enforcement is at the ENFORCE stage for your member (member reference section 6f), the capacity becomes required on every order and a violation answers the business reject 2033 in `9010`, decided before the order reaches the engine, exactly like 2021. |
| `OrderCapacity(528)` | optional | Standard FIX capacity, accepted as an alternative carrier for GATED members only (Revision 2.13): for a member whose capacity enforcement stage is OFF, or who has no licensing profile, the tag stays entirely unparsed, exactly the pre-2.11 wire. For a gated member `P` and `G` map to `DEAL`, `R` to `MTCH`, `A` to `AOTC`, and tag 9021 remains the canonical carrier: at ENFORCE a 528 value with no venue mapping answers 2033 in `9010` and nothing is journaled, and when both 528 and 9021 are present they must agree or the order answers 2033; at WARN the same conditions are counted venue-side and the tag is ignored rather than refused. The mapped value is recorded into the regulatory record exactly as if 9021 had carried it. |
| `9022` | optional | Venue tag: `clientIdType`, `LEI`, `NATIONAL_ID` or `INTERNAL`, as on the REST `reg` object. |
| `9023` | optional | Venue tag: `clientId`, the client identifier the `clientIdType` names. |
| `9024` | optional | Venue tag: `investmentDecisionId`, the algo or person responsible for the investment decision (Art. 26(3) MiFIR). |
| `9025` | optional | Venue tag: `executionDecisionId`, the algo or person responsible for the execution decision. |
| `9026` | optional | Venue tag: `dea`, direct electronic access flag. `Y` means true; any other value is read as `N`, the same convention as tags 9001 and 9004. |
| `9027` | optional | Venue tag: `liquidityProvision`, market-making activity flag. `Y` means true, same convention as `9026`. |
| `9029` | optional | Venue tag: `riskReducing` (revision 2.20, Member API 3.70), RTS 22 field 64: the transaction objectively reduces risk on a commodity derivative. `Y` means true, any other value reads as false, an absent tag leaves the field unset (absent = false on the report); only meaningful on commodity-derivative markets, recorded verbatim elsewhere. NewOrderSingle only (35=G accepts no reg tag). |

Notes:
- **What an omitted order field does.** `Symbol(55)`, `Side(54)`, `OrderQty(38)`
  and `OrdType(40)` are the four tags a NewOrderSingle cannot be understood
  without. Absent or empty, the message is refused BY NAME at the session layer
  with `Reject(35=3)` carrying `SessionRejectReason(373)=1` (required tag
  missing), `RefTagID(371)` = the tag, `RefMsgType(372)=D` and a `Text(58)`
  naming it. That verdict is reached BEFORE the order is built, so nothing
  reaches the engine, no ExecutionReport is produced, and the session stays up.
  A `Reject(35=3)` carries no `ClOrdID(11)` and no `ExecID(17)`: correlate it by
  `RefSeqNum(45)`.
- **A frozen DLT financial instrument or client custody wallet (revision
  2.35 of these rules, Member API 3.70, section 6i).** A `NewOrderSingle(35=D)`
  or an `OrderCancelReplaceRequest(35=G)` on a SECURITY market whose instrument
  is frozen on the Kronos X chain, or whose `Account(1)` client's custody wallet
  is frozen there by the venue registrar, is refused before the engine with the
  venue code `2049` in `9010` (the report or the cancel reject carries the one
  venue text in `Text(58)`), exactly as REST and the WebSocket answer it. A
  wallet freeze also cancels that client's resting orders on SECURITY markets
  (`ExecType(150)=4` with the venue's `ADMIN_CANCEL` reason); your other clients
  and markets are untouched.
- **A PRESENT but invalid value is unchanged.** It keeps the business reject it
  has always had and the same venue code in `9010`: an unknown `Symbol(55)` is
  2001, an unsupported `Side(54)` or `TimeInForce(59)` is 2013, an unsupported
  `OrdType(40)` is 2004, an off-grid `OrderQty(38)` or `Price(44)` is 2002 (a
  38 or 44 that is not a decimal numeral at all, an exponent, a blank, letters,
  is 2013 since revision 2.19, exactly as on REST and WebSocket), and on a
  broker member an absent `Account(1)` is 2010. Only the ABSENT-field path
  changed shape.
- **One exception, and only one: a `Side(54)` outside the FIX 4.4 enumeration.**
  `Side` is the single tag the venue echoes onto its own answer that FIX both
  marks required on an ExecutionReport AND restricts to a fixed value set (`1` to
  `9`, `A` to `G`). Echoing `54=X` back would produce a `35=8` that a
  dictionary-validating client engine rejects outright, so it never sees the
  `9010` it needed; omitting `54` is not available, and substituting a Side you
  did not send is exactly what this venue does not do. So an out-of-enumeration
  `Side(54)` is refused by name at the session layer with `Reject(35=3)` carrying
  `SessionRejectReason(373)=5` (value is incorrect for this tag),
  `RefTagID(371)=54` and `RefMsgType(372)=D`. A value INSIDE the enumeration that
  the venue does not trade is not this case and keeps its 2013, because the
  report echoing it is valid FIX. `Symbol(55)` is not an enumerated field, so an
  unknown symbol is still echoed back exactly as you sent it with 2001.
- **The venue never emits a zero-length value.** A gateway reject used to echo
  `ClOrdID(11)`, `Symbol(55)` and `Side(54)` "as you sent them", so a field you
  omitted came back as `11=`, `55=` or `54=` with nothing after the equals sign,
  which is not valid FIX. Now: `55` and `54` are always present and non empty on
  a reject, because their absence is refused before a reject is built; `11` is
  present when you sent one and ABSENT when you did not. Read `9010` and
  `Text(58)`, not the echoed tags, when you are diagnosing a malformed request.
- The minimum order notional applies exactly as on the native API: an order
  whose `Price(44) x OrderQty(38)` (market orders: current mark x quantity)
  is below the instrument's `minNotional` is rejected with 2028 in `9010`.
  Cancel/Replace re-checks the full replaced quantity.
- Trigger `direction` is derived from side (buy stops trigger at-or-above,
  sell stops at-or-below), matching conventional stop semantics.
- Rate limits and the RTS 9 order-to-trade regime apply exactly as on REST/WS for
  every message the venue ACCEPTS FOR PROCESSING: placements and Cancel/Replace
  all consume the order budget, and cancels are never throttled.
  **A message refused at the SESSION layer never reaches either meter.** A
  `Reject(35=3)` says the venue did not accept the message for processing at all
  (section 4), so a `35=D` missing `Symbol(55)`, `Side(54)`, `OrderQty(38)` or
  `OrdType(40)`, or carrying a `Side` outside the FIX enumeration, spends none of
  your order budget and is not counted as an order message in your order-to-trade
  ratio. The line is the ANSWER, not the reason: everything answered with a
  BUSINESS reject (`35=8`, `35=9`) was accepted for processing and is charged and
  counted as usual, including a `35=D` refused for an unknown symbol or an
  unsupported `OrdType`, a `35=G` that names no order, and a `35=F` that
  identifies no order (counted for the ratio; cancels are never charged against
  the order budget on any transport). The one shape that is answered with a
  business reject and still reaches neither meter is the reserved `KX-` namespace
  2020, and that is unchanged and identical on REST and WebSocket. The session
  layer is therefore the one accounting difference from REST and WebSocket, which
  have no session layer to refuse at; it cannot be used to obtain a larger
  effective budget, since a refused message places no order.
- **The reg tags 9021 to 9027 and 9029** carry the remaining fields of the REST/WS
  `reg` object (`9012` continues to carry `reg.traderId`, unchanged, with its
  2021 Authorised User enforcement unchanged). An ABSENT tag leaves the field
  unset, exactly as omitting the key in the REST `reg` object, and the values
  are recorded verbatim into the per-order regulatory record, so the journaled
  record for the same order is byte-identical across REST, WebSocket and FIX.
  Validation is shared with the native API, including the 512 byte ceiling on
  the total reg record, which rejects 2013. `OrderCancelReplaceRequest (35=G)`
  is amend-only and accepts none of these tags: the regulatory record is set
  at order entry and is immutable.
- Batch order entry (`POST /v1/orders/batch`, WS `orders.batch`) is REST and
  WebSocket only; FIX members submit NewOrderSingle messages in a loop instead.
  The same holds for 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 a FIX member sends
  `OrderCancelReplaceRequest (35=G)` and `OrderCancelRequest (35=F)` per order,
  or uses those routes on a native connection alongside.
- **Reusing a `ClOrdID(11)`** (the reference is unique per member for 24 h) always
  produces exactly one ExecutionReport, on the session that sent the order. 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
  reference whose order is still working or pending trigger is never released. Reusing
  a RELEASED reference is a new order (`150=0`), not either shape below:
  - reused with a DIFFERENT payload: `150=8`, `39=8`, `9010=2009`, the same
    registry rejection REST answers with HTTP 400 and the WebSocket
    `order.place` ack with `success: false`;
  - reused with an IDENTICAL payload: this is the venue's idempotency path, so
    nothing new is created and the ORIGINAL order is restated as `150=I`
    (order status) with venue tag `9015=Y`, `OrderID(37)` and `OrdStatus(39)` of
    that original order. This is the FIX shape of the REST/WebSocket
    `duplicate: true` response.

  Generate unique references. A reused reference never creates a second order on
  any transport, so treating either answer as an accept will leave your order
  state machine tracking an order the venue does not have.
- MARKET+FOK is all-or-none inside the protection collar, exactly as on the
  native API: expect either full trade reports or one Canceled with
  `Text(58)=FOK_UNFILLABLE`.

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

- Cancel by `OrderID(37)` or by `OrigClOrdID(41)`.
- Replace supports `Price(44)` and/or `OrderQty(38)` (qty-down keeps queue
  priority, as on REST).
- Failures answer `OrderCancelReject(9)`: `CxlRejReason(102)=1` unknown order,
  else 0 with the venue reason in `Text(58)`; `CxlRejResponseTo(434)` 1=cancel,
  2=replace. When a venue registry code applies it also rides `9010`, exactly
  as on ExecutionReport rejects.
- **References on an `OrderCancelReject(9)`.** FIX 4.4 requires `OrderID(37)`,
  `ClOrdID(11)` and `OrigClOrdID(41)` on this message, while the venue keeps
  accepting a cancel identified by `OrderID(37)` alone and a replace that never
  carries `41` at all. A reference you did not send is therefore reported as the
  sentinel `KX-NONE`, never as a zero-length value; `OrderID(37)` keeps its
  published `0` sentinel. `KX-` is the venue-reserved namespace and is refused
  with 2020 on order entry, so `KX-NONE` can never collide with a reference the
  venue would accept. Do not match it against your own order table.
  **Sending the tag EMPTY counts as not sending it, for all three.** If your
  client always writes `37`, `11` and `41` and blanks the ones it does not hold,
  the reject you get back carries `37=0`, `11=KX-NONE` and `41=KX-NONE`, never a
  zero-length value and never the tag missing.
- A `35=F` that carries NEITHER `OrigClOrdID(41)` nor `OrderID(37)` is answered
  with `CxlRejReason(102)=1` as before, and `Text(58)` now names the two tags
  instead of describing an order the request never identified.
- A replace carrying a PRESENT but invalid tag is rejected, never partially
  applied and never treated as if the tag were absent: `Price(44)` off the
  instrument tick grid, or `OrderQty(38)` off the `qtyStep` grid, answers
  `OrderCancelReject(9)` with `434=2` and 2002 in `9010`, the same 2002 REST
  and WebSocket return for that input; a 44 or 38 that is not a decimal numeral
  at all (an exponent, a blank, letters) answers the same reject with 2013 in
  `9010`, again as REST and WebSocket do (revision 2.19). Either way the working
  order keeps its original terms.
- In production a member with no licensing profile receives `OrderCancelReject(9)`
  with `434=2`, `102=0` and `9010=2034` on every `35=G` (revision 2.20), the same
  fail-closed posture as `35=D` and as the REST and WebSocket amend (member
  reference section 6f); the request never reaches the engine and the working
  order keeps its terms. The sandbox never answers it.
- An `OrderCancelReject(9)` always means the request was REFUSED and the order is
  unchanged. Success is the ExecutionReport, `150=4` for a cancel and `150=5` for
  a replace. That holds for code 5005 too: see section 4.1. One exception since
  revision 2.31 (member reference 3.86, from the matching engine's replay-contract
  line named in the `TimeInForce(59)` row): 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
  expires the order and is refused as the unknown order, `434=2` for the replace or
  `434=1` for the cancel, `102=1` and 2014 in `9010`, and the order's expiry arrives
  as its own ExecutionReport (`150=C`, `39=C`); once the sweep has reached the order
  the same request is refused `102=1` again (a `35=G` then without `9010`). Either
  way the order has ended and nothing traded. Since revision
  2.31 every `35=G` that the matching engine refuses 2014 carries `102=1`, as a
  `35=F` does: before, an order that ended between the venue's lookup and the
  replace was refused with `102=0`.

## 4. ExecutionReports (35=8)

The session is a **drop copy for the whole member**: reports cover all order
activity of your member id regardless of transport (REST, WebSocket, FIX,
venue-initiated), including liquidation and ADL closes, and every RFQ
execution your member is party to (an RFQ trade, a barrier early close, a
yield contract take), which prints as a fill report with `OrderID(37)` `0` since
no order exists; on a yield contract take `LastPx(31)` is the offer's rate in
basis points and `LastQty(32)` the taken size in the market's asset (Member
API section 6h).

| Field | Content |
|---|---|
| `ExecType(150)` / `OrdStatus(39)` | 0=New, F/1 or F/2=Trade (the order partly filled, or complete, AFTER this fill; revision 2.29, see below), 4=Canceled, C=Expired, 5=Replaced, 8=Rejected, L/0=Triggered (a conditional order activated, see below; revision 2.26), I=order status (restatement, see below). |
| `OrderID(37)`, `ClOrdID(11)` | Venue id and your reference. Venue-initiated closes carry `KX-LQ-{orderId}` / `KX-ADL-{tradeId}` in 11. Tag 11 is ABSENT, never empty, on every report for an order you placed without a reference. |
| `Account(1)` | The end-client reporting identifier (`clientUid`), on every report that has one. It is omitted rather than sent empty on the reports where the venue has none: the 2010 rejection that says the identifier was missing, and the activation report `150=L`, which names the order by `OrderID(37)` and `ClOrdID(11)` only (the order's accept carries its `Account(1)`). |
| `ExecID(17)` | `{engineSeq}-{eventIndex}`: globally unique, totally ordered, and directly comparable with the REST/WS backfill cursor space for cross-transport dedupe. |
| `LastQty(32)`, `LastPx(31)` | This fill's quantity and price (trades only). |
| `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. The same on every session of your member, whichever transport placed the order and whenever the session logged on (revision 2.29, see below). |
| `StopPx(99)` | The trigger price of a stop or take-profit order, on its activation report `150=L` (revision 2.26). |
| `Text(58)` | Cancel reason enum or reject message. |
| `OrdRejReason(103)` | 99 (other); the precise venue code rides `9010`. |
| `9010` | The venue's numeric code (complete registry: KronosX-API-Codes). |
| `9011` | `MEMBER` or `VENUE` (refSource; key unsolicited-close handling on this). |
| `9015` | `Y` on an `ExecType(150)=I` restatement of an order a reused `ClOrdID` already maps to (section 2). Absent otherwise, including on a section 4.1 restatement for a reference you did not reuse. |
| `9016` | The order's hedge-mode position side, `LONG` or `SHORT`, echoed on every report for an order that carried it except its activation report `150=L`, including the reports of a venue-initiated close of a `LONG` or `SHORT` bucket. OMITTED, never empty, on netting orders, so every pre-hedge report is byte-identical. |
| `9019` | outbound, on fill ExecutionReports | What the fill did to the client sub-position: `OPEN`, `INCREASE`, `PARTIAL_CLOSE`, `CLOSE` or `FLIP`. Only ever set on trade legs. Liquidation and ADL fills always reduce; a REVERSAL fill undoes the original trade and can carry `OPEN` or `INCREASE` when the reversed trade had closed a position. Unsolicited fills additionally carry the venue `KX-` reference in `ClOrdID(11)`. |
| `9017` | Queue position at rest, quantity ahead: the quantity resting ahead of the order at its price level (decimal base units, FIFO priority), on the accept report (`150=0`) of a placement that RESTED the order, and on no other report. `0` means front of the level, so PRESENCE is the signal that the order rested; the tag is omitted on full fills, IOC/FOK/market remainders, conditionals accepted pending trigger and every reject, keeping pre-queue reports byte-identical. A snapshot at the moment of rest, measured after any reduce-only trim the same placement caused, and immediately stale: the venue never updates it (member reference section 6). |
| `9018` | Queue position at rest, orders ahead: the integer number of orders ahead at that level, beside `9017` and under the same presence rule (both tags appear together or not at all). |

**Quantities and status on a report (revision 2.29, member reference 3.82).**
`LeavesQty(151)` and `CumQty(14)` are the ORDER's, after the event the report
describes: `CumQty(14)` is the order's filled quantity, counting every fill of
the order whichever transport placed it and whenever your session logged on,
and `LeavesQty(151)` is what is still open (`0` on a cancel, an expiry and a
rejection). A fill report (`150=F`) states the order after THAT fill:
`CumQty(14)` includes this fill and no later one, `LeavesQty(151)` is what this
fill left open, and `OrdStatus(39)` is `1` while anything is open and `2` only on
the fill that completes the order. So an IOC or market order that partly fills
receives `150=F` with `39=1` and the unfilled rest in `151`, then `150=4` with
`39=4`, `151=0` and its true `CumQty(14)`; an order that sweeps several resting
orders in one go receives one `150=F` per fill, `39=1` on every fill but the one
that completes it. On a trade between two of your own end clients each of the two
fill reports carries its own order's `151`, `14` and `39`. A replace (`150=5`)
carries the order's `CumQty(14)` and its new open quantity, also when it reports
a reduce-only order the venue trimmed. A rejection (`150=8`) of an order the venue
had already accepted carries the order's `CumQty(14)` with `151=0`: a post-only
order that partly filled and that a replace (`35=G`) then repriced to cross
receives `150=5` and then `150=8` with `9010=2005`, both with the quantity it
filled; a rejection before the order was booked carries `14=0`. A fill with no order on your side,
`OrderID(37)` `0` (an RFQ or barrier fill, a yield contract take, an ADL close,
an operator's trade reversal), is complete in itself: `39=2`, `151=0` and
`CumQty(14)` equal to `LastQty(32)`. A `150=I` restatement carries the same `151`
and `14` as the order's latest lifecycle report; its `39` is the order's `status`
as `GET /v1/orders/{orderId}` reports it at that moment. Take an order's
cumulative quantity from `CumQty(14)`, or sum `LastQty(32)` over its fills;
never rebuild it as `OrderQty` minus `LeavesQty`, which books the canceled
remainder of an IOC as filled. Before revision 2.29 a fill report carried the
order's state after the whole command that produced the fill, not after the
fill, for an order that partly filled and was then canceled within the same
command (an IOC or market remainder, a self-trade prevention cancel of the
aggressor, an unfunded spot or position-size cancel, a reduce-only trim, a liquidation
slice) and for an order that swept several resting orders: the canceled order
reported `39=2` and `151=0` on its fill and then its cancel with `14=0`, two
reports with a terminal `39` for one order, and every fill of a sweep carried
what the whole command left open (`39=2` and `151=0` on every fill of a sweep
the command completed or canceled, every fill after the first then with
`CumQty(14)` equal to its own `LastQty(32)`; `39=1` with the final `151` on
every fill of a sweep whose rest stayed on the book). `CumQty(14)` counted only
the fills the session itself had been sent since its first report of the order,
so a session that logged on after a fill of the order left that fill out; a
rejection always carried `14=0`, also after the fills of a post-only order
repriced to cross. The values above are in force at the publication of revision
2.29 by the operator's decision of 2026-10-19, the venue being before production
go-live, the 30-day notice of the versioning policy of the member reference
waived for them, as for item (3) of member reference 3.73 and member reference
3.81.

**Conditional activation (revision 2.26, member reference 3.78).** When the
trigger of a stop or take-profit order fires, the venue sends ExecType `L`
("Triggered or Activated by System") with `OrdStatus(39)=0`: `OrderID(37)`,
`ClOrdID(11)` when the order carried one, `Symbol(55)`, `Side(54)`,
`LeavesQty(151)` the order's quantity, `CumQty(14)` `0`, `TransactTime(60)` and
`StopPx(99)` the trigger price. It is a lifecycle transition, pending trigger to
working, never a fill, and it is the drop copy's twin of the WebSocket
`OrderTriggered` frame and the REST backfill's `ORDER_TRIGGERED` row; it does
not repeat `Account(1)` or `9016` (read them from the accept). What follows is
the activated order's ordinary lifecycle: a market child trades or is canceled
`150=4` with `IOC_REMAINDER`, a limit child trades or rests. Before revision
2.26 the venue sent nothing between a conditional's accept and its first fill.
A conditional that the venue cancels at its trigger, before it activates, gets
no `150=L`: its report is the plain cancel `150=4` with the reason in `Text(58)`
(`REDUCE_ONLY_VIOLATION` when the market, your account or a stale mark admits
reduce-only flow only and the order would add risk: on a perpetual an order that
is not reduce-only, on a spot market a buy; `FUNDING_CATCH_UP` when
the order is not reduce-only and the market's funding settlement is more than
60 s overdue).

Gateway-level rejections (rate limit, malformed mapping, unknown symbol) come
back immediately as ExecType 8 with `OrderID(37)=0` and the code in `9010`.
Engine rejections and every accept/fill/cancel arrive through the post-durable
event fan-out, in engine order: a rejected order's terminal report is `150=8`,
`39=8` with the code in `9010`, and an order the engine accepted and then
rejected inside the same command (a post-only order that would cross, section
4.1's example) first receives its `150=0` accept. Where a rejection lands is
transport-specific by design: REST reports the same engine rejection inside
HTTP 200 as `data.status` `REJECTED` plus `data.rejectCode`, the WebSocket ack
is the verdict, and FIX has the ExecutionReport; the member reference, section
6, "Where a rejection lands, per transport", states the convention and that it
stays. Since revision 2.21 (member reference 3.70) a GRID perpetual whose funding
catch-up is pending (its settlement more than 60 s overdue, the venue settling the
missed anchors) answers an opening NewOrderSingle and a non-reduce-only
OrderCancelReplaceRequest with `9010=3014` and the registry text in `Text(58)`
(`150=8`, `39=8` on the order; `35=9` on the replace), reduce-only orders stay
admitted, and a conditional order whose trigger fires inside that window reports
`150=4`, `39=4` with `Text(58)=FUNDING_CATCH_UP` (venue-initiated, like
`MARKET_ACCESS`, and like `POST_ONLY_WOULD_CROSS`, revision 3.70: a post-only
order entered on REST or WebSocket, FIX carries no post-only time in force, that
rested through a call on a scheduled SECURITY market and would cross on the entry
into continuous trading, reported on the drop copy); the `Text(58)` vocabulary on a cancel is the
`cancelReason` table of KronosX-API-Codes section 5.1, an open set. The window
normally lasts seconds:
after a restart the venue lands the catch-up before the acceptor takes a Logon.

**Every NewOrderSingle the venue accepts for processing is answered exactly
once, and exactly one LIFECYCLE report ever closes a given order.** A message
refused at the SESSION layer was never accepted for processing, so the
`Reject(35=3)` that answers a NewOrderSingle with a required tag missing
(section 2) is not one of the answers counted here: it is the whole answer to
that message, it is the only one, and no ExecutionReport follows it. The answer is
either a gateway rejection, or one of the fan-out reports above, or, for a reused
`ClOrdID`, the 2009 rejection or the `150=I` restatement described in section 2,
or the `150=I` restatement described in section 4.1. None of those gateway-built
answers is an order lifecycle event: none is written to the durable event stream
and none is replayed by `GET /v1/stream/events`, the same rule the REST and
WebSocket answers for a reused reference follow. Their `ExecID(17)` is therefore
a gateway-local value, not a `{engineSeq}-{eventIndex}` cursor: correlate them by
`ClOrdID(11)` and keep them out of the dedupe table you use for durable
executions.

**What "closes an order" means, precisely.** The lifecycle reports are
`150` = `0`, `L`, `F`, `4`, `C`, `5` and `8` (`L`, revision 2.26, never carries a
terminal `39`), and exactly one of them ever carries a
terminal `OrdStatus(39)` (`2` filled, `4` canceled, `C` expired, `8` rejected)
for a given order (revision 2.29 corrected the two shapes that broke this, an
order that partly filled and was then canceled within the same command, an IOC
or market remainder, a self-trade prevention cancel of the aggressor, an unfunded spot
or position-size cancel, a reduce-only trim or a liquidation slice, whose fill
reported `39=2` before its cancel, and an order that swept several resting
orders and that the same command completed, whose every fill reported `39=2`:
"Quantities and status on a report" above). `ExecType(150)=I` is NOT a lifecycle report: it is a status
restatement that repeats the order's state at the moment it was built, so it can
carry a terminal `39` for an order the venue already closed, after the lifecycle
report that closed it. That happens whenever the restatement is issued for an
order that is already terminal, for example the section 2 answer to a reused
reference whose original order has since been canceled. So do not assert "at most
one report with a terminal `39` per order"; assert "at most one report with a
terminal `39` and `150` other than `I`", and never let a `150=I` move your own
order state machine.

### 4.1 Code 5005 on an order

`5005 COMMAND_FAILED` means the venue accepted your command, made it durable and
then could not finish processing it. Trading continues normally, and whatever the
command achieved before it stopped is KEPT, exactly as on REST and the
WebSocket, where 5005 arrives as `data.rejectCode` and the rest of `data`
describes what the command did. FIX says the same thing in FIX terms, and the
case you are in is decided by what the command DID, in this order:

- **The engine rejected the order**, whether before it was ever accepted or after
  accepting it (a post-only order that would cross, and the `REJECT_TAKER`
  self-trade policy, are both accepted and then rejected inside one command). The
  engine's own rejection is the answer, with the real reject code in `9010`.
  There is no further report and 5005 is not restated over it.
- **The reference was already taken by a different payload**: the answer is the
  ordinary 2009 rejection of section 2, `150=8`, `39=8`, `9010=2009`. The
  registry refused the request, so nothing was created and the reason you need is
  2009, not 5005.
- **An order exists** (this command created it, or the byte-identical retry of
  section 2 maps to one). You receive the ordinary fan-out reports for it, and
  then ONE further report on the session that sent the order: `ExecType(150)=I`
  (order status) carrying that order's real `OrderID(37)`, `OrdStatus(39)`,
  `LeavesQty(151)` and `CumQty(14)`, with `9010=5005` and the reason in
  `Text(58)`. It is a restatement, not a lifecycle transition: it repeats the
  order's state at that moment, so it carries a terminal `39` when the same
  command also closed the order (an `IOC` remainder canceled, an unfillable
  `FOK`, a complete fill), and the report that CLOSED the order is still the
  fan-out's, never this one. 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 and nothing was rejected**: the answer is a single
  `150=8`, `39=8` with `9010=5005`.

**On a cancel (35=F) or a replace (35=G)**, 5005 is not a verdict either. An
`OrderCancelReject(9)` means the venue refused your request, so it is sent only
when the cancel or the replace did NOT take effect, and then it carries
`9010=5005`. When the request did take effect and the command failed afterwards,
the answer is the ordinary ExecType `4` or `5` from the fan-out and nothing else:
the order really is canceled, or really does carry the new terms. Never read a
missing `OrderCancelReject` as "the venue lost my cancel", and never read one
carrying 5005 as "the order is gone anyway".

Do not resubmit an identical request after a 5005. The venue is deterministic,
so an unchanged resubmission fails the same way. Reconcile against
`GET /v1/orders/open` and `GET /v1/orders/{orderId}`, cancel or manage whatever
the report named, and report the incident to venue operations.

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

Market data is available on any session, trading or drop copy. A subscription
covers one instrument and is session state: it is lost on disconnect and must be
re-issued after re-logon.

### 5.1 The request (35=V)

| Tag | Field | Required | Venue meaning |
|---|---|---|---|
| `MDReqID(262)` | Your subscription reference | REQUIRED | Echoed on every `W`, every `X` and every reject. Key your consumer on it. |
| `SubscriptionRequestType(263)` | Mode | REQUIRED | `0`, `1` or `2`, see the table below. Missing or anything else answers `281=4`. |
| `Symbol(55)` | Venue symbol | REQUIRED | Missing or unknown answers `281=0`. |
| `NoRelatedSym(146)` | Must be `1` | optional, default `1` | One symbol per request; send several requests for several instruments. Any other value answers `281=0`. |
| `MDUpdateType(265)` | Update shape | optional, default `0` | `0` full refresh, `1` incremental. Anything else answers `281=6`. |
| `MarketDepth(264)` | Levels per side | optional, default `5` | ABSENT means 5 levels a side. If you SEND it, it must be an integer from 1 to 25: `0` (full book in FIX 4.4), a larger value, and anything non-numeric or empty all answer `281=5` and subscribe nothing. The venue does not serve a full book and no longer caps a request quietly. |
| `NoMDEntryTypes(267)` + `MDEntryType(269)` | Entry filter | optional, default all three | Repeating group of the types you want: `0` bid, `1` offer, `2` trade. ABSENT means all three. Any other value answers `281=8` and nothing is subscribed. |

The venue reads the actual `269` occurrences rather than trusting the `267`
count, so a miscounted group still yields the entry set you asked for.

**BODY field ORDER is not significant.** The venue parses tag=value pairs into a
map and reads each field by tag, so the order in which you place `264`, `265`,
`267`/`269` or any other non-group body field changes nothing. Three things are
positional and always have been: the standard header must open
`8=FIX.4.4`, `9=` must follow it, and `10=` must sit exactly at the
`BodyLength(9)` boundary. Any of those three wrong is unframable rather than
rejectable, so the venue logs out and closes the connection instead of answering
`35=3`. A tag sent twice in one message keeps its FIRST occurrence and the later
one is ignored. Repeating-group entries are read in arrival order, which is the
only place body order matters, and the only group the venue reads inbound is
`269`.

**Required fields are never defaulted.** A field marked REQUIRED above is
rejected by name when it is absent or carries an empty value; the venue does not
substitute a value for it, because on this message a substituted value silently
changes what the request MEANS. `263` is the case that matters most: it decides
whether you get one snapshot or a stream, so an absent `263` treated as `0`
would give you one `35=W` and then permanent silence, indistinguishable from a
subscription on an instrument that never trades. The optional fields above are a
deliberate contrast: their absence SELECTS the documented default in the same
column and cannot change whether the feed exists, so requests that omit them
keep working exactly as before.

**And a value you DID send is never replaced.** That is the same rule read from
the other side, and it is what `MarketDepth(264)` follows: an ABSENT `264` takes
the documented 5 level default, while a `264` the venue cannot serve is REFUSED
by name rather than adjusted to something else. The venue adjusts no field on
this message.

- `263` and `55`: `MarketDataRequestReject(35=Y)` with `MDReqRejReason(281)`
  `4` and `0` respectively, and a `Text(58)` naming the tag. Nothing is
  subscribed.
- `264` present and outside 1 to 25, or not an integer:
  `MarketDataRequestReject(35=Y)` with `MDReqRejReason(281)=5` and a `Text(58)`
  naming the tag and the servable range. Nothing is subscribed. An ABSENT `264`
  is not this case and is unaffected.
- `262`: a session-level `Reject(35=3)` with `SessionRejectReason(373)=1`
  (required tag missing), `RefTagID(371)=262` and `RefMsgType(372)=V`. A `35=Y`
  cannot answer this one, because `35=Y` must itself carry the `MDReqID` that is
  missing. The session stays up.

This rule applies to every `263` value including the `2` disable: a disable with
no `262`, no `263` or no `55` is rejected rather than guessed at. Note what that
means for the disable specifically, because it is the one case where a refusal
leaves something running: the venue drops the subscription on `55`, so a disable
that omits `262` LOOKS like it should work, but it is refused before it reaches
that step and YOUR SUBSCRIPTION KEEPS DELIVERING. If your unsubscribe path keys
off the symbol alone, add `262` to it, and treat the `35=3` as "still
subscribed" rather than ignoring it.

| 263 | Meaning |
|---|---|
| `0` | **Snapshot once.** One `MarketDataSnapshotFullRefresh(35=W)` is returned and nothing further. `265` does not change the reply here, because there is nothing to increment, but it is still validated: an unsupported value answers `281=6` and no snapshot is sent. `264` is validated here too: a value outside 1 to 25 answers `281=5` and no snapshot is sent. |
| `1` | **Snapshot then updates.** The `W` snapshot is returned, then updates in the shape `265` selected, after every commit batch that changed that instrument's book or its last trade. The trigger is the instrument, not your entry-type filter, so a `269=2` subscriber on `265=0` can receive a refresh whose content did not change; on `265=1` a batch with nothing to say for you produces no message at all. |
| `2` | **Disable.** The subscription is dropped; no reply. A message already queued when the disable arrives may still be delivered, so treat the first quiet period as the boundary. A disable acts on `263` and `55` alone, so `264`, `265` and `269` on it are ignored rather than validated: a disable that echoes back the original request's `264` is accepted even when that same `264` would be refused on a subscribe. The required fields above are still required, and `146` other than 1 or an unknown `55` still answers `281=0`. |

| 265 | Update shape for 263=1 |
|---|---|
| `0` (default) | **Conflated full refresh.** A complete fresh `35=W` after every commit batch that changed the book or the last trade. One refresh per batch, never a partial delta. This is exactly the behaviour of revision 2.1 and earlier, and it is what you get if you do not send tag 265 at all. A subscription that asked for `269=2` sees a batch's trading only as the LAST TRADE PRICE in the next `35=W`: the print-by-print tape, with trade ids, sizes and timestamps, exists only on `35=X`, so select `265=1` if you need it. |
| `1` | **Incremental.** The initial `35=W` snapshot, then a `MarketDataIncrementalRefresh(35=X)` per commit batch carrying only what changed, plus the trade prints of that batch. |

A session may hold up to **32 instrument subscriptions** at once. Over that,
the request answers `281=2`; disable one with `263=2` first. Re-issuing a request
for an instrument you already hold REPLACES that subscription and is never
refused, so recovery works at the cap.

### 5.2 The snapshot (35=W)

`MDReqID(262)`, `Symbol(55)`, then `NoMDEntries(268)` followed by that many
repetitions of `MDEntryType(269)` / `MDEntryPx(270)` / `MDEntrySize(271)`.
Entries are ordered bids first (best first), then offers (best first), then the
last trade. `269` is `0`=bid, `1`=offer, `2`=last trade (price only, no size).
Prices and sizes are exact decimal strings, as everywhere on the venue. Entry
types you did not ask for are simply absent, and `268` is always the true count,
so an empty book yields a valid `W` with `268=0`.

### 5.3 The incremental (35=X)

`MDReqID(262)`, `NoMDEntries(268)`, then that many entries. Each entry BEGINS
with `MDUpdateAction(279)`, which is the group delimiter:

| Tag | On book entries (`269` = 0 or 1) | On trade entries (`269` = 2) |
|---|---|---|
| `MDUpdateAction(279)` | `0` New, `1` Change, `2` Delete | always `0` New: the tape is append-only |
| `MDEntryType(269)` | `0` bid, `1` offer | `2` |
| `MDEntryID(278)` | not sent | the venue trade id, the same id `GET /v1/trades` and the WebSocket `trades` channel carry |
| `Symbol(55)` | the instrument | the instrument |
| `MDEntryPx(270)` | the price level | the trade price |
| `MDEntrySize(271)` | the level's new AGGREGATE size, `0` on a Delete | the traded quantity |
| `MDEntryDate(272)` / `MDEntryTime(273)` | not sent | UTC date `YYYYMMDD` and time `HH:MM:SS.sss` of the execution |
| `MDEntryPositionNo(290)` | the level's 1-based rank on its side: on a New or a Change, its rank in the view this message produces; on a Delete, the rank it held in the view you are currently holding, since it has none in the new one | not sent |
| `9013` | not sent | venue tag: aggressor side, `1` buy, `2` sell |
| `9014` | not sent | venue tag: `NORMAL`, `LIQUIDATION`, `ADL`, `RFQ`, `BARRIER_CLOSE` (the closing trade of a barrier contract closed early by RFQ, Member API revision 3.44), `REVERSAL` or `SWAP` (a yield contract take, Member API revision 3.51: `MDEntryPx(270)` is the offer's annualised rate in basis points, not a price, and `MDEntrySize(271)` the taken size in the market's asset), the same classification the WebSocket `trades` channel publishes as `flags`; the vocabulary is additive, treat unknown values as opaque |

**Key your book by `MDEntryPx(270)`, not by `MDEntryPositionNo(290)`.** A New or
a Delete shifts the rank of every level below it and the venue does not restate
those levels: `290` is the rank at the moment the message was built, useful for
display and for sanity checks, and it is not a stable identifier. `MDEntrySize`
on a Change is the level's new aggregate size, not a delta, so applying the same
Change twice is harmless.

Inside one `35=X` the order is: **the trade entries of that commit batch, in
engine order, then the book entries, bid entries before offer entries.** The book
entries describe the state at the END of the batch, which is the state after
those trades. Do not depend on that ordering to parse: split the group on
`MDUpdateAction(279)` and read `MDEntryType(269)` on each entry.

### 5.4 What the incremental feed guarantees

- **The snapshot and the increments are consistent.** The venue registers the
  subscription and builds the `35=W` in ONE step on the engine thread, and the
  snapshot is released to your session through the same post-durability gate as
  your ExecutionReports, in the order it was built. So an increment for a change
  that happened after that snapshot can never overtake it, and the snapshot is
  never ahead of the durable state it describes. Apply the `W`, then apply every
  following `X` in order.
- **A re-subscribe wins immediately.** Re-issuing the request for a symbol
  replaces the subscription. Anything the venue had already built for the
  subscription you replaced is discarded rather than delivered after the new
  snapshot, so narrowing `MarketDepth(264)` or the `MDEntryType(269)` set can
  never leave your book holding entries the new window would not have sent.
- **Loss is detectable, by `MsgSeqNum(34)`.** Market data shares the session's
  outbound sequence with ExecutionReports, so a missing message is a sequence
  gap like any other. The venue never renumbers and never re-sends: an inbound
  gap ends the session (section 1), and your own gap detection on `34` is the
  signal to reconnect.
- **The venue never silently gaps your book.** If your session falls behind, the
  venue stops sending increments for it rather than racing your queue to the
  disconnect line, and the next message it sends you for that instrument is a
  fresh complete `35=W`. Treat any `35=W` arriving mid-stream as a re-baseline:
  discard your book for that symbol and rebuild it from that snapshot.
- **Trade entries are best effort.** While a session is behind, prints for that
  instrument are dropped rather than queued, and the re-baseline `W` restores the
  book but not the missed prints. If you need a gap-free tape, reconcile against
  `GET /v1/trades` on the REST API: it returns the venue's 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 and not an archive, so reconcile while you are still inside that window.
- **Depth is 1 to 25 levels a side.** 25 is the maximum SERVABLE depth, and a
  request for more is refused with `281=5` rather than trimmed to fit. The diff
  is taken against the window you subscribed at, so a level that rises INTO your
  window because a better one was deleted arrives as a New. You do not have to
  infer it.

### 5.5 Recovery

Re-issue the `MarketDataRequest`. The venue answers with a fresh `35=W` and
re-seeds the incremental baseline from it, so everything that follows is
consistent with the new snapshot. This is the recovery procedure for every case:
after a reconnect, after a detected sequence gap, after a re-baseline `W` you
were not expecting, or any time you doubt your book. There is no market-data
resend and none is needed.

### 5.6 Rejects (35=Y)

`MarketDataRequestReject(35=Y)` carries `MDReqID(262)`, `MDReqRejReason(281)`
and a plain-language `Text(58)`:

| 281 | Meaning |
|---|---|
| `0` | Missing `Symbol(55)`, unknown symbol, or an unsupported request shape such as `NoRelatedSym(146)` other than 1 |
| `2` | This session is at its 32 subscription limit, the engine was momentarily busy, or the venue could not build the snapshot. All three are retryable and `Text(58)` says which one it was. Engine busy: the leader's intake is full; a node that no longer leads sends the leader-change Logout instead (revision 2.25, section 1) |
| `4` | Missing or unsupported `SubscriptionRequestType(263)` |
| `5` | Unsupported `MarketDepth(264)`: PRESENT and either outside 1 to 25 or not an integer. An ABSENT `264` is not this and takes the documented 5 level default |
| `6` | Unsupported `MDUpdateType(265)` |
| `8` | Unsupported `MDEntryType(269)` in the request group |

A rejected request subscribes nothing: there is no partial subscription.

A `MarketDataRequest` with no `MDReqID(262)` is answered by a session-level
`Reject(35=3)` instead, carrying `SessionRejectReason(373)=1`, `RefTagID(371)=262`
and `RefMsgType(372)=V`, for the reason given in section 5.1. Treat it as a
rejected request: nothing is subscribed and the session continues.

`Text(58)` on every one of these names the tag at fault. The venue never answers
a `MarketDataRequest` it REFUSES with silence: every refusal above is an explicit
message naming the field that caused it, and it adjusts no field on this message,
so an unservable value is refused rather than quietly turned into a servable one.
A refusal is also never dropped to save a slow consumer: `35=Y` and `35=3` are
guaranteed-class like your ExecutionReports, so a session too far behind to
receive one is disconnected (section 1) rather than left with an unanswered
request. Only the refresh stream (`35=W`, `35=X`) is best effort.

**Choosing between FIX and the WebSocket.** FIX now carries a real streaming
book and the public trade tape, which is enough for most order-entry and broker
integrations. The native WebSocket still carries more: per-level book checksums
for continuous verification, mark and index prices, funding, the typed
venue-events feed and the sequence-cursored recovery backfill. Run FIX alone if
you need order entry with a book and a tape; add a WebSocket connection if you
need any of the rest.

## 6. Drop copy

Every FIX session is already a **full-member drop copy**: ExecutionReports cover
all of the member's order activity regardless of the transport that originated
it (REST, WebSocket, another FIX session) and including venue-initiated closes.
A separate connection is therefore not required to observe your own flow.

A **dedicated drop-copy session** is available for back-office, risk and
surveillance consumers that must not be able to trade. There are two ways to get
one, and they compose:

1. **Credential level (recommended).** Ask venue operations for a `READ_ONLY`
   credential. It 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 it cannot be undone by a misconfigured client.
2. **Session level.** Send `9003=Y` on Logon with an ordinary trading
   credential when a single connection should be observation-only.

A member may hold up to 5 keys per scope simultaneously; issuing a key adds it
alongside the others and revokes nothing, and each key is revoked individually
(portal keys card or venue operations), taking effect immediately on live FIX
sessions using it. In both cases the venue echoes `9003=Y` on the Logon reply
and then:

- ExecutionReports and market data are delivered normally;
- `NewOrderSingle(D)`, `OrderCancelRequest(F)` and
  `OrderCancelReplaceRequest(G)` are refused at the session level with a
  `Reject(3)` naming the message type, without ever reaching the engine.

Run it with its own API credentials so the trading and observing entitlements
are separable, and remember that a drop-copy session shares the member's rate
limits.

## 7. What FIX does NOT carry

Account and margin queries, funding history, Market Segments configuration, RFQ,
batch order entry, batch amendment and batch cancellation (`POST /v1/orders/batch`,
`/v1/orders/batch/amend` and `/v1/orders/batch/cancel`, revision 3.74 of the
member reference), mark and index prices, book checksums, the typed venue-events
feed and the sequence-cursored recovery backfill remain on the native API
(WebSocket + REST). The lot ledger VIEWS (`GET /v1/lots` and
`GET /v1/lots/closures`) are native-only too: FIX can target a close at a
specific lot with tag `9020`, or by your own opening reference with tag `9028`
(section 2), so the trading lifecycle itself needs no REST call, but reading
lots and reconciling closures happens over REST. FIX is order entry, execution
reporting and market data: run it alongside a native connection for everything
else.

## 8. Conformance checklist

1. Logon with 141=Y, correct credentials; expect Logon echo. Bad credentials
   expect Logout mentioning code 1001.
2. TestRequest answered by Heartbeat with your `TestReqID(112)`.
3. Place far-from-market LIMIT GTC: ExecType 0; cancel it: ExecType 4 with
   `Text=USER_REQUESTED`.
4. Cross against yourself from a second session (different clientUid):
   ExecType F on both sessions with correct LastPx/LastQty/LeavesQty/CumQty,
   each of the two fill reports carrying its own order's `LeavesQty(151)`,
   `CumQty(14)` and `OrdStatus(39)` after that fill (section 4).
5. Unknown symbol: ExecType 8 with `9010=2001`.
6. Resend a live order's `ClOrdID(11)` with a changed `Price(44)`: expect
   ExecType 8 with `9010=2009`. Resend it with the byte-identical payload:
   expect ExecType `I` with `9015=Y` and the ORIGINAL `OrderID(37)`. Prove your
   client treats neither as a new order, and that no NewOrderSingle you send is
   ever left without a reply.
7. Prove your order state machine treats `ExecType(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 (section 4.1) the `39`, `151` and `14` of a
   `150=I` describe an order you still own. Repeat step 6 after canceling the
   original order: the restatement then carries the terminal `39=4` of that
   canceled order, AFTER the `150=4` that closed it, and your client must book it
   as neither a second cancel nor a resurrection. Any "one terminal `39` per
   order" assertion has to exclude `150=I` (section 4).
8. Kill the connection mid-flow; recover via `GET /v1/stream/events` and prove
   your merged log is gap-free by `ExecID` ordering.
9. Send a deliberate MsgSeqNum gap: expect Logout naming the backfill route.
10. MarketDataRequest 263=0: one W with bid/offer entries. 263=1 without tag 265:
    W snapshot then an unsolicited full W after a book change. 263=2: no further
    refreshes. Unknown symbol: MarketDataRequestReject with 281=0.
11. MarketDataRequest 263=1 with 265=1: W snapshot, then rest an order at a new
    price from another session and expect a 35=X with `279=0` at that price; add
    size at the same price and expect `279=1` with the new aggregate; remove
    everything at that price and expect `279=2` with `271=0`. Rebuild the book
    from snapshot plus increments and reconcile it against
    `GET /v1/orderbook?symbol=` at a quiet moment.
12. Request `269=2` and cross a trade: expect a 35=X trade entry with the price,
    the quantity, `272`/`273` and an `MDEntryID(278)` that matches the
    `GET /v1/trades` id for the same execution.
13. Send `265=9`: expect 281=6. Send `269=4`: expect 281=8 and no subscription.
    Send `264=0`, then `264=50`, then `264=abc`: expect `281=5` on each, no `W`,
    and no stream afterwards.
14. Required-field handling on `35=V` (section 5.1). Omit `263`: expect
    `281=4` naming the tag, no `W` and no stream afterwards. Omit `55`: expect
    `281=0` naming the tag. Omit `262`: expect a session-level `Reject(3)` with
    `373=1` and `371=262`, and a session that stays up. Then send the same
    request with `264` and `265` in the opposite order and with `264`, `265` and
    `267` omitted entirely, and confirm both behave exactly like the fully
    specified request: field order is not significant and an ABSENT optional
    field takes its documented default. Keep the ABSENT case and the PRESENT
    unservable case separate in your test matrix: they are the two halves of the
    rule and they answer differently.
15. Disable behaviour with an unservable depth: subscribe with `263=1`, then send
    the disable as `263=2` with `55` and a `264=0` echoed from your original
    request. Expect it to be ACCEPTED silently and the stream to stop. A disable
    acts on `263` and `55` alone.
16. Required-field handling on `35=D` (section 2). Omit `55`, then `54`, then
    `38`, then `40`, one at a time: expect `Reject(35=3)` with `373=1`, the
    matching `371`, `372=D`, NO ExecutionReport, and a session that stays up.
    Prove your client does not sit waiting for a `150=8` after a `35=3`, and that
    it correlates the `35=3` by `RefSeqNum(45)` rather than by `ClOrdID`. Then
    send a PRESENT but unknown `55` and confirm you still get the unchanged
    `150=8` with `9010=2001`, so your code distinguishes the two paths. Then send
    `54=X`: expect `Reject(35=3)` with `373=5` and `371=54`, and send `54=5`
    (a FIX Side the venue does not trade): expect the unchanged `150=8` with
    `9010=2013`.
17. Send a NewOrderSingle with no `ClOrdID(11)` and prove your parser accepts an
    ExecutionReport that has no tag 11 at all. Send a `35=F` identified by
    `OrderID(37)` alone against an unknown order and prove your parser accepts
    `11=KX-NONE` and `41=KX-NONE` without matching them to a real order. Send a
    `TestRequest(1)` with no `112` and expect `Reject(35=3)` with `371=112` and
    no Heartbeat.
18. If your client writes every identifier field and blanks the ones it does not
    hold, send a `35=F` with `37=`, `41=` and `11=` all EMPTY. Expect an
    `OrderCancelReject(9)` carrying `37=0`, `11=KX-NONE` and `41=KX-NONE`, with
    all three tags PRESENT. Confirm your session layer does not reject the
    venue's answer for a missing required field.
19. Log on with `SenderCompID(49)` empty, and separately send any message before
    your Logon. Both answers carry `TargetCompID(56)=CLIENT`, never an empty
    `56`. Send a real CompID in production.
20. Drop-copy session (9003=Y): Logon echoes 9003=Y, order entry answers
    Reject(3), and ExecutionReports for orders placed on your trading session
    still arrive.
21. Hedge mode (tag 9016, order-book perpetual markets only). Place a
    far-from-market LIMIT GTC with `9016=SHORT`: expect ExecType 0 echoing
    `9016=SHORT`, and the same echo on its cancel. Place one WITHOUT the tag and
    prove your parser accepts reports with no tag 9016 at all: on a netting
    order the tag is omitted, never empty. Send `9016=BOTH`: expect the
    business reject `9010=2013`. Send `9016=LONG` on a spot instrument: expect
    `9010=2016`.
22. Queue position (tags 9017/9018). Place a far-from-market LIMIT GTC at an
    empty price level: expect its ExecType 0 report to carry `9017=0` and
    `9018=0`, both PRESENT. Place a second order at the same price: expect its
    accept to carry the first order's resting quantity in `9017` and `9018=1`.
    Send an IOC that cancels its remainder and prove your parser accepts its
    reports with NEITHER tag present: absence means the placement did not rest.
23. Cancel on disconnect (tags 9004/9005). Logon with `9004=Y`, `9005=5000`:
    the Logon reply echoes `9004=Y`. Rest a far-from-market order, then drop
    the TCP connection without a Logout and do not re-logon: within roughly
    the grace window every open order of the member is canceled with
    `Text=USER_REQUESTED` (observe over REST `GET /v1/orders/open` or on the
    WebSocket; a second live FIX TRADING session of the same member would
    suppress the cancel, a drop-copy session would not, so keep FIX trading
    sessions down while you watch). Repeat and re-logon INSIDE the
    grace window: nothing is canceled. On a drop-copy session (9003=Y or a
    READ_ONLY credential) send `9004=Y` and expect the Logon reply to echo
    `9004=N`.
24. Conditional activation (revision 2.26). Place a STOP LIMIT buy (`40=4`)
    with `99` above the market, `9002=MARK`, and a limit `44` that will rest.
    Expect `150=0` with no `9017`. When the mark crosses `99`, expect ONE
    `150=L`, `39=0` carrying your `OrderID(37)`, `ClOrdID(11)` and
    `StopPx(99)`, before anything else for that order; then cancel it and
    expect the ordinary `150=4`. Prove your order state machine moves the order
    from pending trigger to working on `150=L` and does not book it as a fill.
25. Quantities after each fill (revision 2.29, section 4). Rest a SELL of 1,
    then send an IOC BUY of 2 at its price: expect `150=F` with `39=1`,
    `151=1` and `14=1`, then `150=4` with `39=4`, `151=0` and `14=1`, and a
    terminal `39` on the second report only. Rest two SELLs of 1 at two prices
    and send a BUY of 2 through both: expect `39=1`, `151=1`, `14=1`, then
    `39=2`, `151=0`, `14=2`. Partly fill a resting order, log a second session
    on, fill the order again: the second session's `14` counts both fills.
    Rest a post-only BUY of 3 (`18=6`), fill 1 of it, then reprice it by a
    `35=G` across a resting SELL: expect `150=5` with `39=1`, `151=2` and
    `14=1`, then `150=8` with `39=8`, `151=0`, `14=1` and `9010=2005`.
    Prove your client takes an order's cumulative quantity from `CumQty(14)`
    or from the sum of `LastQty(32)`, never as `OrderQty` minus `LeavesQty`.
