HFT Foundations, Latency & Data Structures
Bonus: C++ in High-Frequency Trading

12.1 HFT Foundations, Latency & Data Structures

High-frequency trading is software that buys and sells on exchanges by itself. It reads live market prices (market data), decides in a few microseconds whether to trade, and sends the order; no human clicking. A person needs hundreds of milliseconds just to move a mouse; an HFT system tries to finish that whole decode-decide-send path thousands of times per second.

Those decision times are measured in microseconds (106s10^{-6}\text{s}) or nanoseconds (109s10^{-9}\text{s}). In a web app, a 10-millisecond pause is invisible. On a latency-critical trading path, 10 milliseconds is an eternity: the difference between a fair fill and eating a stale quote.

HFT is not just "C++ with fast computers." It is the discipline of making packet decoding, market-state updates, risk checks, and order transmission predictable when the clock is angry.

Median latency alone can lie to you. A system that usually answers in 2 μs2\ \mu s but occasionally stalls at 200 μs200\ \mu s can be more dangerous than one that consistently answers in 10 μs10\ \mu s. Occasional lateness means adverse fills, missed opportunities, and uncontrolled risk.

Note

HFT systems are regulated financial systems. Examples here teach memory predictability, hardware awareness, and safe software design. They are not trading advice, exchange-protocol specs, or substitutes for venue certification and mandatory risk controls.

Let's look at the critical path, why C++ is common here, how to think about latency tails, and the data layouts that keep the hot path honest.

The Trading Critical Path

Think of the hot path as a clear runway. Everything that must finish before an order can leave the building lives on that runway. Extra luggage belongs somewhere else.

A simplified trading loop looks like this:

The market-data-to-order critical path.

The critical path is the chain of work that must finish before the order can be sent.

A rough latency budget can be expressed as:

Ttotal=Tnetwork-in+Tdecode+Tbook+Tstrategy+Trisk+Tencode+Tnetwork-outT_{\text{total}} = T_{\text{network-in}} + T_{\text{decode}} + T_{\text{book}} + T_{\text{strategy}} + T_{\text{risk}} + T_{\text{encode}} + T_{\text{network-out}}

In a colocated environment, individual stages can be measured in nanoseconds or microseconds. But the most important metric is generally a distribution, especially the upper tail:

p50,p99,p99.9,p99.99p_{50}, \quad p_{99}, \quad p_{99.9}, \quad p_{99.99}

For every item on the hot path, interrogate it like a suspicious suitcase:

  • Does it allocate memory?
  • Does it acquire a lock?
  • Does it make a system call?
  • Does it touch cold memory?
  • Does it create unpredictable branches?
  • Can it block?
  • Can it throw an exception?
  • Does it log synchronously?
  • Does it call a virtual function?
  • Does it access a remote NUMA node?

Here is the runway rule I want you to remember:

Put complex, blocking, failure-prone work on control paths. Keep hot paths bounded, local, and simple.


Why C++ Is Common in HFT

C++ is popular here because it lets you control the messy parts of machines:

  • Object lifetime
  • Memory allocation
  • Data representation
  • Alignment
  • CPU instructions and compiler optimization
  • Threading and atomic memory ordering
  • Kernel and networking integration
  • Zero-copy processing
  • Compile-time abstraction

But C++ is not a latency spell. A C++ application using std::unordered_map, shared ownership, mutex contention, logging, and frequent heap allocation may be slower and less predictable than a carefully designed application in another language.

The advantage comes from deliberate design, not from the file extension.

A typical hot-path C++ profile might favor:

struct MarketEvent {
    std::uint64_t sequence;
    std::int64_t price_ticks;
    std::int32_t quantity;
    std::uint32_t instrument_id;
    std::uint8_t side;
    std::uint8_t type;
};
Compact hot-path market event.

Rather than:

struct MarketEvent {
    std::string symbol;
    double price;
    std::shared_ptr<Order> order;
    std::unordered_map<std::string, std::string> metadata;
};
Allocation-heavy event shape to avoid on the hot path.

The first shape is compact, predictable, allocation-free, and cache-friendly. The second can be fine for a UI, research tool, or admin service. It is a terrible default for a latency-critical event loop.


Latency Is a Distribution, Not One Number

Judging latency by the median alone is like judging a highway by average speed while ignoring the pileups. Let's look at two systems:

SystemMedianp99p_{99}p99.9p_{99.9}Maximum
A1 μs1\ \mu s8 μs8\ \mu s250 μs250\ \mu s4 ms4\ ms
B3 μs3\ \mu s5 μs5\ \mu s8 μs8\ \mu s15 μs15\ \mu s

System A wins the median race. System B may be much safer if you care about stale data or late cancellations.

Tail latency can come from all the usual machine gremlins:

  • Context switches
  • Interrupt handling
  • Page faults
  • Lock contention
  • Dynamic allocation
  • Cache misses
  • TLB misses
  • Garbage collection in adjacent processes
  • NUMA remote-memory access
  • CPU frequency changes
  • Kernel networking queues
  • Logging or disk I/O
  • Branch mispredictions
  • Scheduler migration
  • Hardware contention

A useful measurement table for a production system includes:

MetricMeaning
p50p_{50}Typical latency
p90p_{90}Common slow-path behavior
p99p_{99}Operational tail latency
p99.9p_{99.9}Severe tail latency
MaximumUseful for incident investigation, not a stable statistical metric
CountNumber of samples; percentiles without sample counts can mislead

Never compare benchmark results without confirming:

  • CPU model and clock configuration
  • Compiler and optimization flags
  • CPU affinity
  • NUMA configuration
  • Whether debug logging was enabled
  • Whether the benchmark included I/O
  • Whether warm-up occurred
  • Whether the data set fit in cache
  • Whether the system was otherwise idle

The Floating-Point Price Hazard

Binary floating-point is excellent for scientific computing. It is a trap for most decimal market prices.

For example, decimal 0.10.1 has no exact finite representation in binary floating point. This can produce problematic comparisons and price-level identities.

double a{0.1};
double b{0.2};
double c{0.3};

if (a + b == c) {
    // Do not assume this is reliable.
}
Unreliable floating-point price equality.

Trading systems generally represent prices as integer multiples of a defined tick size.

If a product has a tick size of 0.010.01, then:

price_ticks=price0.01\text{price\_ticks} = \frac{\text{price}}{0.01}

For example:

Decimal priceTick sizeInteger representation
101.25101.250.010.0110,12510{,}125
101.25101.250.250.25405405
101.25101.250.1250.125810810

A lightweight representation:

#include <cstdint>
#include <compare>

struct Price {
    std::int64_t ticks{0};

    constexpr auto operator<=>(const Price&) const = default;
};

struct Quantity {
    std::int64_t units{0};

    constexpr auto operator<=>(const Quantity&) const = default;
};
Integer tick Price and Quantity types.

Now a price is clearly not an arbitrary floating-point soup.

constexpr Price best_bid{10'125};
constexpr Price best_ask{10'126};

if (best_bid < best_ask) {
    // Valid non-crossed market.
}
Comparing bid and ask in ticks.

Overflow matters

The maximum value of a signed 6464-bit integer is:

26312^{63} - 1

Before multiplying quantities and prices, reason about bounds. A check like max() / b is clear, but when b is not a compile-time constant it hides a runtime division on the hot path. Prefer overflow intrinsics or domain bounds known at initialization (tick size, max quantity):

bool multiplication_would_overflow(
    std::int64_t a,
    std::int64_t b
) noexcept {
    // A production version must fully handle signs and zero.
    // GCC/Clang: __builtin_mul_overflow. MSVC: similar intrinsics.
    std::int64_t product{};
    return __builtin_mul_overflow(a, b, &product);
}
Hot-path multiply overflow check via builtin.

For signed financial arithmetic, use a carefully reviewed arithmetic utility, known-bound domain checks, or compiler overflow intrinsics. Overflow in position, notional, or risk math is a correctness and safety defect, not a cute edge case.


Market Data Is a Stateful Stream

Market-data feeds are not a pile of independent JSON blobs. They are a stateful stream of binary messages:

  • Add order
  • Modify order
  • Cancel order
  • Trade
  • Auction event
  • Instrument status update
  • Snapshot
  • Heartbeat
  • Sequence or recovery control message

Venue details differ. One principle does not:

A feed handler must know whether its local state is complete and trustworthy.

Many feeds provide sequence numbers:

struct FeedState {
    std::uint64_t expected_sequence{1};
    bool synchronized{false};
};
Minimal feed synchronization state.

A basic sequence check:

enum class SequenceResult {
    accepted,
    duplicate_or_old,
    gap_detected
};

SequenceResult process_sequence(
    FeedState& state,
    std::uint64_t received_sequence
) noexcept {
    if (received_sequence < state.expected_sequence) {
        return SequenceResult::duplicate_or_old;
    }

    if (received_sequence > state.expected_sequence) {
        state.synchronized = false;
        return SequenceResult::gap_detected;
    }

    ++state.expected_sequence;
    return SequenceResult::accepted;
}
Accept, duplicate, or gap detection for feed sequences.

A sequence gap is not a shrug emoji. Later incremental updates may produce a fantasy book that looks real and is wrong.

A typical recovery process is:

Sequence-gap recovery flow.

Do not keep quoting from a stale or incomplete book unless the strategy and risk design explicitly allow it. Hope is not a recovery protocol.


Parse Binary Protocols Safely

A tempting but unsafe move is casting raw network bytes straight to a C++ struct:

auto* message = reinterpret_cast<const WireMessage*>(buffer);
Unsafe reinterpret cast of wire bytes.

That can explode because of:

  • Alignment requirements
  • Compiler-inserted padding
  • Endianness differences
  • Strict-aliasing issues
  • Untrusted or truncated packet data
  • Protocol layout changes

A safer pattern checks boundaries and copies scalar values with std::memcpy. Advance the remaining span instead of tracking a separate offset. When returning std::optional<T>, construct a named optional and return it so NRVO can apply; returning a bare T into an optional<T> return type can force an extra copy or move. Hot paths often prefer a bool plus out-parameter or an explicit error enum instead.

#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <span>
#include <type_traits>

class Decoder {
public:
    explicit Decoder(std::span<const std::byte> data) noexcept
        : data_(data) {}

    template <typename T>
    std::optional<T> read() noexcept {
        static_assert(std::is_trivially_copyable_v<T>);

        std::optional<T> result;
        if (sizeof(T) > data_.size()) {
            return result;
        }

        result.emplace();
        std::memcpy(&*result, data_.data(), sizeof(T));
        data_ = data_.subspan(sizeof(T));
        return result;
    }

    std::size_t remaining() const noexcept {
        return data_.size();
    }

private:
    std::span<const std::byte> data_;
};
Bounds-checked binary decoder using memcpy.

For a little-endian protocol on a little-endian host:

std::optional<std::uint32_t> read_u32_le(
    Decoder& decoder
) noexcept {
    auto value = decoder.read<std::uint32_t>();

    if (!value.has_value()) {
        return std::nullopt;
    }

    if constexpr (std::endian::native == std::endian::little) {
        return *value;
    } else {
        return std::byteswap(*value);
    }
}
Little-endian u32 read with optional byteswap.

For real protocols, create clear decode functions per message type. Validate all lengths, type fields, reserved fields when required, and protocol-specific constraints.

struct AddOrder {
    std::uint64_t order_id;
    std::int64_t price_ticks;
    std::uint32_t quantity;
    std::uint32_t instrument_id;
    char side;
};

std::optional<AddOrder> decode_add_order(
    std::span<const std::byte> bytes
) noexcept;
Per-message decode function shape.

Avoid treating #pragma pack as a universal fix. Packing can create unaligned loads and glue your in-memory C++ layout to someone else's wire format. Decode explicitly.


Cache Behavior Is Often More Important Than Big-O Notation

An algorithm with prettier Big-O can lose to a simpler layout if it pointer-chases through cold memory.

Modern CPUs are absurdly fast at arithmetic on data already in registers or nearby cache. They get slow and sulky when waiting on DRAM.

A rough hierarchy is:

Storage levelTypical relative cost
RegisterLowest
L1 cacheVery low
L2 cacheLow
Last-level cacheHigher
Local DRAMMuch higher
Remote NUMA DRAMHigher still
Disk / networkOrders of magnitude higher

Exact numbers depend on hardware. The relationship matters more than any fixed cycle count from a blog post.

Consider an array of prices and quantities:

struct Level {
    std::int64_t price_ticks;
    std::int64_t quantity;
};

std::array<Level, 256> levels;
Contiguous price-level array.

This is contiguous. Iteration is friendly to cache lines and prefetchers.

Compare it with a linked structure:

struct Node {
    std::int64_t price_ticks;
    std::int64_t quantity;
    Node* next;
};
Pointer-chasing linked book level.

The deeper problem with linked lists is the loop-carried dependency of iteration: each step waits on the previous node's next pointer before the CPU can issue the next load. That stalls the pipeline even when nodes happen to sit in contiguous memory. Scattered heap nodes add cache misses on top of that dependency chain.

Linked structures are not always wrong. Your access pattern and layout still have to earn their keep under measurement.


Structure of Arrays vs Array of Structures

Suppose a strategy scans quote data.

An array-of-structures layout:

struct Quote {
    std::int64_t price_ticks;
    std::int32_t quantity;
    std::uint32_t venue_id;
};

std::array<Quote, 1024> quotes;
Array of structures quote layout.

A structure-of-arrays layout:

struct Quotes {
    std::array<std::int64_t, 1024> price_ticks;
    std::array<std::int32_t, 1024> quantities;
    std::array<std::uint32_t, 1024> venue_ids;
};
Structure of arrays quote layout.

If you only need prices, structure-of-arrays can cut unnecessary memory traffic.

std::int64_t best_price = std::numeric_limits<std::int64_t>::max();

for (const auto price : quotes.price_ticks) {
    if (price < best_price) {
        best_price = price;
    }
}
Scanning only the price field in SoA layout.

The right choice depends on access patterns:

PatternOften suitable layout
Process all fields of one event at a timeArray of structures
Scan one field over many recordsStructure of arrays
Sparse, dynamic relationshipsIndices or compact pools
Hot and cold data have different access ratesSplit hot and cold fields

A common optimization is hot/cold splitting:

struct HotOrderState {
    std::uint64_t order_id;
    std::int64_t price_ticks;
    std::int64_t leaves_quantity;
    std::uint8_t state;
};

struct ColdOrderMetadata {
    char client_order_id[32];
    char account[32];
    char strategy_name[32];
};
Hot and cold order state split.

Keep the hot structure compact. Large, rarely used metadata should not pollute the cache lines you touch on every update.


A Cache-Conscious Price Ladder

For instruments with bounded price ranges and discrete ticks, an indexed price ladder can be faster and more predictable than a map.

#include <cstddef>
#include <cstdint>
#include <vector>

class PriceLadder {
public:
    PriceLadder(
        std::int64_t minimum_tick,
        std::int64_t maximum_tick
    )
        : minimum_tick_(minimum_tick),
          levels_(static_cast<std::size_t>(
              maximum_tick - minimum_tick + 1
          )) {}

    bool contains(std::int64_t price_tick) const noexcept {
        return price_tick >= minimum_tick_ &&
               static_cast<std::size_t>(price_tick - minimum_tick_)
                   < levels_.size();
    }

    // Copies the quantity out once. Avoids returning a raw pointer that
    // callers may lazily dereference multiple times.
    bool try_get_quantity_at(
        std::int64_t price_tick,
        std::int64_t& quantity
    ) const noexcept {
        if (!contains(price_tick)) {
            return false;
        }
        quantity = levels_[static_cast<std::size_t>(
            price_tick - minimum_tick_
        )];
        return true;
    }

    bool try_set_quantity_at(
        std::int64_t price_tick,
        std::int64_t quantity
    ) noexcept {
        if (!contains(price_tick)) {
            return false;
        }
        levels_[static_cast<std::size_t>(
            price_tick - minimum_tick_
        )] = quantity;
        return true;
    }

private:
    std::int64_t minimum_tick_;
    std::vector<std::int64_t> levels_;
};
O(1) indexed price ladder with bounded access.

Do not expose an unchecked quantity_at for ticks outside the ladder. Prefer get/set helpers that return false on a miss, assert in debug builds, or force callers through contains first. Out-of-range indexing is undefined behavior dressed as market data.

This offers direct access:

O(1)O(1)

It only works well when the price range is reasonably bounded. A huge sparse tick range can waste memory and hurt cache locality.

A hybrid design may use:

  • Dense arrays near the current market
  • Sparse storage outside the active region
  • A configured maximum depth
  • Per-instrument data structures selected by product characteristics

Do not optimize from vibes. Use historical data and realistic replay.


Order Books Need Clear Invariants

A local limit order book must keep its promises. For example:

best bid<best ask\text{best bid} < \text{best ask}

unless a crossed book is explicitly possible during recovery, aggregation, or feed-specific sequencing.

Basic level state:

struct PriceLevel {
    std::int64_t displayed_quantity{0};
    std::uint32_t order_count{0};
};
Simple displayed price level.

For every update, validate domain constraints:

constexpr bool is_valid_quantity(
    std::int64_t quantity
) noexcept {
    return quantity >= 0;
}
Non-negative quantity check.

After reducing a level:

level.displayed_quantity -= cancelled_quantity;

if (level.displayed_quantity < 0) {
    // State is inconsistent.
    // Trigger venue-appropriate recovery rather than continuing blindly.
}
Detect inconsistent negative displayed quantity.

A practical order book often separates:

  1. Feed reconstruction state
  2. Strategy-visible market state
  3. Recovery status
  4. Derived analytics, such as imbalance or microprice

That separation matters because a feed can still be receiving packets while the reconstructed book is garbage.

The Takeaway

Keep the runway clear, measure the tail not just the median, store prices in ticks, treat feeds as stateful streams, decode bytes safely, and make cache-friendly layouts earn their keep.

Practice: sketch a MarketEvent and a FeedState for one instrument, then write down which fields belong on the hot path and which belong in cold metadata.

Finished reading this lesson?