C++ · trading infrastructure

A zero-allocation FIX 4.4 client, ready to copy

The full source for an ultra-low-latency FIX 4.4 TCP socket client and parser, built for Pepperstone test account 4257078. It frames messages off a ring buffer, parses tag=value pairs without copying or allocating, extracts XAUUSD bid/ask, computes seven features, and feeds them to MonstreGating. Every file below is the real source - pick a tab and copy.

Protocol
FIX 4.4 over non-blocking TCP
Parser
Zero-copy, std::string_view, SOH-delimited
Allocation
Zero malloc on the trading hot path
Inbound
1 MiB power-of-two ring buffer
Market data
Snapshot (W) + Incremental (X), XAUUSD
Session
Logon (A) + Heartbeat (0) + TestRequest (1)
Account
Pepperstone test 4257078
Pipeline
f1-f7 features to MonstreGating

Source files

7 files · 1,455 lines
314 lines · CPP
// FixClient.hpp
//
// Ultra-low-latency, zero-allocation FIX 4.4 TCP client and parser.
// Target: RT-PREEMPT Linux / BSD sockets, non-blocking TCP.
//
// Design goals:
// - Zero heap allocation on the hot path (no malloc/new during the trading
// session). All buffers are fixed-size members or stack locals.
// - Zero-copy parsing: tag/value pairs are exposed as std::string_view that
// point directly into the receive ring buffer.
// - Single-threaded reactor model: poll the socket, drain the ring buffer,
// dispatch complete FIX messages, react.
//
// This is standalone trading-system source; it does not depend on the web app
// in this repository.
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <string_view>
#include <array>
namespace fix {
// ---------------------------------------------------------------------------
// Wire constants
// ---------------------------------------------------------------------------
inline constexpr char SOH = '\x01'; // FIX field delimiter
inline constexpr char EQ = '='; // tag=value separator
// Commonly used FIX tags (FIX 4.4).
enum Tag : int {
TAG_BeginString = 8,
TAG_BodyLength = 9,
TAG_MsgType = 35,
TAG_SenderCompID = 49,
TAG_TargetCompID = 56,
TAG_MsgSeqNum = 34,
TAG_SendingTime = 52,
TAG_CheckSum = 10,
TAG_EncryptMethod = 98,
TAG_HeartBtInt = 108,
TAG_ResetSeqNumFlag = 141,
TAG_Username = 553,
TAG_Password = 554,
TAG_TestReqID = 112,
TAG_Symbol = 55,
TAG_MDReqID = 262,
TAG_NoMDEntries = 268,
TAG_MDEntryType = 269, // 0=Bid, 1=Offer/Ask
TAG_MDEntryPx = 270,
TAG_MDEntrySize = 271,
TAG_MDUpdateAction = 279, // 0=New,1=Change,2=Delete (incremental)
};
// MDEntryType values.
inline constexpr char MD_BID = '0';
inline constexpr char MD_ASK = '1';
// ---------------------------------------------------------------------------
// Fast integer / fixed-point decimal parsing (no allocation, no locale).
// ---------------------------------------------------------------------------
// Parse an unsigned integer from a string_view. Returns false on bad input.
inline bool parse_u64(std::string_view sv, uint64_t& out) noexcept {
if (sv.empty()) return false;
uint64_t v = 0;
for (char c : sv) {
if (c < '0' || c > '9') return false;
v = v * 10 + static_cast<uint64_t>(c - '0');
}
out = v;
return true;
}
// Parse a FIX price like "2345.678" into a scaled integer with `scale`
// implied decimals (e.g. scale=5 -> 1 price unit == 1e-5). Avoids floating
// point on the hot path; deterministic and fast. Handles optional sign.
inline bool parse_price_scaled(std::string_view sv, int scale,
int64_t& out) noexcept {
if (sv.empty()) return false;
bool neg = false;
size_t i = 0;
if (sv[0] == '-') { neg = true; i = 1; }
else if (sv[0] == '+') { i = 1; }
int64_t intpart = 0;
int64_t frac = 0;
int fracdigits = 0;
bool seen_dot = false;
bool seen_digit = false;
for (; i < sv.size(); ++i) {
char c = sv[i];
if (c == '.') {
if (seen_dot) return false;
seen_dot = true;
continue;
}
if (c < '0' || c > '9') return false;
seen_digit = true;
if (!seen_dot) {
intpart = intpart * 10 + (c - '0');
} else if (fracdigits < scale) {
frac = frac * 10 + (c - '0');
++fracdigits;
}
// extra fractional digits beyond scale are truncated (price grid is
// known; truncation is deterministic)
}
if (!seen_digit) return false;
int64_t mult = 1;
for (int d = 0; d < scale; ++d) mult *= 10;
// pad fractional part to full scale
for (int d = fracdigits; d < scale; ++d) frac *= 10;
int64_t v = intpart * mult + frac;
out = neg ? -v : v;
return true;
}
// Also expose a double conversion for feature math (done off the parse path
// per message, not per byte). Still allocation-free.
inline double scaled_to_double(int64_t scaled, int scale) noexcept {
double d = static_cast<double>(scaled);
for (int i = 0; i < scale; ++i) d *= 0.1;
return d;
}
// ---------------------------------------------------------------------------
// Ring (circular) buffer for the inbound TCP byte stream.
// Fixed capacity, no allocation. Capacity must be a power of two.
// ---------------------------------------------------------------------------
template <size_t CapacityPow2>
class RingBuffer {
static_assert((CapacityPow2 & (CapacityPow2 - 1)) == 0,
"RingBuffer capacity must be a power of two");
public:
static constexpr size_t kCapacity = CapacityPow2;
static constexpr size_t kMask = CapacityPow2 - 1;
size_t size() const noexcept { return head_ - tail_; }
size_t free_space() const noexcept { return kCapacity - size(); }
bool empty() const noexcept { return head_ == tail_; }
// Pointer/length of the largest contiguous writable region. recv() can
// write directly here, avoiding a copy.
char* write_ptr() noexcept { return &buf_[head_ & kMask]; }
size_t write_contig() const noexcept {
size_t h = head_ & kMask;
size_t fr = free_space();
size_t to_end = kCapacity - h;
return fr < to_end ? fr : to_end;
}
void commit(size_t n) noexcept { head_ += n; }
// Byte access relative to logical tail (0 == oldest unread byte).
char at(size_t logical_off) const noexcept {
return buf_[(tail_ + logical_off) & kMask];
}
// Advance the read cursor, discarding `n` consumed bytes.
void consume(size_t n) noexcept { tail_ += n; }
// Copy `n` bytes starting at logical offset `off` into `dst`. Used only
// when a message wraps the physical end of the ring; rare and bounded.
void copy_out(size_t off, size_t n, char* dst) const noexcept {
for (size_t i = 0; i < n; ++i) dst[i] = at(off + i);
}
// True if the logical range [off, off+n) is physically contiguous.
bool is_contiguous(size_t off, size_t n) const noexcept {
size_t start = (tail_ + off) & kMask;
return start + n <= kCapacity;
}
const char* contig_ptr(size_t off) const noexcept {
return &buf_[(tail_ + off) & kMask];
}
private:
alignas(64) char buf_[kCapacity];
uint64_t head_ = 0; // total bytes written (monotonic)
uint64_t tail_ = 0; // total bytes consumed (monotonic)
};
// ---------------------------------------------------------------------------
// A single FIX field exposed as a zero-copy view into the parse area.
// ---------------------------------------------------------------------------
struct Field {
int tag = 0;
std::string_view value;
};
// Parsed market-data view for one message. Prices are scaled integers.
struct MarketData {
bool has_bid = false;
bool has_ask = false;
int64_t bid_scaled = 0;
int64_t ask_scaled = 0;
int scale = 5; // XAUUSD quoted to 5 dp on most FIX feeds
char msg_type = 0; // 'W' snapshot or 'X' incremental
std::string_view symbol;
};
// ---------------------------------------------------------------------------
// Session configuration.
// ---------------------------------------------------------------------------
struct SessionConfig {
std::string_view host;
uint16_t port = 0;
std::string_view sender_comp_id; // e.g. account-derived comp id
std::string_view target_comp_id; // Pepperstone target
std::string_view username; // 4257078
std::string_view password;
std::string_view symbol = "XAUUSD";
int heartbeat_secs = 30;
bool reset_seq = true;
int price_scale = 5;
};
// Forward decl of the consumer (feature pipeline + gating).
class FeatureSink;
// ---------------------------------------------------------------------------
// The FIX client. Owns the socket, ring buffer, sequence numbers, and the
// zero-alloc parser. Single-threaded; drive it from one reactor loop.
// ---------------------------------------------------------------------------
class FixClient {
public:
explicit FixClient(const SessionConfig& cfg, FeatureSink& sink) noexcept;
~FixClient();
FixClient(const FixClient&) = delete;
FixClient& operator=(const FixClient&) = delete;
// Connect (blocking DNS resolve + non-blocking connect completion) and
// send the Logon message. Returns false on hard failure. Allocation here
// is acceptable (startup, not trading session).
bool connect_and_logon() noexcept;
// One reactor step: read available bytes into the ring buffer and process
// every complete FIX message. Also sends heartbeats / test requests when
// due. Returns false if the connection died. Allocation-free.
bool poll() noexcept;
// Subscribe to market data for the configured symbol (Market Data Request,
// MsgType=V). Sent once after logon is accepted.
bool request_market_data() noexcept;
bool logged_on() const noexcept { return logged_on_; }
int fd() const noexcept { return fd_; }
private:
// --- I/O ---
bool drain_socket() noexcept; // recv into ring buffer
bool send_raw(const char* p, size_t n) noexcept;
// --- message framing & parsing (hot path, zero alloc) ---
// Try to extract one complete message from the ring. Returns the total
// length consumed (0 if no complete message yet). On success, the message
// body view passed to handle_message points either directly into the ring
// (contiguous) or into scratch_ (wrapped).
size_t process_one_message() noexcept;
void handle_message(std::string_view msg) noexcept;
void handle_market_data(std::string_view msg, char msg_type) noexcept;
// --- session admin ---
void send_logon() noexcept;
void send_heartbeat(std::string_view test_req_id = {}) noexcept;
void send_test_request() noexcept;
void maybe_send_heartbeat() noexcept;
void handle_logout(std::string_view msg) noexcept;
// --- outgoing message assembly (fixed scratch buffer, no alloc) ---
// Builds header+body into out_buf_, computes BodyLength + CheckSum.
// body_writer fills the body fields (between header and trailer).
template <typename BodyWriter>
size_t build_message(char msg_type, BodyWriter&& body_writer) noexcept;
static uint8_t checksum(const char* p, size_t n) noexcept;
// --- timing ---
static uint64_t now_ns() noexcept;
// ---------------------------------------------------------------------
SessionConfig cfg_;
FeatureSink& sink_;
int fd_ = -1;
bool logged_on_ = false;
bool md_requested_ = false;
uint64_t out_seq_ = 1; // next outbound MsgSeqNum
uint64_t in_seq_ = 0; // last inbound MsgSeqNum seen
uint64_t last_tx_ns_ = 0;
uint64_t last_rx_ns_ = 0;
bool test_req_pending_ = false;
// Inbound stream ring buffer: 1 MiB, power of two.
RingBuffer<(1u << 20)> ring_;
// Scratch for the rare wrapped-message case and for outbound assembly.
// These are members (no per-message allocation).
alignas(64) char scratch_[64 * 1024]; // wrapped inbound message
alignas(64) char out_buf_[4096]; // outbound message assembly
char sending_time_[32]; // formatted UTC timestamp
};
} // namespace fix

Build with make (g++/clang, C++17). Run the parser and pipeline checks with make test. All files are also committed at /cpp in the repository.

The seven features

Each parsed bid/ask updates a fixed-size rolling window (no heap), then the vector is handed to the gate. The mapping below matches Features.hpp.

f1Mid price - (bid + ask) / 2
f2Spread - ask − bid, liquidity cost
f3Imbalance - mid position in spread, −1…+1
f4Tick return - instantaneous mid return
f5EMA return - fast momentum, α = 0.2
f6Realized vol - rolling stdev of returns
f7Mean distance - mid − rolling mean mid

Hot-path guarantee

After logon, the reactor loop performs no heap allocation. The inbound stream lands directly in a 1 MiB ring buffer via recv into its contiguous write region; framing uses the FIX BodyLength tag to size each message exactly, so the parser never scans for the end of a message. Outbound admin messages assemble into a fixed member buffer, and the price math uses scaled integers to stay deterministic. The only allocation lives in startup (DNS resolve, connect), explicitly outside the trading session.