If you've ever wondered how trading systems send orders to exchanges—placing limit orders, cancelling positions, modifying prices—at speeds measured in microseconds, this post is for you. We're open-sourcing a FIX order routing client written in C++ that achieves 3.5 microsecond latency for sending new orders when compiled with C++17.

This client is tested with Trading Technologies (TT) FIX gateway and with minor modifications works with CQG gateway as well. Through these gateways, you can route orders to virtually any major exchange worldwide.

Why build your own FIX client?

  • Latency: Every microsecond matters in trading. Generic solutions add overhead.
  • Control: Full visibility into order flow, no black boxes.
  • Customization: Tailor the client to your specific workflow.
  • Learning: Understanding FIX is essential for anyone building trading systems.

GitHub Repository: FIX-Order-Routing-Client


Background: What is FIX?

The Financial Information eXchange Protocol

FIX (Financial Information eXchange) is the standard protocol for electronic trading communication. Created in 1992 for equity trading between Fidelity and Salomon Brothers, it's now used globally across asset classes.

FIX is a text-based protocol with messages composed of tag-value pairs:

8=FIX.4.4|9=178|35=D|49=SENDER|56=TARGET|34=2|52=20231215-10:30:00.000|
11=12345|55=ESZ3|54=1|38=10|40=2|44=4500.00|59=0|10=128|

Key elements:

  • Tag 35=D: Message type (D = New Order Single)
  • Tag 11: Client Order ID (your unique identifier)
  • Tag 55: Symbol
  • Tag 54: Side (1=Buy, 2=Sell)
  • Tag 38: Quantity
  • Tag 44: Price
  • Tag 40: Order type (1=Market, 2=Limit)

Why FIX Matters

Despite being 30+ years old, FIX remains dominant because:

  • Universal: Every major exchange and broker supports it
  • Standardized: Clear specifications reduce integration effort
  • Battle-tested: Decades of production use across markets
  • Extensible: Custom fields for venue-specific features

FIX Session vs Application Layer

FIX has two layers:

  • Session layer: Connection management, heartbeats, sequence numbers
  • Application layer: Business messages (orders, executions, cancels)

Our client handles both, building on QuickFIX for session management.


What is Order Routing?

The Order Lifecycle

When you want to buy 10 contracts of ES (S&P 500 E-mini futures):

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Your Trading   │     │    FIX Gateway  │     │    Exchange     │
│    System       │     │   (TT or CQG)   │     │     (CME)       │
└────────┬────────┘     └────────┬────────┘     └────────┬────────┘
         │                       │                       │
         │  NewOrderSingle (D)   │                       │
         │──────────────────────>│                       │
         │                       │   Order Submit        │
         │                       │──────────────────────>│
         │                       │                       │
         │                       │   Order Acknowledged  │
         │                       │<──────────────────────│
         │  ExecutionReport (8)  │                       │
         │<──────────────────────│                       │
         │  (OrdStatus=New)      │                       │
         │                       │                       │
         │                       │   Trade Executed      │
         │                       │<──────────────────────│
         │  ExecutionReport (8)  │                       │
         │<──────────────────────│                       │
         │  (OrdStatus=Filled)   │                       │

Message Types We Support

Message FIX Type Purpose
NewOrderSingle D Place a new order
OrderCancelRequest F Cancel an existing order
OrderCancelReplaceRequest G Modify price or quantity
ExecutionReport 8 Order acknowledgments, fills, rejects
OrderCancelReject 9 Cancel request rejected

Order Types

The client supports standard order types:

  • Market (MKT): Execute immediately at best available price
  • Limit (LIM): Execute at specified price or better
  • Stop (STP): Trigger market order when price reaches threshold
  • Stop-Limit (STL): Trigger limit order when price reaches threshold

Architecture Overview

The client has a clean, focused architecture:

┌─────────────────────────────────────────────────────────────────┐
│                      Your Application                            │
└──────────────────────────┬──────────────────────────────────────┘
                           │ Implements
              ┌────────────▼────────────┐
              │       CallBackIF        │  ← Your callback handler
              │   (abstract interface)  │
              └────────────┬────────────┘
                           │ Receives callbacks
              ┌────────────▼────────────┐
              │      TTFixClient        │  ← Order management logic
              │  (FIX Application)      │
              └────────────┬────────────┘
                           │ Uses
              ┌────────────▼────────────┐
              │       QuickFIX          │  ← Session management
              │   (FIX engine library)  │
              └────────────┬────────────┘
                           │
              ┌────────────▼────────────┐
              │   TT/CQG FIX Gateway    │  ← Exchange connectivity
              └─────────────────────────┘

Design patterns used:

  • Adapter Pattern: Wraps QuickFIX's abstract Application interface
  • Callback Pattern: Decouples business logic from FIX mechanics
  • Object Pooling: Message objects reused to avoid allocation overhead
  • Message Cracker: Type-safe polymorphic dispatch for FIX messages

Core Components

TTFixClient

The main class implementing order management:

class TTFixClient : public FIX::Application,
                    public FIX::MessageCracker
{
public:
    // Send a new order, returns client order ID
    [[nodiscard]] uint64_t sendNewOrderSingle(
        const char *cficode,      // CFI code (e.g., "FXXXXX" for futures)
        const char *securityId,   // Exchange security ID
        const char *symbol,       // Symbol (e.g., "ESZ3")
        double price,             // Limit price
        int qty,                  // Quantity
        char bs,                  // 'B'uy or 'S'ell
        const char *orderType,    // "MKT", "LIM", "STP", "STL"
        const char *timeInForce   // "DAY", "IOC", "GTC"
    );

    // Cancel an order by its client order ID
    uint64_t sendCancelOrderRequest(uint64_t origClOrdId);

    // Modify price or quantity of existing order
    uint64_t sendCancelReplaceRequest(
        uint64_t origClOrdId,
        int qty,
        double price,
        bool has_price
    );

private:
    // QuickFIX Application callbacks
    void onLogon(const FIX::SessionID &);
    void onLogout(const FIX::SessionID &);
    void toAdmin(FIX::Message &, const FIX::SessionID &);
    void fromApp(const FIX::Message &, const FIX::SessionID &);

    // Message crackers for type-safe dispatch
    void onMessage(const FIX44::ExecutionReport &, const FIX::SessionID &);
    void onMessage(const FIX44::OrderCancelReject &, const FIX::SessionID &);
    void onMessage(const FIX44::News &, const FIX::SessionID &);
};

CallBackIF

The interface you implement to receive order events:

struct CallBackIF {
    virtual ~CallBackIF() = default;

    // Session state changes
    virtual void onLogon() noexcept = 0;
    virtual void onLogout() noexcept = 0;

    // Synchronization complete (ready to trade)
    virtual void onNews(const std::string&) noexcept = 0;

    // Order updates: acks, fills, rejects
    virtual void onExecutionReport(
        OrdStatus s,              // Order state (New, Filled, Canceled, etc.)
        ExecType x,               // Why this report (New, Trade, Canceled, etc.)
        uint64_t clOrdId,         // Your order ID
        const std::string &TTOrdId,    // Gateway's order ID
        uint64_t cmeOrdId,        // Exchange's order ID
        uint qty,                 // Total order quantity
        uint cumQty,              // Quantity filled so far
        uint leavesQty,           // Quantity remaining
        bool has_px,              // Was there a fill price?
        double px,                // Fill price
        const std::string &transactionTime,  // Exchange timestamp
        const std::string &TTreceiveTime,    // Gateway receive time
        const std::string &sendingTime,
        const std::string &text
    ) noexcept = 0;

    // Cancel request rejected
    virtual void onOrderCancelReject(
        OrdStatus s,
        uint64_t clOrdId,
        uint64_t origClOrdId,
        const std::string &ttOid,
        uint64_t cmeOrdId,
        int cxlRejResponseTo,
        int cxlRejReason,
        const std::string &complianceText,
        const std::string &transactionTime,
        const std::string &TTreceiveTime,
        const std::string &text
    ) noexcept = 0;
};

OrdStatus and ExecType Enums

Type-safe enums mapping FIX single-character codes:

enum class OrdStatus {
    New = '0',
    PartiallyFilled = '1',
    Filled = '2',
    DoneForDay = '3',
    Canceled = '4',
    Replaced = '5',
    PendingCancel = '6',
    Stopped = '7',
    Rejected = '8',
    Suspended = '9',
    PendingNew = 'A',
    PendingReplace = 'E'
};

enum class ExecType {
    New = '0',
    PartialFill = '1',
    Fill = '2',
    DoneForDay = '3',
    Canceled = '4',
    Replaced = '5',
    PendingCancel = '6',
    Stopped = '7',
    Rejected = '8',
    Suspended = '9',
    PendingNew = 'A',
    Trade = 'F',
    PendingReplace = 'E'
};

How FIX Messages Are Built

NewOrderSingle (Message Type D)

When you call sendNewOrderSingle(), the client builds a FIX message:

uint64_t TTFixClient::sendNewOrderSingle(
    const char *cficode,
    const char *securityId,
    const char *symbol,
    double price,
    int qty,
    char bs,
    const char *orderType,
    const char *timeInForce)
{
    // Generate unique order ID from nanosecond timestamp
    auto [oid, oid_str] = createCLOID();

    // Determine side
    FIX::Side side = (bs == 'B') ? FIX::Side_BUY : FIX::Side_SELL;

    // Build FIX message
    newOrderSingle->set(FIX::ClOrdID(oid_str));         // Tag 11
    newOrderSingle->set(side);                           // Tag 54
    newOrderSingle->set(FIX::TransactTime());            // Tag 60
    newOrderSingle->set(FIX::OrderQty(qty));             // Tag 38
    newOrderSingle->set(ordType);                        // Tag 40
    newOrderSingle->set(FIX::CFICode(cficode));          // Tag 461
    newOrderSingle->set(FIX::SecurityID(securityId));    // Tag 48
    newOrderSingle->set(FIX::Symbol(symbol));            // Tag 55
    newOrderSingle->set(tif);                            // Tag 59

    // Set price based on order type
    if (ordType == FIX::OrdType_LIMIT || ordType == FIX::OrdType_STOP_LIMIT) {
        newOrderSingle->set(FIX::Price(price));          // Tag 44
    }
    if (ordType == FIX::OrdType_STOP || ordType == FIX::OrdType_STOP_LIMIT) {
        newOrderSingle->set(FIX::StopPx(price));         // Tag 99
    }

    // Send via QuickFIX
    FIX::Session::sendToTarget(*newOrderSingle, sessionID);

    return oid;  // Return for cancel/replace operations
}

ExecutionReport Parsing (Message Type 8)

When an execution report arrives, we extract the fields:

void TTFixClient::onMessage(
    const FIX44::ExecutionReport &msg,
    const FIX::SessionID &)
{
    // Skip duplicate messages (retransmissions)
    if (msg.isSetField(FIX::FIELD::PossDupFlag)) {
        FIX::PossDupFlag pdf;
        msg.get(pdf);
        if (pdf) return;  // Duplicate, ignore
    }

    // Extract order state fields
    FIX::OrdStatus os; msg.get(os);
    FIX::ExecType et; msg.get(et);
    FIX::ClOrdID clOrdId; msg.get(clOrdId);
    FIX::OrderID orderId; msg.get(orderId);
    FIX::CumQty cumQty; msg.get(cumQty);
    FIX::LeavesQty leavesQty; msg.get(leavesQty);

    // Get fill price if present
    double px = 0;
    bool has_px = msg.isSetField(FIX::FIELD::LastPx);
    if (has_px) {
        FIX::LastPx lastPx;
        msg.get(lastPx);
        px = lastPx.getValue();
    }

    // Invoke callback
    cb->onExecutionReport(
        convertOrdStatus(os.getValue()),
        convertExecType(et.getValue()),
        std::stoul(clOrdId.getValue()),
        orderId.getValue(),
        cmeOrdId,
        cumQty + leavesQty,
        cumQty,
        leavesQty,
        has_px,
        px,
        transactTime,
        ttReceiveTime,
        sendingTime,
        text
    );
}

Session Management

The News Message Synchronization

FIX sessions have sequence numbers to detect missed messages. When reconnecting, the gateway may retransmit messages. The TT gateway sends a News message (type B) to indicate synchronization is complete:

void TTFixClient::onMessage(const FIX44::News &msg, const FIX::SessionID &) {
    FIX::Headline headline;
    msg.get(headline);

    noNews = false;  // Ready to trade!
    cb->onNews(headline.getValue());
}

Important: The client blocks order submission until onNews() is received:

uint64_t sendNewOrderSingle(...) {
    if (noNews) {
        throw std::runtime_error(
            "No news msg yet, sequence ids not yet synchronized");
    }
    // ... proceed with order
}

Password Injection

FIX Logon messages need a password, injected in the toAdmin() callback:

void TTFixClient::toAdmin(FIX::Message &msg, const FIX::SessionID &) {
    if (msg.getHeader().getField(FIX::FIELD::MsgType) == "A") {
        // This is a Logon message - add password
        msg.setField(FIX::Password(password));
    }
}

Performance Optimizations

Object Pooling

Message objects are allocated once and reused:

class TTFixClient {
private:
    // Allocated once, reused for every order
    FIX44::NewOrderSingle *newOrderSingle = nullptr;
    FIX44::OrderCancelRequest *orderCancelRequest = nullptr;
    FIX44::OrderCancelReplaceRequest *cancelReplaceRequest = nullptr;
};

On first use:

if (!newOrderSingle) {
    newOrderSingle = new FIX44::NewOrderSingle();
    // Set static fields once
    newOrderSingle->set(exchange);
    newOrderSingle->set(account);
    newOrderSingle->set(FIX::SecurityIDSource("8"));
    newOrderSingle->set(FIX::HandlInst('1'));
}

Subsequent orders just update the changing fields—no allocation.

QuickFIX FileStore Hack

QuickFIX normally writes every message to disk for recovery. This adds latency. The qf_hack/FileStore.cpp disables this:

#define NOSTORE  // Disable filesystem I/O

void FileStore::set(int msgSeqNum, const std::string& msg) {
#ifndef NOSTORE
    // Original implementation writes to file
    m_file.seekp(offset);
    m_file.write(msg.c_str(), msg.size());
#endif
    // With NOSTORE, this becomes a no-op
}

Trade-off: You must reset sequence numbers on each login (configured in settings file).

Order ID Generation

Client order IDs use nanosecond timestamps—fast and unique:

std::tuple<int64_t, std::string> createCLOID() {
    uint64_t tse = std::chrono::system_clock::now()
                       .time_since_epoch()
                       .count();
    return std::make_tuple(tse, std::to_string(tse));
}

Usage Example

1. Implement Your Callback

#include "CallBackIF.hpp"
#include <iostream>

class MyOrderHandler : public m2::ttfix::CallBackIF {
public:
    void onLogon() noexcept override {
        std::cout << "Connected to gateway\n";
    }

    void onLogout() noexcept override {
        std::cout << "Disconnected from gateway\n";
    }

    void onNews(const std::string& headline) noexcept override {
        std::cout << "Ready to trade: " << headline << "\n";
        ready_ = true;
    }

    void onExecutionReport(
        OrdStatus s,
        ExecType x,
        uint64_t clOrdId,
        const std::string &TTOrdId,
        uint64_t cmeOrdId,
        uint qty,
        uint cumQty,
        uint leavesQty,
        bool has_px,
        double px,
        const std::string &transactionTime,
        const std::string &TTreceiveTime,
        const std::string &sendingTime,
        const std::string &text
    ) noexcept override {

        std::cout << "ExecutionReport: "
                  << "ClOrdId=" << clOrdId
                  << " Status=" << static_cast<char>(s)
                  << " ExecType=" << static_cast<char>(x)
                  << " CumQty=" << cumQty
                  << " LeavesQty=" << leavesQty;

        if (has_px) {
            std::cout << " FillPx=" << px;
        }
        std::cout << "\n";
    }

    void onOrderCancelReject(...) noexcept override {
        std::cout << "Cancel rejected!\n";
    }

    bool isReady() const { return ready_; }

private:
    bool ready_ = false;
};

2. Initialize the Client

#include "TTFixClient.hpp"
#include "quickfix/FileStoreFactory.h"
#include "quickfix/SocketInitiator.h"

int main() {
    // Create callback handler
    MyOrderHandler handler;

    // Create FIX client
    m2::ttfix::TTFixClient client(
        &handler,
        "CME",                    // Exchange
        "YOUR_ACCOUNT",           // Account ID
        "YOUR_PASSWORD",          // Password
        "TT_GATEWAY",             // Target CompID
        "YOUR_SENDER_ID"          // Sender CompID
    );

    // Initialize QuickFIX
    FIX::SessionSettings settings("settings");
    FIX::FileStoreFactory storeFactory(settings);
    FIX::ScreenLogFactory logFactory(settings);
    FIX::SocketInitiator initiator(client, storeFactory, settings, logFactory);

    // Start connection
    initiator.start();

    // Wait for synchronization
    while (!handler.isReady()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    // Place an order: Buy 10 ES at 4500.00 limit
    uint64_t orderId = client.sendNewOrderSingle(
        "FXXXXX",           // CFI code for futures
        "123456",           // Security ID
        "ESZ3",             // Symbol
        4500.00,            // Price
        10,                 // Quantity
        'B',                // Buy
        "LIM",              // Limit order
        "DAY"               // Day order
    );

    std::cout << "Order placed: " << orderId << "\n";

    // Cancel the order
    client.sendCancelOrderRequest(orderId);

    // Run event loop
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    initiator.stop();
    return 0;
}

3. Build and Run

# Build (requires QuickFIX installed)
g++ -std=c++17 -O2 -I/path/to/quickfix/include \
    main.cpp TTFixClient.cpp \
    -L/path/to/quickfix/lib -lquickfix \
    -lpthread -o fix_client

# Run
./fix_client

Gateway Connectivity

Trading Technologies (TT)

TT provides access to 40+ exchanges including:

  • CME Group: ES, NQ, CL, GC, ZN, ZB
  • ICE: Brent, WTI, Sugar
  • Eurex: DAX, Bund, Bobl
  • CBOE: VIX futures and options

For TT documentation, see: TT FIX Order Routing

CQG

CQG also provides broad exchange connectivity. With minor modifications to message field mappings, this client works with CQG's FIX gateway.

For CQG documentation, see: CQG FIX Connect


Configuration

The settings file configures QuickFIX:

[DEFAULT]
ConnectionType=initiator
HeartBtInt=30
ReconnectInterval=5
FileStorePath=store
FileLogPath=log
StartTime=00:00:00
EndTime=00:00:00
UseDataDictionary=Y
DataDictionary=dictionary_cert/FIX44.xml
ResetOnLogon=Y
ResetOnLogout=Y
ResetOnDisconnect=Y

[SESSION]
BeginString=FIX.4.4
SenderCompID=YOUR_SENDER_ID
TargetCompID=TT_GATEWAY
SocketConnectHost=fix.tradingtechnologies.com
SocketConnectPort=443
SSLEnable=Y
SSLProtocols=TLSv1.2
SSLCertificate=certificate/client.pem
SSLCACertificate=certificate/ca.pem

Key settings:

  • ResetOnLogon=Y: Reset sequence numbers each login (required for FileStore hack)
  • SSLEnable=Y: Use TLS for secure connection
  • DataDictionary: FIX message validation schema

Performance Characteristics

Latency: 3.5 microseconds for NewOrderSingle (C++17, optimized build)

How is this achieved?

  1. Object pooling: No allocation per order
  2. Disabled file I/O: FileStore hack removes disk writes
  3. Nanosecond order IDs: Fast generation without UUID libraries
  4. Efficient callbacks: noexcept callbacks, no exceptions in hot path
  5. C++17 optimizations: Move semantics, if-constexpr, structured bindings

What's NOT optimized:

  • QuickFIX itself (could be replaced with custom session layer)
  • String handling (could use fixed-size buffers)
  • No kernel bypass (would require specialized NICs)

For most use cases, 3.5μs is excellent—faster than many commercial solutions.


Limitations

  • Gateway dependency: Requires TT or CQG subscription
  • FIX 4.4 only: Not tested with other FIX versions
  • Linux focused: Tested on CentOS, should work on other Linux distributions
  • No market data: This is order routing only; use separate market data feed

Conclusion

This open-source FIX order routing client provides:

  • Low-latency order submission: 3.5μs for NewOrderSingle
  • Complete order lifecycle: New, cancel, modify, fills, rejects
  • Clean architecture: Callback pattern decouples business logic
  • Production-ready: Tested with TT gateway
  • Minimal dependencies: Just QuickFIX

Whether you're building a trading system, learning about FIX protocol, or need a foundation for exchange connectivity, this client provides a solid starting point.


GitHub Repository: FIX-Order-Routing-Client

Open source under the MIT License.

Copyright 2022 Vincent Maciejewski, Quant Enterprises & M2 Tech

  • v@m2te.ch
  • https://www.linkedin.com/in/vmayeski/
  • http://m2te.ch/

Previous Post Next Post