FIX Engine
A C++ FIX engine focused on correctness, validation, and protocol learning.
Updates
Initial session state
08 May 2026
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:
- disconnected
- logon sent
- active
- logout sent
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:
LogonHeartbeatTestRequestLogoutResendRequest
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:
- The message passes wire validation.
- The message passes dictionary validation.
- 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:
- A valid incoming Logon moves the session into the active state.
- A TestRequest returns a Heartbeat with the same
112value. - A Logout returns a Logout acknowledgement if one has not already been sent.
- A sequence gap returns a ResendRequest.
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
02 May 2026
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:
- Build the body string from all fields except
8,9, and10. - Add tag
8. - Add a freshly calculated tag
9. - Append the body.
- Calculate the checksum over everything built so far.
- Add tag
10as 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:
- sender and target comp IDs
- outgoing sequence number
- expected incoming sequence number
- disconnected, logon-sent, active, and logout-sent states
The rules can start small:
- outbound admin messages get
BeginString,MsgType,SenderCompID,TargetCompID,MsgSeqNum, andSendingTime - creating an outbound message increments the outgoing sequence number
- receiving a message checks whether tag
34matches the expected incoming sequence number - valid incoming messages increment the expected incoming sequence number
- a valid incoming Logon moves the session into the active state
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
16 Apr 2026
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
30 Mar 2026
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
30 Mar 2026
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
FIXMessageparses raw SOH-delimited FIX payloads into orderedtag=valuefields.- Validation checks both:
BodyLength (9)against computed message body size.CheckSum (10)using modulo-256 over all bytes before tag10.
- The parser preserves field order and exposes all parsed pairs for downstream logic.
- Catch2 tests cover valid messages and malformed/failure cases.
Testing with Catch2
- valid heartbeat / logon / new-order / execution-report style messages
- incorrect checksum
- incorrect body length
- missing checksum
- non-numeric checksum
- duplicate tags (currently treated as invalid by checksum/body-length mismatch)
- truncated message
- empty value field
Dictionary Status
The project now includes FIX42.xml and links pugixml.
FIXDictionarycurrently loads the XML file successfully at startup.- Tag/message-type lookup methods are declared but not implemented yet.
- Dictionary data is not connected to
FIXMessage::validate()yet.
Next Step
Integrate the real dictionary model from FIX42.xml:
- Parse
<fields>and<messages>into in-memory maps. - Implement
isValidTag,isValidMsgType, andgetFieldName. - Add dictionary-aware validation on top of current checksum/body-length validation.