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?
GitHub Repository: FIX-Order-Routing-Client
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:
Despite being 30+ years old, FIX remains dominant because:
FIX has two layers:
Our client handles both, building on QuickFIX for session management.
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 | 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 |
The client supports standard order types:
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:
Application interfaceThe 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 &);
};
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;
};
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'
};
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
}
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
);
}
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
}
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));
}
}
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 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).
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));
}
#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;
};
#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;
}
# 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
TT provides access to 40+ exchanges including:
For TT documentation, see: TT FIX Order Routing
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
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:
Latency: 3.5 microseconds for NewOrderSingle (C++17, optimized build)
How is this achieved?
noexcept callbacks, no exceptions in hot pathWhat's NOT optimized:
For most use cases, 3.5μs is excellent—faster than many commercial solutions.
This open-source FIX order routing client provides:
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