Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
mifid2_reporter.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3// See LICENSE_COMMERCIAL for full terms.
4// mifid2_reporter.hpp — MiFID II RTS 24 Algorithmic Trading Compliance Reporter
5// Phase 10d: Compliance Report Generators
6//
7// Reads DecisionLog Parquet files written by DecisionLogWriter and generates
8// MiFID II RTS 24-compliant reports covering algorithmic trading decisions.
9//
10// Regulatory reference:
11// Commission Delegated Regulation (EU) 2017/589 — RTS 24
12// Article 9: record-keeping for algorithmic trading
13// Annex I: fields required for each order/decision
14//
15// MiFID II RTS 24 Annex I field mapping from DecisionRecord:
16// Field 1 — Entity ID (firm_id from ReportOptions, or strategy_id)
17// Field 2 — Trading venue transaction ID (order_id)
18// Field 3 — Client ID (redacted — GDPR Art.25 pseudonymisation)
19// Field 4 — Investment decision maker (strategy_id = algorithm identifier)
20// Field 5 — Financial instrument (symbol)
21// Field 6 — Unit price / limit price (price)
22// Field 7 — Original quantity (quantity)
23// Field 8 — Date/time (timestamp_ns → ISO 8601 to nanosecond precision)
24// Field 9 — Decision type (ORDER_NEW, ORDER_CANCEL, etc.)
25// Field 10 — Venue (venue)
26// Field 11 — Algorithm model version (model_version)
27// Field 12 — Risk gate outcome (risk_result)
28// Field 13 — Model output score (model_output)
29// Field 14 — Confidence (confidence)
30// Field 15 — Chain sequence number (tamper-evidence)
31// Field 16 — Chain entry hash (hex, 64 chars)
32//
33// Usage:
34// ReportOptions opts;
35// opts.firm_id = "FIRM_LEI_123456";
36// opts.format = ReportFormat::JSON;
37// opts.start_ns = t0;
38// opts.end_ns = t1;
39// auto report = MiFID2Reporter::generate({"decisions_0.parquet"}, opts);
40// if (report) std::cout << report->content;
41
42#pragma once
43
44#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
45#error "signet/ai/compliance/mifid2_reporter.hpp requires SIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier). See LICENSE_COMMERCIAL."
46#endif
47
48#include "signet/error.hpp"
51
52#include <algorithm>
53#include <chrono>
54#include <cstdint>
55#include <cstdio>
56#include <cstdlib>
57#include <cstring>
58#include <ctime>
59#include <sstream>
60#include <string>
61#include <vector>
62
63#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
64# include <stdlib.h> // arc4random_buf
65#elif !defined(_WIN32)
66# include <sys/random.h> // getrandom
67#endif
68
69namespace signet::forge {
70
82public:
93 const std::vector<std::string>& log_files,
94 const ReportOptions& opts = {}) {
95
96 auto license = commercial::require_feature("MiFID2Reporter");
97 if (!license) return license.error();
98
99 if (log_files.empty())
101 "MiFID2Reporter: no log files supplied"};
102
103 std::vector<DecisionRecord> records;
104 bool chain_ok = true;
105 std::string chain_id;
106 bool incomplete_data = false;
107 std::vector<std::string> read_errors;
108
109 for (const auto& path : log_files) {
110 auto rdr_result = DecisionLogReader::open(path);
111 if (!rdr_result) {
112 // Fail on unreadable files — silent skip risks incomplete reports (CWE-754)
113 return Error{ErrorCode::IO_ERROR,
114 "MiFID2Reporter: cannot open log file '" + path +
115 "': " + rdr_result.error().message};
116 }
117 auto& rdr = *rdr_result;
118
119 // Chain verification
120 if (opts.verify_chain) {
121 auto vr = rdr.verify_chain();
122 if (!vr.valid) chain_ok = false;
123 }
124
125 // Metadata (chain_id from first file)
126 if (chain_id.empty()) {
127 auto meta_result = rdr.audit_metadata();
128 if (meta_result) chain_id = meta_result->chain_id;
129 }
130
131 // Read records
132 auto all_result = rdr.read_all();
133 if (!all_result) {
134 if (opts.strict_source_reads) {
135 return Error{ErrorCode::IO_ERROR,
136 "MiFID2Reporter: failed to read records from '" + path +
137 "': " + all_result.error().message};
138 }
139 incomplete_data = true;
140 read_errors.push_back("Failed to read records from '" + path +
141 "': " + all_result.error().message);
142 continue;
143 }
144
145 for (auto& rec : *all_result) {
146 if (rec.timestamp_ns >= opts.start_ns &&
147 rec.timestamp_ns <= opts.end_ns)
148 records.push_back(std::move(rec));
149 }
150 }
151
152 // Sort by timestamp ascending
153 std::sort(records.begin(), records.end(),
154 [](const DecisionRecord& a, const DecisionRecord& b) {
155 return a.timestamp_ns < b.timestamp_ns;
156 });
157
158 auto usage = commercial::record_usage_rows(
159 "MiFID2Reporter::generate", static_cast<uint64_t>(records.size()));
160 if (!usage) return usage.error();
161
162 // Build report
163 ComplianceReport report;
164 report.standard = ComplianceStandard::MIFID2_RTS24;
165 report.format = opts.format;
166 report.chain_verified = chain_ok;
167 report.chain_id = chain_id;
168 report.total_records = static_cast<int64_t>(records.size());
169 report.incomplete_data = incomplete_data;
170 report.read_errors = std::move(read_errors);
171 report.generated_at_ns = now_ns_();
172 report.generated_at_iso = ns_to_iso8601(report.generated_at_ns);
173 report.period_start_iso = ns_to_iso8601(opts.start_ns);
174 report.period_end_iso = (opts.end_ns == (std::numeric_limits<int64_t>::max)())
175 ? "open"
176 : ns_to_iso8601(opts.end_ns);
177 // MiFID II RTS 24 Annex I Table 2, Field 1: unique report ID.
178 // CSPRNG random hex suffix prevents predictable/guessable report IDs
179 // (timestamp alone is insufficient for uniqueness under concurrent generation).
180 report.report_id = opts.report_id.empty()
181 ? ("MIFID2-" + std::to_string(report.generated_at_ns)
182 + "-" + random_hex_suffix_())
183 : opts.report_id;
184
185 switch (opts.format) {
188 report.content = format_json(records, opts, report, chain_ok);
189 break;
191 report.content = format_csv(records, opts);
192 break;
193 }
194
195 return report;
196 }
197
201 [[nodiscard]] static std::string csv_header() {
202 return "timestamp_utc,order_id,firm_id,algo_identifier,"
203 "model_version,instrument,venue,decision_type,"
204 "price,quantity,risk_gate,model_output,confidence,"
205 "buy_sell,order_type,time_in_force,isin,currency,"
206 "short_selling,aggregated_order,parent_order_id,"
207 "chain_seq,chain_hash\n";
208 }
209
218 [[nodiscard]] static std::string record_to_csv_row(
219 const DecisionRecord& rec,
220 const std::string& firm_id = "") {
221
222 std::string row;
223 row += csv_escape(ns_to_iso8601(rec.timestamp_ns)) + ",";
224 row += csv_escape(rec.order_id) + ",";
225 row += csv_escape(firm_id.empty()
226 ? std::to_string(rec.strategy_id) : firm_id) + ",";
227 row += csv_escape(std::to_string(rec.strategy_id)) + ",";
228 row += csv_escape(rec.model_version) + ",";
229 row += csv_escape(rec.symbol) + ",";
230 row += csv_escape(rec.venue) + ",";
231 row += csv_escape(decision_type_str(rec.decision_type)) + ",";
232 row += double_str(rec.price) + ",";
233 row += double_str(rec.quantity) + ",";
234 row += csv_escape(risk_result_str(rec.risk_result)) + ",";
235 row += double_str(rec.model_output) + ",";
236 row += double_str(rec.confidence) + ",";
237 // RTS 24 Annex I mandatory fields (Gap R-4)
238 row += csv_escape(buy_sell_str(rec.buy_sell)) + ",";
239 row += csv_escape(order_type_str(rec.order_type)) + ",";
240 row += csv_escape(time_in_force_str(rec.time_in_force)) + ",";
241 row += csv_escape(rec.isin) + ",";
242 row += csv_escape(rec.currency) + ",";
243 row += (rec.short_selling_flag ? "true" : "false") + std::string(",");
244 row += (rec.aggregated_order ? "true" : "false") + std::string(",");
245 row += csv_escape(rec.parent_order_id) + ",";
246 // chain fields omitted for single-record streaming use
247 row += ",\n";
248 return row;
249 }
250
251private:
252 // -------------------------------------------------------------------------
253 // JSON formatter
254 // -------------------------------------------------------------------------
255
256 static std::string format_json(
257 const std::vector<DecisionRecord>& records,
258 const ReportOptions& opts,
259 const ComplianceReport& meta,
260 bool chain_ok) {
261
262 const bool ndjson = (opts.format == ReportFormat::NDJSON);
263 const std::string ind = opts.pretty_print && !ndjson ? " " : "";
264 const std::string ind2 = opts.pretty_print && !ndjson ? " " : "";
265 const std::string nl = opts.pretty_print && !ndjson ? "\n" : "";
266 const std::string sp = opts.pretty_print && !ndjson ? " " : "";
267
268 if (ndjson) {
269 // One JSON object per record, no outer envelope
270 std::string out;
271 for (const auto& rec : records)
272 out += record_to_json_line(rec, opts) + "\n";
273 return out;
274 }
275
276 std::string o;
277 o += "{" + nl;
278 o += ind + "\"report_type\":" + sp + "\"MiFID_II_RTS_24\"," + nl;
279 o += ind + "\"regulatory_reference\":" + sp
280 + "\"Commission Delegated Regulation (EU) 2017/589 — RTS 24\"," + nl;
281 o += ind + "\"report_id\":" + sp + "\"" + j(meta.report_id) + "\"," + nl;
282 o += ind + "\"generated_at\":" + sp + "\"" + meta.generated_at_iso + "\"," + nl;
283 o += ind + "\"period_start\":" + sp + "\"" + meta.period_start_iso + "\"," + nl;
284 o += ind + "\"period_end\":" + sp + "\"" + meta.period_end_iso + "\"," + nl;
285 o += ind + "\"firm_id\":" + sp + "\""
286 + j(opts.firm_id.empty() ? "UNSPECIFIED" : opts.firm_id)
287 + "\"," + nl;
288 o += ind + "\"chain_id\":" + sp + "\"" + j(meta.chain_id) + "\"," + nl;
289 o += ind + "\"chain_verified\":" + sp + (chain_ok ? "true" : "false") + "," + nl;
290 o += ind + "\"total_records\":" + sp + std::to_string(records.size()) + "," + nl;
291 o += ind + "\"records\":" + sp + "[" + nl;
292
293 for (size_t i = 0; i < records.size(); ++i) {
294 const auto& rec = records[i];
295 o += ind2 + "{" + nl;
296 // Annex I fields
297 o += ind2 + ind + "\"field_01_firm_id\":" + sp
298 + "\"" + j(opts.firm_id.empty() ? std::to_string(rec.strategy_id) : opts.firm_id)
299 + "\"," + nl;
300 o += ind2 + ind + "\"field_02_order_id\":" + sp
301 + "\"" + j(rec.order_id) + "\"," + nl;
302 o += ind2 + ind + "\"field_03_client_id\":" + sp
303 + "\"[REDACTED-GDPR]\"," + nl;
304 o += ind2 + ind + "\"field_04_algo_identifier\":" + sp
305 + "\"" + j(std::to_string(rec.strategy_id)) + "\"," + nl;
306 o += ind2 + ind + "\"field_05_instrument\":" + sp
307 + "\"" + j(rec.symbol) + "\"," + nl;
308 o += ind2 + ind + "\"field_06_price\":" + sp
309 + double_str(rec.price, opts.price_significant_digits) + "," + nl;
310 o += ind2 + ind + "\"field_07_quantity\":" + sp
311 + double_str(rec.quantity) + "," + nl;
312 o += ind2 + ind + "\"field_08_timestamp_utc\":" + sp
313 + "\"" + ns_to_iso8601(rec.timestamp_ns, opts.timestamp_granularity) + "\"," + nl;
314 o += ind2 + ind + "\"field_09_decision_type\":" + sp
315 + "\"" + decision_type_str(rec.decision_type) + "\"," + nl;
316 o += ind2 + ind + "\"field_10_venue\":" + sp
317 + "\"" + j(rec.venue) + "\"," + nl;
318 o += ind2 + ind + "\"field_11_model_version\":" + sp
319 + "\"" + j(rec.model_version) + "\"," + nl;
320 o += ind2 + ind + "\"field_12_risk_gate\":" + sp
321 + "\"" + risk_result_str(rec.risk_result) + "\"," + nl;
322 o += ind2 + ind + "\"field_13_model_output\":" + sp
323 + double_str(rec.model_output) + "," + nl;
324 o += ind2 + ind + "\"field_14_confidence\":" + sp
325 + double_str(rec.confidence) + "," + nl;
326 // MiFID II RTS 24 Annex I mandatory fields (Gap R-4)
327 o += ind2 + ind + "\"field_15_buy_sell\":" + sp
328 + "\"" + buy_sell_str(rec.buy_sell) + "\"," + nl;
329 o += ind2 + ind + "\"field_16_order_type\":" + sp
330 + "\"" + order_type_str(rec.order_type) + "\"," + nl;
331 o += ind2 + ind + "\"field_17_time_in_force\":" + sp
332 + "\"" + time_in_force_str(rec.time_in_force) + "\"," + nl;
333 o += ind2 + ind + "\"field_18_isin\":" + sp
334 + "\"" + j(rec.isin) + "\"," + nl;
335 o += ind2 + ind + "\"field_19_currency\":" + sp
336 + "\"" + j(rec.currency) + "\"," + nl;
337 o += ind2 + ind + "\"field_20_short_selling\":" + sp
338 + (rec.short_selling_flag ? "true" : "false") + "," + nl;
339 o += ind2 + ind + "\"field_21_aggregated_order\":" + sp
340 + (rec.aggregated_order ? "true" : "false") + "," + nl;
341 if (rec.validity_period_ns > 0) {
342 o += ind2 + ind + "\"field_22_validity_period\":" + sp
343 + "\"" + ns_to_iso8601(rec.validity_period_ns) + "\"," + nl;
344 }
345 if (!rec.parent_order_id.empty()) {
346 o += ind2 + ind + "\"field_23_parent_order_id\":" + sp
347 + "\"" + j(rec.parent_order_id) + "\"," + nl;
348 }
349 o += ind2 + ind + "\"field_24_notes\":" + sp
350 + "\"" + j(rec.notes) + "\"";
351 if (opts.include_features && !rec.input_features.empty()) {
352 o += "," + nl;
353 o += ind2 + ind + "\"input_features\":" + sp
354 + features_array(rec.input_features);
355 }
356 o += nl + ind2 + "}";
357 if (i + 1 < records.size()) o += ",";
358 o += nl;
359 }
360 o += ind + "]" + nl;
361 o += "}" + nl;
362 return o;
363 }
364
365 static std::string record_to_json_line(const DecisionRecord& rec,
366 const ReportOptions& opts) {
367 std::string o = "{";
368 o += "\"ts\":\"" + ns_to_iso8601(rec.timestamp_ns, opts.timestamp_granularity) + "\",";
369 o += "\"order_id\":\"" + j(rec.order_id) + "\",";
370 o += "\"algo\":\"" + j(std::to_string(rec.strategy_id)) + "\",";
371 o += "\"model\":\"" + j(rec.model_version) + "\",";
372 o += "\"instrument\":\"" + j(rec.symbol) + "\",";
373 o += "\"venue\":\"" + j(rec.venue) + "\",";
374 o += "\"type\":\"" + decision_type_str(rec.decision_type) + "\",";
375 o += "\"price\":" + double_str(rec.price, opts.price_significant_digits) + ",";
376 o += "\"qty\":" + double_str(rec.quantity) + ",";
377 o += "\"risk_gate\":\"" + risk_result_str(rec.risk_result) + "\",";
378 o += "\"output\":" + double_str(rec.model_output) + ",";
379 o += "\"conf\":" + double_str(rec.confidence) + ",";
380 // RTS 24 Annex I mandatory fields (Gap R-4)
381 o += "\"buy_sell\":\"" + buy_sell_str(rec.buy_sell) + "\",";
382 o += "\"order_type\":\"" + order_type_str(rec.order_type) + "\",";
383 o += "\"tif\":\"" + time_in_force_str(rec.time_in_force) + "\",";
384 o += "\"isin\":\"" + j(rec.isin) + "\",";
385 o += "\"ccy\":\"" + j(rec.currency) + "\",";
386 o += "\"short_sell\":" + std::string(rec.short_selling_flag ? "true" : "false") + ",";
387 o += "\"aggregated\":" + std::string(rec.aggregated_order ? "true" : "false");
388 o += "}";
389 return o;
390 }
391
392 // -------------------------------------------------------------------------
393 // CSV formatter
394 // -------------------------------------------------------------------------
395
396 static std::string format_csv(const std::vector<DecisionRecord>& records,
397 const ReportOptions& opts) {
398 std::string out = csv_header();
399 for (const auto& rec : records) {
400 out += csv_escape(ns_to_iso8601(rec.timestamp_ns, opts.timestamp_granularity)) + ",";
401 out += csv_escape(rec.order_id) + ",";
402 out += csv_escape(opts.firm_id.empty()
403 ? std::to_string(rec.strategy_id) : opts.firm_id) + ",";
404 out += csv_escape(std::to_string(rec.strategy_id)) + ",";
405 out += csv_escape(rec.model_version) + ",";
406 out += csv_escape(rec.symbol) + ",";
407 out += csv_escape(rec.venue) + ",";
408 out += csv_escape(decision_type_str(rec.decision_type)) + ",";
409 out += double_str(rec.price, opts.price_significant_digits) + ",";
410 out += double_str(rec.quantity) + ",";
411 out += csv_escape(risk_result_str(rec.risk_result)) + ",";
412 out += double_str(rec.model_output) + ",";
413 out += double_str(rec.confidence) + ",";
414 // RTS 24 Annex I mandatory fields (Gap R-4)
415 out += csv_escape(buy_sell_str(rec.buy_sell)) + ",";
416 out += csv_escape(order_type_str(rec.order_type)) + ",";
417 out += csv_escape(time_in_force_str(rec.time_in_force)) + ",";
418 out += csv_escape(rec.isin) + ",";
419 out += csv_escape(rec.currency) + ",";
420 out += std::string(rec.short_selling_flag ? "true":"false") + ",";
421 out += std::string(rec.aggregated_order ? "true":"false") + ",";
422 out += csv_escape(rec.parent_order_id) + ",";
423 out += ",\n"; // chain_seq + chain_hash left empty (not in DecisionRecord)
424 }
425 return out;
426 }
427
428 // -------------------------------------------------------------------------
429 // Utility helpers
430 // -------------------------------------------------------------------------
431
432 static int64_t now_ns_() {
433 using namespace std::chrono;
434 return static_cast<int64_t>(
435 duration_cast<nanoseconds>(
436 system_clock::now().time_since_epoch()).count());
437 }
438
439 static std::string ns_to_iso8601(int64_t ns,
441 static_assert(sizeof(std::time_t) >= 8,
442 "Signet compliance reporters require 64-bit time_t for timestamps beyond 2038");
443 if (ns <= 0) return "1970-01-01T00:00:00.000000000Z";
444 const int64_t secs = ns / 1'000'000'000LL;
445 const int64_t ns_part = ns % 1'000'000'000LL;
446 std::time_t t = static_cast<std::time_t>(secs);
447 std::tm tm_buf{};
448#if defined(_WIN32)
449 gmtime_s(&tm_buf, &t);
450 std::tm* utc = &tm_buf;
451#else
452 std::tm* utc = gmtime_r(&t, &tm_buf);
453#endif
454 char date_buf[32];
455 std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%dT%H:%M:%S", utc);
456 char full_buf[48];
457 // MiFID II RTS 24 Art.2(2): configurable timestamp granularity
458 switch (gran) {
460 std::snprintf(full_buf, sizeof(full_buf), "%s.%03lldZ",
461 date_buf, static_cast<long long>(ns_part / 1'000'000LL));
462 break;
464 std::snprintf(full_buf, sizeof(full_buf), "%s.%06lldZ",
465 date_buf, static_cast<long long>(ns_part / 1'000LL));
466 break;
468 default:
469 std::snprintf(full_buf, sizeof(full_buf), "%s.%09lldZ",
470 date_buf, static_cast<long long>(ns_part));
471 break;
472 }
473 return full_buf;
474 }
475
476 static std::string decision_type_str(DecisionType dt) {
477 switch (dt) {
478 case DecisionType::SIGNAL: return "SIGNAL";
479 case DecisionType::ORDER_NEW: return "ORDER_NEW";
480 case DecisionType::ORDER_CANCEL: return "ORDER_CANCEL";
481 case DecisionType::ORDER_MODIFY: return "ORDER_MODIFY";
482 case DecisionType::POSITION_OPEN: return "POSITION_OPEN";
483 case DecisionType::POSITION_CLOSE: return "POSITION_CLOSE";
484 case DecisionType::RISK_OVERRIDE: return "RISK_OVERRIDE";
485 case DecisionType::NO_ACTION: return "NO_ACTION";
486 }
487 return "UNKNOWN";
488 }
489
490 static std::string risk_result_str(RiskGateResult rg) {
491 switch (rg) {
492 case RiskGateResult::PASSED: return "PASSED";
493 case RiskGateResult::REJECTED: return "REJECTED";
494 case RiskGateResult::MODIFIED: return "MODIFIED";
495 case RiskGateResult::THROTTLED:return "THROTTLED";
496 }
497 return "UNKNOWN";
498 }
499
500 // MiFID II RTS 24 Annex I enum stringifiers (Gap R-4)
501 static std::string buy_sell_str(BuySellIndicator bs) {
502 switch (bs) {
503 case BuySellIndicator::BUY: return "BUYI";
504 case BuySellIndicator::SELL: return "SELL";
505 case BuySellIndicator::SHORT_SELL: return "SSEX";
506 }
507 return "UNKNOWN";
508 }
509
510 static std::string order_type_str(OrderType ot) {
511 switch (ot) {
512 case OrderType::MARKET: return "MARKET";
513 case OrderType::LIMIT: return "LIMIT";
514 case OrderType::STOP: return "STOP";
515 case OrderType::STOP_LIMIT: return "STOP_LIMIT";
516 case OrderType::PEGGED: return "PEGGED";
517 case OrderType::OTHER: return "OTHER";
518 }
519 return "UNKNOWN";
520 }
521
522 static std::string time_in_force_str(TimeInForce tif) {
523 switch (tif) {
524 case TimeInForce::DAY: return "DAY";
525 case TimeInForce::GTC: return "GTC";
526 case TimeInForce::IOC: return "IOC";
527 case TimeInForce::FOK: return "FOK";
528 case TimeInForce::GTD: return "GTD";
529 case TimeInForce::OTHER: return "OTHER";
530 }
531 return "UNKNOWN";
532 }
533
534 static constexpr size_t MAX_FIELD_LENGTH = 4096;
535
536 static std::string truncate_field(const std::string& s) {
537 if (s.size() <= MAX_FIELD_LENGTH) return s;
538 return s.substr(0, MAX_FIELD_LENGTH) + "...[TRUNCATED]";
539 }
540
541 static std::string j(const std::string& s) {
542 const std::string safe = truncate_field(s);
543 std::string out;
544 out.reserve(safe.size());
545 for (unsigned char c : safe) {
546 if (c == '"') out += "\\\"";
547 else if (c == '\\') out += "\\\\";
548 else if (c == '/') out += "\\/";
549 else if (c == '\n') out += "\\n";
550 else if (c == '\r') out += "\\r";
551 else if (c == '\t') out += "\\t";
552 else if (c < 0x20) { char buf[8];
553 std::snprintf(buf,sizeof(buf),"\\u%04x",c);
554 out += buf; }
555 else out += static_cast<char>(c);
556 }
557 return out;
558 }
559
560 static std::string csv_escape(const std::string& s) {
561 const std::string safe = truncate_field(s);
562 // If s contains comma, quote, or newline — wrap in quotes and double any quotes
563 if (safe.find_first_of(",\"\n\r") == std::string::npos) return safe;
564 std::string out = "\"";
565 for (char c : safe) {
566 if (c == '"') out += "\"\"";
567 else out += c;
568 }
569 out += "\"";
570 // CWE-1236: CSV formula injection sanitization.
571 // If the value inside quotes starts with a formula trigger character,
572 // prepend a single quote to neutralize it in Excel/LibreOffice.
573 if (out.size() > 1) {
574 char first = out[1]; // first char inside the opening quote
575 if (first == '=' || first == '+' || first == '-' || first == '@' ||
576 first == '\t' || first == '\r') {
577 out.insert(1, 1, '\'');
578 }
579 }
580 return out;
581 }
582
583 static std::string double_str(double v, int significant_digits = 10) {
584 if (std::isnan(v) || std::isinf(v)) return "null";
585 char buf[64];
586 std::snprintf(buf, sizeof(buf), "%.*g", significant_digits, v);
587 return buf;
588 }
589
595 static std::string random_hex_suffix_() {
596 uint8_t buf[4];
597#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
598 arc4random_buf(buf, sizeof(buf)); // kernel CSPRNG, never fails
599#elif defined(_WIN32)
600 // BCryptGenRandom is available on Vista+; for simplicity use rand_s
601 for (auto& b : buf) {
602 unsigned int val;
603 rand_s(&val); // RtlGenRandom-backed CSPRNG
604 b = static_cast<uint8_t>(val);
605 }
606#else
607 // Linux: getrandom() with flags=0 blocks until urandom is seeded
608 size_t written = 0;
609 while (written < sizeof(buf)) {
610 ssize_t ret = getrandom(buf + written, sizeof(buf) - written, 0);
611 if (ret > 0) written += static_cast<size_t>(ret);
612 else break;
613 }
614#endif
615 static constexpr char hex[] = "0123456789abcdef";
616 std::string result(8, '\0');
617 for (size_t i = 0; i < 4; ++i) {
618 result[2 * i] = hex[buf[i] >> 4];
619 result[2 * i + 1] = hex[buf[i] & 0x0F];
620 }
621 return result;
622 }
623
624 static std::string features_array(const std::vector<float>& feats) {
625 std::string o = "[";
626 for (size_t i = 0; i < feats.size(); ++i) {
627 char buf[32]; std::snprintf(buf, sizeof(buf), "%.6g", feats[i]);
628 o += buf;
629 if (i + 1 < feats.size()) o += ",";
630 }
631 o += "]";
632 return o;
633 }
634};
635
636} // namespace signet::forge
static expected< DecisionLogReader > open(const std::string &path)
Open a decision log Parquet file and pre-load all column data.
MiFID II RTS 24 algorithmic trading compliance report generator.
static expected< ComplianceReport > generate(const std::vector< std::string > &log_files, const ReportOptions &opts={})
Generate a MiFID II RTS 24 compliance report from decision log files.
static std::string record_to_csv_row(const DecisionRecord &rec, const std::string &firm_id="")
Serialize a single DecisionRecord as a CSV row.
static std::string csv_header()
Get the static CSV column header line (Annex I field order).
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
TimestampGranularity
Timestamp granularity for MiFID II RTS 24 Art.2(2) compliance.
@ NANOS
9 sub-second digits (default, MiFID II HFT compliant)
RiskGateResult
Outcome of the pre-trade risk gate evaluation.
@ PASSED
All risk checks passed.
@ MODIFIED
Order modified by risk gate (e.g., size reduced)
@ REJECTED
Order rejected by risk gate.
@ THROTTLED
Order delayed by rate limiting.
TimeInForce
Time-in-force classification for MiFID II RTS 24 Annex I Table 2 Field 8.
@ DAY
Day order (valid until end of trading day)
@ IOC
Immediate-Or-Cancel.
@ GTC
Good-Till-Cancelled.
@ MIFID2_RTS24
MiFID II RTS 24 — algorithmic trading records.
@ JSON
Pretty-printed JSON object (default)
@ NDJSON
Newline-delimited JSON — one record per line (streaming-friendly)
@ CSV
Comma-separated values with header row.
BuySellIndicator
Buy/sell direction for MiFID II RTS 24 Annex I Table 2 Field 6.
@ SHORT_SELL
Short selling (RTS 24 Annex I Field 16)
OrderType
Order type classification for MiFID II RTS 24 Annex I Table 2 Field 7.
@ OTHER
Other order type.
@ STOP_LIMIT
Stop-limit order.
DecisionType
Classification of the AI-driven trading decision.
@ NO_ACTION
Model evaluated but no action taken.
@ RISK_OVERRIDE
Risk gate override/rejection.
@ ORDER_NEW
Decision to submit a new order.
@ ORDER_CANCEL
Decision to cancel an existing order.
@ SIGNAL
Raw model signal/prediction.
@ ORDER_MODIFY
Decision to modify an existing order.
@ POSITION_CLOSE
Decision to close a position.
@ POSITION_OPEN
Decision to open a position.
@ IO_ERROR
A file-system or stream I/O operation failed (open, read, write, rename).
The generated compliance report returned to the caller.
std::string chain_id
Chain ID from the first log file processed.
std::string period_start_iso
ISO 8601 representation of opts.start_ns.
std::string report_id
Unique identifier for this report (auto-generated if not supplied).
std::string generated_at_iso
UTC ISO 8601 timestamp at which the report was generated.
std::string period_end_iso
ISO 8601 representation of opts.end_ns (or "open" if unbounded).
A single AI-driven trading decision with full provenance.
std::string symbol
Trading symbol.
std::string order_id
Associated order ID (empty if none)
double price
Decision price.
DecisionType decision_type
What type of decision.
std::string currency
Field 9: Currency (ISO 4217, 3 chars, e.g. "USD")
BuySellIndicator buy_sell
Field 6: Buy/sell direction.
RiskGateResult risk_result
Risk gate outcome.
float model_output
Primary model output (e.g., signal strength)
std::string isin
Field 5: ISIN (ISO 6166, 12 chars)
std::string model_version
Model version hash or identifier.
std::string venue
Execution venue.
TimeInForce time_in_force
Field 8: Time-in-force.
std::string parent_order_id
R-17: Parent order for lifecycle linking.
int64_t timestamp_ns
Decision timestamp (nanoseconds since epoch)
double quantity
Decision quantity.
float confidence
Model confidence [0.0, 1.0].
OrderType order_type
Field 7: Order type.
int32_t strategy_id
Which strategy made this decision.
bool short_selling_flag
Field 16: Short selling indicator.
bool aggregated_order
Field 17: Aggregated order flag.
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101
Query and formatting parameters for compliance report generation.
ReportFormat format
Output serialization format.
TimestampGranularity timestamp_granularity
Timestamp sub-second granularity (MiFID II RTS 24 Art.2(2)).
bool include_features
If true, include raw input feature vectors in the report output.
bool pretty_print
If true, emit human-readable indented JSON (2-space indent).
int price_significant_digits
Significant digits for price fields (MiFID II RTS 24 Annex I Field 6).
std::string firm_id
Organisation / firm identifier for MiFID II field 1.