From 238a4a82e2d53f0fdd8164ae560124d14093738a Mon Sep 17 00:00:00 2001 From: Mohamed Chebbi Date: Fri, 12 Jun 2026 16:57:37 +0200 Subject: [PATCH] Add new message types and data structures for EuroCTP protocol - Implemented ReplayRequest message structure and class to handle replay requests from clients. - Added ReplayStart message structure and class for server-side acknowledgment of replay requests. - Introduced SequenceReset message structure and class to announce new packet-level sequence number. - Created SnapshotEnd message class to mark the end of snapshot transmissions. - Defined various data type aliases and enumerations for consistent handling of message fields. - Added reusable structs for Decimal, FramingHeader, GeoPosition, RptGrpHeader, TimestampNano, and UnitHeader. - Ensured all new types and structures conform to the specifications outlined in the EuroCTP protocol document. --- .github/prompts/pdf-to-cpp-messages.prompt.md | 73 +++- .github/prompts/pdf-to-cpp-types.prompt.md | 251 +++++++++--- include/messages/Common.hpp | 38 +- include/messages/Ebap.hpp | 133 ++++++ include/messages/Ebbo.hpp | 149 +++++++ include/messages/Efba.hpp | 134 ++++++ include/messages/Heartbeat.hpp | 50 +++ include/messages/InstrumentStatus.hpp | 122 ++++++ include/messages/Logon.hpp | 85 ++++ include/messages/Logout.hpp | 78 ++++ include/messages/MessageHeader.hpp | 45 ++- include/messages/MessageIds.hpp | 25 +- .../messages/OrderMatchingSystemStatus.hpp | 101 +++++ include/messages/PacketHeader.hpp | 64 ++- include/messages/PostTrade.hpp | 185 +++++++++ include/messages/ReplayRequest.hpp | 87 ++++ include/messages/ReplayStart.hpp | 82 ++++ include/messages/SequenceReset.hpp | 75 ++++ include/messages/SnapshotEnd.hpp | 49 +++ include/messages/types/Aliases.hpp | 128 ++++++ include/messages/types/Bitmaps.hpp | 93 +++++ include/messages/types/Enums.hpp | 380 ++++++++++++++++++ include/messages/types/GeoPosition.hpp | 37 -- include/messages/types/Structs.hpp | 159 ++++++++ include/messages/types/Types.hpp | 12 +- templates/Type.hpp.template | 90 +++-- 26 files changed, 2518 insertions(+), 207 deletions(-) create mode 100644 include/messages/Ebap.hpp create mode 100644 include/messages/Ebbo.hpp create mode 100644 include/messages/Efba.hpp create mode 100644 include/messages/Heartbeat.hpp create mode 100644 include/messages/InstrumentStatus.hpp create mode 100644 include/messages/Logon.hpp create mode 100644 include/messages/Logout.hpp create mode 100644 include/messages/OrderMatchingSystemStatus.hpp create mode 100644 include/messages/PostTrade.hpp create mode 100644 include/messages/ReplayRequest.hpp create mode 100644 include/messages/ReplayStart.hpp create mode 100644 include/messages/SequenceReset.hpp create mode 100644 include/messages/SnapshotEnd.hpp create mode 100644 include/messages/types/Aliases.hpp create mode 100644 include/messages/types/Bitmaps.hpp create mode 100644 include/messages/types/Enums.hpp delete mode 100755 include/messages/types/GeoPosition.hpp create mode 100644 include/messages/types/Structs.hpp diff --git a/.github/prompts/pdf-to-cpp-messages.prompt.md b/.github/prompts/pdf-to-cpp-messages.prompt.md index aebba64..e092922 100755 --- a/.github/prompts/pdf-to-cpp-messages.prompt.md +++ b/.github/prompts/pdf-to-cpp-messages.prompt.md @@ -8,7 +8,7 @@ description: Génère des structures de messages C++ (style maison) à partir d' Ton rôle : analyser un **PDF de spécification** (ICD, protocole binaire, format de trame) et générer des **messages C++ au style maison** : - struct binaire `Struct` packée (`#pragma pack(push,1)`) avec champs `m_PascalCase`, -- typedefs courts (`uint8`, `int64`, `Decimal`, `TimestampNano`, ...) — **pas de `std::uint*_t`**, +- typedefs courts (`uint8`, `int64`, ...) et **types réutilisables** issus de `include/messages/types/` (`Decimal`, `TimestampNano`, `Isin`, `Mic`, `LogoutCode`, `MdEntryType`, `TradeTypeBitmap`, ...) — **pas de `std::uint*_t`**, - classe wrapper `` qui : - contient `MessageHeader const m_MessageHeader` et `Struct const & m_Struct`, - expose `static constexpr uint16 TYPE`, `const char* NAME`, `unsigned int SIZE`, @@ -20,6 +20,32 @@ Ton rôle : analyser un **PDF de spécification** (ICD, protocole binaire, forma > **Référence obligatoire** : [.github/instructions/cpp-messages.instructions.md](../instructions/cpp-messages.instructions.md). > **Exemple canonique** : [examples/PostTrade.hpp](../../examples/PostTrade.hpp). > **Template** : [templates/Message.hpp.template](../../templates/Message.hpp.template). +> **Workflow types** : [.github/prompts/pdf-to-cpp-types.prompt.md](pdf-to-cpp-types.prompt.md). + +## ⚠️ Règle d'or — Types réutilisables UNIQUEMENT + +Les **structs de messages** doivent référencer **exclusivement** : + +1. Les **typedefs scalaires de base** déclarés dans `include/messages/Common.hpp` : + `uint8`, `int8`, `uint16`, `int16`, `uint32`, `int32`, `uint64`, `int64`. +2. Les **types extraits de la spec** consolidés dans `include/messages/types/` : + - `Aliases.hpp` (chaînes ASCII fixes, identifiants entiers : `Isin`, `Mic`, `Currency`, `SeqNum`, `InternalIdentifier`, `Username`, `Password`, ...). + - `Enums.hpp` (énumérations : `LogoutCode`, `MdEntryType`, `VenueType`, ...). + - `Bitmaps.hpp` (bitmaps : `TradeTypeBitmap`, `QuoteConditionsBitmap`, ...). + - `Structs.hpp` (composés : `Decimal`, `TimestampNano`, `RptGrpHeader`, `FramingHeader`, `UnitHeader`, ...). + +❌ **Interdits dans `Struct`** : +- ❌ Tout `using ... = ...;` ad-hoc local au fichier message. +- ❌ Toute déclaration `enum class` ou `struct` interne. +- ❌ Tout `char m_Foo[N]` brut **si** un alias dédié existe (ex: utiliser `Isin` au lieu de `char m_SecurityID[12]` quand `Isin = char[12]` est déjà défini). +- ❌ Toute redéclaration ou copie d'un type déjà présent dans `types/`. + +✅ **Procédure si un type manque** : +- Identifie la section du PDF qui décrit ce type. +- **Stoppe la génération du message** et invite l'utilisateur à exécuter `/pdf-to-cpp-types` pour cette section avec la catégorie adaptée (`alias` / `enum` / `bitmap` / `struct`). +- Reprends la génération du message **uniquement** une fois que le type est ajouté au fichier consolidé correspondant. + +✅ **Convention de référencement** : dans le `Struct`, utilise les types **non qualifiés** quand ils ont un alias re-exporté dans `protocol::messages` (cas de `Decimal`, `TimestampNano`), sinon préfixe avec `types::` (ex: `types::RptGrpHeader m_PartiesHeader;`). ## Étape 0 — Pré-requis OBLIGATOIRE : `MessageHeader` & `PacketHeader` @@ -30,12 +56,13 @@ Ton rôle : analyser un **PDF de spécification** (ICD, protocole binaire, forma - Taille totale (`MessageHeader::SIZE`) ? - Quels champs doivent apparaître dans `MessageHeader::dump()` ? - Existe-t-il déjà un fichier `include/messages/MessageHeader.hpp` à réutiliser ? + - **Préférer une composition** de structs déjà présentes dans `types/Structs.hpp` (ex: `FramingHeader` + `UnitHeader`) plutôt que de redéclarer les champs. 2. **`PacketHeader`** : le protocole encapsule-t-il les messages dans un paquet de transport ? - Si **oui** : layout, taille, et est-ce que plusieurs `MessageHeader` peuvent suivre un seul `PacketHeader` ? - Si **non** : confirme explicitement « pas de PacketHeader ». -3. **Typedefs domaine** : confirme que `Decimal`, `TimestampNano`, `InputData` correspondent bien aux types réels du projet (sinon adapter `include/messages/Common.hpp`). +3. **Typedefs domaine** : confirme que `Decimal`, `TimestampNano`, `InputData` correspondent bien aux types réels du projet (sinon adapter `include/messages/Common.hpp` et/ou les définitions sous `types/Structs.hpp`). Tant que ces réponses ne sont pas fournies, **n'écris aucun message**. Les stubs actuels : - [include/messages/MessageHeader.hpp](../../include/messages/MessageHeader.hpp) @@ -57,31 +84,40 @@ Si le PDF n'est pas accessible directement, **demande à l'utilisateur** d'extra ### 1. Inventaire des messages Liste tous les messages identifiés : nom, ID/opcode, taille payload, direction (TX/RX), description courte. Présente l'inventaire en tableau Markdown et **demande confirmation** avant de générer du code. -### 2. Analyse de chaque message +### 2. Analyse de chaque message — vérification des types Pour chaque message, identifie : - **Nom** PascalCase (sans suffixe `Message` sauf si la spec l'utilise — ex: `PostTrade`). - **ID / opcode** (constante `MSG_ID_`). -- **Champs** dans l'ordre : nom (PascalCase), type (typedef maison), taille, unité, contraintes. +- **Champs** dans l'ordre, avec leur **mapping vers un type existant** : + + | Champ spec | Type spec | Type C++ à utiliser | Origine | + |------------|-----------|---------------------|---------| + | `MsgSeqNum` | int64 | `int64` | `Common.hpp` | + | `SecurityID` | Alphanumerical 12 | `char m_SecurityID[12]` (équivalent à `Isin`) | `types/Aliases.hpp` | + | `MDEntryPx` | Decimal | `Decimal` | `types/Structs.hpp` | + | `LogoutCode` | uint16 | `uint16` (valeurs validées via `types::LogoutCode` côté logique métier) | `types/Enums.hpp` | + | `TradeType` | bitmap uint8 | `uint8` (masques `TRADE_TYPE_*_MASK`) | `types/Bitmaps.hpp` | + + > **Note** : pour les enums et bitmaps, le **layout binaire reste l'entier sous-jacent** (`uint8`/`uint16`) ; l'enum class et les masques fournissent la **sémantique** mais pas le type effectif du champ. C'est un trade-off explicite du style maison (cohérent avec `examples/PostTrade.hpp`). Documente toutefois le type sémantique en commentaire (`/// see types::LogoutCode`). - **Champs réservés / padding** : `m_Reserved_`. -- **Champs bitfield** si bits non alignés sur octet. -- **Enums** associés. - **CRC / checksum** éventuels. -- **Types réutilisables** (struct composé, enum partagé, bitfield) : - - vérifie d'abord dans `include/messages/types/`, - - sinon, **invite l'utilisateur** à lancer [/pdf-to-cpp-types](pdf-to-cpp-types.prompt.md) pour la section concernée et **attends** que le type soit créé, - - puis référence le type via `#include "messages/types/.hpp"`. + +**Si un type manque** : +- Vérifie d'abord dans les 4 fichiers consolidés `include/messages/types/{Aliases,Enums,Bitmaps,Structs}.hpp`. +- Sinon, **stoppe** et demande à l'utilisateur de lancer `/pdf-to-cpp-types` pour cette section. Ne pas inventer de type ad-hoc dans le fichier message. ### 3. Génération du code C++ Pour chaque message validé, produis : -- `include/messages/.hpp` à partir du template, en respectant **strictement** : +- `include/messages/.hpp` à partir du [template](../../templates/Message.hpp.template), en respectant **strictement** : - `#pragma pack(push, 1)` autour de `Struct`, - - typedefs maison (`uint8`, `int64`, `Decimal`, `TimestampNano`, `char[N]`), + - typedefs maison + types `protocol::messages::types::*` **uniquement**, - préfixe `m_` sur les champs, - classe wrapper avec `MessageHeader const m_MessageHeader` + `reinterpret_cast` sur le payload, - constantes `TYPE` / `NAME` / `SIZE`, - - getters (`std::string` pour `char[N]`, sinon retour direct), + - getters (`std::string` pour `char[N]`, retour direct sinon), - `dump()` avec `DUMP_FIELD_AS_INT` pour `uint8` / `int8`, `DUMP_FIELD` sinon, - `operator<<` inline. +- **Includes minimaux** : un `#include "messages/types/Types.hpp"` (préféré, agrégateur) **OU** `#include "messages/types/Aliases.hpp"` / `Enums.hpp` / `Bitmaps.hpp` / `Structs.hpp` selon ce qui est utilisé. Pas d'`#include "messages/types/.hpp"` individuel — ces fichiers n'existent plus. - Mise à jour de `include/messages/MessageIds.hpp` avec le nouvel opcode. ### 4. Validation à la compilation @@ -96,9 +132,10 @@ static_assert(std::is_trivially_copyable<Struct>::value, "must be triviall ### 5. Récapitulatif final Termine par : - liste des fichiers créés (liens cliquables), +- liste des **types réutilisés** depuis `types/` (et flag explicite si un type a dû être ajouté en cours de route), - hypothèses prises (endianness, padding, types ambigus), - points qui nécessitent une revue humaine (`// TODO:` dans le code), -- une commande de compilation suggérée (ex: `g++ -std=c++17 -Wall -Iinclude examples/PostTrade.hpp -c -o /dev/null`). +- une commande de compilation suggérée (ex: `g++ -std=c++17 -Wall -Iinclude include/messages/.hpp -fsyntax-only`). ## Règles strictes @@ -107,8 +144,11 @@ Termine par : - ❌ **Ne jamais** copier le payload — toujours `reinterpret_cast` + référence const. - ❌ **Ne jamais** inventer un champ absent du PDF (marquer `// TODO: à clarifier`). - ❌ **Ne jamais** générer plus de 5 messages d'un coup sans confirmation utilisateur. +- ❌ **Ne jamais** redéclarer un type (alias / enum / bitmap / struct) déjà présent dans `types/`. +- ❌ **Ne jamais** créer ou inclure un fichier `types/.hpp` individuel — utilisation des 4 fichiers consolidés uniquement. - ✅ **Toujours** préserver le mapping binaire 1:1 avec la spec (ordre, taille, endianness). - ✅ **Toujours** demander `MessageHeader` et `PacketHeader` à l'étape 0. +- ✅ **Toujours** mapper chaque champ vers un type existant dans `types/` ; si manquant, stopper et invoquer `/pdf-to-cpp-types`. ## Variables @@ -121,5 +161,6 @@ Termine par : 1. **Demander `MessageHeader` et `PacketHeader`** (étape 0) — bloquant. 2. Confirmer la lecture du PDF. -3. Produire l'inventaire des messages. -4. Attendre validation avant de générer le code. +3. **Vérifier** que les 4 fichiers consolidés `types/{Aliases,Enums,Bitmaps,Structs}.hpp` existent et couvrent les types nécessaires (sinon → `/pdf-to-cpp-types`). +4. Produire l'inventaire des messages avec **mapping de chaque champ vers un type existant**. +5. Attendre validation avant de générer le code. diff --git a/.github/prompts/pdf-to-cpp-types.prompt.md b/.github/prompts/pdf-to-cpp-types.prompt.md index 2d3ae08..8a21fdd 100755 --- a/.github/prompts/pdf-to-cpp-types.prompt.md +++ b/.github/prompts/pdf-to-cpp-types.prompt.md @@ -1,96 +1,221 @@ --- agent: agent -description: Génère des types C++ réutilisables (structs, enums, bitfields) à partir d'un paragraphe ou d'une table d'un PDF de spec. +description: Génère des types C++ réutilisables (aliases, enums, bitmaps, structs) à partir d'un PDF de spec, regroupés par catégorie dans des fichiers consolidés. --- -# Génération de types C++ depuis un PDF (par paragraphe / table) +# Génération de types C++ depuis un PDF — fichiers consolidés par catégorie -Ton rôle : extraire un **type réutilisable** (struct composé, enum, bitfield, alias) depuis une **section précise** d'un PDF de spécification, et générer la **classe / struct C++ correspondante**, qui pourra ensuite être référencée dans les définitions de messages. +Ton rôle : extraire des **types réutilisables** depuis un PDF de spécification et les **ajouter dans le fichier consolidé** correspondant à leur catégorie. Les types générés alimenteront ensuite le workflow [pdf-to-cpp-messages](pdf-to-cpp-messages.prompt.md). -> Les types générés ici alimentent le workflow [pdf-to-cpp-messages](pdf-to-cpp-messages.prompt.md). -> Ils vivent dans `include/messages/types/`. +## ⚠️ Règle de regroupement (NOUVELLE) + +**Un seul fichier par catégorie**, pas un fichier par type. L'arborescence cible est **fixe** : + +``` +include/messages/types/ +├── Aliases.hpp # tous les `using Foo = char[N]`, `using Bar = int64`, ... +├── Enums.hpp # tous les `enum class X : uint8 { ... }` +├── Bitmaps.hpp # tous les `using FlagsBitmap = uint8` + masques UPPER_SNAKE +├── Structs.hpp # toutes les structs binaires composées (Decimal, TimestampNano, RptGrpHeader, GeoPosition, ...) +└── Types.hpp # agrégateur — `#include` les 4 fichiers ci-dessus +``` + +- ❌ **Ne crée plus jamais** un fichier `types/.hpp` séparé. +- ✅ **Ajoute** chaque nouveau type sous forme de **bloc commenté** (avec traçabilité PDF + section) à l'intérieur du fichier consolidé adapté. +- ✅ **Tri alphabétique** des blocs à l'intérieur de chaque fichier. +- ✅ Si l'un des 4 fichiers n'existe pas encore, **crée-le** avec le squelette donné plus bas. ## Entrée requise (à demander à l'utilisateur si absente) L'utilisateur **doit fournir** : 1. Le **PDF** ou son texte extrait (`specs/*.pdf` ou `specs/*.txt`). -2. Un **identifiant de section** dans le PDF, parmi : - - un **numéro de paragraphe** (ex: `§4.3.2`, `Section 4.3.2`), - - un **numéro / titre de table** (ex: `Table 7`, `Table 4-3 — Coordinate frame`), - - une **plage de pages** (ex: `pages 18-20`). -3. Le **nom du type** souhaité (PascalCase, ex: `GeoPosition`, `CommandStatus`). -4. La **catégorie** du type : - - `struct` — composition de champs, - - `enum` — énumération scalaire, - - `bitfield` — découpage sub-byte, - - `alias` — `using` simple sur un type fondamental. - -Si l'un de ces éléments manque, **demande-le** avant de continuer. Ne devine pas la section. +2. Un **identifiant de section** dans le PDF : + - numéro de paragraphe (ex: `§4.1.2`), + - numéro / titre de table (ex: `Table 7`), + - plage de pages (ex: `pages 18-20`), + - ou bien « toute la spec » si tu dois faire un balayage complet. +3. Le **nom du type** souhaité en PascalCase (ex: `GeoPosition`, `LogoutCode`). Peut être omis si l'utilisateur demande un balayage de section : tu proposes alors une liste à valider. +4. La **catégorie** parmi : + - `alias` — `using Foo = ` (chaînes ASCII fixes incluses), + - `enum` — `enum class X : ` énumération scalaire fermée, + - `bitmap` — `using XBitmap = uint8` + jeu de masques `UPPER_SNAKE` (ex-bitfield), + - `struct` — composition binaire packée de plusieurs champs. + +Si l'un de ces éléments manque, **demande-le**. Ne devine pas la section. ## Étapes 1. **Localisation** - - Confirme la section ciblée (paragraphe / table) en citant 1-3 lignes du PDF. - - Si la section décrit plusieurs types, **liste-les** et demande lequel générer (ou tous, un par un). + - Confirme la section ciblée en citant 1-3 lignes du PDF. + - Si la section décrit plusieurs types, **liste-les** en tableau Markdown (nom proposé, catégorie, sous-jacent / taille) et demande validation. 2. **Extraction** - Selon la catégorie : - - **struct** : liste des champs (nom, type, taille, unité, plage, valeur par défaut). - - **enum** : valeurs (nom, valeur numérique, signification, type sous-jacent). - - **bitfield** : largeur en bits de chaque champ + ordre (LSB-first ou MSB-first selon la spec). - - **alias** : type sous-jacent + contraintes éventuelles (unit, scale). - -3. **Génération du code C++** - Respecte strictement [.github/instructions/cpp-messages.instructions.md](../instructions/cpp-messages.instructions.md). - - Crée le fichier `include/messages/types/.hpp` à partir du template - [templates/Type.hpp.template](../../templates/Type.hpp.template). - - Le header doit contenir : - - Bloc d'en-tête avec **traçabilité** (PDF, section, version, date). - - Le type lui-même (struct/enum/bitfield/alias). - - Les `static_assert` de taille / layout adaptés à la catégorie. - - Les constantes `UPPER_SNAKE` associées (taille attendue, masques pour bitfield). - -4. **Enregistrement** - - Ajoute une entrée dans `include/messages/types/Types.hpp` (header agrégateur) : - ```cpp - #include "messages/types/.hpp" - ``` - - Si le fichier n'existe pas, **crée-le**. + - **alias** : type sous-jacent + taille (`char[N]`, `int64`, etc.) + contraintes (unité, scale, encodage ASCII). + - **enum** : valeurs (nom PascalCase, valeur numérique ou littéral `char`, signification), type sous-jacent. + - **bitmap** : masques (`UPPER_SNAKE_MASK = 0x01`, etc.), valeur "neutre" éventuelle (`*_NONE_VALUE = 0x00`). + - **struct** : liste des champs (nom `m_PascalCase`, type, taille, unité, plage). + +3. **Insertion dans le fichier consolidé** + Selon la catégorie, ajoute un **bloc** dans le fichier correspondant en respectant strictement [.github/instructions/cpp-messages.instructions.md](../instructions/cpp-messages.instructions.md) : + + - Catégorie `alias` → [include/messages/types/Aliases.hpp](../../include/messages/types/Aliases.hpp) + - Catégorie `enum` → [include/messages/types/Enums.hpp](../../include/messages/types/Enums.hpp) + - Catégorie `bitmap` → [include/messages/types/Bitmaps.hpp](../../include/messages/types/Bitmaps.hpp) + - Catégorie `struct` → [include/messages/types/Structs.hpp](../../include/messages/types/Structs.hpp) + + Chaque bloc doit contenir : + - **Séparateur de bloc** : ligne `// === — §
===========================================` + - **Doc Doxygen** rappelant le PDF + section + version (1-3 lignes max). + - **Le type lui-même** (struct/enum/alias/bitmap). + - Les **constantes `UPPER_SNAKE`** associées (`_SIZE`, masques pour bitmap). + - Les **`static_assert`** appropriés (`sizeof`, `is_standard_layout`, `is_trivially_copyable` pour les structs). + +4. **Mise à jour de l'agrégateur** + - Si tu crées l'un des 4 fichiers consolidés pour la première fois, mets à jour [include/messages/types/Types.hpp](../../include/messages/types/Types.hpp) pour qu'il `#include` exactement ces 4 fichiers (et **rien d'autre**). + - Aucun `#include "messages/types/.hpp"` individuel ne doit subsister dans le projet. 5. **Récap** Termine par : - - Lien vers le fichier créé. - - Liste des hypothèses (endianness, padding, ordre des bitfields). - - Suggestion de réutilisation : *« Tu peux maintenant référencer `NomType` dans tes messages via `/pdf-to-cpp-messages` »*. + - Liens vers le(s) fichier(s) consolidé(s) modifié(s) + nom des nouveaux blocs ajoutés. + - Hypothèses prises (endianness, encodage ASCII, taille des champs ambigus). + - Suggestion : *« Tu peux maintenant référencer ces types dans tes messages via `/pdf-to-cpp-messages` ; ils sont accessibles via `#include "messages/types/Types.hpp"` ou les 4 fichiers individuels. »* - Points ambigus à clarifier (`// TODO:` dans le code). +## Squelettes des 4 fichiers consolidés + +Si l'un des fichiers est absent, **crée-le** avec ce squelette (alphabetical sections to be appended below). + +### `Aliases.hpp` +```cpp +// SPDX-License-Identifier: TODO +// All reusable typedefs / `using` aliases extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. +// +// Source : Spec version: Generated: + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === — §
================================================= +// (bloc docté + `using` + constante _SIZE + static_assert si applicable) + +} // namespace protocol::messages::types +``` + +### `Enums.hpp` +```cpp +// SPDX-License-Identifier: TODO +// All reusable scalar enumerations extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === — §
================================================= +// enum class : { ... }; +// inline constexpr unsigned int _SIZE = sizeof(); +// static_assert(...); + +} // namespace protocol::messages::types +``` + +### `Bitmaps.hpp` +```cpp +// SPDX-License-Identifier: TODO +// All reusable bitmap aliases + UPPER_SNAKE masks extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === Bitmap — §
=============================================== +// using Bitmap = uint8; +// inline constexpr uint8 __MASK = 0x01; +// inline constexpr unsigned int _BITMAP_SIZE = sizeof(Bitmap); +// static_assert(...); + +} // namespace protocol::messages::types +``` + +### `Structs.hpp` +```cpp +// SPDX-License-Identifier: TODO +// All reusable composed binary structs extracted from the PDF spec +// (Decimal, TimestampNano, RptGrpHeader, FramingHeader, UnitHeader, ...). +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. + +#pragma once + +#include +#include + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === — §
================================================= +// #pragma pack(push, 1) +// struct { ... }; +// #pragma pack(pop) +// static_assert(sizeof() == N, "..."); +// static_assert(std::is_standard_layout<>::value, "..."); +// static_assert(std::is_trivially_copyable<>::value, "..."); +// inline constexpr unsigned int _SIZE = N; + +} // namespace protocol::messages::types +``` + +### `Types.hpp` (agrégateur — fixe) +```cpp +// SPDX-License-Identifier: TODO +// Aggregator header — pulls every reusable type extracted from the PDF spec. + +#pragma once + +#include "messages/types/Aliases.hpp" +#include "messages/types/Bitmaps.hpp" +#include "messages/types/Enums.hpp" +#include "messages/types/Structs.hpp" +``` + ## Règles strictes (style maison) -- **Un seul type par fichier** (sauf enums étroitement liés à un struct, qui peuvent cohabiter). -- **Préfixe `m_`** sur tous les champs de struct / bitfield (`m_LatitudeE7`, `m_Ready`). -- **Typedefs maison uniquement** : `uint8`, `int16`, `int32`, `uint32`, `int64`, `uint64`, `Decimal`, `TimestampNano`. ❌ pas de `std::uint*_t` dans les structs. -- **Chaînes ASCII fixes** : `char m_Foo[N]` (❌ pas de `std::array`). -- **`#pragma pack(push, 1)` / `#pragma pack(pop)`** autour de toute struct binaire. -- **`enum class`** avec type sous-jacent obligatoire (`enum class X : uint8`). -- **Pas de prefix `Struct`** sur les types réutilisables (réservé aux messages — `Struct`). -- **Ne pas inventer** de champs absents de la spec. Marquer `// TODO: à clarifier — ` si ambigu. -- **Ne pas générer de message** ici. Les messages sont du ressort de `/pdf-to-cpp-messages`. -- Pour les **enums** dont la spec impose une plage réservée (`0x00..0x7F = reserved`), ajoute un commentaire mais **ne crée pas** de valeurs synthétiques. -- Pour les **bitfields**, documenter le **compilateur cible** (l'ordre des bits est implementation-defined). +- ❌ **Ne crée plus** de fichier `types/.hpp` séparé. +- ❌ **N'introduis pas** de nouveau `#include` dans `Types.hpp` autre que les 4 fichiers de catégorie. +- ✅ **Préfixe `m_`** sur tous les champs de struct (`m_LatitudeE7`, `m_Ready`). +- ✅ **Typedefs maison uniquement** : `uint8`, `int16`, `int32`, `uint32`, `int64`, `uint64`. ❌ pas de `std::uint*_t` dans les structs. +- ✅ **Chaînes ASCII fixes** : `using Foo = char[N];` (❌ pas de `std::array`). +- ✅ **`#pragma pack(push, 1)` / `#pragma pack(pop)`** autour de **chaque** struct binaire (même dans `Structs.hpp`). +- ✅ **`enum class`** avec type sous-jacent obligatoire (`enum class X : uint8`). +- ✅ **Pas de suffixe `Struct`** sur les types réutilisables (réservé aux messages — `Struct`). +- ✅ **Ordre alphabétique** des blocs dans chaque fichier consolidé. +- ✅ Pour les **enums** dont la spec impose une plage réservée, ajoute un commentaire mais **ne crée pas** de valeurs synthétiques. +- ✅ **Ne pas inventer** de champs absents de la spec. Marquer `// TODO: à clarifier — ` si ambigu. +- ❌ **Ne pas générer de message** ici. Les messages sont du ressort de `/pdf-to-cpp-messages`. ## Variables - Chemin PDF : ${input:pdfPath:Chemin du PDF (ou texte extrait)} -- Section / Table : ${input:section:Ex. §4.3.2 ou Table 7} -- Nom du type : ${input:typeName:PascalCase, ex: GeoPosition} -- Catégorie : ${input:category|struct,enum,bitfield,alias} +- Section / Table : ${input:section:Ex. §4.3.2 ou Table 7 ou "all"} +- Nom du type : ${input:typeName:PascalCase, ex: GeoPosition (laisser vide pour balayage)} +- Catégorie : ${input:category|alias,enum,bitmap,struct} - Namespace : ${input:cppNamespace:protocol::messages::types} ## Démarrage -1. Confirme l'accès au PDF. +1. Confirme l'accès au PDF (extraction `pdftotext -layout` si besoin). 2. Cite la section ciblée pour valider la localisation. -3. Demande la catégorie si ambiguë. -4. Génère le fichier et ajoute-le à `Types.hpp`. +3. Liste les types détectés (tableau Markdown : nom, catégorie, fichier cible, taille). +4. **Demande validation** si ≥ 5 types vont être ajoutés en un coup. +5. Insère les blocs dans `Aliases.hpp` / `Enums.hpp` / `Bitmaps.hpp` / `Structs.hpp` (crée-les si absents). +6. Vérifie / mets à jour `Types.hpp` pour qu'il référence exactement les 4 fichiers. +7. Compile-check rapide : `g++ -std=c++17 -Iinclude -fsyntax-only -x c++ include/messages/types/Types.hpp`. diff --git a/include/messages/Common.hpp b/include/messages/Common.hpp index 6bb0939..053558c 100755 --- a/include/messages/Common.hpp +++ b/include/messages/Common.hpp @@ -25,33 +25,13 @@ using uint64 = std::uint64_t; using int64 = std::int64_t; // --------------------------------------------------------------------------- -// Domain types — replace the stubs with the real definitions provided by the -// project. Kept here so the messages compile in isolation. +// Domain structs (Decimal, TimestampNano, RptGrpHeader, FramingHeader, +// UnitHeader, GeoPosition) live in include/messages/types/Structs.hpp. +// They are pulled in at the bottom of this header for backward compatibility: +// legacy code referring to `protocol::messages::Decimal` keeps compiling +// thanks to the using-aliases re-exported from Structs.hpp. // --------------------------------------------------------------------------- -/// Decimal price/quantity placeholder. -/// TODO: replace by the real Decimal type used by the project. -struct Decimal -{ - int64 mantissa = 0; - int8 exponent = 0; -}; -inline std::ostream& operator<<(std::ostream& os, Decimal const& d) -{ - return os << d.mantissa << "e" << static_cast(d.exponent); -} - -/// Nanosecond timestamp placeholder. -/// TODO: replace by the real TimestampNano type used by the project. -struct TimestampNano -{ - uint64 ns = 0; -}; -inline std::ostream& operator<<(std::ostream& os, TimestampNano const& t) -{ - return os << t.ns << "ns"; -} - // --------------------------------------------------------------------------- // InputData — opaque view over a raw byte buffer. // TODO: replace by the project's real InputData type if different. @@ -80,3 +60,11 @@ class InputData #define DUMP_FIELD_AS_INT(NAME) << " " #NAME "=" << static_cast(get##NAME()) << "\n" } // namespace protocol::messages + +// --------------------------------------------------------------------------- +// Pull in the consolidated reusable structs AFTER the basic typedefs and +// InputData / DUMP_FIELD have been declared. Structs.hpp itself includes +// Common.hpp, but `#pragma once` short-circuits the recursion. +// --------------------------------------------------------------------------- +#include "messages/types/Structs.hpp" + diff --git a/include/messages/Ebap.hpp b/include/messages/Ebap.hpp new file mode 100644 index 0000000..d3952d3 --- /dev/null +++ b/include/messages/Ebap.hpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.9 European Best Auction Price (EBAP) +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 102 (Application, SchemaID=1). Payload = 117 bytes. +// +// FIX semantic: MarketDataSnapshotFullRefresh [W] +// Constants NOT on the wire: Symbol="[N/A]", SecurityIDSource=4, NoMDEntries=1, +// NoTrdRegTimestamps=5, +// TrdRegTimestampType_0..4 / TrdRegTimestampOrigin_0..4 +// (always 2/P, 11/P, 11/C, 34/P, 36/C). + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// EBAP payload (§4.2.9). Five TrdRegTimestamps are always carried in fixed +/// order; their Type / Origin metadata is constant and therefore omitted from +/// the wire (see TrdRegTimestampType / TrdRegTimestampOrigin enums for the +/// implied values). +struct EbapStruct +{ + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Isin m_SecurityID; ///< 12B — ISO 6166 ISIN. + types::Mic m_SecurityExchange; ///< 4B — instrument place-of-listing MIC. + types::Mic m_MostLiquidMarketID; ///< 4B — most relevant market MIC (MRMTL). + types::MdEntryType m_MDEntryType; ///< 1B — 'Q' or 'J' (char enum). + types::Decimal m_MDEntryPx; ///< 9B — volume-weighted auction price. + types::Currency m_Currency; ///< 3B — ISO 4217 price currency. + types::Decimal m_MDEntrySize; ///< 9B — auction volume. + types::Decimal m_LowPx; ///< 9B — lowest auction price. + types::Decimal m_HighPx; ///< 9B — highest auction price. + types::QuoteConditionsBitmap m_QuoteConditions; ///< 1B — bitmap (0=none, 2=Out of sequence). + types::InternalIdentifier m_MDEntryID; ///< 8B — EBAP identifier. + types::TimestampNano m_TrdRegTimestamp_0; ///< 8B — Time in (Reception), implied Type=2, Origin=P. + types::TimestampNano m_TrdRegTimestamp_1; ///< 8B — Publicly reported (Diss.), implied Type=11, Origin=P. + types::TimestampNano m_TrdRegTimestamp_2; ///< 8B — Publicly reported (Pub.), implied Type=11, Origin=C. + types::TimestampNano m_TrdRegTimestamp_3; ///< 8B — Reference time for NBBO, implied Type=34, Origin=P. + types::TimestampNano m_TrdRegTimestamp_4; ///< 8B — Update time (Indicative), implied Type=36, Origin=C. +}; + +#pragma pack(pop) + +static_assert(sizeof(EbapStruct) == 117, + "EbapStruct size mismatch with spec (117 bytes)"); +static_assert(std::is_standard_layout::value, + "EbapStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "EbapStruct must be trivially copyable"); + +/// European Best Auction Price (EBAP) application message (§4.2.9). +class Ebap +{ + MessageHeader const m_MessageHeader; + EbapStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_EBAP; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(EbapStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Ebap instance. + explicit Ebap(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getSecurityID() const { return std::string(m_Struct.m_SecurityID, types::ISIN_SIZE); } + std::string getSecurityExchange() const { return std::string(m_Struct.m_SecurityExchange, types::MIC_SIZE); } + std::string getMostLiquidMarketID() const { return std::string(m_Struct.m_MostLiquidMarketID, types::MIC_SIZE); } + types::MdEntryType getMDEntryType() const { return m_Struct.m_MDEntryType; } + types::Decimal getMDEntryPx() const { return m_Struct.m_MDEntryPx; } + std::string getCurrency() const { return std::string(m_Struct.m_Currency, types::CURRENCY_SIZE); } + types::Decimal getMDEntrySize() const { return m_Struct.m_MDEntrySize; } + types::Decimal getLowPx() const { return m_Struct.m_LowPx; } + types::Decimal getHighPx() const { return m_Struct.m_HighPx; } + types::QuoteConditionsBitmap getQuoteConditions() const { return m_Struct.m_QuoteConditions; } + types::InternalIdentifier getMDEntryID() const { return m_Struct.m_MDEntryID; } + types::TimestampNano getTrdRegTimestamp_0() const { return m_Struct.m_TrdRegTimestamp_0; } + types::TimestampNano getTrdRegTimestamp_1() const { return m_Struct.m_TrdRegTimestamp_1; } + types::TimestampNano getTrdRegTimestamp_2() const { return m_Struct.m_TrdRegTimestamp_2; } + types::TimestampNano getTrdRegTimestamp_3() const { return m_Struct.m_TrdRegTimestamp_3; } + types::TimestampNano getTrdRegTimestamp_4() const { return m_Struct.m_TrdRegTimestamp_4; } + + void dump(std::ostream & os) const + { + os << "Ebap:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(SecurityID) + DUMP_FIELD(SecurityExchange) + DUMP_FIELD(MostLiquidMarketID) + ; + os << " MDEntryType=" << static_cast(m_Struct.m_MDEntryType) << "\n"; + os + DUMP_FIELD(MDEntryPx) + DUMP_FIELD(Currency) + DUMP_FIELD(MDEntrySize) + DUMP_FIELD(LowPx) + DUMP_FIELD(HighPx) + DUMP_FIELD_AS_INT(QuoteConditions) + DUMP_FIELD(MDEntryID) + DUMP_FIELD(TrdRegTimestamp_0) + DUMP_FIELD(TrdRegTimestamp_1) + DUMP_FIELD(TrdRegTimestamp_2) + DUMP_FIELD(TrdRegTimestamp_3) + DUMP_FIELD(TrdRegTimestamp_4) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, Ebap const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/Ebbo.hpp b/include/messages/Ebbo.hpp new file mode 100644 index 0000000..95112bf --- /dev/null +++ b/include/messages/Ebbo.hpp @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.8 European Best Bid and Offer (EBBO) +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 101 (Application, SchemaID=1). Payload = 121 bytes. +// +// FIX semantic: MarketDataSnapshotFullRefresh [W] +// Constants NOT on the wire: Symbol="[N/A]", SecurityIDSource=4, NoMDEntries=2, +// NoTrdRegTimestamps=5, +// TrdRegTimestampType_0..4 / TrdRegTimestampOrigin_0..4 +// (always 2/P, 11/P, 11/C, 34/P, 36/C). + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// EBBO payload (§4.2.8). Two fixed MDEntries (Bid then Offer) and five +/// TrdRegTimestamps with implied Type / Origin metadata (constants omitted +/// from the wire). +struct EbboStruct +{ + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Isin m_SecurityID; ///< 12B — ISO 6166 ISIN. + types::Mic m_SecurityExchange; ///< 4B — instrument place-of-listing MIC. + types::Mic m_MostLiquidMarketID; ///< 4B — most relevant market MIC (MRMTL). + + // MDEntry #0 — Bid side ---------------------------------------------------- + types::MdEntryType m_MDEntryType_0; ///< 1B — '0' (Bid), 'J' (Empty book) or 'j' (Uncrossing bid). + types::Decimal m_MDEntryPx_0; ///< 9B — best bid price. + types::Currency m_Currency_0; ///< 3B — ISO 4217 currency. + types::Decimal m_MDEntrySize_0; ///< 9B — best bid volume. + + // MDEntry #1 — Offer side -------------------------------------------------- + types::MdEntryType m_MDEntryType_1; ///< 1B — '1' (Offer), 'J' (Empty book) or 'k' (Uncrossing offer). + types::Decimal m_MDEntryPx_1; ///< 9B — best offer price. + types::Currency m_Currency_1; ///< 3B — ISO 4217 currency. + types::Decimal m_MDEntrySize_1; ///< 9B — best offer volume. + + types::QuoteConditionsBitmap m_QuoteConditions; ///< 1B — bitmap (0/1/2/4/8). + types::InternalIdentifier m_MDEntryID; ///< 8B — EBBO identifier. + + types::TimestampNano m_TrdRegTimestamp_0; ///< 8B — Time in (Reception), implied Type=2, Origin=P. + types::TimestampNano m_TrdRegTimestamp_1; ///< 8B — Publicly reported (Diss.), implied Type=11, Origin=P. + types::TimestampNano m_TrdRegTimestamp_2; ///< 8B — Publicly reported (Pub.), implied Type=11, Origin=C. + types::TimestampNano m_TrdRegTimestamp_3; ///< 8B — Reference time for NBBO, implied Type=34, Origin=P. + types::TimestampNano m_TrdRegTimestamp_4; ///< 8B — Update time, implied Type=36, Origin=C. +}; + +#pragma pack(pop) + +static_assert(sizeof(EbboStruct) == 121, + "EbboStruct size mismatch with spec (121 bytes)"); +static_assert(std::is_standard_layout::value, + "EbboStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "EbboStruct must be trivially copyable"); + +/// European Best Bid and Offer (EBBO) application message (§4.2.8). +class Ebbo +{ + MessageHeader const m_MessageHeader; + EbboStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_EBBO; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(EbboStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Ebbo instance. + explicit Ebbo(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getSecurityID() const { return std::string(m_Struct.m_SecurityID, types::ISIN_SIZE); } + std::string getSecurityExchange() const { return std::string(m_Struct.m_SecurityExchange, types::MIC_SIZE); } + std::string getMostLiquidMarketID() const { return std::string(m_Struct.m_MostLiquidMarketID, types::MIC_SIZE); } + + types::MdEntryType getMDEntryType_0() const { return m_Struct.m_MDEntryType_0; } + types::Decimal getMDEntryPx_0() const { return m_Struct.m_MDEntryPx_0; } + std::string getCurrency_0() const { return std::string(m_Struct.m_Currency_0, types::CURRENCY_SIZE); } + types::Decimal getMDEntrySize_0() const { return m_Struct.m_MDEntrySize_0; } + + types::MdEntryType getMDEntryType_1() const { return m_Struct.m_MDEntryType_1; } + types::Decimal getMDEntryPx_1() const { return m_Struct.m_MDEntryPx_1; } + std::string getCurrency_1() const { return std::string(m_Struct.m_Currency_1, types::CURRENCY_SIZE); } + types::Decimal getMDEntrySize_1() const { return m_Struct.m_MDEntrySize_1; } + + types::QuoteConditionsBitmap getQuoteConditions() const { return m_Struct.m_QuoteConditions; } + types::InternalIdentifier getMDEntryID() const { return m_Struct.m_MDEntryID; } + types::TimestampNano getTrdRegTimestamp_0() const { return m_Struct.m_TrdRegTimestamp_0; } + types::TimestampNano getTrdRegTimestamp_1() const { return m_Struct.m_TrdRegTimestamp_1; } + types::TimestampNano getTrdRegTimestamp_2() const { return m_Struct.m_TrdRegTimestamp_2; } + types::TimestampNano getTrdRegTimestamp_3() const { return m_Struct.m_TrdRegTimestamp_3; } + types::TimestampNano getTrdRegTimestamp_4() const { return m_Struct.m_TrdRegTimestamp_4; } + + void dump(std::ostream & os) const + { + os << "Ebbo:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(SecurityID) + DUMP_FIELD(SecurityExchange) + DUMP_FIELD(MostLiquidMarketID) + ; + os << " MDEntryType_0=" << static_cast(m_Struct.m_MDEntryType_0) << "\n"; + os + DUMP_FIELD(MDEntryPx_0) + DUMP_FIELD(Currency_0) + DUMP_FIELD(MDEntrySize_0) + ; + os << " MDEntryType_1=" << static_cast(m_Struct.m_MDEntryType_1) << "\n"; + os + DUMP_FIELD(MDEntryPx_1) + DUMP_FIELD(Currency_1) + DUMP_FIELD(MDEntrySize_1) + DUMP_FIELD_AS_INT(QuoteConditions) + DUMP_FIELD(MDEntryID) + DUMP_FIELD(TrdRegTimestamp_0) + DUMP_FIELD(TrdRegTimestamp_1) + DUMP_FIELD(TrdRegTimestamp_2) + DUMP_FIELD(TrdRegTimestamp_3) + DUMP_FIELD(TrdRegTimestamp_4) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, Ebbo const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/Efba.hpp b/include/messages/Efba.hpp new file mode 100644 index 0000000..e5559aa --- /dev/null +++ b/include/messages/Efba.hpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.10 European Frequent Batch Auction (EFBA) +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 103 (Application, SchemaID=1). Payload = 117 bytes. +// +// FIX semantic: MarketDataSnapshotFullRefresh [W] +// Constants NOT on the wire: Symbol="[N/A]", SecurityIDSource=4, NoMDEntries=1, +// TradingSessionID=1, TradingSessionSubID=14 (ODAU), +// NoTrdRegTimestamps=5, +// TrdRegTimestampType_0..4 / TrdRegTimestampOrigin_0..4 +// (always 2/P, 11/P, 11/C, 34/P, 36/C). +// +// NB: EFBA shares the exact same wire layout as EBAP (§4.2.9), but with the +// constraint that the auction is always a Frequent Batch Auction (ODAU). + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// EFBA payload (§4.2.10). Layout identical to EbapStruct. +struct EfbaStruct +{ + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Isin m_SecurityID; ///< 12B — ISO 6166 ISIN. + types::Mic m_SecurityExchange; ///< 4B — instrument place-of-listing MIC. + types::Mic m_MostLiquidMarketID; ///< 4B — most relevant market MIC (MRMTL). + types::MdEntryType m_MDEntryType; ///< 1B — 'Q' (Auction clearing price) or 'J' (Empty book). + types::Decimal m_MDEntryPx; ///< 9B — volume-weighted auction price. + types::Currency m_Currency; ///< 3B — ISO 4217 price currency. + types::Decimal m_MDEntrySize; ///< 9B — auction volume. + types::Decimal m_LowPx; ///< 9B — lowest auction price. + types::Decimal m_HighPx; ///< 9B — highest auction price. + types::QuoteConditionsBitmap m_QuoteConditions; ///< 1B — bitmap (0=none, 2=Out of sequence). + types::InternalIdentifier m_MDEntryID; ///< 8B — EFBA identifier. + types::TimestampNano m_TrdRegTimestamp_0; ///< 8B — Time in (Reception), implied Type=2, Origin=P. + types::TimestampNano m_TrdRegTimestamp_1; ///< 8B — Publicly reported (Diss.), implied Type=11, Origin=P. + types::TimestampNano m_TrdRegTimestamp_2; ///< 8B — Publicly reported (Pub.), implied Type=11, Origin=C. + types::TimestampNano m_TrdRegTimestamp_3; ///< 8B — Reference time for NBBO, implied Type=34, Origin=P. + types::TimestampNano m_TrdRegTimestamp_4; ///< 8B — Update time (Indicative), implied Type=36, Origin=C. +}; + +#pragma pack(pop) + +static_assert(sizeof(EfbaStruct) == 117, + "EfbaStruct size mismatch with spec (117 bytes)"); +static_assert(std::is_standard_layout::value, + "EfbaStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "EfbaStruct must be trivially copyable"); + +/// European Frequent Batch Auction (EFBA) application message (§4.2.10). +class Efba +{ + MessageHeader const m_MessageHeader; + EfbaStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_EFBA; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(EfbaStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Efba instance. + explicit Efba(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getSecurityID() const { return std::string(m_Struct.m_SecurityID, types::ISIN_SIZE); } + std::string getSecurityExchange() const { return std::string(m_Struct.m_SecurityExchange, types::MIC_SIZE); } + std::string getMostLiquidMarketID() const { return std::string(m_Struct.m_MostLiquidMarketID, types::MIC_SIZE); } + types::MdEntryType getMDEntryType() const { return m_Struct.m_MDEntryType; } + types::Decimal getMDEntryPx() const { return m_Struct.m_MDEntryPx; } + std::string getCurrency() const { return std::string(m_Struct.m_Currency, types::CURRENCY_SIZE); } + types::Decimal getMDEntrySize() const { return m_Struct.m_MDEntrySize; } + types::Decimal getLowPx() const { return m_Struct.m_LowPx; } + types::Decimal getHighPx() const { return m_Struct.m_HighPx; } + types::QuoteConditionsBitmap getQuoteConditions() const { return m_Struct.m_QuoteConditions; } + types::InternalIdentifier getMDEntryID() const { return m_Struct.m_MDEntryID; } + types::TimestampNano getTrdRegTimestamp_0() const { return m_Struct.m_TrdRegTimestamp_0; } + types::TimestampNano getTrdRegTimestamp_1() const { return m_Struct.m_TrdRegTimestamp_1; } + types::TimestampNano getTrdRegTimestamp_2() const { return m_Struct.m_TrdRegTimestamp_2; } + types::TimestampNano getTrdRegTimestamp_3() const { return m_Struct.m_TrdRegTimestamp_3; } + types::TimestampNano getTrdRegTimestamp_4() const { return m_Struct.m_TrdRegTimestamp_4; } + + void dump(std::ostream & os) const + { + os << "Efba:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(SecurityID) + DUMP_FIELD(SecurityExchange) + DUMP_FIELD(MostLiquidMarketID) + ; + os << " MDEntryType=" << static_cast(m_Struct.m_MDEntryType) << "\n"; + os + DUMP_FIELD(MDEntryPx) + DUMP_FIELD(Currency) + DUMP_FIELD(MDEntrySize) + DUMP_FIELD(LowPx) + DUMP_FIELD(HighPx) + DUMP_FIELD_AS_INT(QuoteConditions) + DUMP_FIELD(MDEntryID) + DUMP_FIELD(TrdRegTimestamp_0) + DUMP_FIELD(TrdRegTimestamp_1) + DUMP_FIELD(TrdRegTimestamp_2) + DUMP_FIELD(TrdRegTimestamp_3) + DUMP_FIELD(TrdRegTimestamp_4) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, Efba const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/Heartbeat.hpp b/include/messages/Heartbeat.hpp new file mode 100644 index 0000000..2ce1e3a --- /dev/null +++ b/include/messages/Heartbeat.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.1 Heartbeat +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 1 (Administrative, SchemaID=2). Empty payload. + +#pragma once + +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +/// Heartbeat — sent every second on a channel that is otherwise idle. +/// Conforms to the standard SBE packet/message header structure but carries +/// no payload (§4.2.1). +class Heartbeat +{ + MessageHeader const m_MessageHeader; + +public: + static constexpr unsigned int TYPE = MSG_ID_HEARTBEAT; + static constexpr unsigned int SIZE = MessageHeader::SIZE; // header only, no payload + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Heartbeat instance. + explicit Heartbeat(InputData const & data) + : m_MessageHeader(data) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + void dump(std::ostream & os) const + { + os << "Heartbeat:\n"; + m_MessageHeader.dump(os); + } +}; + +inline std::ostream& operator<<(std::ostream& out, Heartbeat const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/InstrumentStatus.hpp b/include/messages/InstrumentStatus.hpp new file mode 100644 index 0000000..9059085 --- /dev/null +++ b/include/messages/InstrumentStatus.hpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.12 Instrument status +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 301 (Application, SchemaID=1). Payload = 61 bytes. +// +// FIX semantic: SecurityStatus [f] +// Constant fields (Symbol, SecurityIDSource=4, TradingSessionID=1) are NOT +// part of the wire payload per the SBE encoding table. + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// InstrumentStatus payload (§4.2.12). +struct InstrumentStatusStruct +{ + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Isin m_SecurityID; ///< 12B — ISO 6166 ISIN. + types::SecurityStatus m_SecurityStatus; ///< 1B — 0/1/5/9 (ACTV/RMOV/SUSP). + types::Currency m_Currency; ///< 3B — ISO 4217 currency code. + types::Mic m_MarketID; ///< 4B — trading venue MIC. + types::Mic m_SecurityExchange; ///< 4B — place-of-listing MIC. + types::MostLiquidMarketIndicator m_MostLiquidMarketIndicator; ///< 1B — 0/1 (FALSE/TRUE). + types::VenueType m_VenueType; ///< 1B — char enum (B/Q/A/N/H/z). + types::TradingSessionSubId m_TradingSessionSubID; ///< 1B — trading-system phase. + types::MatchType m_MatchType; ///< 1B — trading-system phase (mutually exclusive w/ TradingSessionSubID). + types::SecurityTradingStatus m_SecurityTradingStatus; ///< 1B — 0/2/3 (HALT/Resume). + types::TimestampNano m_TransactTime; ///< 8B — dissemination time. + types::TimestampNano m_EffectiveTime; ///< 8B — instrument status start time. + types::InternalIdentifier m_MDEntryID; ///< 8B — instrument status identifier. +}; + +#pragma pack(pop) + +static_assert(sizeof(InstrumentStatusStruct) == 61, + "InstrumentStatusStruct size mismatch with spec (61 bytes)"); +static_assert(std::is_standard_layout::value, + "InstrumentStatusStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "InstrumentStatusStruct must be trivially copyable"); + +/// InstrumentStatus application message — disseminates the regulatory / +/// trading status of a single instrument (§4.2.12). +class InstrumentStatus +{ + MessageHeader const m_MessageHeader; + InstrumentStatusStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_INSTRUMENT_STATUS; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(InstrumentStatusStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this instance. + explicit InstrumentStatus(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getSecurityID() const { return std::string(m_Struct.m_SecurityID, types::ISIN_SIZE); } + types::SecurityStatus getSecurityStatus() const { return m_Struct.m_SecurityStatus; } + std::string getCurrency() const { return std::string(m_Struct.m_Currency, types::CURRENCY_SIZE); } + std::string getMarketID() const { return std::string(m_Struct.m_MarketID, types::MIC_SIZE); } + std::string getSecurityExchange() const { return std::string(m_Struct.m_SecurityExchange, types::MIC_SIZE); } + types::MostLiquidMarketIndicator getMostLiquidMarketIndicator() const { return m_Struct.m_MostLiquidMarketIndicator; } + types::VenueType getVenueType() const { return m_Struct.m_VenueType; } + types::TradingSessionSubId getTradingSessionSubID() const { return m_Struct.m_TradingSessionSubID; } + types::MatchType getMatchType() const { return m_Struct.m_MatchType; } + types::SecurityTradingStatus getSecurityTradingStatus() const { return m_Struct.m_SecurityTradingStatus; } + types::TimestampNano getTransactTime() const { return m_Struct.m_TransactTime; } + types::TimestampNano getEffectiveTime() const { return m_Struct.m_EffectiveTime; } + types::InternalIdentifier getMDEntryID() const { return m_Struct.m_MDEntryID; } + + void dump(std::ostream & os) const + { + os << "InstrumentStatus:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(SecurityID) + ; + os << " SecurityStatus=" << static_cast(m_Struct.m_SecurityStatus) << "\n"; + os + DUMP_FIELD(Currency) + DUMP_FIELD(MarketID) + DUMP_FIELD(SecurityExchange) + ; + os << " MostLiquidMarketIndicator=" << static_cast(m_Struct.m_MostLiquidMarketIndicator) << "\n"; + os << " VenueType=" << static_cast(m_Struct.m_VenueType) << "\n"; + os << " TradingSessionSubID=" << static_cast(m_Struct.m_TradingSessionSubID) << "\n"; + os << " MatchType=" << static_cast(m_Struct.m_MatchType) << "\n"; + os << " SecurityTradingStatus=" << static_cast(m_Struct.m_SecurityTradingStatus) << "\n"; + os + DUMP_FIELD(TransactTime) + DUMP_FIELD(EffectiveTime) + DUMP_FIELD(MDEntryID) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, InstrumentStatus const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/Logon.hpp b/include/messages/Logon.hpp new file mode 100644 index 0000000..802f14c --- /dev/null +++ b/include/messages/Logon.hpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.2 Logon message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 10 (Administrative, SchemaID=2). Payload = 112 bytes. + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// Logon payload — sent by clients connecting to the TCP replay channel +/// (§4.2.2). +struct LogonStruct +{ + types::Username m_Username; ///< 32B — client username for authentication. + types::Password m_Password; ///< 32B — client password for authentication. + types::AppId m_ClientAppId; ///< 32B — client application identifier. + types::AppVersion m_ClientAppVersion; ///< 16B — client application version. +}; + +#pragma pack(pop) + +static_assert(sizeof(LogonStruct) == 112, + "LogonStruct size mismatch with spec (112 bytes = 32+32+32+16)"); +static_assert(std::is_standard_layout::value, + "LogonStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "LogonStruct must be trivially copyable"); + +/// Logon administrative message — authenticates a client onto the replay +/// channel and conveys the client's app identity (§4.2.2). +class Logon +{ + MessageHeader const m_MessageHeader; + LogonStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_LOGON; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(LogonStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Logon instance. + explicit Logon(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + std::string getUsername() const { return std::string(m_Struct.m_Username, types::USERNAME_SIZE); } + std::string getPassword() const { return std::string(m_Struct.m_Password, types::PASSWORD_SIZE); } + std::string getClientAppId() const { return std::string(m_Struct.m_ClientAppId, types::APP_ID_SIZE); } + std::string getClientAppVersion() const { return std::string(m_Struct.m_ClientAppVersion, types::APP_VERSION_SIZE); } + + void dump(std::ostream & os) const + { + os << "Logon:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(Username) + DUMP_FIELD(Password) + DUMP_FIELD(ClientAppId) + DUMP_FIELD(ClientAppVersion) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, Logon const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/Logout.hpp b/include/messages/Logout.hpp new file mode 100644 index 0000000..df80559 --- /dev/null +++ b/include/messages/Logout.hpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.3 Logout message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 11 (Administrative, SchemaID=2). Payload = 258 bytes. + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// Logout payload — server-initiated session termination notice (§4.2.3). +struct LogoutStruct +{ + uint16 m_LogoutCode; ///< 2B — root cause code (see §). + types::LogoutReason m_LogoutReason; ///< 256B — free-form reason text (alphanumeric). +}; + +#pragma pack(pop) + +static_assert(sizeof(LogoutStruct) == 258, + "LogoutStruct size mismatch with spec (258 bytes = 2+256)"); +static_assert(std::is_standard_layout::value, + "LogoutStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "LogoutStruct must be trivially copyable"); + +/// Logout administrative message — sent by the server before disconnecting +/// the session (§4.2.3). +class Logout +{ + MessageHeader const m_MessageHeader; + LogoutStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_LOGOUT; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(LogoutStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this Logout instance. + explicit Logout(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + uint16 getLogoutCode() const { return m_Struct.m_LogoutCode; } + std::string getLogoutReason() const { return std::string(m_Struct.m_LogoutReason, types::LOGOUT_REASON_SIZE); } + + void dump(std::ostream & os) const + { + os << "Logout:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(LogoutCode) + DUMP_FIELD(LogoutReason) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, Logout const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/MessageHeader.hpp b/include/messages/MessageHeader.hpp index 856d2d3..e665006 100755 --- a/include/messages/MessageHeader.hpp +++ b/include/messages/MessageHeader.hpp @@ -1,48 +1,73 @@ // SPDX-License-Identifier: TODO -// MessageHeader — STUB. +// MessageHeader — concrete implementation for EuroCTP Consolidated Tape feed. +// Composition of: +// - FramingHeader (§4.1.6) — 2 bytes +// - UnitHeader (§4.1.7) — 8 bytes +// Total: 10 bytes, little-endian (§4.1.4). // -// ⚠️ The real layout depends on the protocol spec. The /pdf-to-cpp-messages -// workflow MUST ask the user for the actual MessageHeader definition before -// generating any messages and replace this stub. +// Source : specs/euro_ctp_spec.pdf — sections §4.1.6 and §4.1.7 +// Spec version: 1.3 Generated: 2026-06-12 #pragma once #include +#include #include "messages/Common.hpp" +#include "messages/types/Structs.hpp" // FramingHeader + UnitHeader namespace protocol::messages { #pragma pack(push, 1) -/// TODO: replace fields below with the real MessageHeader described in the spec. +/// EuroCTP message header on the wire = FramingHeader || UnitHeader. +/// Wire layout (little-endian): +/// uint16 MessageLength | uint16 MsgSize | uint16 TemplateID | uint16 SchemaID | uint16 Version struct MessageHeaderStruct { - uint16 m_MessageLength; ///< Total length of the message in bytes (incl. header). - uint16 m_MessageType; ///< Opcode of the payload. + types::FramingHeader m_FramingHeader; ///< §4.1.6 — outer SBE framing. + types::UnitHeader m_UnitHeader; ///< §4.1.7 — SBE unit / template descriptor. }; #pragma pack(pop) +static_assert(sizeof(MessageHeaderStruct) == 10, + "MessageHeaderStruct size mismatch with spec (10 bytes = 2 + 8)"); +static_assert(std::is_standard_layout::value, + "MessageHeaderStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "MessageHeaderStruct must be trivially copyable"); + class MessageHeader { MessageHeaderStruct const & m_Struct; public: static constexpr unsigned int SIZE = sizeof(MessageHeaderStruct); + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this MessageHeader instance. explicit MessageHeader(InputData const & data) : m_Struct(*reinterpret_cast(data.data())) { } - uint16 getMessageLength() const { return m_Struct.m_MessageLength; } - uint16 getMessageType() const { return m_Struct.m_MessageType; } + // Framing header accessor ------------------------------------------------ + uint16 getMessageLength() const { return m_Struct.m_FramingHeader.m_MessageLength; } + + // Unit header accessors -------------------------------------------------- + uint16 getMsgSize() const { return m_Struct.m_UnitHeader.m_MsgSize; } + uint16 getTemplateID() const { return m_Struct.m_UnitHeader.m_TemplateID; } + uint16 getSchemaID() const { return m_Struct.m_UnitHeader.m_SchemaID; } + uint16 getVersion() const { return m_Struct.m_UnitHeader.m_Version; } void dump(std::ostream & os) const { os DUMP_FIELD(MessageLength) - DUMP_FIELD(MessageType) + DUMP_FIELD(MsgSize) + DUMP_FIELD(TemplateID) + DUMP_FIELD(SchemaID) + DUMP_FIELD(Version) ; } }; diff --git a/include/messages/MessageIds.hpp b/include/messages/MessageIds.hpp index 8d536e5..c1c3404 100755 --- a/include/messages/MessageIds.hpp +++ b/include/messages/MessageIds.hpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: TODO // Auto-generated message ID registry — keep in sync with the spec. +// Source : specs/euro_ctp_spec.pdf — sections §4.1.1.1 / §4.1.1.2 +// Spec version: 1.3 Generated: 2026-06-12 #pragma once @@ -7,8 +9,25 @@ namespace protocol::messages { -// Add one constant per message extracted from the PDF spec. -inline constexpr uint16 MSG_ID_POST_TRADE = 201; -// inline constexpr uint16 MSG_ID_ = ; +// --------------------------------------------------------------------------- +// Administrative messages — SchemaID = 2 (§4.1.1.1) +// --------------------------------------------------------------------------- +inline constexpr uint16 MSG_ID_HEARTBEAT = 1; +inline constexpr uint16 MSG_ID_SEQUENCE_RESET = 2; +inline constexpr uint16 MSG_ID_LOGON = 10; +inline constexpr uint16 MSG_ID_LOGOUT = 11; +inline constexpr uint16 MSG_ID_REPLAY_REQUEST = 20; +inline constexpr uint16 MSG_ID_REPLAY_START = 21; +inline constexpr uint16 MSG_ID_SNAPSHOT_END = 30; + +// --------------------------------------------------------------------------- +// Application messages — SchemaID = 1 (§4.1.1.2) +// --------------------------------------------------------------------------- +inline constexpr uint16 MSG_ID_EBBO = 101; +inline constexpr uint16 MSG_ID_EBAP = 102; +inline constexpr uint16 MSG_ID_EFBA = 103; +inline constexpr uint16 MSG_ID_POST_TRADE = 201; +inline constexpr uint16 MSG_ID_INSTRUMENT_STATUS = 301; +inline constexpr uint16 MSG_ID_ORDER_MATCHING_SYSTEM_STATUS = 302; } // namespace protocol::messages diff --git a/include/messages/OrderMatchingSystemStatus.hpp b/include/messages/OrderMatchingSystemStatus.hpp new file mode 100644 index 0000000..ac133e2 --- /dev/null +++ b/include/messages/OrderMatchingSystemStatus.hpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.13 Order matching system status +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 302 (Application, SchemaID=1). Payload = 38 bytes. +// +// FIX semantic: TradingSessionStatus [h] + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// OrderMatchingSystemStatus payload (§4.2.13). +/// ⚠️ NOTE on m_MarketID : the spec table contradicts itself — column "SBE +/// data type" reads "uint8" while the "#bytes" column reads "4" and the +/// Format column requires an ISO 10383 MIC code (4 ASCII letters). The wire +/// reality is 4 bytes, so we type the field as `types::Mic` (char[4]). +struct OrderMatchingSystemStatusStruct +{ + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Mic m_MarketID; ///< 4B — trading venue MIC (see note above). + types::VenueType m_VenueType; ///< 1B — char enum (B/Q/A/N/H/z). + types::TradSesStatus m_TradSesStatus; ///< 1B — uint8 enum (2/7/8). + types::TimestampNano m_TransactTime; ///< 8B — dissemination time (ns since Epoch). + types::TimestampNano m_EffectiveTime; ///< 8B — system status start time (ns since Epoch). + types::InternalIdentifier m_MDEntryID; ///< 8B — instrument status identifier. +}; + +#pragma pack(pop) + +static_assert(sizeof(OrderMatchingSystemStatusStruct) == 38, + "OrderMatchingSystemStatusStruct size mismatch with spec (38 bytes)"); +static_assert(std::is_standard_layout::value, + "OrderMatchingSystemStatusStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "OrderMatchingSystemStatusStruct must be trivially copyable"); + +/// OrderMatchingSystemStatus application message — disseminates the status of +/// EuroCTP's order-matching system (§4.2.13). +class OrderMatchingSystemStatus +{ + MessageHeader const m_MessageHeader; + OrderMatchingSystemStatusStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_ORDER_MATCHING_SYSTEM_STATUS; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(OrderMatchingSystemStatusStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this instance. + explicit OrderMatchingSystemStatus(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getMarketID() const { return std::string(m_Struct.m_MarketID, types::MIC_SIZE); } + types::VenueType getVenueType() const { return m_Struct.m_VenueType; } + types::TradSesStatus getTradSesStatus() const { return m_Struct.m_TradSesStatus; } + types::TimestampNano getTransactTime() const { return m_Struct.m_TransactTime; } + types::TimestampNano getEffectiveTime() const { return m_Struct.m_EffectiveTime; } + types::InternalIdentifier getMDEntryID() const { return m_Struct.m_MDEntryID; } + + void dump(std::ostream & os) const + { + os << "OrderMatchingSystemStatus:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(MarketID) + ; + os << " VenueType=" << static_cast(m_Struct.m_VenueType) << "\n"; + os << " TradSesStatus=" << static_cast(m_Struct.m_TradSesStatus) << "\n"; + os + DUMP_FIELD(TransactTime) + DUMP_FIELD(EffectiveTime) + DUMP_FIELD(MDEntryID) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, OrderMatchingSystemStatus const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/PacketHeader.hpp b/include/messages/PacketHeader.hpp index a4e80ee..b78ef15 100755 --- a/include/messages/PacketHeader.hpp +++ b/include/messages/PacketHeader.hpp @@ -1,52 +1,92 @@ // SPDX-License-Identifier: TODO -// PacketHeader — STUB. +// PacketHeader — concrete implementation for EuroCTP Consolidated Tape feed. // -// ⚠️ Some protocols wrap one or more MessageHeader inside a PacketHeader -// (transport-level framing). The /pdf-to-cpp-messages workflow MUST ask the -// user whether a PacketHeader exists and, if so, replace this stub with the -// real layout. +// One PacketHeader prefixes every multicast packet and may be followed by one +// or more (FramingHeader + UnitHeader + payload) tuples. Wire layout is +// little-endian (§4.1.4). +// +// Source : specs/euro_ctp_spec.pdf — section §4.1.5 (Packet header) +// Spec version: 1.3 Generated: 2026-06-12 #pragma once #include +#include #include "messages/Common.hpp" namespace protocol::messages { +/// Magic value of the EncodingType field, in little-endian byte order +/// (§4.1.5 — "Value is 0xEC79 (60537) in little-endian encoding"). +inline constexpr uint16 PACKET_HEADER_ENCODING_TYPE = 0xEC79; + #pragma pack(push, 1) -/// TODO: replace fields below with the real PacketHeader described in the spec -/// (or delete this file if the protocol has no packet-level framing). +/// EuroCTP packet header on the wire. +/// Wire layout (little-endian): +/// uint16 EncodingType | uint16 PacketSize | uint8 FeedID | uint8 ChannelID | +/// uint8 PacketSeqVersion | uint64 PacketSeqNum | uint64 DisseminationTime +/// Total: 23 bytes. struct PacketHeaderStruct { - uint32 m_PacketSeqNum; ///< Packet sequence number. - uint64 m_SendingTime; ///< Sending time (ns since epoch, mod 2^64). + uint16 m_EncodingType; ///< Encoding identifier — always 0xEC79 (little-endian). + uint16 m_PacketSize; ///< Overall packet size including this header. + uint8 m_FeedID; ///< Feed ID (see types::FeedId, §3.1.1). + uint8 m_ChannelID; ///< Channel ID — partition within the feed (spec spelling: "ChanelID"). + uint8 m_PacketSeqVersion; ///< Version of the packet-level sequence number. + uint64 m_PacketSeqNum; ///< Monotonically-increasing packet sequence number. + uint64 m_DisseminationTime; ///< Nanoseconds since Epoch (publication time). }; #pragma pack(pop) +static_assert(sizeof(PacketHeaderStruct) == 23, + "PacketHeaderStruct size mismatch with spec (23 bytes)"); +static_assert(std::is_standard_layout::value, + "PacketHeaderStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "PacketHeaderStruct must be trivially copyable"); + class PacketHeader { PacketHeaderStruct const & m_Struct; public: static constexpr unsigned int SIZE = sizeof(PacketHeaderStruct); + /// @param data Raw byte buffer that MUST start at the PacketHeader and + /// remain alive for the lifetime of this PacketHeader instance. explicit PacketHeader(InputData const & data) : m_Struct(*reinterpret_cast(data.data())) { } - uint32 getPacketSeqNum() const { return m_Struct.m_PacketSeqNum; } - uint64 getSendingTime() const { return m_Struct.m_SendingTime; } + uint16 getEncodingType() const { return m_Struct.m_EncodingType; } + uint16 getPacketSize() const { return m_Struct.m_PacketSize; } + uint8 getFeedID() const { return m_Struct.m_FeedID; } + uint8 getChannelID() const { return m_Struct.m_ChannelID; } + uint8 getPacketSeqVersion() const { return m_Struct.m_PacketSeqVersion; } + uint64 getPacketSeqNum() const { return m_Struct.m_PacketSeqNum; } + uint64 getDisseminationTime() const { return m_Struct.m_DisseminationTime; } void dump(std::ostream & os) const { os + DUMP_FIELD(EncodingType) + DUMP_FIELD(PacketSize) + DUMP_FIELD_AS_INT(FeedID) + DUMP_FIELD_AS_INT(ChannelID) + DUMP_FIELD_AS_INT(PacketSeqVersion) DUMP_FIELD(PacketSeqNum) - DUMP_FIELD(SendingTime) + DUMP_FIELD(DisseminationTime) ; } }; +inline std::ostream& operator<<(std::ostream& out, PacketHeader const& obj) +{ + obj.dump(out); + return out; +} + } // namespace protocol::messages diff --git a/include/messages/PostTrade.hpp b/include/messages/PostTrade.hpp new file mode 100644 index 0000000..4d536e1 --- /dev/null +++ b/include/messages/PostTrade.hpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.11 Post-trade +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 201 (Application, SchemaID=1). Payload = 170 bytes (with 1 Parties entry). +// +// FIX semantic: MarketDataIncrementalRefresh [X] +// +// Constants NOT on the wire (per the SBE table): +// NoMDEntries=1, MDEntryType=2 (Trade), Symbol="[N/A]", +// SecurityIDSource=4 (ISIN), NoRegulatoryTradeIDs=1, +// RegulatoryTradeIDType=5 (TVTIC), NoTradeTypes=0 (encoded as bitmap), +// NoTradePriceConditions=0 (bitmap), NoTrdRegPublications=0 (bitmap), +// TrdRegPublicationType=0 (except for 'LRGS' where it is 1), +// NoTrdRegTimestamps=5, +// TrdRegTimestampType_0..4 / TrdRegTimestampOrigin_0..4 +// (always 1/C, 2/P, 2/C, 11/P, 11/C), +// PartyIDSource=G (MIC). +// +// ⚠️ Special handling notes: +// • MDOriginType is ALWAYS present on the wire (1 byte) but the spec +// mandates the application IGNORE it when LastMkt is 'SINT' or 'XOFF'. +// • is a variable-size repeating group prefixed by a +// RptGrpHeader. This struct hard-codes a single entry to keep the type +// trivially copyable. TODO: introduce a dynamic-view class to handle +// N>1 parties (e.g. when both PartyRole=62 and 73 are present). + +#pragma once + +#include +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// PostTrade Parties group — single entry layout (PartyID + PartyRole). +/// PartyIDSource is constant ('G' MIC) and therefore omitted from the wire. +struct PostTradePartyEntry +{ + types::Mic m_PartyID; ///< 4B — venue MIC. + types::PartyRole m_PartyRole; ///< 1B — 62 (Report originator) or 73 (Execution venue). +}; + +static_assert(sizeof(PostTradePartyEntry) == 5, + "PostTradePartyEntry size mismatch (5 bytes = 4+1)"); + +/// PostTrade payload (§4.2.11). Single Parties entry (TODO: variable). +struct PostTradeStruct +{ + types::MdUpdateAction m_MDUpdateAction; ///< 1B — 0/1/2 (New/AMND/CANC). + types::SeqNum m_MsgSeqNum; ///< 8B — instrument-level sequence number. + types::Isin m_SecurityID; ///< 12B — ISO 6166 ISIN. + types::Mic m_SecurityExchange; ///< 4B — place-of-listing MIC. + types::Mic m_LastMkt; ///< 4B — venue-of-execution MIC. + types::Decimal m_MDEntryPx; ///< 9B — traded price (NULL when PNDG/NOAP). + types::Currency m_Currency; ///< 3B — ISO 4217 price currency. + types::Decimal m_MDEntrySize; ///< 9B — quantity. + types::MdOriginType m_MDOriginType; ///< 1B — trading system type (ignore when LastMkt='SINT'/'XOFF'). + types::AlgorithmicTradeIndicator m_AlgorithmicTradeIndicator; ///< 1B — 0/1 (Non-algo/ALGO). + types::MdQualityIndicatorBitmap m_MDQualityIndicator; ///< 1B — suspicious trade flag bitmap. + types::TransactionIdentifier m_RegulatoryTradeID; ///< 52B — TVTIC code (alphanumeric, up to 52 chars). + types::TradeTypeBitmap m_TradeType; ///< 1B — bitmap (Portfolio/Benchmark/Package). + types::TradePriceConditionBitmap m_TradePriceCondition; ///< 1B — bitmap (SDIV/NPFT/PNDG/NOAP). + types::TrdRegPublicationReasonBitmap m_TrdRegPublicationReason; ///< 1B — bitmap (NLIQ/OILQ/PRIC/RFPT/LRGS). + types::MmtFlag m_MMTCode; ///< 14B — MMT efficient encoding flag string. + types::TradeCondition m_TradeCondition; ///< 1B — char enum ('0' or 'k'). + types::TimestampNano m_TrdRegTimestamp_0; ///< 8B — Execution time, implied Type=1, Origin=C. + types::TimestampNano m_TrdRegTimestamp_1; ///< 8B — Time in (CTP), implied Type=2, Origin=P. + types::TimestampNano m_TrdRegTimestamp_2; ///< 8B — Time in (APA), implied Type=2, Origin=C. + types::TimestampNano m_TrdRegTimestamp_3; ///< 8B — Publication by CTP, implied Type=11, Origin=P. + types::TimestampNano m_TrdRegTimestamp_4; ///< 8B — Publication by Contrib, implied Type=11, Origin=C. + types::RptGrpHeader m_NoPartyIDs; ///< 2B — SBE group header (BlockLength + NumInGroup). + PostTradePartyEntry m_Parties[1]; ///< 5B — TODO: support N>1 entries via dynamic view. +}; + +#pragma pack(pop) + +static_assert(sizeof(PostTradeStruct) == 170, + "PostTradeStruct size mismatch with spec (170 bytes with 1 Parties entry)"); +static_assert(std::is_standard_layout::value, + "PostTradeStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "PostTradeStruct must be trivially copyable"); + +/// PostTrade application message — disseminates a single executed trade +/// (§4.2.11). Uses the FIX MarketDataIncrementalRefresh [X] template. +class PostTrade +{ + MessageHeader const m_MessageHeader; + PostTradeStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_POST_TRADE; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(PostTradeStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this PostTrade instance. + explicit PostTrade(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + types::MdUpdateAction getMDUpdateAction() const { return m_Struct.m_MDUpdateAction; } + types::SeqNum getMsgSeqNum() const { return m_Struct.m_MsgSeqNum; } + std::string getSecurityID() const { return std::string(m_Struct.m_SecurityID, types::ISIN_SIZE); } + std::string getSecurityExchange() const { return std::string(m_Struct.m_SecurityExchange, types::MIC_SIZE); } + std::string getLastMkt() const { return std::string(m_Struct.m_LastMkt, types::MIC_SIZE); } + types::Decimal getMDEntryPx() const { return m_Struct.m_MDEntryPx; } + std::string getCurrency() const { return std::string(m_Struct.m_Currency, types::CURRENCY_SIZE); } + types::Decimal getMDEntrySize() const { return m_Struct.m_MDEntrySize; } + types::MdOriginType getMDOriginType() const { return m_Struct.m_MDOriginType; } + types::AlgorithmicTradeIndicator getAlgorithmicTradeIndicator() const { return m_Struct.m_AlgorithmicTradeIndicator; } + types::MdQualityIndicatorBitmap getMDQualityIndicator() const { return m_Struct.m_MDQualityIndicator; } + std::string getRegulatoryTradeID() const { return std::string(m_Struct.m_RegulatoryTradeID, types::TRANSACTION_IDENTIFIER_SIZE); } + types::TradeTypeBitmap getTradeType() const { return m_Struct.m_TradeType; } + types::TradePriceConditionBitmap getTradePriceCondition() const { return m_Struct.m_TradePriceCondition; } + types::TrdRegPublicationReasonBitmap getTrdRegPublicationReason() const { return m_Struct.m_TrdRegPublicationReason; } + std::string getMMTCode() const { return std::string(m_Struct.m_MMTCode, types::MMT_FLAG_SIZE); } + types::TradeCondition getTradeCondition() const { return m_Struct.m_TradeCondition; } + types::TimestampNano getTrdRegTimestamp_0() const { return m_Struct.m_TrdRegTimestamp_0; } + types::TimestampNano getTrdRegTimestamp_1() const { return m_Struct.m_TrdRegTimestamp_1; } + types::TimestampNano getTrdRegTimestamp_2() const { return m_Struct.m_TrdRegTimestamp_2; } + types::TimestampNano getTrdRegTimestamp_3() const { return m_Struct.m_TrdRegTimestamp_3; } + types::TimestampNano getTrdRegTimestamp_4() const { return m_Struct.m_TrdRegTimestamp_4; } + + types::RptGrpHeader const & getNoPartyIDs() const { return m_Struct.m_NoPartyIDs; } + PostTradePartyEntry const & getParty(uint8 idx) const { return m_Struct.m_Parties[idx]; } + std::string getPartyID(uint8 idx) const { return std::string(m_Struct.m_Parties[idx].m_PartyID, types::MIC_SIZE); } + types::PartyRole getPartyRole(uint8 idx) const { return m_Struct.m_Parties[idx].m_PartyRole; } + + void dump(std::ostream & os) const + { + os << "PostTrade:\n"; + m_MessageHeader.dump(os); + os << " MDUpdateAction=" << static_cast(m_Struct.m_MDUpdateAction) << "\n"; + os + DUMP_FIELD(MsgSeqNum) + DUMP_FIELD(SecurityID) + DUMP_FIELD(SecurityExchange) + DUMP_FIELD(LastMkt) + DUMP_FIELD(MDEntryPx) + DUMP_FIELD(Currency) + DUMP_FIELD(MDEntrySize) + ; + os << " MDOriginType=" << static_cast(m_Struct.m_MDOriginType) << "\n"; + os << " AlgorithmicTradeIndicator=" << static_cast(m_Struct.m_AlgorithmicTradeIndicator) << "\n"; + os + DUMP_FIELD_AS_INT(MDQualityIndicator) + DUMP_FIELD(RegulatoryTradeID) + DUMP_FIELD_AS_INT(TradeType) + DUMP_FIELD_AS_INT(TradePriceCondition) + DUMP_FIELD_AS_INT(TrdRegPublicationReason) + DUMP_FIELD(MMTCode) + ; + os << " TradeCondition=" << static_cast(m_Struct.m_TradeCondition) << "\n"; + os + DUMP_FIELD(TrdRegTimestamp_0) + DUMP_FIELD(TrdRegTimestamp_1) + DUMP_FIELD(TrdRegTimestamp_2) + DUMP_FIELD(TrdRegTimestamp_3) + DUMP_FIELD(TrdRegTimestamp_4) + ; + os << " NoPartyIDs.m_EntryLength=" << static_cast(m_Struct.m_NoPartyIDs.m_EntryLength) + << " m_NumOfEntries=" << static_cast(m_Struct.m_NoPartyIDs.m_NumOfEntries) << "\n"; + os << " Party[0].PartyID=" << getPartyID(0) + << " PartyRole=" << static_cast(getPartyRole(0)) << "\n"; + } +}; + +inline std::ostream& operator<<(std::ostream& out, PostTrade const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/ReplayRequest.hpp b/include/messages/ReplayRequest.hpp new file mode 100644 index 0000000..f535b16 --- /dev/null +++ b/include/messages/ReplayRequest.hpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.4 Replay Request message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 20 (Administrative, SchemaID=2). Payload = 19 bytes. + +#pragma once + +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// ReplayRequest payload — issued by clients on the TCP replay channel +/// (§4.2.4). +struct ReplayRequestStruct +{ + uint8 m_FeedID; ///< 1B — feed for which the replay is requested. + uint8 m_ChannelID; ///< 1B — channel within the feed. + uint8 m_PacketSeqVersion; ///< 1B — must match the most recent SequenceReset value. + types::SeqNum m_BeginSeqNo; ///< 8B — first packet sequence to replay. + types::SeqNum m_EndSeqNo; ///< 8B — last packet sequence to replay. +}; + +#pragma pack(pop) + +static_assert(sizeof(ReplayRequestStruct) == 19, + "ReplayRequestStruct size mismatch with spec (19 bytes = 1+1+1+8+8)"); +static_assert(std::is_standard_layout::value, + "ReplayRequestStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "ReplayRequestStruct must be trivially copyable"); + +/// ReplayRequest administrative message — asks EuroCTP to retransmit a range +/// of packets for a given channel (§4.2.4). +class ReplayRequest +{ + MessageHeader const m_MessageHeader; + ReplayRequestStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_REPLAY_REQUEST; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(ReplayRequestStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this ReplayRequest instance. + explicit ReplayRequest(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + uint8 getFeedID() const { return m_Struct.m_FeedID; } + uint8 getChannelID() const { return m_Struct.m_ChannelID; } + uint8 getPacketSeqVersion() const { return m_Struct.m_PacketSeqVersion; } + types::SeqNum getBeginSeqNo() const { return m_Struct.m_BeginSeqNo; } + types::SeqNum getEndSeqNo() const { return m_Struct.m_EndSeqNo; } + + void dump(std::ostream & os) const + { + os << "ReplayRequest:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD_AS_INT(FeedID) + DUMP_FIELD_AS_INT(ChannelID) + DUMP_FIELD_AS_INT(PacketSeqVersion) + DUMP_FIELD(BeginSeqNo) + DUMP_FIELD(EndSeqNo) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, ReplayRequest const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/ReplayStart.hpp b/include/messages/ReplayStart.hpp new file mode 100644 index 0000000..6be1bf9 --- /dev/null +++ b/include/messages/ReplayStart.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.5 Replay Start message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 21 (Administrative, SchemaID=2). Payload = 17 bytes. + +#pragma once + +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// ReplayStart payload — server-side acknowledgement that a ReplayRequest is +/// being processed (§4.2.5). +struct ReplayStartStruct +{ + uint8 m_PacketSeqVersion; ///< 1B — packet-level sequence number version. + types::SeqNum m_BeginSeqNo; ///< 8B — first packet sequence that will be replayed. + types::SeqNum m_EndSeqNo; ///< 8B — last packet sequence that will be replayed. +}; + +#pragma pack(pop) + +static_assert(sizeof(ReplayStartStruct) == 17, + "ReplayStartStruct size mismatch with spec (17 bytes = 1+8+8)"); +static_assert(std::is_standard_layout::value, + "ReplayStartStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "ReplayStartStruct must be trivially copyable"); + +/// ReplayStart administrative message — sent by EuroCTP to confirm a replay +/// is about to begin and to advertise the actual range that will be resent +/// (§4.2.5). +class ReplayStart +{ + MessageHeader const m_MessageHeader; + ReplayStartStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_REPLAY_START; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(ReplayStartStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this ReplayStart instance. + explicit ReplayStart(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + uint8 getPacketSeqVersion() const { return m_Struct.m_PacketSeqVersion; } + types::SeqNum getBeginSeqNo() const { return m_Struct.m_BeginSeqNo; } + types::SeqNum getEndSeqNo() const { return m_Struct.m_EndSeqNo; } + + void dump(std::ostream & os) const + { + os << "ReplayStart:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD_AS_INT(PacketSeqVersion) + DUMP_FIELD(BeginSeqNo) + DUMP_FIELD(EndSeqNo) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, ReplayStart const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/SequenceReset.hpp b/include/messages/SequenceReset.hpp new file mode 100644 index 0000000..6e61a75 --- /dev/null +++ b/include/messages/SequenceReset.hpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.7 Sequence reset message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 2 (Administrative, SchemaID=2). Payload = 1 byte. + +#pragma once + +#include +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +#pragma pack(push, 1) + +/// SequenceReset payload — single byte indicating the new packet-level +/// sequence-number version for the channel (§4.2.7). +struct SequenceResetStruct +{ + uint8 m_PacketSeqVersion; ///< Version of the packet-level sequence number. +}; + +#pragma pack(pop) + +static_assert(sizeof(SequenceResetStruct) == 1, + "SequenceResetStruct size mismatch with spec (1 byte)"); +static_assert(std::is_standard_layout::value, + "SequenceResetStruct must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "SequenceResetStruct must be trivially copyable"); + +/// SequenceReset administrative message — announces the new packet-level +/// sequence-number version for a given channel (§4.2.7). +class SequenceReset +{ + MessageHeader const m_MessageHeader; + SequenceResetStruct const & m_Struct; + +public: + static constexpr unsigned int TYPE = MSG_ID_SEQUENCE_RESET; + static constexpr unsigned int SIZE = MessageHeader::SIZE + sizeof(SequenceResetStruct); + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this SequenceReset instance. + explicit SequenceReset(InputData const & data) + : m_MessageHeader(data) + , m_Struct(*reinterpret_cast(data.data() + MessageHeader::SIZE)) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + uint8 getPacketSeqVersion() const { return m_Struct.m_PacketSeqVersion; } + + void dump(std::ostream & os) const + { + os << "SequenceReset:\n"; + m_MessageHeader.dump(os); + os + DUMP_FIELD_AS_INT(PacketSeqVersion) + ; + } +}; + +inline std::ostream& operator<<(std::ostream& out, SequenceReset const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/SnapshotEnd.hpp b/include/messages/SnapshotEnd.hpp new file mode 100644 index 0000000..58ac765 --- /dev/null +++ b/include/messages/SnapshotEnd.hpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: TODO +// Auto-generated from specs/euro_ctp_spec.pdf — §4.2.6 Snapshot End message +// Spec version: 1.3 Generated: 2026-06-12 +// TemplateID = 30 (Administrative, SchemaID=2). Empty payload. + +#pragma once + +#include + +#include "messages/Common.hpp" +#include "messages/MessageHeader.hpp" +#include "messages/MessageIds.hpp" +#include "messages/types/Types.hpp" + +namespace protocol::messages { + +/// SnapshotEnd — marks the end of a snapshot transmission on a snapshot channel. +/// FeedID / ChannelID are provided in the packet header. No payload (§4.2.6). +class SnapshotEnd +{ + MessageHeader const m_MessageHeader; + +public: + static constexpr unsigned int TYPE = MSG_ID_SNAPSHOT_END; + static constexpr unsigned int SIZE = MessageHeader::SIZE; // header only, no payload + + /// @param data Raw byte buffer that MUST start at the FramingHeader and + /// remain alive for the lifetime of this SnapshotEnd instance. + explicit SnapshotEnd(InputData const & data) + : m_MessageHeader(data) + { + } + + MessageHeader const & getMessageHeader() const { return m_MessageHeader; } + + void dump(std::ostream & os) const + { + os << "SnapshotEnd:\n"; + m_MessageHeader.dump(os); + } +}; + +inline std::ostream& operator<<(std::ostream& out, SnapshotEnd const& obj) +{ + obj.dump(out); + return out; +} + +} // namespace protocol::messages diff --git a/include/messages/types/Aliases.hpp b/include/messages/types/Aliases.hpp new file mode 100644 index 0000000..c9a0e64 --- /dev/null +++ b/include/messages/types/Aliases.hpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: TODO +// All reusable typedefs / `using` aliases extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. +// +// Source : specs/euro_ctp_spec.pdf — section §4.1.2 (Data types) +// Spec version: 1.3 Generated: 2026-06-12 +// Do not edit manually unless spec changed. + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === AppId — §4.1.2 ========================================================== +/// Client application identifier — alphanumerical, fixed 32 bytes (ASCII). +/// Used in the Logon message (ClientAppId field). +using AppId = char[32]; + +inline constexpr unsigned int APP_ID_SIZE = 32; +static_assert(sizeof(AppId) == APP_ID_SIZE, + "AppId size mismatch with spec (32 bytes)"); + +// === AppVersion — §4.1.2 ===================================================== +/// Client application version — alphanumerical, fixed 16 bytes (ASCII). +/// Used in the Logon message (ClientAppVersion field). +using AppVersion = char[16]; + +inline constexpr unsigned int APP_VERSION_SIZE = 16; +static_assert(sizeof(AppVersion) == APP_VERSION_SIZE, + "AppVersion size mismatch with spec (16 bytes)"); + +// === Currency — §4.1.2 ======================================================= +/// ISO 4217 currency code — alpha, fixed 3 bytes (ASCII). +using Currency = char[3]; + +inline constexpr unsigned int CURRENCY_SIZE = 3; +static_assert(sizeof(Currency) == CURRENCY_SIZE, + "Currency size mismatch with spec (3 bytes)"); + +// === EsmaCode — §4.1.2 ======================================================= +/// ESMA reference code — alpha, fixed 4 bytes (ASCII). +using EsmaCode = char[4]; + +inline constexpr unsigned int ESMA_CODE_SIZE = 4; +static_assert(sizeof(EsmaCode) == ESMA_CODE_SIZE, + "EsmaCode size mismatch with spec (4 bytes)"); + +// === InternalIdentifier — §4.1.2 ============================================= +/// Internal identifier (e.g. EBBO/EBAP/EFBA/Trade IDs) — int64, 8 bytes (LE). +using InternalIdentifier = int64; + +inline constexpr unsigned int INTERNAL_IDENTIFIER_SIZE = 8; +static_assert(sizeof(InternalIdentifier) == INTERNAL_IDENTIFIER_SIZE, + "InternalIdentifier size mismatch with spec (8 bytes)"); + +// === Isin — §4.1.2 =========================================================== +/// ISO 6166 instrument identifier (ISIN) — alphanumerical, fixed 12 bytes (ASCII). +using Isin = char[12]; + +inline constexpr unsigned int ISIN_SIZE = 12; +static_assert(sizeof(Isin) == ISIN_SIZE, + "Isin size mismatch with spec (12 bytes)"); + +// === LogoutReason — §4.1.2 =================================================== +/// Free-form logout reason text — alphanumerical, fixed 256 bytes (ASCII). +/// Carried by the Logout administrative message (see §4.2.3). +using LogoutReason = char[256]; + +inline constexpr unsigned int LOGOUT_REASON_SIZE = 256; +static_assert(sizeof(LogoutReason) == LOGOUT_REASON_SIZE, + "LogoutReason size mismatch with spec (256 bytes)"); + +// === Mic — §4.1.2 ============================================================ +/// ISO 10383 Market Identifier Code — alpha, fixed 4 bytes (ASCII). +/// Used for SecurityExchange (207), LastMkt (30), MarketID (1301), PartyID (448), etc. +using Mic = char[4]; + +inline constexpr unsigned int MIC_SIZE = 4; +static_assert(sizeof(Mic) == MIC_SIZE, + "Mic size mismatch with spec (4 bytes)"); + +// === MmtFlag — §4.1.2 ======================================================== +/// Market Model Typology (MMT) flag — efficient encoding, fixed 14 bytes (ASCII). +/// Built by EuroCTP from contributors' input following MMT 5.0 standard. +/// Each character represents a position starting at 1; '?' = no value for a +/// mandatory position. +using MmtFlag = char[14]; + +inline constexpr unsigned int MMT_FLAG_SIZE = 14; +static_assert(sizeof(MmtFlag) == MMT_FLAG_SIZE, + "MmtFlag size mismatch with spec (14 bytes)"); + +// === Password — §4.1.2 ======================================================= +/// Logon password — alphanumerical, fixed 32 bytes (ASCII). +using Password = char[32]; + +inline constexpr unsigned int PASSWORD_SIZE = 32; +static_assert(sizeof(Password) == PASSWORD_SIZE, + "Password size mismatch with spec (32 bytes)"); + +// === SeqNum — §4.1.2 ========================================================= +/// Sequence number (instrument-level / packet-level) — int64, 8 bytes (LE). +using SeqNum = int64; + +inline constexpr unsigned int SEQ_NUM_SIZE = 8; +static_assert(sizeof(SeqNum) == SEQ_NUM_SIZE, + "SeqNum size mismatch with spec (8 bytes)"); + +// === TransactionIdentifier — §4.1.2 ========================================== +/// Trading Venue Transaction Identification Code (TVTIC) — alphanumerical, 52 B (ASCII). +/// Identifies a single transaction per Article 12 of Commission Delegated Regulation +/// (EU) 2017/580. Carried in RegulatoryTradeID (1903) of post-trade messages. +using TransactionIdentifier = char[52]; + +inline constexpr unsigned int TRANSACTION_IDENTIFIER_SIZE = 52; +static_assert(sizeof(TransactionIdentifier) == TRANSACTION_IDENTIFIER_SIZE, + "TransactionIdentifier size mismatch with spec (52 bytes)"); + +// === Username — §4.1.2 ======================================================= +/// Logon username — alphanumerical, fixed 32 bytes (ASCII). +using Username = char[32]; + +inline constexpr unsigned int USERNAME_SIZE = 32; +static_assert(sizeof(Username) == USERNAME_SIZE, + "Username size mismatch with spec (32 bytes)"); + +} // namespace protocol::messages::types diff --git a/include/messages/types/Bitmaps.hpp b/include/messages/types/Bitmaps.hpp new file mode 100644 index 0000000..2ea243b --- /dev/null +++ b/include/messages/types/Bitmaps.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: TODO +// All reusable bitmap aliases + UPPER_SNAKE masks extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. +// +// Source : specs/euro_ctp_spec.pdf — sections §4.2.8 / §4.2.11 +// Spec version: 1.3 Generated: 2026-06-12 +// Do not edit manually unless spec changed. + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === MdQualityIndicatorBitmap — §4.2.11 ====================================== +/// MDQualityIndicator (FIX Tag 3105) — suspicious-trade flag bitmap. +/// Spec values: 0, 1, 2, 4 — described as a bitmap. Use the masks below to +/// test bits with bitwise AND. Value 0 (no mask set) means 'No data quality issue'. +using MdQualityIndicatorBitmap = uint8; + +inline constexpr uint8 MD_QUALITY_INDICATOR_NONE_VALUE = 0x00; ///< 'FALSE' (no flag set). +inline constexpr uint8 MD_QUALITY_INDICATOR_SUSPICIOUS_TRADE_MASK = 0x01; ///< 'TRUE' — Suspicious trade. +inline constexpr uint8 MD_QUALITY_INDICATOR_SIZE_DEVIATION_MASK = 0x02; ///< Suspicious due to size deviation. +inline constexpr uint8 MD_QUALITY_INDICATOR_PRICE_DEVIATION_MASK = 0x04; ///< Suspicious due to price deviation. + +inline constexpr unsigned int MD_QUALITY_INDICATOR_BITMAP_SIZE = sizeof(MdQualityIndicatorBitmap); +static_assert(MD_QUALITY_INDICATOR_BITMAP_SIZE == 1, + "MdQualityIndicatorBitmap size mismatch with spec (1 byte)"); + +// === QuoteConditionsBitmap — §§4.2.8 / 4.2.9 / 4.2.10 ======================== +/// QuoteConditions (FIX Tag 276) — quote conditions bitmap. +/// EBBO supports values "0, 1, 2, 3, 4, 6, 8 or 10" — bitwise combinations. +/// EBAP / EFBA only use NONE or the OutOfSequence bit. +using QuoteConditionsBitmap = uint8; + +inline constexpr uint8 QUOTE_CONDITIONS_NONE_VALUE = 0x00; ///< none. +inline constexpr uint8 QUOTE_CONDITIONS_LOCKED_MASK = 0x01; ///< 'E' Locked. +inline constexpr uint8 QUOTE_CONDITIONS_OUT_OF_SEQUENCE_MASK = 0x02; ///< 'o' Out of sequence. +inline constexpr uint8 QUOTE_CONDITIONS_CROSSED_DUE_TO_LATEST_BID_MASK = 0x04; ///< '8' Crossed (latest bid). +inline constexpr uint8 QUOTE_CONDITIONS_CROSSED_DUE_TO_LATEST_OFFER_MASK = 0x08; ///< '9' Crossed (latest offer). + +inline constexpr unsigned int QUOTE_CONDITIONS_BITMAP_SIZE = sizeof(QuoteConditionsBitmap); +static_assert(QUOTE_CONDITIONS_BITMAP_SIZE == 1, + "QuoteConditionsBitmap size mismatch with spec (1 byte)"); + +// === TradePriceConditionBitmap — §4.2.11 ===================================== +/// TradePriceCondition (FIX Tag 1839) — repeating-group content encoded as a bitmap. +/// NoTradePriceConditions (1838) is constant 0 — the group content is collapsed +/// into the single byte below. +using TradePriceConditionBitmap = uint8; + +inline constexpr uint8 TRADE_PRICE_CONDITION_SPECIAL_DIVIDEND_MASK = 0x01; ///< '13' 'SDIV'. +inline constexpr uint8 TRADE_PRICE_CONDITION_NON_PRICE_FORMING_MASK = 0x02; ///< '15' 'NPFT'. +inline constexpr uint8 TRADE_PRICE_CONDITION_PRICE_PENDING_MASK = 0x04; ///< '17' 'PNDG'. +inline constexpr uint8 TRADE_PRICE_CONDITION_PRICE_NOT_APPLICABLE_MASK = 0x08; ///< '18' 'NOAP'. + +inline constexpr unsigned int TRADE_PRICE_CONDITION_BITMAP_SIZE = sizeof(TradePriceConditionBitmap); +static_assert(TRADE_PRICE_CONDITION_BITMAP_SIZE == 1, + "TradePriceConditionBitmap size mismatch with spec (1 byte)"); + +// === TradeTypeBitmap — §4.2.11 =============================================== +/// TradeType (FIX Tag 3006) — repeating-group content encoded as a bitmap. +/// NoTradeTypes (3005) is constant 0 — the group content is collapsed into +/// the single byte below. +using TradeTypeBitmap = uint8; + +inline constexpr uint8 TRADE_TYPE_PORTFOLIO_TRADE_MASK = 0x01; ///< '50' 'PORT'. +inline constexpr uint8 TRADE_TYPE_BENCHMARK_MASK = 0x02; ///< '64' 'BENC'. +inline constexpr uint8 TRADE_TYPE_PACKAGE_TRADE_MASK = 0x04; ///< '65' 'CONT'. + +inline constexpr unsigned int TRADE_TYPE_BITMAP_SIZE = sizeof(TradeTypeBitmap); +static_assert(TRADE_TYPE_BITMAP_SIZE == 1, + "TradeTypeBitmap size mismatch with spec (1 byte)"); + +// === TrdRegPublicationReasonBitmap — §4.2.11 ================================= +/// TrdRegPublicationReason (FIX Tag 2670) — repeating-group content as a bitmap. +/// NoTrdRegPublications (2668) is constant 0 — the group content is collapsed +/// into the single byte below. +/// LRGS (16) corresponds to TrdRegPublicationType = PostTradeDeferral; the four +/// other bits correspond to TrdRegPublicationType = PreTradeTransparencyWaiver. +using TrdRegPublicationReasonBitmap = uint8; + +inline constexpr uint8 TRD_REG_PUBLICATION_REASON_NLIQ_MASK = 0x01; ///< 'NLIQ'. +inline constexpr uint8 TRD_REG_PUBLICATION_REASON_OILQ_MASK = 0x02; ///< 'OILQ'. +inline constexpr uint8 TRD_REG_PUBLICATION_REASON_PRIC_MASK = 0x04; ///< 'PRIC'. +inline constexpr uint8 TRD_REG_PUBLICATION_REASON_RFPT_MASK = 0x08; ///< 'RFPT'. +inline constexpr uint8 TRD_REG_PUBLICATION_REASON_LRGS_MASK = 0x10; ///< 'LRGS' (Large in Scale deferral). + +inline constexpr unsigned int TRD_REG_PUBLICATION_REASON_BITMAP_SIZE = sizeof(TrdRegPublicationReasonBitmap); +static_assert(TRD_REG_PUBLICATION_REASON_BITMAP_SIZE == 1, + "TrdRegPublicationReasonBitmap size mismatch with spec (1 byte)"); + +} // namespace protocol::messages::types diff --git a/include/messages/types/Enums.hpp b/include/messages/types/Enums.hpp new file mode 100644 index 0000000..dfe3ba5 --- /dev/null +++ b/include/messages/types/Enums.hpp @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: TODO +// All reusable scalar enumerations extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. +// +// Source : specs/euro_ctp_spec.pdf — sections §3.1.1, §4.1.1.x, §4.1.7, §4.1.8, §4.2.x +// Spec version: 1.3 Generated: 2026-06-12 +// Do not edit manually unless spec changed. + +#pragma once + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === AdminTemplateId — §4.1.1.1 ============================================== +/// Template ID for administrative messages (SchemaID = 2). +/// Carried in UnitHeader.m_TemplateID. +enum class AdminTemplateId : uint16 +{ + Heartbeat = 1, + SequenceReset = 2, + Logon = 10, + Logout = 11, + ReplayRequest = 20, + ReplayStart = 21, + SnapshotEnd = 30 +}; + +inline constexpr unsigned int ADMIN_TEMPLATE_ID_SIZE = sizeof(AdminTemplateId); +static_assert(ADMIN_TEMPLATE_ID_SIZE == 2, + "AdminTemplateId underlying size mismatch with spec (2 bytes)"); + +// === AlgorithmicTradeIndicator — §4.2.11 ===================================== +/// AlgorithmicTradeIndicator (FIX Tag 2667) — flags whether the trade was algorithmic. +enum class AlgorithmicTradeIndicator : uint8 +{ + NonAlgorithmic = 0, + Algorithmic = 1 +}; + +inline constexpr unsigned int ALGORITHMIC_TRADE_INDICATOR_SIZE = sizeof(AlgorithmicTradeIndicator); +static_assert(ALGORITHMIC_TRADE_INDICATOR_SIZE == 1, + "AlgorithmicTradeIndicator underlying size mismatch with spec (1 byte)"); + +// === ApplicationTemplateId — §4.1.1.2 ======================================== +/// Template ID for application (market data) messages (SchemaID = 1). +/// Carried in UnitHeader.m_TemplateID. +enum class ApplicationTemplateId : uint16 +{ + Ebbo = 101, + Ebap = 102, + Efba = 103, + PostTrade = 201, + InstrumentStatus = 301, + OrderMatchingSystemStatus = 302 +}; + +inline constexpr unsigned int APPLICATION_TEMPLATE_ID_SIZE = sizeof(ApplicationTemplateId); +static_assert(APPLICATION_TEMPLATE_ID_SIZE == 2, + "ApplicationTemplateId underlying size mismatch with spec (2 bytes)"); + +// === FeedId — §3.1.1 ========================================================= +/// Identifies the type of feed carried by a multicast channel. +/// Carried in PacketHeader.m_FeedID. +enum class FeedId : uint8 +{ + PreTrade = 1, + PostTrade = 2, + RegulatoryStatus = 3 +}; + +inline constexpr unsigned int FEED_ID_SIZE = sizeof(FeedId); +static_assert(FEED_ID_SIZE == 1, + "FeedId underlying size mismatch with spec (1 byte)"); + +// === LogoutCode — §4.1.8 ===================================================== +/// Logout reason code — root cause for session termination. +/// Carried in Logout message LogoutCode (uint16) — see §4.2.3. +/// Note: the spec reserves the unenumerated ranges; do not invent new values. +enum class LogoutCode : uint16 +{ + Success = 0, + BadEncoding = 1, + UnexpectedMessage = 2, + BadMessageLength = 3, + InvalidCredentials = 101, + TooManyConnections = 102, + InvalidChannelId = 103, + TooManyReplayRequests = 104, + InvalidReplayRequest = 105, + BadSequenceVersion = 201, + OtherError = 999 +}; + +inline constexpr unsigned int LOGOUT_CODE_SIZE = sizeof(LogoutCode); +static_assert(LOGOUT_CODE_SIZE == 2, + "LogoutCode underlying size mismatch with spec (2 bytes)"); + +// === MatchType — §4.2.12 ===================================================== +/// MatchType (FIX Tag 574) — Trading system phase, mutually exclusive with TradingSessionSubID. +enum class MatchType : uint8 +{ + Empty = 0, + OffExchangeTradeReporting = 1, + OnExchangeTradeReporting = 3, + SystematicInternaliserTradeReporting = 9 +}; + +inline constexpr unsigned int MATCH_TYPE_SIZE = sizeof(MatchType); +static_assert(MATCH_TYPE_SIZE == 1, + "MatchType underlying size mismatch with spec (1 byte)"); + +// === MdEntryType — §§4.2.8–4.2.11 ============================================ +/// MDEntryType (FIX Tag 269) — type of market data entry. +/// Wire encoding: char (1 byte ASCII). Values are aggregated across EBBO, EBAP, +/// EFBA and Post-trade messages (each message constrains a subset). +enum class MdEntryType : char +{ + Bid = '0', ///< EBBO. + Offer = '1', ///< EBBO. + Trade = '2', ///< Post-trade. + EmptyBook = 'J', ///< EBBO/EBAP/EFBA. + UncrossingBid = 'j', ///< EBBO. + UncrossingOffer = 'k', ///< EBBO. + AuctionClearingPrice = 'Q' ///< EBAP/EFBA. +}; + +inline constexpr unsigned int MD_ENTRY_TYPE_SIZE = sizeof(MdEntryType); +static_assert(MD_ENTRY_TYPE_SIZE == 1, + "MdEntryType underlying size mismatch with spec (1 byte)"); + +// === MdOriginType — §4.2.11 ================================================== +/// MDOriginType (FIX Tag 1024) — type of trading system on which the instrument is traded. +/// Per §4.2.11 the field is omitted when LastMkt(30) is set to 'SINT' or 'XOFF'. +enum class MdOriginType : uint8 +{ + CentralLimitOrderBook = 0, ///< CLOB. + QuoteDrivenMarket = 3, ///< QDTS. + AuctionDrivenMarket = 5, ///< PATS. + QuoteNegotiation = 6, ///< RFQT. + HybridMarket = 8, ///< HYBR. + OtherMarket = 9 ///< OTHR. +}; + +inline constexpr unsigned int MD_ORIGIN_TYPE_SIZE = sizeof(MdOriginType); +static_assert(MD_ORIGIN_TYPE_SIZE == 1, + "MdOriginType underlying size mismatch with spec (1 byte)"); + +// === MdUpdateAction — §4.2.11 ================================================ +/// MDUpdateAction (FIX Tag 279) — used when amending or cancelling a previously +/// reported transaction. +enum class MdUpdateAction : uint8 +{ + New = 0, + Change = 1, ///< 'AMND'. + Delete = 2 ///< 'CANC'. +}; + +inline constexpr unsigned int MD_UPDATE_ACTION_SIZE = sizeof(MdUpdateAction); +static_assert(MD_UPDATE_ACTION_SIZE == 1, + "MdUpdateAction underlying size mismatch with spec (1 byte)"); + +// === MostLiquidMarketIndicator — §4.2.12 ===================================== +/// MostLiquidMarketIndicator (FIX Tag 3103) — most relevant market in terms of liquidity. +enum class MostLiquidMarketIndicator : uint8 +{ + False = 0, + True = 1 +}; + +inline constexpr unsigned int MOST_LIQUID_MARKET_INDICATOR_SIZE = sizeof(MostLiquidMarketIndicator); +static_assert(MOST_LIQUID_MARKET_INDICATOR_SIZE == 1, + "MostLiquidMarketIndicator underlying size mismatch with spec (1 byte)"); + +// === PartyIdSource — §4.2.11 ================================================= +/// PartyIDSource (FIX Tag 447) — identifier scheme used for PartyID. +/// Wire encoding: char (always 'G' = MIC per spec). +enum class PartyIdSource : char +{ + MarketIdentifierCode = 'G' ///< Constant value. +}; + +inline constexpr unsigned int PARTY_ID_SOURCE_SIZE = sizeof(PartyIdSource); +static_assert(PARTY_ID_SOURCE_SIZE == 1, + "PartyIdSource underlying size mismatch with spec (1 byte)"); + +// === PartyRole — §4.2.11 ===================================================== +/// PartyRole (FIX Tag 452) — role of the party in the post-trade message. +enum class PartyRole : uint8 +{ + ReportOriginator = 62, ///< Venue of publication, with PartyIDSource = 62. + ExecutionVenue = 73 ///< Third-country trading venue, with PartyIDSource = 73. +}; + +inline constexpr unsigned int PARTY_ROLE_SIZE = sizeof(PartyRole); +static_assert(PARTY_ROLE_SIZE == 1, + "PartyRole underlying size mismatch with spec (1 byte)"); + +// === RegulatoryTradeIdType — §4.2.11 ========================================= +/// RegulatoryTradeIDType (FIX Tag 1906) — type of transaction identifier. +/// Wire encoding: uint8 (always 5 = TVTIC per spec). +enum class RegulatoryTradeIdType : uint8 +{ + TradingVenueTransactionIdentifier = 5 ///< TVTIC, constant value. +}; + +inline constexpr unsigned int REGULATORY_TRADE_ID_TYPE_SIZE = sizeof(RegulatoryTradeIdType); +static_assert(REGULATORY_TRADE_ID_TYPE_SIZE == 1, + "RegulatoryTradeIdType underlying size mismatch with spec (1 byte)"); + +// === SchemaId — §4.1.7 ======================================================= +/// Schema identifier — disambiguates the template space carried by UnitHeader.m_TemplateID. +enum class SchemaId : uint16 +{ + Application = 1, ///< Application messages (market data). + Administrative = 2 ///< Administrative messages (control flow). +}; + +inline constexpr unsigned int SCHEMA_ID_SIZE = sizeof(SchemaId); +static_assert(SCHEMA_ID_SIZE == 2, + "SchemaId underlying size mismatch with spec (2 bytes)"); + +// === SecurityIdSource — §4.2.x =============================================== +/// SecurityIDSource (FIX Tag 22) — identifier scheme used for SecurityID. +/// Wire encoding: uint8 (always 4 = ISIN per spec). +enum class SecurityIdSource : uint8 +{ + Isin = 4 ///< Constant value. +}; + +inline constexpr unsigned int SECURITY_ID_SOURCE_SIZE = sizeof(SecurityIdSource); +static_assert(SECURITY_ID_SOURCE_SIZE == 1, + "SecurityIdSource underlying size mismatch with spec (1 byte)"); + +// === SecurityStatus — §4.2.12 ================================================ +/// SecurityStatus (FIX Tag 965) — instrument status (except 'HALT'). +enum class SecurityStatus : uint8 +{ + Empty = 0, + Active = 1, ///< 'ACTV' (when there is no previous status). + Delisted = 5, ///< 'RMOV'. + Suspended = 9 ///< 'SUSP'. +}; + +inline constexpr unsigned int SECURITY_STATUS_SIZE = sizeof(SecurityStatus); +static_assert(SECURITY_STATUS_SIZE == 1, + "SecurityStatus underlying size mismatch with spec (1 byte)"); + +// === SecurityTradingStatus — §4.2.12 ========================================= +/// SecurityTradingStatus (FIX Tag 326) — instrument trading status. +/// 'Resume' is used when state changes to 'ACTV' after being in 'SUSP' or 'HALT'. +enum class SecurityTradingStatus : uint8 +{ + Empty = 0, + TradingHalt = 2, ///< 'HALT'. + Resume = 3 ///< 'ACTV' when previous status was 'SUSP' or 'HALT'. +}; + +inline constexpr unsigned int SECURITY_TRADING_STATUS_SIZE = sizeof(SecurityTradingStatus); +static_assert(SECURITY_TRADING_STATUS_SIZE == 1, + "SecurityTradingStatus underlying size mismatch with spec (1 byte)"); + +// === TradeCondition — §4.2.11 ================================================ +/// TradeCondition (FIX Tag 277) — out-of-sequence flag. +enum class TradeCondition : char +{ + False = '0', ///< 'FALSE'. + True = 'k' ///< 'TRUE'. +}; + +inline constexpr unsigned int TRADE_CONDITION_SIZE = sizeof(TradeCondition); +static_assert(TRADE_CONDITION_SIZE == 1, + "TradeCondition underlying size mismatch with spec (1 byte)"); + +// === TradingSessionId — §§4.2.10, 4.2.12 ===================================== +/// TradingSessionID (FIX Tag 336) — trading session identifier. +/// Wire encoding: uint8 (always 1 = Day per spec). +enum class TradingSessionId : uint8 +{ + Day = 1 ///< Constant value. +}; + +inline constexpr unsigned int TRADING_SESSION_ID_SIZE = sizeof(TradingSessionId); +static_assert(TRADING_SESSION_ID_SIZE == 1, + "TradingSessionId underlying size mismatch with spec (1 byte)"); + +// === TradingSessionSubId — §§4.2.10, 4.2.12 ================================== +/// TradingSessionSubID (FIX Tag 625) — trading system phase. +/// Mutually exclusive with MatchType (574) in Instrument status messages. +/// EFBA always uses OnDemandAuction (constant value). +enum class TradingSessionSubId : uint8 +{ + Empty = 0, + ScheduledOpeningAuction = 2, ///< 'SOAU'. + ContinuousTrading = 3, ///< 'COTR'. + ScheduledClosingAuction = 4, ///< 'SCAU'. + AtMarketCloseTrading = 5, ///< 'MACT'. + ScheduledIntradayAuction = 6, ///< 'SIAU'. + UndefinedAuction = 8, ///< 'UDUC'. + UnscheduledAuction = 9, ///< 'UAUC'. + OutOfMainSessionTrading = 10, ///< 'OMST'. + OnDemandAuction = 14, ///< 'ODAU' (FBA). + Other = 99 ///< 'OTSP'. +}; + +inline constexpr unsigned int TRADING_SESSION_SUB_ID_SIZE = sizeof(TradingSessionSubId); +static_assert(TRADING_SESSION_SUB_ID_SIZE == 1, + "TradingSessionSubId underlying size mismatch with spec (1 byte)"); + +// === TradSesStatus — §4.2.13 ================================================= +/// TradSesStatus (FIX Tag 340) — status of the trading system. +enum class TradSesStatus : uint8 +{ + Open = 2, ///< 'ACTV'. + Outage = 7, ///< 'OTAG'. + PartialOutage = 8 ///< 'POTG'. +}; + +inline constexpr unsigned int TRAD_SES_STATUS_SIZE = sizeof(TradSesStatus); +static_assert(TRAD_SES_STATUS_SIZE == 1, + "TradSesStatus underlying size mismatch with spec (1 byte)"); + +// === TrdRegPublicationType — §4.2.11 ========================================= +/// TrdRegPublicationType (FIX Tag 2669) — publication type for the post-trade entry. +enum class TrdRegPublicationType : uint8 +{ + PreTradeTransparencyWaiver = 0, ///< Combined with TrdRegPublicationReason. + PostTradeDeferral = 1 ///< 'LRGS'. +}; + +inline constexpr unsigned int TRD_REG_PUBLICATION_TYPE_SIZE = sizeof(TrdRegPublicationType); +static_assert(TRD_REG_PUBLICATION_TYPE_SIZE == 1, + "TrdRegPublicationType underlying size mismatch with spec (1 byte)"); + +// === TrdRegTimestampOrigin — §§4.2.8–4.2.11 ================================== +/// TrdRegTimestampOrigin (FIX Tag 771) — origin of the regulatory timestamp. +enum class TrdRegTimestampOrigin : char +{ + Contributor = 'C', ///< Trading venue. + Publisher = 'P' +}; + +inline constexpr unsigned int TRD_REG_TIMESTAMP_ORIGIN_SIZE = sizeof(TrdRegTimestampOrigin); +static_assert(TRD_REG_TIMESTAMP_ORIGIN_SIZE == 1, + "TrdRegTimestampOrigin underlying size mismatch with spec (1 byte)"); + +// === TrdRegTimestampType — §§4.2.8–4.2.11 ==================================== +/// TrdRegTimestampType (FIX Tag 770) — semantic of a regulatory timestamp. +/// Each occurrence is paired with a TrdRegTimestampOrigin. +enum class TrdRegTimestampType : uint8 +{ + ExecutionTime = 1, ///< Trading date and time. + TimeIn = 2, ///< Reception date and time. + PubliclyReported = 11, ///< Dissemination/Publication date and time. + ReferenceTimeForBboOrAuction = 34, ///< Reference time for NBBO / EBBO / EBAP / EFBA. + UpdateTime = 36 ///< Entry / Indicative date and time. +}; + +inline constexpr unsigned int TRD_REG_TIMESTAMP_TYPE_SIZE = sizeof(TrdRegTimestampType); +static_assert(TRD_REG_TIMESTAMP_TYPE_SIZE == 1, + "TrdRegTimestampType underlying size mismatch with spec (1 byte)"); + +// === VenueType — §§4.2.12, 4.2.13 ============================================ +/// VenueType (FIX Tag 1430) — type of trading system on which the instrument / +/// system status is provided. +enum class VenueType : char +{ + CentralLimitOrderBook = 'B', ///< CLOB. + QuoteDrivenMarket = 'Q', ///< QDTS. + AuctionDrivenMarket = 'A', ///< PATS. + QuoteNegotiation = 'N', ///< RFQT. + HybridMarket = 'H', ///< HYBR. + OtherMarket = 'z' ///< OTHR (lower-case as per spec). +}; + +inline constexpr unsigned int VENUE_TYPE_SIZE = sizeof(VenueType); +static_assert(VENUE_TYPE_SIZE == 1, + "VenueType underlying size mismatch with spec (1 byte)"); + +} // namespace protocol::messages::types diff --git a/include/messages/types/GeoPosition.hpp b/include/messages/types/GeoPosition.hpp deleted file mode 100755 index 9f6f216..0000000 --- a/include/messages/types/GeoPosition.hpp +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: TODO -// Auto-generated from sample_spec.pdf — section §4.3.2 (Table 7) -// Spec version: 1.0 Generated: 2026-06-12 -// Category: struct -// Do not edit manually unless spec changed. - -#pragma once - -#include - -#include "messages/Common.hpp" - -namespace protocol::messages::types { - -#pragma pack(push, 1) - -/// WGS-84 geographic position with altitude. -/// @see sample_spec.pdf §4.3.2 — Table 7 -struct GeoPosition -{ - int32 m_LatitudeE7; ///< Latitude in 1e-7 deg, range [-90e7, +90e7]. - int32 m_LongitudeE7; ///< Longitude in 1e-7 deg, range [-180e7, +180e7]. - int32 m_AltitudeMm; ///< Altitude above ellipsoid, millimeters. - uint32 m_AccuracyMm; ///< Horizontal 1-sigma accuracy, millimeters. - uint32 m_TimestampMs; ///< GPS time of fix, milliseconds (mod 2^32). -}; - -#pragma pack(pop) - -static_assert(sizeof(GeoPosition) == 20, - "GeoPosition size mismatch with spec (20 bytes)"); -static_assert(std::is_standard_layout::value, - "GeoPosition must be standard-layout"); -static_assert(std::is_trivially_copyable::value, - "GeoPosition must be trivially copyable"); - -} // namespace protocol::messages::types diff --git a/include/messages/types/Structs.hpp b/include/messages/types/Structs.hpp new file mode 100644 index 0000000..a8e3b2f --- /dev/null +++ b/include/messages/types/Structs.hpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: TODO +// All reusable composed binary structs extracted from the PDF spec. +// Sections are kept alphabetical. Add new entries via /pdf-to-cpp-types. +// +// Source : specs/euro_ctp_spec.pdf — sections §4.1.2, §4.1.3, §4.1.6, §4.1.7 +// Spec version: 1.3 Generated: 2026-06-12 +// Do not edit manually unless spec changed. + +#pragma once + +#include +#include + +#include "messages/Common.hpp" + +namespace protocol::messages::types { + +// === Decimal — §4.1.2 ======================================================== +/// Floating-point decimal used for prices and sizes (FIX SBE encoding). +/// Value = m_Mantissa * 10^m_Exponent. Up to 17 decimal places per spec. +/// Wire layout (little-endian): int64 mantissa (8B) || int8 exponent (1B) = 9B. +#pragma pack(push, 1) +struct Decimal +{ + int64 m_Mantissa; ///< Signed 64-bit mantissa. + int8 m_Exponent; ///< Decimal exponent (typically negative). +}; +#pragma pack(pop) + +inline std::ostream& operator<<(std::ostream& os, Decimal const& d) +{ + return os << d.m_Mantissa << "e" << static_cast(d.m_Exponent); +} + +inline constexpr unsigned int DECIMAL_SIZE = 9; +static_assert(sizeof(Decimal) == DECIMAL_SIZE, + "Decimal size mismatch with spec (9 bytes)"); +static_assert(std::is_standard_layout::value, + "Decimal must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "Decimal must be trivially copyable"); + +// === FramingHeader — §4.1.6 ================================================== +/// SBE framing header — supports stream-based message framing. +/// Precedes the UnitHeader on the wire. Wire layout (little-endian): uint16 (2B). +#pragma pack(push, 1) +struct FramingHeader +{ + uint16 m_MessageLength; ///< Overall message length including headers, in bytes. +}; +#pragma pack(pop) + +inline constexpr unsigned int FRAMING_HEADER_SIZE = 2; +static_assert(sizeof(FramingHeader) == FRAMING_HEADER_SIZE, + "FramingHeader size mismatch with spec (2 bytes)"); +static_assert(std::is_standard_layout::value, + "FramingHeader must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "FramingHeader must be trivially copyable"); + +// === GeoPosition — §4.3.2 (legacy sample) ==================================== +/// WGS-84 geographic position with altitude. +/// Kept from the original sample spec to demonstrate composed binary structs. +#pragma pack(push, 1) +struct GeoPosition +{ + int32 m_LatitudeE7; ///< Latitude in 1e-7 deg, range [-90e7, +90e7]. + int32 m_LongitudeE7; ///< Longitude in 1e-7 deg, range [-180e7, +180e7]. + int32 m_AltitudeMm; ///< Altitude above ellipsoid, millimeters. + uint32 m_AccuracyMm; ///< Horizontal 1-sigma accuracy, millimeters. + uint32 m_TimestampMs; ///< GPS time of fix, milliseconds (mod 2^32). +}; +#pragma pack(pop) + +inline constexpr unsigned int GEO_POSITION_SIZE = 20; +static_assert(sizeof(GeoPosition) == GEO_POSITION_SIZE, + "GeoPosition size mismatch with spec (20 bytes)"); +static_assert(std::is_standard_layout::value, + "GeoPosition must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "GeoPosition must be trivially copyable"); + +// === RptGrpHeader — §4.1.2 =================================================== +/// SBE repeating-group header. +/// Wire layout (little-endian): uint8 entryLength (1B) || uint8 numOfEntries (1B) = 2B. +#pragma pack(push, 1) +struct RptGrpHeader +{ + uint8 m_EntryLength; ///< Fixed-length size, in bytes, of one group entry's fixed fields. + uint8 m_NumOfEntries; ///< Number of entries in the repeating group. +}; +#pragma pack(pop) + +inline constexpr unsigned int RPT_GRP_HEADER_SIZE = 2; +static_assert(sizeof(RptGrpHeader) == RPT_GRP_HEADER_SIZE, + "RptGrpHeader size mismatch with spec (2 bytes)"); +static_assert(std::is_standard_layout::value, + "RptGrpHeader must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "RptGrpHeader must be trivially copyable"); + +// === TimestampNano — §§4.1.2 / 4.1.3 ========================================= +/// Nanoseconds-since-Epoch timestamp. +/// Epoch base = 1970-01-01 (UTC); will be rebased on 2036-01-01 per spec. +/// Wire layout (little-endian): uint64 (8B). +#pragma pack(push, 1) +struct TimestampNano +{ + uint64 m_Ns; ///< Nanoseconds since Epoch. +}; +#pragma pack(pop) + +inline std::ostream& operator<<(std::ostream& os, TimestampNano const& t) +{ + return os << t.m_Ns << "ns"; +} + +inline constexpr unsigned int TIMESTAMP_NANO_SIZE = 8; +static_assert(sizeof(TimestampNano) == TIMESTAMP_NANO_SIZE, + "TimestampNano size mismatch with spec (8 bytes)"); +static_assert(std::is_standard_layout::value, + "TimestampNano must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "TimestampNano must be trivially copyable"); + +// === UnitHeader — §4.1.7 ===================================================== +/// SBE unit header — identifies the SBE template used to decode the payload. +/// Follows the FramingHeader on the wire. +/// Wire layout (little-endian): uint16 (2B) × 4 = 8 B. +#pragma pack(push, 1) +struct UnitHeader +{ + uint16 m_MsgSize; ///< Length of the entire message including the message header (bytes). + uint16 m_TemplateID; ///< Template ID — message type identifier (see §4.1.1). + uint16 m_SchemaID; ///< Schema ID: 1 = Application, 2 = Administrative. + uint16 m_Version; ///< Schema version used for backward compatibility. +}; +#pragma pack(pop) + +inline constexpr unsigned int UNIT_HEADER_SIZE = 8; +static_assert(sizeof(UnitHeader) == UNIT_HEADER_SIZE, + "UnitHeader size mismatch with spec (8 bytes)"); +static_assert(std::is_standard_layout::value, + "UnitHeader must be standard-layout"); +static_assert(std::is_trivially_copyable::value, + "UnitHeader must be trivially copyable"); + +} // namespace protocol::messages::types + +// ============================================================================= +// Backward-compat aliases — re-export the most-used domain structs in the +// parent namespace `protocol::messages` so legacy code (e.g. +// examples/PostTrade.hpp) referring to `Decimal` / `TimestampNano` keeps +// compiling without qualifying with `types::`. +// ============================================================================= +namespace protocol::messages { +using Decimal = types::Decimal; +using TimestampNano = types::TimestampNano; +} // namespace protocol::messages diff --git a/include/messages/types/Types.hpp b/include/messages/types/Types.hpp index b17aa54..6963ad8 100755 --- a/include/messages/types/Types.hpp +++ b/include/messages/types/Types.hpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: TODO -// Aggregator header for all reusable types extracted from the PDF spec. -// Add one #include per generated type in include/messages/types/. +// Aggregator header — pulls every reusable type extracted from the PDF spec. +// Do NOT add any other include here. New types are added inside one of the +// 4 category files below via /pdf-to-cpp-types. #pragma once -// Generated types — keep alphabetical. -#include "messages/types/GeoPosition.hpp" -// #include "messages/types/.hpp" +#include "messages/types/Aliases.hpp" +#include "messages/types/Bitmaps.hpp" +#include "messages/types/Enums.hpp" +#include "messages/types/Structs.hpp" diff --git a/templates/Type.hpp.template b/templates/Type.hpp.template index a24f60d..1581406 100755 --- a/templates/Type.hpp.template +++ b/templates/Type.hpp.template @@ -1,66 +1,84 @@ // SPDX-License-Identifier: TODO +// ============================================================================= +// Type insertion template — sections to APPEND inside the consolidated files +// under include/messages/types/. NEVER create a per-type .hpp file: use the +// 4 category files (Aliases.hpp / Enums.hpp / Bitmaps.hpp / Structs.hpp). +// // Auto-generated from {{SPEC_PDF}} — section/table {{SPEC_SECTION}} // Spec version: {{SPEC_VERSION}} Generated: {{DATE}} -// Category: {{CATEGORY}} (struct | enum | bitfield | alias) +// Category: {{CATEGORY}} (alias | enum | bitmap | struct) // Do not edit manually unless spec changed. +// ============================================================================= +// +// Choose ONE block depending on CATEGORY and PASTE it (sorted alphabetically) +// inside the matching category file. The surrounding `namespace +// protocol::messages::types { ... }` block is already present in the file — +// only paste the section content (separator + doc + type definition + +// asserts + constants). -#pragma once +// ---- (1) ALIAS → paste into include/messages/types/Aliases.hpp ------------- -#include - -#include "messages/Common.hpp" +// === {{TYPE_NAME}} — §{{SPEC_SECTION}} ======================================= +/// {{BRIEF_DESCRIPTION}} +/// @see {{SPEC_PDF}} §{{SPEC_SECTION}} +using {{TYPE_NAME}} = {{UNDERLYING_TYPE}}; // ex: char[12], int64, uint64 -namespace {{NAMESPACE}} { +inline constexpr unsigned int {{NAME_UPPER}}_SIZE = {{EXPECTED_SIZE}}; +static_assert(sizeof({{TYPE_NAME}}) == {{NAME_UPPER}}_SIZE, + "{{TYPE_NAME}} size mismatch with spec ({{EXPECTED_SIZE}} bytes)"); -// ============================================================================ -// Choose ONE block depending on CATEGORY. Delete the unused blocks. -// ============================================================================ -// ---- (1) STRUCT ------------------------------------------------------------ -#pragma pack(push, 1) +// ---- (2) ENUM → paste into include/messages/types/Enums.hpp --------------- +// === {{TYPE_NAME}} — §{{SPEC_SECTION}} ======================================= /// {{BRIEF_DESCRIPTION}} /// @see {{SPEC_PDF}} §{{SPEC_SECTION}} -struct {{TYPE_NAME}} +enum class {{TYPE_NAME}} : {{UNDERLYING_TYPE}} // uint8 / uint16 / char { - {{FIELDS}} // ex: int32 m_LatitudeE7; uint32 m_AccuracyMm; + {{ENUM_VALUES}} // ex: Ok = 0x00, Error = 0xFF, Bid = '0' }; -#pragma pack(pop) +inline constexpr unsigned int {{NAME_UPPER}}_SIZE = sizeof({{TYPE_NAME}}); +static_assert({{NAME_UPPER}}_SIZE == {{EXPECTED_SIZE}}, + "{{TYPE_NAME}} underlying size mismatch with spec ({{EXPECTED_SIZE}} byte(s))"); -static_assert(sizeof({{TYPE_NAME}}) == {{EXPECTED_SIZE}}, - "{{TYPE_NAME}} size mismatch with spec ({{EXPECTED_SIZE}} bytes)"); -static_assert(std::is_standard_layout<{{TYPE_NAME}}>::value, - "{{TYPE_NAME}} must be standard-layout"); -static_assert(std::is_trivially_copyable<{{TYPE_NAME}}>::value, - "{{TYPE_NAME}} must be trivially copyable"); -// ---- (2) ENUM -------------------------------------------------------------- +// ---- (3) BITMAP → paste into include/messages/types/Bitmaps.hpp ------------- + +// === {{TYPE_NAME}}Bitmap — §{{SPEC_SECTION}} ================================= /// {{BRIEF_DESCRIPTION}} /// @see {{SPEC_PDF}} §{{SPEC_SECTION}} -enum class {{TYPE_NAME}} : {{UNDERLYING_TYPE}} // uint8 / uint16 / uint32 -{ - {{ENUM_VALUES}} // ex: Ok = 0x00, Error = 0xFF -}; +using {{TYPE_NAME}}Bitmap = {{UNDERLYING_TYPE}}; // typically uint8 + +inline constexpr {{UNDERLYING_TYPE}} {{NAME_UPPER}}_NONE_VALUE = 0x00; +inline constexpr {{UNDERLYING_TYPE}} {{NAME_UPPER}}_FOO_MASK = 0x01; // ex +inline constexpr {{UNDERLYING_TYPE}} {{NAME_UPPER}}_BAR_MASK = 0x02; // ex +// ... add more masks as needed, alphabetical inside the bitmap is OK ... + +inline constexpr unsigned int {{NAME_UPPER}}_BITMAP_SIZE = sizeof({{TYPE_NAME}}Bitmap); +static_assert({{NAME_UPPER}}_BITMAP_SIZE == {{EXPECTED_SIZE}}, + "{{TYPE_NAME}}Bitmap size mismatch with spec ({{EXPECTED_SIZE}} byte(s))"); + -// ---- (3) BITFIELD ---------------------------------------------------------- -// Bit ordering is implementation-defined. -// Target compiler: {{TARGET_COMPILER}} ({{BIT_ORDER}}-first). +// ---- (4) STRUCT → paste into include/messages/types/Structs.hpp ------------- + +// === {{TYPE_NAME}} — §{{SPEC_SECTION}} ======================================= #pragma pack(push, 1) +/// {{BRIEF_DESCRIPTION}} +/// @see {{SPEC_PDF}} §{{SPEC_SECTION}} struct {{TYPE_NAME}} { - {{BITFIELDS}} // ex: uint8 m_Ready : 1; uint8 m_Reserved : 7; + {{FIELDS}} // ex: int32 m_LatitudeE7; uint32 m_AccuracyMm; }; #pragma pack(pop) -static_assert(sizeof({{TYPE_NAME}}) == {{EXPECTED_SIZE}}, +inline constexpr unsigned int {{NAME_UPPER}}_SIZE = {{EXPECTED_SIZE}}; +static_assert(sizeof({{TYPE_NAME}}) == {{NAME_UPPER}}_SIZE, "{{TYPE_NAME}} size mismatch with spec ({{EXPECTED_SIZE}} bytes)"); +static_assert(std::is_standard_layout<{{TYPE_NAME}}>::value, + "{{TYPE_NAME}} must be standard-layout"); +static_assert(std::is_trivially_copyable<{{TYPE_NAME}}>::value, + "{{TYPE_NAME}} must be trivially copyable"); -// ---- (4) ALIAS ------------------------------------------------------------- -/// {{BRIEF_DESCRIPTION}} -/// @see {{SPEC_PDF}} §{{SPEC_SECTION}} -using {{TYPE_NAME}} = {{UNDERLYING_TYPE}}; - -} // namespace {{NAMESPACE}}