Quantitative Finance · Book 13 · Technology

Low-Latency Software

Low-Latency Software · Technology

15Protocols I: FIX

An order sent this morning from a fund to its broker most likely travelled as a line of text: 35=D for a new order, 55=ESZ6 for the instrument, 54=1 for a buy, 38=5 for the quantity, each pair ended by a control character. The format was first written in 1992 so that Fidelity Investments and Salomon Brothers could exchange equity trading information electronically, and it became the common language of orders between the buy side, brokers and many venues: the Financial Information eXchange protocol. It is old, verbose, slow to parse naively, and everywhere. This chapter builds a FIX engine: the codec that reads and writes the text without copying it, the session layer that keeps two firms’ views of a conversation identical through disconnections, and the measurements that say what the protocol costs.

15.1 Tag-value encoding and the message

Definition 15.1 (FIX protocol, tag-value encoding)

The FIX protocol is a standard for messages about trading (orders, executions, quotes, market data, allocations) between firms, maintained by the FIX Trading Community, with a session layer that delivers them in order and without loss. In its tag-value encoding a message is a sequence of fields tag=value, each ended by the byte SOH (value 1), where the tag is a number from the protocol’s dictionary and the value is text.

Three fields open every message, in a fixed order: BeginString (tag 8, the protocol version), BodyLength (9) and MsgType (35). One closes it: CheckSum (10), whose field and trailing SOH mark the end of the message. Between them come the rest of the standard header (the sender’s and target’s identifiers 49 and 56, the sequence number 34, the sending time 52 in UTC) and the body. BodyLength counts the bytes from the start of the MsgType field up to, not including, the CheckSum field, delimiters included; CheckSum is the sum of every byte before it modulo 256, written as three digits. Figure 15.1 takes apart the example that the protocol’s usual description works through, a logon whose body is 65 bytes long and whose checksum is 062; the build’s tests reproduce both numbers.

A FIX 4.2 logon as a byte string, with | standing for the SOH delimiter. BodyLength and CheckSum are computed over the spans shown; a receiver recomputes both and rejects the message if either differs.
Figure 15.1. A FIX 4.2 logon as a byte string, with | standing for the SOH delimiter. BodyLength and CheckSum are computed over the spans shown; a receiver recomputes both and rejects the message if either differs.

The checksum is a weak check, and knowing how weak tells a reader what it is for.

Proposition 15.2 (What the FIX checksum detects)

Changing a single byte of a message always changes its checksum. Swapping two bytes, or two fields, never does; nor does any set of changes whose values sum to a multiple of 256.

Proof. Changing byte bb into b′≠bb' \ne b changes the sum by b′−bb' - b, with 0<∣b′−b∣<2560 < |b' - b| < 256, which is not a multiple of 256. A sum does not depend on the order of its terms, so a permutation leaves it unchanged; and changes δ1,…,δk\delta_1, \dots, \delta_k leave the sum modulo 256 unchanged exactly when ∑jδj≡0(mod256)\sum_j \delta_j \equiv 0 \pmod{256}. ∎

The transport underneath, TCP, already carries its own checksums on every segment. The FIX checksum and body length are therefore less a defence against corruption on the wire than a check on the framing: a program that has lost its place in a byte stream, or a message cut short or stitched together by a faulty gateway, is caught at the next message. Framing itself uses the first two fields: once 8= and 9= are read, the message’s full length is known, the body plus the seven bytes of 10=nnn and its delimiter.

15.2 The session layer: sequence numbers, heartbeats, resend and gap fill

Definition 15.3 (FIX session, heartbeat, resend request, gap fill)

A FIX session is the bidirectional conversation between two parties identified by their sender and target identifiers, from a logon to a logout, in which each side numbers the messages it sends 1, 2, 3, … and keeps them so that it can send them again. A heartbeat is a message sent when a side has sent nothing for an agreed interval, so that silence can be told from a dead connection. A resend request asks the other side to send again a range of its messages, after a gap in their sequence numbers has been detected. A gap fill is a sequence-reset message, sent in answer to a resend request, that stands for a run of administrative messages that will not be sent again, moving the receiver’s expected sequence number past them.

Sequence numbers (One Quant Book 1, chapter 28, for market data) are the session layer’s whole mechanism. Each side keeps two counters: the number of its next outgoing message and the number it expects next from the other side. A message with the expected number is processed and the counter moves on. A higher number means messages were lost: the receiver sends a ResendRequest (MsgType 2) from the expected number to “infinity” (EndSeqNo 0), and holds the later messages until the gap is filled. A lower number is either a duplicate, marked with PossDupFlag (43) and ignored, or a fatal error: the two sides no longer agree, and the receiver logs out. The answer to a resend request contains the application messages again, marked PossDupFlag with their original sending time (122), and replaces administrative messages (heartbeats, test requests, resend requests themselves) by SequenceReset messages with GapFillFlag (123) set: a heartbeat from half an hour ago means nothing now. Figure 15.2 draws the golden conversation of the build around its gap, and Listing 15.1 is the same conversation as the engine traces it.

The build’s golden conversation around its gap. Two of the broker’s messages are lost; the next one reveals the gap, which a resend request closes: the application message comes again marked as a possible duplicate, the heartbeat is replaced by a gap fill, and the message held back is delivered in order. Later the broker asks for ours, and our administrative message is gap-filled in turn.
Figure 15.2. The build’s golden conversation around its gap. Two of the broker’s messages are lost; the next one reveals the gap, which a resend request closes: the application message comes again marked as a possible duplicate, the heartbeat is replaced by a gap fill, and the message held back is delivered in order. Later the broker asks for ours, and our administrative message is gap-filled in turn.
14:30:31.000 BROKER -> Heartbeat(0) seq 3
14:31:01.500 BROKER -> ExecutionReport(8) seq 6  39=2
14:31:01.500          gap expected=4 received=6
14:31:01.500 FIRM   -> ResendRequest(2) seq 3  7=4 16=0
14:31:01.510 BROKER -> ExecutionReport(8) seq 4  43=Y 39=1
14:31:01.510          deliver 4 8
14:31:01.510 BROKER -> SequenceReset(4) seq 5  43=Y 123=Y 36=6
14:31:01.510          gap fill to 6
14:31:01.510          deliver 6 8
14:31:01.510          gap closed next_in=7
14:31:01.511 BROKER -> ExecutionReport(8) seq 6  43=Y 39=2
14:31:01.511          duplicate 6 ignored
14:31:10.000 FIRM   -> OrderCancelRequest(F) seq 4
14:31:10.100 BROKER -> ResendRequest(2) seq 7  7=3 16=0
14:31:10.100 FIRM   -> SequenceReset(4) seq 3  43=Y 123=Y 36=4
14:31:10.100 FIRM   -> OrderCancelRequest(F) seq 4  43=Y
Listing 15.1. The golden conversation around the gap, as ll_fix.short prints the engine’s trace (the full trace is expected_trace.txt in the build’s data). code/low-latency/15-protocols-i-fix/python/trace_short.txt

Heartbeats make silence meaningful. At logon both sides agree an interval, HeartBtInt (108), here 30 seconds. A side that has sent nothing for that long sends a Heartbeat (0). A side that has heard nothing for somewhat longer (the build waits 1.2 intervals) sends a TestRequest (1) with an identifier that the answering heartbeat must carry, and if that answer does not come within another interval it declares the connection dead. The detection time is the price of a long interval: with 30 seconds, a silently dead connection is noticed after about 66, during which the counterparty’s messages go nowhere and must be resent.

        const std::int64_t seq = m.seq();
        if (seq > next_in) {
            if (t == "2") resend(m, now);
            queue_[seq] = std::string(p, n);
            if (!resend_asked_) {
                resend_asked_ = true;
                log(now, "gap expected=" + std::to_string(next_in) + " received=" + std::to_string(seq));
                emit("2", field(7, std::to_string(next_in)) + field(16, "0"), now);
            }
            return Error::ok;
        }
        if (seq < next_in) {
            if (m.get(43) == "Y") {
                log(now, "duplicate " + std::to_string(seq) + " ignored");
                return Error::ok;
            }
            log(now, "seq " + std::to_string(seq) + " too low, expected " + std::to_string(next_in));
            state = "logout_sent";
            emit("5", field(58, "MsgSeqNum too low, expecting " + std::to_string(next_in) + " but received " +
                                    std::to_string(seq)), now);
            disconnect(now, "sequence too low");
            return Error::ok;
        }
Listing 15.2. The C++ engine’s sequence check: a gap asks for a resend and holds the message; a duplicate is ignored; a number too low ends the session. code/firm/fixengine/cpp/firm_fixengine.hpp

15.3 The application layer: orders and execution reports

On top of the session, each application message carries one business event. A NewOrderSingle (35=D) carries the firm’s order identifier ClOrdID (11), the instrument (55), the side (54), the quantity (38), the order type (40) and, for a limit order, the price (44); an OrderCancelRequest (35=F) names the order to cancel by its original identifier (41) and gives the request its own new one. The answer to each is an execution report (35=8), which the specification uses “to confirm the receipt of an order”, to confirm changes, to relay status and fills and to reject orders; One Quant Book 10, chapter 26, describes it from the venue’s side. Its OrdStatus (39) walks the order lifecycle of One Quant Book 7, chapter 17: 0 new, 1 partially filled, 2 filled, 4 cancelled, 8 rejected; LastQty (32) and LastPx (31) describe the latest fill, CumQty (14) and LeavesQty (151) the totals. A partial fill of 2 of 5 contracts at 5723.25 therefore reads 39=1, 32=2, 31=5723.25, 14=2, 151=3.

The session and application layers meet in one place: resent messages. An execution report that arrives with PossDupFlag may or may not have been processed before (the original might have arrived and only its successor been lost). The application must therefore make processing idempotent, recognising a fill it has already booked by its execution identifier (17), or it books a fill twice. The session layer delivers each sequence number once; it cannot know what the application did with the original.

15.4 Parsing fast

Definition 15.4 (FIX engine)

A FIX engine is the software that implements the FIX session layer for an application: it frames and validates incoming messages, maintains sequence numbers, heartbeats and the store of sent messages, answers resend requests, and hands application messages to the program in order.

A naive parser splits the message on SOH, converts each tag to an integer and copies each value into a map from tag to string: several allocations per field (chapter 6), a tree insertion per field, and one pass per byte. The build’s parser does none of that. It finds every delimiter of the message with the vector scan of chapter 14, parses each tag in place, and records for each field only its tag, offset and length in a fixed array: the values stay where they are, in the receive buffer, and are read as string views. Body length and checksum are verified in the same pass.

    Error parse(const char* p, std::size_t n) {
        p_ = p;
        n_ = n;
        count_ = 0;
        if (n < 5 || p[0] != '8' || p[1] != '=' || p[n - 1] != SOH) return Error::framing;
        std::uint32_t soh[kMaxFields];
        const std::size_t k = simdscan::positions(p, n, SOH, soh, kMaxFields);
        if (k == kMaxFields && soh[k - 1] != n - 1) return Error::too_many_fields;
        std::size_t start = 0;
        for (std::size_t i = 0; i < k; ++i) {
            std::uint64_t tag = 0;
            std::size_t used = 0;
            if (!simdscan::parse_uint(p + start, soh[i] - start, tag, used) || p[start + used] != '=') return Error::tag;
            fields_[count_++] = FieldRef{static_cast<int>(tag), static_cast<std::uint32_t>(start + used + 1),
                                         static_cast<std::uint32_t>(soh[i] - start - used - 1)};
            start = soh[i] + 1;
        }
        if (count_ < 4 || fields_[0].tag != 8 || fields_[1].tag != 9 || fields_[2].tag != 35 ||
            fields_[count_ - 1].tag != 10)
            return Error::order;
        for (std::size_t i = 3; i + 1 < count_; ++i)  // found by chapter 25's fuzzer: a CheckSum inside the body
            if (fields_[i].tag == 10 || fields_[i].tag == 8 || fields_[i].tag == 9) return Error::order;
        const std::size_t body_start = fields_[1].off + fields_[1].len + 1;
        const std::size_t body_end = fields_[count_ - 1].off - 3;          // first byte of "10="
        std::uint64_t declared = 0;
        std::size_t used = 0;
        simdscan::parse_uint(p + fields_[1].off, fields_[1].len, declared, used);
        if (used != fields_[1].len || declared != body_end - body_start) return Error::body_length;
        const std::string_view ck = value(count_ - 1);
        std::uint64_t c = 0;
        if (ck.size() != 3 || !simdscan::parse_uint(ck.data(), 3, c, used) || used != 3 || c != checksum(p, body_end))
            return Error::checksum;
        return Error::ok;
    }
Listing 15.3. The zero-copy parse: delimiters from the vector scan, tags parsed in place, then the order of the framing fields, the body length and the checksum. code/firm/fixengine/cpp/firm_fixengine.hpp

Figure 15.3 measures both on execution reports of about 205 bytes. The zero-copy view parses one in about 80 ns80\,\mathrm{n}\mathrm{s}, some twelve million a second on one core; the map-based parser takes about 900 ns900\,\mathrm{n}\mathrm{s}, eleven times longer; the Python reference about 5 µs5\,\text{µ}\mathrm{s}. Building is symmetric: the fixed-buffer builder, which writes digits itself and computes the length and checksum in place, produces a new order in about 60 ns60\,\mathrm{n}\mathrm{s}, against about 330 ns330\,\mathrm{n}\mathrm{s} for the same message assembled by string concatenation.

Parsing and building FIX execution reports of about 205 bytes (1 000 distinct messages, median of 40 passes). Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores; C++ with GCC 11 at -O2, Python 3.10. Data: bench_fix.py.
Figure 15.3. Parsing and building FIX execution reports of about 205 bytes (1 000 distinct messages, median of 40 passes). Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores; C++ with GCC 11 at -O2, Python 3.10. Data: bench_fix.py.

Parsing is rarely the whole cost of a FIX connection: the session layer writes each sent message to its store, often on disk, before or after sending it, and the counterparty’s engine, network and matching take far longer than 80 ns80\,\mathrm{n}\mathrm{s}. But on a gateway that carries a broker’s whole flow, or on a market maker’s order path through a venue that speaks only FIX, the difference between 80 ns80\,\mathrm{n}\mathrm{s} and a microsecond per message is the difference between a component that disappears from the latency budget of chapter 1 and one that owns part of it.

15.5 Where FIX is still used

FIX is the language of order routing between firms: from asset managers’ order management systems to brokers, from brokers’ algorithms to venues, and for direct market access (One Quant Book 1, chapter 4), where a client’s orders reach the venue through the broker’s infrastructure. Some venues accept it for order entry and drop copies, as the dated box below shows for one of them. Where speed is the product, venues have moved to binary protocols with fixed-offset fields (chapter 16), and the FIX Trading Community itself has standardised binary alternatives: Simple Binary Encoding for the messages, and FIXP, a “lightweight point-to-point protocol” for the session, whose own introduction names as its basis the FIX session layer and exchange protocols such as SoupBinTCP. The tag-value engine remains the one every trading firm needs somewhere.

As of September 2026 — FIX order entry today

Coinbase Exchange documents three ways to trade (consulted September 2026): a REST interface “for lower-frequency trading”, a FIX order-entry interface “for higher-frequency trading”, and FIX and WebSocket market data. Its FIX baseline is FIX 5.0 SP2; sessions are logged out every Saturday; and “resend requests are not supported”: each connection starts a new session with new sequence numbers, so a client that loses its connection recovers the state of its orders by other means, such as the venue’s drop-copy sessions. The FIXP session standard reached version 1.0 in April 2021.

15.6 Tutorial: a FIX engine in three languages

Goal. Parse and build FIX messages without copying them, replay a conversation with a gap through the session layer in Python and C++, and measure the parsers. End state: Figure 15.3, the trace of Listing 15.1, and green tests in Python, C++ and Rust.

  1. Check the published example. The tests decode the logon of Figure 15.1 and find a body of 65 bytes and a checksum of 062, then change one byte (checksum error) and the declared length (body-length error).
  2. Replay the golden conversation. make_fix_fixtures.py scripts the broker’s side (a lost fill and a lost heartbeat, a resend request from the broker, a silent broker); firm_fixengine.replay runs our session through it and writes the trace; the C++ test runs the C++ session through the same conversation and compares its trace with the Python one, line for line; the Rust tests parse and rebuild every message of it byte for byte.
  3. Shorten the trace with python ll_fix.py, which prints Listing 15.1.
  4. Measure with python bench_fix.py: the C++ parsers and builders on 1 000 execution reports, and the Python reference on the same messages.

What to change next. Delete the gap fill from the broker’s answer in the conversation and replay it: the session waits for message 5 forever. Then make the session answer a message whose tag is not a number with a Reject (35=3).

15.7 Build: the FIX engine

Purpose. The firm’s FIX connections: broker and venue order entry where FIX is the protocol, drop copies, and the test counterparties of chapter 25. The order gateway of chapter 21 uses its codec for FIX venues.

Interface. Python reference firm_fixengine: checksum, encode, decode, frame, Session (logon, send, on_bytes, on_timer, logout, trace), replay. C++20 firm::fix: View::parse, get, frame, Builder (start, add, finish), Session with the same methods and trace. Rust firm_fixengine: View::parse, get, frame, encode, checksum.

Rules. The parser copies nothing and rejects a message whose framing fields, body length or checksum are wrong; the builder allocates nothing; the session numbers every message, stores every one it sends, answers resend requests with application messages marked PossDupFlag and gap fills for administrative ones, holds messages received ahead of a gap, and ends the session on a sequence number too low without PossDupFlag.

Acceptance tests. code/firm/fixengine/: the published example’s body length and checksum; checksum, body-length and field-order corruptions caught; framing of concatenated and partial streams; the golden conversation’s trace reproduced by the Python reference and, line for line, by the C++ session; every message of that trace parsed and rebuilt byte for byte in Rust; heartbeat, test request and timeout; logout on a sequence number too low; reset mode.

Stretch. A persistent message store (chapter 23’s binary log), a Reject for malformed messages, FIXT 1.1 sessions carrying FIX 5.0 application messages, and the session layer in Rust.

Sources and further reading

  • FIX Trading Community, FIX Latest (Extension Pack 312) message and field dictionary, as published by the Orchimate browser (fields 7, 8, 9, 10, 16, 34, 35, 36, 43, 49, 52, 56, 108, 112, 123; messages 0, 1, 2, 4, 5, 8, A, D, 9).
  • FIX Trading Community, FIX Performance Session Layer (FIXP) specification (GitHub, versions 1.0 and 1.1).
  • Wikipedia, “Financial Information eXchange” (history; the worked body-length and checksum example).
  • Coinbase Developer Documentation, Exchange FIX API: connectivity.

15.8 Exercises

Exercise 15.1 ★

Compute the body length and checksum of the heartbeat 8=FIX.4.4|9=?|35=0|49=A|56=B|34=2|52=20260925-14:30:00.000|10=?|, where | is SOH, and check your answer with firm_fixengine.encode.

Solution

Solution of Exercise 15.1.

The body is 35=0| (5 bytes), 49=A| (5), 56=B| (5), 34=2| (5) and the sending time field (25): 45 bytes, so 9=45. The bytes before 10= sum to a number whose remainder modulo 256 is 75: 10=075, as firm_fixengine.encode confirms for sequence number 2 at 14:30:00.

Exercise 15.2 ★

With a heartbeat interval of 30 seconds and the build’s rules, the broker falls silent at 10:00:00. When does our session send a test request, and when does it declare the connection dead if nothing comes back?

Solution

Solution of Exercise 15.2.

A test request after 1.2 intervals of silence, at 10:00:36; if no heartbeat carrying its identifier arrives within one more interval, the session declares the connection dead at 10:01:06, 66 seconds after the last message.

Exercise 15.3 ★

Our session expects sequence number 52 and receives 57. What does it send, what does it do with message 57, and what must the broker send back?

Solution

Solution of Exercise 15.3.

A ResendRequest with BeginSeqNo 52 and EndSeqNo 0; it holds message 57. The broker resends 52–56 (and everything after, as EndSeqNo 0 asks): application messages with PossDupFlag and OrigSendingTime, administrative ones replaced by SequenceReset-GapFill; then 57 is delivered from the queue, and the resent copy of 57 is ignored as a duplicate.

Exercise 15.4 ★★

Give two different, well-formed messages of the same length with the same checksum, and explain with Proposition 15.2 why the protocol accepts the risk.

Solution

Solution of Exercise 15.4.

Swapping the sender and target identifiers of the heartbeat of exercise 1 gives 49=B|56=A| instead of 49=A|56=B|: the same bytes in another order, so the same length and the same checksum, 075. The checksum only has to catch framing errors and single-byte damage; TCP’s own checksums protect the bytes in transit, and the session layer rejects a message addressed to the wrong party by its identifiers.

Exercise 15.5 ★★

Why are heartbeats and test requests gap-filled instead of sent again? What would go wrong if they were resent?

Solution

Solution of Exercise 15.5.

They describe the link at the moment they were sent: a heartbeat says “I am alive now”, a test request asks for an answer now, a resend request asks for messages that may since have been sent. Resent later they would be meaningless or harmful: a stale test request would demand an answer, a stale resend request would trigger a second resend. Gap fill moves the sequence past them without replaying them.

Exercise 15.6 ★★

A gateway receives 200 000 execution reports a second at peak. Using the measured costs, how much of a core does parsing take with the zero-copy view, the map-based parser and the Python reference?

Solution

Solution of Exercise 15.6.

On the committed measurement: 200 000×84 ns≈1.7%200\,000 \times 84\,\mathrm{n}\mathrm{s} \approx 1.7\% of a core with the zero-copy view, 200 000×892 ns≈18%200\,000 \times 892\,\mathrm{n}\mathrm{s} \approx 18\% with the map-based parser, and 200 000×5.3 µs≈107%200\,000 \times 5.3\,\text{µ}\mathrm{s} \approx 107\% with the Python reference, which cannot keep up at all.

Exercise 15.7 ★★★

Coding. Make the Python and C++ sessions answer a message whose tag is not a number with a Reject (35=3) carrying RefSeqNum (45), without advancing past it, and add the case to the golden conversation so that both traces still agree.

Solution

Solution of Exercise 15.7.

In on_bytes, catch the decoding error of kind tag before the sequence check, read MsgSeqNum from the raw bytes if it is present, and emit 35=3 with 45=<that number> and a text; then advance the expected number (the message was received, only not processed). Do the same in the C++ on_bytes on Error::tag, add a malformed message to make_fix_fixtures.py, regenerate the expected trace and run both tests.

Exercise 15.8 ★★★

Find the flaw. “To keep things simple our engine resets both sequence numbers to 1 at every reconnection, so it never needs resend logic.”

Solution

Solution of Exercise 15.8.

Messages sent while the connection was down (or in flight when it broke) are never delivered: an execution report for a fill is lost, and the firm’s position is wrong. Resetting is acceptable only with another way to recover the state of every order, such as a drop copy or order-status queries after each reconnection, as venues that support no resend requests expect.

15.9 Problem: The Weekend Gap

Problem 15.1

Weekend problem — resynchronising after a disconnection

A broker’s engine keeps writing to its store the messages of a session whose connection to us is down, and resends them when we reconnect. Assume messages of 205 bytes, a 1 Gb/s link with a round trip of 1 ms1\,\mathrm{m}\mathrm{s}, a heartbeat interval of 30 seconds, and the measured parsing costs (measured_fix.csv). Consider two disconnections: a busy one, a link that dies silently while the broker sends 200 execution reports a second, and a quiet one, 48 hours over a weekend with one application message an hour and a heartbeat whenever 30 seconds pass without one.

Part I — The busy outage.

  1. How long does the build’s session take to notice the dead link, and why?
  2. How many messages must the broker resend (ll_fix.sender_stream)? How many are heartbeats?
  3. How many bytes is that, and how long do they take on the wire?
  4. How long does resynchronising take with the C++ engine, and with the Python reference?

Part II — The quiet weekend.

  1. How many messages sit in the broker’s store for us after 48 hours? How many are application messages?
  2. How many messages does the answer to our resend request contain, with gap fill and without?
  3. How long does resynchronising take with the Python reference, each way?
  4. Why does gap fill change nothing in the busy case?

Part III — The design.

  1. Why may administrative messages never be resent as they were?
  2. What must the application do with a resent execution report marked PossDupFlag?
  3. A venue that supports no resend requests starts a new session at each connection. How do you learn about fills that happened while you were disconnected?
  4. What must the engine persist to survive its own restart without a gap?

Part IV — The verdict.

  1. State the named result: messages resent and time to resynchronise in both cases, with and without gap fill.
  2. Would a 1-second heartbeat interval help the busy case? At what cost?
  3. What if the broker’s store kept only its last 10 000 messages?
  4. Should the strategy act on resent execution reports as if they had just happened?
  5. How would you test the resend logic before connecting to a broker?
  6. Can the Python reference keep up with the busy session in normal running?
  7. Which of the two cases would you design the engine for?
  8. In one sentence: what is the session layer for?
Solution

Solution of Problem 15.1.

  1. About 66 seconds: a test request after 36 seconds of silence and one more 30-second interval for its answer.
  2. 13 227 execution reports on the fixed seed, and no heartbeat: at 200 messages a second the broker is never idle for 30 seconds.
  3. 13 227×205=2 711 53513\,227 \times 205 = 2\,711\,535 bytes, about 21.7 ms21.7\,\mathrm{m}\mathrm{s} at 1 Gb/s.
  4. Two round trips (logon, resend request), the bytes, and the parsing: about 24.8 ms24.8\,\mathrm{m}\mathrm{s} with the C++ engine and about 94 ms94\,\mathrm{m}\mathrm{s} with the Python reference.
  5. 5 776 messages, of which 38 are application messages and the rest heartbeats.
  6. With gap fill 77: the 38 application messages and 39 gap fills, one per run of heartbeats. Without, 5 776.
  7. About 2.5 ms2.5\,\mathrm{m}\mathrm{s} with gap fill against 42 ms42\,\mathrm{m}\mathrm{s} without.
  8. There are no administrative messages to replace.
  9. They refer to the link at the time they were sent (exercise 5).
  10. Check whether it has already been processed, by its execution identifier, and book it only once.
  11. From the venue’s drop copy or by querying the status of every open order after reconnecting, and reconcile with the firm’s own records before trading again.
  12. Its next outgoing and expected incoming sequence numbers, and the messages it sent (to answer resend requests), written before or as they are sent.
  13. Named result. A busy 66-second outage at 200 messages a second leaves 13 227 messages to resend, about 25 ms25\,\mathrm{m}\mathrm{s} to resynchronise with the C++ engine and 94 ms94\,\mathrm{m}\mathrm{s} with the Python reference, gap fill or not; a quiet 48-hour weekend leaves 5 776 messages, which gap fill reduces to 77, from 42 ms42\,\mathrm{m}\mathrm{s} to 2.5 ms2.5\,\mathrm{m}\mathrm{s}.
  14. Yes: detection in 2.2 seconds leaves about 450 messages instead of 13 227; the cost is a heartbeat a second each way when idle, and false alarms whenever a peer is briefly slow.
  15. Messages older than the store cannot be resent: the broker must gap-fill or reset over them, and the lost execution reports must be recovered by reconciliation.
  16. It should update positions and order states from them at once, but judge them as past events: prices and the book have moved since their original sending time (122).
  17. With golden conversations (lost messages, duplicates, resend requests in both directions, resets), as the build does, and against the exchange simulator of One Quant Book 10.
  18. Yes: 200 messages a second at 5.3 µs5.3\,\text{µ}\mathrm{s} is under 0.1% of a core; it is the resend burst that it takes tens of milliseconds to absorb.
  19. The busy case: its resend is the large one, and it happens when markets are moving.
  20. To keep two firms’ views of their conversation identical, message for message, whatever the network does.

15.10 Interview questions

Interview question 15.1 ★ developer

Describe the structure of a FIX message. How does a receiver know where a message ends?

Solution

Solution of Interview question 15.1.

Fields tag=value separated by SOH: BeginString, BodyLength and MsgType first, the rest of the header, the body, and CheckSum last. The receiver reads the first two fields; BodyLength gives the length up to the checksum field, which is always seven bytes (10=nnn and SOH).

What the interviewer is looking for: body length for framing, checksum as the terminator.

Interview question 15.2 ★★ developer

Walk through what happens when a FIX session detects a gap in incoming sequence numbers.

Solution

Solution of Interview question 15.2.

The receiver sees a sequence number above the expected one, sends a ResendRequest from the expected number (EndSeqNo 0), and holds the later messages. The sender resends application messages with PossDupFlag and OrigSendingTime, and gap-fills administrative ones with SequenceReset. The receiver processes them in order, releases the held messages, and ignores duplicates.

What the interviewer is looking for: hold, resend, gap fill, duplicates.

Interview question 15.3 ★★ developer

How would you write a FIX parser that does not allocate? What does it return?

Solution

Solution of Interview question 15.3.

Find every delimiter (vectorised), parse each tag in place, and record (tag, offset, length) in a fixed array; values are views into the receive buffer, valid as long as it is. Validate the framing fields, body length and checksum in the same pass. Return the view, or an error kind.

What the interviewer is looking for: views, a fixed array, and validation in one pass.

Interview question 15.4 ★★ developer

What are heartbeats and test requests for, and how would you choose the heartbeat interval?

Solution

Solution of Interview question 15.4.

To tell silence from a dead connection: a side idle for the interval sends a heartbeat; a side that hears nothing sends a test request and gives up if it gets no answer. A shorter interval detects failures sooner, so fewer messages go into the void, at the price of more traffic and of false alarms when a peer pauses.

What the interviewer is looking for: detection time against traffic and false alarms.

Interview question 15.5 ★★ developer

Why do many venues use binary protocols for order entry instead of FIX tag-value? What do they give up?

Solution

Solution of Interview question 15.5.

Fixed-offset binary fields need no search, no number parsing and less bandwidth, and fixed sizes simplify framing: tens of nanoseconds per message, predictable. They give up readability, the common dictionary shared across venues, and flexibility: each venue has its own layouts and versions.

What the interviewer is looking for: parse cost and determinism against interoperability.

Interview question 15.6 ★★★ developer

Your engine restarted in the middle of the day and the broker says your sequence number is too low. What happened, and how do you recover without losing or duplicating fills?

Solution

Solution of Interview question 15.6.

The engine lost its outgoing sequence number (not persisted, or restored from an old copy) and restarted below the broker’s expected number. Restore the persisted numbers and store; if they are lost, agree a sequence reset with the broker out of band, then reconcile every order’s state from the broker’s records or a drop copy before trading, booking fills by execution identifier so that none is counted twice.

What the interviewer is looking for: persisted state, a controlled reset, and reconciliation.

Terms defined in this chapter

See all 2333 terms in the glossary