FIX Engine

A C++ FIX engine focused on correctness, validation, and protocol learning.

Updates

Initial session state

Moving into the session layer

The last update ended with the idea that the engine should move from message construction into session state. I did not want to jump straight into TCP yet because sockets would make it harder to see whether the actual FIX session rules were correct. The useful step before networking is a session object that can build outbound admin messages and react to incoming messages just to get something working.

I added a FIXSession class for this. It owns a small SessionConfig, which contains the FIX version, sender comp ID, target comp ID, and heartbeat interval. It also tracks the current session state, the next outgoing sequence number, and the next incoming sequence number expected from the counterparty.

The states are deliberately simple for now:

This is not the complete FIX session model yet, but it is enough to start enforcing the basic lifecycle.

Outbound admin messages

The session can now create the main admin messages that will eventually be sent over the network:

Each outbound message gets the standard session header fields using addField() from FIXMessage:

8=FIX.4.2
35=<message type>
49=<sender comp ID>
56=<target comp ID>
34=<next outgoing sequence number>
52=<current UTC sending time>

Then the message-specific fields are added. For example, a Logon gets 98=0 for no encryption and 108=<heartbeat interval>. A TestRequest gets 112=<test request ID>. A ResendRequest gets 7=<begin seq no> and 16=<end seq no>.

After the fields are added, the message is serialized so that BodyLength(9) and CheckSum(10) are calculated correctly. The outgoing sequence number is then incremented.

Incoming messages and sequence numbers

The main method is onIncoming(). It takes a parsed FIXMessage and returns any messages that should be sent back.

Before doing anything session-specific, it checks three things:

  1. The message passes wire validation.
  2. The message passes dictionary validation.
  3. The counterparty fields are in the expected direction.

For example, if my session is configured as BUY -> SELL, then an incoming message should be from SELL -> BUY. That means tag 49 should be SELL and tag 56 should be BUY.

After that, the session checks tag 34, the message sequence number.

If the incoming sequence number is exactly what the session expected, the message is processed and the expected incoming sequence number is incremented.

If the incoming sequence number is too high, the session has detected a gap. For example, if it expected 34=1 but received 34=3, then messages 1 and 2 are missing. In that case, the session returns a ResendRequest:

35=2
7=1
16=2

That means “resend messages 1 through 2”.

At the moment, low sequence numbers are ignored. That is not the final behaviour, but it is enough for this first pass. Later this needs to distinguish duplicates, possible duplicates, and genuinely invalid low sequence numbers.

Basic admin reactions

The first session behaviours are now in place:

This is still not a full session layer. There are no heartbeat timers yet, no persistence, no replay of stored messages, no SequenceReset handling, and no TCP transport. But the important part is that the session logic is now separate from networking. When transport is added, it should only need to frame bytes, parse messages, pass them into FIXSession, and write back any serialized responses.


Message construction and the path to sessions

Moving from parsing to construction

Up to this point, the engine has mostly worked in one direction: take a raw FIX string, split it into ordered tag-value pairs, and then validate the structural parts of the message. That gave me the parser, checksum validation, body length validation, dictionary-backed field lookup, and typed wrappers for the core admin messages.

The next missing piece was being able to go the other way. A FIX engine should not only read messages, it should also be able to build them. This is where FIXMessage::serialize() comes in.

The goal of serialization is to turn the internal field representation back into a valid FIX wire message:

8=FIX.4.2<SOH>9=...<SOH>35=...<SOH>49=...<SOH>56=...<SOH>10=...<SOH>

The important part is that tags 9 and 10 cannot just be copied from the current object. BodyLength(9) and CheckSum(10) are derived from the message itself, so they have to be recalculated whenever a message is serialized.

BodyLength and CheckSum

BodyLength is the number of bytes after the complete 9=<value><SOH> field and before the checksum field. It includes the tag digits, equals signs, values, and SOH separators for the body fields.

For example:

35=0<SOH>

has length 5:

3 + 5 + = + 0 + SOH

CheckSum is different. It is calculated by summing every byte from the start of the message up to, but not including, tag 10, then taking the result modulo 256. FIX also expects it to be formatted as exactly three digits, so a checksum of 8 becomes 008.

The serialization flow now looks like this:

  1. Build the body string from all fields except 8, 9, and 10.
  2. Add tag 8.
  3. Add a freshly calculated tag 9.
  4. Append the body.
  5. Calculate the checksum over everything built so far.
  6. Add tag 10 as a three-digit value.

This means a message can be parsed, modified later, and serialized with fresh structural fields instead of stale ones.

Building messages from scratch

I also added a default constructor and a public addField() method. This means FIXMessage is no longer only a parser around an existing raw string. It can also be used as a small message builder:

FIXMessage msg;
msg.addField("8", "FIX.4.2");
msg.addField("35", "0");
msg.addField("49", "SENDER");
msg.addField("56", "TARGET");
msg.addField("34", "2");
msg.addField("52", "20260311-12:00:00.000");

std::string wire = msg.serialize();

The distinction between addField() and the internal serialization helper matters. addField() updates the actual message state: the ordered field vector and lookup map. The helper used inside serialize() only appends tag=value<SOH> text to an output string.

This gives the engine a basic round-trip path:

raw FIX string -> FIXMessage -> serialized FIX string -> FIXMessage -> validate()

That is a useful foundation because the session layer will need to create outbound messages constantly. Unfortunately, this is causing the FIXMessage class to be bloated, if we have serialize() in this class then I think this class needs be broken down further. We should have a factory type class that can simply build valid FIX messages.

Next: session state

The next step is the session layer. I am not going straight to TCP sockets yet. The first version should be session state and sequence numbers without networking.

The initial session model should track:

The rules can start small:

After that, the harder parts can be layered on: heartbeat timers, test requests, resend requests, gap fills, persistence, and finally TCP transport.


Reading and parsing the XML FIX Dictionary

Understanding XML dictionary structure

An XML file has the concept of nodes and nodes can be children of other nodes. The ‘node’ I was looking at was the ‘field’ node, which contained the mapping between a tag number and what it means. This isn’t enough. A FIX string is more complicated than this.

What a FIX string is actually made of

A FIX string is made of a header, a message body and a trailer. Every FIX string, has ‘required’ fields for the header, message body and trailer. For example, the header must contain a field denoting the message type, a NewOrderSingle for example. The XML dictionary contains the required fields for a certain message type, continuing with our NewOrderSingle example, the XML dictionary would look like this:

<message name='NewOrderSingle' msgtype='D' msgcat='app'>
 <field name='ClOrdID' required='Y' />
 <field name='ClientID' required='N' />
 <field name='ExecBroker' required='N' />
 <field name='Account' required='N' />

We can see here that the message name is NewOrderSingle with associated msgtype='D'. The D is what will be on the FIX string. Below the first node, there are lines of xml that begin with <field name = .... These are nodes that are the children of <message>. The 4 <field> lines are describing the possible values that NewOrderSingle can take and whether they are required or not. The requirement value is incredibly important and makes the approach to validation easier. My previous original thinking was that we move to a typed message model, effectively classes for different message types (which is what I am still doing) but each message type will hold the required fields. This would become messy fast. With dictionary based validation, we can do validation at the parsing step.

This example was about the message types, the XML also has <header> and <trailer>, which contain information about what values exist for the header and trailer and whether they are applied or not. Having understood the structure, traversing the XML with pugixml is now trivial. FIXDictionary contains hashmaps between tags and values, values and tags, as well as for enums…

Enum business

Some fields in FIX don’t just take any value — they take from a fixed set of options. For example, tag 54 (Side) can be 1 for Buy or 2 for Sell. In the XML dictionary, these are represented as <value> nodes nested inside a <field> node:

<field number='54' name='Side' type='CHAR'>
 <value enum='1' description='BUY' />
 <value enum='2' description='SELL' />
</field>

FIXDictionary stores these in a nested hashmap — outer key is the tag number, inner key is the enum value, and the result is the human-readable description. So given a raw FIX tag-value pair like 54=1, we can resolve it to Side=BUY. This is handled by getEnumDescription() which looks the description up from the nested hashmap.


Dictionary build-up

Parsing the XML

I am using pugixml to load the xml document I am currently working with. The structure of XML was fairly straight forward, I was able to read the pugixml documentation and read the FIX42.xml dictionary. I used unordered_map to go through the fields and store the attributes, the number was the tag and the value was what that tag means.

The idea is that this should happen only once when on startup, then we should get fast lookups. At the moment, I am working with a local dictionary but the plan is to eventually move to an online dictionary through API calls.


FIX Engine: First Update

My thinking

At the moment, the project needs basic parsing so that it can split into tag value pairs, with basic error detection such as verifying body length and checksum

What Is Implemented

  1. FIXMessage parses raw SOH-delimited FIX payloads into ordered tag=value fields.
  2. Validation checks both:
    • BodyLength (9) against computed message body size.
    • CheckSum (10) using modulo-256 over all bytes before tag 10.
  3. The parser preserves field order and exposes all parsed pairs for downstream logic.
  4. Catch2 tests cover valid messages and malformed/failure cases.

Testing with Catch2

Dictionary Status

The project now includes FIX42.xml and links pugixml.

Next Step

Integrate the real dictionary model from FIX42.xml: