FIX 4.4 API
Order entry, execution reporting and streaming market data over FIX 4.4. The FIX surface maps onto exactly the same engine command path, risk checks, rate limits and post-durable delivery as REST and the WebSocket: an ExecutionReport is never emitted before the journal made the event durable, and every execution carries the same ExecID that identifies it on the other two transports.
This page is the rules of engagement. It documents what the acceptor actually does, including the places where Kronos X deliberately departs from a textbook FIX engine. The most important departure is that the venue keeps no FIX message store: recovery after a disconnect is a REST call, not a ResendRequest. Read Sequence numbers and recovery below before you write a session layer.
Connectivity
| Environment | Host | Port | Venue SenderCompID |
|---|---|---|---|
| Sandbox | sandbox.mtf.perpetuals.net | 9879 | KRONOSX |
| Production | production.mtf.perpetuals.com | confirmed by venue operations at onboarding | KRONOSX |
sandbox.mtf.perpetuals.net:9879, three nodes behind that one entry host; it replaces sandbox.mtf.perpetuals.com from the date venue operations announce. Connect to the configured host and port only, never to a node address: a leader change happens on the sandbox as it can on production (see Leader change below).- FIX 4.4 only. The first nine bytes of every inbound message must be
8=FIX.4.4. Any otherBeginStringis a framing error and the connection is dropped. There is no FIX 5.0 or FIXT session layer. - TLS is mandatory (TLS 1.3, the same publicly trusted venue certificate as the REST API). Complete the handshake before writing a single FIX byte. There is no plaintext port, and there is no client-certificate requirement: authentication is the API key on Logon.
- Your CompID is whatever you send as
SenderCompID(49)on Logon. The venue records it from that message and echoes it asTargetCompID(56)on everything it sends you. It is not pre-registered, so it must be stable across reconnects only for your own reconciliation. - Credentials are the same API key and secret used on REST and WebSocket. The key carries a scope: a
READ_ONLYcredential logs on successfully but cannot trade on any transport. See Drop copy and credential scope below.
Wire format
Framing
Standard tag=value with SOH (0x01) as the field separator. The venue generates and validates both length fields; get them wrong and the session ends immediately.
8=FIX.4.4|9=<bodyLength>|35=<msgType>|49=<sender>|56=<target>|34=<seqNum>|52=<sendingTime>|...|10=<checkSum>|
| Rule | Definition |
|---|---|
BeginString(8) | Exactly FIX.4.4, and it must be the first field. |
BodyLength(9) | Must be the second field, immediately after the SOH that ends tag 8. Its value is the number of bytes starting after the SOH that terminates 9=..., up to and including the SOH that precedes 10=. Range accepted: 1 to 65536. |
CheckSum(10) | The sum of every byte before 10= (that is, the whole message including 8= and 9=), modulo 256, rendered as exactly three digits with leading zeros. It must sit precisely at the offset implied by BodyLength. |
SendingTime(52) | yyyyMMdd-HH:mm:ss.fff in UTC, millisecond precision, always three fractional digits. The venue stamps its own outbound messages in the same format. |
| Duplicate tags | For scalar lookups the venue keeps the first occurrence of a tag and ignores later ones. Repeating groups are preserved in arrival order. |
BeginString, a BodyLength that does not land on 10=, a checksum mismatch or an unterminated field all produce a Logout(5) carrying the reason in Text(58), followed by a socket close about 150 ms later. This is not punitive: FIX has no resynchronisation primitive, so once the byte offsets are wrong there is no defined way to find the next message boundary. Test your codec against a known-good message before you point it at the venue.
Reading the stream
TCP will hand your reader half a message, or two and a half messages, in a single read. The venue does not pad, align or delay frames to make one read equal one message. Your codec must append every read to a buffer and then loop: parse the length prefix, wait until BodyLength plus the seven trailer bytes (10=xxx plus SOH) are present, take that message, and try again on the remainder. Assuming one read equals one message is the single most common cause of a first-day conformance failure.
BodyLength values shown are correct as written, because | and SOH are both one byte. The CheckSum values are computed over the real SOH bytes, so recomputing them over the pipe rendering will not match.
Session layer
Logon (35=A)
Nothing but a Logon is accepted before authentication. Any other message type on a fresh connection produces a Logout with Text(58)=logon required and a close.
| Tag | Field | Required | Value and failure mode |
|---|---|---|---|
98 | EncryptMethod | yes | Must be 0. Confidentiality is provided by TLS; there is no FIX-level encryption. Anything else: Logout, EncryptMethod(98) must be 0: transport TLS only. |
108 | HeartBtInt | yes | Integer seconds, 5 to 60 inclusive. Outside that range or unparseable: Logout, HeartBtInt(108) must be 5..60 seconds. |
141 | ResetSeqNumFlag | yes | Must be Y on every logon, not only the first of the day. Absent or N: Logout, ResetSeqNumFlag(141)=Y is required on every logon. |
553 | Username | yes | Your API key. Unknown key or wrong secret: Logout, authentication failed (1001). The secret is compared in fixed time. |
554 | Password | yes | Your API secret, sent verbatim inside the TLS tunnel. FIX does not use the REST HMAC recipe. |
34 | MsgSeqNum | yes | Must be 1, which follows from 141=Y. Anything else: Logout, MsgSeqNum must be 1 on a 141=Y logon. |
49 | SenderCompID | yes | Your CompID. Recorded here and echoed as 56 on every venue message. Defaults to CLIENT if omitted, which you do not want in your logs. |
9003 | DropCopyOnly | no | Venue tag. Y makes this session observation-only. See Drop copy and credential scope below. |
9004 | CancelOnDisconnect | no | Venue tag. Y arms cancel-on-disconnect for this session (trading sessions only; ignored and forced to N on a drop-copy session). See Cancel on disconnect below. |
9005 | CoD grace | no | Venue tag. Grace window in milliseconds for 9004, clamped to 0 to 30000; absent means 0 (cancel immediately). |
Checks run in the order 98, 141, 108, credentials, 34, so the first failure names the real cause. On success the venue replies with a Logon of its own on outbound sequence 1, echoing 98, 108, 141=Y and the effective values of 9003 and 9004, so a drop-copy session that sent 9004=Y reads N back. Treat that echo as the authoritative statement of what the session can do.
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. The TLS handshake must complete within 5 seconds. Concurrent-connection ceilings are enforced at accept, with the surplus connection closed immediately and no FIX frame sent: 512 FIX connections in total, 64 connections that have not yet completed a Logon across all addresses, and 8 such per remote address. An orderly client never approaches these; bound your reconnect storms.Cancel on disconnect (tags 9004/9005)
An opt-in protection, armed PER SESSION and in memory only: re-arm on every logon. When an armed session disconnects, for any reason, 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, 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 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 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 and the grace window, that it is ONE command (every report carries the same engine sequence prefix in ExecID(17)), that it covers every open order of the member, and that it answers no request (revision 2.30 states it; the value stays USER_REQUESTED by design).
- Only a trading Logon disarms it. A FIX TRADING Logon within the grace window clears the pending timer. Timers are per transport: only a FIX Logon clears a FIX timer, a WebSocket reconnect does not and vice versa (the WebSocket arms its own via the
session.cancelOnDisconnectop). - Drop-copy sessions count for neither side. A drop-copy session (
9003=Yor a READ_ONLY credential) cannot arm the protection, its presence does not satisfy the liveness probe, and a drop-copy Logon does not disarm a pending timer: an always-connected drop-copy feed can never silently void the protection a trading session armed. A credential that cannot manage orders neither triggers cancels nor vetoes them. - Last-instant reconnects win. The venue re-checks liveness immediately before submitting the cancel-all, so a trading Logon landing 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, so treat it as advisory.
- A venue restart voids arming. Arming is in-memory by design: a restart or failover voids every pending timer and every armed session uniformly, nothing fires during or after it, and every re-logon starts unarmed.
- A leader change voids it too (clustered deployments, revision 2.25). 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.
- Orders only, never positions. The venue does not close, reduce or hedge any position on a disconnect.
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 recovery procedure below (the REST backfill of the order and execution families) 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-issue your MarketDataRequests, and run the recovery procedure below over the durable REST backfill. Neither the session nor its subscriptions nor its cancel-on-disconnect arming survive a restart. The WebSocket twin is close status 1001 with reason venue restart.
// client -> venue
8=FIX.4.4|9=130|35=A|49=MEMBER07|56=KRONOSX|34=1|52=20260801-09:14:03.117|98=0|108=30|141=Y|553=kx_live_7f3a9c21|554=s3cr3t-not-a-real-key|9003=N|10=172|
// venue -> client
8=FIX.4.4|9=83|35=A|49=KRONOSX|56=MEMBER07|34=1|52=20260801-09:14:03.129|98=0|108=30|141=Y|9003=N|10=217|
Leader change (clustered deployments, revision 2.25)
A single-node deployment is unchanged. On a clustered deployment one node, the leader, accepts orders, and the venue's entry address always leads to it. When the node your session is on hands the leadership to another node, every FIX session on it receives Logout(35=5) with Text(58)="leader change, reconnect" before the TCP close. The venue is not restarting: do what you do after a restart, only the reason on the wire differs. The clustered sandbox of revision 2.30 (three nodes behind sandbox.mtf.perpetuals.net, from the date venue operations announce) sends this Logout at every leader change there, including the leader hand-over venue operations stage at an agreed time for Conformance Test Plan case I-05.
- A
NewOrderSingle(35=D),OrderCancelRequest(35=F),OrderCancelReplaceRequest(35=G)orMarketDataRequest(35=V)received by a node that is not the leader is answered withLogout(35=5),Text(58)"leader change, reconnect", then the close; reconcile byClOrdID(11)on the leader before you resubmit. Session-level messages are answered as before. A drop-copy session's order entry stays refused withBusinessMessageReject(35=3),373=99on every node, as before. Reconnect to the leader and recover through the event stream. Before revision 2.25 such a session was answered9010=5001"engine busy, retry" (or a35=9"engine busy") for as long as the node's demotion was delayed. 9010=5001"engine busy, retry" on an order and the35=9"engine busy" reject 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 byClOrdID(11)on the leader before you resubmit.- A session the leader change logged out arms no cancel-on-disconnect, like a restart; your resting orders survive on the new leader.
- 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.
- (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 WebSocket5030and its leader hint are on the REST and WebSocket pages. 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 byClOrdID(11)on the leader, and never send it again under a newClOrdID(11). A cancel or a replace whose150=4or150=5you 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. - (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, 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 byClOrdID(11)on the leader before you resubmit, and never send it again under a newClOrdID(11). REST and, since revision 3.83 of the member reference, the WebSocket answer the same fault5000.
Heartbeats and timeouts
- Send
Heartbeat(0)whenever you have been silent for yourHeartBtInt. The venue does the same on its own outbound direction. - A
TestRequest(1)from either side must be answered with aHeartbeat(0)echoingTestReqID(112). The venue answers yours immediately, on the session read path. Always send112: the answering Heartbeat has to echo it, so a TestRequest without it leaves the venue nothing legal to send back and answers a session-levelReject(35=3)with373=1,371=112,372=1, and no Heartbeat. - After 1.2x
HeartBtIntof inbound silence the venue sends a TestRequest with a generatedTestReqID. After 2x it gives up:Logout(5)withText(58)=heartbeat timeout, then close. - Nothing follows a venue Logout (revision 2.26): until the disconnect the session acts on no message it receives, so a Logon that crosses the
logon timeoutLogout 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; recover them throughGET /v1/stream/events. - The liveness sweep runs once per second, so treat the thresholds as approximate to within a second and do not tune your own timers to sit exactly on them.
Sequence numbers and recovery
141=Y is mandatory every time), an inbound gap ends the session, and a ResendRequest(2) is answered with a GapFill, never with replayed traffic. Do not build a resend or persistence layer against this venue. Recovery is the REST cursor backfill described below.
| Situation | Venue behaviour |
|---|---|
Inbound MsgSeqNum higher than expected | Session over. Logout with Text(58) naming the observed and expected numbers and the backfill route, then close. Re-logon with 141=Y. |
Inbound MsgSeqNum lower than expected, with PossDupFlag(43)=Y | Silently discarded as an already-processed duplicate. The session continues. |
Inbound MsgSeqNum lower than expected, without 43=Y | Treated as a gap: Logout and close. |
You send ResendRequest(2) | The venue answers SequenceReset(4) taking the next number in its own outbound sequence, with GapFillFlag(123)=Y and NewSeqNo(36) set to the number after that. The venue's MsgSeqNum(34) is strictly increasing for the life of a session and never renumbers, so your own gap detection keeps working across this reply. No application message is ever replayed on the FIX session. |
You send SequenceReset(4) | Accepted when NewSeqNo(36) is greater than the expected inbound number, which advances the expectation. A lower value is ignored. |
You send Logout(5) | The venue replies with a Logout and closes. |
Unsupported MsgType | Reject(3) with RefSeqNum(45), RefMsgType(372), SessionRejectReason(373)=3 and an explanatory Text(58). The session continues. |
// gap detected: the Logout tells you exactly where to go
8=FIX.4.4|9=181|35=5|49=KRONOSX|56=MEMBER07|34=12|52=20260801-09:22:10.640|58=MsgSeqNum 14 where 12 expected: FIX resend is not supported, re-logon with 141=Y and recover via GET /v1/stream/events|10=149|
// answer to a ResendRequest: a GapFill, not a replay
8=FIX.4.4|9=71|35=4|49=KRONOSX|56=MEMBER07|34=18|52=20260801-09:21:00.500|123=Y|36=19|10=149|
Why not FIX resend
A FIX message store only recovers what one session missed, in that session's own numbering, and only as far back as the store is retained. It cannot help a member who trades on REST and watches on FIX, and it says nothing about venue-initiated activity that arrived while the process was down.
Kronos X instead journals every engine event once, durably, and serves it back over REST from a sequence cursor. ExecID(17) on a FIX ExecutionReport is {engineSeq}-{eventIndex}, the identical key that the REST backfill and the WebSocket private channels carry. One key reconciles all three transports, the ordering is total and engine-defined rather than per-session, and replaying it is idempotent by construction.
Recovery procedure after any disconnect, a venue restart Logout included:
- Reconnect, Logon with
141=Y, and re-issue anyMarketDataRequest: market data subscriptions are session state and do not survive. - Buffer inbound ExecutionReports. Do not apply them to your order state yet.
- Take your resume point (below; on a first start with nothing persisted,
GET /v1/stream/cursor). - Page
GET /v1/stream/events?afterCursor=<resume point>&channels=order,execution(or with nochannelsfilter) untilhasMoreis false and the store status isCOMPLETE. - Apply the backfill, then drain the buffer, discarding anything whose
ExecIDyou have already applied. - Resume live processing. Persist the
nextCursorof your last page and the cursor of the last event you process as you go, so the next recovery is short.
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.
Because this path is mandatory, a FIX-only integration is not viable. Provision REST credentials alongside your FIX session before go live. See REST API for the backfill endpoints.
Order entry (35=D)
| Tag | Field | Required | Meaning |
|---|---|---|---|
11 | ClOrdID | recommended | Your order reference. Maximum 36 characters, unique per member per 24 hours, and used as the idempotency key. The KX- prefix is reserved for venue-generated references and is rejected with 2020. Not enforced, matching the native API: an order sent without one is accepted and simply has no reference, so it has no duplicate protection, cannot be canceled or replaced by OrigClOrdID(41). Its ExecutionReports OMIT tag 11 entirely rather than carrying an empty one, so do not require tag 11 on an inbound report. Send one on every order. |
1 | Account | conditional | The end-client reporting identifier (clientUid). This is the one row where an ABSENT tag still yields a BUSINESS reject rather than a session-level one, because a market-maker member may legitimately omit it: missing gives 2010, except on a market-maker member, where the configured house identifier is applied. It is echoed on every engine report that has one, which is how you attribute a liquidation fill to a client, and it is omitted rather than sent empty on the 2010 rejection that says it was missing. 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)), 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. |
55 | Symbol | yes | Venue symbol, for example BTC-USD-PERP. ABSENT or empty is refused BY NAME with Reject(35=3), see the box below the table. PRESENT but unknown still gives 2001. |
54 | Side | yes | 1 buy, 2 sell. ABSENT or empty is refused BY NAME with Reject(35=3). Any other PRESENT value gives 2013. |
38 | OrderQty | yes | Quantity as an exact decimal string in base-asset units, aligned to the instrument qtyStep. ABSENT or empty is refused BY NAME with Reject(35=3). Misalignment gives 2002 (a value that is not a decimal numeral at all, an exponent, a blank, letters, gives 2013); an order below the instrument minNotional gives 2028 in tag 9010. |
40 | OrdType | yes | 1 Market, 2 Limit, 3 StopMarket, 4 StopLimit. ABSENT or empty is refused BY NAME with Reject(35=3). Any other PRESENT value gives 2004. |
44 | Price | conditional | Required for Limit. For StopLimit it is the limit price applied after the trigger fires. Must align to tickSize. |
99 | StopPx | conditional | Trigger price, required for OrdType 3 and 4. Absent gives 2013. |
59 | TimeInForce | no | 1 GTC (the default when the tag is absent), 3 IOC, 4 FOK, 6 GTD. Any other value gives 2013, including 59=0: the venue does not offer DAY, so the FIX default value is rejected rather than accepted. Send 59 explicitly. |
126 | ExpireTime | conditional | GTD only. UTC, accepted as yyyyMMdd-HH:mm:ss or yyyyMMdd-HH:mm:ss.fff. Since rules of engagement revision 2.31 the order never trades at or after it, measured against the time the venue sequences each command: an order, a replace or a cancel (each answered 102=1 with 9010 = 2014), a trigger or a call auction's uncross that reaches it from that instant expires it (150=C), 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 API waived for them, as for item (3) of Member API revision 3.73, revision 3.81 and revision 3.82. The matching engine applies them from its replay-contract line for Member API 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. |
18 | ExecInst | no | A space-delimited list. Containing 6 (participate don't initiate) makes the order post-only, which overrides 59. Post-only on anything other than a Limit gives 2013. |
9001 | ReduceOnly | no | Venue tag. Y marks the order reduce-only. Perpetuals only; on a spot instrument it gives 2016. |
9002 | TriggerSource | no | Venue tag for stop types: LAST (default), MARK or INDEX. |
9016 | PositionSide | no | Venue tag, hedge mode: LONG or SHORT 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 (netting applies inside the bucket, so a buy tagged SHORT reduces the short bucket). 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. Member-level margin is unaffected; see the REST page for the bucket model. |
9012 | TraderID | conditional | Venue tag. The venue-issued Trader ID of the Authorised User entering the order, the FIX carrier for reg.traderId on the native API. Required once your member has a registered Authorised User; absent or not a registered active id gives 2021. See the note below. |
9021 | TradingCapacity | no | Venue tag: reg.tradingCapacity, DEAL (own account), MTCH (matched principal) or AOTC (any other capacity). Recorded verbatim into the same per-order regulatory record as the REST/WS reg object; see the reg-tags note below. Under ENFORCE-stage capacity enforcement (staged per member) the capacity becomes required and a violation answers the business reject 2033 in 9010, decided before the engine. |
528 | OrderCapacity | no | Standard FIX capacity, read for GATED members only (stage WARN or ENFORCE): at OFF, or with no licensing profile, the tag stays entirely unparsed. For a gated member P and G map to DEAL, R to MTCH, A to AOTC; tag 9021 remains canonical. At ENFORCE an unmappable value answers 2033 and nothing is journaled, and when both tags are present they must agree; at WARN the same conditions are counted venue-side and ignored. The mapped value is recorded exactly as if 9021 had carried it. |
9022 | ClientIdType | no | Venue tag: reg.clientIdType, LEI, NATIONAL_ID or INTERNAL. |
9023 | ClientId | no | Venue tag: reg.clientId, the identifier that 9022 names. |
9024 | InvestmentDecisionId | no | Venue tag: reg.investmentDecisionId, the algo or person responsible for the investment decision (Art. 26(3) MiFIR). |
9025 | ExecutionDecisionId | no | Venue tag: reg.executionDecisionId, the algo or person responsible for the execution decision. |
9026 | DEA | no | Venue tag: reg.dea, direct electronic access flag. Y means true; any other value reads as N, the 9001/9004 convention. |
9027 | LiquidityProvision | no | Venue tag: reg.liquidityProvision, market-making activity flag. Y means true, same convention as 9026. |
9029 | RiskReducing | no | Venue tag: reg.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). |
60 | TransactTime | no | Accepted and ignored. The venue timestamps from the engine clock. |
Reject(35=3) with SessionRejectReason(373)=1, RefTagID(371) naming 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 of any kind follows, and the session stays up. A 35=3 carries no ClOrdID(11) and no ExecID(17): correlate it by RefSeqNum(45), and do not leave your client waiting for a 150=8 after one.
conditional means the tag depends on another field's value or on your member type, and its absence keeps the BUSINESS reject and the venue code shown in its row.
no means an absent tag selects the documented default in its row.
A field that is PRESENT with a bad value is unchanged in every case: it keeps the business reject it has always had and the same venue code in tag
9010. And a value you actually SENT is never replaced by one the venue prefers, which is why 59=0 is rejected rather than read as GTC.
The venue never emits a zero-length value for any tag on any message. A gateway reject used to echo
ClOrdID(11), Symbol(55) and Side(54) "as you sent them", so a tag you omitted came back as 11=, 55= or 54= with nothing after the equals sign, which is not valid FIX. Now a tag the venue cannot fill is omitted, or carries a documented sentinel where FIX requires it to be present (KX-NONE on an OrderCancelReject), or the message is refused by name. That holds on the STANDARD HEADER too, which no body-level rule can reach: SenderCompID(49) and TargetCompID(56) are required on every FIX message, so an absent or empty 49, and anything the venue must answer before your Logon arrives, comes back as TargetCompID(56)=CLIENT rather than an empty 56=. 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.
reg object. 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. Tag 9012 continues to carry reg.traderId unchanged, with its 2021 enforcement unchanged.
Trader ID (
9012) enforcement is staged per member. A member with no registered Authorised Users is not gated at all, so an existing integration that never sends the tag keeps working exactly as before. From the moment venue operations registers your member's first Authorised User, every order must carry a registered, active Trader ID or it is rejected with 9010=2021 and Text(58)=reg.traderId missing or not a registered Authorised User. Ids are issued by the venue in the form TR-{memberId}-{8 hex characters}, for example TR-12-9F3A21BC. The rule is identical on REST and the WebSocket, where the same value travels as reg.traderId, because all three transports build the order through one enforcement point. The refusal happens in the gateway, before a command is built, so it is a pre-engine rejection: correlate it by ClOrdID(11) and expect no Account(1).
Order semantics
- Trigger direction is derived from side, never sent: a buy stop triggers at or above
StopPx, a sell stop at or below. That is conventional stop semantics and matches the native API. - Prefer
9002=MARKfor stop-losses.LASTcan be moved by a single small print, which is exactly the condition under which a protective stop should not fire. - MARKET with FOK is all or none inside the protection collar: either a complete set of fill reports or one Canceled with
Text(58)=FOK_UNFILLABLE. Never a partial. MARKET with GTD or post-only is rejected outright. - Market orders are margined at the collar price, not at the touch. An empty book gives 2007.
- Take-profit order types are not reachable over FIX.
OrdType(40)accepts only 1 to 4, soTAKE_PROFIT_MARKETandTAKE_PROFIT_LIMITmust be placed on REST or the WebSocket. Their reports still arrive on your FIX session, because the session is a full-member drop copy. - There are no iceberg, trailing-stop or pegged orders anywhere on the venue.
MaxFloor(111)is not supported.
Reusing a ClOrdID
ClOrdID(11) is unique per member for 24 hours and is the venue's idempotency key. Reusing one while the venue still holds it never creates a second order, and the venue answers the NewOrderSingle either way, on the session that sent it:
The venue holds a reference for 24 hours, 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 below. 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.
| You send | Answer | Carries |
|---|---|---|
| A reference used before, with a different payload | 150=8, 39=8 | 9010=2009, 103=99, 58=duplicate clOrdId with different payload, 37=0 |
| A reference used before, with the byte-identical payload | 150=I (order status) | 9015=Y, the original OrderID(37), that order's current 39, 151 and 14 |
The 150=I restatement is the FIX shape of the REST and WebSocket duplicate: true response: the same idempotency path, reported in the form FIX has for it. It is a status snapshot rather than a lifecycle event, so like the 2009 rejection it is never written to the durable event stream and never appears in a backfill, and its ExecID(17) is a gateway-local dup-<n> value outside the cursor space.
150=I as a second working order, or that leaves the 2009 unhandled and keeps the request outstanding, will drift from the venue. Generate unique references, and reserve resends of an identical payload for the one case they are meant for: recovering a request whose answer you lost.
55, 54, 38 or 40 is refused by name with a session-level Reject(35=3), as described above the table. The JSON order surface rejects an unrecognised member with code 2017 precisely so that a field you believe you set can never be silently dropped. The FIX codec cannot offer that guarantee for tags it does not read: it reads the tags in the table above and ignores everything else. So sending MaxFloor(111) does not produce an iceberg and does not produce an error, it simply does nothing. Validate your outbound tag set in your own mapper. Similarly, an ExpireTime(126) the venue cannot parse is treated as absent, after which a GTD order is rejected by the engine with 2013 rather than at the gateway.
Annotated example
A post-only, reduce-only buy of 25 BTC (base-asset units, as every OrderQty is) at 61250.50, on member CompID MEMBER07 for end client CLIENT-0007:
8=FIX.4.4|9=176|35=D|49=MEMBER07|56=KRONOSX|34=2|52=20260801-09:15:22.481|11=mm-4f21-000318|1=CLIENT-0007|55=BTC-USD-PERP|54=1|38=25|40=2|44=61250.50|59=1|18=6|9001=Y|60=20260801-09:15:22.481|10=118|
8=FIX.4.4 BeginString, always first, always exactly this
9=176 bytes from after this field's SOH to the SOH before 10=
35=D NewOrderSingle
49=MEMBER07 your CompID (venue echoes it as 56)
56=KRONOSX venue CompID, constant
34=2 second message of the session (Logon was 1)
52=20260801-09:15:22.481 SendingTime, UTC, always .fff
11=mm-4f21-000318 ClOrdID: your key, unique per 24 h, not KX- prefixed
1=CLIENT-0007 Account: clientUid, mandatory, echoed on every report
55=BTC-USD-PERP Symbol
54=1 Side: buy
38=25 OrderQty, qtyStep-aligned decimal string
40=2 OrdType: limit
44=61250.50 Price, tick-aligned decimal string
59=1 TimeInForce: GTC
18=6 ExecInst contains 6: post-only, overrides 59
9001=Y venue tag: reduce-only
60=20260801-09:15:22.481 TransactTime, accepted and ignored
10=118 CheckSum over every preceding byte, mod 256, 3 digits
Rejections before the engine
Rate limiting, the order-to-trade gate and tag mapping are enforced in the gateway, before a command is ever built. Those rejections come back synchronously as an ExecutionReport with 150=8, 39=8 and OrderID(37)=0. A message MISSING a required tag is refused one step earlier still, at the session layer, with a Reject(35=3): that verdict is reached before any of these gateway checks run, so a 35=D that both omits a required tag and would have been rate limited answers the 35=3. Such a message was never accepted for processing, so its Reject is the whole answer to it, it is not one of the answers counted by the reply-completeness rule below, and it consumes no order-budget token and is not counted in your RTS 9 order-to-trade ratio. Anything answered with a business reject WAS accepted for processing and is charged and counted as usual. The same session-layer refusal applies to a Side(54) outside the FIX 4.4 enumeration (1 to 9, A to G), with SessionRejectReason(373)=5 instead of 1: Side is the one tag the venue echoes onto its own answer that FIX both marks required on an ExecutionReport and restricts to a value set, so echoing 54=X would produce a report your engine refuses. A Side inside the enumeration that the venue does not trade, 54=5 for example, keeps its unchanged 9010=2013. Engine rejections arrive through the post-durable report chain as the order's terminal report, 150=8, 39=8 and the code in 9010 (an order the engine accepted and then rejected inside the same command, a post-only order that would cross, first receives its 150=0 accept). That REST reports the same verdict inside an HTTP 200 as data.status REJECTED and the WebSocket in its ack is deliberate: each transport uses its own place for a verdict, and the convention stays.
8=FIX.4.4|9=204|35=8|49=KRONOSX|56=MEMBER07|34=8|52=20260801-09:15:52.311|37=0|11=mm-4f21-000502|17=rej-1794350118-4|150=8|39=8|55=FOO-USD-PERP|54=1|151=0|14=0|103=99|9010=2001|58=unknown symbol|60=20260801-09:15:52.311|10=166|
ExecID(17) is a synthetic rej-<n> value that is not in the cursor space, and it carries no Account(1). Correlate it by ClOrdID(11), never by ExecID, and do not feed it to the same dedupe table you use for durable executions. There is a THIRD answer shape that is neither an ExecutionReport nor durable: the session-level Reject(35=3) for a required tag your message did not carry. It has no ClOrdID(11) and no ExecID(17) at all, so its correlation key is RefSeqNum(45), and no ExecutionReport follows it. Engine-level rejections are the opposite: they are journaled, carry a real {engineSeq}-{eventIndex} ExecID and Account(1), and do appear in the backfill. The two answers to a reused ClOrdID sit in between: the command does reach the engine, but its verdict is on the reference rather than on an order, so it is not in the member event stream and carries a gateway-local ExecID like a pre-engine rejection. Correlate those by ClOrdID(11) too.
Cancel and cancel/replace
| Tag | 35=F cancel | 35=G replace |
|---|---|---|
37 OrderID | Preferred identifier. Used when present and non-zero. | Required. There is no fallback. |
41 OrigClOrdID | Fallback when 37 is absent: resolved against your own ClOrdID index for this member. | Echoed on a reject, but not used to identify the order. |
11 ClOrdID | Your reference for the cancel request itself. Echoed on a reject. | Same. |
44 Price | n/a | New limit price. Omit to leave the price unchanged. |
38 OrderQty | n/a | New total order quantity. Omit to leave it unchanged. |
35=G identifies the order only by OrderID(37). Sending a replace with just OrigClOrdID(41) is refused with CxlRejReason(102)=1 and text OrderID(37) required, even though the same identifier works perfectly well on a cancel. Second, OrderQty(38) on a replace is the new total quantity of the order, including quantity already filled, not the new remaining quantity. A value at or below CumQty is rejected with 2013. Reducing quantity keeps queue priority; changing price loses it.
A PRESENT but invalid Price(44) or OrderQty(38) on a replace is rejected, never treated as "not supplied" and never partially applied: a 44 off the instrument tick grid, or a 38 off the qtyStep grid, answers OrderCancelReject(9) with 434=2 and 2002 in 9010, exactly the 2002 the same input earns on REST and WebSocket; 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 exactly as REST and WebSocket do. Either way the working order keeps its original terms. Validate against the instrument grid from GET /v1/instruments before you send.
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 API section 6f); the request never reaches the engine and the working order keeps its terms. The sandbox never answers it.
OrderCancelReject (35=9)
| Tag | Field | Content |
|---|---|---|
37 | OrderID | Echo of the 37 you sent, or 0 if you sent none. Sending the tag EMPTY counts as not sending it, so 37= gets the 0 sentinel; FIX 4.4 requires this tag on a 35=9, so it is never omitted and never zero length. |
11 | ClOrdID | Echo of your request reference, or the sentinel KX-NONE if you sent none. FIX 4.4 requires this tag on a 35=9, so it can be neither omitted nor left empty, and KX- is the venue-reserved namespace (refused with 2020 on order entry), so the sentinel can never collide with a real reference. Do not match it against your order table. |
41 | OrigClOrdID | Echo of your original reference, or the sentinel KX-NONE if you sent none, for the same reason as 11. A replace never carries 41, so a rejected 35=G always reports the sentinel here. |
39 | OrdStatus | Always 8 on this message. |
102 | CxlRejReason | 1 when the order is unknown (never existed, or already terminal, or belongs to another member). 0 for every other reason, with the detail in Text(58). |
434 | CxlRejResponseTo | 1 in response to a cancel, 2 in response to a replace. |
58 | Text | The venue reason, for example unknown order, rate limit, retry after 2ms, order-to-trade ratio block, engine busy, order not found. engine busy: the leader's intake is full; a node that no longer leads sends the leader-change Logout instead (revision 2.25). |
9010 | Venue code | Present when the reject maps to a venue registry code: replace-validation rejects (2002 for a misaligned 44/38, 2013 for one that is not a decimal numeral), on production 2034 when your member has no licensing profile (revision 2.20, the fail-closed paragraph above) and engine rejects (for example 2028 below the minimum notional, 2013 for a replace at or below CumQty). The same numeric registry as ExecutionReport rejects. Refusals identified only at the session layer (rate limit, ratio block, engine busy, an order id the session cannot resolve) answer with Text(58) and CxlRejReason(102) only. |
8=FIX.4.4|9=133|35=9|49=KRONOSX|56=MEMBER07|34=9|52=20260801-09:16:02.880|37=0|11=mm-4f21-000401|41=mm-4f21-000106|39=8|434=1|102=1|58=unknown order|10=026|
35=9 is emitted only on failure. A successful cancel arrives as an ExecutionReport with 150=4, and a successful replace as one with 150=5, through the post-durable fan-out rather than as a synchronous answer. A state machine that waits for a reply to the cancel message itself will hang on the happy path. Drive order state exclusively from ExecutionReports and treat 35=9 as a negative acknowledgement only.
ExecutionReport (35=8)
Your session is a full-member drop copy. Reports cover every order of your member id regardless of which transport originated it: this FIX session, another FIX session, REST, the WebSocket, or the venue itself through liquidation and ADL. You do not need a second connection to observe your own flow.
| Tag | Field | Present | Content |
|---|---|---|---|
37 | OrderID | always | The venue order id, or 0 when the order never reached the book. |
11 | ClOrdID | when you sent one | Your reference. On venue-initiated closes it is KX-LQ-{orderId} for a liquidation order or KX-ADL-{tradeId} for an auto-deleveraging close. ABSENT, never empty, on every report for an order you placed without a reference. |
1 | Account | fan-out reports | The clientUid, carried on every report the engine produced, including engine rejections. Absent on pre-engine gateway rejections, on the one engine rejection where the venue has no identifier to carry, the 2010 that says it was missing, and on the activation report 150=L, which names the order by 37 and 11 only (its accept carries the clientUid). |
17 | ExecID | always | {engineSeq}-{eventIndex}: the durable event identity, totally ordered, identical on REST and the WebSocket. Your dedupe key. |
150 | ExecType | always | See the combination table below. |
39 | OrdStatus | always | See the combination table below. |
55 | Symbol | always | Venue symbol. Empty on a rejection where the instrument could not be resolved. |
54 | Side | always | 1 buy, 2 sell, from the perspective of your order. On a maker fill this is your resting side, not the aggressor's. |
151 | LeavesQty | always | The order's open quantity after the event this report describes: on a fill, what that fill left open; 0 on a cancel, an expiry and a rejection (revision 2.29). See CumQty and LeavesQty below. |
14 | CumQty | always | The order's cumulative filled quantity after that event: on a fill, this fill included and no later one. Every fill of the order counts, whichever transport placed it and whenever your session logged on (revision 2.29). |
60 | TransactTime | always | Engine event time, yyyyMMdd-HH:mm:ss.fff UTC, millisecond precision: the sequencing time of the event the report describes (the cancel's time on a 150=4, the fill's on a 150=F), the instant the native order frames carry in acceptedTs, never the time the order was first accepted. |
32 | LastQty | trades | Quantity of this fill only. |
31 | LastPx | trades | Price of this fill only, exact decimal string. |
99 | StopPx | activations | The trigger price of a stop or take-profit order, on its activation report 150=L (revision 2.26). |
58 | Text | cancels, expiries, rejects | The cancel reason enum, or the reject message. |
103 | OrdRejReason | rejects | Always 99 (other). FIX's reason enumeration is far coarser than the venue's, so the standard tag is deliberately not overloaded. |
9010 | venue code | rejects | The precise numeric venue code, for example 2001. The full registry is on Reference. Branch on this, never on 103 or on 58 text. |
9011 | RefSource | trades | MEMBER or VENUE. Key your unsolicited-close handling on this. |
9015 | Duplicate | restatements | Y on the 150=I answer to a reused ClOrdID with an identical payload. Absent on every other report, including the 150=I that answers a 5005 on a reference you did not reuse. |
9016 | PositionSide | hedge-mode orders | LONG or SHORT, the order's hedge-mode position side, 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. |
9017 | QueueQtyAhead | rested placements | 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, pending-trigger accepts and every reject, keeping pre-queue reports byte-identical. A snapshot at the moment of rest, immediately stale: the venue never updates it. |
9018 | QueueOrdersAhead | rested placements | 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). |
ExecType and OrdStatus combinations
| Event | 150 | 39 | Also carries |
|---|---|---|---|
| Order accepted, resting or pending trigger | 0 | 0 | 151 = full quantity, 14 = 0 |
| Conditional order activated (its trigger fired; revision 2.26) | L | 0 | 99 = trigger price, 151 = full quantity, 14 = 0; no 1, no 9016. A transition from pending trigger to working, never a fill. Before revision 2.26 nothing was sent here. A conditional the venue cancels at its trigger, before it activates, gets the plain 150=4 instead, with REDUCE_ONLY_VIOLATION or FUNDING_CATCH_UP in 58. |
| Fill that leaves the order working | F | 1 | 32, 31, 9011; 151 = what this fill left open (> 0), 14 = filled so far, this fill included. Also the fill of an IOC or market order whose remainder is then canceled. |
| Fill that completes the order | F | 2 | 32, 31, 9011; 151 = 0, 14 = the order's whole quantity. Only the last fill of the order. A fill with 37=0 (RFQ, barrier, yield take, ADL, reversal) is always this row, with 14 = 32. |
| Canceled | 4 | 4 | 58 = cancel reason; 151 = 0; 14 = the order's filled quantity, so an IOC remainder carries what the IOC filled |
| Expired (GTD or venue expiry) | C | C | 58 = GTD_EXPIRED; 151 = 0; 14 = the order's filled quantity |
| Replaced | 5 | 0 or 1 | 39 = 1 when the order already has fills, otherwise 0. It is not 5. 14 = the order's filled quantity, 151 = its new open quantity; also on a reduce-only order the venue trimmed. |
| Rejected | 8 | 8 | 103 = 99, 9010 = venue code, 58 = message; 151 = 0; 14 = the order's filled quantity, 0 unless the order filled before the rejection (a partly filled post-only order that a 35=G repriced to cross, 9010=2005, after its 150=5) |
Restatement of an order a reused ClOrdID maps to | I | that order's status | 9015=Y; 37 = the original order id. See Reusing a ClOrdID. |
| Restatement of an order whose command hit a 5005 | I | that order's status | 9010=5005, no 9015; 37 = the real order id. See Code 5005. |
150=I is a restatement, never a lifecycle transition. Both rows above report the CURRENT state of an order at the moment the report was built; neither closes it. Exactly one report with a LIFECYCLE ExecType(150), that is 150 other than I, ever carries a terminal OrdStatus for a given order, and it always comes from the fan-out: the fill that completes it, the cancel, the expiry or the engine rejection. A 150=I restatement CAN repeat a terminal OrdStatus after that report, because the order it restates may already be terminal (a canceled order behind a reused ClOrdID, an IOC remainder canceled inside the same command). So write the assertion as "at most one terminal 39 with 150 other than I", never as "at most one terminal 39", and never let a 150=I move your order state machine: a client that books it as a terminal state will abandon a live order.
// partial fill: 15 of 25 done at 61250.50, 10 still working
8=FIX.4.4|9=213|35=8|49=KRONOSX|56=MEMBER07|34=7|52=20260801-09:15:41.204|37=880412|11=mm-4f21-000318|1=CLIENT-0007|17=4193887-2|150=F|39=1|55=BTC-USD-PERP|54=1|151=10|14=15|60=20260801-09:15:41.203|32=15|31=61250.50|9011=MEMBER|10=177|
Durability, dedupe and CumQty
No ExecutionReport is emitted before the journal made the event durable. Reports are constructed on the engine thread as events are produced, held in a pending list, and released to session queues only after the group commit that covers them. A report you have received therefore describes state that survives a venue restart. Pre-engine gateway rejections are the sole exception, and they are exceptional precisely because no state was created.
ExecID(17) together with OrderID(37), not on ExecID alone. ExecID identifies the durable engine event, and a trade event has two sides. When both sides belong to your member, which is permitted between two different end clients, your session receives two ExecutionReports carrying the same ExecID with different OrderID(37), ClOrdID(11) and Side(54). A dedupe table keyed on ExecID alone silently discards one leg of every same-member cross, and your position will drift.
CumQty(14) and LeavesQty(151) are the order's, after the event the report describes (revision 2.29). A fill report states the order after that fill: 14 includes this fill and no later one, 151 is what this fill left open, and 39 is 2 only on the fill that completes the order. An IOC or market order that partly fills therefore receives 150=F with 39=1 and the unfilled rest in 151, then 150=4 with 151=0 and its true 14; an order that sweeps several resting orders receives 39=1 on every fill but the one that completes it. 14 counts every fill of the order, whichever transport placed it and whenever your session logged on, so it is the same on every session of your member and after a re-logon, and a 150=I restatement carries the same 151 and 14 as the order's latest report. Take an order's cumulative quantity from 14, or sum 32 over its fills; never rebuild it as OrderQty minus LeavesQty, which books a canceled IOC remainder as filled. Before revision 2.29 a fill carried the order's state after the whole command that produced it, 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, 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 14 equal to its own 32; 39=1 with the final 151 on every fill of a sweep whose rest stayed on the book); 14 counted only the fills the session had itself been sent, from its first report of the order, and a rejection always carried 14=0, also after the fills of a post-only order repriced to cross. These values 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 API waived for them, as for item (3) of Member API revision 3.73 and revision 3.81.
Code 5005: the venue could not finish
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. That is the same contract REST and the WebSocket publish, where 5005 arrives as data.rejectCode and the rest of data describes what the command did, so an orderId there is a real order. FIX says the same thing in FIX terms, and always in exactly one further report. Which row you are in is decided by what the command DID, read top to bottom:
| What the command achieved | What you receive |
|---|---|
The engine rejected the order, before OR after accepting it (a post-only order that would cross, and the REJECT_TAKER self-trade policy, are accepted and then rejected inside one command) | The engine's own rejection, with the real reject code in 9010. Nothing further: 5005 is not restated over a reason that is more specific than it. |
| The reference was already taken by a different payload | The ordinary 2009 rejection, 150=8, 39=8, 9010=2009. The registry refused the request, so nothing was created and 2009 is the reason you need. |
| An order exists: resting, partially filled, filled, or closed by this same command | The ordinary fan-out reports for it, then one 150=I on the session that sent the order, carrying that order's real OrderID(37), OrdStatus(39), LeavesQty(151) and CumQty(14), plus 9010=5005 and the reason in Text(58). It repeats the order's state at that moment, so the 39 is terminal when the command also closed the order, and the report that CLOSED it is still the fan-out's. 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 | A single 150=8, 39=8 with 9010=5005. |
A cancel (35=F) or replace (35=G) that took effect and then failed | The ordinary 150=4 or 150=5 and nothing else. An OrderCancelReject(9) means REFUSED, so you receive one carrying 9010=5005 only when the cancel or replace did NOT take effect. |
GET /v1/orders/open and GET /v1/orders/{orderId}, then cancel or manage it. Report the incident to venue operations; every 5005 is a defect report, not routine flow control.
Cancel reasons in Text(58)
One of a closed enumeration: 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: the venue cancelled the order on a DENIED access decision or an admission flip), FUNDING_CATCH_UP (revision 3.70, venue-initiated: a conditional 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). USER_REQUESTED covers a cancel of yours on any transport, a member-wide cancel-all, the member portal's kill switch and the cancel on disconnect you armed; no ExecRestatementReason(378) tells them apart. INSTRUMENT_HALT also marks the cancels of a delisting, which is how a re-listing begins: a tickSize or qtyStep change is never made in place (the REST page, Instruments), and FIX carries no instrument reference or status message, so re-read GET /v1/instruments after an announced date and on any unexpected 9010=2001, 2002 or 3006.
Venue-initiated closes
Liquidation and ADL reach your book as ordinary trade reports on orders you never sent. They carry 9011=VENUE and a ClOrdID in the reserved KX- namespace. That namespace cannot collide with yours: any member ClOrdID beginning KX- is rejected at submission with 2020, so the two spaces are disjoint by construction. Branch on 9011 rather than on the shape of the reference, because 9011 is the field whose meaning is defined; the prefix is an implementation of it. Your order state machine must tolerate reports for order ids it has never seen. See Trading rules for liquidation and ADL mechanics.
Drop copy and credential scope
Every FIX session is already a full-member drop copy, so a separate connection is not needed merely to observe your own flow. A dedicated drop-copy session exists for back-office, risk and surveillance consumers that must not be able to trade. There are two ways to get one and they compose: the effective state is drop-copy if either applies.
| Level | How | Scope | Can it be undone by the client? |
|---|---|---|---|
| Credential recommended | Ask venue operations for a READ_ONLY key. | Every transport. The same key is refused on REST and WebSocket writes with code 1005. | No. It is a property of the key. 9003=N on Logon does not lift it. |
| Session | Send 9003=Y on Logon with an ordinary trading credential. | This TCP session only. | Yes, by reconnecting without the tag. Use it for convenience, not as a control. |
The Logon reply echoes the effective value in 9003, so a client can always tell which mode it got. On a drop-copy session, ExecutionReports and market data flow normally, and D, F and G are refused at the session layer with a Reject(3) naming the message type, without ever reaching the engine.
8=FIX.4.4|9=143|35=3|49=KRONOSX|56=RISK01|34=3|52=20260801-09:20:44.010|45=2|372=D|373=99|58=drop-copy session: order entry is disabled, use a trading session|10=225|
Reject(3), not an ExecutionReport carrying code 1005. Code 1005 is what the same read-only key produces on a REST or WebSocket write. Handle both if your risk system shares an error taxonomy across transports. A member may hold up to 5 keys of each scope at the same time (multi-key credentials): issuing a key never revokes another, and revocation is explicit, per key, and immediate, ending live FIX and WebSocket sessions authenticated with the revoked key while the member's other keys keep working. Note also that every session draws on the member's rate limits, which are keyed per member and shared across all of the member's keys, drop-copy sessions included.
Market data (35=V, 35=W, 35=X)
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. A session may hold up to 32 instrument subscriptions at once.
| Tag | Field | Required | Value |
|---|---|---|---|
262 | MDReqID | required | Your subscription reference. Echoed on every W, every X and every reject. Key your consumer on it. |
263 | SubscriptionRequestType | required | 0 snapshot once, 1 snapshot then updates, 2 unsubscribe. Missing or anything else is rejected with 281=4. |
55 | Symbol | required | Venue symbol. Missing or unknown is rejected with 281=0. |
146 | NoRelatedSym | optional, default 1 | Must be 1. One symbol per request; send several requests for several instruments. |
265 | MDUpdateType | optional, default 0 | 0 full refresh, 1 incremental. Anything else is rejected with 281=6. |
264 | MarketDepth | optional, default 5 | Levels per side. 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. |
267 + 269 | NoMDEntryTypes / MDEntryType | optional, default all three | Repeating group selecting what you want: 0 bid, 1 offer, 2 trade. Absent means all three. Any other value is rejected with 281=8. The venue reads the actual 269 occurrences rather than trusting the 267 count. |
Body field order is not significant. The venue parses tag=value pairs into
a map and reads every 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. Repeating-group entries
are read in arrival order, which is the only place body order matters, and
269 is the only group the venue reads inbound.
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, 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 read as
0 would give you one W and then permanent silence,
indistinguishable from a subscription on an instrument that never trades. The
optional fields are the deliberate contrast: their absence selects the documented
default in the same column and can never change whether the feed exists, so a
request carrying only 262, 263, 146 and
55 works exactly as it always has.
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.
263and55:MarketDataRequestReject(35=Y)withMDReqRejReason(281)4and0respectively, and aText(58)naming the tag. Nothing is subscribed.264present and outside 1 to 25, or not an integer:MarketDataRequestReject(35=Y)withMDReqRejReason(281)=5and aText(58)naming the tag and the servable range. Nothing is subscribed. An ABSENT264is not this case and is unaffected.262: a session-levelReject(35=3)withSessionRejectReason(373)=1(required tag missing),RefTagID(371)=262andRefMsgType(372)=V. A35=Ycannot answer this one, because35=Ymust itself carry theMDReqIDthat is missing. The session stays up.
This 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.
55, so a 263=2 that omits 262 looks like it
should work, but it is refused before it reaches that step and the 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 | Behaviour |
|---|---|
0 | One MarketDataSnapshotFullRefresh(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. |
1 | The W snapshot is returned, then updates in the shape 265 selects, 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. Released on the same post-durable flush as ExecutionReports. |
2 | The subscription is dropped and no reply is sent: silence is the confirmation. A message already queued when the disable arrives may still be delivered, so treat the first quiet period as the boundary rather than the last message. 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 or absent | Conflated full refresh. A complete fresh W after every commit batch that changed the book or the last trade. One refresh per batch, never a partial delta. This is the behaviour of revision 2.1 and earlier: a client that does not send tag 265 sees no change at all. A subscription that asked for 269=2 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. |
Re-issuing a request for an instrument you already hold replaces that subscription and is never refused, so recovery works even at the subscription limit.
The body carries MDReqID(262), Symbol(55), then NoMDEntries(268) followed by that many repetitions of MDEntryType(269), MDEntryPx(270) and MDEntrySize(271). Entries are ordered bids first, then offers, then the last trade. All prices and sizes are exact decimal strings. Entry types you did not ask for are simply absent.
269 | Entry | 270 | 271 |
|---|---|---|---|
0 | Bid level, best first | price | aggregate size |
1 | Offer level, best first | price | aggregate size |
2 | Last trade | price | omitted |
268 is the true entry count: bid levels plus offer levels plus one when the instrument has traded. A book with no resting orders and no prints yields a valid W with 268=0, so your parser must handle an empty group. Because the last-trade entry has no 271, drive the repeating-group parser off 269 rather than assuming a fixed three-tag stride.
The body carries MDReqID(262), NoMDEntries(268), then that many entries. Each entry begins with MDUpdateAction(279), which is the group delimiter.
| Tag | Book entries (269 = 0 or 1) | Trade entries (269 = 2) |
|---|---|---|
279 MDUpdateAction | 0 New, 1 Change, 2 Delete | always 0 New: the tape is append-only |
269 MDEntryType | 0 bid, 1 offer | 2 |
278 MDEntryID | not sent | the venue trade id, the same id GET /v1/trades and the WebSocket trades channel carry |
55 Symbol | the instrument | the instrument |
270 MDEntryPx | the price level | the trade price |
271 MDEntrySize | the level's new aggregate size, 0 on a Delete | the traded quantity |
272 / 273 | not sent | UTC date YYYYMMDD and time HH:MM:SS.sss of the execution |
290 MDEntryPositionNo | 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 or REVERSAL |
Inside one X the order is fixed: 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, that 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.
MDEntryPx(270), never 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 sanity checks, not an identifier. MDEntrySize on a Change is the level's new aggregate size and not a delta, so applying the same Change twice is harmless.
// request: snapshot plus incremental updates, 5 levels a side, book and tape
8=FIX.4.4|9=135|35=V|49=MEMBER07|56=KRONOSX|34=3|52=20260803-09:15:04.988|262=md-btc-1|263=1|265=1|264=5|146=1|55=BTC-USD-PERP|267=3|269=0|269=1|269=2|10=107|
// snapshot: 2 bids, 2 offers, 1 last trade
8=FIX.4.4|9=217|35=W|49=KRONOSX|56=MEMBER07|34=4|52=20260803-09:15:05.002|262=md-btc-1|55=BTC-USD-PERP|268=5|269=0|270=61250.00|271=42|269=0|270=61249.50|271=118|269=1|270=61250.50|271=37|269=1|270=61251.00|271=90|269=2|270=61250.00|10=128|
// increment: one print, then the resulting book state, bid entries before offer
// entries (the best bid grew, the best offer was partly consumed by that print)
8=FIX.4.4|9=293|35=X|49=KRONOSX|56=MEMBER07|34=5|52=20260803-09:15:07.114|262=md-btc-1|268=3|279=0|269=2|278=880412|55=BTC-USD-PERP|270=61250.50|271=12|272=20260803|273=09:15:07.109|9013=1|9014=NORMAL|279=1|269=0|55=BTC-USD-PERP|270=61250.00|271=50|290=1|279=1|269=1|55=BTC-USD-PERP|270=61250.50|271=25|290=1|10=067|
// re-baseline after this session fell behind: discard the book and rebuild from this W.
// The 269=2 entry is the last trade price, which every W carries for a subscriber
// that asked for trades, so 268 counts it.
8=FIX.4.4|9=164|35=W|49=KRONOSX|56=MEMBER07|34=6|52=20260803-09:15:09.771|262=md-btc-1|55=BTC-USD-PERP|268=3|269=0|270=61250.00|271=50|269=1|270=61251.00|271=90|269=2|270=61250.50|10=235|
// reject
8=FIX.4.4|9=108|35=Y|49=KRONOSX|56=MEMBER07|34=7|52=20260803-09:15:31.777|262=md-xyz-9|281=0|58=unknown symbol FOO-USD-PERP|10=054|
What the incremental feed guarantees
- The snapshot and the increments are consistent. The venue registers the subscription and builds the
Win 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 theW, then apply every followingXin 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 theMDEntryType(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. - 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
W. Treat anyWarriving mid-stream as a re-baseline: discard your book for that symbol and rebuild from it. - Trade entries are best effort. While a session is behind, prints for that instrument are dropped rather than queued, and the re-baseline
Wrestores the book but not the missed prints. Reconcile againstGET /v1/tradesif you need a gap-free tape: 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=5rather 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 never have to infer it.
Recovery
Re-issue the MarketDataRequest. The venue answers with a fresh W and re-seeds the incremental baseline from it, so everything that follows is consistent with the new snapshot. That is the recovery procedure for every case: after a reconnect, after a detected sequence gap, after an unexpected re-baseline W, or any time you doubt your book. There is no market-data resend and none is needed.
281 | MarketDataRequestReject(35=Y) reason |
|---|---|
0 | Missing Symbol(55), unknown symbol, or an unsupported request shape such as 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) |
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. 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, and 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 rather than left with an unanswered request. Only the refresh
stream (35=W, 35=X) is best effort.
V after re-logon.
Rate limits and controls
| Message | Order bucket | Read bucket | RTS 9 order-to-trade |
|---|---|---|---|
D NewOrderSingle | consumes 1 | no | counted; blocked with 1004 when over |
G CancelReplace | consumes 1 | no | counted; blocked with 1004 when over |
F CancelRequest | no | no | counted, never blocked |
V MarketDataRequest | no | no | no |
any of the above refused at the SESSION layer (35=3) | no | no | no |
- There are two independent full-capacity token buckets per member, shared across the member's keys: 6000 reads/min and a 40000/min order budget. Order placements, batch entries (REST/WS only), and amendments all consume the order budget; cancels are never throttled. Order entry does not consume a read token. Because both refill to full capacity, 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 API section 4); a burst above it answers a report with
Text(58)carryingretryAfterMsunder one second. - Batch order entry (
POST /v1/orders/batch, WSorders.batch) is not offered over FIX: submitNewOrderSinglemessages in a loop instead. Nor are batch amendment (POST /v1/orders/batch/amend, WSorders.batchAmend) and batch cancellation (POST /v1/orders/batch/cancel, WSorders.batchCancel), Member API revision 3.74: FIX 4.4 has no message that amends or cancels a list of orders by id, so sendOrderCancelReplaceRequest(35=G)orOrderCancelRequest(35=F)per order, or use those routes on a native connection alongside. - The buckets refill continuously, not at a window boundary, and a rate-limit reject tells you the wait:
Text(58)readsrate limit, retry after 2ms. Sleep that long and resend. Treating the limit as a fixed window costs you up to a minute of dead time for a wait that is usually a couple of milliseconds. After ten5005answers of yours within 60 seconds the reject readsrate limit: repeated engine faults (5005), retry after 41250ms, and that wait can be up to 60 seconds (Member API section 4, revision 3.79): sleep it, and never resend the commands that answered5005unchanged. A reduce-onlyD(9001=Y) and a cancel are not refused by that budget; a replace (G) is. - FIX order entry is metered at member granularity: all FIX sessions of one member draw on a single order bucket, accounted separately from the REST and WebSocket bucket of the key you logged on with. Do not size a FIX burst on the assumption that it shares tokens with your REST traffic, in either direction.
- Cancels are always admitted so that a member can always flatten its resting exposure, including while the order-to-trade regime is blocking new orders. They still count toward the ratio, so a cancel storm will eventually block your new orders with 1004.
- A rate-limit or ratio refusal on
Dis an ExecutionReport150=8with9010=1003or9010=1004. OnGit is anOrderCancelReject(9)with102=0and434=2.
What FIX does not carry
FIX is order entry, execution reporting and market data. Everything else stays on the native API and needs a REST credential:
Conformance checklist
Run this against the sandbox before requesting production access. Each item maps to a behaviour above that has broken a real integration.
- Round-trip your codec against a known-good message: parse it, rebuild it byte for byte, and confirm
BodyLengthandCheckSummatch. Then corrupt one checksum digit and assert the venue closes the connection. - Feed your parser a message split across three TCP reads, and two messages in one read. Assert both are decoded correctly.
- Logon with
141=Yand valid credentials; assert the Logon echo carries98,108,141=Yand9003. Then logon with a bad secret and assert a Logout naming 1001. - Omit
141, and separately send34other than 1; assert the venue refuses each logon with the matching reason. - Answer a venue
TestRequest(1)with aHeartbeat(0)echoingTestReqID(112). Then stay silent past 2xHeartBtIntand assert the venue sendsLogout(5)withText(58)=heartbeat timeoutand disconnects. - Place a STOP LIMIT (
40=4) with9002=MARKand a99the mark will cross; assert one150=L,39=0with your37,11and99when it does, and that your order state moves from pending trigger to working without booking a fill. - Place a far-from-market LIMIT GTC; assert
150=0with yourClOrdID(11), yourAccount(1)and151equal to the full quantity. Cancel it; assert an ExecutionReport with150=4and58=USER_REQUESTED, and that your state machine did not wait for a35=9. - Cancel an unknown order; assert
OrderCancelReject(9)with102=1and434=1. - Replace with only
OrigClOrdID(41); assert it is refused. Replace correctly byOrderID(37)with a reduced total quantity; assert150=5with39=0or39=1, not39=5. - Cross against a second session; assert
150=Fon both sides with correct31,32,151,14, the correct per-side54, and9011=MEMBER, each side's151,14and39being its own order's after that fill. - Rest a SELL of 1, then send an IOC BUY of 2 at its price: assert
150=Fwith39=1,151=1,14=1, then150=4with39=4,151=0,14=1, and that only the second carries a terminal39. Rest two SELLs of 1 at two prices and send a BUY of 2 through both: assert39=1,151=1,14=1, then39=2,151=0,14=2. Partly fill a resting order, log a second session on and fill it again: assert that session's14counts both fills. Fill 1 of a post-only BUY of 3 (18=6) and reprice it by a35=Gacross a resting SELL: assert150=5with151=2,14=1, then150=8with151=0,14=1. - Cross two of your own end clients (different
Account(1)); assert you receive two reports sharing oneExecID(17)and that your dedupe key kept both. - Send an unknown symbol; assert
150=8with37=0,103=99and9010=2001, and that your code branched on9010. - Send
OrdType(40)=5; assert9010=2004and that your mapper routes take-profit orders to REST instead. - Resend a working order's
ClOrdID(11)with a changedPrice(44); assert150=8with9010=2009. Resend it with the byte-identical payload; assert150=Iwith9015=Yand the originalOrderID(37). Assert your client booked no second order in either case, and that everyDyou sent has exactly one answer outstanding at zero. - Prove your order state machine treats
150=Ias a restatement and never as a terminal state, and that a report carrying9010does not by itself close an order. On a 5005 the39,151and14of a150=Idescribe an order you still own and still have to manage. Then cancel that order and resend itsClOrdIDwith the identical payload: the restatement now carries the terminal39=4of the canceled order, after the150=4that closed it. Assert your client books it as neither a second cancel nor a resurrection, and that any "one terminal39per order" check you run excludes150=I. - Send a deliberate
MsgSeqNumgap; assert a Logout naming the backfill route, then re-logon cleanly with141=Y. - Kill the connection mid-flow. Recover using the buffer-then-backfill procedure, and prove the merged log is gap-free and duplicate-free by
ExecIDordering. Assert the cumulative quantity you rebuilt for each order equals the14of its latest report and the sum of32over its fills, neverOrderQtyminusLeavesQty, which overstates an order whose remainder was canceled. MarketDataRequestwith263=0: assert oneWwith bid and offer entries. With263=1and no tag265: assert an unsolicited fullWafter a book change. With263=2: assert refreshes stop. Unknown symbol: assert35=Ywith281=0. Assert your group parser handles268=0and the last-trade entry with no271.263=1with265=1: assert aW, then rest an order at a new price from a second session and assert anXwith279=0at that price; add size at the same price and assert279=1with the summed size; remove it all and assert279=2with271=0. Rebuild the book from snapshot plus increments and reconcile it againstGET /v1/orderbook?symbol=while the market is quiet.- Subscribe with
267=1,269=2and cross a trade: assert anXtrade entry whose278matches theGET /v1/tradesid for the same execution, and whose270and271match its price and quantity. Assert your book code ignored the entry and your tape code recorded it. - Send
265=9: assert35=Ywith281=6. Send269=4: assert281=8and that noWfollows, so nothing was partially subscribed. Send264=0, then264=50, then264=abc: assert281=5and noWon each. - Required fields on
35=V. Omit263: assert281=4naming the tag, noW, and no stream after a later book change. Omit55: assert281=0naming the tag. Omit262: assert a session-levelReject(3)with373=1and371=262, and a session that stays up. Then send the same request with264and265in the opposite order, and again with264,265and267omitted entirely: assert both behave exactly like the fully specified request. Keep the ABSENT case and the PRESENT unservable case separate in your matrix: they are the two halves of the rule and they answer differently. - Disable with an unservable depth: subscribe with
263=1, then send263=2with55and a264=0echoed from the original request. Assert it is accepted silently and the stream stops. - Required fields on
35=D. Omit55, then54, then38, then40, one at a time: assert a session-levelReject(35=3)with373=1, the matching371,372=D, that NO ExecutionReport follows, and that the session stays up. Assert your client clears its outstanding-request counter without waiting for a150=8, and correlates the reject byRefSeqNum(45). Then send a PRESENT but unknown55and assert the unchanged150=8with9010=2001. - Send a NewOrderSingle with no
ClOrdID(11): assert your parser accepts an ExecutionReport with no tag 11. Send a35=FbyOrderID(37)alone against an unknown order: assert your parser accepts11=KX-NONEand41=KX-NONEwithout matching them to a real order. Send aTestRequest(1)with no112: assertReject(35=3)with371=112and no Heartbeat. - Send
54=X: assertReject(35=3)with373=5and371=54. Send54=5: assert the unchanged150=8with9010=2013. Send a35=Fwith37=,41=and11=all EMPTY: assert anOrderCancelReject(9)carrying37=0,11=KX-NONEand41=KX-NONE, all three PRESENT. Log on withSenderCompID(49)empty, and separately send anything before your Logon: assert both answers carryTargetCompID(56)=CLIENT, never an empty56. - Log on a
READ_ONLYcredential, or send9003=Y; assert the Logon echoes9003=Y, thatDis refused withReject(3), and that ExecutionReports for orders placed on your trading session still arrive on it.