1443 righe
Nessun fine linea
60 KiB
MQL5
1443 righe
Nessun fine linea
60 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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\<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++)
|
|
{
|
|
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
|
|
//+------------------------------------------------------------------+ |