Skip to content

SymbolRegistry

SymbolRegistry assigns and resolves stable SymbolId and ExchangeId values for instruments and exchanges. Exposes full metadata (SymbolInfo, ExchangeInfo) for fast, type-safe access across the engine.

struct SymbolInfo
{
  SymbolId id{0};
  std::string exchange;
  std::string symbol;
  InstrumentType type = InstrumentType::Spot;
  Price tickSize{Price::fromDouble(0.01)};
  ExchangeId exchangeId{InvalidExchangeId};

  std::optional<Price> strike;
  std::optional<TimePoint> expiry;
  std::optional<OptionType> optionType;

  // Contract spec. Defaults preserve spot/perp behavior: 1.0 multiplier, cash
  // settlement, European exercise. Options override per venue (US equity 100x,
  // physical-settled American; crypto 1x, cash, European).
  double contractMultiplier{1.0};
  SettlementType settlementType{SettlementType::Cash};
  ExerciseStyle exerciseStyle{ExerciseStyle::European};
  std::optional<std::string> settlementCcy;

  // Per-symbol fixed-point scale. The default 1e8 reproduces the compile-time
  // Price/Quantity scale exactly, so CEX symbols and persisted registries are
  // unchanged. A DEX token whose price (~1e-10) or supply (~1e12) range does not
  // fit 1e8 in int64 sets a different scale here and converts through the
  // scale-aware Decimal overloads.
  int64_t priceScale{Price::Scale};
  int64_t qtyScale{Quantity::Scale};
};

// Validate a symbol's per-symbol scale before registration. A scale must be
// positive and small enough to leave usable integer headroom in int64. The
// default 1e8 always passes, so CEX symbols and old registries are never
// rejected. The cap is 1e18, which still leaves ~9 integer units in int64.
std::expected<void, std::string> validateSymbolScale(const SymbolInfo& info);

class SymbolRegistry : public ISubsystem
{
public:
  static constexpr size_t kMaxExchanges = 32;
  static constexpr size_t kMaxSymbols = 4096;
  static constexpr size_t kMaxEquivalentsPerSymbol = 8;

  // Exchange management
  ExchangeId registerExchange(std::string_view name,
                              VenueType type = VenueType::CentralizedExchange);
  std::optional<ExchangeInfo> getExchange(ExchangeId id) const;
  ExchangeId getExchangeId(std::string_view name) const;
  size_t exchangeCount() const;

  // Symbol registration (legacy string-based API)
  SymbolId registerSymbol(const std::string& exchange, const std::string& symbol);
  SymbolId registerSymbol(const SymbolInfo& info);
  std::optional<SymbolId> getSymbolId(const std::string& exchange,
                                      const std::string& symbol) const;
  std::optional<SymbolInfo> getSymbolInfo(SymbolId id) const;
  std::pair<std::string, std::string> getSymbolName(SymbolId id) const;

  // Symbol registration (ExchangeId-based API)
  SymbolId registerSymbol(ExchangeId exchange, std::string_view symbol);
  ExchangeId getExchangeForSymbol(SymbolId symbol) const;

  // Symbol equivalence (cross-exchange mapping)
  void mapEquivalentSymbols(std::span<const SymbolId> equivalentSymbols);
  EquivalentSymbols getEquivalentSymbols(SymbolId symbol) const;
  SymbolId getEquivalentOnExchange(SymbolId symbol, ExchangeId exchange) const;

  // Persistence
  bool saveToFile(const std::filesystem::path& path) const;
  bool loadFromFile(const std::filesystem::path& path);
  std::vector<std::byte> serialize() const;
  bool deserialize(std::span<const std::byte> data);

  // Utilities
  void clear();
  std::vector<SymbolInfo> getAllSymbols() const;
  size_t size() const;
};

Purpose

  • Provide a thread-safe, bidirectional mapping between human-readable instrument keys and compact numeric SymbolIds.
  • Manage exchange registrations with ExchangeId for multi-exchange scenarios.
  • Support cross-exchange symbol equivalence for arbitrage and routing.
  • Expose complete instrument metadata (SymbolInfo) for latency-critical components without repeated parsing.
  • Support persistence and serialization for replay and data recording scenarios.

Exchange Management

Method Description
registerExchange Registers an exchange, returns ExchangeId.
getExchange Returns a copy of the ExchangeInfo for the given ID, or std::nullopt.
getExchangeId Lookup ExchangeId by name.
exchangeCount Returns number of registered exchanges.

Symbol Registration

Method Description
registerSymbol(str) Registers a spot instrument, returns existing id if present.
registerSymbol(info) Registers any instrument type with full metadata.
registerSymbol(ExchangeId, symbol) Registers using ExchangeId instead of string.
getSymbolId Forward lookup from (exchange, symbol) to SymbolId.
getSymbolName Reverse lookup from SymbolId to (exchange, symbol) string pair.
getSymbolInfo Returns std::optional<SymbolInfo> for the given ID.
getExchangeForSymbol Returns ExchangeId for a given symbol.

Symbol Equivalence

For cross-exchange trading, symbols on different exchanges can be mapped as equivalent:

// Register symbols on different exchanges
auto btcBinance = registry.registerSymbol("binance", "BTCUSDT");
auto btcBybit = registry.registerSymbol("bybit", "BTCUSDT");
auto btcOkx = registry.registerSymbol("okx", "BTC-USDT");

// Map them as equivalent
registry.mapEquivalentSymbols({btcBinance, btcBybit, btcOkx});

// Query equivalents
auto equivalents = registry.getEquivalentSymbols(btcBinance);  // Returns all 3
auto bybitEquiv = registry.getEquivalentOnExchange(btcBinance, bybitExchangeId);
Method Description
mapEquivalentSymbols Links symbols as equivalent across exchanges.
getEquivalentSymbols Returns a copy of the equivalence list, at most kMaxEquivalentsPerSymbol entries.
getEquivalentOnExchange Returns equivalent symbol on a specific exchange.

Persistence

Method Description
saveToFile Persists registry to a binary file.
loadFromFile Loads registry from a binary file.
serialize Returns binary representation as std::vector<std::byte>.
deserialize Restores state from binary data.

Capacity Limits

Constant Value Description
kMaxExchanges 32 Maximum number of exchanges.
kMaxSymbols 4096 Maximum number of symbols.
kMaxEquivalentsPerSymbol 8 Maximum equivalents per symbol.

Internal Design

  • Composite key "exchange:symbol" ensures O(1) forward lookups via _map.
  • _symbols is an unordered_map for direct id lookups.
  • _symbolToExchange provides O(1) symbol-to-exchange mapping.
  • Equivalence uses flat storage: [sym * kMaxEquivalentsPerSymbol ... (sym+1) * kMaxEquivalentsPerSymbol) for O(1) lookup.
  • A single shared mutex protects all structures. Registration is rare and takes it exclusively. Lookups are frequent, run concurrently with each other under a shared lock, and wait only while a registration or a clear() is in flight. Every accessor goes through it, getExchange and getExchangeForSymbol and venueTypeForSymbol included: reading _symbolToExchange while another thread resized it was a read past the end of a freed buffer.
  • registerExchange fills the entry before the exchange count admits it, so an id a reader is allowed to dereference always carries a populated name and venue type.
  • Accessors return values, never addresses into registry storage. getExchange used to return const ExchangeInfo* and getEquivalentSymbols a std::span, both pointing at memory the lock protected only until the accessor returned. The caller then read through them with nothing held, and a concurrent clear() or registration rewrote the storage underneath: a thread sanitizer run caught exactly that, an unsynchronized nameView() against clear() zeroing the table. Locking inside the accessor cannot fix it, because the unsafe read happens at the call site. Both copy out under the lock now.

Notes

  • Inherits from ISubsystem for lifecycle management integration.
  • InstrumentType allows immediate filtering (Spot, Future, Option) without extra registry calls in hot paths.
  • SymbolId remains a compact, contiguous 32-bit value suitable for array indices in event buses and order books.
  • Option-specific fields (strike, expiry, optionType) are populated only when type == InstrumentType::Option.
  • tickSize stores the minimum price increment for the instrument (default 0.01).
  • exchangeId links symbol directly to its exchange for O(1) access.
  • Persistence methods are useful for storing symbol mappings alongside recorded market data.