2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| CsvStorage.mqh |
|
2026-09-16 15:37:55 +03:00 | | | //| Copyright 2026, MetaQuotes Ltd. |
|
2026-09-10 16:43:28 +03:00 | | | //| www.mql5.com |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
| | | #ifndef REQUEST_LATENCY_LAB_CSV_STORAGE_MQH
|
| | | #define REQUEST_LATENCY_LAB_CSV_STORAGE_MQH
|
| | |
|
| | | #include "..\..\Include\RequestLatencyLab\Models.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\RequestTracker.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\EventJournal.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\ExperimentRunner.mqh"
|
2026-09-10 16:43:28 +03:00 | | | #include "..\..\Include\RequestLatencyLab\Statistics.mqh"
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-10 16:43:28 +03:00 | | | #define LAB_DATA_ROOT "RequestLatencyLab"
|
2026-09-09 01:19:31 +03:00 | | |
|
| | | class CCsv
|
| | | {
|
| | | public:
|
| | | //--- экранирование ячейки (CSV_SCHEMA: кавычка удваивается)
|
2026-09-16 15:37:55 +03:00 | | | static string HeaderKeyValue() { return("key,value,value_type,status,source\r\n"); }
|
| | | static string Esc(const string value);
|
| | | static string BuildRow(const string &cols[], const int count);
|
| | | static string Header(const string &names[]) { return(BuildRow(names, ArraySize(names))); }
|
| | | //--- UTF-8 кодирование (только вне горячего пути)
|
| | | static bool Utf8Encode(const string s, uchar &out[]);
|
| | | static bool Utf8Decode(const uchar &in[], const int len, string &out);
|
| | | //--- сохранение файла в песочнице Files\ (защита от выхода)
|
| | | static bool SaveUtf8(const string rel_path, const string content, LabError &err,
|
| | | const bool common = false);
|
| | | static bool LoadUtf8(const string rel_path, string &content, LabError &err,
|
| | | const bool common = false);
|
| | | //--- разбор строки с учётом кавычек (сохраняя пустые ячейки)
|
| | | static bool ParseRow(const string line, string &cols[], int &count);
|
| | | //--- R8-S1: число ЛОГИЧЕСКИХ записей CSV (общий quote-aware счётчик для
|
| | | //--- read-back и reader): перевод строки ВНУТРИ кавычек — данные, а не
|
| | | //--- разделитель; пустой финальный фрагмент после завершающего \n не
|
| | | //--- считается отдельной записью. Идентичен SplitCsvRecords.
|
| | | static int CountCsvRecords(const string content);
|
| | | //--- контрольное чтение: файл существует и число записей совпадает
|
| | | static bool ReadBackVerifyFile(const string rel_path, const int expected_rows,
|
| | | LabError &err);
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Писатели файлов отчёта |
|
| | | //+------------------------------------------------------------------+
|
| | | class CCsvReport
|
| | | {
|
| | | public:
|
| | | //--- samples.csv: одна строка на доказанный вызов
|
| | | static string BuildSamples(const CRequestTracker *tracker, const string campaign_id,
|
| | | const string configuration_id);
|
| | | //--- events.csv: append-only первичные записи
|
| | | static string BuildEvents(const CEventJournal *journal);
|
| | | //--- schedule.csv из плана
|
| | | static string BuildSchedule(const CExperimentRunner *runner, const string session_id);
|
| | | //--- checks.csv
|
| | | static string BuildChecks(const CheckRecord &checks[], const int count);
|
| | | //--- manifest.csv: паспорт запуска (ключевой реестр)
|
| | | static string BuildManifest(const RunManifest &m, const LabSettings &s,
|
| | | const SymbolRules &rules);
|
| | | //--- deals.csv (схема CSV_SCHEMA.md)
|
| | | static string BuildDeals(const CRequestTracker *tracker, const string session_id);
|
| | | //--- значение метрики записи: вычисленное, иначе fallback по T0..T6
|
| | | static long MetricValue(const RequestMetadata &r, const int metric, bool &ok);
|
| | | //--- summary.csv (схема CSV_SCHEMA.md, ТЗ §7)
|
| | | static string BuildSummary(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings);
|
| | | //--- histogram.csv (схема CSV_SCHEMA.md, ТЗ §7)
|
| | | static string BuildHistogram(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings);
|
| | | //--- R4-B8: offset_signs.csv — знаковая метрика T2-T4, классифицированная
|
| | | //--- как NEGATIVE/ZERO/POSITIVE (не обычная latency-гистограмма).
|
| | | static string BuildOffsetSigns(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings);
|
| | | //--- R4-B8: comparisons.csv — сравнение условий A/B по каждой метрике
|
| | | //--- (delta медиан/p90); E4 использует ту же схему для 5 серий (ReportBuilder).
|
| | | static string BuildComparisons(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings);
|
| | | //--- market_windows.csv (наблюдаемые окна классификатора E3)
|
| | | static string BuildMarketWindows(const MarketWindow &wins[], const int count,
|
| | | const string session_id);
|
| | | //--- experiment_manifest.csv (связь условий A/B и ролей)
|
| | | static string BuildExperimentManifest(const CExperimentRunner *runner,
|
| | | const string session_id,
|
| | | const LabSettings &settings);
|
| | | //--- checks.csv: набор самопроверок экспортируемого набора данных
|
| | | static string BuildRunChecks(const CRequestTracker *tracker,
|
| | | const CEventJournal *journal,
|
| | | const CExperimentRunner *runner,
|
| | | const LabSettings &settings);
|
| | | //--- загрузка калибровки из Files\RequestLatencyLab\<id>.csv
|
| | | static bool LoadCalibration(const string calibration_id, Calibration &cal, LabError &err);
|
| | |
|
| | | private:
|
| | | static void AddKey(string &out, string &cols[], const string key, const string value);
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| CCsv:: — реализации. |
|
| | | //+------------------------------------------------------------------+
|
| | | string CCsv::Esc(const string value)
|
| | | {
|
| | | if(StringFind(value, ",") < 0 && StringFind(value, "\"") < 0 &&
|
| | | StringFind(value, "\r") < 0 && StringFind(value, "\n") < 0)
|
| | | return(value);
|
| | | string out = "\"";
|
| | | const int len = StringLen(value);
|
| | | for(int i = 0; i < len; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort ch = StringGetCharacter(value, i);
|
| | | if(ch == '"')
|
| | | out += "\"\"";
|
| | | else
|
| | | out += ShortToString(ch);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | out += "\"";
|
| | | return(out);
|
| | | }
|
| | | string CCsv::BuildRow(const string &cols[], const int count)
|
| | | {
|
| | | string line = "";
|
| | | for(int i = 0; i < count; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(i > 0)
|
| | | line += ",";
|
| | | line += Esc(cols[i]);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | line += "\r\n";
|
| | | return(line);
|
| | | }
|
| | | bool CCsv::Utf8Encode(const string s, uchar &out[])
|
| | | {
|
| | | const int n = StringToCharArray(s, out, 0, WHOLE_ARRAY, CP_UTF8);
|
| | | if(n <= 0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | ArrayResize(out, 0);
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | ArrayResize(out, n - 1); // без терминального нуля
|
| | | return(true);
|
| | | }
|
| | | bool CCsv::Utf8Decode(const uchar &in[], const int len, string &out)
|
| | | {
|
| | | uchar b[];
|
| | | if(ArrayResize(b, len + 1) != (len + 1))
|
| | | return(false);
|
| | | for(int i = 0; i < len; i++)
|
| | | b[i] = in[i];
|
| | | b[len] = 0;
|
| | | out = CharArrayToString(b, 0, len, CP_UTF8);
|
| | | return(true);
|
| | | }
|
| | | bool CCsv::SaveUtf8(const string rel_path, const string content, LabError &err,
|
| | | const bool common)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_CSV;
|
| | | if(StringFind(rel_path, "..") >= 0 || StringFind(rel_path, ":") >= 0)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 1;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "invalid sandbox path";
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | uchar bytes[];
|
| | | if(!Utf8Encode(content, bytes))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 2;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "utf8 encode failed";
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const int h = FileOpen(rel_path, FILE_WRITE | FILE_BIN | (common ? FILE_COMMON : 0));
|
| | | if(h == INVALID_HANDLE)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 3;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "cannot open " + rel_path + " err=" + IntegerToString(GetLastError());
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const int written = (int)FileWriteArray(h, bytes);
|
| | | FileClose(h);
|
| | | if(written != ArraySize(bytes))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.code = 4;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = "short write detected";
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
| | | bool CCsv::LoadUtf8(const string rel_path, string &content, LabError &err,
|
| | | const bool common)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_CSV;
|
| | | const int h = FileOpen(rel_path, FILE_READ | FILE_BIN | (common ? FILE_COMMON : 0));
|
| | | if(h == INVALID_HANDLE)
|
| | | {
|
| | | err.code = 5;
|
| | | err.message = "cannot open " + rel_path + " err=" + IntegerToString(GetLastError());
|
| | | return(false);
|
| | | }
|
| | | uchar bytes[];
|
| | | const int total = (int)FileSize(h);
|
| | | if(total > 0)
|
| | | {
|
| | | if(ArrayResize(bytes, total) != total)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | FileClose(h);
|
2026-09-10 16:43:28 +03:00 | | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const uint got = FileReadArray(h, bytes, 0, total);
|
| | | if(got != (uint)total)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | FileClose(h);
|
| | | err.code = 6;
|
2026-09-12 22:14:24 +03:00 | | | err.severity = LAB_SEV_BLOCKER;
|
2026-09-16 15:37:55 +03:00 | | | err.message = "short read " + rel_path;
|
2026-09-10 16:43:28 +03:00 | | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | ArrayResize(bytes, 0);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | FileClose(h);
|
| | | return(Utf8Decode(bytes, total, content));
|
| | | }
|
| | | bool CCsv::ParseRow(const string line, string &cols[], int &count)
|
| | | {
|
| | | count = 0;
|
| | | const int len = StringLen(line);
|
| | | string cur = "";
|
| | | bool in_quotes = false;
|
| | | for(int i = 0; i < len; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort ch0 = StringGetCharacter(line, i);
|
| | | if(in_quotes)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(ch0 == '"')
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(i + 1 < len && StringGetCharacter(line, i + 1) == '"')
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | cur += "\"";
|
| | | i++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | else
|
2026-09-16 15:37:55 +03:00 | | | in_quotes = false;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | else
|
2026-09-16 15:37:55 +03:00 | | | cur += ShortToString(ch0);
|
| | | }
|
| | | else
|
| | | {
|
| | | if(ch0 == '"')
|
| | | in_quotes = true;
|
| | | else
|
| | | if(ch0 == ',')
|
| | | {
|
| | | if(ArraySize(cols) <= count && ArrayResize(cols, count + 16) != (count + 16))
|
| | | return(false);
|
| | | cols[count] = cur;
|
| | | count++;
|
| | | cur = "";
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | cur += ShortToString(ch0);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | if(in_quotes)
|
| | | return(false); // R8-S1: незакрытая кавычка — повреждение
|
| | | if(ArraySize(cols) <= count && ArrayResize(cols, count + 16) != (count + 16))
|
| | | return(false);
|
| | | cols[count] = cur;
|
| | | count++;
|
| | | return(true);
|
| | | }
|
| | | int CCsv::CountCsvRecords(const string content)
|
| | | {
|
| | | int count = 0;
|
| | | const int len = StringLen(content);
|
| | | bool in_quotes = false;
|
| | | for(int i = 0; i < len; i++)
|
2026-09-11 18:16:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ushort ch = StringGetCharacter(content, i);
|
| | | if(ch == '"')
|
2026-09-11 18:16:32 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(in_quotes && i + 1 < len && StringGetCharacter(content, i + 1) == '"')
|
| | | i++;
|
| | | else
|
| | | in_quotes = !in_quotes;
|
| | | continue;
|
2026-09-11 18:16:32 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!in_quotes && ch == '\n')
|
2026-09-11 18:16:32 +03:00 | | | count++;
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | if(len > 0 && StringGetCharacter(content, len - 1) != '\n')
|
| | | count++;
|
| | | return(count);
|
| | | }
|
| | | bool CCsv::ReadBackVerifyFile(const string rel_path, const int expected_rows,
|
| | | LabError &err)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_CSV;
|
| | | string content;
|
| | | if(!LoadUtf8(rel_path, content, err))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | err.message = "read-back failed: " + err.message;
|
| | | return(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R8-S1: логические записи CSV (кавычки/встроенные \n/CRLF), как
|
| | | //--- в reader (LoadSamples); физическое деление по '\n' разорвало бы
|
| | | //--- встроенный перевод строки внутри кавычек.
|
| | | const int rows = CountCsvRecords(content);
|
| | | if(rows != expected_rows)
|
| | | {
|
| | | err.code = 6;
|
| | | err.severity = LAB_SEV_BLOCKER;
|
| | | err.message = StringFormat("row count mismatch: expected %d got %d",
|
| | | expected_rows, rows);
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-16 15:37:55 +03:00 | | | //| CCsvReport:: — реализации. |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-16 15:37:55 +03:00 | | | string CCsvReport::BuildSamples(const CRequestTracker *tracker, const string campaign_id,
|
| | | const string configuration_id)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string header[] =
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "request_id", "sequence", "send_start_us", "send_return_us", "request_event_us",
|
| | | "order_event_us", "first_deal_us", "final_event_us", "retcode", "completed",
|
| | | "campaign_id", "experiment_id", "series_id", "session_id", "configuration_id",
|
| | | "condition_id", "slot_id", "parent_sequence", "cleanup_attempt", "role",
|
| | | "is_warmup", "operation", "mode", "logging_mode", "symbol", "side",
|
| | | "correlation_status", "correlation_method", "order_ticket", "position_ticket",
|
| | | "position_identifier", "last_deal_us", "deal_count", "executed_volume",
|
| | | "callback_volume", "history_volume", "deal_coverage_complete", "present_mask",
|
| | | "recovered", "conflict", "measurement_interrupted", "scenario_deviation",
|
| | | "outcome", "outcome_at_final", "final_source", "trading_state_known",
|
| | | "confirmation_invalidated", "confirmation_record_id",
|
| | | "current_confirmation_record_id", "observation_deadline_us",
|
| | | "collection_deadline_us", "collection_close_us", "collection_closed",
|
| | | "deadline_exceeded", "market_regime", "calibration_id",
|
| | | "bid_at_send", "ask_at_send", "spread_at_send", "tick_time_msc",
|
| | | "actual_type", "actual_filling", "deviation_points", "comment",
|
| | | "request_price", "callback_coverage_complete", "market_window_id",
|
| | | "market_window_seq"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[80];
|
| | | const int n = tracker.Count();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r))
|
| | | continue;
|
| | | const uint pm = r.present_mask;
|
| | | //--- R4-B8: отсутствующее значение = пустое поле (CSV_SCHEMA),
|
| | | //--- а не 0: IntegerToString(0) не отличить от реальной метки 0.
|
| | | cols[0] = (r.has_request_id ? IntegerToString(r.sample.request_id) : "");
|
| | | cols[1] = IntegerToString(r.sample.sequence);
|
| | | cols[2] = ((pm & (uint)LAB_MASK_T0) != 0 ? IntegerToString(r.sample.send_start_us) : "");
|
| | | cols[3] = ((pm & (uint)LAB_MASK_T1) != 0 ? IntegerToString(r.sample.send_return_us) : "");
|
| | | cols[4] = ((pm & (uint)LAB_MASK_T2) != 0 ? IntegerToString(r.sample.request_event_us) : "");
|
| | | cols[5] = ((pm & (uint)LAB_MASK_T3) != 0 ? IntegerToString(r.sample.order_event_us) : "");
|
| | | cols[6] = ((pm & (uint)LAB_MASK_T4) != 0 ? IntegerToString(r.sample.first_deal_us) : "");
|
| | | cols[7] = ((pm & (uint)LAB_MASK_T6) != 0 ? IntegerToString(r.sample.final_event_us) : "");
|
| | | cols[8] = (r.has_retcode ? IntegerToString(r.sample.retcode) : "");
|
| | | cols[9] = (r.sample.completed ? "1" : "0");
|
| | | cols[10] = campaign_id;
|
| | | cols[11] = LabExperimentName(r.plan.experiment_id);
|
| | | cols[12] = IntegerToString(r.plan.series_id);
|
| | | cols[13] = r.key.session_id;
|
| | | cols[14] = configuration_id;
|
| | | cols[15] = r.plan.condition_id;
|
| | | cols[16] = IntegerToString(r.plan.slot_id);
|
| | | cols[17] = IntegerToString(r.plan.parent_sequence);
|
| | | cols[18] = "0";
|
| | | cols[19] = LabRoleName(r.plan.role);
|
| | | cols[20] = (r.is_warmup ? "1" : "0");
|
| | | cols[21] = LabOperationName(r.plan.operation);
|
| | | cols[22] = (r.plan.mode == LAB_MODE_SYNC ? "SYNC" : "ASYNC");
|
| | | cols[23] = (r.plan.logging_mode == LAB_LOG_MINIMAL ? "MINIMAL" : "VERBOSE");
|
| | | cols[24] = r.plan.symbol;
|
| | | cols[25] = (r.plan.side == LAB_SIDE_BUY ? "BUY" : (r.plan.side == LAB_SIDE_SELL ? "SELL" : ""));
|
| | | cols[26] = LabCorrelationName(r.correlation_status);
|
| | | cols[27] = r.correlation_method;
|
| | | cols[28] = IntegerToString(r.order_ticket);
|
| | | cols[29] = IntegerToString(r.position_ticket);
|
| | | cols[30] = IntegerToString(r.position_identifier);
|
| | | cols[31] = ((pm & (uint)LAB_MASK_T5) != 0 ? IntegerToString(r.last_deal_us) : "");
|
| | | cols[32] = IntegerToString(r.deal_count);
|
| | | cols[33] = (r.executed_volume > 0.0 ? DoubleToString(r.executed_volume, 10) : "");
|
| | | cols[34] = (r.callback_volume > 0.0 ? DoubleToString(r.callback_volume, 10) : "");
|
| | | cols[35] = (r.history_volume > 0.0 ? DoubleToString(r.history_volume, 10) : "");
|
| | | cols[36] = (r.deal_coverage_complete ? "1" : "0");
|
| | | cols[37] = IntegerToString(r.present_mask);
|
| | | cols[38] = (r.recovered ? "1" : "0");
|
| | | cols[39] = (r.conflict ? "1" : "0");
|
| | | cols[40] = (r.measurement_interrupted ? "1" : "0");
|
| | | cols[41] = (r.scenario_deviation ? "1" : "0");
|
| | | cols[42] = LabOutcomeName(r.outcome);
|
| | | cols[43] = LabOutcomeName(r.outcome_at_final);
|
| | | cols[44] = LabFinalSourceName(r.final_source);
|
| | | cols[45] = (r.trading_state_known ? "1" : "0");
|
| | | cols[46] = (r.confirmation_invalidated ? "1" : "0");
|
| | | cols[47] = IntegerToString(r.confirmation_record_id);
|
| | | cols[48] = IntegerToString(r.current_confirmation_record_id);
|
| | | cols[49] = IntegerToString(r.observation_deadline_us);
|
| | | cols[50] = IntegerToString(r.collection_deadline_us);
|
| | | cols[51] = IntegerToString(r.collection_close_us);
|
| | | cols[52] = (r.collection_closed ? "1" : "0");
|
| | | cols[53] = (r.deadline_exceeded ? "1" : "0");
|
| | | cols[54] = LabRegimeName(r.market_regime);
|
| | | cols[55] = r.calibration_id;
|
| | | cols[56] = DoubleToString(r.bid_at_send, 10);
|
| | | cols[57] = DoubleToString(r.ask_at_send, 10);
|
| | | cols[58] = DoubleToString(r.spread_at_send, 10);
|
| | | cols[59] = (r.tick_time_msc != 0 ? IntegerToString(r.tick_time_msc) : "");
|
| | | cols[60] = (r.plan.actual_known ? IntegerToString(r.plan.actual_type) : "");
|
| | | cols[61] = (r.plan.actual_known ? IntegerToString(r.plan.actual_filling) : "");
|
| | | cols[62] = IntegerToString(r.plan.deviation_points);
|
| | | cols[63] = r.plan.comment;
|
| | | cols[64] = (r.plan.request_price > 0.0 ? DoubleToString(r.plan.request_price, 10) : "");
|
| | | cols[65] = (r.callback_coverage_complete ? "1" : "0"); // R5-B4
|
| | | //--- R8-S2: связь sample->окно E3 (market_windows.csv)
|
| | | cols[66] = r.market_window_id;
|
| | | cols[67] = (r.market_window_seq != 0 ? IntegerToString(r.market_window_seq) : "");
|
| | | out += CCsv::BuildRow(cols, 68);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildEvents(const CEventJournal *journal)
|
| | | {
|
| | | const string header[] =
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "session_id", "event_sequence", "record_kind", "local_time_us", "origin_sequence",
|
| | | "target_sequence", "ref_event_sequence", "transaction_type", "request_id",
|
| | | "order_ticket", "deal_ticket", "position_ticket", "handler_enter_us",
|
| | | "handler_exit_us", "payload_json", "quality_code"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[16];
|
| | | const int n = journal.Count();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | LabEvent e;
|
| | | if(!journal.GetByIndex(i, e))
|
| | | continue;
|
| | | cols[0] = e.session_id;
|
| | | cols[1] = IntegerToString(e.event_sequence);
|
| | | cols[2] = LabKindName(e.kind);
|
| | | cols[3] = IntegerToString(e.local_time_us);
|
| | | cols[4] = IntegerToString(e.origin_sequence);
|
| | | cols[5] = IntegerToString(e.target_sequence);
|
| | | cols[6] = IntegerToString(e.ref_event_sequence);
|
| | | cols[7] = e.transaction_type;
|
| | | cols[8] = IntegerToString(e.request_id);
|
| | | cols[9] = IntegerToString(e.order_ticket);
|
| | | cols[10] = IntegerToString(e.deal_ticket);
|
| | | cols[11] = IntegerToString(e.position_ticket);
|
| | | cols[12] = IntegerToString(e.handler_enter_us);
|
| | | cols[13] = IntegerToString(e.handler_exit_us);
|
| | | cols[14] = e.payload_json;
|
| | | cols[15] = e.quality_code;
|
| | | out += CCsv::BuildRow(cols, 16);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildSchedule(const CExperimentRunner *runner, const string session_id)
|
| | | {
|
| | | const string header[] =
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "session_id", "row_id", "row_kind", "slot_id", "experiment_id", "series_id",
|
| | | "condition_id", "block_id", "role", "operation", "side", "mode", "logging_mode",
|
| | | "planned_order", "sequence", "parent_sequence", "status", "reason_code"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[18];
|
| | | const int n = runner.SlotCount();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | ScheduleSlot s;
|
| | | if(!runner.GetSlot(i, s))
|
| | | continue;
|
| | | cols[0] = session_id;
|
| | | cols[1] = IntegerToString(i + 1);
|
| | | cols[2] = "SLOT";
|
| | | cols[3] = IntegerToString(s.plan.slot_id);
|
| | | cols[4] = LabExperimentName(s.plan.experiment_id);
|
| | | cols[5] = IntegerToString(s.plan.series_id);
|
| | | cols[6] = s.plan.condition_id;
|
| | | cols[7] = IntegerToString(s.plan.block_id);
|
| | | cols[8] = LabRoleName(s.plan.role);
|
| | | cols[9] = LabOperationName(s.plan.operation);
|
| | | cols[10] = (s.plan.side == LAB_SIDE_BUY ? "BUY" : (s.plan.side == LAB_SIDE_SELL ? "SELL" : ""));
|
| | | cols[11] = (s.plan.mode == LAB_MODE_SYNC ? "SYNC" : "ASYNC");
|
| | | cols[12] = (s.plan.logging_mode == LAB_LOG_MINIMAL ? "MINIMAL" : "VERBOSE");
|
| | | cols[13] = IntegerToString(s.plan.planned_order);
|
| | | cols[14] = IntegerToString(s.plan.sequence);
|
| | | cols[15] = IntegerToString(s.plan.parent_sequence);
|
| | | cols[16] = LabSlotStatusName(s.status);
|
| | | cols[17] = s.reason_code;
|
| | | out += CCsv::BuildRow(cols, 18);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildChecks(const CheckRecord &checks[], const int count)
|
| | | {
|
| | | const string header[] =
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "check_run_id", "check_id", "case_key", "status", "severity", "expected",
|
| | | "actual", "reason_code", "source_ref", "evidence_path"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[10];
|
| | | for(int i = 0; i < count; i++)
|
| | | {
|
| | | const CheckRecord c = checks[i];
|
| | | cols[0] = "run1";
|
| | | cols[1] = c.check_id;
|
| | | cols[2] = c.case_key;
|
| | | cols[3] = LabCheckStatusName(c.status);
|
| | | cols[4] = LabSeverityName(c.severity);
|
| | | cols[5] = c.expected;
|
| | | cols[6] = c.actual;
|
| | | cols[7] = c.reason_code;
|
| | | cols[8] = c.source_ref;
|
| | | cols[9] = c.evidence_path;
|
| | | out += CCsv::BuildRow(cols, 10);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildManifest(const RunManifest &m, const LabSettings &s,
|
| | | const SymbolRules &rules)
|
| | | {
|
| | | const string header[] = {"key", "value", "value_type", "status", "source"};
|
| | | string out = CCsv::Header(header);
|
| | | string cols[5];
|
| | | AddKey(out, cols, "project_name",
|
| | | "mql5-execution-microstructure-02-request-latency");
|
| | | AddKey(out, cols, "document_status", "draft");
|
| | | AddKey(out, cols, "campaign_id", m.campaign_id);
|
| | | AddKey(out, cols, "session_id", m.session_id);
|
| | | AddKey(out, cols, "experiment_id", LabExperimentName(m.experiment_id));
|
| | | AddKey(out, cols, "series_id", IntegerToString(m.series_id));
|
| | | AddKey(out, cols, "configuration_id", s.configuration_id);
|
| | | AddKey(out, cols, "data_origin", m.data_origin);
|
| | | AddKey(out, cols, "terminal_build",
|
| | | IntegerToString(TerminalInfoInteger(TERMINAL_BUILD)));
|
| | | AddKey(out, cols, "server_name", AccountInfoString(ACCOUNT_SERVER));
|
| | | AddKey(out, cols, "symbol", s.symbol);
|
| | | AddKey(out, cols, "price_tick_size", DoubleToString(rules.tick_size, 10));
|
| | | AddKey(out, cols, "volume_min", DoubleToString(rules.volume_min, 8));
|
| | | AddKey(out, cols, "volume_step", DoubleToString(rules.volume_step, 8));
|
| | | AddKey(out, cols, "magic", IntegerToString(s.magic));
|
| | | AddKey(out, cols, "count_per_condition", IntegerToString(s.count_per_condition));
|
| | | AddKey(out, cols, "warmup_per_condition", IntegerToString(s.warmup_per_condition));
|
| | | AddKey(out, cols, "outcome_timeout_ms", IntegerToString(s.outcome_timeout_ms));
|
| | | AddKey(out, cols, "late_grace_ms", IntegerToString(s.late_grace_ms));
|
| | | AddKey(out, cols, "pause_ms", IntegerToString(s.pause_ms));
|
| | | AddKey(out, cols, "timer_requested_ms", IntegerToString(s.timer_requested_ms));
|
| | | AddKey(out, cols, "feature_window_sec", IntegerToString(s.feature_window_sec));
|
| | | AddKey(out, cols, "feature_max_age_ms", IntegerToString(s.feature_max_age_ms));
|
| | | AddKey(out, cols, "calibration_id", s.calibration_id);
|
| | | AddKey(out, cols, "seed", IntegerToString(s.seed));
|
| | | AddKey(out, cols, "cleanup_max_attempts", IntegerToString(s.cleanup_max_attempts));
|
| | | AddKey(out, cols, "sample_capacity", IntegerToString(s.sample_capacity));
|
| | | AddKey(out, cols, "event_capacity", IntegerToString(s.event_capacity));
|
| | | AddKey(out, cols, "volume_units_tolerance",
|
| | | DoubleToString(s.volume_units_tolerance, 10));
|
| | | AddKey(out, cols, "run_status", m.run_status);
|
| | | AddKey(out, cols, "stop_reason", m.stop_reason);
|
| | | AddKey(out, cols, "n_main_dispatched", IntegerToString(m.n_main_dispatched));
|
| | | AddKey(out, cols, "n_warmup_dispatched", IntegerToString(m.n_warmup_dispatched));
|
| | | AddKey(out, cols, "n_cleanup_dispatched", IntegerToString(m.n_cleanup_dispatched));
|
| | | AddKey(out, cols, "n_dispatch_uncertain", IntegerToString(m.n_dispatch_uncertain));
|
| | | AddKey(out, cols, "source_commit", m.source_commit);
|
| | | AddKey(out, cols, "source_dirty", (m.source_dirty ? "1" : "0"));
|
| | | AddKey(out, cols, "source_package_sha256", m.source_package_sha256);
|
| | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildDeals(const CRequestTracker *tracker, const string session_id)
|
| | | {
|
| | | const string header[] =
|
| | | {
|
| | | "session_id", "account_alias", "deal_ticket", "order_ticket", "owner_sequence",
|
| | | "symbol", "deal_type", "entry", "reason", "magic", "position_ticket",
|
| | | "position_identifier", "callback_first_us", "callback_event_id", "has_callback",
|
| | | "callback_volume", "history_volume", "history_price", "deal_time_msc",
|
| | | "history_read_event_id", "recovered", "correction_count", "deleted", "quality_codes"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[24];
|
| | | const int n = tracker.DealCount();
|
| | | for(int i = 0; i < n; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | DealRecord d;
|
| | | if(!tracker.GetDeal(i, d))
|
| | | continue;
|
| | | cols[0] = session_id;
|
| | | cols[1] = AccountInfoString(ACCOUNT_NAME);
|
| | | cols[2] = IntegerToString(d.deal_ticket);
|
| | | cols[3] = IntegerToString(d.order_ticket);
|
| | | cols[4] = IntegerToString(d.owner_sequence);
|
| | | cols[5] = d.symbol;
|
| | | cols[6] = IntegerToString(d.deal_type);
|
| | | cols[7] = IntegerToString(d.entry);
|
| | | cols[8] = IntegerToString(d.reason);
|
| | | cols[9] = IntegerToString(d.magic);
|
| | | cols[10] = IntegerToString(d.position_ticket);
|
| | | cols[11] = IntegerToString(d.position_identifier);
|
| | | cols[12] = IntegerToString(d.callback_first_us);
|
| | | cols[13] = IntegerToString(d.callback_event_id);
|
| | | cols[14] = (d.has_callback ? "1" : "0");
|
| | | cols[15] = DoubleToString(d.callback_volume, 10);
|
| | | cols[16] = DoubleToString(d.history_volume, 10);
|
| | | cols[17] = DoubleToString(d.history_price, 10);
|
| | | cols[18] = IntegerToString(d.deal_time_msc);
|
| | | cols[19] = IntegerToString(d.history_read_event_id);
|
| | | cols[20] = (d.recovered ? "1" : "0");
|
| | | cols[21] = IntegerToString(d.correction_count);
|
| | | cols[22] = (d.deleted ? "1" : "0");
|
| | | cols[23] = (d.deleted ? "DELETED" : "");
|
| | | out += CCsv::BuildRow(cols, 24);
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | long CCsvReport::MetricValue(const RequestMetadata &r, const int metric, bool &ok)
|
| | | {
|
| | | ok = false;
|
| | | if(r.metrics_has[metric])
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | ok = true;
|
| | | return(r.metrics_value[metric]);
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const uint pm = r.present_mask;
|
| | | LatencySample s = r.sample;
|
| | | switch(metric)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | case LAB_METRIC_CALL_DURATION:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T1) != 0)
|
| | | { ok = true; return(SignedDelta(s.send_return_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_REQUEST_DELAY:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T2) != 0)
|
| | | { ok = true; return(SignedDelta(s.request_event_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_ORDER_DELAY:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T3) != 0)
|
| | | { ok = true; return(SignedDelta(s.order_event_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_FIRST_DEAL_DELAY:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T4) != 0)
|
| | | { ok = true; return(SignedDelta(s.first_deal_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_LAST_DEAL_DELAY:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T5) != 0)
|
| | | { ok = true; return(SignedDelta(r.last_deal_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_FINAL_STATE_DELAY:
|
| | | if((pm & (uint)LAB_MASK_T0) != 0 && (pm & (uint)LAB_MASK_T6) != 0)
|
| | | { ok = true; return(SignedDelta(s.final_event_us, s.send_start_us)); }
|
| | | break;
|
| | | case LAB_METRIC_REMAINING_AFTER_RETURN:
|
| | | if((pm & (uint)LAB_MASK_T1) != 0 && (pm & (uint)LAB_MASK_T6) != 0)
|
| | | { ok = true; return(SignedDelta(s.final_event_us, s.send_return_us)); }
|
| | | break;
|
| | | case LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET:
|
| | | if((pm & (uint)LAB_MASK_T2) != 0 && (pm & (uint)LAB_MASK_T4) != 0)
|
| | | { ok = true; return(SignedDelta(s.request_event_us, s.first_deal_us)); }
|
| | | break;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(0);
|
| | | }
|
| | | string CCsvReport::BuildSummary(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings)
|
| | | {
|
| | | const string header[] =
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "experiment_id", "condition_id", "series_id", "metric_id", "outcome_group",
|
| | | "population", "observation_unit", "segment_key", "source_set_id", "unit",
|
| | | "n_attempted", "n_applicable", "n_not_applicable", "n_applicability_unknown",
|
| | | "n_valid", "n_missing", "n_late", "n_conflict", "n_interrupted", "n_used",
|
| | | "minimum", "mean", "median", "p90", "p95", "p99", "p999", "maximum", "stddev",
|
| | | "percentile_method", "method_fingerprint",
|
| | | "p95_tail_expected_n", "p99_tail_expected_n", "p999_tail_expected_n",
|
| | | "tail_warning_codes", "n_deadline_exceeded", "deadline_denominator",
|
| | | "deadline_rate", "n_timestamp_missing_at_deadline", "missing_timestamp_rate",
|
| | | "conditional_distribution"
|
| | | };
|
| | | const string groups[3] = {"ALL", "SUCCESS", "REJECTED"}; // R5-S5
|
| | | string out = CCsv::Header(header);
|
| | | string cols[41];
|
| | | string conds[16];
|
| | | int cond_count = 0;
|
| | | //--- R4-B8: CLEANUP и warmup исключаются из summary (только MAIN-наблюдения)
|
| | | const int n = tracker.Count();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP)
|
| | | continue;
|
| | | bool known = false;
|
| | | for(int c = 0; c < cond_count; c++)
|
| | | if(conds[c] == r.plan.condition_id)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | known = true;
|
| | | break;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!known && cond_count < 16)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | conds[cond_count] = r.plan.condition_id;
|
| | | cond_count++;
|
2026-09-10 16:43:28 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R4-B8 + R5-S5: группы исходов анализируются отдельно
|
| | | //--- (ALL, SUCCESS=успешные исходы, REJECTED); статистика считается
|
| | | //--- по фактическим VALID значениям единой метрики.
|
| | | for(int g = 0; g < 3; g++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string grp = groups[g];
|
| | | for(int ccond = 0; ccond < cond_count + 1; ccond++)
|
2026-09-12 22:14:24 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- последний проход (ccond==cond_count) — combined строка A+B
|
| | | const string cond = (ccond < cond_count ? conds[ccond] : "");
|
2026-09-12 22:14:24 +03:00 | | | for(int metric = 0; metric < 8; metric++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | double values[];
|
| | | int vc = 0;
|
| | | if(ArrayResize(values, 256) != 256)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | ulong n_attempted = 0, n_valid = 0, n_missing = 0, n_late = 0, n_conflict = 0,
|
| | | n_interrupted = 0, n_not_applicable = 0, n_applicability_unknown = 0,
|
| | | n_applicable = 0, n_deadline = 0, n_deadline_denom = 0, n_t6missing = 0;
|
2026-09-12 22:14:24 +03:00 | | | for(int i = 0; i < n; i++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | RequestMetadata r;
|
2026-09-12 22:14:24 +03:00 | | | if(!tracker.GetByIndex(i, r) || r.is_warmup ||
|
| | | r.plan.role == LAB_ROLE_CLEANUP)
|
2026-09-10 16:43:28 +03:00 | | | continue;
|
2026-09-12 22:14:24 +03:00 | | | if(StringLen(cond) > 0 && r.plan.condition_id != cond)
|
2026-09-10 16:43:28 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | //--- R4-B8/R5-S5: отказы — отдельная группа; SUCCESS —
|
| | | //--- базовые успешные исходы (не REJECTED/UNKNOWN)
|
| | | if(grp == "REJECTED" && r.outcome != LAB_OUT_REJECTED)
|
| | | continue;
|
| | | if(grp == "SUCCESS" &&
|
| | | (r.outcome == LAB_OUT_REJECTED || r.outcome == LAB_OUT_UNKNOWN))
|
| | | continue;
|
| | | n_attempted++;
|
| | | if(r.observation_deadline_us > 0)
|
| | | n_deadline_denom++;
|
| | | if(r.deadline_exceeded)
|
| | | {
|
| | | n_deadline++;
|
| | | if((r.present_mask & (uint)LAB_MASK_T6) == 0)
|
| | | n_t6missing++;
|
| | | }
|
| | | //--- единая семантика: EvaluateMetric (как в offline rebuild)
|
2026-09-10 22:54:26 +03:00 | | | MetricObservation mo;
|
2026-09-12 22:14:24 +03:00 | | | bool ev = false;
|
| | | CRequestTracker::EvaluateMetric(r, (ENUM_LAB_METRIC)metric, mo, ev);
|
2026-09-16 15:37:55 +03:00 | | | if(!ev)
|
| | | {
|
| | | n_applicability_unknown++;
|
2026-09-10 16:43:28 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | if(mo.applicability == LAB_APP_NOT_APPLICABLE)
|
| | | {
|
| | | n_not_applicable++;
|
| | | continue;
|
| | | }
|
| | | if(mo.applicability == LAB_APP_UNKNOWN)
|
| | | {
|
| | | n_applicability_unknown++;
|
| | | continue;
|
| | | }
|
| | | n_applicable++;
|
| | | //--- R7-S1: CONFLICT/INTERRUPTED учитываются ДО проверки
|
| | | //--- has_value: конфликт порядка меток без числа не должен
|
| | | //--- засчитываться как MISSING (потеря callback).
|
| | | const ENUM_LAB_FITNESS cat = mo.category;
|
| | | if(cat == LAB_FIT_CONFLICT)
|
| | | {
|
| | | n_conflict++;
|
| | | continue;
|
| | | }
|
| | | if(cat == LAB_FIT_INTERRUPTED)
|
| | | {
|
| | | n_interrupted++;
|
| | | continue;
|
| | | }
|
| | | if(!mo.has_value)
|
| | | {
|
| | | n_missing++;
|
| | | continue;
|
| | | }
|
| | | if(cat == LAB_FIT_VALID)
|
| | | {
|
| | | if(vc >= ArraySize(values) &&
|
| | | ArrayResize(values, vc + 256) != vc + 256)
|
| | | break;
|
| | | values[vc] = (double)mo.value_us;
|
| | | vc++;
|
| | | n_valid++;
|
| | | }
|
| | | else
|
| | | if(cat == LAB_FIT_LATE)
|
| | | n_late++;
|
| | | else
|
| | | if(cat == LAB_FIT_MISSING)
|
| | | n_missing++;
|
| | | else
|
| | | n_not_applicable++;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | StatSummaryRow row;
|
| | | row.Zero();
|
| | | //--- R5-S5: n=1 допустим (min/mean/median/квантили есть,
|
| | | //--- stddev остаётся пустым — выборочное n-1 неприменимо).
|
| | | if(vc >= 1)
|
| | | CStatistics::Compute(values, vc, row);
|
| | | row.experiment_id = LabExperimentName(settings.experiment_id);
|
| | | row.condition_id = cond;
|
| | | row.series_id = IntegerToString(settings.series_id);
|
| | | row.metric_id = LabMetricName((ENUM_LAB_METRIC)metric);
|
| | | row.source_set_id = session_id;
|
| | | row.unit = "us";
|
| | | row.n_attempted = n_attempted;
|
| | | row.n_applicable = n_applicable;
|
| | | row.n_not_applicable = n_not_applicable;
|
| | | row.n_applicability_unknown = n_applicability_unknown;
|
| | | row.n_valid = n_valid;
|
| | | row.n_missing = n_missing;
|
| | | row.n_late = n_late;
|
| | | row.n_conflict = n_conflict;
|
| | | row.n_interrupted = n_interrupted;
|
| | | row.n_deadline_exceeded = n_deadline;
|
| | | row.deadline_denominator = n_deadline_denom;
|
| | | row.deadline_rate = (n_deadline_denom > 0 ? (double)n_deadline / n_deadline_denom : 0.0);
|
| | | row.n_timestamp_missing_at_deadline = n_t6missing;
|
| | | row.missing_timestamp_rate = (n_deadline_denom > 0 ? (double)n_t6missing / n_deadline_denom : 0.0);
|
| | | cols[0] = row.experiment_id;
|
| | | cols[1] = row.condition_id;
|
| | | cols[2] = row.series_id;
|
| | | cols[3] = row.metric_id;
|
| | | cols[4] = grp;
|
| | | cols[5] = "PRIMARY";
|
| | | cols[6] = "REQUEST";
|
| | | cols[7] = "{}";
|
| | | cols[8] = row.source_set_id;
|
| | | cols[9] = row.unit;
|
| | | cols[10] = IntegerToString(row.n_attempted);
|
| | | cols[11] = IntegerToString(row.n_applicable);
|
| | | cols[12] = IntegerToString(row.n_not_applicable);
|
| | | cols[13] = IntegerToString(row.n_applicability_unknown);
|
| | | cols[14] = IntegerToString(row.n_valid);
|
| | | cols[15] = IntegerToString(row.n_missing);
|
| | | cols[16] = IntegerToString(row.n_late);
|
| | | cols[17] = IntegerToString(row.n_conflict);
|
| | | cols[18] = IntegerToString(row.n_interrupted);
|
| | | cols[19] = IntegerToString(row.n_used);
|
| | | const bool has_stats = row.has_stats;
|
| | | cols[20] = (has_stats ? DoubleToString(row.minimum, 10) : "");
|
| | | cols[21] = (has_stats ? DoubleToString(row.mean, 10) : "");
|
| | | cols[22] = (has_stats ? DoubleToString(row.median, 10) : "");
|
| | | cols[23] = (has_stats ? DoubleToString(row.p90, 10) : "");
|
| | | cols[24] = (has_stats ? DoubleToString(row.p95, 10) : "");
|
| | | cols[25] = (has_stats ? DoubleToString(row.p99, 10) : "");
|
| | | cols[26] = (has_stats ? DoubleToString(row.p999, 10) : "");
|
| | | cols[27] = (has_stats ? DoubleToString(row.maximum, 10) : "");
|
| | | cols[28] = (has_stats && row.n_used >= 2 ? DoubleToString(row.stddev, 10) : "");
|
| | | cols[29] = row.percentile_method;
|
| | | cols[30] = row.percentile_method + "_V1";
|
| | | cols[31] = DoubleToString(row.p95_tail_expected_n, 10);
|
| | | cols[32] = DoubleToString(row.p99_tail_expected_n, 10);
|
| | | cols[33] = DoubleToString(row.p999_tail_expected_n, 10);
|
| | | cols[34] = row.tail_warning_codes;
|
| | | cols[35] = IntegerToString(row.n_deadline_exceeded);
|
| | | cols[36] = IntegerToString(row.deadline_denominator);
|
| | | cols[37] = DoubleToString(row.deadline_rate, 12);
|
| | | cols[38] = IntegerToString(row.n_timestamp_missing_at_deadline);
|
| | | cols[39] = DoubleToString(row.missing_timestamp_rate, 12);
|
| | | cols[40] = (row.conditional_distribution ? "1" : "0");
|
| | | out += CCsv::BuildRow(cols, 41);
|
2026-09-10 16:43:28 +03:00 | | | }
|
| | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildHistogram(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings)
|
| | | {
|
| | | const string header[] =
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "experiment_id", "condition_id", "series_id", "metric_id", "outcome_group",
|
| | | "population", "segment_key", "bin_index", "lower_us", "upper_us", "count",
|
| | | "denominator", "share"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[13];
|
| | | const ulong lower[10] = {0, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000};
|
| | | const ulong upper[10] = {1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000, 0};
|
| | | string conds[16];
|
| | | int cond_count = 0;
|
| | | const int n = tracker.Count();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP)
|
| | | continue;
|
| | | bool known = false;
|
| | | for(int c = 0; c < cond_count; c++)
|
| | | if(conds[c] == r.plan.condition_id)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | known = true;
|
| | | break;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!known && cond_count < 16)
|
| | | {
|
| | | conds[cond_count] = r.plan.condition_id;
|
| | | cond_count++;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | for(int c = 0; c < cond_count + 1; c++)
|
| | | {
|
| | | const string cond = (c < cond_count ? conds[c] : "");
|
| | | for(int metric = 0; metric < 8; metric++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- R4-B8: знаковая метрика T2-T4 не попадает в latency-гистограмму
|
| | | //--- (отдельный offset_signs.csv с NEGATIVE/ZERO/POSITIVE)
|
| | | if(metric == (int)LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET)
|
| | | continue;
|
| | | long values[];
|
| | | int vc = 0;
|
2026-09-12 22:14:24 +03:00 | | | for(int i = 0; i < n; i++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
| | | RequestMetadata r;
|
2026-09-12 22:14:24 +03:00 | | | if(!tracker.GetByIndex(i, r) || r.is_warmup ||
|
| | | r.plan.role == LAB_ROLE_CLEANUP)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
2026-09-12 22:14:24 +03:00 | | | if(StringLen(cond) > 0 && r.plan.condition_id != cond)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
| | | MetricObservation mo;
|
2026-09-12 22:14:24 +03:00 | | | bool ev = false;
|
2026-09-16 15:37:55 +03:00 | | | CRequestTracker::EvaluateMetric(r, (ENUM_LAB_METRIC)metric, mo, ev);
|
2026-09-12 22:14:24 +03:00 | | | if(!ev || mo.applicability != LAB_APP_APPLICABLE ||
|
| | | !mo.has_value || mo.category != LAB_FIT_VALID)
|
2026-09-10 22:54:26 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | const int cap = ArraySize(values);
|
| | | if(vc >= cap && ArrayResize(values, vc + 256) != vc + 256)
|
| | | break;
|
| | | values[vc] = mo.value_us;
|
| | | vc++;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | ulong bins[10];
|
| | | ArrayInitialize(bins, 0);
|
| | | const int denominator = vc;
|
| | | if(vc > 0)
|
| | | HistogramCount(values, vc, bins);
|
| | | for(int b = 0; b < 10; b++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | cols[0] = LabExperimentName(settings.experiment_id);
|
| | | cols[1] = cond;
|
| | | cols[2] = IntegerToString(settings.series_id);
|
2026-09-16 15:37:55 +03:00 | | | cols[3] = LabMetricName((ENUM_LAB_METRIC)metric);
|
| | | cols[4] = "ALL";
|
| | | cols[5] = "PRIMARY";
|
| | | cols[6] = "{}";
|
| | | cols[7] = IntegerToString(b);
|
| | | cols[8] = IntegerToString(lower[b]);
|
| | | cols[9] = (upper[b] == 0 ? "" : IntegerToString(upper[b]));
|
| | | cols[10] = IntegerToString(bins[b]);
|
| | | cols[11] = IntegerToString(denominator);
|
| | | cols[12] = (denominator > 0 ? DoubleToString((double)bins[b] / denominator, 12) : "0");
|
| | | out += CCsv::BuildRow(cols, 13);
|
2026-09-10 22:54:26 +03:00 | | | }
|
| | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildOffsetSigns(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings)
|
| | | {
|
| | | const string header[] =
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "experiment_id", "condition_id", "series_id", "metric_id", "sign",
|
| | | "low_us", "high_us", "count", "denominator", "share"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[10];
|
| | | string conds[16];
|
| | | int cond_count = 0;
|
| | | const int n = tracker.Count();
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP)
|
| | | continue;
|
| | | bool known = false;
|
| | | for(int c = 0; c < cond_count; c++)
|
| | | if(conds[c] == r.plan.condition_id)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | known = true;
|
| | | break;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(!known && cond_count < 16)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | conds[cond_count] = r.plan.condition_id;
|
| | | cond_count++;
|
2026-09-10 16:43:28 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | for(int c = 0; c < cond_count + 1; c++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string cond = (c < cond_count ? conds[c] : "");
|
| | | const ENUM_LAB_METRIC metric = LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET;
|
| | | long neg = 0, zero = 0, pos = 0;
|
2026-09-12 22:14:24 +03:00 | | | for(int i = 0; i < n; i++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r) || r.is_warmup ||
|
| | | r.plan.role == LAB_ROLE_CLEANUP)
|
2026-09-10 16:43:28 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | if(StringLen(cond) > 0 && r.plan.condition_id != cond)
|
| | | continue;
|
| | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | CRequestTracker::EvaluateMetric(r, metric, mo, ev);
|
| | | if(!ev || mo.applicability != LAB_APP_APPLICABLE ||
|
| | | !mo.has_value || mo.category != LAB_FIT_VALID)
|
| | | continue;
|
| | | if(mo.value_us < 0)
|
| | | neg++;
|
| | | else
|
| | | if(mo.value_us == 0)
|
| | | zero++;
|
| | | else
|
| | | pos++;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | const long total = neg + zero + pos;
|
| | | const string signs[3] = {"NEGATIVE", "ZERO", "POSITIVE"};
|
| | | const long counts[3] = {neg, zero, pos};
|
| | | for(int k = 0; k < 3; k++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | cols[0] = LabExperimentName(settings.experiment_id);
|
| | | cols[1] = cond;
|
2026-09-12 22:14:24 +03:00 | | | cols[2] = IntegerToString(settings.series_id);
|
2026-09-16 15:37:55 +03:00 | | | cols[3] = LabMetricName(metric);
|
| | | cols[4] = signs[k];
|
| | | cols[5] = (k == 0 ? "" : (k == 1 ? "0" : "1"));
|
| | | cols[6] = (k == 0 ? "-1" : (k == 1 ? "0" : "")); // условные границы знака
|
| | | cols[7] = IntegerToString(counts[k]);
|
| | | cols[8] = IntegerToString(total);
|
| | | cols[9] = (total > 0 ? DoubleToString((double)counts[k] / total, 12) : "0");
|
2026-09-12 22:14:24 +03:00 | | | out += CCsv::BuildRow(cols, 10);
|
2026-09-10 16:43:28 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildComparisons(const CRequestTracker *tracker, const string session_id,
|
| | | const LabSettings &settings)
|
| | | {
|
| | | //--- R7-S2: ТЗ §7, стр.419 — delta = B−A и относительное изменение
|
| | | //--- (B−A)/A для mean/median/p95 по паре серий и объединению.
|
| | | const string header[] =
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | "experiment_id", "condition_pair", "metric_id", "unit", "outcome_group",
|
| | | "a_n", "a_mean", "a_median", "a_p95", "b_n", "b_mean", "b_median", "b_p95",
|
| | | "delta_mean", "delta_median", "delta_p95", "rel_mean", "rel_median", "rel_p95"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[19];
|
| | | for(int metric = 0; metric < 8; metric++)
|
| | | {
|
| | | double va[], vb[];
|
| | | int na = 0, nb = 0;
|
| | | if(ArrayResize(va, 256) != 256 || ArrayResize(vb, 256) != 256)
|
| | | continue;
|
| | | const int n = tracker.Count();
|
| | | for(int i = 0; i < n; i++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | RequestMetadata r;
|
2026-09-16 15:37:55 +03:00 | | | if(!tracker.GetByIndex(i, r) || r.is_warmup ||
|
| | | r.plan.role == LAB_ROLE_CLEANUP)
|
2026-09-10 16:43:28 +03:00 | | | continue;
|
2026-09-16 15:37:55 +03:00 | | | MetricObservation mo;
|
| | | bool ev = false;
|
| | | CRequestTracker::EvaluateMetric(r, (ENUM_LAB_METRIC)metric, mo, ev);
|
| | | if(!ev || mo.applicability != LAB_APP_APPLICABLE || !mo.has_value ||
|
| | | mo.category != LAB_FIT_VALID)
|
| | | continue;
|
| | | if(r.plan.condition_id == "A")
|
2026-09-10 19:04:10 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(na >= ArraySize(va) && ArrayResize(va, na + 256) != na + 256)
|
| | | continue;
|
| | | va[na] = (double)mo.value_us;
|
| | | na++;
|
2026-09-10 19:04:10 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
| | | if(r.plan.condition_id == "B")
|
| | | {
|
| | | if(nb >= ArraySize(vb) && ArrayResize(vb, nb + 256) != nb + 256)
|
| | | continue;
|
| | | vb[nb] = (double)mo.value_us;
|
| | | nb++;
|
| | | }
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | double ma = 0, pa = 0, mb = 0, pb = 0, ea = 0, eb = 0;
|
| | | const bool ha = (na >= 1 && LabAvgValue(va, na, ea) &&
|
| | | LabPercentile(va, na, 50.0, ma) && LabPercentile(va, na, 95.0, pa));
|
| | | const bool hb = (nb >= 1 && LabAvgValue(vb, nb, eb) &&
|
| | | LabPercentile(vb, nb, 50.0, mb) && LabPercentile(vb, nb, 95.0, pb));
|
| | | cols[0] = LabExperimentName(settings.experiment_id);
|
| | | cols[1] = "A_vs_B";
|
| | | cols[2] = LabMetricName((ENUM_LAB_METRIC)metric);
|
| | | cols[3] = "us";
|
| | | cols[4] = "ALL";
|
| | | cols[5] = IntegerToString(na);
|
| | | cols[6] = (ha ? DoubleToString(ea, 10) : "");
|
| | | cols[7] = (ha ? DoubleToString(ma, 10) : "");
|
| | | cols[8] = (ha ? DoubleToString(pa, 10) : "");
|
| | | cols[9] = IntegerToString(nb);
|
| | | cols[10] = (hb ? DoubleToString(eb, 10) : "");
|
| | | cols[11] = (hb ? DoubleToString(mb, 10) : "");
|
| | | cols[12] = (hb ? DoubleToString(pb, 10) : "");
|
| | | //--- delta = B−A; rel = (B−A)/A
|
| | | cols[13] = (ha && hb ? DoubleToString(eb - ea, 10) : "");
|
| | | cols[14] = (ha && hb ? DoubleToString(mb - ma, 10) : "");
|
| | | cols[15] = (ha && hb ? DoubleToString(pb - pa, 10) : "");
|
| | | cols[16] = (ha && hb && MathAbs(ea) > 1e-12 ? DoubleToString((eb - ea) / ea, 10) : "");
|
| | | cols[17] = (ha && hb && MathAbs(ma) > 1e-12 ? DoubleToString((mb - ma) / ma, 10) : "");
|
| | | cols[18] = (ha && hb && MathAbs(pa) > 1e-12 ? DoubleToString((pb - pa) / pa, 10) : "");
|
| | | out += CCsv::BuildRow(cols, 19);
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildMarketWindows(const MarketWindow &wins[], const int count,
|
| | | const string session_id)
|
| | | {
|
| | | const string header[] =
|
| | | {
|
| | | "session_id", "dataset_id", "window_id", "sequence", "calibration_id",
|
| | | "symbol", "from_msc", "to_msc", "anchor_tick_msc", "n_returned", "n_valid_info",
|
| | | "quote_frequency_hz", "mid_range_ticks", "regime", "valid", "quality_codes",
|
| | | "ticks_checksum"
|
| | | }; // R8-S2
|
| | | string out = CCsv::Header(header);
|
| | | string cols[17];
|
| | | for(int i = 0; i < count; i++)
|
| | | {
|
| | | const MarketWindow w = wins[i];
|
| | | cols[0] = session_id;
|
| | | cols[1] = w.dataset_id;
|
| | | cols[2] = w.window_id;
|
| | | cols[3] = IntegerToString(w.sequence);
|
| | | cols[4] = w.calibration_id;
|
| | | cols[5] = w.symbol;
|
| | | cols[6] = IntegerToString(w.from_msc);
|
| | | cols[7] = IntegerToString(w.to_msc);
|
| | | cols[8] = IntegerToString(w.anchor_tick_msc);
|
| | | cols[9] = IntegerToString(w.n_returned);
|
| | | cols[10] = IntegerToString(w.n_valid_info);
|
| | | cols[11] = DoubleToString(w.quote_frequency_hz, 12);
|
| | | cols[12] = DoubleToString(w.mid_range_ticks, 12);
|
| | | cols[13] = LabRegimeName(w.regime);
|
| | | cols[14] = (w.valid ? "1" : "0");
|
| | | cols[15] = w.quality_codes;
|
| | | cols[16] = (w.ticks_checksum != 0 ? IntegerToString(w.ticks_checksum) : ""); // R8-S2
|
| | | out += CCsv::BuildRow(cols, 17);
|
| | | }
|
| | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildExperimentManifest(const CExperimentRunner *runner,
|
| | | const string session_id, const LabSettings &settings)
|
| | | {
|
| | | const string header[] =
|
| | | {
|
| | | "session_id", "experiment_id", "series_id", "condition_id", "block_id",
|
| | | "role", "operation", "planned_count", "dispatched_count", "reuse"
|
| | | };
|
| | | string out = CCsv::Header(header);
|
| | | string cols[10];
|
| | | string key_[64];
|
| | | string role_[64];
|
| | | string op_[64];
|
| | | ulong plan_[64];
|
| | | ulong disp_[64];
|
| | | int nk = 0;
|
| | | const int n = runner.SlotCount();
|
| | | for(int i = 0; i < n; i++)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | ScheduleSlot s;
|
| | | if(!runner.GetSlot(i, s))
|
| | | continue;
|
| | | const string key = StringFormat("%s|%s|%s", s.plan.condition_id,
|
| | | LabRoleName(s.plan.role),
|
| | | LabOperationName(s.plan.operation));
|
| | | int idx = -1;
|
| | | for(int k = 0; k < nk; k++)
|
| | | if(key_[k] == key)
|
| | | {
|
| | | idx = k;
|
| | | break;
|
| | | }
|
| | | if(idx < 0 && nk < 64)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | idx = nk;
|
| | | nk++;
|
| | | key_[idx] = key;
|
| | | role_[idx] = LabRoleName(s.plan.role);
|
| | | op_[idx] = LabOperationName(s.plan.operation);
|
| | | plan_[idx] = 0;
|
| | | disp_[idx] = 0;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | if(idx >= 0)
|
2026-09-10 16:43:28 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | plan_[idx]++;
|
| | | if(s.status == LAB_SLOT_DISPATCHED)
|
| | | disp_[idx]++;
|
| | | }
|
| | | }
|
| | | for(int k = 0; k < nk; k++)
|
| | | {
|
| | | string part[];
|
| | | const int parts = StringSplit(key_[k], '|', part);
|
| | | cols[0] = session_id;
|
| | | cols[1] = LabExperimentName(settings.experiment_id);
|
| | | cols[2] = IntegerToString(settings.series_id);
|
| | | cols[3] = (parts >= 1 ? part[0] : "");
|
| | | cols[4] = "0";
|
| | | cols[5] = role_[k];
|
| | | cols[6] = op_[k];
|
| | | cols[7] = IntegerToString(plan_[k]);
|
| | | cols[8] = IntegerToString(disp_[k]);
|
| | | //--- B8: для E4 паспорт помечает reuse=1 (анализ серий E1 без повторов)
|
| | | cols[9] = (settings.experiment_id == LAB_EXP_E4 ? "1" : "0");
|
| | | out += CCsv::BuildRow(cols, 10);
|
| | | }
|
| | | return(out);
|
| | | }
|
| | | string CCsvReport::BuildRunChecks(const CRequestTracker *tracker,
|
| | | const CEventJournal *journal, const CExperimentRunner *runner,
|
| | | const LabSettings &settings)
|
| | | {
|
| | | CheckRecord rec[10];
|
| | | int rc = 0;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-DATA-01";
|
| | | rec[rc].case_key = "journal-nonempty";
|
| | | rec[rc].status = (journal.Count() > 0 ? LAB_CHECK_PASS : LAB_CHECK_FAIL);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "journal events recorded";
|
| | | rec[rc].actual = IntegerToString(journal.Count());
|
| | | rec[rc].source_ref = "RequestLatencyLab.mq5";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-DATA-02";
|
| | | rec[rc].case_key = "samples-nonempty";
|
| | | rec[rc].status = (tracker.Count() > 0 ? LAB_CHECK_PASS : LAB_CHECK_FAIL);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "tracker records recorded";
|
| | | rec[rc].actual = IntegerToString(tracker.Count());
|
| | | rec[rc].source_ref = "RequestLatencyLab.mq5";
|
| | | rc++;
|
| | | int planned = 0;
|
| | | int dispatched = 0;
|
| | | for(int i = 0; i < runner.SlotCount(); i++)
|
| | | {
|
| | | ScheduleSlot s;
|
| | | if(!runner.GetSlot(i, s))
|
| | | continue;
|
| | | planned++;
|
| | | if(s.status == LAB_SLOT_DISPATCHED)
|
| | | dispatched++;
|
| | | }
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-SCHED-01";
|
| | | rec[rc].case_key = "main-slots-dispatched";
|
| | | rec[rc].status = (dispatched >= planned ? LAB_CHECK_PASS : LAB_CHECK_NOT_APPLICABLE);
|
| | | rec[rc].severity = LAB_SEV_INFO;
|
| | | rec[rc].expected = "all planned slots dispatched";
|
| | | rec[rc].actual = StringFormat("%d/%d", dispatched, planned);
|
| | | rec[rc].source_ref = "ExperimentRunner.mqh";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-CORR-01";
|
| | | rec[rc].case_key = "no-unresolved";
|
| | | rec[rc].status = (tracker.UnresolvedCount() == 0 ? LAB_CHECK_PASS : LAB_CHECK_FAIL);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "0 unresolved requests";
|
| | | rec[rc].actual = IntegerToString(tracker.UnresolvedCount());
|
| | | rec[rc].source_ref = "RequestTracker.mqh";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-DEAL-01";
|
| | | rec[rc].case_key = "deals-recorded";
|
| | | rec[rc].status = (tracker.DealCount() > 0 ? LAB_CHECK_PASS : LAB_CHECK_NOT_APPLICABLE);
|
| | | rec[rc].severity = LAB_SEV_INFO;
|
| | | rec[rc].expected = "deals recorded";
|
| | | rec[rc].actual = IntegerToString(tracker.DealCount());
|
| | | rec[rc].source_ref = "RequestTracker.mqh";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-DEAL-02";
|
| | | rec[rc].case_key = "no-silent-deal-registry-overflow";
|
| | | rec[rc].status = (tracker.DealLostCount() == 0 ? LAB_CHECK_PASS : LAB_CHECK_FAIL);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "deal registry without silent overflow (R6-S4)";
|
| | | rec[rc].actual = IntegerToString(tracker.DealLostCount());
|
| | | rec[rc].source_ref = "RequestTracker.mqh";
|
| | | rc++;
|
| | | ulong with_t0 = 0;
|
| | | ulong completed = 0;
|
| | | ulong completed_with_t6 = 0;
|
| | | for(int i = 0; i < tracker.Count(); i++)
|
| | | {
|
| | | RequestMetadata r;
|
| | | if(!tracker.GetByIndex(i, r))
|
| | | continue;
|
| | | if((r.present_mask & (uint)LAB_MASK_T0) != 0)
|
| | | with_t0++;
|
| | | if(r.sample.completed)
|
| | | {
|
| | | completed++;
|
| | | if((r.present_mask & (uint)LAB_MASK_T6) != 0)
|
| | | completed_with_t6++;
|
| | | }
|
| | | }
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-STAT-01";
|
| | | rec[rc].case_key = "t0-present";
|
| | | rec[rc].status = (with_t0 > 0 ? LAB_CHECK_PASS : LAB_CHECK_NOT_APPLICABLE);
|
| | | rec[rc].severity = LAB_SEV_INFO;
|
| | | rec[rc].expected = "records with T0";
|
| | | rec[rc].actual = IntegerToString(with_t0);
|
| | | rec[rc].source_ref = "StateReader.mqh";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-T6-01";
|
| | | rec[rc].case_key = "completed-have-t6";
|
| | | rec[rc].status = ((completed == 0 || completed == completed_with_t6) ?
|
| | | LAB_CHECK_PASS : LAB_CHECK_FAIL);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "completed=true => T6; incomplete без T6 допустим (ТЗ)";
|
| | | rec[rc].actual = StringFormat("%I64u/%I64u", completed_with_t6, completed);
|
| | | rec[rc].source_ref = "CompletionPolicy.mqh";
|
| | | rc++;
|
| | | rec[rc].Zero();
|
| | | rec[rc].check_id = "RUN-REGIME-01";
|
| | | rec[rc].case_key = "e3-calibration";
|
| | | rec[rc].status = (settings.experiment_id != LAB_EXP_E3 ? LAB_CHECK_NOT_APPLICABLE : LAB_CHECK_NOT_RUN);
|
| | | rec[rc].severity = LAB_SEV_WARNING;
|
| | | rec[rc].expected = "E3 requires calibration file";
|
| | | rec[rc].actual = settings.calibration_id;
|
| | | rec[rc].source_ref = "MarketRegime.mqh";
|
| | | rc++;
|
| | | return(BuildChecks(rec, rc));
|
| | | }
|
| | | bool CCsvReport::LoadCalibration(const string calibration_id, Calibration &cal, LabError &err)
|
| | | {
|
| | | err.Reset();
|
| | | err.component = LAB_COMP_CSV;
|
| | | cal.Zero();
|
| | | if(calibration_id == "")
|
| | | {
|
| | | err.message = "calibration_id empty";
|
| | | return(false);
|
| | | }
|
| | | const string path = StringFormat("%s\\%s.csv", LAB_DATA_ROOT, calibration_id);
|
| | | string content;
|
| | | if(!CCsv::LoadUtf8(path, content, err, true) && !CCsv::LoadUtf8(path, content, err))
|
| | | return(false);
|
| | | string lines[];
|
| | | const int n = StringSplit(content, '\n', lines);
|
| | | for(int i = 0; i < n; i++)
|
| | | {
|
| | | string s = lines[i];
|
| | | StringReplace(s, "\r", "");
|
| | | if(StringLen(s) == 0)
|
| | | continue;
|
| | | string cols[];
|
| | | int c = 0;
|
| | | if(!CCsv::ParseRow(s, cols, c) || c < 2)
|
| | | continue;
|
| | | const string key = cols[0];
|
| | | const string val = cols[1];
|
| | | if(key == "calibration_id")
|
| | | cal.calibration_id = val;
|
| | | else
|
| | | if(key == "symbol")
|
| | | cal.symbol = val;
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "frequency_p50")
|
| | | cal.frequency_p50 = StringToDouble(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "frequency_p90")
|
| | | cal.frequency_p90 = StringToDouble(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "range_p50")
|
| | | cal.range_p50 = StringToDouble(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "range_p90")
|
| | | cal.range_p90 = StringToDouble(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "valid_window_count")
|
| | | cal.valid_window_count = (int)StringToInteger(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "invalid_window_count")
|
| | | cal.invalid_window_count = (int)StringToInteger(val);
|
2026-09-12 22:14:24 +03:00 | | | else
|
2026-09-16 15:37:55 +03:00 | | | if(key == "trading_session_count")
|
| | | cal.trading_session_count = (int)StringToInteger(val);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- S5 (audit-3): калибровка валидна только при совпадении символа,
|
| | | //--- достаточном числе отдельных торговых сессий и окнах >=1000.
|
| | | //--- (невырожденность порогов проверяется в классе MarketRegime)
|
| | | if(cal.calibration_id != "" && cal.valid_window_count >= 1000 &&
|
| | | cal.trading_session_count >= 5)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | cal.valid = true;
|
| | | return(true);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | err.message = "calibration file incomplete: " + calibration_id +
|
| | | (cal.trading_session_count < 5 ? " (sessions<5)" : "");
|
| | | return(false);
|
| | | }
|
| | | void CCsvReport::AddKey(string &out, string &cols[], const string key, const string value)
|
| | | {
|
| | | cols[0] = key;
|
| | | cols[1] = value;
|
| | | cols[2] = "string";
|
| | | cols[3] = "KNOWN";
|
| | | cols[4] = "DERIVED";
|
| | | out += CCsv::BuildRow(cols, 5);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-12 22:14:24 +03:00 | | | #endif // REQUEST_LATENCY_LAB_CSV_STORAGE_MQH
|
| | | //+------------------------------------------------------------------+
|