FIX (Financial Information eXchange) protocol was designed in 1992 and looks like it. Tag-value pairs delimited by the ASCII SOH character (0x01). Fields in a prescribed order that parsers nonetheless receive out of order. A specification document larger than most novels. And yet: FIX is the universal language of electronic trading. Every exchange, every prime broker, every ECN speaks it. You will encounter it.

The Wire Format

A FIX message is a sequence of tag=value pairs separated by SOH (displayed here as |):

8=FIX.4.4|9=178|35=D|49=CLIENT1|56=BROKER1|34=1|52=20120807-09:31:02.123|
11=ORD001|55=EUR/USD|54=1|38=1000000|40=2|44=1.08450|59=0|10=123|

Breaking this down:

TagField nameValueMeaning
8BeginStringFIX.4.4Protocol version
9BodyLength178Byte count of body (validated)
35MsgTypeDNew Order Single
49SenderCompIDCLIENT1Sending firm identifier
56TargetCompIDBROKER1Receiving firm identifier
34MsgSeqNum1Sequence number (must be gapless)
52SendingTime2012…UTC timestamp
11ClsOrdIDORD001Client-assigned order ID
55SymbolEUR/USDInstrument
54Side11=Buy, 2=Sell
38OrderQty1000000Quantity
40OrdType22=Limit
44Price1.08450Limit price
59TimeInForce00=Day
10CheckSum123Sum of all bytes mod 256

The 9 (BodyLength) and 10 (CheckSum) fields are how FIX validates message integrity — not for security, but for detecting transmission corruption. Tag 8 must be first; tag 10 must be last. Everything else is technically flexible but in practice ordered by convention.

Message Types You’ll Live With

MsgTypeNameDirectionPurpose
DNew Order SingleClient → BrokerSubmit a new order
FOrder Cancel RequestClient → BrokerCancel an existing order
GOrder Cancel/ReplaceClient → BrokerModify an existing order
8Execution ReportBroker → ClientFill, partial fill, rejection
9Order Cancel RejectBroker → ClientCancel request rejected
VMarket Data RequestClient → BrokerSubscribe to prices
WMarket Data SnapshotBroker → ClientFull order book / quote
XMarket Data Inc. RefreshBroker → ClientIncremental price update
0HeartbeatBothSession keepalive (30s default)
ALogonBothSession establishment
5LogoutBothClean session termination

For an FX broker, the most important ones are D/F/G (order lifecycle on the client side) and 8 (execution reports, which carry fills and rejections).

The Session Layer: Where Things Go Wrong

FIX has a session layer (sequence numbers, heartbeats, logon/logout) sitting below the application layer (orders, quotes). The session layer is the source of most operational pain.

Sequence numbers are monotonically increasing per session, per direction. If you receive message 7 and the next one is 9, you have a sequence gap and must send a ResendRequest. The counterparty should replay messages 8–9. If they can’t (the message was never sent), they send a GapFill. If your application doesn’t handle this correctly, you drop messages silently — which in a trading context means missing fills or cancellations.

Normal flow:                Gap scenario:

→ msg seq 5                 → msg seq 5
→ msg seq 6                 → msg seq 6
→ msg seq 7                 → msg seq 8  ← gap! expected 7
→ msg seq 8
                            ← ResendRequest (7,7)
                            → msg seq 7  (resent)
                            → msg seq 9  (continue)

Session reset happens at start of day (configurable) and invalidates the sequence counter. Botching the reset — starting a new session before the counterparty is ready, or not persisting sequence numbers across process restarts — causes immediate Logon rejection. FIX sessions that haven’t been properly reset are one of the most common morning war stories at trading firms.

Parsing Considerations

Tag-value parsing sounds trivial. At 200,000 messages/second with a 200-byte average message, the naive implementation allocates a HashMap<Integer, String> per message — 200,000 objects plus their key/value strings. Minor GC fires constantly.

The production approach: a pre-allocated parser that writes into a fixed array indexed by tag number (most FIX tags are under 1000). No allocation, O(1) field lookup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Naive — allocates on every parse
public Map<Integer, String> parse(byte[] msg) {
    Map<Integer, String> fields = new HashMap<>();
    // ... split on SOH, parse tag=value pairs
    return fields;
}

// Zero-allocation — reuse the array
public class FIXMessage {
    private final String[] values = new String[1024]; // indexed by tag

    public void parse(byte[] msg, int offset, int length) {
        // in-place parse, write directly into values[]
        // no allocation
    }

    public String get(int tag) { return values[tag]; }
    public void reset() { Arrays.fill(values, null); } // reuse for next message
}

The further optimisation: don’t convert tag values to String at all. Keep them as byte[] slices and only convert to the target type (int, double, String) when accessed. A fill price parsed as a double from bytes is faster than Double.parseDouble(string).

QuickFIX/J vs Rolling Your Own

QuickFIX/J is the standard Java FIX library. It handles session management, sequence numbers, message store and replay, and generates strongly-typed message classes from the FIX specification XML. For getting connected quickly, it’s the right starting point.

Its limitations for latency-sensitive work:

  • The generated message classes allocate heavily
  • Session management is in separate threads with locking
  • The message store (for replay) is file or database backed, adding I/O to the critical path

We ran QuickFIX/J for our low-volume, low-latency-requirement connections (voice broker give-up, operations systems) and a bespoke zero-allocation parser for the high-volume market data feeds and order routing. The bespoke parser was ~4x faster at peak throughput but required maintaining compatibility with each counterparty’s dialect of FIX, which is more work than it sounds — FIX implementations have vendor-specific quirks that the spec doesn’t cover.

The FIX Dialect Problem

The specification exists. Counterparty implementations do not read it faithfully. Common divergences:

  • Extra required fields: some brokers require custom tags (6000+) that are not in the spec
  • Different enum values: OrdType=P for “Pegged” is not in FIX 4.4 but some venues use it
  • Timestamp precision: the spec says milliseconds; some counterparties send microseconds or nothing
  • Encoding: ASCII by default, but some newer implementations send binary FIX (FAST, SBE) on the same socket

In practice, each counterparty gets its own session configuration and sometimes its own message parser. FIX interoperability is a spectrum, not a guarantee.