Skip to content

SymbolStateMap

SymbolStateMap is an O(1) container for per-symbol state, indexed by SymbolId.

template <typename State, size_t MaxSymbols = 256>
class SymbolStateMap;

Purpose

  • Provide O(1) access to per-symbol state
  • Optimize for the common case (symbol IDs < 256)
  • Handle overflow gracefully for large symbol counts
  • Cache-line aligned for optimal memory access

Design

The container uses a two-tier approach:

  1. Flat array (symbols 0-255): Direct O(1) access, cache-line aligned
  2. Overflow vector (symbols >= 256): Linear search, rare case
alignas(64) std::array<State, kMaxSymbols> _flat{};
std::array<bool, kMaxSymbols> _initialized{};
OverflowStorage<State, std::is_move_constructible_v<State>> _overflowStorage;

OverflowStorage is conditional: for a move-constructible State it holds a std::vector<std::pair<SymbolId, State>>; for a non-move-constructible State it is an empty stub whose size() is always 0. So a State that cannot be moved gets no overflow tier at all — only the flat array is available, and symbols at or above kMaxSymbols are not storable.

API

Access

// Get or create state for symbol (O(1) for sym < 256)
State& operator[](SymbolId symbol) noexcept;
const State& operator[](SymbolId symbol) const noexcept;

// Get pointer if exists, nullptr otherwise
State* tryGet(SymbolId symbol) noexcept;
const State* tryGet(SymbolId symbol) const noexcept;

// Check if symbol has state
bool contains(SymbolId symbol) const noexcept;

Iteration

// Iterate over all initialized symbols
template <typename Func>
void forEach(Func&& fn);

// Func signature: void(SymbolId, State&)

Utilities

void clear() noexcept;          // Clear all state
size_t size() const noexcept;   // Count of initialized symbols

Example

// Per-symbol position tracking
struct PositionState
{
  double quantity{0.0};
  double avgPrice{0.0};
  double realizedPnl{0.0};
};

SymbolStateMap<PositionState> positions;

// Update position
positions[btcSymbol].quantity += 10.0;
positions[btcSymbol].avgPrice = 50000.0;

// Check if tracked
if (positions.contains(ethSymbol))
{
  auto& state = positions[ethSymbol];
  // ...
}

// Iterate all positions
positions.forEach([](SymbolId sym, PositionState& pos) {
  std::cout << "Symbol " << sym << ": " << pos.quantity << "\n";
});

// Accumulate total PnL
double totalPnl = 0.0;
positions.forEach([&totalPnl](SymbolId, const PositionState& pos) {
  totalPnl += pos.realizedPnl;
});

Performance

Operation Complexity (sym < 256) Complexity (sym >= 256)
operator[] O(1) O(n) overflow search
contains O(1) O(n) overflow search
tryGet O(1) O(n) overflow search
forEach O(MaxSymbols) O(MaxSymbols + n)

For most trading systems with < 256 symbols, all operations are O(1).

Memory

  • Flat array: MaxSymbols * sizeof(State) bytes (64-byte aligned)
  • Initialized flags: MaxSymbols bytes
  • Overflow: dynamic allocation only when needed

Default with SymbolContext: ~2 MB for 256 symbols. SymbolContext is ~8 KB, dominated by NLevelOrderBook<512>'s two std::array<Quantity, 512> (2 x 512 x 8 bytes = 8192 bytes).

Template Parameters

template <typename State, size_t MaxSymbols = 256>
Parameter Default Description
State - Per-symbol state type
MaxSymbols 256 Flat array size, symbols >= this go to overflow

See Also