//+------------------------------------------------------------------+ //| CsvStorage.mqh | //| Copyright 2026, MetaQuotes Ltd. | //| www.mql5.com | //+------------------------------------------------------------------+ #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" #include "..\..\Include\RequestLatencyLab\Statistics.mqh" #define LAB_DATA_ROOT "RequestLatencyLab" class CCsv { public: //--- экранирование ячейки (CSV_SCHEMA: кавычка удваивается) 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\.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++) { const ushort ch = StringGetCharacter(value, i); if(ch == '"') out += "\"\""; else out += ShortToString(ch); } out += "\""; return(out); } string CCsv::BuildRow(const string &cols[], const int count) { string line = ""; for(int i = 0; i < count; i++) { if(i > 0) line += ","; line += Esc(cols[i]); } 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) { ArrayResize(out, 0); return(false); } 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) { err.code = 1; err.severity = LAB_SEV_BLOCKER; err.message = "invalid sandbox path"; return(false); } uchar bytes[]; if(!Utf8Encode(content, bytes)) { err.code = 2; err.severity = LAB_SEV_BLOCKER; err.message = "utf8 encode failed"; return(false); } const int h = FileOpen(rel_path, FILE_WRITE | FILE_BIN | (common ? FILE_COMMON : 0)); if(h == INVALID_HANDLE) { err.code = 3; err.severity = LAB_SEV_BLOCKER; err.message = "cannot open " + rel_path + " err=" + IntegerToString(GetLastError()); return(false); } const int written = (int)FileWriteArray(h, bytes); FileClose(h); if(written != ArraySize(bytes)) { 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) { FileClose(h); return(false); } const uint got = FileReadArray(h, bytes, 0, total); if(got != (uint)total) { FileClose(h); err.code = 6; err.severity = LAB_SEV_BLOCKER; err.message = "short read " + rel_path; return(false); } } else { ArrayResize(bytes, 0); } 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++) { const ushort ch0 = StringGetCharacter(line, i); if(in_quotes) { if(ch0 == '"') { if(i + 1 < len && StringGetCharacter(line, i + 1) == '"') { cur += "\""; i++; } else in_quotes = false; } else 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 = ""; } else cur += ShortToString(ch0); } } 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++) { const ushort ch = StringGetCharacter(content, i); if(ch == '"') { if(in_quotes && i + 1 < len && StringGetCharacter(content, i + 1) == '"') i++; else in_quotes = !in_quotes; continue; } if(!in_quotes && ch == '\n') count++; } 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)) { err.message = "read-back failed: " + err.message; return(false); } //--- 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); } //+------------------------------------------------------------------+ //| CCsvReport:: — реализации. | //+------------------------------------------------------------------+ string CCsvReport::BuildSamples(const CRequestTracker *tracker, const string campaign_id, const string configuration_id) { const string header[] = { "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); } return(out); } string CCsvReport::BuildEvents(const CEventJournal *journal) { const string header[] = { "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); } return(out); } string CCsvReport::BuildSchedule(const CExperimentRunner *runner, const string session_id) { const string header[] = { "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); } return(out); } string CCsvReport::BuildChecks(const CheckRecord &checks[], const int count) { const string header[] = { "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); } 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++) { 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); } return(out); } long CCsvReport::MetricValue(const RequestMetadata &r, const int metric, bool &ok) { ok = false; if(r.metrics_has[metric]) { ok = true; return(r.metrics_value[metric]); } const uint pm = r.present_mask; LatencySample s = r.sample; switch(metric) { 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; } return(0); } string CCsvReport::BuildSummary(const CRequestTracker *tracker, const string session_id, const LabSettings &settings) { const string header[] = { "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) { known = true; break; } if(!known && cond_count < 16) { conds[cond_count] = r.plan.condition_id; cond_count++; } } //--- R4-B8 + R5-S5: группы исходов анализируются отдельно //--- (ALL, SUCCESS=успешные исходы, REJECTED); статистика считается //--- по фактическим VALID значениям единой метрики. for(int g = 0; g < 3; g++) { const string grp = groups[g]; for(int ccond = 0; ccond < cond_count + 1; ccond++) { //--- последний проход (ccond==cond_count) — combined строка A+B const string cond = (ccond < cond_count ? conds[ccond] : ""); for(int metric = 0; metric < 8; metric++) { double values[]; int vc = 0; if(ArrayResize(values, 256) != 256) continue; 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; for(int i = 0; i < n; i++) { RequestMetadata r; if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP) continue; if(StringLen(cond) > 0 && r.plan.condition_id != cond) continue; //--- 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) MetricObservation mo; bool ev = false; CRequestTracker::EvaluateMetric(r, (ENUM_LAB_METRIC)metric, mo, ev); if(!ev) { n_applicability_unknown++; continue; } 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++; } 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); } } } return(out); } string CCsvReport::BuildHistogram(const CRequestTracker *tracker, const string session_id, const LabSettings &settings) { const string header[] = { "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) { known = true; break; } if(!known && cond_count < 16) { conds[cond_count] = r.plan.condition_id; cond_count++; } } for(int c = 0; c < cond_count + 1; c++) { const string cond = (c < cond_count ? conds[c] : ""); for(int metric = 0; metric < 8; metric++) { //--- 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; for(int i = 0; i < n; i++) { RequestMetadata r; if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP) continue; if(StringLen(cond) > 0 && r.plan.condition_id != cond) continue; 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; const int cap = ArraySize(values); if(vc >= cap && ArrayResize(values, vc + 256) != vc + 256) break; values[vc] = mo.value_us; vc++; } ulong bins[10]; ArrayInitialize(bins, 0); const int denominator = vc; if(vc > 0) HistogramCount(values, vc, bins); for(int b = 0; b < 10; b++) { cols[0] = LabExperimentName(settings.experiment_id); cols[1] = cond; cols[2] = IntegerToString(settings.series_id); 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); } } } return(out); } string CCsvReport::BuildOffsetSigns(const CRequestTracker *tracker, const string session_id, const LabSettings &settings) { const string header[] = { "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) { known = true; break; } if(!known && cond_count < 16) { conds[cond_count] = r.plan.condition_id; cond_count++; } } for(int c = 0; c < cond_count + 1; c++) { 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; for(int i = 0; i < n; i++) { RequestMetadata r; if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP) continue; 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++; } 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++) { cols[0] = LabExperimentName(settings.experiment_id); cols[1] = cond; cols[2] = IntegerToString(settings.series_id); 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"); out += CCsv::BuildRow(cols, 10); } } 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[] = { "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++) { RequestMetadata r; if(!tracker.GetByIndex(i, r) || r.is_warmup || r.plan.role == LAB_ROLE_CLEANUP) continue; 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") { if(na >= ArraySize(va) && ArrayResize(va, na + 256) != na + 256) continue; va[na] = (double)mo.value_us; na++; } 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++; } } 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); } 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++) { 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) { idx = nk; nk++; key_[idx] = key; role_[idx] = LabRoleName(s.plan.role); op_[idx] = LabOperationName(s.plan.operation); plan_[idx] = 0; disp_[idx] = 0; } if(idx >= 0) { 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; else if(key == "frequency_p50") cal.frequency_p50 = StringToDouble(val); else if(key == "frequency_p90") cal.frequency_p90 = StringToDouble(val); else if(key == "range_p50") cal.range_p50 = StringToDouble(val); else if(key == "range_p90") cal.range_p90 = StringToDouble(val); else if(key == "valid_window_count") cal.valid_window_count = (int)StringToInteger(val); else if(key == "invalid_window_count") cal.invalid_window_count = (int)StringToInteger(val); else if(key == "trading_session_count") cal.trading_session_count = (int)StringToInteger(val); } //--- S5 (audit-3): калибровка валидна только при совпадении символа, //--- достаточном числе отдельных торговых сессий и окнах >=1000. //--- (невырожденность порогов проверяется в классе MarketRegime) if(cal.calibration_id != "" && cal.valid_window_count >= 1000 && cal.trading_session_count >= 5) { cal.valid = true; return(true); } 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); } #endif // REQUEST_LATENCY_LAB_CSV_STORAGE_MQH //+------------------------------------------------------------------+