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.
- FIX 4.4 over non-blocking TCP
- Zero-copy, std::string_view, SOH-delimited
- Zero malloc on the trading hot path
- 1 MiB power-of-two ring buffer
- Snapshot (W) + Incremental (X), XAUUSD
- Logon (A) + Heartbeat (0) + TestRequest (1)
- Pepperstone test 4257078
- f1-f7 features to MonstreGating
Source files
// 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 delimiterinline 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 fixThe 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.
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.