Skip to content

CME Stp FIX And Ilink2 Tag Value Encoding

cme-stp-fix-and-ilink2-tag-value-encodingsource

Use when a system speaks tag=value FIX to CME Group: an STP FIX 4.4 trade-capture session, a replay over archived iLink 2 flow, or a conformance harness. Not for live order entry, which moved to iLink 3 binary; see cme-globex-futures-api-integration.

Version
2.0.0
Reading
7 min
Hands off to
6
Handed off from
4
License
Apache-2.0
CoversCME STP FIX 4.4CME iLink 2 (decommissioned)CME iLink 3 (SBE/FIXP)

When to Use

Use this skill when a system speaks tag=value FIX to or about CME Group: a CME STP FIX 4.4 trade-capture session, a replay or simulator over archived iLink 2 flow, a conformance harness, or a port of legacy CME FIX order-entry code onto iLink 3.

It covers the four things hand-rolled CME FIX code gets wrong:

  1. Emitting a "FIX message" with no BeginString, BodyLength, SendingTime or CheckSum — a string no counterparty can frame, let alone accept.
  2. Re-requesting a resend on every out-of-sequence message, turning one gap into a resend storm and a disconnect.
  3. Putting a value in Tag 8000 that CME does not define (only O and N exist).
  4. Rounding a price to a fixed number of decimals, which mis-prices any instrument quoted more finely.

When NOT to Use

  • For live CME order entry. CME order entry no longer accepts tag=value FIX. iLink 2 — the FIX 4.2-based order-entry protocol these tags come from — was decommissioned on the Market Segment Gateway on 2021-03-28 and on the Convenience Gateway on 2025-04-06. Live order entry is iLink 3: FIX Simple Binary Encoding over the FIXP session layer. Nothing this module emits will reach CME Globex. Use to_ilink3_order_fields() for the field mapping and see cme-globex-futures-api-integration.
  • As a FIX session engine. This module frames messages and tracks sequence numbers. It does not own a socket, heartbeats, TestRequest liveness, logon negotiation, resend replay from a message store, or sequence persistence across restarts. For a production session, use a maintained engine (QuickFIX, OnixS) and keep this module for the CME-specific field semantics. See fix-protocol-session-management-across-venues.
  • For iLink 3 sequencing. FIXP has no MsgSeqNum(34), no ResendRequest(35=2) and no SequenceReset(35=4). Its recovery is Retransmit Request / NotApplied, and the state machine here does not transfer. See matching-engine-throttle-and-message-gapping-detection.
  • For CME market data. MDP 3.0 is SBE multicast, not FIX. See exchange-multicast-feed-handling.

Prerequisites

  • A session identity for the venue in question: SenderCompID / TargetCompID, plus an operator ID registered under CME Rule 576 (Tag 50 in tag=value FIX; Tag 5392 SenderID, 20 bytes, in iLink 3).
  • Self-Match Prevention IDs registered in the CME Firm Administrator Dashboard (FADB). An SMP ID must be numeric to survive a port to iLink 3, where Tag 2362 is a uInt64 — a string that is legal in tag=value FIX is not representable there.
  • Python 3.10+. Standard library only.

Workflow

  1. Decide which protocol you are actually on before writing a byte. If the destination is CME order entry, stop: that is iLink 3 SBE, and a tag=value encoder is the wrong tool. If it is CME STP, it is FIX 4.4 — set begin_string="FIX.4.4".

  2. Build messages through build_fix_message() and send them verbatim. It emits 8=…|9=…|35=… first and 10=… last, with SOH delimiters and a UTC SendingTime(52) at millisecond precision. BodyLength and CheckSum are computed over exactly those bytes: re-serialising the message downstream invalidates both.

  3. Populate the CME order tags, with the values CME defines. 1028=N for automated flow, Y for a human-entered order — required on CME order messages since June 2011. 50 carries the Rule 576 operator ID. 7928 is the registered SMP ID, and 8000 is either O (cancel oldest — the resting order) or N (cancel newest — the aggressor). Omitting Tag 8000 is legal; CME's default is to cancel the resting order. create_new_order_single() rejects any other value rather than serialising it.

  4. Pass prices as decimal strings or Decimal. They are serialised without rounding and without scientific notation. A fixed-precision format is the failure mode here: four decimals turns a 1.05125 FX future into 1.0513, a full tick away.

  5. Drive inbound sequencing through process_inbound_message() and act on what it returns.

    • A ResendRequest(35=2) means a newly detected gap — send it. Messages that arrive while that request is outstanding are the peer working through the range; the engine deliberately does not re-request on each one.
    • A Logout(35=5) means the peer sent a sequence number below the expected one without PossDupFlag(43)=Y. The FIX session layer treats that as unrecoverable: send the Logout, then drop the transport connection. Do not keep trading on it.
    • None means in sequence, an admissible duplicate, or recovery already in flight — three different things. Gate business processing on last_inbound_accepted, which is true only for an in-sequence message. Applying a duplicate or an out-of-sequence ExecutionReport replays or reorders fills.
  6. Persist both sequence counters. They live in memory here. A process restart that resets them to 1 mid-session desynchronises the session; the reset policy (and when ResetSeqNumFlag(141)=Y is appropriate) is venue-specific — take it from the venue's session spec, not from this module's defaults.

  7. When porting to iLink 3, run to_ilink3_order_fields() first. It fails loudly on the two fields that do not survive a naive port: a non-numeric SMP ID (Tag 2362 is a uInt64) and an operator ID longer than the 20-byte SenderID field.

Full procedure: see references/workflows.md. Protocol status, tags, values and sources: see references/standards.md. Printable pre-flight checklist: see assets/checklist.md.

Common Pitfalls

  • Treating a delimited tag list as a FIX message. Without BeginString, BodyLength and CheckSum there is nothing for the peer to frame on, and a |-delimited string is a log rendering, not the wire format. The wire delimiter is SOH (\x01).
  • Re-requesting a resend on every out-of-sequence message. During recovery the peer is replaying the range you asked for, and those messages are legitimately ahead of your expectation. Issuing a fresh ResendRequest for each one floods the session and ends in a disconnect — the opposite of recovery.
  • Ignoring a too-low sequence number. Logging it and moving on silently discards everything the peer sends afterwards, including ExecutionReports: the position on the book stops matching the position in the process. The session layer requires a Logout and a disconnect, except for SequenceReset with GapFillFlag(123)=N, which is honoured whatever its own sequence number.
  • Inventing Tag 8000 values. CME defines exactly O (CancelOldest) and N (CancelNewest). R, B, CANCEL_RESTING and similar inventions are rejected by the venue — and O means "cancel the oldest, i.e. resting, order", not "cancel outgoing".
  • Assuming a missing Tag 1028 produces a session-level reject. CME answers an invalid or missing ManualOrderIndicator with a Business Reject (35=j, BusinessRejectReason(380)=100), not 35=3. Code that only inspects 35=3 will treat the order as live.
  • Rounding prices to a fixed precision. f"{price:.4f}" is silently wrong for every instrument quoted below four decimals, and a float price carries binary representation artifacts into the wire format.
  • Reusing an iLink 2 SMP ID on iLink 3. Tag 7928 was an alphanumeric string; Tag 2362 is a uInt64. SMP_888 has no iLink 3 representation, and the operator ID moves from Tag 50 to Tag 5392 at the same time.
  • Defaulting the account. A hard-coded placeholder account routes real risk to whatever that string happens to resolve to. Tag 1 is required here for that reason.
  • Consuming a sequence number for a message that is never sent. The counter advances when the message is built, so messages must be transmitted in build order and a build that is discarded leaves a permanent hole the peer will ask you to fill.

Verification

  • Confirm the message starts 8=FIX.4.x<SOH>9=<n><SOH>35=… and ends 10=<nnn><SOH>, and that 9= equals the byte count between the end of the 9= field and the start of 10=, derived independently rather than from the module's own arithmetic.
  • Confirm the checksum equals the sum of every preceding byte modulo 256, zero-padded to three digits.
  • Confirm a price of 1.05125 serialises unrounded, Decimal("0.000005") does not become 5E-6, and NaN/Infinity raise.
  • Confirm smp_instruction="R" raises, "O" and "N" are accepted, and None omits Tag 8000 while leaving Tag 7928 in place.
  • Confirm a value containing SOH or = is rejected rather than forging a field.
  • Confirm one gap yields exactly one ResendRequest, that sequences 6, 7 and 8 arriving during recovery yield none, and that recovery clears once the range is filled.
  • Confirm a too-low sequence number without PossDupFlag returns a Logout(35=5) and marks the session terminated, while the same message with 43=Y is discarded quietly.
  • Confirm last_inbound_accepted is true only for the in-sequence message — not for the duplicate and not for the message that arrived mid-recovery, both of which also return None.
  • Confirm to_ilink3_order_fields() maps 50→5392, 7928→2362 (as an integer) and 1028 to 0/1, and raises on a non-numeric SMP ID.
  • Run python -m unittest discover -s skills/cme-stp-fix-and-ilink2-tag-value-encoding/scripts and confirm a 100% pass rate.
  • Against a venue test session only: send one built message unmodified through the real transport and confirm it is accepted. A framing bug that unit tests cannot see is one where the transport rewrites the bytes.

Verify it, from the repository root

python -m unittest discover -s skills/cme-stp-fix-and-ilink2-tag-value-encoding/scripts

Hands off to 6

Skills this document names, usually in When NOT to Use, as the owner of a case it excludes.

Handed off from 4

Skills that name this one as the place a case belongs. The reverse edges of the graph.