Skip to content

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.

example/markets.cpp
// 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)