Market data feeds¶
Five real exchange protocols modelled with RBE, each one a different way of telling where a message ends.
The protocol headers under example/markets/ declare the messages and compose them into frames; the
driver example/markets.cpp serializes a sample of each feed and decodes it back.
CMake target: markets.
The wire helper the driver uses writes the parts of a datagram back to back:
/// Serializes the parts back to back: what a feed handler would find in a datagram.
template<typename... Ts>
auto wire(Ts const&... parts) -> std::vector<std::byte> {
auto out = std::vector<std::byte>((rbe::wire_size_of<Ts>() + ...));
auto offset = std::size_t {0};
((offset += rbe::serialize(std::span {out}.subspan(offset), parts)), ...);
return out;
}
Aquis¶
Every message carries its total length and the packet header carries a count. The packet is
count-delimited: the many walks exactly count messages, measuring each one, and the packet has no
length() of its own, only rbe::frames::distance.
/// Aquis: a UDP payload is a `PacketHeader` followed by `count` messages. The packet is parsed once and its
/// payload is a `many`: iterate it, and let every frame dispatch on the id its header carries.
void aquis_packet() {
using namespace aquis;
constexpr auto header_size = rbe::wire_size_of<Header>();
auto const datagram = wire( // clang-format off
PacketHeader {.count = 3}, //
Header {.msg_type = message_type_t::heartbeat, .length = header_size, .seq_no = 1},
Header {.msg_type = message_type_t::order_cancel, .length = header_size + rbe::wire_size_of<OrderCancel>(), .seq_no = 2},
OrderCancel {.security_id = 7, .order_ref = 42, .timestamp = 1},
Header {.msg_type = message_type_t::order_add, .length = header_size + rbe::wire_size_of<OrderAdd>(), .seq_no = 3},
OrderAdd {.security_id = 7, .side = side_t::buy, .quantity = 100, .price = 2500, .order_ref = 43, .timestamp = 2}
);
auto [packet_header, messages] =
rbe::try_deserialize<aquis::packet>(datagram).value(); // a many iterates as an lvalue
std::println("aquis: packet of {} messages", packet_header.field<"count">());
for (auto const [header, payload]: messages) {
std::print(" seq {}: ", header.field<"seq_no">());
payload.match(
[](Heartbeat const&) { std::println("heartbeat"); },
[](proxy<OrderAdd> const add) { std::println("order add, {} @ {}", add.field<"quantity">(), add.field<"price">()); },
[](rbe::unmatched auto const other) { std::println("not handled, id known: {}", other.known_id); }
); // clang-format on
}
}
example/markets/aquis.hpp
/************************************************************************
* Copyright (c) 2026 Alvaro Cabrera Barrio
* This code is licensed under MIT license (see LICENSE.txt for details)
************************************************************************/
/**
* @file aquis.hpp
* @version 1.0
* @date 05/08/2026
* @brief Aquis Stock Exchange multicast market data protocol.
*
* Message set for the AQSE Market Data Feed, technical specification
* v1.2.3 (April 2024). All integers are little-endian and 1-byte packed
* on the wire (spec §3.1).
*
* Every market data message starts with a 6-byte header carrying the
* message type, its total wire length, and the stream sequence number.
* The header is declared once as `Header` and composed with the message
* set through `rbe::frames::frame`: `rbe::id` on the header field selects the
* message annotated with the matching `rbe::id(value)`, and
* `rbe::frame_length` bounds it. See `message` and `packet` at the end.
*/
#pragma once
#include <rbe/rbe.hpp>
#include <cstdint>
#include "rbe/annotations/empty.hpp"
#include "rbe/annotations/length.hpp"
#include "rbe/core/memory_layout.hpp"
namespace aquis {
// ─────────────────────────────────────────────────────────────────────
// Semantic type aliases (spec §3.1)
// ─────────────────────────────────────────────────────────────────────
/// Price with 5 implied decimal places. e.g. 1'462'500 → 14.625.
using price_t = std::uint64_t;
/// Elapsed nanoseconds since Unix epoch (00:00 UTC, 1970-01-01).
/// AQSE clock resolution is μs; the value is scaled to ns (×1000).
using timestamp_t = std::uint64_t;
using security_id_t = std::uint16_t; ///< Numeric identifier of a security.
using trader_id_t = std::uint16_t; ///< Numeric identifier of a market maker.
using order_ref_t = std::uint32_t; ///< Unique order reference for the day.
using trade_ref_t = std::uint32_t; ///< Trade reference (not unique per day).
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
enum class message_type_t : std::uint8_t {
heartbeat = 1,
order_add = 2,
order_cancel = 3,
order_modify = 4,
trade = 5,
trade_bust = 6,
tick_table_data = 7,
security_definition = 8,
security_status = 9,
snapshot_start = 10,
book_status = 11,
book_entry = 12,
login = 13,
replay_request = 14,
replay_response = 15,
aod_update = 17,
quote_add_replace = 20,
quote_cancel = 21,
security_statistics = 22,
trader_definition = 23,
trade_report = 25,
trade_report_modify = 26,
trade_report_cancel = 27,
};
enum class side_t : std::uint8_t {
buy = 1,
sell = 2,
};
enum class trading_status_t : std::uint8_t {
active = 1,
halted = 2,
suspended = 3,
};
/// Trade category (spec §3.6.2). Other codes may be added.
enum class trade_type_t : std::uint8_t {
visible = 1, ///< Trade against visible book quantity.
hidden = 2, ///< Trade against hidden or reserve quantity.
auction_on_demand = 6,
trade_capture_report = 8,
};
enum class price_type_t : std::uint8_t {
opening = 2,
closing = 5,
};
enum class response_code_t : std::uint8_t {
login_successful = 0,
bad_begin_seq_no = 1,
bad_end_seq_no = 2,
};
// ─────────────────────────────────────────────────────────────────────
// Fixed-width text fields (left-justified ASCII, 0x00 padded)
// ─────────────────────────────────────────────────────────────────────
using umtf_t = std::array<char, 6>;
using isin_t = std::array<char, 12>;
using currency_t = std::array<char, 3>;
using mic_t = std::array<char, 4>;
using name_t = std::array<char, 10>;
using sender_id_t = std::array<char, 16>;
using username_t = std::array<char, 10>;
using password_t = std::array<char, 10>;
// ─────────────────────────────────────────────────────────────────────
// Multicast packet header (spec §3.2)
// ─────────────────────────────────────────────────────────────────────
// clang-format off
/// Prefix of every UDP payload. Followed by `count` back-to-back messages.
struct[[=rbe::pack_le]] PacketHeader {
[[=rbe::payload_count]] std::uint8_t count;
};
// ─────────────────────────────────────────────────────────────────────
// Common market data message header (spec §3.3.1)
// ─────────────────────────────────────────────────────────────────────
/// 6-byte header prefix of every market data message.
///
/// Declared once for the whole protocol and composed with the message set
/// in `message`: `msg_type` selects the message whose `rbe::id(value)`
/// matches, and `length` covers the header plus the message.
struct[[=rbe::pack_le]] Header {
[[= rbe::id]] message_type_t msg_type {};
[[= rbe::frame_length]] std::uint8_t length {};
std::uint32_t seq_no {};
};
// ─────────────────────────────────────────────────────────────────────
// Continuous data feed messages (spec §3.3–§3.4)
// ─────────────────────────────────────────────────────────────────────
// Empty messages are supported by explicitly annotating them as empty
struct [[=rbe::pack_le, =rbe::id(message_type_t::heartbeat), =rbe::empty]] Heartbeat {};
static_assert(rbe::explicitly_empty<Heartbeat>);
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Heartbeat>() == rbe::wire_size_of<Header>());
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_add)]] OrderAdd {
security_id_t security_id;
side_t side;
std::uint32_t quantity;
price_t price;
order_ref_t order_ref;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderAdd>() == 33);
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_cancel)]] OrderCancel {
security_id_t security_id;
order_ref_t order_ref;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderCancel>() == 20);
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_modify)]] OrderModify {
security_id_t security_id;
std::uint32_t quantity;
price_t price;
order_ref_t order_ref;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderModify>() == 32);
struct [[=rbe::pack_le, =rbe::id(message_type_t::quote_add_replace)]] QuoteAddReplace {
trader_id_t trader_id;
security_id_t security_id;
std::uint32_t bid_quantity;
price_t bid_price;
std::uint32_t offer_quantity;
price_t offer_price;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<QuoteAddReplace>() == 42);
struct [[=rbe::pack_le, =rbe::id(message_type_t::quote_cancel)]] QuoteCancel {
trader_id_t trader_id;
security_id_t security_id;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<QuoteCancel>() == 18);
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade)]] Trade {
security_id_t security_id;
trade_type_t trade_type;
std::uint32_t quantity;
price_t price;
order_ref_t order_ref; ///< Zero unless trade_type == visible.
trade_ref_t trade_ref;
timestamp_t timestamp;
std::uint32_t binary_mmt; ///< See spec §3.6.1.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Trade>() == 41);
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_report)]] TradeReport {
security_id_t security_id;
trade_type_t trade_type;
std::uint32_t quantity;
price_t price;
trade_ref_t trade_ref;
timestamp_t timestamp;
std::uint32_t binary_mmt;
timestamp_t transact_time;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeReport>() == 45);
/// Emitted after a TradeReportCancel of the original report (spec §3.4.8).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_report_modify)]] TradeReportModify {
security_id_t security_id;
trade_type_t trade_type;
std::uint32_t quantity;
price_t price;
trade_ref_t trade_ref;
timestamp_t timestamp;
std::uint32_t binary_mmt;
timestamp_t transact_time;
trade_ref_t orig_trade_ref;
timestamp_t orig_timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeReportModify>() == 57);
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_report_cancel)]] TradeReportCancel {
security_id_t orig_security_id;
trade_type_t orig_trade_type;
std::uint32_t orig_quantity;
price_t orig_price;
trade_ref_t trade_ref;
timestamp_t timestamp;
std::uint32_t binary_mmt;
timestamp_t transact_time;
trade_ref_t orig_trade_ref;
timestamp_t orig_timestamp;
timestamp_t orig_transact_time;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeReportCancel>() == 65);
/// Applies only to order-generated trades (spec §3.4.10).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_bust)]] TradeBust {
security_id_t security_id;
std::uint32_t quantity;
price_t price;
trade_ref_t trade_ref;
timestamp_t timestamp;
std::uint32_t binary_mmt;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeBust>() == 36);
/// Disseminates opening and closing prices.
struct [[=rbe::pack_le, =rbe::id(message_type_t::security_statistics)]] SecurityStatistics {
security_id_t security_id;
price_t price;
price_type_t price_type;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SecurityStatistics>() == 25);
/// Pre-market identification of quote originators.
struct [[=rbe::pack_le, =rbe::id(message_type_t::trader_definition)]] TraderDefinition {
trader_id_t trader_id;
sender_id_t sender_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TraderDefinition>() == 24);
/// Pre-market dynamic tick tables / static ticks driving price increments.
struct [[=rbe::pack_le, =rbe::id(message_type_t::tick_table_data)]] TickTableData {
std::uint8_t tick_table_id;
name_t name;
price_t threshold;
price_t tick_size;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TickTableData>() == 33);
/// Pre-market security reference data. Occasionally emitted intraday
/// when a correction is required.
struct [[=rbe::pack_le, =rbe::id(message_type_t::security_definition)]] SecurityDefinition {
security_id_t security_id;
umtf_t umtf; ///< Not applicable to AQSE.
isin_t isin;
currency_t currency;
mic_t mic;
std::uint8_t tick_table_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SecurityDefinition>() == 34);
/// Published when the trading status of a security or its parent market changes.
/// market_flags bit layout (spec §3.4.15):
/// bit 0 Trading: 0 = CT closed / AoD off, 1 = CT open / AoD on
/// bits 1-2 Reserved
/// bit 3 Pre-open/close: 0 = not a pre phase, 1 = pre phase
/// bits 4-7 Reserved
struct [[=rbe::pack_le, =rbe::id(message_type_t::security_status)]] SecurityStatus {
security_id_t security_id;
trading_status_t trading_status;
std::uint8_t market_flags;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SecurityStatus>() == 18);
// ─────────────────────────────────────────────────────────────────────
// Auction On Demand (AoD) feed messages (spec §3.5)
// ─────────────────────────────────────────────────────────────────────
/// First AoDUpdate signals the start of an auction. Subsequent updates
/// publish the indicative price and matched volume.
struct [[=rbe::pack_le, =rbe::id(message_type_t::aod_update)]] AoDUpdate {
security_id_t security_id;
price_t indicative_price;
std::uint32_t match_vol;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AoDUpdate>() == 28);
// AoD Trade and AoD Trade Bust reuse the Trade / TradeBust structures
// from §3.4 (spec §3.5.2, §3.5.3).
// ─────────────────────────────────────────────────────────────────────
// Snapshot feed messages (spec §3.7)
// ─────────────────────────────────────────────────────────────────────
/// First message of each snapshot; identifies the continuous-stream
/// seq_no that this snapshot corresponds to.
struct [[=rbe::pack_le, =rbe::id(message_type_t::snapshot_start)]] SnapshotStart {
std::uint32_t stream_seq_no;
std::uint16_t security_count;
timestamp_t timestamp;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SnapshotStart>() == 20);
/// Book status for one security within a snapshot. Followed by `entries`
/// BookEntry messages.
struct [[=rbe::pack_le, =rbe::id(message_type_t::book_status)]] BookStatus {
security_id_t security_id;
trading_status_t trading_status;
std::uint8_t market_flags;
std::uint16_t entries;
std::uint32_t closing_buy_qty;
std::uint32_t closing_sell_qty;
price_t indicative_price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<BookStatus>() == 28);
/// One open order in the book snapshot. Published in price-time priority
/// per side.
struct [[=rbe::pack_le, =rbe::id(message_type_t::book_entry)]] BookEntry {
security_id_t security_id;
side_t side;
std::uint32_t quantity;
price_t price;
order_ref_t order_ref;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<BookEntry>() == 25);
// ─────────────────────────────────────────────────────────────────────
// Replay service messages — TCP/IP (spec §3.8)
// ─────────────────────────────────────────────────────────────────────
// seq_no in the header is ignored on the replay channel.
struct [[=rbe::pack_le, =rbe::id(message_type_t::login)]] Login {
username_t username;
password_t password;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Login>() == 26);
struct [[=rbe::pack_le, =rbe::id(message_type_t::replay_request)]] ReplayRequest {
std::uint32_t begin_seq_no;
std::uint32_t end_seq_no;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReplayRequest>() == 14);
struct [[=rbe::pack_le, =rbe::id(message_type_t::replay_response)]] ReplayResponse {
response_code_t response_code;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReplayResponse>() == 7);
// clang-format on
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
using messages = rbe::frames::any<
Heartbeat, OrderAdd, OrderCancel, OrderModify, QuoteAddReplace, QuoteCancel, Trade, TradeReport, TradeReportModify,
TradeReportCancel, TradeBust, SecurityStatistics, TraderDefinition, TickTableData, SecurityDefinition,
SecurityStatus, AoDUpdate, SnapshotStart, BookStatus, BookEntry, Login, ReplayRequest, ReplayResponse>;
/// One market data message: `Header` followed by the message selected by `msg_type`.
using message = rbe::frames::frame<Header, messages>;
/// One multicast UDP payload: `PacketHeader` followed by `count` back-to-back messages.
/// `PacketHeader` carries no length, so the packet runs to the end of the datagram.
using packet = rbe::frames::frame<PacketHeader, rbe::frames::many<message>>;
} // namespace aquis
Cboe PITCH¶
A length-prefixed unit header wraps a run of messages, each with a one-byte length and a one-byte type.
Thirty-five candidates in one any, dispatched by the type.
example/markets/cboe.hpp
/************************************************************************
* Copyright (c) 2026 Alvaro Cabrera Barrio
* This code is licensed under MIT license (see LICENSE.txt for details)
************************************************************************/
/**
* @file cboe.hpp
* @version 1.0
* @date 05/08/2026
* @brief Cboe market data protocols: European Multicast PITCH and US TOP.
*
* Two distinct protocols are exposed through nested namespaces:
*
* - `cboe::pitch` — Cboe Titanium Europe Multicast PITCH v6.67
* (2 April 2026). Binary, little-endian, delivered over UDP with
* each frame prefixed by a Sequenced Unit Header wrapping one or
* more back-to-back sequenced messages. Every message starts with
* a 2-byte header: `length` (byte 0) then `msg_type` (byte 1).
*
* - `cboe::top` — Cboe US Equities TOP v1.3.6 (6 September 2024).
* Fixed-length, line-oriented ASCII messages delivered over TCP.
* Each message begins with a 1-byte ASCII message type and is
* terminated by an LF (0x0A). There is no on-wire length field —
* the wire size is fixed per message type.
*
* Each protocol declares its `Header` once and composes it with its
* message set through `rbe::frames::frame`: `rbe::id` on the header field selects
* the message annotated with the matching `rbe::id(value)`. See `message`
* (and `packet` for PITCH) at the end of each namespace.
*/
#pragma once
#include <rbe/rbe.hpp>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <tuple>
namespace cboe {
// =====================================================================
// Cboe Titanium Europe — Multicast PITCH v6.67 (spec §4)
// =====================================================================
namespace pitch {
// ─────────────────────────────────────────────────────────────────────
// Semantic type aliases (spec §2.2)
// ─────────────────────────────────────────────────────────────────────
/// Unsigned little-endian 8-byte price with 4 implied decimal places on
/// BXE/CXE (denominator 10 000) or 6 implied decimal places on TRF
/// (denominator 1 000 000). Spec §2.2 "Binary Long Price".
using long_price_t = std::uint64_t;
/// Unsigned little-endian 2-byte price with 2 implied decimal places
/// (denominator 100). Spec §2.2 "Binary Short Price".
using short_price_t = std::uint16_t;
/// Nanosecond offset from the most recent Time message for the unit.
using time_offset_t = std::uint32_t;
/// Whole seconds since midnight London time (spec §4.1).
using time_t = std::uint32_t;
/// Elapsed nanoseconds since the Unix epoch (spec §4.9.4).
using timestamp_t = std::uint64_t;
using order_id_t = std::uint64_t; ///< Day-specific order identifier.
using execution_id_t = std::uint64_t; ///< Cboe day-unique execution id (12-char base-36 packed in 8 bytes; spec §2.5).
using trade_id_t = std::uint64_t; ///< Cboe trade identifier, unique for ≥ 7 calendar days (spec §4.9.4).
using sequence_t = std::uint32_t;
// ─────────────────────────────────────────────────────────────────────
// Fixed-width text fields (left-justified ASCII, space padded)
// ─────────────────────────────────────────────────────────────────────
using symbol_t = std::array<char, 8>; ///< Standard PITCH symbol, spec §4.3.1.
using symbol_short_t = std::array<char, 6>; ///< Symbol carried by Short-form messages, spec §4.3.2.
using isin_t = std::array<char, 12>; ///< 12-char ISIN used by Trade – Unknown Symbol, spec §4.9.5.
using currency_t = std::array<char, 3>; ///< ISO 4217 currency, spec §4.9.4.
using mic_t = std::array<char, 4>; ///< Execution venue MIC, spec §4.9.4.
using participant_id_t = std::array<char, 4>; ///< Systematic Internaliser attribution, spec §4.3.3.
using index_ticker_t = std::array<char, 10>; ///< Index ticker code, spec §4.16.1.
using execution_flags_t = std::array<char, 4>; ///< 4-char MMT flags on Order Executed messages, spec §4.4.1.
using trade_flags_t = std::array<char, 5>; ///< 5-char MMT flags on non-Extended Trade messages, spec §4.9.3.
using extended_trade_flags_t = std::array<char, 14>; ///< 14-char MMT flags on Trade – Extended, spec §4.9.6.
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
/// PITCH 2.X, GRP and Spin Server message identifiers (spec Appendix A).
/// Note that GRP and Spin Server share `login` (0x01) and `login_response`
/// (0x02); they are dispatched on distinct TCP channels.
enum class message_type_t : std::uint8_t {
// Gap Request Proxy / Spin Server (spec §3, §5)
login = 0x01,
login_response = 0x02,
gap_request = 0x03,
gap_response = 0x04,
spin_image_available = 0x80,
spin_request = 0x81,
spin_response = 0x82,
spin_finished = 0x83,
// PITCH 2.X market data (spec §4)
time = 0x20,
unit_clear = 0x97,
add_order_long = 0x40,
add_order_short = 0x22,
add_order_expanded = 0x2F,
order_executed = 0x23,
order_executed_at_price_size = 0x24,
reduce_size_long = 0x25,
reduce_size_short = 0x26,
modify_order_long = 0x27,
modify_order_short = 0x28,
delete_order = 0x29,
trade_long = 0x41,
trade_short = 0x2B,
trade_break = 0x2C,
end_of_session = 0x2D,
trading_status = 0x31,
trade_extended = 0x32,
trade_unknown_symbol = 0x35,
statistics = 0x34,
transaction_begin = 0xBC,
transaction_end = 0xBD,
auction_summary = 0x96,
auction_update = 0xAC,
index_quote = 0xD8,
index_quote_edsp = 0xD9,
};
/// Buy/Sell side indicator (ASCII, spec §4.3).
enum class side_t : std::uint8_t {
buy = 'B',
sell = 'S',
};
/// Login/Spin login status (spec §3.2, §5.2).
enum class login_status_t : std::uint8_t {
accepted = 'A',
not_authorised = 'N',
session_in_use = 'B',
invalid_session = 'S',
};
/// Gap request response status (spec §3.4).
enum class gap_status_t : std::uint8_t {
accepted = 'A',
out_of_range = 'O',
daily_limit_exhausted = 'D',
minute_limit_exhausted = 'M',
second_limit_exhausted = 'S',
count_limit_exceeded = 'C',
invalid_unit = 'I',
};
/// Spin Response status (spec §5.5).
enum class spin_status_t : std::uint8_t {
accepted = 'A',
out_of_range = 'O',
spin_in_progress = 'S',
};
/// Trading status for a security (spec §4.13).
enum class trading_status_code_t : std::uint8_t {
trading = 'T',
off_book_reporting = 'R',
closed = 'C',
suspension = 'S',
no_reference_price = 'N',
volatility_interruption = 'V', ///< Static collar.
volatility_auction = 'I',
opening_auction = 'O',
closing_auction = 'E',
halt = 'H', ///< Reserved for future use.
market_order_imbalance_ext = 'M',
price_monitoring_extension = 'P',
closing_cross = 'U',
halt_auction = 'Y',
};
/// Auction type (spec §4.15).
enum class auction_type_t : std::uint8_t {
opening = 'O',
closing = 'C',
halt = 'H',
volatility = 'V',
periodic = 'P',
closing_cross = 'U',
};
/// Auction Update — tolerance vs Cboe EBBO collar (spec §4.15.1).
enum class outside_tolerance_t : std::uint8_t {
outside = 'O',
inside = 'I',
not_specified = '-',
};
/// Auction Update — whether primary-market quotes are in the EBBO used
/// to collar this update (spec §4.15.1).
enum class includes_primary_t : std::uint8_t {
includes = 'P',
excludes = 'N',
not_specified = '-',
};
/// Statistics Message price kind (spec §4.14).
enum class statistic_type_t : std::uint8_t {
closing = 'C',
high = 'H',
low = 'L',
opening = 'O',
previous_closing = 'P',
};
/// Statistics Message price provenance (spec §4.14).
enum class price_determination_t : std::uint8_t {
normal = '0',
manual = '1', ///< Adjusted by market supervision.
};
/// Index Quote status (spec §4.16.1).
enum class index_status_t : std::uint8_t {
normal = 'N',
indicative = 'I',
closing = 'C',
};
// clang-format off
// ─────────────────────────────────────────────────────────────────────
// Cboe Sequenced Unit Header (spec §2.4)
// ─────────────────────────────────────────────────────────────────────
/// 8-byte UDP frame prefix that wraps `count` sequenced or unsequenced
/// PITCH messages. A `count` of zero indicates a heartbeat frame.
struct[[=rbe::pack_le]] SequencedUnitHeader {
[[=rbe::frame_length]] std::uint16_t length; ///< Length of entire block including this header.
std::uint8_t count; ///< Number of messages that follow.
std::uint8_t unit; ///< Unit that applies to the enclosed messages.
sequence_t sequence; ///< Sequence of the first enclosed sequenced message.
};
// ─────────────────────────────────────────────────────────────────────
// Common PITCH message header (spec §2.1)
// ─────────────────────────────────────────────────────────────────────
/// 2-byte header prefix of every PITCH / GRP / Spin message. Cboe puts
/// `length` FIRST (offset 0), then the message type (offset 1). Declared
/// once for the whole protocol and composed with the message set in
/// `message`; `length` covers the header plus the message.
struct[[=rbe::pack_le]] Header {
[[= rbe::frame_length]] std::uint8_t length {};
[[= rbe::id]] message_type_t msg_type {};
};
// ─────────────────────────────────────────────────────────────────────
// Gap Request Proxy messages — TCP (spec §3)
// ─────────────────────────────────────────────────────────────────────
/// GRP / Spin Server login (spec §3.1, §5.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::login)]] Login {
std::array<char,4> session_sub_id;
std::array<char,4> username;
std::array<char,2> filler; ///< Space filled.
std::array<char,10> password;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Login>() == 22);
/// Response to a Login (spec §3.2, §5.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::login_response)]] LoginResponse {
login_status_t status;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<LoginResponse>() == 3);
/// Request retransmission of a sequenced range (spec §3.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::gap_request)]] GapRequest {
std::uint8_t unit;
sequence_t sequence; ///< Lowest sequence in the requested range.
std::uint16_t count;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<GapRequest>() == 9);
/// Reply to a GapRequest (spec §3.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::gap_response)]] GapResponse {
std::uint8_t unit;
sequence_t sequence;
std::uint16_t count;
gap_status_t status;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<GapResponse>() == 10);
// ─────────────────────────────────────────────────────────────────────
// Spin Server messages — TCP (spec §5)
// ─────────────────────────────────────────────────────────────────────
/// Advertises the highest sequence for which a spin is currently available (spec §5.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::spin_image_available)]] SpinImageAvailable {
sequence_t sequence;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SpinImageAvailable>() == 6);
/// Request a spin at a previously advertised sequence (spec §5.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::spin_request)]] SpinRequest {
sequence_t sequence;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SpinRequest>() == 6);
/// Response to a SpinRequest (spec §5.5).
struct [[=rbe::pack_le, =rbe::id(message_type_t::spin_response)]] SpinResponse {
sequence_t sequence;
std::uint32_t order_count; ///< Number of Add Order messages that will follow. 0 on reject.
spin_status_t status;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SpinResponse>() == 11);
/// End-of-spin marker; not sent if the SpinRequest was rejected (spec §5.6).
struct [[=rbe::pack_le, =rbe::id(message_type_t::spin_finished)]] SpinFinished {
sequence_t sequence;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SpinFinished>() == 6);
// ─────────────────────────────────────────────────────────────────────
// PITCH 2.X market data messages (spec §4)
// ─────────────────────────────────────────────────────────────────────
/// Whole-second timestamp base for the unit (spec §4.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::time)]] Time {
time_t time;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Time>() == 6);
/// Instructs feed recipients to clear all orders for the Cboe book of
/// the enclosing Sequenced Unit (spec §4.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::unit_clear)]] UnitClear {
time_offset_t time_offset;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<UnitClear>() == 6);
/// Newly accepted visible order — long form (spec §4.3.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_long)]] AddOrderLong {
time_offset_t time_offset;
order_id_t order_id;
side_t side;
std::uint32_t quantity;
symbol_t symbol;
long_price_t price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderLong>() == 35);
/// Newly accepted visible order — short form (spec §4.3.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_short)]] AddOrderShort {
time_offset_t time_offset;
order_id_t order_id;
side_t side;
std::uint16_t quantity;
symbol_short_t symbol;
short_price_t price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderShort>() == 25);
/// Newly accepted quote/order carrying attribution — used on the Cboe
/// Systematic Internaliser platform (spec §4.3.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_expanded)]] AddOrderExpanded {
time_offset_t time_offset;
order_id_t order_id;
side_t side;
std::uint32_t quantity;
symbol_t symbol;
long_price_t price;
std::uint8_t add_flags; ///< Bit 1 = SI Quote; bits 0, 2-7 reserved.
participant_id_t participant_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderExpanded>() == 40);
/// Visible order executed at its resting price (spec §4.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_executed)]] OrderExecuted {
time_offset_t time_offset;
order_id_t order_id;
std::uint32_t executed_shares;
execution_id_t execution_id;
execution_flags_t execution_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderExecuted>() == 30);
/// Visible order executed at a price different from the resting price (spec §4.5).
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_executed_at_price_size)]] OrderExecutedAtPriceSize {
time_offset_t time_offset;
order_id_t order_id;
std::uint32_t executed_shares;
std::uint32_t remaining_shares; ///< 0 → order fully removed from the book.
execution_id_t execution_id;
long_price_t price;
execution_flags_t execution_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderExecutedAtPriceSize>() == 42);
/// Partial visible-order cancel — long form (spec §4.6.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::reduce_size_long)]] ReduceSizeLong {
time_offset_t time_offset;
order_id_t order_id;
std::uint32_t cancelled_shares;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReduceSizeLong>() == 18);
/// Partial visible-order cancel — short form (spec §4.6.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::reduce_size_short)]] ReduceSizeShort {
time_offset_t time_offset;
order_id_t order_id;
std::uint16_t cancelled_shares;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReduceSizeShort>() == 16);
/// Visible order modification — long form (spec §4.7.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::modify_order_long)]] ModifyOrderLong {
time_offset_t time_offset;
order_id_t order_id;
std::uint32_t shares;
long_price_t price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ModifyOrderLong>() == 26);
/// Visible order modification — short form (spec §4.7.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::modify_order_short)]] ModifyOrderShort {
time_offset_t time_offset;
order_id_t order_id;
std::uint16_t shares;
short_price_t price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ModifyOrderShort>() == 18);
/// Complete visible-order cancel (spec §4.8).
struct [[=rbe::pack_le, =rbe::id(message_type_t::delete_order)]] DeleteOrder {
time_offset_t time_offset;
order_id_t order_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<DeleteOrder>() == 14);
/// Hidden-order or routed execution — long form (spec §4.9.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_long)]] TradeLong {
time_offset_t time_offset;
order_id_t order_id; ///< Obfuscated by default (spec §4.9).
side_t side; ///< Always 'B' for hidden trades.
std::uint32_t shares;
symbol_t symbol;
long_price_t price;
execution_id_t execution_id;
trade_flags_t trade_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeLong>() == 48);
/// Hidden-order or routed execution — short form (spec §4.9.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_short)]] TradeShort {
time_offset_t time_offset;
order_id_t order_id;
side_t side;
std::uint16_t shares;
symbol_short_t symbol;
short_price_t price;
execution_id_t execution_id;
trade_flags_t trade_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeShort>() == 38);
/// Extended trade details, only used on the Cboe European platform (spec §4.9.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_extended)]] TradeExtended {
time_offset_t time_offset;
std::uint64_t shares;
symbol_t symbol;
long_price_t price; ///< 0 if price pending (Level 3.8 = N).
trade_id_t trade_id;
timestamp_t trade_timestamp;
mic_t execution_venue;
currency_t currency;
std::uint8_t cboe_trade_flags; ///< 1-char alphanumeric, see spec §4.9.7.
extended_trade_flags_t extended_trade_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeExtended>() == 68);
/// Trade reported on an ISIN not known to Cboe — TRF only (spec §4.9.5).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_unknown_symbol)]] TradeUnknownSymbol {
time_offset_t time_offset;
std::uint64_t shares;
isin_t symbol; ///< ISIN in place of local symbol.
long_price_t price;
trade_id_t trade_id;
timestamp_t trade_timestamp;
mic_t execution_venue;
currency_t currency;
std::uint8_t cboe_trade_flags;
extended_trade_flags_t extended_trade_flags;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeUnknownSymbol>() == 72);
/// Break of an order-generated trade — carries only the execution id
/// of the broken trade (spec Appendix A / Appendix B, Trade Break).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_break)]] TradeBreak {
time_offset_t time_offset;
execution_id_t execution_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeBreak>() == 14);
/// End-of-session marker for the unit (spec §4.10).
struct [[=rbe::pack_le, =rbe::id(message_type_t::end_of_session)]] EndOfSession {
time_offset_t time_offset;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<EndOfSession>() == 6);
/// Start-of-transaction marker; subsequent messages up to the matching
/// TransactionEnd belong to the same transaction block (spec §4.11).
struct [[=rbe::pack_le, =rbe::id(message_type_t::transaction_begin)]] TransactionBegin {
time_offset_t time_offset;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TransactionBegin>() == 6);
/// End-of-transaction marker (spec §4.12).
struct [[=rbe::pack_le, =rbe::id(message_type_t::transaction_end)]] TransactionEnd {
time_offset_t time_offset;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TransactionEnd>() == 6);
/// Change in trading status for a security (spec §4.13).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trading_status)]] TradingStatus {
time_offset_t time_offset;
symbol_t symbol;
trading_status_code_t status;
std::array<char,3> reserved;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradingStatus>() == 18);
/// Disseminates opening / closing / high / low statistics prices — Cboe
/// European platform only (spec §4.14).
struct [[=rbe::pack_le, =rbe::id(message_type_t::statistics)]] Statistics {
time_offset_t time_offset;
symbol_t symbol;
long_price_t price;
statistic_type_t statistic_type;
price_determination_t price_determination;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Statistics>() == 24);
/// Indicative price / size during a call or extension phase (spec §4.15.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::auction_update)]] AuctionUpdate {
time_offset_t time_offset;
symbol_t symbol;
auction_type_t auction_type;
long_price_t reference_price;
long_price_t indicative_price;
std::uint32_t indicative_shares;
outside_tolerance_t outside_tolerance;
includes_primary_t includes_primary;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AuctionUpdate>() == 37);
/// Post-uncross auction result (spec §4.15.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::auction_summary)]] AuctionSummary {
time_offset_t time_offset;
symbol_t symbol;
auction_type_t auction_type;
long_price_t price;
std::uint32_t shares;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AuctionSummary>() == 27);
/// Real-time index quote (spec §4.16.1) — XIC/XID/XIE feeds only.
struct [[=rbe::pack_le, =rbe::id(message_type_t::index_quote)]] IndexQuote {
std::uint64_t timestamp; ///< Nanoseconds since midnight.
index_ticker_t index_ticker;
long_price_t price;
index_status_t index_status;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<IndexQuote>() == 29);
/// End-of-day exchange delivery settlement price for an index (spec §4.16.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::index_quote_edsp)]] IndexQuoteEDSP {
std::uint64_t timestamp;
index_ticker_t index_ticker;
long_price_t price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<IndexQuoteEDSP>() == 28);
// clang-format on
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
using messages = rbe::frames::any<
Login, LoginResponse, GapRequest, GapResponse, SpinImageAvailable, SpinRequest, SpinResponse, SpinFinished, Time,
UnitClear, AddOrderLong, AddOrderShort, AddOrderExpanded, OrderExecuted, OrderExecutedAtPriceSize, ReduceSizeLong,
ReduceSizeShort, ModifyOrderLong, ModifyOrderShort, DeleteOrder, TradeLong, TradeShort, TradeExtended,
TradeUnknownSymbol, TradeBreak, EndOfSession, TransactionBegin, TransactionEnd, TradingStatus, Statistics,
AuctionUpdate, AuctionSummary, IndexQuote, IndexQuoteEDSP>;
/// One PITCH / GRP / Spin message: `Header` followed by the message selected by `msg_type`.
using message = rbe::frames::frame<Header, messages>;
/// One UDP frame: `SequencedUnitHeader` followed by `count` back-to-back messages
/// (a `count` of zero is a heartbeat).
using packet = rbe::frames::frame<SequencedUnitHeader, rbe::frames::many<message>>;
} // namespace pitch
// =====================================================================
// Cboe US Equities — TOP v1.3.6 (spec §1–§10)
// =====================================================================
//
// TOP is a fixed-length, line-oriented ASCII protocol. Every message
// starts with a single-byte ASCII message type at offset 0 and ends
// with an LF (0x0A). There is no on-wire length field — the wire size
// is fixed per message type — so the shared `Header` here declares
// only `rbe::id`. The terminating LF is framing, not a message field:
// the message types below stop at their last field, and `line` (see
// the framing section) is the frame that accounts for the LF and for
// any LF padding after it. Messages with no fields of their own, such
// as the heartbeats, are `rbe::empty` candidates.
namespace top {
// ─────────────────────────────────────────────────────────────────────
// ASCII text field aliases (spec §1.3)
// ─────────────────────────────────────────────────────────────────────
using timestamp_t = std::array<char, 8>; ///< 8-digit ms past midnight, Eastern Time.
using seconds_t = std::array<char, 5>; ///< 5-digit seconds past midnight.
using milliseconds_t = std::array<char, 3>; ///< 3-digit ms past last Seconds message.
using symbol_short_t = std::array<char, 4>; ///< 4-char symbol.
using symbol_long_t = std::array<char, 6>; ///< 6-char symbol.
using symbol_wide_t = std::array<char, 8>; ///< 8-char symbol (ISRA / expanded).
using price_short_t = std::array<char, 5>; ///< 3+2 short-form price.
using price_long_t = std::array<char, 10>; ///< 6+4 long-form price.
using price_extended_t = std::array<char, 14>; ///< 8+6 extended-form price.
using qty_short_t = std::array<char, 5>; ///< Short-form quantity.
using qty_long_t = std::array<char, 6>; ///< Long/expanded/extended quantity.
using volume_t = std::array<char, 9>; ///< Cumulative volume (short trade uses 7).
using volume_short_t = std::array<char, 7>;
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
/// Every TOP message begins with one of these ASCII bytes (spec §4–§10).
enum class message_type_t : std::uint8_t {
logon = 'L',
logon_accepted = 'C',
logon_rejected = 'J',
expanded_spin = 's',
extended_spin = 'S',
spin_done = 'D',
server_heartbeat = 'H',
client_heartbeat = 'R',
seconds = 'T',
milliseconds = 'M',
extended_bid_update = 'N',
expanded_bid_update = 'E',
long_bid_update = 'B',
short_bid_update = 'b',
extended_ask_update = 'n',
expanded_ask_update = 'e',
long_ask_update = 'A',
short_ask_update = 'a',
expanded_two_sided_update = 'F',
long_two_sided_update = 'U',
short_two_sided_update = 'u',
extended_two_sided_update = 'd',
extended_trade = 'r',
expanded_trade = 'f',
long_trade = 'V',
short_trade = 'v',
trading_status = 't',
};
/// Boolean flag encoding on the wire.
enum class boolean_t : std::uint8_t {
yes = 'Y',
no = 'N',
};
/// Logon Rejected reason (spec §4.3).
enum class reject_reason_t : std::uint8_t {
auth_problem = 'A',
};
/// Halt status for Spin (spec §5.1.1) — Trading or Halted.
enum class halt_status_spin_t : std::uint8_t {
halted = 'H',
trading = 'T',
};
/// Halt status for Trading Status message (spec §10.1).
enum class halt_status_t : std::uint8_t {
accepting_for_queuing = 'A',
halted = 'H',
quote_only = 'Q',
suspension = 'S',
trading = 'T',
};
/// Reg SHO short-sale price-test flag (spec §5.1.1, §10.1).
enum class reg_sho_action_t : std::uint8_t {
no_test = '0',
in_effect = '1',
};
// ─────────────────────────────────────────────────────────────────────
// Common TOP message header
// ─────────────────────────────────────────────────────────────────────
/// TOP messages have no on-wire length field, only a 1-byte ASCII type.
/// Declared once for the whole protocol and composed with the message set
/// in `message`; `msg_type` selects the message and the wire size is fixed
/// per message type.
struct[[= rbe::pack_le]] Header {
[[= rbe::id]] message_type_t msg_type {};
};
// ─────────────────────────────────────────────────────────────────────
// Session / logon messages (spec §4)
// ─────────────────────────────────────────────────────────────────────
// clang-format off
/// Client → server logon (spec §4.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::logon)]] Logon {
std::array<char,6> username;
std::array<char,10> password;
boolean_t spin_flag; ///< 'Y' → send a spin of current top of book.
};
/// Server → client acceptance (spec §4.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::logon_accepted), =rbe::empty]] LogonAccepted {
};
/// Server → client rejection (spec §4.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::logon_rejected)]] LogonRejected {
reject_reason_t reject_reason;
};
// ─────────────────────────────────────────────────────────────────────
// Spin messages (spec §5)
// ─────────────────────────────────────────────────────────────────────
/// Per-symbol snapshot delivered during a spin, expanded form for ISRA
/// symbol sizes (spec §5.1.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::expanded_spin)]] ExpandedSpin {
timestamp_t timestamp;
symbol_wide_t symbol;
price_long_t bid_price;
qty_long_t bid_quantity;
price_long_t ask_price;
qty_long_t ask_quantity;
timestamp_t last_trade_time;
price_long_t last_trade_price;
qty_long_t last_trade_size;
volume_t cumulative_volume;
halt_status_spin_t halt_status;
reg_sho_action_t reg_sho_action;
std::uint8_t reserved_1;
std::uint8_t reserved_2;
};
/// Per-symbol snapshot with extended (14-char) prices (spec §5.1.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::extended_spin)]] ExtendedSpin {
timestamp_t timestamp;
symbol_wide_t symbol;
price_extended_t bid_price;
qty_long_t bid_quantity;
price_extended_t ask_price;
qty_long_t ask_quantity;
timestamp_t last_trade_time;
price_extended_t last_trade_price;
qty_long_t last_trade_size;
volume_t cumulative_volume;
halt_status_spin_t halt_status;
reg_sho_action_t reg_sho_action;
std::uint8_t reserved_1;
std::uint8_t reserved_2;
};
/// End-of-spin marker (spec §5.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::spin_done), =rbe::empty]] SpinDone {
};
// ─────────────────────────────────────────────────────────────────────
// Heartbeat messages (spec §6)
// ─────────────────────────────────────────────────────────────────────
struct [[=rbe::pack_le, =rbe::id(message_type_t::server_heartbeat), =rbe::empty]] ServerHeartbeat {
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::client_heartbeat), =rbe::empty]] ClientHeartbeat {
};
// ─────────────────────────────────────────────────────────────────────
// Time update messages (spec §7)
// ─────────────────────────────────────────────────────────────────────
/// Seconds past midnight, Eastern (spec §7.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::seconds)]] Seconds {
seconds_t seconds;
};
/// Milliseconds since the last Seconds message (spec §7.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::milliseconds)]] Milliseconds {
milliseconds_t milliseconds;
};
// ─────────────────────────────────────────────────────────────────────
// Bid / Ask update messages (spec §8.1)
// ─────────────────────────────────────────────────────────────────────
struct [[=rbe::pack_le, =rbe::id(message_type_t::extended_bid_update)]] ExtendedBidUpdate {
symbol_wide_t symbol;
price_extended_t bid_price;
qty_long_t bid_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::expanded_bid_update)]] ExpandedBidUpdate {
symbol_wide_t symbol;
price_long_t bid_price;
qty_long_t bid_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::long_bid_update)]] LongBidUpdate {
symbol_long_t symbol;
price_long_t bid_price;
qty_long_t bid_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::short_bid_update)]] ShortBidUpdate {
symbol_short_t symbol;
price_short_t bid_price;
qty_short_t bid_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::extended_ask_update)]] ExtendedAskUpdate {
symbol_wide_t symbol;
price_extended_t ask_price;
qty_long_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::expanded_ask_update)]] ExpandedAskUpdate {
symbol_wide_t symbol;
price_long_t ask_price;
qty_long_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::long_ask_update)]] LongAskUpdate {
symbol_long_t symbol;
price_long_t ask_price;
qty_long_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::short_ask_update)]] ShortAskUpdate {
symbol_short_t symbol;
price_short_t ask_price;
qty_short_t ask_quantity;
};
// ─────────────────────────────────────────────────────────────────────
// Two-sided update messages (spec §8.2)
// ─────────────────────────────────────────────────────────────────────
struct [[=rbe::pack_le, =rbe::id(message_type_t::expanded_two_sided_update)]] ExpandedTwoSidedUpdate {
symbol_wide_t symbol;
price_long_t bid_price;
qty_long_t bid_quantity;
price_long_t ask_price;
qty_long_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::long_two_sided_update)]] LongTwoSidedUpdate {
symbol_long_t symbol;
price_long_t bid_price;
qty_long_t bid_quantity;
price_long_t ask_price;
qty_long_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::short_two_sided_update)]] ShortTwoSidedUpdate {
symbol_short_t symbol;
price_short_t bid_price;
qty_short_t bid_quantity;
price_short_t ask_price;
qty_short_t ask_quantity;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::extended_two_sided_update)]] ExtendedTwoSidedUpdate {
symbol_wide_t symbol;
price_extended_t bid_price;
qty_long_t bid_quantity;
price_extended_t ask_price;
qty_long_t ask_quantity;
};
// ─────────────────────────────────────────────────────────────────────
// Trade messages (spec §9)
// ─────────────────────────────────────────────────────────────────────
struct [[=rbe::pack_le, =rbe::id(message_type_t::extended_trade)]] ExtendedTrade {
symbol_wide_t symbol;
price_extended_t last_price;
qty_long_t last_quantity;
volume_t cumulative_volume;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::expanded_trade)]] ExpandedTrade {
symbol_wide_t symbol;
price_long_t last_price;
qty_long_t last_quantity;
volume_t cumulative_volume;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::long_trade)]] LongTrade {
symbol_long_t symbol;
price_long_t last_price;
qty_long_t last_quantity;
volume_t cumulative_volume;
};
struct [[=rbe::pack_le, =rbe::id(message_type_t::short_trade)]] ShortTrade {
symbol_short_t symbol;
price_short_t last_price;
qty_short_t last_quantity;
volume_short_t cumulative_volume; ///< Short trade uses a 7-digit cumulative volume.
};
// ─────────────────────────────────────────────────────────────────────
// Administrative messages (spec §10)
// ─────────────────────────────────────────────────────────────────────
/// Change in a security's trading state (spec §10.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trading_status)]] TradingStatus {
symbol_wide_t symbol;
halt_status_t halt_status;
reg_sho_action_t reg_sho_action;
std::uint8_t reserved_1;
std::uint8_t reserved_2;
};
// clang-format on
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
using messages = rbe::frames::any<
Logon, LogonAccepted, LogonRejected, ExpandedSpin, ExtendedSpin, SpinDone, ServerHeartbeat, ClientHeartbeat,
Seconds, Milliseconds, ExtendedBidUpdate, ExpandedBidUpdate, LongBidUpdate, ShortBidUpdate, ExtendedAskUpdate,
ExpandedAskUpdate, LongAskUpdate, ShortAskUpdate, ExpandedTwoSidedUpdate, LongTwoSidedUpdate, ShortTwoSidedUpdate,
ExtendedTwoSidedUpdate, ExtendedTrade, ExpandedTrade, LongTrade, ShortTrade, TradingStatus>;
/// One TOP message without its line terminator: `Header` followed by the message selected by
/// `msg_type`, delimited by the id it carries. It is the message as a value, not
/// as it sits on the wire: on the wire every message is followed by an LF that no candidate accounts
/// for, so a `many<message>` would land on that LF -- `line` is the frame for a TOP stream.
using message = rbe::frames::frame<Header, messages>;
/// One TOP message as it sits on the wire: `message` plus its terminating LF and any LF padding after it.
///
/// TOP is line-oriented, so the end of a message is readable without knowing its type: the first LF
/// after the header. `line_view` is a custom frame that keeps the default header (step 1) and the default
/// payload construction (step 3, an `any` over the line, LF included) and swaps only the extent (step 2)
/// for that scan. Two things follow: `length()` covers the terminator and the padding, so `many<line>`
/// advances line by line, and the extent does not depend on the id, so an unknown `msg_type` is yielded
/// as an `any` over its own line and iteration goes on past it.
class line_view : public rbe::frames::dsrl::base_frame<Header, messages::dsrl_type> {
using base = rbe::frames::dsrl::base_frame<Header, messages::dsrl_type>;
public:
/// The scan for the terminator is not detectable from the shape, and the shape would get it wrong. For
/// this case is necessary to declare the delimiting concept explicitly.
using delimiting_concept = rbe::frames::self_delimited_tag;
static constexpr auto make(buffer_type const buf, std::size_t const scanned_offset = 0) -> std::optional<line_view> {
auto const hdr = header_return_type::make(buf); // step 1
if (not hdr.has_value()) {
return std::nullopt;
}
auto const rest = buf.subspan(hdr->length());
auto const extent = line_extent(rest, scanned_offset); // step 2: the only custom part
if (not extent.has_value()) {
return std::nullopt; // incomplete line: wait for more bytes
}
auto const line = rest.first(*extent);
return rbe::frames::dsrl::try_construct_payload<payload_type>(*hdr, line) // step 3
.transform([&](payload_return_type const p) { return line_view {*hdr, p, hdr->length() + line.size()}; });
}
/// precondition: buf holds a whole line
constexpr explicit line_view(buffer_type const buf) : line_view(*make(buf)) { }
[[nodiscard]] constexpr auto length() const -> size_type { return line_length_; }
private:
/// up to and including the terminating LF, then every LF that follows it; nullopt if the line is not complete yet
static constexpr auto line_extent(buffer_type const rest, std::size_t const scanned_offset = 0)
-> std::optional<size_type> {
auto const safe_offset = std::min(scanned_offset, rest.size());
auto const unsearched = rest.subspan(safe_offset);
auto const lf = std::ranges::find(unsearched, std::byte {'\n'});
if (lf == unsearched.end()) {
return std::nullopt;
}
// Calculate total extent from the beginning of 'rest', not from 'unsearched'
return std::make_optional(std::ranges::distance(rest.begin(), lf) + 1);
}
constexpr line_view(header_return_type const hdr, payload_return_type const p, size_type const n) :
base(hdr, p), line_length_(n) { }
size_type line_length_;
};
/// The vocabulary serder for `line_view`: what makes `rbe::frames::many<line>` compose, exactly as `rbe::frames::blob`
/// or `rbe::frames::frame` do. It keeps the header-plus-payload shape, so the delimiting concepts classify it.
struct line {
using header_type = Header;
using payload_type = messages;
using delimiting_concept = rbe::frames::self_delimited_tag; ///< the terminator scan of line_view, see above
using dsrl_type = line_view;
};
/// A run of TOP lines, LF padding included.
using lines = rbe::frames::many<line>;
} // namespace top
} // namespace cboe
Cboe TOP¶
Fixed-width ASCII lines terminated by a line feed, and no length anywhere. line_view is a custom frame
that scans for the terminator; many<line> walks the stream line by line and an unknown message type is
skipped rather than stopping the walk. The custom frame how-to takes it apart.
/// Cboe TOP: a line-oriented ASCII feed. `line` is a custom frame that delimits by the LF, so `many<line>` walks
/// the stream line by line and an unknown message type is yielded as an unknown id and iteration goes on.
void cboe_top_lines() {
using namespace cboe::top;
auto const stream = wire(
Header {.msg_type = message_type_t::seconds}, Seconds {.seconds = {'3', '6', '0', '0', '0'}}, std::array {'\n'},
Header {.msg_type = static_cast<message_type_t>('?')}, std::array {'x', 'y', '\n'},
Header {.msg_type = message_type_t::milliseconds}, Milliseconds {.milliseconds = {'0', '4', '2'}},
std::array {'\n'}
);
std::println("cboe top:");
for (auto const [header, payload]: stream | rbe::views::many<line_view>()) {
payload.match( // clang-format off
[](proxy<Seconds> const s) { std::println(" seconds {}", as_text(s.field<"seconds">().as_span())); },
[](proxy<Milliseconds> const ms) { std::println(" milliseconds {}", as_text(ms.field<"milliseconds">().as_span())); },
[](rbe::unmatched auto const other) { std::println(" skipped a line, id known: {}", other.known_id); }
); // clang-format on
}
}
London Stock Exchange¶
A unit header with a length and a count, then messages that each carry their length: the packet is a many
that can be filtered by id with rbe::views::with_ids before any field is read.
/// London: like Aquis, but every message carries its own length, so the packet payload is a `many` that can be
/// filtered with the views before touching a single field: `with_ids` keeps the ids you ask for, `known_ids` the ones
/// the code knows.
void london_packet() {
using namespace lse;
constexpr auto header_size = rbe::wire_size_of<Header>();
constexpr auto delete_size = header_size + rbe::wire_size_of<OrderDelete>();
constexpr auto event_size = header_size + rbe::wire_size_of<SystemEvent>();
auto const delete_of = [](order_id_t const id) {
auto del = OrderDelete {};
del.order_id = id;
return del;
};
auto const datagram = wire(
UnitHeader {.length = rbe::wire_size_of<UnitHeader>() + 2 * delete_size + event_size, .message_count = 3},
Header {.length = delete_size, .msg_type = message_type_t::order_delete}, delete_of(1),
Header {.length = event_size, .msg_type = message_type_t::system_event}, SystemEvent {},
Header {.length = delete_size, .msg_type = message_type_t::order_delete}, delete_of(2)
);
auto const packet = rbe::try_deserialize<lse::packet>(datagram).value();
auto [unit_header, messages] = packet;
std::println("london: packet of {} messages, {} bytes", unit_header.field<"message_count">(), packet.length());
for (auto const [header, payload]: messages | rbe::views::with_ids(message_type_t::order_delete)) {
payload.match(
[](proxy<OrderDelete> const del) { std::println(" order {} deleted", del.field<"order_id">()); },
[](rbe::unmatched auto const) { }
);
}
}
example/markets/london.hpp
/************************************************************************
* Copyright (c) 2026 Alvaro Cabrera Barrio
* This code is licensed under MIT license (see LICENSE.txt for details)
************************************************************************/
/**
* @file london.hpp
* @version 1.0
* @date 05/08/2026
* @brief London Stock Exchange Group Ticker Plant (GTP) market data protocol.
*
* Message set for the LSE Group Ticker Plant, GTP002 – Technical Guide
* (London Stock Exchange), issue 25.1 (11 December 2024). All multi-byte
* integers are little-endian on the wire (spec §3.5) and messages are
* 1-byte packed.
*
* Each UDP packet begins with an 8-byte Unit Header (spec §3.8) followed
* by one or more back-to-back messages. Every message starts with a
* 3-byte header carrying its 2-byte length and 1-byte type code. The
* header is declared once as `Header` and composed with the message set
* through `rbe::frames::frame`: `rbe::id` on the header field selects the message
* annotated with the matching `rbe::id(value)`, and `rbe::frame_length`
* bounds it. See `message` and `packet` at the end.
*/
#pragma once
#include <rbe/rbe.hpp>
#include <cstdint>
#include <tuple>
namespace lse {
// ─────────────────────────────────────────────────────────────────────
// Semantic type aliases (spec §3.5)
// ─────────────────────────────────────────────────────────────────────
/// Signed 64-bit price with 8 implied decimal places.
using price_t = std::int64_t;
/// Signed 64-bit price with 4 implied decimal places.
using price4_t = std::int64_t;
/// Unsigned 64-bit size with 8 implied decimal places.
using size_t_ = std::uint64_t;
/// Unsigned 64-bit size with 4 implied decimal places.
using size4_t = std::uint64_t;
/// Signed 64-bit quantity with 8 implied decimal places (Int Size).
using int_size_t = std::int64_t;
/// Nanoseconds since Unix epoch (UDT, spec §3.5).
using timestamp_t = std::uint64_t;
using instrument_id_t = std::uint64_t; ///< GTP instrument identifier.
using order_id_t = std::uint64_t; ///< Unique order identifier.
using trade_id_t = std::uint64_t; ///< Unique trade identifier.
using request_id_t = std::uint32_t; ///< Client-supplied echo value.
using sequence_number_t = std::uint32_t; ///< Real-time channel sequence number.
using exchange_market_size_t = std::uint64_t; ///< Exchange Market Size (EMS).
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
enum class message_type_t : std::uint8_t {
// Administrative messages (§3.6)
login_request = 0x01,
login_response = 0x02,
replay_request = 0x03,
replay_response = 0x04,
recovery_request = 0x81,
recovery_response = 0x82,
replay_and_recovery_complete = 0x83,
// Application messages (§3.7)
system_event = 0x53,
instrument_directory = 0x70,
instrument_directory_equities = 0x52,
instrument_status = 0x48,
add_order_mbo = 0x41,
add_order_short_mbo = 0x65,
add_order_mbp = 0x66,
add_order_short_mbp = 0x67,
add_order_incremental = 0x46,
order_modify = 0x55,
order_delete = 0x44,
top_of_book = 0x69,
order_book_clear = 0x79,
trade = 0x50,
statistics = 0x77,
statistics_update = 0x6A,
statistics_snapshot = 0x6b,
mifid_trade = 0x51,
trade_summary = 0x57,
analytics = 0x61,
};
/// Buy/sell side (Byte 'B'/'S').
enum class side_t : std::uint8_t {
buy = 'B',
sell = 'S',
};
/// Login Response status (§3.10.2).
enum class login_status_t : std::uint8_t {
accepted = 'A',
compid_inactive_or_suspended = 'a',
login_limit_reached = 'b',
service_unavailable = 'c',
max_connections_reached = 'd',
failed_other = 'e',
invalid_compid_or_ip = 'f',
};
/// Replay Response status (§3.10.3).
enum class replay_status_t : std::uint8_t {
accepted = 'A',
request_limit_reached = 'D',
out_of_range = 'O',
replay_unavailable = 'U',
concurrent_limit_reached = 'c',
failed_other = 'e',
};
/// Recovery Response status (§3.10.4).
enum class recovery_status_t : std::uint8_t {
accepted = 'A',
out_of_range = 'O',
invalid_group_or_instrument = 'a',
request_limit_reached = 'b',
concurrent_limit_reached = 'c',
invalid_recovery_type = 'd',
failed_other = 'e',
};
/// Recovery Request scope (§3.9.3).
enum class request_level_t : std::uint8_t {
instrument = 0,
group = 1,
multicast_channel = 2,
};
/// Recovery Request data category (§3.9.3).
enum class recovery_type_t : std::uint8_t {
instrument_directory = 0,
order_book = 1,
all_trades = 2,
statistics = 3,
instrument_status = 4,
reserved = 5,
system_event = 6,
};
/// Order-book type (§3.9.3 & §3.11.x).
enum class order_book_type_t : std::uint8_t {
all = 0, ///< Recovery Request only.
firm_quote_book = 1,
off_book = 2,
electronic_order_book = 3,
private_rfq = 4,
};
/// Source venue identifier (§3.11.x, UInt16).
enum class source_venue_t : std::uint16_t {
london_stock_exchange = 1,
};
/// System Event code (§3.11.1).
enum class event_code_t : std::uint8_t {
end_of_day = 'C',
start_of_day = 'O',
};
/// Instrument trading status (§3.11.4).
enum class trading_status_t : std::uint8_t {
halt = 'H',
halt_matching_partition_suspended = 'J',
halt_system_suspended = 'K',
regular_trading = 'T', ///< Or Start Trade Reporting.
halt_regulatory = 'P',
end_trade_reporting = 't',
opening_auction_call = 'a',
post_close = 'b',
closed = 'c',
closing_auction_call = 'd',
aesp_auction_call = 'e',
resume_auction = 'f',
pre_mandatory = 'm',
mandatory = 'n',
post_mandatory = 'o',
edsp_auction_call = 'q',
periodic_auction_call = 'r',
inactive = '1',
suspended = '2',
no_active_session = 'w',
end_of_post_close = 'x',
closing_price_crossing_session = 'u',
};
/// Instrument Status session change reason (§3.11.4).
enum class session_change_reason_t : std::uint8_t {
scheduled_transition = 0,
extended_by_market_ops = 1,
shortened_by_market_ops = 2,
market_order_imbalance = 3,
price_outside_range = 4,
aesp_or_circuit_breaker = 5,
unavailable = 9,
};
/// Add Order Incremental order type (§3.11.9).
enum class order_type_t : std::uint8_t {
limit_or_firm_quote = 0,
market = 1,
named_limit = 3,
named_market = 4,
executable_quotes = 5,
};
/// Clearing settlement mode (§3.11.3).
enum class clearing_type_t : std::uint8_t {
not_cleared = 0,
cleared = 1,
};
/// Trade category (§3.11.14 / §3.11.18).
enum class trade_type_t : std::uint8_t {
regular = 0,
auction_trade_bulk = 1,
auction_trade_individual = 2,
on_book_trade_cancellation = 9,
trade_correction = 11,
rfq_trade = 22,
rfq_trade_cancellation = 23,
rfq_trade_correction = 24,
};
/// Auction type (§3.11.14 / §3.11.17 / §3.11.18).
enum class auction_type_t : std::uint8_t {
none = 0,
closing_auction = 'C',
opening_auction = 'O',
aesp = 'A',
edsp = 'B',
resume_auction = 'E',
periodic_auction = 'F',
};
/// Hidden Execution Indicator (§3.11.14).
enum class hidden_execution_indicator_t : std::uint8_t {
not_applicable = 0,
visible = 1,
hidden = 2,
};
/// Trade Qualifier (§3.11.14 / §3.11.18).
enum class trade_qualifier_t : std::uint8_t {
na = ' ',
closing_price_cross = 'C',
};
/// Statistic type (§3.11.16, UInt16).
enum class statistic_type_t : std::uint16_t {
indicative_auction_uncrossing = 1,
official_opening_price = 2,
official_closing_price = 3,
trade_high_on_book = 4,
trade_low_on_book = 5,
trade_high_all = 6,
trade_low_all = 7,
trade_high_52_week = 8,
trade_low_52_week = 9,
best_closing_bid = 10,
best_closing_ask = 11,
static_reference_price = 16,
dynamic_reference_price = 17,
};
/// Statistics Update auction info (§3.11.16).
enum class auction_info_t : std::uint8_t {
not_applicable = 0,
buy_imbalance = 'B',
no_imbalance = 'N',
insufficient_orders_for_auction = 'O',
sell_imbalance = 'S',
};
/// Statistics opening/closing price indicator (§3.11.16 / §3.11.17).
enum class price_indicator_t : std::uint8_t {
none = 0,
ut = 'A',
at = 'B',
mid_of_bbo = 'C',
last_at = 'D',
last_ut = 'E',
manual = 'F',
previous_close = 'I',
};
/// Statistics Snapshot imbalance direction (§3.11.17).
enum class imbalance_direction_t : std::uint8_t {
buy_imbalance = 'B',
no_imbalance = 'N',
insufficient_orders_for_auction = 'O',
sell_imbalance = 'S',
};
/// MiFID II Trade market mechanism (§3.11.18).
enum class market_mechanism_t : std::uint8_t {
central_limit_order_book = 1,
request_for_quote = 6,
};
/// MiFID II Trade trading mode (§3.11.18).
enum class trading_mode_t : std::uint8_t {
scheduled_opening_auction = 'O',
scheduled_closing_auction = 'K',
scheduled_intraday_auction = 'I',
unscheduled_auction = 'U',
continuous_trading = '2',
at_market_close_trading = '3',
};
// ─────────────────────────────────────────────────────────────────────
// Fixed-width text fields (left-justified ASCII, space padded)
// ─────────────────────────────────────────────────────────────────────
using comp_id_t = std::array<char, 8>; ///< CompID (username).
using isin_t = std::array<char, 12>; ///< ISIN (§3.11.2).
using sedol_t = std::array<char, 8>; ///< SEDOL (§3.11.3).
using currency_t = std::array<char, 3>; ///< ISO 4217 currency code.
using segment_t = std::array<char, 6>; ///< Segment identifier.
using symbol_t = std::array<char, 8>; ///< Instrument symbol.
using description_t = std::array<char, 40>; ///< Instrument description.
using venue_instrument_id_t = std::array<char, 11>; ///< Source-venue instrument id.
using tick_id_t = std::array<char, 2>; ///< Tick structure id.
using ex_marker_code_t = std::array<char, 2>; ///< Ex-marker.
using country_code_t = std::array<char, 3>; ///< Country of register.
using security_exchange_t = std::array<char, 11>; ///< Security Exchange (not applicable to LSE).
using participant_t = std::array<char, 11>; ///< Trading participant identity.
using rfq_id_t = std::array<char, 10>; ///< Private RFQ identifier.
using group_id_t = std::array<char, 6>; ///< Recovery Request group id.
using date_ascii_t = std::array<char, 8>; ///< YYYYMMDD ASCII date.
using time_ascii_t = std::array<char, 6>; ///< HHMMSS ASCII time.
using date_time_iso_t = std::array<char, 27>; ///< ISO 8601 date-time string.
using mifid_decimal_t = std::array<char, 20>; ///< {DECIMAL-n/m} numeric string.
using transaction_id_t = std::array<char, 52>; ///< MiFID transaction identification code.
using iic_type_t = std::array<char, 4>; ///< Instrument Identification Code Type (e.g. "ISIN").
using price_notation_t = std::array<char, 4>; ///< "MONE" or "PERC".
using venue_of_execution_t = std::array<char, 4>; ///< MIC of execution venue.
using pt_flag_t = std::array<char, 4>; ///< Post-trade 4-byte alpha flag.
// ─────────────────────────────────────────────────────────────────────
// Unit Header (spec §3.8)
// ─────────────────────────────────────────────────────────────────────
/// Prefix of every UDP payload. Followed by `message_count` back-to-back
/// application or administrative messages. A Unit Header with
/// `message_count == 0` is the server's heartbeat (§3.10.1).
struct[[= rbe::pack_le]] UnitHeader {
[[= rbe::frame_length]] std::uint16_t length {}; ///< Total packet size incl. this header and payload.
std::uint8_t message_count {}; ///< Number of payload messages that follow.
std::uint8_t market_data_group {}; ///< Identity of the market data group (Byte).
sequence_number_t sequence_number {}; ///< Sequence number of the first payload message.
};
// ─────────────────────────────────────────────────────────────────────
// Common message header (§3.9 / §3.11)
// ─────────────────────────────────────────────────────────────────────
/// 3-byte header prefix of every LSE GTP message.
///
/// Declared once for the whole protocol and composed with the message set
/// in `message`: `msg_type` selects the message whose `rbe::id(value)`
/// matches, and `length` covers the header plus the message.
struct[[= rbe::pack_le]] Header {
[[= rbe::frame_length]] std::uint16_t length {};
[[= rbe::id]] message_type_t msg_type {};
};
// ─────────────────────────────────────────────────────────────────────
// Administrative messages (spec §3.9 – §3.10)
// ─────────────────────────────────────────────────────────────────────
// clang-format off
/// Client → server: log in to replay or recovery channel (§3.9.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::login_request)]] LoginRequest {
comp_id_t username;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<LoginRequest>() == 11);
/// Server → client: response to a login request (§3.10.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::login_response)]] LoginResponse {
login_status_t status;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<LoginResponse>() == 4);
/// Client → server: request retransmission on the replay channel (§3.9.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::replay_request)]] ReplayRequest {
std::uint32_t first_message; ///< Sequence number of first message in range.
std::uint32_t count; ///< Number of messages to be resent.
request_id_t request_id; ///< Echoed back in the Replay Response.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReplayRequest>() == 15);
/// Server → client: response to a replay request (§3.10.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::replay_response)]] ReplayResponse {
std::uint32_t first_message; ///< Zero if status != accepted.
std::uint32_t count; ///< Number of messages to follow (excl. completion).
replay_status_t status;
request_id_t request_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReplayResponse>() == 16);
/// Client → server: request a snapshot/reference data set (§3.9.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::recovery_request)]] RecoveryRequest {
request_level_t request_level;
instrument_id_t instrument; ///< Only used when request_level == instrument.
group_id_t group_id; ///< Only used when request_level == group.
order_book_type_t order_book_type;///< Only used when request_level == instrument.
source_venue_t source_venue; ///< Mandatory when request_level == group.
recovery_type_t recovery_type;
sequence_number_t sequence_number;///< Only valid if recovery_type == all_trades.
request_id_t request_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<RecoveryRequest>() == 30);
/// Server → client: response to a recovery request (§3.10.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::recovery_response)]] RecoveryResponse {
sequence_number_t sequence_number; ///< Snapshot sync point on real-time channel.
std::uint32_t count; ///< Number of messages to follow (excl. completion).
recovery_status_t status;
request_id_t request_id;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<RecoveryResponse>() == 16);
/// Server → client: marks the end of a replay or recovery session (§3.10.5).
struct [[=rbe::pack_le, =rbe::id(message_type_t::replay_and_recovery_complete)]] ReplayAndRecoveryComplete {
request_id_t request_id;
trading_status_t trading_status; ///< Populated only at end of individual order-book snapshot.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<ReplayAndRecoveryComplete>() == 8);
// ─────────────────────────────────────────────────────────────────────
// Application messages (spec §3.11)
// ─────────────────────────────────────────────────────────────────────
/// Start and end-of-day marker (§3.11.1).
struct [[=rbe::pack_le, =rbe::id(message_type_t::system_event)]] SystemEvent {
timestamp_t timestamp;
event_code_t event_code;
source_venue_t source_venue;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<SystemEvent>() == 14);
/// Instrument reference data — common set (§3.11.2).
struct [[=rbe::pack_le, =rbe::id(message_type_t::instrument_directory)]] InstrumentDirectory {
timestamp_t timestamp;
instrument_id_t instrument;
isin_t isin;
std::uint8_t allowed_book_types; ///< Bit field, see §3.11.2.
source_venue_t source_venue;
venue_instrument_id_t venue_instrument_id;
tick_id_t tick_id;
price_t reserved_field_1;
price_t dynamic_circuit_breaker_tolerances;
price_t static_circuit_breaker_tolerances;
segment_t segment;
std::array<char,12> reserved_field_2;
std::array<char,11> reserved_field_3;
currency_t currency;
std::uint8_t partition_id; ///< Byte.
std::array<char,4> reserved_field_4;
price4_t average_daily_turnover; ///< Not applicable to LSE.
std::array<char,8> reserved_field_5;
std::uint8_t reserved_field_6; ///< Bit field.
price_t reserved_field_7;
price_t reserved_field_8;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<InstrumentDirectory>() == 141);
/// Instrument reference data — equity-specific (§3.11.3).
struct [[=rbe::pack_le, =rbe::id(message_type_t::instrument_directory_equities)]] InstrumentDirectoryEquities {
timestamp_t timestamp;
instrument_id_t instrument;
isin_t isin;
sedol_t sedol;
std::uint8_t allowed_book_types; ///< Bit field, see §3.11.3.
source_venue_t source_venue;
venue_instrument_id_t venue_instrument_id;
segment_t segment;
currency_t currency;
tick_id_t tick_id;
price_t previous_day_closing_price;
price_t reserved_field_1;
price_t dynamic_circuit_breaker_tolerances;
price_t static_circuit_breaker_tolerances;
std::uint8_t reserved_field_2; ///< Bit field.
std::uint8_t reserved_field_3;
date_ascii_t expiration_date;
date_ascii_t listing_start_date;
date_ascii_t listing_end_date;
size_t_ minimum_lot; ///< Minimum Lot / Minimum Execution Size.
price_t last_price_in_preceding_session;
date_ascii_t last_price_in_preceding_session_date;
std::uint8_t reserved_field_4;
date_ascii_t reserved_field_5;
date_ascii_t reserved_field_6;
ex_marker_code_t ex_marker_code;
std::uint8_t security_type;
country_code_t country_of_register;
exchange_market_size_t exchange_market_size;
size_t_ minimum_peak_size_multiplier;
price_t security_maximum_spread;
clearing_type_t clearing_type;
price_t strike_price;
security_exchange_t security_exchange; ///< Not applicable to LSE.
std::array<char,12> reserved_field_7;
std::uint8_t reserved_field_8;
std::uint64_t reserved_field_9;
size_t_ reserved_field_10;
std::uint8_t partition_id; ///< Byte.
size_t_ reserved_field_11;
price_t reserved_field_12;
std::uint32_t reserved_field_13;
std::uint16_t reserved_field_14;
symbol_t symbol;
description_t description;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<InstrumentDirectoryEquities>() == 313);
/// Scheduled / unscheduled trading-status change (§3.11.4).
struct [[=rbe::pack_le, =rbe::id(message_type_t::instrument_status)]] InstrumentStatus {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
trading_status_t trading_status;
session_change_reason_t session_change_reason;
time_ascii_t new_end_time; ///< Local time (not UTC); spaces if no change.
order_book_type_t order_book_type;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<InstrumentStatus>() == 30);
/// First order of an MBO snapshot side (§3.11.5).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_mbo)]] AddOrderMBO {
timestamp_t timestamp;
order_id_t order_id;
side_t side;
size_t_ size;
instrument_id_t instrument;
price_t price;
price_t reserved_field;
source_venue_t source_venue;
order_book_type_t order_book_type;
participant_t participant;
std::uint8_t depth; ///< Total orders on this side.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderMBO>() == 67);
/// Subsequent order of an MBO snapshot side (§3.11.6).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_short_mbo)]] AddOrderShortMBO {
order_id_t order_id;
size_t_ size;
price_t price;
price_t reserved_field;
participant_t participant;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderShortMBO>() == 46);
/// First price point of an MBP snapshot side (§3.11.7).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_mbp)]] AddOrderMBP {
timestamp_t timestamp;
side_t side;
size_t_ size;
instrument_id_t instrument;
price_t price;
price_t reserved_field;
source_venue_t source_venue;
order_book_type_t order_book_type;
std::uint16_t splits; ///< Number of orders at this price point.
std::uint8_t depth;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderMBP>() == 50);
/// Subsequent price point of an MBP snapshot side (§3.11.8).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_short_mbp)]] AddOrderShortMBP {
size_t_ size;
price_t price;
price_t reserved_field;
std::uint16_t splits;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderShortMBP>() == 29);
/// New displayable order in the retrospective order book (§3.11.9).
struct [[=rbe::pack_le, =rbe::id(message_type_t::add_order_incremental)]] AddOrderIncremental {
timestamp_t timestamp;
order_id_t order_id;
side_t side;
size_t_ size;
instrument_id_t instrument;
price_t price;
timestamp_t transaction_time;
source_venue_t source_venue;
order_book_type_t order_book_type;
participant_t participant;
order_type_t order_type;
rfq_id_t rfq_id; ///< Populated only for Private RFQ.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<AddOrderIncremental>() == 77);
/// Order price/size modification (§3.11.10).
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_modify)]] OrderModify {
timestamp_t timestamp;
order_id_t order_id;
instrument_id_t instrument;
side_t side;
std::uint8_t flags; ///< Bit 0 – Priority Flag (§3.11.10).
order_book_type_t order_book_type;
size_t_ new_quantity;
price_t new_price;
price_t reserved_field;
source_venue_t source_venue;
price_t previous_price;
size_t_ previous_quantity;
timestamp_t transaction_time;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderModify>() == 80);
/// Order removal from the retrospective order book (§3.11.11).
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_delete)]] OrderDelete {
timestamp_t timestamp;
order_id_t order_id;
instrument_id_t instrument;
side_t side;
order_book_type_t order_book_type;
source_venue_t source_venue;
price_t previous_price;
size_t_ previous_quantity;
timestamp_t transaction_time;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderDelete>() == 55);
/// Level-1 top-of-book update (§3.11.12).
struct [[=rbe::pack_le, =rbe::id(message_type_t::top_of_book)]] TopOfBook {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
size_t_ bid_market_size; ///< Aggregated market-order size.
price_t bid_limit_price;
price_t reserved_field_1;
size_t_ bid_limit_size;
size_t_ offer_market_size;
price_t offer_limit_price;
price_t reserved_field_2;
size_t_ offer_limit_size;
order_book_type_t order_book_type;
std::uint8_t flags; ///< Bit 0 – Bid depth, bit 1 – Offer depth.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TopOfBook>() == 87);
/// Remove all orders from an instrument's book (§3.11.13).
struct [[=rbe::pack_le, =rbe::id(message_type_t::order_book_clear)]] OrderBookClear {
timestamp_t timestamp;
source_venue_t source_venue;
instrument_id_t instrument;
order_book_type_t order_book_type;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<OrderBookClear>() == 22);
/// Executed trade (§3.11.14).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade)]] Trade {
timestamp_t timestamp;
timestamp_t transaction_time;
source_venue_t source_venue;
size_t_ executed_size;
instrument_id_t instrument;
price_t price;
price_t reserved_field;
trade_id_t trade_id;
trade_type_t trade_type;
auction_type_t auction_type; ///< Only relevant when trade_type == auction_trade_bulk.
std::uint8_t flags; ///< Bit 0 – Cancellation, bit 1 – Correction.
hidden_execution_indicator_t hidden_execution_indicator;
trade_qualifier_t trade_qualifier;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Trade>() == 66);
/// Frequently-updated derived statistics (§3.11.15).
struct [[=rbe::pack_le, =rbe::id(message_type_t::statistics)]] Statistics {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
size4_t volume;
size4_t volume_on_book;
price4_t vwap;
price4_t vwap_on_book;
std::uint32_t number_of_trades;
std::uint32_t number_of_trades_on_book;
price4_t turnover;
price4_t turnover_on_book;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Statistics>() == 77);
/// Infrequently-updated derived statistics (§3.11.16).
struct [[=rbe::pack_le, =rbe::id(message_type_t::statistics_update)]] StatisticsUpdate {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
statistic_type_t statistic_type;
price_t statistic_price;
size_t_ statistic_size;
auction_type_t auction_type; ///< Populated if statistic_type == indicative_auction_uncrossing.
size_t_ imbalance_quantity;
auction_info_t auction_info; ///< Populated if statistic_type == indicative_auction_uncrossing.
price_indicator_t opening_closing_price_indicator; ///< Populated if statistic_type == official_opening/closing_price.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<StatisticsUpdate>() == 50);
/// Full statistics snapshot for recovery (§3.11.17).
struct [[=rbe::pack_le, =rbe::id(message_type_t::statistics_snapshot)]] StatisticsSnapshot {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
size4_t volume;
size4_t volume_on_book;
price4_t vwap;
price4_t vwap_on_book;
std::uint32_t number_of_trades;
std::uint32_t number_of_trades_on_book;
price4_t turnover;
price4_t turnover_on_book;
price_t official_opening_price;
price_t official_closing_price;
price_t trade_high_on_book;
price_t trade_low_on_book;
price_t trade_high;
price_t trade_low;
price_t trade_high_52_week;
price_t trade_low_52_week;
price_indicator_t opening_price_indicator;
price_indicator_t closing_price_indicator;
price_t iau_price; ///< Last indicative auction crossing price.
size_t_ iau_paired_size;
size_t_ imbalance_quantity;
imbalance_direction_t imbalance_direction;
price_t best_closing_bid_price;
price_t best_closing_ask_price;
size_t_ best_closing_bid_size;
size_t_ best_closing_ask_size;
price_t reserved_field_1;
price_t reserved_field_2;
size_t_ reserved_field_3;
price_t reserved_field_4;
auction_type_t auction_type;
price_t last_trade_price;
size_t_ last_trade_quantity;
timestamp_t last_trade_time;
price_t static_reference_price;
price_t dynamic_reference_price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<StatisticsSnapshot>() == 273);
/// MiFID II compliant trade (§3.11.18).
struct [[=rbe::pack_le, =rbe::id(message_type_t::mifid_trade)]] MiFIDTrade {
timestamp_t timestamp;
source_venue_t source_venue;
instrument_id_t instrument;
transaction_id_t transaction_identification_code;
trade_type_t trade_type;
auction_type_t auction_type; ///< Only relevant when trade_type == auction_trade_bulk.
mifid_decimal_t mifid_price;
mifid_decimal_t mifid_quantity;
date_time_iso_t trading_date_and_time;
iic_type_t instrument_identification_code_type;
isin_t instrument_identification_code;
price_notation_t price_notation;
currency_t price_major_currency;
mifid_decimal_t notional_amount;
currency_t notional_currency;
venue_of_execution_t venue_of_execution;
date_time_iso_t publication_date_and_time;
pt_flag_t pt_ref_price_waiver_flag; ///< Not applicable to LSE.
pt_flag_t reserved_field_1;
pt_flag_t market_closing_price_flag; ///< "CLSE" or blank.
pt_flag_t pt_algo_trade; ///< "ALGO" or blank.
pt_flag_t pt_cancellation_flag; ///< "CANC" or blank.
pt_flag_t pt_amendment_flag; ///< "AMND" or blank.
std::uint8_t reserved_field_2;
std::array<char,3> reserved_field_3;
mifid_decimal_t reserved_field_4;
pt_flag_t reserved_field_5;
trade_qualifier_t trade_qualifier;
market_mechanism_t market_mechanism;
trading_mode_t trading_mode;
std::uint8_t transaction_category; ///< 'D' Dark, '-' None.
std::uint8_t negotiation_indicator; ///< '-' Not a Negotiated Trade.
std::uint8_t agency_cross_indicator; ///< '-' No Agency Cross Trade.
std::uint8_t modification_indicator; ///< 'C' CANC, 'A' AMND, '-' New.
std::uint8_t reference_price_indicator; ///< '1' CLSE, '-' Not Reference.
std::uint8_t special_dividend_indicator; ///< '-' No Special Dividend.
std::uint8_t off_book_automated_indicator;///< '-' Unspecified.
std::uint8_t price_formation_indicator; ///< 'P' Plain-Vanilla.
std::uint8_t algorithmic_indicator; ///< 'H' ALGO, '-' Not Algorithmic.
std::uint8_t post_trade_deferral_reason; ///< '-' Immediate Publication.
std::uint8_t deferral_enrichment_type; ///< '-' Not Applicable.
std::uint8_t duplicative_indicator; ///< '-' Unique Trade Report.
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<MiFIDTrade>() == 286);
/// Aggregated multi- and single-fill trade summary (§3.11.19).
struct [[=rbe::pack_le, =rbe::id(message_type_t::trade_summary)]] TradeSummary {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
timestamp_t transaction_time;
price_t far_price;
size_t_ total_executed_quantity;
size_t_ total_hidden_executed_quantity;
size_t_ deleted_order_quantity; ///< Deleted due to SEP.
std::uint8_t side; ///< ' ' No side, 'B' Buy, 'S' Sell.
int_size_t best_bid_size; ///< -1 if top of book not disclosed.
price_t best_bid_price; ///< -1 if top of book not disclosed, 0 if market order on top.
int_size_t best_offer_size;
price_t best_offer_price;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<TradeSummary>() == 94);
/// Order-book activity statistics (§3.11.20).
struct [[=rbe::pack_le, =rbe::id(message_type_t::analytics)]] Analytics {
timestamp_t timestamp;
instrument_id_t instrument;
source_venue_t source_venue;
timestamp_t start_time;
timestamp_t end_time;
std::uint32_t buy_order_count;
std::uint32_t sell_order_count;
size4_t buy_order_size;
size4_t sell_order_size;
std::uint32_t buy_order_cancellations;
std::uint32_t sell_order_cancellations;
std::uint32_t buy_limit_order_cancellations;
std::uint32_t buy_market_order_cancellations;
std::uint32_t sell_limit_order_cancellations;
std::uint32_t sell_market_order_cancellations;
price_t bid_ask_spread;
price_t vwap_buy;
price_t vwap_sell;
};
static_assert(rbe::wire_size_of<Header>() + rbe::wire_size_of<Analytics>() == 109);
// clang-format on
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
using messages = rbe::frames::any<
LoginRequest, LoginResponse, ReplayRequest, ReplayResponse, RecoveryRequest, RecoveryResponse,
ReplayAndRecoveryComplete, SystemEvent, InstrumentDirectory, InstrumentDirectoryEquities, InstrumentStatus,
AddOrderMBO, AddOrderShortMBO, AddOrderMBP, AddOrderShortMBP, AddOrderIncremental, OrderModify, OrderDelete,
TopOfBook, OrderBookClear, Trade, Statistics, StatisticsUpdate, StatisticsSnapshot, MiFIDTrade, TradeSummary,
Analytics>;
/// One GTP message: `Header` followed by the message selected by `msg_type`.
using message = rbe::frames::frame<Header, messages>;
/// One UDP packet: `UnitHeader` followed by `message_count` back-to-back messages
/// (a `message_count` of zero is a heartbeat).
using packet = rbe::frames::frame<UnitHeader, rbe::frames::many<message>>;
} // namespace lse
Nasdaq¶
SoupBinTCP wraps exactly one ITCH message and declares its length; the ITCH message has no length of its
own. Two nested frames, read with flatten().
/// Nasdaq: SoupBinTCP wraps exactly one ITCH message and its `payload_length` says how long it is. The ITCH
/// message has no length of its own, the enclosing packet bounds it.
void nasdaq_packet() {
using namespace nasdaq;
auto const stream = wire(
SoupHeader {.length = rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<AddOrder>()},
ItchHeader {.msg_type = message_type_t::add_order, .stock_locate = 1, .tracking_number = 2},
AddOrder {
.order_reference_number = 1001,
.buy_sell_indicator = buy_sell_t::buy,
.shares = 300,
.stock = {'A', 'C', 'M', 'E', ' ', ' ', ' ', ' '},
.price = 1'250'000
}
);
auto const packet = rbe::try_deserialize<nasdaq::packet>(stream).value();
std::println("nasdaq: soup packet of {} bytes", packet.length());
// flatten walks the nested frames: both headers and the innermost payload in one go
auto const [soup, itch, payload] = packet.flatten();
std::println(" soup length {}, itch tracking number {}", soup.field<"length">(), itch.field<"tracking_number">());
payload.match(
[](proxy<AddOrder> const add) {
std::println(
" add order {}: {} shares @ {}", add.field<"order_reference_number">(), add.field<"shares">(),
add.field<"price">()
);
},
[](rbe::unmatched auto const other) { std::println(" not handled, id known: {}", other.known_id); }
);
}
example/markets/nasdaq.hpp
/************************************************************************
* Copyright (c) 2026 Alvaro Cabrera Barrio
* This code is licensed under MIT license (see LICENSE.txt for details)
************************************************************************/
/**
* @file nasdaq.hpp
* @version 1.0
* @date 05/08/2026
* @brief Nasdaq TotalView-ITCH 5.0 market data protocol.
*
* Message set for the Nasdaq TotalView-ITCH 5.0 direct data feed
* (Nasdaq Stock Market, LLC). All integer fields are big-endian
* (network byte order) and 1-byte packed on the wire (spec "Data
* Types").
*
* ITCH itself carries no length field within each message; the feed
* is delivered over SoupBinTCP, "Compressed SoupBinTCP" or MoldUDP64,
* each of which prefixes every payload with its own length. That
* layout is modelled as two nested frames: `SoupHeader`, a 2-byte
* big-endian length prefix that excludes itself (`rbe::payload_length`),
* wraps one ITCH message, which is the 11-byte `ItchHeader` (message
* type + stock locate + tracking number + 6-byte nanosecond timestamp)
* followed by the message selected by its type. See `message` and
* `packet` at the end.
*/
#pragma once
#include <rbe/rbe.hpp>
#include <cstdint>
#include <tuple>
namespace nasdaq {
// ─────────────────────────────────────────────────────────────────────
// Semantic type aliases (spec "Data Types")
// ─────────────────────────────────────────────────────────────────────
/// Price with 4 implied decimal places. e.g. 1'462'500 → 146.2500.
/// Maximum value is 200,000.0000 (decimal, 0x77359400 hex).
using price4_t = std::uint32_t;
/// Price with 8 implied decimal places. Used only by the MWCB Decline
/// Level message.
using price8_t = std::uint64_t;
/// Nanoseconds since midnight (Eastern Time). Wire-encoded in 6 bytes.
/// TODO: support 48-bit unsigned integer type
// TODO: use rbe::uint48_t once it exists; until then the 6 bytes are kept raw.
// using timestamp_t = rbe::uint48_t;
using timestamp_t = std::array<std::uint8_t, 6>;
using stock_locate_t = std::uint16_t; ///< Dynamically assigned locate code (0 = not stock-dependent).
using tracking_number_t = std::uint16_t; ///< Nasdaq internal tracking number.
using order_ref_t = std::uint64_t; ///< Day-unique order reference number.
using match_number_t = std::uint64_t; ///< Day-unique match number (referenced by Broken Trade).
using shares_t = std::uint32_t; ///< 32-bit share count field.
using shares64_t = std::uint64_t; ///< 64-bit share count (Cross Trade, NOII paired/imbalance shares).
// ─────────────────────────────────────────────────────────────────────
// Fixed-width text fields (left-justified ASCII, space-padded)
// ─────────────────────────────────────────────────────────────────────
using stock_t = std::array<char, 8>; ///< Stock symbol.
using mpid_t = std::array<char, 4>; ///< Nasdaq Market Participant Identifier.
using trading_action_reason_t = std::array<char, 4>; ///< See spec Appendix C.
using issue_sub_type_t = std::array<char, 2>; ///< See spec Appendix E.
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
/// One byte ASCII discriminator carried at offset 0 of every ITCH message.
enum class message_type_t : std::uint8_t {
system_event = 'S',
stock_directory = 'R',
stock_trading_action = 'H',
reg_sho_restriction = 'Y',
market_participant_position = 'L',
mwcb_decline_level = 'V',
mwcb_status = 'W',
ipo_quoting_period_update = 'K',
luld_auction_collar = 'J',
operational_halt = 'h',
add_order = 'A',
add_order_mpid = 'F',
order_executed = 'E',
order_executed_with_price = 'C',
order_cancel = 'X',
order_delete = 'D',
order_replace = 'U',
trade = 'P',
cross_trade = 'Q',
broken_trade = 'B',
noii = 'I',
rpii = 'N',
dlcr = 'O',
};
/// System event codes (spec §1.1).
enum class system_event_t : std::uint8_t {
start_of_messages = 'O',
start_of_system_hours = 'S',
start_of_market_hours = 'Q',
end_of_market_hours = 'M',
end_of_system_hours = 'E',
end_of_messages = 'C',
};
/// Listing market / market tier for the issue (spec §1.2.1).
enum class market_category_t : std::uint8_t {
nasdaq_global_select = 'Q',
nasdaq_global = 'G',
nasdaq_capital = 'S',
nyse = 'N',
nyse_american = 'A',
nyse_arca = 'P',
bats_z = 'Z',
iex = 'V',
not_available = ' ',
};
/// Nasdaq continued-listing compliance status (spec §1.2.1).
enum class financial_status_t : std::uint8_t {
deficient = 'D',
delinquent = 'E',
bankrupt = 'Q',
suspended = 'S',
deficient_and_bankrupt = 'G',
deficient_and_delinquent = 'H',
delinquent_and_bankrupt = 'J',
deficient_delinquent_and_bankrupt = 'K',
creations_redemptions_suspended = 'C',
normal = 'N',
not_available = ' ',
};
enum class round_lots_only_t : std::uint8_t {
yes = 'Y', ///< Nasdaq system only accepts round lots for this issue.
no = 'N',
};
enum class authenticity_t : std::uint8_t {
live_production = 'P',
test = 'T',
};
enum class short_sale_threshold_t : std::uint8_t {
restricted = 'Y',
not_restricted = 'N',
not_available = ' ',
};
enum class ipo_flag_t : std::uint8_t {
yes = 'Y',
no = 'N',
not_available = ' ',
};
enum class luld_reference_price_tier_t : std::uint8_t {
tier_1 = '1', ///< NMS Stocks and select ETPs.
tier_2 = '2', ///< Other NMS Stocks.
not_available = ' ',
};
enum class etp_flag_t : std::uint8_t {
yes = 'Y',
no = 'N',
not_available = ' ',
};
enum class inverse_indicator_t : std::uint8_t {
yes = 'Y',
no = 'N',
};
/// Current trading state disseminated by the Stock Trading Action
/// message (spec §1.2.2).
enum class trading_state_t : std::uint8_t {
halted = 'H', ///< Halted across all U.S. equity markets / SROs.
paused = 'P', ///< Paused (Nasdaq-listed securities only).
quotation_only = 'Q', ///< Quotation only period for cross-SRO halt or pause.
trading_on_nasdaq = 'T',
};
/// Reg SHO short-sale price-test restriction status (spec §1.2.3).
enum class reg_sho_action_t : std::uint8_t {
no_price_test = '0',
restriction_intraday = '1', ///< Restriction in effect due to an intraday price drop.
restriction_remains = '2',
};
enum class primary_market_maker_t : std::uint8_t {
yes = 'Y',
no = 'N',
};
/// Registration status vs SEC Rules 101/104 of Regulation M (spec §1.2.4).
enum class market_maker_mode_t : std::uint8_t {
normal = 'N',
passive = 'P',
syndicate = 'S',
pre_syndicate = 'R',
penalty = 'L',
};
/// Market participant registration status in the issue (spec §1.2.4).
enum class market_participant_state_t : std::uint8_t {
active = 'A',
excused_withdrawn = 'E',
withdrawn = 'W',
suspended = 'S',
deleted = 'D',
};
/// MWCB level breached in the MWCB Status message (spec §1.2.5.2).
enum class mwcb_level_t : std::uint8_t {
level_1 = '1',
level_2 = '2',
level_3 = '3',
};
/// IPO Quotation Release Qualifier (spec §1.2.6).
enum class ipo_quotation_release_qualifier_t : std::uint8_t {
anticipated = 'A', ///< Anticipated quotation release time.
cancelled = 'C', ///< IPO release cancelled / postponed.
};
/// Market centre for the Operational Halt message (spec §1.2.8).
enum class market_code_t : std::uint8_t {
nasdaq = 'Q',
bx = 'B',
psx = 'X',
};
/// Operational Halt Action (spec §1.2.8).
enum class operational_halt_action_t : std::uint8_t {
halted = 'H', ///< Operationally halted on the identified market.
trading = 'T', ///< Operational halt lifted and trading resumed.
};
/// Buy/Sell indicator (spec §1.3.1). Note: the Trade (Non-Cross)
/// message always sets this to `buy` regardless of resting side
/// (effective 2014-07-14).
enum class buy_sell_t : std::uint8_t {
buy = 'B',
sell = 'S',
};
/// Printable flag on the Order Executed With Price message (spec §1.4.2).
enum class printable_t : std::uint8_t {
non_printable = 'N',
printable = 'Y',
};
/// Nasdaq cross session for Cross Trade / NOII (spec §1.5.2 / §1.6).
/// The value 'A' (Extended Trading Close) is NOII-only.
enum class cross_type_t : std::uint8_t {
opening = 'O',
closing = 'C',
ipo_halt_pause = 'H',
extended_trading_close = 'A',
};
/// Market side of the NOII order imbalance (spec §1.6).
enum class imbalance_direction_t : std::uint8_t {
buy = 'B',
sell = 'S',
no_imbalance = 'N',
insufficient_orders = 'O',
paused = 'P',
};
/// Absolute % deviation of Near Indicative Clearing Price to Current
/// Reference Price (spec §1.6).
enum class price_variation_indicator_t : std::uint8_t {
less_than_1_pct = 'L',
pct_1_to_2 = '1',
pct_2_to_3 = '2',
pct_3_to_4 = '3',
pct_4_to_5 = '4',
pct_5_to_6 = '5',
pct_6_to_7 = '6',
pct_7_to_8 = '7',
pct_8_to_9 = '8',
pct_9_to_10 = '9',
pct_10_to_20 = 'A',
pct_20_to_30 = 'B',
pct_30_or_more = 'C',
not_calculated = ' ',
};
/// Retail interest side indicator on the RPII message (spec §1.7).
enum class interest_flag_t : std::uint8_t {
buy_side = 'B',
sell_side = 'S',
both = 'A',
none = 'N',
};
/// DLCR Open Eligibility Status (spec §1.8).
enum class open_eligibility_status_t : std::uint8_t {
not_eligible = 'N',
eligible = 'Y',
};
// ─────────────────────────────────────────────────────────────────────
// Common ITCH message header
// ─────────────────────────────────────────────────────────────────────
/// SoupBinTCP length prefix of every framed ITCH message: the length of
/// the ITCH message that follows, excluding this prefix.
struct[[= rbe::pack_be]] SoupHeader {
[[= rbe::payload_length]] std::uint16_t length {};
};
/// 11-byte common header of every ITCH message.
///
/// Layout on the wire:
/// [0..1) msg_type — one-byte ASCII message-type code
/// [1..3) stock_locate — dynamically assigned locate code (0 = not stock-dependent)
/// [3..5) tracking_number — Nasdaq internal tracking number
/// [5..11) timestamp — nanoseconds since midnight (Eastern Time)
///
/// Declared once for the whole protocol and composed with the message set
/// in `message`: `msg_type` selects the message whose `rbe::id(value)` matches.
struct[[= rbe::pack_be]] ItchHeader {
[[= rbe::id]] message_type_t msg_type {};
stock_locate_t stock_locate {};
tracking_number_t tracking_number {};
timestamp_t timestamp {};
};
// ─────────────────────────────────────────────────────────────────────
// Message definitions
// ─────────────────────────────────────────────────────────────────────
// clang-format off
// --- System event (spec §1.1) ----------------------------------------
struct [[=rbe::pack_be, =rbe::id(message_type_t::system_event)]] SystemEvent {
system_event_t event_code;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<SystemEvent>() == 12);
// --- Stock-related messages (spec §1.2) ------------------------------
/// Stock Directory (spec §1.2.1). Disseminated at the start of the day
/// for every active symbol; occasionally intraday for corrections.
struct [[=rbe::pack_be, =rbe::id(message_type_t::stock_directory)]] StockDirectory {
stock_t stock;
market_category_t market_category;
financial_status_t financial_status;
std::uint32_t round_lot_size;
round_lots_only_t round_lots_only;
std::uint8_t issue_classification; ///< See spec Appendix D.
issue_sub_type_t issue_sub_type; ///< See spec Appendix E.
authenticity_t authenticity;
short_sale_threshold_t short_sale_threshold;
ipo_flag_t ipo_flag;
luld_reference_price_tier_t luld_reference_price_tier;
etp_flag_t etp_flag;
std::uint32_t etp_leverage_factor; ///< Rounded down to nearest integer.
inverse_indicator_t inverse_indicator;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<StockDirectory>() == 39);
/// Stock Trading Action (spec §1.2.2).
struct [[=rbe::pack_be, =rbe::id(message_type_t::stock_trading_action)]] StockTradingAction {
stock_t stock;
trading_state_t trading_state;
std::uint8_t reserved;
trading_action_reason_t reason; ///< See spec Appendix C.
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<StockTradingAction>() == 25);
/// Reg SHO Short Sale Price Test Restricted Indicator (spec §1.2.3).
struct [[=rbe::pack_be, =rbe::id(message_type_t::reg_sho_restriction)]] RegSHORestriction {
stock_t stock;
reg_sho_action_t reg_sho_action;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<RegSHORestriction>() == 20);
/// Market Participant Position (spec §1.2.4).
struct [[=rbe::pack_be, =rbe::id(message_type_t::market_participant_position)]] MarketParticipantPosition {
mpid_t mpid;
stock_t stock;
primary_market_maker_t primary_market_maker;
market_maker_mode_t market_maker_mode;
market_participant_state_t market_participant_state;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<MarketParticipantPosition>() == 26);
/// MWCB Decline Level Message (spec §1.2.5.1). Stock Locate always 0.
struct [[=rbe::pack_be, =rbe::id(message_type_t::mwcb_decline_level)]] MWCBDeclineLevel {
price8_t level_1;
price8_t level_2;
price8_t level_3;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<MWCBDeclineLevel>() == 35);
/// MWCB Status Message (spec §1.2.5.2). Stock Locate always 0.
struct [[=rbe::pack_be, =rbe::id(message_type_t::mwcb_status)]] MWCBStatus {
mwcb_level_t breached_level;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<MWCBStatus>() == 12);
/// IPO Quoting Period Update (spec §1.2.6). Stock Locate always 0.
struct [[=rbe::pack_be, =rbe::id(message_type_t::ipo_quoting_period_update)]] IPOQuotingPeriodUpdate {
stock_t stock;
std::uint32_t ipo_quotation_release_time; ///< Seconds since midnight; 0 if cancelled.
ipo_quotation_release_qualifier_t ipo_quotation_release_qualifier;
price4_t ipo_price; ///< 0 if quotation cancelled/postponed.
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<IPOQuotingPeriodUpdate>() == 28);
/// LULD Auction Collar (spec §1.2.7).
struct [[=rbe::pack_be, =rbe::id(message_type_t::luld_auction_collar)]] LULDAuctionCollar {
stock_t stock;
price4_t auction_collar_reference_price;
price4_t upper_auction_collar_price;
price4_t lower_auction_collar_price;
std::uint32_t auction_collar_extension;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<LULDAuctionCollar>() == 35);
/// Operational Halt (spec §1.2.8).
struct [[=rbe::pack_be, =rbe::id(message_type_t::operational_halt)]] OperationalHalt {
stock_t stock;
market_code_t market_code;
operational_halt_action_t operational_halt_action;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OperationalHalt>() == 21);
// --- Add Order messages (spec §1.3) ----------------------------------
/// Add Order — No MPID Attribution (spec §1.3.1).
struct [[=rbe::pack_be, =rbe::id(message_type_t::add_order)]] AddOrder {
order_ref_t order_reference_number;
buy_sell_t buy_sell_indicator;
shares_t shares;
stock_t stock;
price4_t price;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<AddOrder>() == 36);
/// Add Order with MPID Attribution (spec §1.3.2).
struct [[=rbe::pack_be, =rbe::id(message_type_t::add_order_mpid)]] AddOrderMPID {
order_ref_t order_reference_number;
buy_sell_t buy_sell_indicator;
shares_t shares;
stock_t stock;
price4_t price;
mpid_t attribution;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<AddOrderMPID>() == 40);
// --- Modify Order messages (spec §1.4) -------------------------------
/// Order Executed Message (spec §1.4.1).
struct [[=rbe::pack_be, =rbe::id(message_type_t::order_executed)]] OrderExecuted {
order_ref_t order_reference_number;
shares_t executed_shares;
match_number_t match_number;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OrderExecuted>() == 31);
/// Order Executed With Price Message (spec §1.4.2). May be marked
/// non-printable when the shares are rolled into a later bulk print.
struct [[=rbe::pack_be, =rbe::id(message_type_t::order_executed_with_price)]] OrderExecutedWithPrice {
order_ref_t order_reference_number;
shares_t executed_shares;
match_number_t match_number;
printable_t printable;
price4_t execution_price;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OrderExecutedWithPrice>() == 36);
/// Order Cancel Message — partial cancellation (spec §1.4.3).
struct [[=rbe::pack_be, =rbe::id(message_type_t::order_cancel)]] OrderCancel {
order_ref_t order_reference_number;
shares_t cancelled_shares;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OrderCancel>() == 23);
/// Order Delete Message — full cancellation (spec §1.4.4).
struct [[=rbe::pack_be, =rbe::id(message_type_t::order_delete)]] OrderDelete {
order_ref_t order_reference_number;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OrderDelete>() == 19);
/// Order Replace Message (spec §1.4.5). Side, stock and MPID are not
/// carried — firms should retain them from the original Add Order.
struct [[=rbe::pack_be, =rbe::id(message_type_t::order_replace)]] OrderReplace {
order_ref_t original_order_reference_number;
order_ref_t new_order_reference_number;
shares_t shares;
price4_t price;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<OrderReplace>() == 35);
// --- Trade messages (spec §1.5) --------------------------------------
/// Trade Message (Non-Cross) (spec §1.5.1). Emitted for non-displayable
/// order matches. `order_reference_number` is always zero (effective
/// 2010-12-06) and `buy_sell_indicator` is always `buy` (effective
/// 2014-07-14).
struct [[=rbe::pack_be, =rbe::id(message_type_t::trade)]] Trade {
order_ref_t order_reference_number;
buy_sell_t buy_sell_indicator;
shares_t shares;
stock_t stock;
price4_t price;
match_number_t match_number;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<Trade>() == 44);
/// Cross Trade Message (spec §1.5.2). Sent after the Opening, Closing
/// and EMC cross events for every active issue.
struct [[=rbe::pack_be, =rbe::id(message_type_t::cross_trade)]] CrossTrade {
shares64_t shares;
stock_t stock;
price4_t cross_price;
match_number_t match_number;
cross_type_t cross_type;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<CrossTrade>() == 40);
/// Broken Trade / Order Execution Message (spec §1.5.3). References
/// the match number of a previous execution or trade.
struct [[=rbe::pack_be, =rbe::id(message_type_t::broken_trade)]] BrokenTrade {
match_number_t match_number;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<BrokenTrade>() == 19);
// --- NOII (spec §1.6) ------------------------------------------------
/// Net Order Imbalance Indicator (spec §1.6).
struct [[=rbe::pack_be, =rbe::id(message_type_t::noii)]] NOII {
shares64_t paired_shares;
shares64_t imbalance_shares;
imbalance_direction_t imbalance_direction;
stock_t stock;
price4_t far_price;
price4_t near_price;
price4_t current_reference_price;
cross_type_t cross_type;
price_variation_indicator_t price_variation_indicator;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<NOII>() == 50);
// --- RPII (spec §1.7) ------------------------------------------------
/// Retail Price Improvement Indicator (spec §1.7).
struct [[=rbe::pack_be, =rbe::id(message_type_t::rpii)]] RPII {
stock_t stock;
interest_flag_t interest_flag;
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<RPII>() == 20);
// --- DLCR (spec §1.8) ------------------------------------------------
/// Direct Listing with Capital Raise Price Discovery Message (spec §1.8).
/// Disseminated once per second after the DLCR volatility test passes.
struct [[=rbe::pack_be, =rbe::id(message_type_t::dlcr)]] DLCR {
stock_t stock;
open_eligibility_status_t open_eligibility_status;
price4_t minimum_allowable_price; ///< 20% below Registration Statement Lower Price.
price4_t maximum_allowable_price; ///< 80% above Registration Statement Highest Price.
price4_t near_execution_price; ///< Current reference price when volatility test passed.
std::uint64_t near_execution_time; ///< Time at which the near execution price was set.
price4_t lower_price_range_collar; ///< 10% below the Near Execution Price.
price4_t upper_price_range_collar; ///< 10% above the Near Execution Price.
};
static_assert(rbe::wire_size_of<ItchHeader>() + rbe::wire_size_of<DLCR>() == 48);
// clang-format on
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
using messages = rbe::frames::any<
SystemEvent, StockDirectory, StockTradingAction, RegSHORestriction, MarketParticipantPosition, MWCBDeclineLevel,
MWCBStatus, IPOQuotingPeriodUpdate, LULDAuctionCollar, OperationalHalt, AddOrder, AddOrderMPID, OrderExecuted,
OrderExecutedWithPrice, OrderCancel, OrderDelete, OrderReplace, Trade, CrossTrade, BrokenTrade, NOII, RPII, DLCR>;
/// One ITCH message: `ItchHeader` followed by the message selected by `msg_type`.
/// ITCH carries no length of its own: it is bounded by the enclosing `packet`.
using message = rbe::frames::frame<ItchHeader, messages>;
/// One SoupBinTCP packet: `SoupHeader` followed by exactly one ITCH message.
using packet = rbe::frames::frame<SoupHeader, message>;
} // namespace nasdaq
OPRA¶
The header carries a category and a type and no length: the message is implicitly delimited, the
candidate the id selects has the size. Control is a header-only message, =rbe::empty, zero bytes of
payload.
/// OPRA: the message header carries no length and the category picks the layout. `Control` is a header-only message,
/// an `rbe::empty` alternative with a wire size of 0.
void opra_message() {
using namespace opra;
auto const datagram = wire(
Header {.msg_category = msg_category_t::control, .msg_type = std::to_underlying(control_type_t::start_of_day)}
);
auto const [header, payload] = rbe::try_deserialize<opra::message>(datagram).value();
auto const msg_type = header.field<"msg_type">();
payload.match(
[msg_type](Control const&) { std::println("opra: control, type {}", msg_type); },
[](rbe::unmatched auto const other) { std::println("opra: not handled, id known: {}", other.known_id); }
);
}
example/markets/opra.hpp
/************************************************************************
* Copyright (c) 2026 Alvaro Cabrera Barrio
* This code is licensed under MIT license (see LICENSE.txt for details)
************************************************************************/
/**
* @file opra.hpp
* @version 1.0
* @date 05/08/2026
* @brief OPRA (Options Price Reporting Authority) Binary Data Recipient
* Interface protocol.
*
* Message set for the OPRA Binary Data Recipient Interface Specification,
* version 6.4a (July 1, 2026). OPRA is the US options consolidated feed
* disseminating trades, quotes and administrative data from all Options
* Participants over IP multicast.
*
* All numeric fields are 1/2/4/8-byte binary integers transmitted in
* network byte order (BIG-ENDIAN) per spec §3.04. Every IP multicast
* payload begins with a 21-byte `BlockHeader` (spec §3.05) followed by
* one or more messages. Each message is prefixed by a 12-byte message
* `Header` (spec §4.0) carrying the Participant ID, Message Category,
* Message Type and Message Indicator that together identify the message
* layout. OPRA does not carry a per-message length on the wire (except
* for Administrative category-C messages): message length is derived
* from the (category, indicator) tuple as defined in §5.
*
* The message `Header` is declared once and composed with the message set
* through `rbe::frames::frame` (see `message` at the end). This is a partial model:
* `rbe::id` only dispatches on `msg_category`, while OPRA also needs
* `msg_type` for some categories and `msg_indicator` to size the optional
* BBO appendages, and Administrative messages carry their length inside
* the body. Those gaps are marked with TODOs.
*/
#pragma once
#include <rbe/rbe.hpp>
#include <cstdint>
#include <tuple>
namespace opra {
// ─────────────────────────────────────────────────────────────────────
// Semantic type aliases (spec §3.04, §7)
// ─────────────────────────────────────────────────────────────────────
/// Price / index value with a floating decimal position given by the
/// accompanying Denominator Code (see denominator_code_t). Signed
/// 4-byte integer in all message categories except Short Quote
/// (Category q), which uses `short_price_t`. Represents the whole and
/// decimal portion of a price with the denominator determining the
/// location of the decimal point (spec §7.06, §7.11, §7.22, …).
using price_t = std::int32_t;
/// 2-byte unsigned Short Quote price/size wire type (spec §6.04.2).
using short_price_t = std::uint16_t;
/// 8-byte signed underlying stock price (spec §7.36).
using underlying_price_t = std::int64_t;
/// 4-byte unsigned volume / size / open interest quantity.
using size_t = std::uint32_t;
/// Block sequence number of the first message in a transmission block
/// (spec §3.05.6). Rolls over from 4,294,967,295 to 1.
using block_seq_no_t = std::uint32_t;
using transaction_id_t = std::uint32_t; ///< Reserved for internal use (spec §4.01.5).
using participant_ref_t = std::uint32_t; ///< Participant reference number (spec §4.01.6).
using trade_id_t = std::uint32_t; ///< Trade Identifier – for future use (spec §7.33).
/// 32-bit seconds since Unix epoch (spec §3.05.8, first half of Block Timestamp).
using epoch_seconds_t = std::uint32_t;
/// Nanosecond fraction of the Block Timestamp (spec §3.05.8, second half).
using nanoseconds_t = std::uint32_t;
// ─────────────────────────────────────────────────────────────────────
// Enumerations
// ─────────────────────────────────────────────────────────────────────
/// Participant / Processor identifier (spec §4.01.1).
enum class participant_id_t : std::uint8_t {
amex = 'A', ///< NYSE American
box = 'B', ///< Boston Options Exchange
cboe = 'C', ///< Cboe Options Exchange
emerald = 'D', ///< MIAX Emerald
edgx = 'E', ///< Cboe EDGX Options Exchange
mx2 = 'G', ///< MX2 Options
gemx = 'H', ///< Nasdaq GEMX
ise = 'I', ///< Nasdaq ISE
mrx = 'J', ///< Nasdaq MRX
miax = 'M', ///< Miami International Securities Exchange
nyse = 'N', ///< NYSE / NYSE Arca
opra = 'O', ///< Options Price Reporting Authority (sent by SIAC)
pearl = 'P', ///< MIAX PEARL
nasd = 'Q', ///< NASDAQ Options Market
sphr = 'S', ///< MIAX Sapphire
bx = 'T', ///< NASDAQ BX Options
memx = 'U', ///< Members Options Exchange
iex = 'V', ///< IEX Options LLC
c2 = 'W', ///< Cboe C2 Options Exchange
phlx = 'X', ///< NASDAQ PHLX
bats = 'Z', ///< Cboe BZX Options Exchange
};
/// Message Category (spec §4.01.2). Determines the base message layout.
enum class msg_category_t : std::uint8_t {
last_sale = 'a', ///< Equity and Index Last Sale
open_interest = 'd', ///< Open Interest
eod_summary = 'f', ///< Equity and Index End of Day Summary
long_quote = 'k', ///< Long Equity and Index Quote
short_quote = 'q', ///< Short Equity and Index Quote
administrative = 'C', ///< Administrative (unformatted, variable length)
control = 'H', ///< Control (Start of Day, End of Day, etc.)
series_mapping = 'R', ///< Series Mapping Message
underlying_value = 'Y', ///< Underlying Value Message
};
/// Message Type codes for Category `a` (Equity and Index Last Sale)
/// messages (spec §6.01). All mutually exclusive.
enum class last_sale_type_t : std::uint8_t {
cancel_previous = 'A', ///< CANC – Cancel a previously reported (non-last, non-opening) trade.
out_of_sequence = 'B', ///< OSEQ – Late report, out of sequence.
cancel_last = 'C', ///< CNCL – Cancel the last reported trade.
late_in_sequence = 'D', ///< LATE – Late report, in correct sequence.
cancel_opening = 'E', ///< CNCO – Cancel the opening trade (others followed).
opening_late_oseq = 'F', ///< OPEN – Late opening trade, out of sequence.
cancel_only = 'G', ///< CNOL – Cancel the only reported trade.
opening_late_in_seq = 'H', ///< OPNL – Late opening trade, in sequence.
auto_execution = 'I', ///< AUTO – Electronic execution.
reopening = 'J', ///< REOP – Reopening after halt.
intermarket_sweep = 'S', ///< ISOI – Intermarket Sweep Order execution.
single_leg_auction = 'a', ///< SLAN – Single Leg Auction, Non-ISO.
single_leg_auction_iso = 'b', ///< SLAI – Single Leg Auction, ISO.
single_leg_cross = 'c', ///< SLCN – Single Leg Cross, Non-ISO.
single_leg_cross_iso = 'd', ///< SCLI – Single Leg Cross, ISO.
single_leg_floor = 'e', ///< SLFT – Single Leg Floor Trade.
multi_leg_electronic = 'f', ///< MLET – Multi Leg auto-electronic trade.
multi_leg_auction = 'g', ///< MLAT – Multi Leg Auction.
multi_leg_cross = 'h', ///< MLCT – Multi Leg Cross.
multi_leg_floor = 'i', ///< MLFT – Multi Leg floor trade.
multi_leg_esl = 'j', ///< MESL – Multi Leg auto-electronic trade against single legs.
stock_opt_auction = 'k', ///< TLAT – Stock Options Auction.
multi_leg_asl = 'l', ///< MASL – Multi Leg Auction against single legs.
multi_leg_fsl = 'm', ///< MFSL – Multi Leg floor trade against single legs.
stock_opt_electronic = 'n', ///< TLET – Stock Options auto-electronic trade.
stock_opt_cross = 'o', ///< TLCT – Stock Options Cross.
stock_opt_floor = 'p', ///< TLFT – Stock Options floor trade.
stock_opt_esl = 'q', ///< TESL – Stock Options auto-electronic trade against single legs.
stock_opt_asl = 'r', ///< TASL – Stock Options Auction against single legs.
stock_opt_fsl = 's', ///< TFSL – Stock Options floor trade against single legs.
cbmo = 't', ///< CBMO – Multi Leg Floor Trade of Proprietary Products.
mctp = 'u', ///< MCTP – Multilateral Compression Trade of Proprietary Products.
extended_hours = 'v', ///< EXHT – Extended Hours Trade (to be retired; see Trading Session Identifier).
};
/// Message Type codes for quote categories `k` and `q` (spec §6.04).
enum class quote_type_t : std::uint8_t {
regular = ' ', ///< Regular Trading (space filled).
non_firm = 'F', ///< Non-Firm Quote.
indicative = 'I', ///< Indicative Value.
rotation = 'R', ///< Rotation.
trading_halted = 'T', ///< Trading Halted.
auto_execution_eligible = 'A', ///< Eligible for Automatic Execution.
bid_customer_interest = 'B', ///< Bid contains Customer Trading Interest.
offer_customer_interest = 'O', ///< Offer contains Customer Trading Interest.
both_customer_interest = 'C', ///< Both Bid and Offer contain Customer Trading Interest.
offer_not_firm = 'X', ///< Offer Side of Quote Not Firm; Bid Side Firm.
bid_not_firm = 'Y', ///< Bid Side of Quote Not Firm; Offer Side Firm.
};
/// Message Type codes for Category `H` (Control) messages (spec §6.06).
enum class control_type_t : std::uint8_t {
start_of_day = 'C', ///< Start of Day (Participant ID 'O').
start_of_summary = 'E', ///< Start of Summary (Participant ID != 'O').
end_of_summary = 'F', ///< End of Summary (Participant ID != 'O').
end_of_day = 'J', ///< End of Day (Participant ID 'O').
reset_block_seq_no = 'K', ///< Reset Block Sequence Number (Participant ID 'O').
start_of_open_int = 'L', ///< Start of Open Interest (Participant ID 'O').
end_of_open_int = 'M', ///< End of Open Interest (Participant ID 'O').
line_integrity = 'N', ///< Line Integrity (Participant ID 'O').
dr_activation = 'P', ///< Disaster Recovery Data Center Activation (Participant ID 'O').
};
/// Message Type codes for Category `Y` (Underlying Value) messages (spec §6.08).
enum class underlying_value_type_t : std::uint8_t {
last_sale = ' ', ///< Index based on Last Sale (space filled).
bid_and_offer = 'I', ///< Index based on Bid and Offer.
};
/// Message Type code for Category `R` (Series Mapping) messages (spec §6.07).
enum class series_mapping_type_t : std::uint8_t {
series_mapping = 'A',
};
/// BBO Indicator carried in the Message Indicator field of quote
/// messages (categories k, q). Determines whether a Best Bid / Best
/// Offer appendage follows the quote payload (spec §7.01).
enum class bbo_indicator_t : std::uint8_t {
no_bid_change_no_offer_change = 'A',
no_bid_change_contains_offer = 'B',
no_bid_change_offer_appendage = 'C',
no_bid_change_no_offer = 'D',
contains_bid_no_offer_change = 'E',
contains_bid_contains_offer = 'F',
contains_bid_offer_appendage = 'G',
contains_bid_no_offer = 'H',
no_bid_no_offer_change = 'I',
no_bid_contains_offer = 'J',
no_bid_offer_appendage = 'K',
no_bid_no_offer = 'L',
bid_appendage_no_offer_change = 'M',
bid_appendage_contains_offer = 'N',
bid_appendage_offer_appendage = 'O', ///< Double appendage.
bid_appendage_no_offer = 'P',
not_in_bbo = ' ', ///< Quote did not meet BBO requirements.
};
/// Data Feed Indicator (spec §3.05.3). Always 'O' for OPRA.
enum class data_feed_indicator_t : std::uint8_t {
opra = 'O',
};
/// Retransmission Indicator (spec §3.05.4).
enum class retransmission_indicator_t : std::uint8_t {
original = ' ', ///< Not a retransmitted block.
retransmitted = 'V', ///< Retransmitted block.
};
/// Session Indicator in the Block Header (spec §3.05.5) and Message
/// Header field carried in some contexts (spec §7.30).
///
/// Values 1–5 identify the Global Trading Hours (GTH) session for
/// Monday–Friday respectively; 0x00 is Regular Trading Hours; 'X' is
/// used on messages generated by OPRA itself during GTH.
enum class session_indicator_t : std::uint8_t {
regular = 0x00,
gth_monday = 0x01,
gth_tuesday = 0x02,
gth_wednesday = 0x03,
gth_thursday = 0x04,
gth_friday = 0x05,
gth_opra = 'X',
};
/// Trading Session Identifier (spec §7.34) present in Equity and Index
/// Last Sale messages.
enum class trading_session_id_t : std::uint8_t {
regular = 0,
extended = 1,
};
/// Denominator Code (spec §7.13). Indicates the position of the
/// floating decimal point within the associated price / value field.
enum class denominator_code_t : std::uint8_t {
d_10 = 'A', ///< 1 decimal place.
d_100 = 'B', ///< 2 decimal places.
d_1000 = 'C', ///< 3 decimal places.
d_10000 = 'D', ///< 4 decimal places.
d_100000 = 'E', ///< 5 decimal places.
d_1000000 = 'F', ///< 6 decimal places (not valid for Strike Price).
d_10000000 = 'G', ///< 7 decimal places (not valid for Strike Price).
d_100000000 = 'H', ///< 8 decimal places (Underlying Price only).
no_fraction = 'I', ///< No fraction.
};
/// Expiration Month code (spec §7.14). Identifies both the month and
/// whether the option is a Call ('A'–'L') or a Put ('M'–'X').
enum class expiration_month_t : std::uint8_t {
call_january = 'A',
call_february = 'B',
call_march = 'C',
call_april = 'D',
call_may = 'E',
call_june = 'F',
call_july = 'G',
call_august = 'H',
call_september = 'I',
call_october = 'J',
call_november = 'K',
call_december = 'L',
put_january = 'M',
put_february = 'N',
put_march = 'O',
put_april = 'P',
put_may = 'Q',
put_june = 'R',
put_july = 'S',
put_august = 'T',
put_september = 'U',
put_october = 'V',
put_november = 'W',
put_december = 'X',
};
/// Version code of the OPRA binary protocol (spec §3.05.1).
enum class version_t : std::uint8_t {
v6 = 6, ///< Current version.
};
// ─────────────────────────────────────────────────────────────────────
// Fixed-width text fields (ASCII, left justified, space filled)
// ─────────────────────────────────────────────────────────────────────
/// 5-byte Security Symbol used in all message categories except Short
/// Quote (spec §7.29).
using symbol_t = std::array<char, 5>;
/// 4-byte Security Symbol used in Short Quote (Category q) messages
/// (spec §6.04.2).
using short_symbol_t = std::array<char, 4>;
// ─────────────────────────────────────────────────────────────────────
// Common sub-structures
// ─────────────────────────────────────────────────────────────────────
/// 3-byte Expiration Block: month/day/year (spec §7.14).
struct[[= rbe::pack_be]] ExpirationBlock {
expiration_month_t month {};
std::uint8_t day {}; ///< 1–31.
std::uint8_t year {}; ///< 0–99, starting with year 2000.
};
/// 8-byte Block Timestamp (spec §3.05.8): seconds since Unix epoch
/// followed by nanoseconds within the second. Marks completion of
/// processing for the block.
struct[[= rbe::pack_be]] BlockTimestamp {
epoch_seconds_t seconds {};
nanoseconds_t nanoseconds {};
};
// ─────────────────────────────────────────────────────────────────────
// Transmission Block Header (spec §3.05) – prefixes every IP multicast
// payload; 21 bytes.
// ─────────────────────────────────────────────────────────────────────
struct[[= rbe::pack_be]] BlockHeader {
version_t version {version_t::v6};
[[= rbe::frame_length]] std::uint16_t block_size {}; ///< Total block bytes incl. header, data, pad.
data_feed_indicator_t data_feed_indicator {data_feed_indicator_t::opra};
retransmission_indicator_t retransmission_indicator {retransmission_indicator_t::original};
session_indicator_t session_indicator {session_indicator_t::regular};
block_seq_no_t block_seq_no {}; ///< Sequence number of the first message in the block.
std::uint8_t messages_in_block {};
BlockTimestamp block_timestamp {};
std::uint16_t block_checksum {}; ///< Lower 16 bits of the 32-bit sum of all bytes.
};
// ─────────────────────────────────────────────────────────────────────
// Message Header (spec §4.0) – 12-byte prefix of every OPRA message.
//
// The `msg_category` field carries the primary discriminator used by
// `rbe::id`. OPRA does not carry a per-message length on the wire
// (Administrative messages carry a separate `msg_data_length` field
// inside the message body); message length is derived from the
// (category, msg_indicator) tuple per §5. As a result no
// `[[= rbe::frame_length]]` is applied here.
// ─────────────────────────────────────────────────────────────────────
struct[[= rbe::pack_be]] Header {
participant_id_t participant_id {};
[[= rbe::id]] msg_category_t msg_category {};
std::uint8_t msg_type {}; ///< ASCII: last_sale_type_t / quote_type_t / control_type_t / …
std::uint8_t msg_indicator {}; ///< Space unless quote (then bbo_indicator_t).
transaction_id_t transaction_id {}; ///< Reserved – ignore (spec §4.01.5).
participant_ref_t participant_ref {}; ///< Optional Participant Reference Number.
};
// ─────────────────────────────────────────────────────────────────────
// BBO Appendages (spec §6.04.3, §6.04.4). Not standalone messages –
// they trail Long/Short quote payloads when the BBO Indicator so
// requires. Included here for on-wire modelling; not part of the
// `messages` tuple.
// ─────────────────────────────────────────────────────────────────────
/// 10-byte Best Bid or Best Offer appendage (spec §6.04.3).
/// Appended when the BBO Indicator is one of C/G/K (single best offer)
/// or M/N/P (single best bid).
struct[[= rbe::pack_be]] SingleAppendage {
participant_id_t participant_id;
denominator_code_t denominator_code;
price_t price;
size_t size;
};
/// 20-byte Best Bid and Best Offer appendage (spec §6.04.4).
/// Appended when the BBO Indicator is 'O' (double appendage).
struct[[= rbe::pack_be]] DoubleAppendage {
participant_id_t best_bid_participant_id;
denominator_code_t best_bid_denominator_code;
price_t best_bid_price;
size_t best_bid_size;
participant_id_t best_offer_participant_id;
denominator_code_t best_offer_denominator_code;
price_t best_offer_price;
size_t best_offer_size;
};
// ─────────────────────────────────────────────────────────────────────
// Category 'a' – Equity and Index Last Sale (spec §6.01), 43 bytes.
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::last_sale) ]] EquityIndexLastSale {
symbol_t security_symbol;
std::uint8_t reserved1 {};
ExpirationBlock expiration;
denominator_code_t strike_price_denom;
price_t strike_price;
size_t volume;
denominator_code_t premium_price_denom;
price_t premium_price;
trade_id_t trade_identifier {}; ///< For future use – Hex 0x00.
trading_session_id_t trading_session_id {trading_session_id_t::regular};
std::uint8_t reserved2[3] {};
};
// ─────────────────────────────────────────────────────────────────────
// Category 'd' – Open Interest (spec §6.02), 30 bytes.
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::open_interest) ]] OpenInterest {
symbol_t security_symbol;
std::uint8_t reserved {};
ExpirationBlock expiration;
denominator_code_t strike_price_denom;
price_t strike_price;
size_t open_interest_volume;
};
// ─────────────────────────────────────────────────────────────────────
// Category 'f' – Equity and Index End of Day Summary (spec §6.03), 72 bytes.
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::eod_summary) ]] EquityIndexEodSummary {
symbol_t security_symbol;
std::uint8_t reserved {};
ExpirationBlock expiration;
denominator_code_t strike_price_denom;
price_t strike_price;
size_t volume;
size_t open_interest_volume;
denominator_code_t premium_price_denom;
price_t open_price;
price_t high_price;
price_t low_price;
price_t last_price;
price_t net_change; ///< Signed – may be negative.
denominator_code_t underlying_price_denom;
underlying_price_t underlying_price;
price_t bid_price;
price_t offer_price;
};
// ─────────────────────────────────────────────────────────────────────
// Category 'k' – Long Equity and Index Quote (spec §6.04.1), 43 bytes
// (plus optional 10-byte SingleAppendage or 20-byte DoubleAppendage
// selected by the BBO Indicator in the Message Header).
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::long_quote) ]] LongQuote {
symbol_t security_symbol;
std::uint8_t reserved {};
ExpirationBlock expiration;
denominator_code_t strike_price_denom;
price_t strike_price;
denominator_code_t premium_price_denom;
price_t bid_price;
size_t bid_size;
price_t offer_price;
size_t offer_size;
};
// ─────────────────────────────────────────────────────────────────────
// Category 'q' – Short Equity and Index Quote (spec §6.04.2), 29 bytes
// (plus optional appendage). Strike Price Denominator implied 'A',
// Premium Price Denominator implied 'B'.
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::short_quote) ]] ShortQuote {
short_symbol_t security_symbol;
ExpirationBlock expiration;
short_price_t strike_price;
short_price_t bid_price;
short_price_t bid_size;
short_price_t offer_price;
short_price_t offer_size;
};
// ─────────────────────────────────────────────────────────────────────
// Category 'C' – Administrative (spec §6.05). Fixed prefix is 14 bytes
// (12-byte Message Header + 2-byte Message Data Length); the variable-
// length Message Data field (printable ASCII 32–126, max 200 bytes)
// follows and is not modelled here. Administrative messages are always
// sent one per block.
// ─────────────────────────────────────────────────────────────────────
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::administrative) ]] Administrative {
std::uint16_t msg_data_length {}; ///< Length of the trailing Message Data field, 0–200.
// Message Data (variable, printable ASCII) follows on the wire.
};
// ─────────────────────────────────────────────────────────────────────
// Category 'H' – Control messages (spec §6.06). Message Header only,
// 12 bytes. The Type sub-code (control_type_t) discriminates the
// specific control action within the shared layout.
// ─────────────────────────────────────────────────────────────────────
// Header-only: annotated rbe::empty so it is wirable with a size of 0 and can be an alternative of `messages`.
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::control), = rbe::empty ]] Control { };
// ─────────────────────────────────────────────────────────────────────
// Category 'R' – Series Mapping Message (spec §6.07), 155 bytes.
// ─────────────────────────────────────────────────────────────────────
// TODO: also discriminated by msg_type == series_mapping_type_t::series_mapping, which rbe::id cannot express yet.
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::series_mapping) ]] SeriesMapping {
symbol_t security_symbol;
ExpirationBlock expiration;
denominator_code_t strike_price_denom;
price_t strike_price;
std::uint16_t multicast_line_number; ///< 1–96; the output line the series is assigned to.
std::uint8_t reserved[128] {};
};
// ─────────────────────────────────────────────────────────────────────
// Category 'Y' – Underlying Value (spec §6.08).
// ─────────────────────────────────────────────────────────────────────
/// Underlying Value – Last Sale (Message Type ' '), 27 bytes (spec §6.08.1).
// TODO: also discriminated by msg_type == underlying_value_type_t::last_sale, which rbe::id cannot express yet.
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::underlying_value) ]] UnderlyingValueLastSale {
symbol_t security_symbol;
std::uint8_t reserved1 {};
denominator_code_t index_value_denom;
price_t index_value;
std::uint8_t reserved2[4] {};
};
/// Underlying Value – Bid and Offer (Message Type 'I'), 27 bytes (spec §6.08.2).
// TODO: also discriminated by msg_type == underlying_value_type_t::bid_and_offer, which rbe::id cannot express yet.
struct[[ = rbe::pack_be, = rbe::id(msg_category_t::underlying_value) ]] UnderlyingValueBidOffer {
symbol_t security_symbol;
std::uint8_t reserved {};
denominator_code_t index_value_denom;
price_t bid_index_value;
price_t offer_index_value;
};
// ─────────────────────────────────────────────────────────────────────
// Framing
// ─────────────────────────────────────────────────────────────────────
// TODO: UnderlyingValueLastSale and UnderlyingValueBidOffer share msg_category 'Y' and are told apart by
// msg_type, which rbe::id cannot express yet.
// TODO: `UnderlyingValueBidOffer` is left out because it answers to the same category as
// `UnderlyingValueLastSale`: telling them apart needs the composite discriminant the two
// structs above already flag, and a candidate list may not declare one id twice.
using messages = rbe::frames::any<
EquityIndexLastSale, OpenInterest, EquityIndexEodSummary, LongQuote, ShortQuote, Administrative, Control,
SeriesMapping, UnderlyingValueLastSale>;
/// One OPRA message: `Header` followed by the message selected by `msg_category`.
/// TODO: the length is implied by the message category and the BBO indicator;
/// until that is expressible the payload runs to the end of the buffer.
using message = rbe::frames::frame<Header, messages>;
// TODO: `rbe::frames::frame<BlockHeader, rbe::frames::many<message>>` needs `message` to be
// self-delimiting (implied lengths and composite discriminants).
} // namespace opra
How the frames classify¶
The driver states, as static_asserts, how every protocol classifies at compile time: which frames are
self-delimited, which need a walk, and that a custom frame composes like a library one.
// Every protocol composes into frames, classified at compile time by how they are delimited.
static_assert(rbe::frames::self_delimited<aquis::message>);
// PacketHeader carries no length, only a count, and a message has no constant size: the count delimits the
// packet by measuring every message, so it is count-delimited and not constant_length -- it has no
// length(), and rbe::frames::distance(packet) is the walk that answers for it
static_assert(rbe::frames::count_delimited<aquis::packet>);
static_assert(not rbe::frames::constant_length<aquis::packet>);
static_assert(rbe::frames::self_delimited<cboe::pitch::message>);
static_assert(rbe::frames::self_delimited<cboe::pitch::packet>);
static_assert(rbe::frames::self_delimited<lse::message>);
static_assert(rbe::frames::self_delimited<cboe::top::message>); // the any alternatives imply their length
static_assert(rbe::frames::implicitly_delimited<cboe::top::message>); // the id settles it, in O(1)
// A custom frame built on base_frame is a frame like any other: it composes with many and classifies.
static_assert(rbe::frames::dsrl::is_frame<cboe::top::line_view>);
static_assert(rbe::frames::frame_serder<cboe::top::line>);
static_assert(rbe::frames::self_delimited<cboe::top::line>);
static_assert(rbe::frames::serder_payload<cboe::top::lines>);
static_assert(rbe::frames::self_delimited<lse::packet>);
static_assert(rbe::frames::self_delimited<nasdaq::message>); // the any alternatives imply their length
static_assert(rbe::frames::implicitly_delimited<nasdaq::message>); // the id settles it, in O(1)
static_assert(rbe::frames::self_delimited<nasdaq::packet>);
static_assert(rbe::frames::self_delimited<opra::message>); // the any alternatives imply their length
static_assert(rbe::frames::implicitly_delimited<opra::message>); // the id settles it, in O(1)