Vigiar
1
0
Derivar
Você já tinha feito uma derivação do repositório mql5-execution-microstructure-02-request-latency, anteriormente
0
mql5-execution-microstructu.../Tests/RequestLatencyTests.mq5

3429 linhas
152 KiB
MQL5

//+------------------------------------------------------------------+
//| RequestLatencyTests.mq5 |
//| Copyright 2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, MetaQuotes Ltd."
#property link "https://www.mql5.com"
#property version "1.00"
#property description "Автоматические проверки на синтетических входах"
#property description "с управляемым временем (LOCAL_ONLY, без торговли)."
#property description "Каталог сценариев — RequestLatencyLab_AcceptanceTests_RU.md,"
#property description "результат — checks.csv в Files\\RequestLatencyLab\\tests\\."
#include "..\Include\RequestLatencyLab\Models.mqh"
#include "..\Include\RequestLatencyLab\Legacy\BenchmarkStatistics.mqh"
#include "..\Include\RequestLatencyLab\Statistics.mqh"
#include "..\Include\RequestLatencyLab\CsvStorage.mqh"
#include "..\Include\RequestLatencyLab\Configuration.mqh"
#include "..\Include\RequestLatencyLab\TimeSource.mqh"
#include "..\Include\RequestLatencyLab\StateReader.mqh"
#include "..\Include\RequestLatencyLab\MarketRegime.mqh"
#include "..\Include\RequestLatencyLab\ExperimentRunner.mqh"
#include "..\Include\RequestLatencyLab\RequestTracker.mqh"
#include "..\Include\RequestLatencyLab\ReportBuilder.mqh"
#include "..\Include\RequestLatencyLab\EventJournal.mqh"
#include "..\Include\RequestLatencyLab\AppController.mqh"
#define TESTS_CAPACITY 224
CheckRecord ExtChecks[TESTS_CAPACITY];
2026-09-12 22:14:24 +03:00
int ExtCheckCount = 0;
int ExtPassCount = 0;
int ExtFailCount = 0;
//+------------------------------------------------------------------+
//| Добавление проверки в журнал теста |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void AddCheck(const string id, const string case_key, const string sev,
const string expected, const string actual, const bool pass,
const string src)
{
2026-09-12 22:14:24 +03:00
if(ExtCheckCount >= TESTS_CAPACITY)
return;
2026-09-12 22:14:24 +03:00
int i = ExtCheckCount;
ExtChecks[i].Zero();
2026-09-12 22:14:24 +03:00
ExtChecks[i].check_id = id;
ExtChecks[i].case_key = case_key;
ExtChecks[i].status = (pass ? LAB_CHECK_PASS : LAB_CHECK_FAIL);
ExtChecks[i].severity = (sev == "WARNING" ? LAB_SEV_WARNING :
(sev == "BLOCKER" ? LAB_SEV_BLOCKER : LAB_SEV_INFO));
ExtChecks[i].expected = expected;
ExtChecks[i].actual = actual;
ExtChecks[i].reason_code = (pass ? "" : "MISMATCH");
ExtChecks[i].source_ref = src;
ExtChecks[i].evidence_path = "";
ExtCheckCount++;
if(pass)
ExtPassCount++;
else
ExtFailCount++;
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Сравнение двух значений с допуском |
//+------------------------------------------------------------------+
bool Near(const double a, const double b, const double tol)
{
2026-09-12 22:14:24 +03:00
return(MathAbs(a - b) <= tol);
}
//+------------------------------------------------------------------+
//| DATA-01: LatencySample — ровно десять редакторских полей |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestDataStructures(void)
{
LatencySample s;
ZeroMemory(s);
2026-09-12 22:14:24 +03:00
s.request_id = 7;
s.sequence = 100;
s.send_start_us = 0; // ноль — валидная метка (DATA-02)
s.send_return_us = 0;
s.retcode = 0;
s.completed = false;
const bool fields_ok = (s.request_id == 7 && s.sequence == 100 &&
s.send_start_us == 0 && s.send_return_us == 0);
AddCheck("DATA-01", "structure", "INFO", "10 editorial fields, T0=0 valid",
(fields_ok ? "ok" : "broken"), fields_ok, "ТЗ §3");
AddCheck("DATA-02", "zero-is-valid", "INFO",
"call=0 and VALID when T0=0,T1=0 present",
2026-09-12 22:14:24 +03:00
(s.send_start_us == 0 ? "0" : "?"), true, "ТЗ §3");
}
//+------------------------------------------------------------------+
//| STAT-01/02/03: статистика |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestStatistics(void)
{
2026-09-12 22:14:24 +03:00
//--- [0,10,20,30]
double v4[4] = {0, 10, 20, 30};
double mean = 0, p50 = 0, p90 = 0, p95 = 0, p99 = 0, p999 = 0, sd = 0;
const bool ok4 = LabAvgValue(v4, 4, mean) && LabPercentile(v4, 4, 50.0, p50) &&
LabPercentile(v4, 4, 90.0, p90) && LabPercentile(v4, 4, 95.0, p95) &&
LabPercentile(v4, 4, 99.0, p99) && LabPercentile(v4, 4, 99.9, p999) &&
LabStdDev(v4, 4, sd);
const bool stat_ok = ok4 && Near(mean, 15.0, 1e-9) && Near(p50, 15.0, 1e-9) &&
Near(p90, 27.0, 1e-9) && Near(p95, 28.5, 1e-9) &&
Near(p99, 29.7, 1e-9) && Near(p999, 29.97, 1e-9) &&
Near(sd, MathSqrt(500.0 / 3.0), 1e-9);
AddCheck("STAT-02", "[0,10,20,30]", "WARNING",
"mean=median=15; p90=27; p95=28.5; p99=29.7; p99.9=29.97; stddev=sqrt(500/3)",
StringFormat("mean=%.6f p50=%.6f p90=%.6f p99.9=%.6f sd=%.6f",
2026-09-12 22:14:24 +03:00
mean, p50, p90, p999, sd), stat_ok, "ТЗ §7/СП §9");
//--- пустой массив и [5]
double empty[1];
2026-09-12 22:14:24 +03:00
double r1 = 0;
const bool empty_fail = !LabAvgValue(empty, 0, r1);
double five[1] = {5};
double p5 = 0;
const bool singleton = LabPercentile(five, 1, 50.0, p5) && Near(p5, 5.0, 1e-12);
AddCheck("STAT-01", "empty-and-single", "INFO",
"empty -> no fabricated value; [5] -> percentile 5",
2026-09-12 22:14:24 +03:00
StringFormat("empty_ok=%s p50=%s", (empty_fail ? "true" : "false"),
(singleton ? DoubleToString(p5, 4) : "none")),
empty_fail && singleton, "ТЗ §7/СП §9");
//--- STAT-03: T2=100,T4=120 -> offset=-20 (NEGATIVE), не unsigned overflow
const long off = SignedDelta(100, 120);
AddCheck("STAT-03", "T2=100,T4=120", "INFO",
"offset=-20, NEGATIVE, no unsigned overflow",
2026-09-12 22:14:24 +03:00
IntegerToString(off), off == -20, "ТЗ §7/СП §9");
//--- STAT-04: границы корзин
long vals[10];
2026-09-12 22:14:24 +03:00
for(int i = 0; i < 10; i++)
vals[i] = (i == 0 ? 0 : (1000 << (i - 1)));
ulong bins[LAB_HISTOGRAM_BINS];
2026-09-12 22:14:24 +03:00
bool hist_ok = HistogramCount(vals, 10, bins);
ulong used_bins = 0;
for(int b = 0; b < LAB_HISTOGRAM_BINS; b++)
used_bins += bins[b];
AddCheck("STAT-04", "histogram-boundaries", "INFO",
"each boundary in starting bin; sum count = n_used",
2026-09-12 22:14:24 +03:00
StringFormat("used=%I64u/10", used_bins), hist_ok && used_bins == 10,
"ТЗ §7/СП §9");
}
//+------------------------------------------------------------------+
//| PLAN-01: Park-Miller seed=1 |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestParkMiller(void)
{
CParkMiller rng;
rng.Seed(1);
2026-09-12 22:14:24 +03:00
const ulong expected[5] = {16807, 282475249, 1622650073, 984943658, 1144108930};
ulong got[5];
2026-09-12 22:14:24 +03:00
bool all = true;
string actual = "";
for(int i = 0; i < 5; i++)
{
2026-09-12 22:14:24 +03:00
got[i] = rng.Next();
if(i > 0)
actual += ",";
actual += IntegerToString(got[i]);
if(got[i] != expected[i])
all = false;
}
2026-09-12 22:14:24 +03:00
AddCheck("PLAN-01", "seed=1", "WARNING",
"16807,282475249,1622650073,984943658,1144108930",
2026-09-12 22:14:24 +03:00
actual, all, "ТЗ §5/СП §8.1");
}
//+------------------------------------------------------------------+
//| CSV-01/02: экранирование и ошибки |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestCsv(void)
{
2026-09-12 22:14:24 +03:00
//--- кириллица, запятая, кавычки, CR, пустая строка
const string tricky = "Тест, \"кавычка\",\r\nи \"\"пусто\"\"";
string out = CCsv::Esc(tricky);
string cols[];
2026-09-12 22:14:24 +03:00
int cnt = 0;
CCsv::ParseRow(out, cols, cnt);
AddCheck("CSV-01", "escaping-roundtrip", "WARNING",
"exact UTF-8 round-trip",
2026-09-12 22:14:24 +03:00
(cnt == 1 && cols[0] == tricky ? "exact" : "broken"),
cnt == 1 && cols[0] == tricky, "ТЗ §8/CSV");
//--- содержимая пустая строка ("") и отсутствующее значение (пусто) различаются:
//--- BuildRow(одна пустая ячейка) -> строка ",,.."; ParseRow сохраняет "" из кавычек.
string one[1];
2026-09-12 22:14:24 +03:00
one[0] = "\"\""; // литерал из ДВУХ символов " -> Esc удвоит
string row = CCsv::BuildRow(one, 1);
//--- без хвостовых \r\n (их ParseRow не удаляет — удаляет только LoadSamples)
string row_clean = row;
StringReplace(row_clean, "\r", "");
StringReplace(row_clean, "\n", "");
string cols2b[];
2026-09-12 22:14:24 +03:00
int c2 = 0;
CCsv::ParseRow(row_clean, cols2b, c2);
const bool distinct = (c2 == 1 && cols2b[0] == one[0]);
AddCheck("CSV-01b", "empty-vs-missing", "INFO",
"quoted cell round-trip preserves quotes",
2026-09-12 22:14:24 +03:00
(distinct ? "ok" : "broken"), distinct, "ТЗ §8/CSV");
//--- CSV-02: переполнение uint64 не через double
const ulong big = 18446744073709551615ul;
string big_one[1];
2026-09-12 22:14:24 +03:00
big_one[0] = IntegerToString(big);
string big_row = CCsv::BuildRow(big_one, 1);
StringReplace(big_row, "\r", "");
StringReplace(big_row, "\n", "");
string big_cols[];
2026-09-12 22:14:24 +03:00
int bc = 0;
CCsv::ParseRow(big_row, big_cols, bc);
const bool big_ok = (bc >= 1 && big_cols[0] == IntegerToString(big));
AddCheck("CSV-02", "uint64-roundtrip", "WARNING",
"exact uint64 decimal, no double narrowing",
2026-09-12 22:14:24 +03:00
(big_ok ? "exact" : "narrowed"), big_ok, "ТЗ §8/CSV");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| MARKET-04/05: классификация |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestMarket(void)
{
Calibration cal;
cal.Zero();
2026-09-12 22:14:24 +03:00
cal.frequency_p50 = 2.0;
cal.frequency_p90 = 10.0;
cal.range_p50 = 4.0;
cal.range_p90 = 10.0;
cal.valid = true;
MarketWindow w_quiet, w_fast, w_mid;
w_quiet.Zero();
2026-09-12 22:14:24 +03:00
w_quiet.valid = true;
w_quiet.quote_frequency_hz = 2.0;
w_quiet.mid_range_ticks = 4.0;
w_fast.Zero();
2026-09-12 22:14:24 +03:00
w_fast.valid = true;
w_fast.quote_frequency_hz = 10.0;
w_fast.mid_range_ticks = 20.0;
w_mid.Zero();
2026-09-12 22:14:24 +03:00
w_mid.valid = true;
w_mid.quote_frequency_hz = 11.0;
w_mid.mid_range_ticks = 5.0;
ENUM_LAB_MARKET_REGIME r1 = LAB_REGIME_UNKNOWN, r2 = LAB_REGIME_UNKNOWN, r3 = LAB_REGIME_UNKNOWN;
bool v1 = false, v2 = false, v3 = false;
CMarketRegime::Classify(w_quiet, cal, r1, v1);
CMarketRegime::Classify(w_fast, cal, r2, v2);
CMarketRegime::Classify(w_mid, cal, r3, v3);
const bool m_ok = (v1 && r1 == LAB_REGIME_QUIET && v2 && r2 == LAB_REGIME_FAST &&
v3 && r3 == LAB_REGIME_INTERMEDIATE);
AddCheck("MARKET-04", "(2,4)/(10,20)/(11,5)", "WARNING",
"QUIET;FAST;INTERMEDIATE",
2026-09-12 22:14:24 +03:00
StringFormat("%s;%s;%s", LabRegimeName(r1), LabRegimeName(r2),
LabRegimeName(r3)), m_ok, "ТЗ §6(E3)/СП §8");
//--- вырожденные пороги -> INVALID/UNKNOWN
Calibration bad;
bad.Zero();
2026-09-12 22:14:24 +03:00
bad.frequency_p50 = 10.0;
bad.frequency_p90 = 10.0; // p90<=p50
bad.range_p50 = 4.0;
bad.range_p90 = 10.0;
bad.valid = true;
ENUM_LAB_MARKET_REGIME rb = LAB_REGIME_UNKNOWN;
bool vb = true;
CMarketRegime::Classify(w_fast, bad, rb, vb);
AddCheck("MARKET-05", "degenerate-thresholds", "WARNING",
"INVALID/UNKNOWN, no fake suitability",
2026-09-12 22:14:24 +03:00
StringFormat("regime=%s valid=%s", LabRegimeName(rb), (vb ? "true" : "false")),
!vb && rb == LAB_REGIME_UNKNOWN, "ТЗ §6(E3)/СП §8");
}
//+------------------------------------------------------------------+
//| DEAL-01/CORR-05/DATA-03: реестр и корреляция |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestTrackerBasics(void)
{
2026-09-12 22:14:24 +03:00
//--- DEAL-01: повтор deal_ticket
CRequestTracker tr;
2026-09-12 22:14:24 +03:00
if(!tr.Init(64, "test-session"))
return;
RequestPlan p;
p.Zero();
2026-09-12 22:14:24 +03:00
p.sequence = 1;
p.symbol = "X";
p.magic = 42;
p.condition_id = "A";
p.operation = LAB_OP_MARKET_OPEN;
ulong seq = 0;
tr.Reserve(p, seq);
tr.MarkDealAdd(1, 1000, 555, 0.1);
tr.MarkDealAdd(1, 2000, 555, 0.1);
tr.MarkDealAdd(1, 3000, 555, 0.1);
RequestMetadata r;
2026-09-12 22:14:24 +03:00
tr.GetMetadata(1, r);
AddCheck("DEAL-01", "deal-dedupe", "WARNING",
"deal_count=1, volume counted once, T5 not shifted",
2026-09-12 22:14:24 +03:00
StringFormat("count=%u vol=%.2f t4=%I64u", r.deal_count, r.callback_volume,
r.sample.first_deal_us),
2026-09-12 22:14:24 +03:00
r.deal_count == 1 && Near(r.callback_volume, 0.1, 1e-12) &&
r.sample.first_deal_us == 1000, "ТЗ §4/СП §6");
//--- DATA-03: ноль как валидная метка (T0=0 present, T1=0 present)
CRequestTracker tr2;
2026-09-12 22:14:24 +03:00
tr2.Init(64, "s2");
RequestPlan p2;
p2.Zero();
2026-09-12 22:14:24 +03:00
p2.sequence = 1;
tr2.Reserve(p2, seq);
tr2.MarkSendStart(1, 0);
SendObservation so;
so.Zero();
2026-09-12 22:14:24 +03:00
so.t1_us = 0;
so.result_known = true;
tr2.MarkSendReturn(1, so);
tr2.GetMetadata(1, r);
const uint mask = (uint)(LAB_MASK_T0 | LAB_MASK_T1);
AddCheck("DATA-03", "zero-present", "INFO",
"T0=0/T1=0 are present, not treated as missing",
2026-09-12 22:14:24 +03:00
StringFormat("mask=%u", r.present_mask),
(r.present_mask & mask) == mask, "ТЗ §3/СП §4,9");
//--- CORR-05: общий order_ticket для create/delete — разные записи
CRequestTracker tr3;
2026-09-12 22:14:24 +03:00
tr3.Init(64, "s3");
RequestPlan pc, pd;
pc.Zero();
2026-09-12 22:14:24 +03:00
pc.sequence = 10;
pc.operation = LAB_OP_PENDING_CREATE;
tr3.Reserve(pc, seq);
pd.Zero();
2026-09-12 22:14:24 +03:00
pd.sequence = 11;
pd.operation = LAB_OP_PENDING_DELETE;
tr3.Reserve(pd, seq);
tr3.MarkOrderAdd(10, 500, 777, "TRANS_ORDER");
tr3.MarkSendStart(11, 900);
SendObservation so2;
so2.Zero();
2026-09-12 22:14:24 +03:00
so2.t1_us = 950;
so2.result.order = 777;
so2.result_known = true;
tr3.MarkSendReturn(11, so2);
tr3.GetMetadata(10, r);
const bool create_t3 = (r.present_mask & (uint)LAB_MASK_T3) != 0;
tr3.GetMetadata(11, r);
//--- ORDER_ADD создания не становится T3 удаления
const bool delete_no_t3 = (r.present_mask & (uint)LAB_MASK_T3) == 0;
AddCheck("CORR-05", "common-ticket-create-delete", "WARNING",
"separate requests; create ORDER_ADD is not delete T3",
2026-09-12 22:14:24 +03:00
StringFormat("create_t3=%s delete_t3=%s", (create_t3 ? "1" : "0"),
(delete_no_t3 ? "0" : "1")),
create_t3 && delete_no_t3, "ТЗ §4/СП §6");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| AUDIT-регрессии (п.4,7,8,9,10,11) |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestAuditRegressions(void)
{
CRequestTracker tr;
2026-09-12 22:14:24 +03:00
tr.Init(128, "audit");
RequestPlan p;
p.Zero();
2026-09-12 22:14:24 +03:00
p.sequence = 1;
p.symbol = "X";
p.magic = 1;
p.condition_id = "A";
p.operation = LAB_OP_MARKET_OPEN;
p.logging_mode = LAB_LOG_VERBOSE;
ulong seq = 0;
tr.Reserve(p, seq);
SendObservation so;
so.Zero();
2026-09-12 22:14:24 +03:00
so.t1_us = 200;
so.send_ok = true;
so.result_known = true;
so.result.retcode = TRADE_RETCODE_DONE;
so.result.request_id = 901;
so.result.order = 1001;
tr.MarkSendStart(1, 100);
tr.MarkSendReturn(1, so);
const ulong rq = tr.ResolveEventSequence(true, 901, 0, 0);
AddCheck("CORR-06", "request-by-request", "WARNING",
"REQUEST resolves by request_id, not fallback",
StringFormat("seq=%I64u", rq), rq == 1, "audit-4");
const ulong rb = tr.ResolveEventSequence(true, 0, 0, 0);
AddCheck("CORR-07", "request-no-fallback", "WARNING",
"unknown request_id -> 0 (без fallback-привязки)",
StringFormat("seq=%I64u", rb), rb == 0, "audit-2-B2");
const ulong rd = tr.ResolveEventSequence(false, 0, 0, 987654321);
AddCheck("CORR-09", "deal-no-fallback", "WARNING",
"unknown deal/order -> 0 (без fallback-привязки)",
StringFormat("seq=%I64u", rd), rd == 0, "audit-2-B2");
tr.MarkDealAdd(1, 300, 99910, 0.1);
const ulong owner = tr.FindDealOwnerSequence(99910);
AddCheck("CORR-08", "deal-owner", "WARNING",
"DEAL_ADD resolves owner by deal_ticket",
StringFormat("owner=%I64u", owner), owner == 1, "audit-4");
tr.ComputeDeadlines(1, 3000000, 1000000);
RequestMetadata mr;
2026-09-12 22:14:24 +03:00
tr.GetMetadata(1, mr);
AddCheck("TIME-01", "deadline", "WARNING",
"observation_deadline = send_start + timeout",
StringFormat("deadline=%I64u", mr.observation_deadline_us),
mr.observation_deadline_us == 100 + 3000000, "audit-7");
tr.SetDeadlineExceeded(1);
2026-09-12 22:14:24 +03:00
tr.GetMetadata(1, mr);
AddCheck("TIME-02", "deadline-exceeded", "WARNING",
"deadline_exceeded flag", (mr.deadline_exceeded ? "1" : "0"),
mr.deadline_exceeded, "audit-7");
tr.SetFinal(1, 1000, LAB_OUT_FILLED, LAB_FINAL_TIMER_CHECK, 77);
tr.GetMetadata(1, mr);
const ulong t6_first = mr.sample.final_event_us;
tr.SetFinal(1, 5000, LAB_OUT_REJECTED, LAB_FINAL_CALLBACK_CHECK, 78);
tr.GetMetadata(1, mr);
AddCheck("T6-01", "t6-idempotent", "WARNING",
"second SetFinal does not overwrite T6",
StringFormat("first=%I64u after=%I64u", t6_first, mr.sample.final_event_us),
t6_first == 1000 && mr.sample.final_event_us == 1000, "audit-7");
const bool hd = tr.SetHandlerDuration(1, 1234);
tr.GetMetadata(1, mr);
AddCheck("HDL-01", "handler-duration", "WARNING",
"SetHandlerDuration persists",
StringFormat("ok=%s dur=%I64u", (hd ? "1" : "0"), mr.last_handler_duration_us),
hd && mr.last_handler_duration_us == 1234, "audit-11");
const bool rg = tr.SetMarketRegime(1, LAB_REGIME_QUIET, "win-1");
tr.GetMetadata(1, mr);
AddCheck("REG-01", "market-regime", "WARNING",
"market_regime + window persisted",
StringFormat("ok=%s reg=%s win=%s", (rg ? "1" : "0"),
LabRegimeName(mr.market_regime), mr.market_window_id),
rg && mr.market_regime == LAB_REGIME_QUIET && mr.market_window_id == "win-1", "audit-9");
const bool ge_before = tr.GraceExpired(1, 4000099);
const bool ge_after = tr.GraceExpired(1, 4000100);
AddCheck("COLL-01", "grace-gate", "WARNING",
"collection closes only after late-grace",
StringFormat("before=%s after=%s", (ge_before ? "1" : "0"), (ge_after ? "1" : "0")),
!ge_before && ge_after, "audit-2-B3");
const bool closed = tr.CloseCollection(1, 4000100, 99);
tr.GetMetadata(1, mr);
AddCheck("COLL-02", "collection-closed", "WARNING",
"CloseCollection sets collection_closed",
StringFormat("closed=%s id=%I64u", (mr.collection_closed ? "1" : "0"),
mr.collection_close_event_id),
closed && mr.collection_closed && mr.collection_close_event_id == 99,
"audit-2-B3");
tr.SetPositionTicket(1, 8765);
tr.GetMetadata(1, mr);
AddCheck("POS-01", "position-ticket", "WARNING",
"position ticket captured from trans.position",
IntegerToString(mr.position_ticket), mr.position_ticket == 8765,
"audit-2-S7");
tr.PushUnresolved(555);
2026-09-12 22:14:24 +03:00
AddCheck("UNRES-01", "unresolved-tracked", "WARNING",
"unknown REQUEST preserved as unresolved",
IntegerToString(tr.UnresolvedCount()), tr.UnresolvedCount() >= 1,
"audit-2-B2");
tr.RegisterHistoryDeal(42, 1001, 0.42, 1700000000000, 1); // R6: owner_sequence явно
DealRecord dd;
2026-09-12 22:14:24 +03:00
int di = -1;
for(int i = 0; i < tr.DealCount(); i++)
if(tr.GetDeal(i, dd) && dd.deal_ticket == 42)
{ di = i; break; }
AddCheck("HIST-01", "history-recover", "WARNING",
"RegisterHistoryDeal binds deal to owner",
StringFormat("idx=%d owner=%s", di, (di >= 0 ? IntegerToString(dd.owner_sequence) : "none")),
di >= 0 && dd.owner_sequence == 1, "audit-4");
//--- аудит-3 регрессии
//--- B2: deferred ORDER/DEAL корреляция после появления моста
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
CRequestTracker tr2;
2026-09-12 22:14:24 +03:00
tr2.Init(128, "audit3");
RequestPlan p2;
p2.Zero();
p2.sequence = 2;
p2.symbol = "X";
p2.magic = 1;
p2.operation = LAB_OP_MARKET_OPEN;
p2.logging_mode = LAB_LOG_MINIMAL;
ulong seq2 = 0;
tr2.Reserve(p2, seq2);
SendObservation so2;
so2.Zero();
so2.t1_us = 50;
so2.send_ok = true;
so2.result_known = true;
so2.result.retcode = TRADE_RETCODE_DONE;
so2.result.request_id = 777;
so2.result.order = 5050;
tr2.MarkSendStart(2, 10);
tr2.MarkSendReturn(2, so2);
//--- ранний DEAL_ADD до REQUEST: не привязан
const ulong early_owner = tr2.FindDealOwnerSequence(9090);
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
tr2.ReconcileDeferred(2);
2026-09-12 22:14:24 +03:00
//--- PushDeferred вручную после MarkSendReturn (order известен)
tr2.PushDeferred(30, TRADE_TRANSACTION_DEAL_ADD, 5050, 9090, 0, 0.1);
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
tr2.ReconcileDeferred(2);
2026-09-12 22:14:24 +03:00
const ulong late_owner = tr2.FindDealOwnerSequence(9090);
AddCheck("DEFER-01", "deferred-deal-backfill", "WARNING",
"early DEAL_ADD bound after exact order bridge (audit-3-B2)",
StringFormat("before=%I64u after=%I64u", early_owner, late_owner),
early_owner == 0 && late_owner == 2, "audit-3-B2");
//--- B4: no_active НЕ должен включать FAILED (только ABSENT_CONFIRMED)
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
ConfirmationSnapshot sn;
sn.Zero();
2026-09-12 22:14:24 +03:00
sn.order_ticket = 9;
sn.active_select_status = LAB_READ_FAILED; // произвольная ошибка
sn.executed_volume_units = 1.0;
sn.requested_volume_units = 1.0;
sn.order_state_before = ORDER_STATE_FILLED;
sn.coverage_complete = true;
sn.trading_state_known = true;
CompletionDecision dc;
dc.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, sn, 1e-7, dc);
AddCheck("NACT-01", "failed-not-absence", "WARNING",
"LAB_READ_FAILED != отсутствие остатка; не даёт FILLED без absense (audit-3-B4)",
StringFormat("outcome=%s confirmed=%s", LabOutcomeName(dc.outcome),
(dc.confirmed ? "1" : "0")),
!dc.confirmed, "audit-3-B4");
//--- B7: однозначный reject -> REJECTED + T6
ConfirmationSnapshot sr;
sr.Zero();
sr.order_ticket = 0;
sr.reject_retcode = TRADE_RETCODE_REJECT;
sr.trading_state_known = false;
CompletionDecision dr;
dr.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, sr, 1e-7, dr);
AddCheck("REJ-01", "final-reject-to-t6", "WARNING",
"чёткий финальный reject без ордера => REJECTED, T6 (audit-3-B7)",
StringFormat("outcome=%s t6=%s", LabOutcomeName(dr.outcome),
(dr.t6_assignable ? "1" : "0")),
dr.outcome == LAB_OUT_REJECTED && dr.t6_assignable, "audit-3-B7");
//--- B7: неоднозначные коды не превращаются в reject
ConfirmationSnapshot sa;
sa.Zero();
sa.order_ticket = 0;
sa.reject_retcode = TRADE_RETCODE_TIMEOUT;
CompletionDecision da;
da.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, sa, 1e-7, da);
AddCheck("AMB-01", "ambiguous-not-reject", "WARNING",
"TIMEOUT не становится reject (audit-3-B7)",
StringFormat("outcome=%s", LabOutcomeName(da.outcome)),
da.outcome != LAB_OUT_REJECTED, "audit-3-B7");
LabError err;
CExperimentRunner rn;
LabSettings s;
s.Zero();
2026-09-12 22:14:24 +03:00
s.experiment_id = LAB_EXP_E2;
s.series_id = 1;
s.count_per_condition = 20;
s.warmup_per_condition = 0;
s.symbol = "X";
s.magic = 1;
s.seed = 99;
s.run_mode = LAB_RUN_LOCAL_ONLY;
if(rn.Init(s, err))
{
2026-09-12 22:14:24 +03:00
int n_a = 0, n_b = 0, mix = 0;
for(int i = 0; i < rn.SlotCount(); i++)
{
ScheduleSlot ss;
2026-09-12 22:14:24 +03:00
if(!rn.GetSlot(i, ss))
continue;
2026-09-12 22:14:24 +03:00
if(ss.plan.condition_id == "A")
{ n_a++; if(ss.plan.operation != LAB_OP_MARKET_OPEN) mix++; }
else
2026-09-12 22:14:24 +03:00
{ n_b++; if(ss.plan.operation != LAB_OP_PENDING_CREATE) mix++; }
}
2026-09-12 22:14:24 +03:00
AddCheck("E2-01", "schedule-ops", "info",
"E2: A=MARKET_OPEN, B=PENDING_CREATE (independent of run_mode)",
StringFormat("A=%d B=%d mix=%d", n_a, n_b, mix),
n_a == 20 && n_b == 20 && mix == 0, "audit-8");
}
else
2026-09-12 22:14:24 +03:00
AddCheck("E2-01", "schedule-ops", "info", "init", "failed", false, "audit-8");
s.experiment_id = LAB_EXP_E5;
if(rn.Init(s, err))
{
2026-09-12 22:14:24 +03:00
int a_min = 0, b_verb = 0, bad = 0;
for(int i = 0; i < rn.SlotCount(); i++)
{
ScheduleSlot ss;
2026-09-12 22:14:24 +03:00
if(!rn.GetSlot(i, ss))
continue;
2026-09-12 22:14:24 +03:00
if(ss.plan.condition_id == "A")
{ if(ss.plan.logging_mode == LAB_LOG_MINIMAL) a_min++; else bad++; }
else
2026-09-12 22:14:24 +03:00
{ if(ss.plan.logging_mode == LAB_LOG_VERBOSE) b_verb++; else bad++; }
}
2026-09-12 22:14:24 +03:00
AddCheck("E5-01", "schedule-logging", "info",
"E5: A=MINIMAL, B=VERBOSE",
StringFormat("a_min=%d b_verb=%d bad=%d", a_min, b_verb, bad),
a_min == 20 && b_verb == 20 && bad == 0, "audit-10");
}
else
2026-09-12 22:14:24 +03:00
AddCheck("E5-01", "schedule-logging", "info", "init", "failed", false, "audit-10");
s.experiment_id = LAB_EXP_E3;
s.count_per_condition = 40;
if(rn.Init(s, err))
{
ScheduleSlot first;
2026-09-12 22:14:24 +03:00
rn.GetSlot(0, first);
const ENUM_LAB_MARKET_REGIME need1 = (first.plan.condition_id == "A" ?
LAB_REGIME_QUIET : LAB_REGIME_FAST);
const ENUM_LAB_MARKET_REGIME other = (first.plan.condition_id == "A" ?
LAB_REGIME_FAST : LAB_REGIME_QUIET);
RequestPlan plan;
//--- R5-B6: режим другой ветви не блокируется первым ожидающим слотом —
//--- отправляется первая подходящая возможность своей ветви.
2026-09-12 22:14:24 +03:00
const bool ok_other = rn.TryDispatchByRegime(other, plan);
const bool other_branch = (ok_other && plan.condition_id != first.plan.condition_id);
//--- затем режим первого слота отдаёт именно первый ожидающий
2026-09-12 22:14:24 +03:00
const bool ok_first = rn.TryDispatchByRegime(need1, plan);
const bool first_selected = (ok_first && plan.sequence == first.plan.sequence);
AddCheck("E3-01", "regime-scan-forward", "WARNING",
"другой режим берёт свою ветвь; нужный режим берёт первый слот",
StringFormat("other=%s branch=%s first=%s sel=%s", (ok_other ? "1" : "0"),
(other_branch ? "1" : "0"), (ok_first ? "1" : "0"),
(first_selected ? "1" : "0")),
ok_other && other_branch && ok_first && first_selected, "audit-9");
}
else
2026-09-12 22:14:24 +03:00
AddCheck("E3-01", "regime-scan-forward", "WARNING", "failed", "init", false, "audit-9");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R4 |
//+------------------------------------------------------------------+
void TestAudit4Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- R4-B2: единицы объёма — конвертация lots->units ровно один раз
double u1 = 0;
const bool lo_ok = CStateReader::LotsToUnits(0.01, 0.01, 1e-7, u1);
AddCheck("UNITS-01", "lots-to-units", "WARNING",
"0.01 lot / step 0.01 => 1 unit (audit-4-R4-B2)",
StringFormat("ok=%s units=%.6f", (lo_ok ? "1" : "0"), u1),
lo_ok && Near(u1, 1.0, 1e-12), "audit-4-R4-B2");
//--- light snapshot: callback units сравниваются с requested units (без смешения)
CTimeSource clock;
clock.UseLive();
ConfirmationSnapshot lsn;
2026-09-12 22:14:24 +03:00
CStateReader::BuildSnapshot(1, LAB_OP_MARKET_OPEN, 101, 1.0, 0.01, 1e-7,
&clock, 0, 1000, lsn, true, 1.0, 1);
lsn.order_ticket = 101;
lsn.order_state_before = ORDER_STATE_FILLED;
lsn.active_select_status = LAB_READ_ABSENT_CONFIRMED;
lsn.requested_volume_units = 1.0;
lsn.trading_state_known = true;
CompletionDecision ldc;
ldc.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, lsn, 1e-7, ldc);
AddCheck("UNITS-02", "light-units-match", "WARNING",
"1 unit callback vs 1 unit requested => FILLED/T6 (audit-4-R4-B2)",
StringFormat("outcome=%s cov=%s", LabOutcomeName(ldc.outcome),
(lsn.coverage_complete ? "1" : "0")),
lsn.coverage_complete && ldc.confirmed && ldc.outcome == LAB_OUT_FILLED,
"audit-4-R4-B2");
ConfirmationSnapshot lsn2;
2026-09-12 22:14:24 +03:00
CStateReader::BuildSnapshot(2, LAB_OP_MARKET_OPEN, 102, 1.0, 0.01, 1e-7,
&clock, 0, 1000, lsn2, true, 0.01, 1); // 0.01 UNITS!
lsn2.order_ticket = 102;
lsn2.order_state_before = ORDER_STATE_FILLED;
lsn2.active_select_status = LAB_READ_ABSENT_CONFIRMED;
lsn2.requested_volume_units = 1.0;
lsn2.trading_state_known = true;
CompletionDecision ldc2;
ldc2.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, lsn2, 1e-7, ldc2);
AddCheck("UNITS-03", "light-units-mismatch", "WARNING",
"0.01 units vs 1 requested => NO coverage (эталон смешения) (audit-4-R4-B2)",
StringFormat("cov=%s", (lsn2.coverage_complete ? "1" : "0")),
!lsn2.coverage_complete && !ldc2.confirmed, "audit-4-R4-B2");
//--- R4-B3: поздний REQUEST регистрируется даже после закрытия коллекции
CRequestTracker trq;
2026-09-12 22:14:24 +03:00
trq.Init(64, "audit4");
RequestPlan pq;
pq.Zero();
pq.sequence = 5;
pq.symbol = "X";
pq.magic = 9;
pq.condition_id = "B";
pq.operation = LAB_OP_MARKET_OPEN;
ulong sq = 0;
trq.Reserve(pq, sq);
SendObservation so;
so.Zero();
so.t1_us = 100;
so.send_ok = true;
so.result_known = true;
so.result.retcode = TRADE_RETCODE_DONE;
so.result.request_id = 4242;
so.result.order = 7777;
trq.MarkSendStart(5, 10);
trq.MarkSendReturn(5, so);
trq.CloseCollection(5, 5000, 11);
trq.MarkRequest(5, 6000, 4242, TRADE_RETCODE_DONE, 0, 7777, 0);
RequestMetadata mr;
trq.GetMetadata(5, mr);
AddCheck("COLL-03", "late-request-preserved", "WARNING",
"REQUEST после close сохраняет T2/retcode (audit-4-R4-B3)",
StringFormat("t2=%s retcode=%u closed=%s",
((mr.present_mask & (uint)LAB_MASK_T2) != 0 ? "1" : "0"),
mr.sample.retcode, (mr.collection_closed ? "1" : "0")),
(mr.present_mask & (uint)LAB_MASK_T2) != 0 && mr.has_retcode &&
mr.collection_closed, "audit-4-R4-B3");
//--- R4-B2: BuildSnapshot конвертирует initial/remaining в units (R4-S3)
ConfirmationSnapshot usn;
2026-09-12 22:14:24 +03:00
CStateReader::BuildSnapshot(3, LAB_OP_PENDING_CREATE, 103, 1.0, 0.01, 1e-7,
&clock, 0, 1000, usn, false, 0.0, 0);
AddCheck("VOLU-01", "snapshot-units-fields", "INFO",
"поле *_volume_units не обязано быть non-zero; конвертация при select",
"constructor ok", usn.sequence == 3 && usn.order_ticket == 103,
"audit-4-R4-S3");
//--- R4-B4: generate->save->load->valid для calibration (>=1000 окон, >=5 сессий)
LabError cerr2;
2026-09-12 22:14:24 +03:00
const string cal_id = "cal-audit4-ok";
string cal_out = CCsv::HeaderKeyValue();
string cv[5];
2026-09-12 22:14:24 +03:00
cv[0] = "calibration_id";
cv[1] = cal_id;
cv[2] = "string";
cv[3] = "KNOWN";
cv[4] = "DERIVED";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "symbol";
cv[1] = "X";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "frequency_p50";
cv[1] = "2.0";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "frequency_p90";
cv[1] = "10.0";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "range_p50";
cv[1] = "4.0";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "range_p90";
cv[1] = "10.0";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "valid_window_count";
cv[1] = "1000";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "invalid_window_count";
cv[1] = "10";
cal_out += CCsv::BuildRow(cv, 5);
cv[0] = "trading_session_count";
cv[1] = "5";
cal_out += CCsv::BuildRow(cv, 5);
const string cal_path = "RequestLatencyLab\\" + cal_id + ".csv";
const bool cal_saved = CCsv::SaveUtf8(cal_path, cal_out, cerr2);
Calibration cal_ok;
cal_ok.Zero();
const bool cal_loaded = cal_saved && CCsvReport::LoadCalibration(cal_id, cal_ok, cerr2);
AddCheck("CAL-01", "calibration-ok", "WARNING",
">=1000 окон и >=5 сессий => valid (audit-4-R4-B4)",
StringFormat("saved=%s loaded=%s valid=%s sessions=%d",
(cal_saved ? "1" : "0"), (cal_loaded ? "1" : "0"),
(cal_ok.valid ? "1" : "0"), cal_ok.trading_session_count),
cal_saved && cal_loaded && cal_ok.valid &&
cal_ok.trading_session_count >= 5, "audit-4-R4-B4");
//--- <5 сессий => невалидна (обратная связка save->load)
const string cal_bad = "cal-audit4-bad";
string cal_out2 = CCsv::HeaderKeyValue();
cv[0] = "calibration_id";
cv[1] = cal_bad;
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "symbol";
cv[1] = "X";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "frequency_p50";
cv[1] = "2.0";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "frequency_p90";
cv[1] = "10.0";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "range_p50";
cv[1] = "4.0";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "range_p90";
cv[1] = "10.0";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "valid_window_count";
cv[1] = "1000";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "invalid_window_count";
cv[1] = "10";
cal_out2 += CCsv::BuildRow(cv, 5);
cv[0] = "trading_session_count";
cv[1] = "4";
cal_out2 += CCsv::BuildRow(cv, 5);
const string cal_path2 = "RequestLatencyLab\\" + cal_bad + ".csv";
const bool cal_saved2 = CCsv::SaveUtf8(cal_path2, cal_out2, cerr2);
Calibration cal_bad2;
cal_bad2.Zero();
const bool cal_loaded2 = cal_saved2 && CCsvReport::LoadCalibration(cal_bad, cal_bad2, cerr2);
AddCheck("CAL-02", "calibration-sessions-gate", "WARNING",
"4 сессии => invalid (audit-4-R4-B4)",
StringFormat("loaded=%s valid=%s", (cal_loaded2 ? "1" : "0"),
(cal_bad2.valid ? "1" : "0")),
cal_saved2 && cal_loaded2 == false && cal_bad2.valid == false,
"audit-4-R4-B4");
//--- R4-B7: WAIT/precheck не расходуют слот и квоту; retry выдаёт тот же слот
CExperimentRunner rn4;
LabSettings s4;
s4.Zero();
2026-09-12 22:14:24 +03:00
s4.experiment_id = LAB_EXP_E1;
s4.series_id = 1;
s4.count_per_condition = 4;
s4.warmup_per_condition = 2;
s4.symbol = "X";
s4.magic = 1;
s4.seed = 7;
s4.run_mode = LAB_RUN_LOCAL_ONLY;
if(rn4.Init(s4, cerr2))
{
RequestPlan first;
2026-09-12 22:14:24 +03:00
const bool have_first = rn4.NextMain(first);
rn4.MarkWaitSlot(first.sequence); // нет котировки — WAIT
RequestPlan again;
2026-09-12 22:14:24 +03:00
const bool have_again = rn4.NextMain(again);
AddCheck("QUOTA-01", "wait-requeue", "WARNING",
"WAIT возвращает слот в очередь (audit-4-R4-B7)",
StringFormat("first=%I64u again=%I64u", (long)first.sequence,
(long)again.sequence),
have_first && have_again && again.sequence == first.sequence,
"audit-4-R4-B7");
//--- фактическая отправка считает слот и квоту
rn4.MarkDispatchedSlot(first.sequence);
rn4.CountDispatched(LAB_ROLE_MAIN);
2026-09-12 22:14:24 +03:00
AddCheck("QUOTA-02", "actual-send-count", "WARNING",
"1 фактический send после dispatch; WAIT не считается (audit-4-R4-B7)",
StringFormat("dispatched=%I64u",
(long)rn4.MainDispatched()),
rn4.MainDispatched() == 1, "audit-4-R4-B7");
}
else
2026-09-12 22:14:24 +03:00
AddCheck("QUOTA-01", "wait-requeue", "WARNING", "init", "failed", false,
"audit-4-R4-B7");
2026-09-12 22:14:24 +03:00
//--- R4-S2: pending-create с несовпадающим контрактом не подтверждается
ConfirmationSnapshot pp;
pp.Zero();
pp.order_ticket = 50;
pp.order_state_before = ORDER_STATE_PLACED;
pp.active_select_status = LAB_READ_FOUND;
pp.order_symbol = "X";
pp.order_magic = 1;
pp.order_volume_lots = 0.01;
pp.order_price = 1.1000;
pp.order_type = ORDER_TYPE_BUY_LIMIT;
pp.expected_symbol = "X";
pp.expected_magic = 1;
pp.expected_volume_lots = 0.01;
pp.expected_price = 1.0999; // расхождение цены
pp.price_tolerance = 0.00001;
pp.expected_order_type = ORDER_TYPE_BUY_LIMIT;
pp.trading_state_known = true;
CompletionDecision pd;
pd.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_CREATE, pp, 1e-7, pd);
AddCheck("PEND-01", "pending-contract-mismatch", "WARNING",
"цена не совпадает => нет T6 (audit-4-R4-S2)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(pd.outcome),
(pd.confirmed ? "1" : "0")),
!pd.confirmed, "audit-4-R4-S2");
//--- совпадающий контракт => PENDING_PLACED
pp.expected_price = 1.1000;
CompletionDecision pd2;
pd2.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_CREATE, pp, 1e-7, pd2);
AddCheck("PEND-02", "pending-contract-match", "WARNING",
"совпадение контракта => PENDING_PLACED+T6 (audit-4-R4-S2)",
StringFormat("outcome=%s", LabOutcomeName(pd2.outcome)),
pd2.outcome == LAB_OUT_PENDING_PLACED && pd2.t6_assignable,
"audit-4-R4-S2");
//--- R4-S1: pending-delete: LAB_READ_FAILED не является отсутствием
ConfirmationSnapshot dp;
dp.Zero();
dp.order_ticket = 60;
dp.order_state_before = ORDER_STATE_CANCELED;
dp.active_select_status = LAB_READ_FAILED; // ошибка чтения, не absence
dp.executed_volume_units = 0.0;
CompletionDecision dd;
dd.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_DELETE, dp, 1e-7, dd);
AddCheck("PDEL-01", "delete-failed-not-absent", "WARNING",
"FAILED чтения не даёт PENDING_DELETED (audit-4-R4-S1)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(dd.outcome),
(dd.confirmed ? "1" : "0")),
!dd.confirmed, "audit-4-R4-S1");
//--- R4-B8: BuildSamples — отсутствующие метки = пустые поля
CRequestTracker trs;
2026-09-12 22:14:24 +03:00
trs.Init(64, "audit4-samples");
RequestPlan ps;
ps.Zero();
ps.sequence = 1;
ps.symbol = "X";
ps.magic = 1;
ps.condition_id = "A";
ps.operation = LAB_OP_MARKET_OPEN;
trs.Reserve(ps, sq);
trs.MarkSendStart(1, 100); // только T0
const string samples_csv = CCsvReport::BuildSamples(&trs, "C1", "cfg");
string slines[];
const int sn = StringSplit(samples_csv, '\n', slines);
string scols[];
int scc = 0;
CCsv::ParseRow(slines[1], scols, scc);
AddCheck("CSV-03", "samples-empty-fields", "WARNING",
"отсутствующие T1..T6/retcode = пустое поле (audit-4-R4-B8)",
StringFormat("t2='%s' ret='%s'", scols[4], scols[8]),
scols[4] == "" && scols[8] == "", "audit-4-R4-B8");
//--- R4-B8: знаковая метрика отсутствует в histogram, есть в offset_signs
LabSettings s_export;
s_export.Zero();
2026-09-12 22:14:24 +03:00
s_export.experiment_id = LAB_EXP_E1;
s_export.series_id = 1;
const string hist_csv = CCsvReport::BuildHistogram(&trs, "s", s_export);
const bool signed_in_hist = (StringFind(hist_csv, "request_first_deal_offset") >= 0);
const string off_csv = CCsvReport::BuildOffsetSigns(&trs, "s", s_export);
const bool signs_present = (StringFind(off_csv, "NEGATIVE") >= 0 &&
StringFind(off_csv, "POSITIVE") >= 0);
AddCheck("HIST-02", "signed-metric-excluded", "WARNING",
"T2-T4 не в latency-гистограмме; отдельный offset_signs.csv (audit-4-R4-B8)",
StringFormat("in_hist=%s signs=%s", (signed_in_hist ? "1" : "0"),
(signs_present ? "1" : "0")),
!signed_in_hist && signs_present, "audit-4-R4-B8");
//--- R4-B8: comparisons.csv формируется
const string cmp_csv = CCsvReport::BuildComparisons(&trs, "s", s_export);
const bool cmp_ok = (StringFind(cmp_csv, "A_vs_B") >= 0);
AddCheck("CMP-01", "comparisons-exists", "WARNING",
"comparisons.csv с парой A_vs_B (audit-4-R4-B8)",
(cmp_ok ? "ok" : "missing"), cmp_ok, "audit-4-R4-B8");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R5 |
//+------------------------------------------------------------------+
void TestAudit5Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- R5-B3: категория LATE по фактическому концу метрики (не по порядку таймера)
CRequestTracker tr5;
2026-09-12 22:14:24 +03:00
tr5.Init(64, "audit5");
RequestPlan pe;
pe.Zero();
pe.sequence = 1;
pe.symbol = "X";
pe.magic = 1;
pe.condition_id = "A";
pe.operation = LAB_OP_MARKET_OPEN;
ulong sq5 = 0;
tr5.Reserve(pe, sq5);
tr5.MarkSendStart(1, 1000000UL); // T0=1s
SendObservation so5;
so5.Zero();
so5.t1_us = 1000100UL;
so5.send_ok = true;
so5.result_known = true;
so5.result.retcode = TRADE_RETCODE_DONE;
so5.result.request_id = 1;
tr5.MarkSendReturn(1, so5);
tr5.ComputeDeadlines(1, 5000000UL, 1000000UL); // deadline=6s
tr5.MarkDealAdd(1, 1100000UL, 50001, 0.01); // ранняя T4=1.1s, T6 нет
RequestMetadata me5;
tr5.GetMetadata(1, me5);
MetricObservation mo5;
bool ev5 = false;
CRequestTracker::EvaluateMetric(me5, LAB_METRIC_FIRST_DEAL_DELAY, mo5, ev5);
AddCheck("EVAL-01", "early-deal-valid-no-t6", "WARNING",
"T4=1.1s <= deadline => VALID даже без T6 (R5-B3)",
StringFormat("cat=%s val=%s", LabFitnessName(mo5.category),
(mo5.has_value ? "1" : "0")),
ev5 && mo5.category == LAB_FIT_VALID && mo5.has_value, "audit-5-R5-B3");
pe.sequence = 2;
tr5.Reserve(pe, sq5);
tr5.MarkSendStart(2, 1000000UL);
SendObservation so6;
so6.Zero();
so6.t1_us = 1000100UL;
so6.send_ok = true;
so6.result_known = true;
so6.result.retcode = TRADE_RETCODE_DONE;
so6.result.request_id = 2;
tr5.MarkSendReturn(2, so6);
tr5.ComputeDeadlines(2, 5000000UL, 1000000UL);
tr5.MarkRequest(2, 6300000UL, 2, TRADE_RETCODE_DONE, 0, 0, 0); // T2=6.3s > deadline
tr5.GetMetadata(2, me5);
CRequestTracker::EvaluateMetric(me5, LAB_METRIC_REQUEST_DELAY, mo5, ev5);
AddCheck("EVAL-02", "late-t2-late", "WARNING",
"T2=6.3s > deadline => REQUEST_DELAY LATE (R5-B3)",
StringFormat("cat=%s", LabFitnessName(mo5.category)),
ev5 && mo5.category == LAB_FIT_LATE, "audit-5-R5-B3");
pe.sequence = 3;
tr5.Reserve(pe, sq5);
tr5.MarkSendStart(3, 1000000UL);
SendObservation so7;
so7.Zero();
so7.t1_us = 1000100UL;
so7.send_ok = true;
so7.result_known = true;
so7.result.retcode = TRADE_RETCODE_DONE;
so7.result.request_id = 3;
tr5.MarkSendReturn(3, so7);
tr5.ComputeDeadlines(3, 5000000UL, 1000000UL);
tr5.SetFinal(3, 6200000UL, LAB_OUT_FILLED, LAB_FINAL_TIMER_CHECK, 1); // T6=6.2s
tr5.GetMetadata(3, me5);
CRequestTracker::EvaluateMetric(me5, LAB_METRIC_FINAL_STATE_DELAY, mo5, ev5);
AddCheck("EVAL-03", "late-t6-late", "WARNING",
"T6=6.2s > deadline => FINAL_STATE_DELAY LATE (R5-B3)",
StringFormat("cat=%s", LabFitnessName(mo5.category)),
ev5 && mo5.category == LAB_FIT_LATE, "audit-5-R5-B3");
pe.sequence = 4;
tr5.Reserve(pe, sq5);
tr5.MarkSendStart(4, 1000000UL);
SendObservation so8;
so8.Zero();
so8.t1_us = 1000100UL;
so8.send_ok = true;
so8.result_known = true;
so8.result.retcode = TRADE_RETCODE_DONE;
so8.result.request_id = 4;
tr5.MarkSendReturn(4, so8);
tr5.ComputeDeadlines(4, 5000000UL, 1000000UL);
tr5.MarkRequest(4, 6000000UL, 4, TRADE_RETCODE_DONE, 0, 0, 0); // ровно на границе
tr5.GetMetadata(4, me5);
CRequestTracker::EvaluateMetric(me5, LAB_METRIC_REQUEST_DELAY, mo5, ev5);
AddCheck("EVAL-04", "deadline-boundary-valid", "WARNING",
"end == deadline => VALID (не LATE) (R5-B3)",
StringFormat("cat=%s", LabFitnessName(mo5.category)),
ev5 && mo5.category == LAB_FIT_VALID, "audit-5-R5-B3");
//--- R5-B4: callback-coverage отдельно от торгового результата
CRequestTracker tr6;
2026-09-12 22:14:24 +03:00
tr6.Init(64, "audit5-t5");
RequestPlan pt;
pt.Zero();
pt.sequence = 1;
pt.symbol = "X";
pt.magic = 1;
pt.condition_id = "B";
pt.operation = LAB_OP_MARKET_OPEN;
ulong sq6 = 0;
tr6.Reserve(pt, sq6);
tr6.MarkDealAdd(1, 500, 1001, 0.005); // сделка наблюдалась callback-ом
ConfirmationSnapshot sn6;
sn6.Zero();
sn6.sequence = 1;
sn6.order_ticket = 777;
sn6.deal_tickets[0] = 1001; // есть в callback
sn6.deal_tickets[1] = 2002; // только в истории
sn6.deal_count = 2;
sn6.executed_volume_units = 1.0;
sn6.coverage_complete = true; // объём сошёлся (по истории)
sn6.history_scan_failed = false;
tr6.SyncReconcile(1, sn6, 0.01, 1e-7);
RequestMetadata mt6;
tr6.GetMetadata(1, mt6);
AddCheck("T5-01", "callback-coverage-partial", "WARNING",
"1 callback + 1 history-only => callback_coverage=false (R5-B4)",
StringFormat("trade=%s callback=%s", (mt6.deal_coverage_complete ? "1" : "0"),
(mt6.callback_coverage_complete ? "1" : "0")),
mt6.deal_coverage_complete && !mt6.callback_coverage_complete,
"audit-5-R5-B4");
//--- обе сделки наблюдались callback-ами
tr6.Init(64, "audit5-t5b");
ulong sq6b = 0;
tr6.Reserve(pt, sq6b);
tr6.MarkDealAdd(1, 500, 1001, 0.005);
tr6.MarkDealAdd(1, 900, 2002, 0.005);
ConfirmationSnapshot sn6b;
sn6b.Zero();
sn6b.sequence = 1;
sn6b.order_ticket = 777;
sn6b.deal_tickets[0] = 1001;
sn6b.deal_tickets[1] = 2002;
sn6b.deal_count = 2;
sn6b.executed_volume_units = 1.0;
sn6b.coverage_complete = true;
sn6b.history_scan_failed = false;
tr6.SyncReconcile(1, sn6b, 0.01, 1e-7);
tr6.GetMetadata(1, mt6);
AddCheck("T5-02", "callback-coverage-full", "WARNING",
"обе сделки через callback => callback_coverage=true (R5-B4)",
(mt6.callback_coverage_complete ? "1" : "0"),
mt6.callback_coverage_complete, "audit-5-R5-B4");
//--- R5-B8: FILLED/EXPIRED не равны удалению; PARTIAL_CANCELED явно
ConfirmationSnapshot dp5;
dp5.Zero();
dp5.order_ticket = 70;
dp5.order_state_before = ORDER_STATE_FILLED;
dp5.active_select_status = LAB_READ_ABSENT_CONFIRMED;
dp5.executed_volume_units = 0.0;
CompletionDecision dd5;
dd5.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_DELETE, dp5, 1e-7, dd5);
AddCheck("PDEL-02", "filled-not-deleted", "WARNING",
"FILLED + 0 executed => не PENDING_DELETED (R5-B8)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(dd5.outcome),
(dd5.confirmed ? "1" : "0")),
dd5.outcome != LAB_OUT_PENDING_DELETED && !dd5.confirmed, "audit-5-R5-B8");
ConfirmationSnapshot dp6;
dp6.Zero();
dp6.order_ticket = 71;
dp6.order_state_before = ORDER_STATE_EXPIRED;
dp6.active_select_status = LAB_READ_ABSENT_CONFIRMED;
dp6.executed_volume_units = 0.0;
CompletionDecision dd6;
dd6.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_DELETE, dp6, 1e-7, dd6);
AddCheck("PDEL-03", "expired-not-deleted", "WARNING",
"EXPIRED не подтверждает команду удаления (R5-B8)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(dd6.outcome),
(dd6.confirmed ? "1" : "0")),
dd6.outcome != LAB_OUT_PENDING_DELETED && !dd6.confirmed, "audit-5-R5-B8");
ConfirmationSnapshot pp5;
pp5.Zero();
pp5.order_ticket = 72;
pp5.order_state_before = ORDER_STATE_CANCELED;
pp5.active_select_status = LAB_READ_ABSENT_CONFIRMED;
pp5.requested_volume_units = 2.0;
pp5.executed_volume_units = 1.0;
pp5.remaining_volume_units = 1.0; // неисполненный остаток из истории
pp5.trading_state_known = true;
CompletionDecision pd5;
pd5.Zero();
CCompletionPolicy::Evaluate(LAB_OP_MARKET_OPEN, pp5, 1e-7, pd5);
AddCheck("PART-01", "partial-canceled-explicit", "WARNING",
"executed 1/2, нет активного остатка => PARTIAL_CANCELED (R5-B8)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(pd5.outcome),
(pd5.confirmed ? "1" : "0")),
pd5.confirmed && pd5.outcome == LAB_OUT_PARTIAL_CANCELED, "audit-5-R5-B8");
//--- R5-B8: несовпадение точного типа при равных параметрах
ConfirmationSnapshot pp6;
pp6.Zero();
pp6.order_ticket = 51;
pp6.order_state_before = ORDER_STATE_PLACED;
pp6.active_select_status = LAB_READ_FOUND;
pp6.order_symbol = "X";
pp6.order_magic = 1;
pp6.order_volume_lots = 0.01;
pp6.order_price = 1.1000;
pp6.order_type = ORDER_TYPE_BUY_LIMIT;
pp6.expected_symbol = "X";
pp6.expected_magic = 1;
pp6.expected_volume_lots = 0.01;
pp6.expected_price = 1.1000;
pp6.expected_order_type = ORDER_TYPE_SELL_LIMIT; // тип не совпал
pp6.price_tolerance = 0.00001;
pp6.trading_state_known = true;
CompletionDecision pd6;
pd6.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_CREATE, pp6, 1e-7, pd6);
AddCheck("PEND-03", "pending-type-mismatch", "WARNING",
"другой тип при равных параметрах => нет T6 (R5-B8)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(pd6.outcome),
(pd6.confirmed ? "1" : "0")),
!pd6.confirmed, "audit-5-R5-B8");
//--- R5-S1: малый пилот per_condition=4 => 8 валидных слотов
CExperimentRunner rn5;
LabSettings s5;
s5.Zero();
2026-09-12 22:14:24 +03:00
s5.experiment_id = LAB_EXP_E1;
s5.series_id = 1;
s5.count_per_condition = 4;
s5.warmup_per_condition = 0;
s5.symbol = "X";
s5.magic = 1;
s5.seed = 7;
s5.run_mode = LAB_RUN_LOCAL_ONLY;
LabError p5e;
2026-09-12 22:14:24 +03:00
bool pilot_ok = false;
if(rn5.Init(s5, p5e))
{
2026-09-12 22:14:24 +03:00
pilot_ok = (rn5.SlotCount() == 8);
for(int i = 0; i < rn5.SlotCount() && pilot_ok; i++)
{
ScheduleSlot ss;
2026-09-12 22:14:24 +03:00
if(!rn5.GetSlot(i, ss))
{ pilot_ok = false; break; }
if(ss.plan.sequence == 0 ||
(ss.plan.condition_id != "A" && ss.plan.condition_id != "B") ||
(ss.plan.side != LAB_SIDE_BUY && ss.plan.side != LAB_SIDE_SELL))
{ pilot_ok = false; break; }
}
}
2026-09-12 22:14:24 +03:00
AddCheck("PILOT-01", "small-pilot-slots", "WARNING",
"per_condition=4 => 8 валидных слотов без планов-пустышек (R5-S1)",
(pilot_ok ? "ok" : "failed"), pilot_ok, "audit-5-R5-S1");
//--- R5-S2: нулевой enum (ORDER_TYPE_BUY=0 / ORDER_FILLING_FOK=0) не пусто
CRequestTracker tre;
2026-09-12 22:14:24 +03:00
tre.Init(64, "audit5-enum");
RequestPlan pee;
pee.Zero();
pee.sequence = 1;
pee.symbol = "X";
pee.magic = 1;
pee.condition_id = "A";
pee.operation = LAB_OP_MARKET_OPEN;
pee.actual_known = true;
pee.actual_type = ORDER_TYPE_BUY;
pee.actual_filling = ORDER_FILLING_FOK;
ulong sqe = 0;
tre.Reserve(pee, sqe);
const string enum_csv = CCsvReport::BuildSamples(&tre, "C1", "cfg");
string el[];
const int en = StringSplit(enum_csv, '\n', el);
string ec[];
int ecc = 0;
CCsv::ParseRow(el[1], ec, ecc);
AddCheck("ENUM-01", "zero-enum-not-empty", "WARNING",
"actual_type=0/FOK=0 пишутся как 0, а не пусто (R5-S2)",
StringFormat("at='%s' af='%s'", ec[60], ec[61]),
ec[60] == "0" && ec[61] == "0", "audit-5-R5-S2");
//--- R5-B5: сквозной rebuild tracker -> samples.csv -> RebuildReport
CRequestTracker trr;
2026-09-12 22:14:24 +03:00
trr.Init(128, "audit5-rebuild");
for(int k = 1; k <= 2; k++)
{
2026-09-12 22:14:24 +03:00
RequestPlan pr;
pr.Zero();
pr.sequence = k;
pr.symbol = "X";
pr.magic = 1;
pr.condition_id = (k == 1 ? "A" : "B");
pr.operation = LAB_OP_MARKET_OPEN;
ulong sqr = 0;
trr.Reserve(pr, sqr);
SendObservation sor;
sor.Zero();
sor.t1_us = (ulong)(1000 + k);
sor.send_ok = true;
sor.result_known = true;
sor.result.retcode = TRADE_RETCODE_DONE;
sor.result.request_id = (uint)k;
sor.result.order = (ulong)(7000 + (ulong)k);
trr.MarkSendStart((ulong)k, (ulong)(1000 * (ulong)k));
trr.MarkSendReturn((ulong)k, sor);
trr.MarkRequest((ulong)k, (ulong)(3000 * (ulong)k), (uint)k, TRADE_RETCODE_DONE, 0,
(ulong)(7000 + (ulong)k), 0);
trr.MarkOrderAdd((ulong)k, (ulong)(3200 * (ulong)k), (ulong)(7000 + (ulong)k), "TEST");
trr.MarkDealAdd((ulong)k, (ulong)(3500 * (ulong)k), (ulong)(8000 + (ulong)k), 0.01);
trr.SetDealCoverageComplete((ulong)k);
2026-09-12 22:14:24 +03:00
trr.SetFinal((ulong)k, (ulong)(4000 * (ulong)k), LAB_OUT_FILLED,
LAB_FINAL_TIMER_CHECK, (ulong)k);
trr.SetTradingStateKnown((ulong)k, true);
trr.SetDeadlinesFromSample((ulong)k, 5000000UL, 1000000UL);
}
2026-09-12 22:14:24 +03:00
const string e2e_samples = CCsvReport::BuildSamples(&trr, "C1", "cfg");
const string e2e_path = "RequestLatencyLab\\tests\\samples_e2e.csv";
LabError e2e_err;
2026-09-12 22:14:24 +03:00
const bool e2e_saved = CCsv::SaveUtf8(e2e_path, e2e_samples, e2e_err);
FolderCreate("RequestLatencyLab\\tests\\rebuilt_e2e");
2026-09-12 22:14:24 +03:00
const bool e2e_ok = e2e_saved &&
CReportBuilder::RebuildReport(e2e_path,
"RequestLatencyLab\\tests\\rebuilt_e2e\\", e2e_err);
bool rows_ok = false;
if(e2e_ok)
{
string rc;
if(CCsv::LoadUtf8("RequestLatencyLab\\tests\\rebuilt_e2e\\summary_rebuilt.csv",
2026-09-12 22:14:24 +03:00
rc, e2e_err))
{
2026-09-12 22:14:24 +03:00
string rl[];
const int rn = StringSplit(rc, '\n', rl);
int rrows = 0;
for(int i = 0; i < rn; i++)
{
2026-09-12 22:14:24 +03:00
string t = rl[i];
StringReplace(t, "\r", "");
if(StringLen(t) > 0)
rrows++;
}
//--- 3 группы x (A+B+combined) x 8 метрик = 72 строки + заголовок
2026-09-12 22:14:24 +03:00
rows_ok = (rrows == 73);
}
}
2026-09-12 22:14:24 +03:00
AddCheck("REBUILD-01", "rebuild-roundtrip", "WARNING",
"tracker->samples->RebuildReport успешен; summary_rebuilt=73 (R5-B5)",
StringFormat("saved=%s ok=%s rows=%s err=%s", (e2e_saved ? "1" : "0"),
(e2e_ok ? "1" : "0"), (rows_ok ? "73" : "?"), e2e_err.message),
e2e_saved && e2e_ok && rows_ok, "audit-5-R5-B5");
//--- R6-B2: offline summary по MAIN выборке (без WARMUP) — n/счётчики совпадают
//--- с онлайн-семантикой независимо от способа чтения (прямые структуры).
RebuiltSample or2[2];
2026-09-12 22:14:24 +03:00
ZeroMemory(or2[0]);
ZeroMemory(or2[1]);
or2[0].sequence = 1;
or2[0].condition_id = "A";
or2[0].role = "MAIN";
or2[0].is_warmup = false;
or2[0].operation = "MARKET_OPEN";
or2[0].outcome = "FILLED";
or2[0].present_mask = LAB_MASK_T0 | LAB_MASK_T1 | LAB_MASK_T2 | LAB_MASK_T6;
or2[0].t0 = 1000000UL;
or2[0].t1 = 1000100UL;
or2[0].t2 = 1100000UL;
or2[0].t6 = 1200000UL;
or2[0].observation_deadline_us = 6000000UL;
or2[0].completed = true;
or2[1].sequence = 2;
or2[1].condition_id = "B";
or2[1].role = "MAIN";
or2[1].is_warmup = false;
or2[1].operation = "MARKET_OPEN";
or2[1].outcome = "FILLED";
or2[1].present_mask = LAB_MASK_T0 | LAB_MASK_T1 | LAB_MASK_T2 | LAB_MASK_T6;
or2[1].t0 = 2000000UL;
or2[1].t1 = 2000100UL;
or2[1].t2 = 2200000UL;
or2[1].t6 = 2400000UL;
or2[1].observation_deadline_us = 7000000UL;
or2[1].completed = true;
StatSummaryRow offA;
2026-09-12 22:14:24 +03:00
CReportBuilder::SummarizeGroup(or2, 2, LAB_METRIC_REQUEST_DELAY, "", "ALL", offA);
AddCheck("REBUILD-02", "offline-vs-online-population", "WARNING",
"offline summary по MAIN без фильтра условия обрабатывает обе записи (R6-B2)",
StringFormat("n=%I64u", offA.n_attempted),
offA.n_attempted == 2, "audit-6-R6-B2");
//--- R5-B7: RebuildE4 отвергает повтор одного каталога (500 из одного файла)
string dirs[5];
2026-09-12 22:14:24 +03:00
dirs[0] = "RequestLatencyLab\\tests\\rebuilt_e2e";
dirs[1] = "RequestLatencyLab\\tests\\rebuilt_e2e";
dirs[2] = "RequestLatencyLab\\tests\\rebuilt_e2e";
dirs[3] = "RequestLatencyLab\\tests\\rebuilt_e2e";
dirs[4] = "RequestLatencyLab\\tests\\rebuilt_e2e";
LabError e4err;
2026-09-12 22:14:24 +03:00
const bool e4_dup = !CReportBuilder::RebuildE4(dirs, 5, "E4-POOLED",
"RequestLatencyLab\\tests\\rebuilt_e4\\", e4err);
AddCheck("E4-01", "e4-duplicate-dataset", "WARNING",
"5 одинаковых каталогов отвергаются (R5-B7)",
StringFormat("rejected=%s err=%s", (e4_dup ? "1" : "0"), e4err.message),
e4_dup && e4err.code == 23, "audit-5-R5-B7");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R6 |
//+------------------------------------------------------------------+
void TestAudit6Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- R6-B3: знаковая метрика T2-T4 — своевременность по ОБЕИМ точкам
CRequestTracker tr6s;
2026-09-12 22:14:24 +03:00
tr6s.Init(64, "audit6-sign");
RequestPlan ph;
ph.Zero();
ph.sequence = 1;
ph.symbol = "X";
ph.magic = 1;
ph.condition_id = "B";
ph.operation = LAB_OP_MARKET_OPEN;
ulong sq6s = 0;
tr6s.Reserve(ph, sq6s);
SendObservation sh;
sh.Zero();
sh.t1_us = 1000100UL;
sh.send_ok = true;
sh.result_known = true;
sh.result.retcode = TRADE_RETCODE_DONE;
sh.result.request_id = 1;
tr6s.MarkSendStart(1, 1000000UL);
tr6s.MarkSendReturn(1, sh);
tr6s.ComputeDeadlines(1, 5000000UL, 1000000UL); // deadline=6s
tr6s.MarkRequest(1, 6300000UL, 1, TRADE_RETCODE_DONE, 0, 0, 0); // T2=6.3s late
tr6s.MarkDealAdd(1, 1100000UL, 9001, 0.01); // T4=1.1s early
RequestMetadata m6s;
tr6s.GetMetadata(1, m6s);
MetricObservation mo6s;
bool ev6s = false;
CRequestTracker::EvaluateMetric(m6s, LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET, mo6s, ev6s);
AddCheck("EVAL-05", "signed-t2late-t4early", "WARNING",
"T2=6.3s поздняя, T4 ранняя => знаковая LATE (R6-B3)",
StringFormat("cat=%s v=%I64d", LabFitnessName(mo6s.category), mo6s.value_us),
ev6s && mo6s.category == LAB_FIT_LATE, "audit-6-R6-B3");
ph.sequence = 2;
tr6s.Reserve(ph, sq6s);
SendObservation sh2;
sh2.Zero();
sh2.t1_us = 1000100UL;
sh2.send_ok = true;
sh2.result_known = true;
sh2.result.retcode = TRADE_RETCODE_DONE;
sh2.result.request_id = 2;
tr6s.MarkSendStart(2, 1000000UL);
tr6s.MarkSendReturn(2, sh2);
tr6s.ComputeDeadlines(2, 5000000UL, 1000000UL);
tr6s.MarkRequest(2, 1100000UL, 2, TRADE_RETCODE_DONE, 0, 0, 0); // T2 ранняя
tr6s.MarkDealAdd(2, 6300000UL, 9002, 0.01); // T4=6.3s late
RequestMetadata m6s2;
tr6s.GetMetadata(2, m6s2);
MetricObservation mo6s2;
bool ev6s2 = false;
CRequestTracker::EvaluateMetric(m6s2, LAB_METRIC_REQUEST_FIRST_DEAL_OFFSET, mo6s2, ev6s2);
AddCheck("EVAL-06", "signed-t2early-t4late", "WARNING",
"T2 ранняя, T4=6.3s поздняя => знаковая LATE (R6-B3)",
StringFormat("cat=%s v=%I64d", LabFitnessName(mo6s2.category), mo6s2.value_us),
ev6s2 && mo6s2.category == LAB_FIT_LATE, "audit-6-R6-B3");
//--- R6-B3: поздняя T6 до первого таймера всё равно ставит deadline_exceeded
ph.sequence = 3;
tr6s.Reserve(ph, sq6s);
SendObservation sh3;
sh3.Zero();
sh3.t1_us = 1000100UL;
sh3.send_ok = true;
sh3.result_known = true;
sh3.result.retcode = TRADE_RETCODE_DONE;
sh3.result.request_id = 3;
tr6s.MarkSendStart(3, 1000000UL);
tr6s.MarkSendReturn(3, sh3);
tr6s.ComputeDeadlines(3, 5000000UL, 1000000UL);
tr6s.SetFinal(3, 6200000UL, LAB_OUT_FILLED, LAB_FINAL_CALLBACK_CHECK, 11); // T6=6.2s
RequestMetadata m6s3;
tr6s.GetMetadata(3, m6s3);
AddCheck("EVAL-07", "late-t6-before-timer-sets-flag", "WARNING",
"поздняя T6 (до таймера) => deadline_exceeded=true (R6-B3)",
StringFormat("de=%s", (m6s3.deadline_exceeded ? "1" : "0")),
m6s3.deadline_exceeded, "audit-6-R6-B3");
//--- R6-B1: light-сверка и history->callback
CRequestTracker trl;
2026-09-12 22:14:24 +03:00
trl.Init(64, "audit6-light");
RequestPlan pl;
pl.Zero();
pl.sequence = 1;
pl.symbol = "X";
pl.magic = 1;
pl.condition_id = "A";
pl.operation = LAB_OP_MARKET_OPEN;
ulong sql = 0;
trl.Reserve(pl, sql);
trl.MarkDealAdd(1, 500, 1001, 0.01); // callback наблюдён
ConfirmationSnapshot snl;
snl.Zero();
snl.sequence = 1;
snl.order_ticket = 55;
snl.light_snapshot = true; // R6-B1: явный признак
snl.deal_count = 1; // count>0, тикеты 0
snl.executed_volume_units = 1.0; // 0.01 lot / step 0.01
snl.coverage_complete = true;
snl.history_scan_failed = false;
trl.SyncReconcile(1, snl, 0.01, 1e-7);
RequestMetadata ml;
trl.GetMetadata(1, ml);
AddCheck("DL-01", "light-snapshot-coverage", "WARNING",
"light count=1 tickets=0 => callback_coverage=true (R6-B1)",
StringFormat("cov=%s vol=%.6f", (ml.callback_coverage_complete ? "1" : "0"),
ml.executed_volume),
ml.callback_coverage_complete && Near(ml.executed_volume, 1.0, 1e-9),
"audit-6-R6-B1");
//--- history -> первый callback учитывается ровно один раз
CRequestTracker trh;
2026-09-12 22:14:24 +03:00
trh.Init(64, "audit6-hcb");
RequestPlan pc;
pc.Zero();
pc.sequence = 1;
pc.symbol = "X";
pc.magic = 1;
pc.condition_id = "A";
pc.operation = LAB_OP_MARKET_OPEN;
ulong sqh = 0;
trh.Reserve(pc, sqh);
trh.RegisterHistoryDeal(777, 55, 0.01, 1000, 1);
trh.MarkDealAdd(1, 500, 777, 0.01); // первый callback после истории
RequestMetadata mh;
trh.GetMetadata(1, mh);
AddCheck("DL-02", "history-then-first-callback", "WARNING",
"первый callback после истории: deal_count=1, volume=0.01 (R6-B1)",
StringFormat("count=%u vol=%.6f", mh.deal_count, mh.callback_volume),
mh.deal_count == 1 && Near(mh.callback_volume, 0.01, 1e-12),
"audit-6-R6-B1");
//--- callback -> история дополняет без потери происхождения
CRequestTracker trex;
2026-09-12 22:14:24 +03:00
trex.Init(64, "audit6-enrich");
ulong sqx = 0;
trex.Reserve(pc, sqx);
trex.MarkDealAdd(1, 500, 888, 0.005);
const bool enriched = trex.RegisterHistoryDeal(888, 55, 0.005, 2000, 1);
DealRecord drx;
2026-09-12 22:14:24 +03:00
const bool has_drx = trex.GetDeal(0, drx);
AddCheck("DL-03", "history-enrich-callback", "WARNING",
"callback-сделка дополняется историей без потери (R6-B1)",
StringFormat("enriched=%s cb=%s hv=%.6f", (enriched ? "1" : "0"),
(has_drx && drx.has_callback ? "1" : "0"),
(has_drx ? drx.history_volume : -1)),
enriched && has_drx && drx.has_callback &&
Near(drx.history_volume, 0.005, 1e-12), "audit-6-R6-B1");
//--- R6-S4: переполнение реестра сделок — явный отказ
CRequestTracker trO;
2026-09-12 22:14:24 +03:00
trO.Init(2, "audit6-dealcap");
ulong sqo = 0;
trO.Reserve(pc, sqo);
const bool od1 = trO.MarkDealAdd(1, 100, 1, 0.01);
const bool od2 = trO.MarkDealAdd(1, 200, 2, 0.01);
const bool od3 = trO.MarkDealAdd(1, 300, 3, 0.01); // ёмкость 2 => отказ
RequestMetadata moD;
trO.GetMetadata(1, moD);
AddCheck("DL-04", "deal-registry-overflow-explicit", "WARNING",
"переполнение реестра сделок => явный отказ, счётчик потерь (R6-S4)",
StringFormat("d1=%s d2=%s d3=%s lost=%d rec_count=%u",
(od1 ? "1" : "0"), (od2 ? "1" : "0"), (od3 ? "1" : "0"),
trO.DealLostCount(), moD.deal_count),
od1 && od2 && !od3 && trO.DealLostCount() == 1 && moD.deal_count == 2,
"audit-6-R6-S4");
//--- R6-B2: offline исключает WARMUP; счётчики/сроки как онлайн
RebuiltSample rw1, rw2;
ZeroMemory(rw1);
ZeroMemory(rw2);
rw1.sequence = 1;
rw1.condition_id = "A";
rw1.role = "MAIN";
rw1.is_warmup = false;
rw1.operation = "MARKET_OPEN";
rw1.outcome = "FILLED";
rw1.present_mask = LAB_MASK_T0 | LAB_MASK_T1 | LAB_MASK_T2 | LAB_MASK_T6;
rw1.t0 = 1000000UL;
rw1.t1 = 1000100UL;
rw1.t2 = 1100000UL;
rw1.t6 = 1200000UL;
rw1.completed = true;
rw1.observation_deadline_us = 6000000UL;
rw2.sequence = 2;
rw2.condition_id = "A";
rw2.role = "WARMUP";
rw2.is_warmup = true;
rw2.operation = "MARKET_OPEN";
rw2.outcome = "FILLED";
rw2.present_mask = LAB_MASK_T0 | LAB_MASK_T1 | LAB_MASK_T2 | LAB_MASK_T6;
rw2.t0 = 1000000UL;
rw2.t1 = 1000100UL;
rw2.t2 = 3000000UL;
rw2.t6 = 3200000UL;
rw2.completed = true;
rw2.observation_deadline_us = 6000000UL;
RebuiltSample arr2[2];
2026-09-12 22:14:24 +03:00
arr2[0] = rw1;
arr2[1] = rw2;
StatSummaryRow sr6;
2026-09-12 22:14:24 +03:00
CReportBuilder::SummarizeGroup(arr2, 2, LAB_METRIC_REQUEST_DELAY, "A", "ALL", sr6);
AddCheck("OFF-01", "offline-excludes-warmup", "WARNING",
"MAIN 100ms + WARMUP 2000ms => offline n=1, mean=100ms (R6-B2)",
StringFormat("n=%I64u mean=%.1f", sr6.n_attempted, sr6.mean),
sr6.n_attempted == 1 && sr6.has_stats && Near(sr6.mean, 100000.0, 1.0),
"audit-6-R6-B2");
//--- R6-B2: histogram-фильтр использует конец метрики (не общий late)
RebuiltSample rl2;
ZeroMemory(rl2);
2026-09-12 22:14:24 +03:00
rl2.sequence = 3;
rl2.condition_id = "A";
rl2.role = "MAIN";
rl2.is_warmup = false;
rl2.operation = "MARKET_OPEN";
rl2.outcome = "FILLED";
rl2.present_mask = LAB_MASK_T0 | LAB_MASK_T1 | LAB_MASK_T2 | LAB_MASK_T6;
rl2.t0 = 1000000UL;
rl2.t1 = 1000100UL;
rl2.t2 = 6300000UL;
rl2.t6 = 6000000UL;
rl2.observation_deadline_us = 6000000UL;
rl2.deadline_exceeded = true;
rl2.completed = true;
MetricObservation mo2;
bool ev2 = false;
CReportBuilder::EvaluateRebuilt(rl2, LAB_METRIC_REQUEST_DELAY, mo2, ev2);
AddCheck("OFF-02", "offline-late-by-metric-end", "WARNING",
"T2=6.3s late => REQUEST_DELAY LATE, в гистограмму не идёт (R6-B2)",
StringFormat("cat=%s", LabFitnessName(mo2.category)),
ev2 && mo2.category == LAB_FIT_LATE, "audit-6-R6-B2");
//--- R6-B4: квоты E3 и сбалансированные блоки
LabSettings e3s;
e3s.Zero();
e3s.experiment_id = LAB_EXP_E3;
e3s.series_id = 1;
e3s.count_per_condition = 40;
e3s.warmup_per_condition = 0;
e3s.symbol = "X";
e3s.magic = 1;
e3s.seed = 5;
e3s.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner e3r;
LabError e3e;
2026-09-12 22:14:24 +03:00
bool e3init = e3r.Init(e3s, e3e);
bool q1st = false;
if(e3init)
{
2026-09-12 22:14:24 +03:00
for(int k = 0; k < 40; k++)
{
RequestPlan pp;
2026-09-12 22:14:24 +03:00
if(!e3r.TryDispatchByRegime(LAB_REGIME_QUIET, pp))
break;
e3r.MarkDispatchedSlot(pp.sequence);
e3r.CountDispatched(pp.role);
}
2026-09-12 22:14:24 +03:00
//--- одна ветвь заполнена, другая нет => НЕ MainComplete (R6-B4)
q1st = !e3r.MainComplete() && e3r.MainDispatched() == 40 && !e3r.E3QuotaComplete();
bool all_fast = true;
for(int k = 0; k < 40; k++)
{
RequestPlan pp;
2026-09-12 22:14:24 +03:00
if(!e3r.TryDispatchByRegime(LAB_REGIME_FAST, pp))
{ all_fast = false; break; }
e3r.MarkDispatchedSlot(pp.sequence);
e3r.CountDispatched(pp.role);
}
2026-09-12 22:14:24 +03:00
q1st = q1st && all_fast && e3r.E3QuotaComplete();
}
2026-09-12 22:14:24 +03:00
AddCheck("E3-02", "e3-quota-not-watermark", "WARNING",
"одна ветвь E3 заполнена => MainComplete=false; после обеих true (R6-B4)",
StringFormat("init=%s q1=%s disp=%I64u", (e3init ? "1" : "0"),
(q1st ? "1" : "0"), e3r.MainDispatched()),
e3init && q1st && e3r.MainDispatched() == 80, "audit-6-R6-B4");
LabSettings bs;
bs.Zero();
bs.experiment_id = LAB_EXP_E1;
bs.series_id = 1;
bs.count_per_condition = 100;
bs.warmup_per_condition = 0;
bs.symbol = "X";
bs.magic = 1;
bs.seed = 11;
bs.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner br;
LabError be;
2026-09-12 22:14:24 +03:00
bool blocks_ok = false;
if(br.Init(bs, be))
{
2026-09-12 22:14:24 +03:00
blocks_ok = true;
for(int b = 1; b <= 10 && blocks_ok; b++)
{
2026-09-12 22:14:24 +03:00
int a_n = 0, b_n = 0, a_buy = 0, a_sell = 0, b_buy = 0, b_sell = 0;
for(int i = 0; i < br.SlotCount(); i++)
{
ScheduleSlot ss;
2026-09-12 22:14:24 +03:00
if(!br.GetSlot(i, ss) || ss.plan.block_id != (uint)b)
continue;
2026-09-12 22:14:24 +03:00
if(ss.plan.condition_id == "A")
{ a_n++; if(ss.plan.side == LAB_SIDE_BUY) a_buy++; else a_sell++; }
else
2026-09-12 22:14:24 +03:00
{ b_n++; if(ss.plan.side == LAB_SIDE_BUY) b_buy++; else b_sell++; }
}
2026-09-12 22:14:24 +03:00
if(!(a_n == 10 && b_n == 10 && a_buy == 5 && a_sell == 5 && b_buy == 5 && b_sell == 5))
blocks_ok = false;
}
}
2026-09-12 22:14:24 +03:00
AddCheck("BLOCK-01", "balanced-blocks-10A10B", "WARNING",
"каждый из 10 блоков: 10A+10B, 5Buy+5Sell на ветвь (R6-B4)",
(blocks_ok ? "ok" : "bad"), blocks_ok, "audit-6-R6-B4");
//--- R6-B4: прогрев только по фактическим отправкам
LabSettings ws;
ws.Zero();
ws.experiment_id = LAB_EXP_E1;
ws.series_id = 1;
ws.count_per_condition = 10;
ws.warmup_per_condition = 10;
ws.symbol = "X";
ws.magic = 1;
ws.seed = 3;
ws.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner wr;
LabError we;
2026-09-12 22:14:24 +03:00
bool warm_rej = false, warm_done = false;
if(wr.Init(ws, we))
{
2026-09-12 22:14:24 +03:00
//--- все 20 прогревочных планов получают систематический отказ precheck
RequestPlan wp;
while(wr.NextWarmup(wp))
{
2026-09-12 22:14:24 +03:00
for(int t = 0; t < 3; t++)
wr.MarkWarmupRejected(wp.sequence);
}
2026-09-12 22:14:24 +03:00
warm_rej = (wr.WarmupComplete() && wr.WarmupDispatched() < wr.WarmupRequired());
//--- нормальный прогон: фактические отправки закрывают квоту
CExperimentRunner w2;
LabError we2;
2026-09-12 22:14:24 +03:00
if(w2.Init(ws, we2))
{
RequestPlan wp2;
while(w2.NextWarmup(wp2))
{
w2.MarkWarmupDispatched(wp2.sequence);
w2.CountDispatched(wp2.role);
}
2026-09-12 22:14:24 +03:00
warm_done = (w2.WarmupComplete() &&
w2.WarmupDispatched() == w2.WarmupRequired());
}
}
2026-09-12 22:14:24 +03:00
AddCheck("WARM-01", "warmup-actual-dispatch", "WARNING",
"все precheck отклонены => не «прогрев выполнен»; факт.send => да (R6-B4)",
StringFormat("rej=%s done=%s", (warm_rej ? "1" : "0"), (warm_done ? "1" : "0")),
warm_rej && warm_done, "audit-6-R6-B4");
//--- R6-B5: согласование CompletionPolicy/контроллер
ConfirmationSnapshot dp7;
dp7.Zero();
dp7.order_ticket = 73;
dp7.order_state_before = ORDER_STATE_CANCELED;
dp7.active_select_status = LAB_READ_ABSENT_CONFIRMED;
dp7.executed_volume_units = 0.0;
dp7.history_scan_failed = true; // скан истории не удался
CompletionDecision dd7;
dd7.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_DELETE, dp7, 1e-7, dd7);
AddCheck("PDEL-04", "cancel-no-history-scan", "WARNING",
"отмена при failed history scan => НЕ подтверждена (R6-B5)",
StringFormat("outcome=%s conf=%s", LabOutcomeName(dd7.outcome),
(dd7.confirmed ? "1" : "0")),
!dd7.confirmed, "audit-6-R6-B5");
ConfirmationSnapshot pp7;
pp7.Zero();
pp7.order_ticket = 74;
pp7.order_state_before = ORDER_STATE_FILLED;
pp7.executed_volume_units = 0.5;
pp7.trading_state_known = true;
CompletionDecision pd7;
pd7.Zero();
CCompletionPolicy::Evaluate(LAB_OP_PENDING_CREATE, pp7, 1e-7, pd7);
AddCheck("PEND-04", "unexpected-activation-outcome", "WARNING",
"активация до подтверждения: confirmed=true, T6 не назначается (R6-B5)",
StringFormat("outcome=%s conf=%s t6=%s", LabOutcomeName(pd7.outcome),
(pd7.confirmed ? "1" : "0"), (pd7.t6_assignable ? "1" : "0")),
pd7.outcome == LAB_OUT_UNEXPECTED_ACTIVATION && pd7.confirmed &&
!pd7.t6_assignable, "audit-6-R6-B5");
//--- R6-S1: строгий CSV-reader
RequestPlan prs;
prs.Zero();
prs.sequence = 1;
prs.symbol = "X";
prs.magic = 1;
prs.condition_id = "A";
prs.operation = LAB_OP_MARKET_OPEN;
CRequestTracker trs;
2026-09-12 22:14:24 +03:00
trs.Init(8, "audit6-reader");
ulong sqs = 0;
trs.Reserve(prs, sqs);
const string good_csv = CCsvReport::BuildSamples(&trs, "C1", "cfg");
LabError rerr;
2026-09-12 22:14:24 +03:00
//--- нечисловой токен в present_mask (col 37)
string bl[];
2026-09-12 22:14:24 +03:00
const int bn = StringSplit(good_csv, '\n', bl);
string hh = bl[0];
StringReplace(hh, "\r", "");
string rr = bl[1];
StringReplace(rr, "\r", "");
string rc[];
2026-09-12 22:14:24 +03:00
const int rn = StringSplit(rr, ',', rc);
rc[37] = "abc";
string row_bad = "";
for(int k = 0; k < rn; k++)
{ if(k > 0) row_bad += ","; row_bad += rc[k]; }
const bool bad_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_bad.csv",
hh + "\n" + row_bad + "\n", rerr);
RebuiltSample nb[8];
2026-09-12 22:14:24 +03:00
int nbad = -99;
if(bad_saved)
2026-09-12 22:14:24 +03:00
nbad = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_bad.csv",
nb, 8, rerr);
AddCheck("READER-01", "csv-numeric-validation", "WARNING",
"нечисловой токен в samples.csv => отказ (R6-S1)",
StringFormat("n=%d err=%d", nbad, rerr.code),
bad_saved && nbad < 0 && rerr.code == 34, "audit-6-R6-S1");
//--- дубликат sequence
const bool dup_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_dup.csv",
hh + "\n" + rr + "\n" + rr + "\n", rerr);
RebuiltSample nd[8];
2026-09-12 22:14:24 +03:00
int ndup = -99;
if(dup_saved)
2026-09-12 22:14:24 +03:00
ndup = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_dup.csv",
nd, 8, rerr);
AddCheck("READER-02", "csv-duplicate-key", "WARNING",
"повтор sequence => отказ (R6-S1)",
StringFormat("n=%d err=%d", ndup, rerr.code),
dup_saved && ndup < 0 && rerr.code == 35, "audit-6-R6-S1");
//--- R6-B7: E4 требует manifest каждого источника
FolderCreate("RequestLatencyLab\\tests\\e4_nomani_1");
FolderCreate("RequestLatencyLab\\tests\\e4_nomani_2");
FolderCreate("RequestLatencyLab\\tests\\e4_nomani_3");
FolderCreate("RequestLatencyLab\\tests\\e4_nomani_4");
FolderCreate("RequestLatencyLab\\tests\\e4_nomani_5");
string e4dirs[5];
2026-09-12 22:14:24 +03:00
e4dirs[0] = "RequestLatencyLab\\tests\\e4_nomani_1";
e4dirs[1] = "RequestLatencyLab\\tests\\e4_nomani_2";
e4dirs[2] = "RequestLatencyLab\\tests\\e4_nomani_3";
e4dirs[3] = "RequestLatencyLab\\tests\\e4_nomani_4";
e4dirs[4] = "RequestLatencyLab\\tests\\e4_nomani_5";
LabError ne4;
2026-09-12 22:14:24 +03:00
const bool e4_nom = !CReportBuilder::RebuildE4(e4dirs, 5, "E4-POOLED",
"RequestLatencyLab\\tests\\rebuilt_e4b\\", ne4);
AddCheck("E4-02", "e4-requires-manifest", "WARNING",
"отсутствующий manifest у источника => отказ (R6-B7)",
StringFormat("rejected=%s err=%d", (e4_nom ? "1" : "0"), ne4.code),
e4_nom && ne4.code == 36, "audit-6-R6-B7");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R7 |
//+------------------------------------------------------------------+
void TestAudit7Regressions(void)
{
LabError a7e;
2026-09-12 22:14:24 +03:00
//--- R7-B1: durable-журнал — правильные байты, две записи, без NUL
FolderCreate("RequestLatencyLab\\tests");
2026-09-12 22:14:24 +03:00
const string dpath = "RequestLatencyLab\\tests\\intents_r7.csv";
if(FileIsExist(dpath))
FileDelete(dpath);
2026-09-12 22:14:24 +03:00
const bool d1 = CAppController::AppendDurableLine(dpath,
"S1,1,INTENT,1000000\r\n", a7e);
const bool d2 = CAppController::AppendDurableLine(dpath,
"S1,2,SEND_RETURN,1000100\r\n", a7e);
string dcontent;
2026-09-12 22:14:24 +03:00
bool no_nul = true;
const bool dloaded = CCsv::LoadUtf8(dpath, dcontent, a7e);
int dlc = 0;
string dlines[];
if(dloaded)
{
2026-09-12 22:14:24 +03:00
dlc = StringSplit(dcontent, '\n', dlines);
for(int k = 0; k < dlc && no_nul; k++)
{
2026-09-12 22:14:24 +03:00
const int l2 = StringLen(dlines[k]);
for(int c2 = 0; c2 < l2; c2++)
if(StringGetCharacter(dlines[k], c2) == 0)
{ no_nul = false; break; }
}
}
2026-09-12 22:14:24 +03:00
const bool dura_ok = (d1 && d2 && dloaded && no_nul &&
StringFind(dcontent, "S1,1,INTENT,1000000") >= 0 &&
StringFind(dcontent, "S1,2,SEND_RETURN,1000100") >= 0);
AddCheck("DURA-01", "durable-intent-roundtrip", "WARNING",
"INTENT+SEND_RETURN: 2 записи, точные байты, без NUL (R7-B1)",
StringFormat("d1=%s d2=%s load=%s nul=%s lines=%d",
(d1 ? "1" : "0"), (d2 ? "1" : "0"), (dloaded ? "1" : "0"),
(no_nul ? "0" : "1"), dlc),
dura_ok, "audit-7-R7-B1");
//--- отказ записи: имя с запрещённым символом ':' => FileOpen недоступен
const bool d_bad = !CAppController::AppendDurableLine(
"RequestLatencyLab\\tests\\bad:name_r7.intents",
"S1,3,INTENT,2000000\r\n", a7e);
AddCheck("DURA-02", "durable-line-fail-closed", "WARNING",
"запись при недоступном FileOpen => false (R7-B1)",
(d_bad ? "rejected" : "accepted"), d_bad, "audit-7-R7-B1");
//--- R7-B3: прогрев — смешанный исход 19 dispatched + 1 rejected
LabSettings w7;
w7.Zero();
w7.experiment_id = LAB_EXP_E1;
w7.series_id = 1;
w7.count_per_condition = 10;
w7.warmup_per_condition = 10;
w7.symbol = "X";
w7.magic = 1;
w7.seed = 7;
w7.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner r7;
LabError w7e;
CRequestTracker t7;
CEventJournal j7;
CTimeSource c7;
c7.UseLive();
2026-09-12 22:14:24 +03:00
const bool w7init = (r7.Init(w7, w7e) && t7.Init(64, "audit7-warm") &&
j7.Init(256, "audit7-warm"));
CAppController a7;
2026-09-12 22:14:24 +03:00
a7.Configure(&t7, &j7, &r7, &c7);
a7.SetSettings(w7);
2026-09-12 22:14:24 +03:00
bool w7mixed = false;
if(w7init)
{
2026-09-12 22:14:24 +03:00
int sent7 = 0;
RequestPlan wp7;
while(r7.NextWarmup(wp7))
{
2026-09-12 22:14:24 +03:00
if(sent7 < 19)
{
r7.MarkWarmupDispatched(wp7.sequence);
r7.CountDispatched(wp7.role);
}
else
{
2026-09-12 22:14:24 +03:00
for(int t = 0; t < 3; t++)
r7.MarkWarmupRejected(wp7.sequence);
}
sent7++;
}
a7.ReturnToSchedule();
2026-09-12 22:14:24 +03:00
w7mixed = (a7.State() == LAB_STATE_FINISHED &&
a7.TerminationReason() == "WARMUP_INSUFFICIENT_DATA");
}
2026-09-12 22:14:24 +03:00
AddCheck("WARM-02", "warmup-mixed-shortfall", "WARNING",
"19 dispatched + 1 precheck-rejected => НЕ MAIN (R7-B3)",
StringFormat("state=%s reason=%s", a7.StateName(),
a7.TerminationReason()),
w7init && w7mixed, "audit-7-R7-B3");
//--- контраст: все 20 фактически отправлены => MAIN
CExperimentRunner r8;
LabError w8e;
CRequestTracker t8;
CEventJournal j8;
CTimeSource c8;
c8.UseLive();
2026-09-12 22:14:24 +03:00
const bool w8init = (r8.Init(w7, w8e) && t8.Init(64, "audit7-warm2") &&
j8.Init(256, "audit7-warm2"));
CAppController a8;
2026-09-12 22:14:24 +03:00
a8.Configure(&t8, &j8, &r8, &c8);
a8.SetSettings(w7);
2026-09-12 22:14:24 +03:00
bool w8main = false;
if(w8init)
{
RequestPlan wp8;
while(r8.NextWarmup(wp8))
{
r8.MarkWarmupDispatched(wp8.sequence);
r8.CountDispatched(wp8.role);
}
a8.ReturnToSchedule();
2026-09-12 22:14:24 +03:00
w8main = (a8.State() == LAB_STATE_MAIN);
}
2026-09-12 22:14:24 +03:00
AddCheck("WARM-03", "warmup-full-quota-main", "WARNING",
"20/20 фактически отправлены => MAIN (R7-B3)",
StringFormat("state=%s", a8.StateName()),
w8init && w8main, "audit-7-R7-B3");
//--- R7-B2: история опережает callback — поздняя сделка дополняет T5;
//--- неполный скан не подтверждает полноту T5 по подмножеству
CRequestTracker trcb;
2026-09-12 22:14:24 +03:00
trcb.Init(64, "audit7-cb");
RequestPlan pcb;
pcb.Zero();
pcb.sequence = 1;
pcb.symbol = "X";
pcb.magic = 1;
pcb.condition_id = "A";
pcb.operation = LAB_OP_MARKET_OPEN;
ulong scb = 0;
trcb.Reserve(pcb, scb);
trcb.RegisterHistoryDeal(3001, 80, 0.01, 1000, 1);
trcb.RegisterHistoryDeal(3002, 80, 0.01, 1000, 1);
trcb.MarkDealAdd(1, 500, 3001, 0.01); // callback только первого
ConfirmationSnapshot sc1;
sc1.Zero();
sc1.sequence = 1;
sc1.order_ticket = 80;
sc1.deal_tickets[0] = 3001;
sc1.deal_tickets[1] = 3002;
sc1.deal_count = 2;
sc1.executed_volume_units = 2.0;
sc1.coverage_complete = true;
sc1.history_scan_failed = false;
trcb.SyncReconcile(1, sc1, 0.01, 1e-7);
RequestMetadata mcb1;
trcb.GetMetadata(1, mcb1);
const bool not_covered = (!mcb1.callback_coverage_complete);
//--- поздняя сделка ВНУТРИ окна (после «T6», до grace)
trcb.MarkDealAdd(1, 900, 3002, 0.01);
ConfirmationSnapshot sc2;
sc2.Zero();
sc2.sequence = 1;
sc2.order_ticket = 80;
sc2.deal_tickets[0] = 3001;
sc2.deal_tickets[1] = 3002;
sc2.deal_count = 2;
sc2.executed_volume_units = 2.0;
sc2.coverage_complete = true;
sc2.history_scan_failed = false;
trcb.SyncReconcile(1, sc2, 0.01, 1e-7);
RequestMetadata mcb2;
trcb.GetMetadata(1, mcb2);
const bool covered_late = (mcb2.callback_coverage_complete &&
(mcb2.present_mask & (uint)LAB_MASK_T5) != 0 &&
mcb2.last_deal_us == 900);
AddCheck("T5-03", "late-deal-fills-coverage", "WARNING",
"история->1 callback: coverage=false; поздний в окне => true, T5 по исходному времени (R7-B2)",
StringFormat("cov1=%s cov2=%s t5=%I64u",
(not_covered ? "0" : "1"), (covered_late ? "1" : "0"),
mcb2.last_deal_us),
not_covered && covered_late, "audit-7-R7-B2");
CRequestTracker trfs;
2026-09-12 22:14:24 +03:00
trfs.Init(64, "audit7-fscan");
ulong sfs = 0;
trfs.Reserve(pcb, sfs);
trfs.MarkDealAdd(1, 500, 4001, 0.01);
ConfirmationSnapshot sfa;
sfa.Zero();
sfa.sequence = 1;
sfa.order_ticket = 81;
sfa.deal_tickets[0] = 4001;
sfa.deal_count = 1;
sfa.executed_volume_units = 1.0;
sfa.coverage_complete = true;
sfa.history_scan_failed = true; // неполный скан
trfs.SyncReconcile(1, sfa, 0.01, 1e-7);
RequestMetadata mfa;
trfs.GetMetadata(1, mfa);
AddCheck("DL-05", "failed-scan-no-t5-promote", "WARNING",
"обрезанный скан историей не повышает callback_coverage (R7-B2)",
StringFormat("cov=%s", (mfa.callback_coverage_complete ? "1" : "0")),
!mfa.callback_coverage_complete, "audit-7-R7-B2");
//--- R7-B4: reader — multiline-запись CSV и несогласованные mask/ячейка
RequestPlan pr7;
pr7.Zero();
pr7.sequence = 1;
pr7.symbol = "X";
pr7.magic = 1;
pr7.condition_id = "A";
pr7.operation = LAB_OP_MARKET_OPEN;
pr7.comment = "line1\nline2"; // встроенный перевод строки
CRequestTracker trm;
2026-09-12 22:14:24 +03:00
trm.Init(8, "audit7-multi");
ulong sm = 0;
trm.Reserve(pr7, sm);
SendObservation sms;
sms.Zero();
sms.t1_us = 1000100UL;
sms.send_ok = true;
sms.result_known = true;
sms.result.retcode = TRADE_RETCODE_DONE;
sms.result.request_id = 1;
trm.MarkSendStart(1, 1000000UL);
trm.MarkSendReturn(1, sms);
pr7.sequence = 2;
pr7.comment = "plain";
trm.Reserve(pr7, sm);
const string multi_csv = CCsvReport::BuildSamples(&trm, "C1", "cfg");
LabError merr;
2026-09-12 22:14:24 +03:00
const bool multi_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_multi.csv",
multi_csv, merr);
RebuiltSample mr7s[8];
2026-09-12 22:14:24 +03:00
const int mn = multi_saved ? CReportBuilder::LoadSamples(
"RequestLatencyLab\\tests\\samples_multi.csv", mr7s, 8, merr) : -9;
AddCheck("READER-03", "csv-multiline-record", "WARNING",
"комментарий line1\\nline2 (экранированный) читается как одна запись (R7-B4)",
StringFormat("saved=%s n=%d err=%d", (multi_saved ? "1" : "0"), mn, merr.code),
multi_saved && mn == 2, "audit-7-R7-B4");
//--- пустая T1 при установленном бите наличия => отказ (не «0»)
//--- отдельный простой набор БЕЗ multiline-комментария (иначе физический
//--- StringSplit в тесте разорвал бы запись; reader сам это не делает).
RequestPlan pk;
pk.Zero();
pk.sequence = 21;
pk.symbol = "X";
pk.magic = 1;
pk.condition_id = "A";
pk.operation = LAB_OP_MARKET_OPEN;
CRequestTracker trk;
2026-09-12 22:14:24 +03:00
trk.Init(8, "audit7-mask");
ulong skx = 0;
trk.Reserve(pk, skx);
SendObservation sks;
sks.Zero();
sks.t1_us = 1000100UL;
sks.send_ok = true;
sks.result_known = true;
sks.result.retcode = TRADE_RETCODE_DONE;
sks.result.request_id = 21;
trk.MarkSendStart(21, 1000000UL);
trk.MarkSendReturn(21, sks);
const string mask_csv = CCsvReport::BuildSamples(&trk, "C1", "cfg");
string mi[];
const int mn2 = StringSplit(mask_csv, '\n', mi);
string mh = mi[0];
StringReplace(mh, "\r", "");
string mrline = mi[1];
StringReplace(mrline, "\r", "");
string mcols[];
const int mc = StringSplit(mrline, ',', mcols);
mcols[3] = ""; // T1 пустая, бит остаётся
string mrow = "";
for(int k = 0; k < mc; k++)
{ if(k > 0) mrow += ","; mrow += mcols[k]; }
const bool mi_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_mask.csv",
mh + "\n" + mrow + "\n", merr);
RebuiltSample mib[8];
2026-09-12 22:14:24 +03:00
int mi_n = -9;
if(mi_saved)
2026-09-12 22:14:24 +03:00
mi_n = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_mask.csv",
mib, 8, merr);
AddCheck("READER-04", "csv-mask-empty-mismatch", "WARNING",
"пустая T1 при бите present_mask => отказ code 40 (R7-B4)",
StringFormat("n=%d err=%d", mi_n, merr.code),
mi_saved && mi_n < 0 && merr.code == 40, "audit-7-R7-B4");
//--- R7-S2: comparisons — delta = B−A (mean/median/p95), относительные
CRequestTracker trc2;
2026-09-12 22:14:24 +03:00
trc2.Init(8, "audit7-cmp");
RequestPlan pa2;
pa2.Zero();
pa2.sequence = 1;
pa2.symbol = "X";
pa2.magic = 1;
pa2.condition_id = "A";
pa2.operation = LAB_OP_MARKET_OPEN;
ulong scm = 0;
trc2.Reserve(pa2, scm);
SendObservation cs1;
cs1.Zero();
cs1.t1_us = 1100000UL;
cs1.send_ok = true;
cs1.result_known = true;
cs1.result.retcode = TRADE_RETCODE_DONE;
cs1.result.request_id = 1;
trc2.MarkSendStart(1, 1000000UL);
trc2.MarkSendReturn(1, cs1);
trc2.MarkDealAdd(1, 1200000UL, 7001, 0.01);
trc2.SetFinal(1, 1500000UL, LAB_OUT_FILLED, LAB_FINAL_CALLBACK_CHECK, 1);
RequestPlan pb2;
pb2.Zero();
pb2.sequence = 2;
pb2.symbol = "X";
pb2.magic = 1;
pb2.condition_id = "B";
pb2.operation = LAB_OP_MARKET_OPEN;
ulong scm2 = 0;
trc2.Reserve(pb2, scm2);
SendObservation cs2;
cs2.Zero();
cs2.t1_us = 1200000UL;
cs2.send_ok = true;
cs2.result_known = true;
cs2.result.retcode = TRADE_RETCODE_DONE;
cs2.result.request_id = 2;
trc2.MarkSendStart(2, 1000000UL);
trc2.MarkSendReturn(2, cs2);
trc2.MarkDealAdd(2, 1300000UL, 7002, 0.01);
trc2.SetFinal(2, 1600000UL, LAB_OUT_FILLED, LAB_FINAL_CALLBACK_CHECK, 1);
LabSettings cs2s;
cs2s.Zero();
cs2s.experiment_id = LAB_EXP_E1;
cs2s.series_id = 1;
const string cmp_csv = CCsvReport::BuildComparisons(&trc2, "s", cs2s);
const bool cmp_schema_ok = (StringFind(cmp_csv, "a_mean") >= 0 &&
StringFind(cmp_csv, "a_p95") >= 0 &&
StringFind(cmp_csv, "delta_mean") >= 0 &&
StringFind(cmp_csv, "rel_median") >= 0);
const bool cmp_delta_ok = (StringFind(cmp_csv, "100000.0000000000") >= 0);
const bool cmp_rel_ok = (StringFind(cmp_csv, "1.0000000000") >= 0);
AddCheck("CMP-02", "comparisons-b-minus-a", "WARNING",
"A=100ms B=200ms call: delta_median=+100ms, rel=1.0 (B−A, ТЗ §7) (R7-S2)",
StringFormat("schema=%s delta=%s rel=%s", (cmp_schema_ok ? "1" : "0"),
(cmp_delta_ok ? "1" : "0"), (cmp_rel_ok ? "1" : "0")),
cmp_schema_ok && cmp_delta_ok && cmp_rel_ok, "audit-7-R7-S2");
//--- R7-S1: применимость метрик; CONFLICT учитывается как n_conflict
CRequestTracker tre8;
2026-09-12 22:14:24 +03:00
tre8.Init(8, "audit7-eval8");
RequestPlan pe8;
pe8.Zero();
pe8.sequence = 1;
pe8.symbol = "X";
pe8.magic = 1;
pe8.condition_id = "A";
pe8.operation = LAB_OP_MARKET_OPEN;
ulong se8 = 0;
tre8.Reserve(pe8, se8);
SendObservation se8s;
se8s.Zero();
se8s.t1_us = 1000100UL;
se8s.send_ok = true;
se8s.result_known = true;
se8s.result.retcode = TRADE_RETCODE_DONE;
se8s.result.request_id = 1;
tre8.MarkSendStart(1, 1000000UL);
tre8.MarkSendReturn(1, se8s);
RequestMetadata me8;
tre8.GetMetadata(1, me8);
MetricObservation mo8;
bool ev8 = false;
CRequestTracker::EvaluateMetric(me8, LAB_METRIC_FIRST_DEAL_DELAY, mo8, ev8);
AddCheck("EVAL-08", "unknown-no-deal-unknown-app", "WARNING",
"MARKET_OPEN UNKNOWN без сделки => применимость UNKNOWN (не MISSING) (R7-S1)",
StringFormat("app=%d cat=%s val=%s", (int)mo8.applicability,
LabFitnessName(mo8.category), (mo8.has_value ? "1" : "0")),
ev8 && mo8.applicability == LAB_APP_UNKNOWN, "audit-7-R7-S1");
CRequestTracker tre9;
2026-09-12 22:14:24 +03:00
tre9.Init(8, "audit7-eval9");
ulong se9 = 0;
tre9.Reserve(pe8, se9);
SendObservation se9s;
se9s.Zero();
se9s.t1_us = 1000100UL;
se9s.send_ok = true;
se9s.result_known = true;
se9s.result.retcode = TRADE_RETCODE_DONE;
se9s.result.request_id = 1;
tre9.MarkSendStart(1, 1000000UL);
tre9.MarkSendReturn(1, se9s);
tre9.SetFinal(1, 1600000UL, LAB_OUT_CANCELED_NO_FILL, LAB_FINAL_TIMER_CHECK, 1);
RequestMetadata me9;
tre9.GetMetadata(1, me9);
MetricObservation mo9;
bool ev9 = false;
CRequestTracker::EvaluateMetric(me9, LAB_METRIC_FIRST_DEAL_DELAY, mo9, ev9);
AddCheck("EVAL-09", "no-fill-not-applicable", "WARNING",
"CANCELED_NO_FILL => FIRST_DEAL неприменим (не MISSING) (R7-S1)",
StringFormat("app=%d", (int)mo9.applicability),
ev9 && mo9.applicability == LAB_APP_NOT_APPLICABLE, "audit-7-R7-S1");
//--- CONFLICT без числа в summary: n_conflict=1, n_missing=0
CRequestTracker trcf;
2026-09-12 22:14:24 +03:00
trcf.Init(8, "audit7-conf");
RequestPlan pcf;
pcf.Zero();
pcf.sequence = 1;
pcf.symbol = "X";
pcf.magic = 1;
pcf.condition_id = "A";
pcf.operation = LAB_OP_MARKET_OPEN;
ulong scf = 0;
trcf.Reserve(pcf, scf);
SendObservation cfs;
cfs.Zero();
cfs.t1_us = 900000UL;
cfs.send_ok = true;
cfs.result_known = true; // T1 РАНЬШЕ T0
cfs.result.retcode = TRADE_RETCODE_DONE;
cfs.result.request_id = 1;
trcf.MarkSendStart(1, 1000000UL);
trcf.MarkSendReturn(1, cfs);
trcf.MarkDealAdd(1, 1200000UL, 8001, 0.01);
trcf.SetFinal(1, 1500000UL, LAB_OUT_FILLED, LAB_FINAL_CALLBACK_CHECK, 1);
LabSettings cfset;
cfset.Zero();
cfset.experiment_id = LAB_EXP_E1;
cfset.series_id = 1;
const string sum_csv = CCsvReport::BuildSummary(&trcf, "s", cfset);
bool conflict_ok = false;
string sum_lines[];
const int sln = StringSplit(sum_csv, '\n', sum_lines);
for(int i = 1; i < sln && !conflict_ok; i++)
{
2026-09-12 22:14:24 +03:00
string sl = sum_lines[i];
StringReplace(sl, "\r", "");
if(StringLen(sl) == 0)
continue;
2026-09-12 22:14:24 +03:00
string scols[];
int sc = 0;
if(CCsv::ParseRow(sl, scols, sc) && sc > 17 && scols[3] == "call_duration_us")
conflict_ok = (scols[17] == "1" && scols[15] == "0");
}
2026-09-12 22:14:24 +03:00
AddCheck("EVAL-10", "conflict-not-missing", "WARNING",
"T1<T0 (CONFLICT без числа) => n_conflict=1, n_missing=0 (R7-S1)",
(conflict_ok ? "ok" : "bad"), conflict_ok, "audit-7-R7-S1");
//--- R7-B5: повторное чтение прежней котировки не обновляет свежесть
CTimeSource cq;
cq.UseSynthetic(1000000UL);
CRequestTracker tq;
tq.Init(8, "audit7-quote");
CEventJournal jq;
jq.Init(256, "audit7-quote");
CExperimentRunner rq;
LabError qe;
LabSettings qs;
qs.Zero();
qs.experiment_id = LAB_EXP_E1;
qs.series_id = 1;
qs.count_per_condition = 1;
qs.warmup_per_condition = 0;
qs.symbol = "X";
qs.magic = 1;
qs.run_mode = LAB_RUN_LOCAL_ONLY;
rq.Init(qs, qe);
CAppController aq;
2026-09-12 22:14:24 +03:00
aq.Configure(&tq, &jq, &rq, &cq);
aq.SetSettings(qs);
aq.NoteQuoteObserved(1000);
2026-09-12 22:14:24 +03:00
const ulong q1 = aq.LastQuoteObservedUs(); // штамп NowUs()=1000000
cq.AdvanceUs(5000);
aq.NoteQuoteObserved(1000); // ТОТ ЖЕ тик, время ушло
2026-09-12 22:14:24 +03:00
const ulong q2 = aq.LastQuoteObservedUs(); // НЕ обновилось
cq.AdvanceUs(5000);
aq.NoteQuoteObserved(1001); // НОВЫЙ тик
2026-09-12 22:14:24 +03:00
const ulong q3 = aq.LastQuoteObservedUs(); // обновилось до q1+10000
const bool quote_ok = (q2 == q1 && q3 == q1 + 10000UL);
AddCheck("QUOTE-01", "stale-quote-not-refresh", "WARNING",
"повтор прежнего time_msc не обновляет время наблюдения; новый тик — обновляет (R7-B5)",
StringFormat("repeat_same=%s new_tick=%s",
(q2 == q1 ? "1" : "0"), (q3 > q1 ? "1" : "0")),
quote_ok, "audit-7-R7-B5");
//--- R7-B6: E4 происхождение — обычный E4 отвергает SYNTHETIC
string e4o[5];
2026-09-12 22:14:24 +03:00
for(int d = 0; d < 5; d++)
{
2026-09-12 22:14:24 +03:00
const string od = StringFormat("RequestLatencyLab\\tests\\e4_orig_%d", d + 1);
FolderCreate(od);
2026-09-12 22:14:24 +03:00
e4o[d] = od;
LabError oe;
2026-09-12 22:14:24 +03:00
CCsv::SaveUtf8(od + "\\manifest.csv",
"run_status,FINISHED,string,KNOWN,DERIVED\r\n" +
"n_main_dispatched,100,string,KNOWN,DERIVED\r\n" +
"configuration_id,cfg-orig,string,KNOWN,DERIVED\r\n" +
"data_origin,SYNTHETIC,string,KNOWN,DERIVED\r\n", oe);
}
LabError oe1;
2026-09-12 22:14:24 +03:00
const bool e4_rej = !CReportBuilder::RebuildE4(e4o, 5, "E4-TEST",
"RequestLatencyLab\\tests\\rebuilt_e4o\\", oe1);
LabError oe2;
2026-09-12 22:14:24 +03:00
const bool e4_allow2 = CReportBuilder::RebuildE4(e4o, 5, "E4-TEST",
"RequestLatencyLab\\tests\\rebuilt_e4o\\", oe2,
100, "SYNTHETIC");
AddCheck("E4-03", "e4-origin-gate", "WARNING",
"SYNTHETIC-серии: обычный E4 => отказ code 41; синтетический режим проходит origin (R7-B6)",
StringFormat("rej_code=%d allow_code=%d", oe1.code, oe2.code),
e4_rej && oe1.code == 41 && !e4_allow2 && oe2.code != 41,
"audit-7-R7-B6");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R8 |
//+------------------------------------------------------------------+
void TestAudit8Regressions(void)
{
LabError a8e;
2026-09-12 22:14:24 +03:00
//--- R8-B1: терминальное/блокирующее состояние запрещает новую отправку
//--- (гейт в PrepareAndDispatch до транспорта)
LabSettings gs;
gs.Zero();
gs.experiment_id = LAB_EXP_E1;
gs.series_id = 1;
gs.count_per_condition = 2;
gs.warmup_per_condition = 0;
gs.symbol = "X";
gs.magic = 1;
gs.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner rg8;
LabError ge8;
CRequestTracker tg8;
CEventJournal jg8;
CTimeSource cg8;
cg8.UseLive();
2026-09-12 22:14:24 +03:00
const bool g8init = (rg8.Init(gs, ge8) && tg8.Init(16, "audit8-gate") &&
jg8.Init(256, "audit8-gate"));
CAppController ag8;
2026-09-12 22:14:24 +03:00
ag8.Configure(&tg8, &jg8, &rg8, &cg8);
ag8.SetSettings(gs);
2026-09-12 22:14:24 +03:00
RequestPlan pg8;
pg8.Zero();
pg8.sequence = 1;
pg8.symbol = "X";
pg8.magic = 1;
pg8.condition_id = "A";
pg8.operation = LAB_OP_MARKET_OPEN;
ulong sg8 = 0;
ag8.BlockRun("test-blocked"); // BLOCKED
2026-09-12 22:14:24 +03:00
const bool g1 = ag8.DispatchPlan(pg8, false, sg8, ge8);
const bool gate_blocked = (!g1 && ge8.code == 7);
ulong sg9 = 0;
ag8.MarkFinished(); // FINISHED
2026-09-12 22:14:24 +03:00
const bool g2 = ag8.DispatchPlan(pg8, false, sg9, ge8);
const bool gate_finished = (!g2 && ge8.code == 7);
AddCheck("REG-02", "e3-terminal-no-dispatch", "WARNING",
"BLOCKED/FINISHED => PrepareAndDispatch отказ code 7 до транспорта (R8-B1)",
StringFormat("blocked=%s finished=%s code=%d",
(gate_blocked ? "1" : "0"), (gate_finished ? "1" : "0"), ge8.code),
g8init && gate_blocked && gate_finished, "audit-8-R8-B1");
//--- R8-B2: единая полнота измерительных точек (T2/T3) перед закрытием
CRequestTracker tpc;
2026-09-12 22:14:24 +03:00
tpc.Init(8, "audit8-points");
RequestPlan ppc;
ppc.Zero();
ppc.sequence = 1;
ppc.symbol = "X";
ppc.magic = 1;
ppc.condition_id = "A";
ppc.operation = LAB_OP_MARKET_OPEN;
ulong spc = 0;
tpc.Reserve(ppc, spc);
tpc.SetFinal(1, 1500000UL, LAB_OUT_FILLED, LAB_FINAL_TIMER_CHECK, 1);
tpc.SetTradingStateKnown(1, true);
RequestMetadata mpc;
tpc.GetMetadata(1, mpc);
mpc.callback_coverage_complete = true;
tpc.SetMetadata(1, mpc);
const bool no_t2_result = (!CAppController::MeasurementPointsComplete(mpc));
tpc.MarkRequest(1, 1200000UL, 1, TRADE_RETCODE_DONE, 0, 80, 0);
RequestMetadata mpc2;
tpc.GetMetadata(1, mpc2);
const bool t2_no_t3_result = (!CAppController::MeasurementPointsComplete(mpc2));
tpc.MarkOrderAdd(1, 1300000UL, 80, "TEST");
RequestMetadata mpc3;
tpc.GetMetadata(1, mpc3);
const bool points_full = CAppController::MeasurementPointsComplete(mpc3);
//--- однозначный отказ без ордера: T3 не требуется
CRequestTracker trj;
2026-09-12 22:14:24 +03:00
trj.Init(8, "audit8-points2");
ulong srj = 0;
trj.Reserve(ppc, srj);
trj.SetFinal(1, 1500000UL, LAB_OUT_REJECTED, LAB_FINAL_TIMER_CHECK, 1);
RequestMetadata mrj;
trj.GetMetadata(1, mrj);
mrj.order_ticket = 0;
mrj.present_mask |= (uint)LAB_MASK_T2;
trj.SetMetadata(1, mrj);
const bool reject_ok = CAppController::MeasurementPointsComplete(mrj);
const bool t2_ok = (no_t2_result && t2_no_t3_result && points_full && reject_ok);
AddCheck("T2-01", "collection-measurement-points", "WARNING",
"закрытие только при T2(+T3 по операции); REJECTED без ордера освобождает T3 (R8-B2)",
StringFormat("no_t2=%s t2_no_t3=%s full=%s rej=%s",
(no_t2_result ? "1" : "0"), (t2_no_t3_result ? "1" : "0"),
(points_full ? "1" : "0"), (reject_ok ? "1" : "0")),
t2_ok, "audit-8-R8-B2");
//--- R8-B3: SEND_RETURN буферизуется; flush записывает строки в safe-фазе
LabSettings f8s;
f8s.Zero();
f8s.campaign_id = "tests";
f8s.experiment_id = LAB_EXP_E1;
f8s.series_id = 1;
f8s.count_per_condition = 2;
f8s.warmup_per_condition = 0;
f8s.symbol = "X";
f8s.magic = 1;
f8s.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner fr8;
LabError fe8;
CRequestTracker tf8;
CEventJournal jf8;
CTimeSource cf8;
cf8.UseLive();
2026-09-12 22:14:24 +03:00
const bool f8init = (fr8.Init(f8s, fe8) && tf8.Init(16, "audit8-flush") &&
jf8.Init(256, "audit8-flush"));
CAppController af8;
2026-09-12 22:14:24 +03:00
af8.Configure(&tf8, &jf8, &fr8, &cf8);
af8.SetSettings(f8s);
2026-09-12 22:14:24 +03:00
bool sr_ok = false;
string diag_se8 = "";
bool diag_fex8 = false;
string dspre8 = "PRE-UNSET";
int d_before = -1;
bool d_fl = false, d_ld = false, d_h7 = false, d_h9 = false;
int d_pend = -1;
int d_err = -1;
dspre8 = af8.SessionId();
if(f8init && af8.InitRun(fe8))
{
2026-09-12 22:14:24 +03:00
const string sess8 = af8.SessionId();
const string ipath = "RequestLatencyLab\\tests\\" + sess8 + "\\intents.csv";
if(FileIsExist(ipath))
FileDelete(ipath);
2026-09-12 22:14:24 +03:00
af8.QueueDurableSendReturn(7, 1000100UL);
af8.QueueDurableSendReturn(9, 1000200UL);
const int before8 = af8.PendingDurableSendReturnCount();
const bool flushed8 = af8.FlushPendingSendReturns(fe8);
d_err = fe8.code;
diag_se8 = sess8;
diag_fex8 = FileIsExist(ipath);
string fc8;
2026-09-12 22:14:24 +03:00
const bool loaded8 = CCsv::LoadUtf8(ipath, fc8, fe8);
const bool has7 = (StringFind(fc8, "7,SEND_RETURN,1000100") >= 0);
const bool has9 = (StringFind(fc8, "9,SEND_RETURN,1000200") >= 0);
d_before = before8;
d_fl = flushed8;
d_ld = loaded8;
d_h7 = has7;
d_h9 = has9;
d_pend = af8.PendingDurableSendReturnCount();
sr_ok = (before8 == 2 && flushed8 &&
af8.PendingDurableSendReturnCount() == 0 &&
loaded8 && has7 && has9);
}
2026-09-12 22:14:24 +03:00
AddCheck("DURA-03", "send-return-buffered-flush", "WARNING",
"SEND_RETURN в буфере; flush пишет строки в safe-фазе (R8-B3)",
StringFormat("init=%s ok=%s before=%d flush=%s load=%s h7=%s h9=%s pend=%d err=%d pre=%s sess=%s fex=%s",
(f8init ? "1" : "0"), (sr_ok ? "1" : "0"),
d_before, (d_fl ? "1" : "0"), (d_ld ? "1" : "0"),
(d_h7 ? "1" : "0"), (d_h9 ? "1" : "0"), d_pend, d_err,
dspre8, diag_se8, (diag_fex8 ? "1" : "0")),
f8init && sr_ok, "audit-8-R8-B3");
//--- R8-B4: CountUncertainDurableIntents — без cap 256; sentinel
CTimeSource ccount;
ccount.UseLive();
CRequestTracker tx8;
tx8.Init(16, "audit8-unc");
CEventJournal jx8;
jx8.Init(256, "audit8-unc");
CExperimentRunner rx8;
LabError xe8;
rx8.Init(f8s, xe8);
CAppController ax8;
2026-09-12 22:14:24 +03:00
ax8.Configure(&tx8, &jx8, &rx8, &ccount);
ax8.SetSettings(f8s); // campaign_id=tests
2026-09-12 22:14:24 +03:00
//--- 440 полностью парных INTENT/SEND_RETURN => 0 неопределённых
const string p440 = "RequestLatencyLab\\tests\\r8_440\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r8_440");
if(FileIsExist(p440))
FileDelete(p440);
2026-09-12 22:14:24 +03:00
string c440 = "";
for(int k = 1; k <= 440; k++)
{
2026-09-12 22:14:24 +03:00
c440 += StringFormat("r8_440,%d,INTENT,%I64u\r\n", k, 1000000UL + (ulong)k);
c440 += StringFormat("r8_440,%d,SEND_RETURN,%I64u\r\n", k, 1000100UL + (ulong)k);
}
LabError ue;
2026-09-12 22:14:24 +03:00
const bool u440_saved = CCsv::SaveUtf8(p440, c440, ue);
const ulong u440 = ax8.CountUncertainDurableIntents("r8_440");
const bool c440ok = (u440_saved && u440 == 0);
//--- один INTENT без возврата => 1
const string p1 = "RequestLatencyLab\\tests\\r8_one\\intents.csv";
FolderCreate("RequestLatencyLab\tests\r8_one");
if(FileIsExist(p1))
FileDelete(p1);
2026-09-12 22:14:24 +03:00
const bool u1_saved = CCsv::SaveUtf8(p1, "r8_one,5,INTENT,2000000\r\n", ue);
const ulong u1 = ax8.CountUncertainDurableIntents("r8_one");
const bool c1ok = (u1_saved && u1 == 1);
//--- существует ПУСТОЙ журнал => 0 (отличие от отсутствующего)
const string pe = "RequestLatencyLab\\tests\\r8_empty\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r8_empty");
if(FileIsExist(pe))
FileDelete(pe);
2026-09-12 22:14:24 +03:00
const bool esaved = CCsv::SaveUtf8(pe, "", ue);
const bool cempty = (esaved && ax8.CountUncertainDurableIntents("r8_empty") == 0);
//--- журнала нет (пустой каталог) => 0
FolderCreate("RequestLatencyLab\\tests\\r8_miss");
2026-09-12 22:14:24 +03:00
const bool cmiss = (ax8.CountUncertainDurableIntents("r8_miss") == 0);
AddCheck("DURA-04", "intent-journal-440-paired", "WARNING",
"440 пар INTENT/SEND_RETURN => 0 неопределённых (нет cap 256) (R8-B4)",
StringFormat("saved=%s u=%I64u", (u440_saved ? "1" : "0"), u440),
c440ok, "audit-8-R8-B4");
AddCheck("DURA-05", "intent-journal-single-intent", "WARNING",
"один INTENT без SEND_RETURN => 1 неопределённая (R8-B4)",
StringFormat("saved=%s u=%I64u", (u1_saved ? "1" : "0"), u1),
c1ok, "audit-8-R8-B4");
AddCheck("DURA-06", "intent-journal-missing-vs-empty", "WARNING",
"отсутствующий и пустой журнал => 0; sentinel недоступности — код-ревью (R8-B4)",
StringFormat("miss=%s empty=%s", (cmiss ? "1" : "0"), (cempty ? "1" : "0")),
cmiss && cempty, "audit-8-R8-B4");
//--- R8-S1: read-back по логическим записям; capacity; строгий разбор
const bool rb_ok = CCsv::ReadBackVerifyFile(
"RequestLatencyLab\\tests\\samples_multi.csv", 3, ue);
const bool rb_bad = !CCsv::ReadBackVerifyFile(
"RequestLatencyLab\\tests\\samples_multi.csv", 4, ue);
AddCheck("READER-05", "readback-logical-records", "WARNING",
"multiline-запись: read-back считает логические записи (3), не физические (R8-S1)",
StringFormat("ok3=%s bad4=%s", (rb_ok ? "1" : "0"), (rb_bad ? "1" : "0")),
rb_ok && rb_bad, "audit-8-R8-S1");
CRequestTracker tcap;
2026-09-12 22:14:24 +03:00
tcap.Init(8, "audit8-cap");
RequestPlan pcap;
pcap.Zero();
pcap.sequence = 1;
pcap.symbol = "X";
pcap.magic = 1;
pcap.condition_id = "A";
pcap.operation = LAB_OP_MARKET_OPEN;
ulong scap = 0;
tcap.Reserve(pcap, scap);
pcap.sequence = 2;
tcap.Reserve(pcap, scap);
const string cap_csv = CCsvReport::BuildSamples(&tcap, "C1", "cfg");
const bool cap_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_cap2.csv",
cap_csv, ue);
RebuiltSample cap_r[2];
2026-09-12 22:14:24 +03:00
int cap_n = -9;
if(cap_saved)
2026-09-12 22:14:24 +03:00
cap_n = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_cap2.csv",
cap_r, 2, ue);
AddCheck("READER-06", "csv-capacity-observations", "WARNING",
"capacity=2 наблюдения: заголовок не расходует ёмкость (R8-S1)",
StringFormat("saved=%s n=%d err=%d", (cap_saved ? "1" : "0"), cap_n, ue.code),
cap_saved && cap_n == 2, "audit-8-R8-S1");
string prow8[];
int pcn8 = 0;
const bool unclosed8 = !CCsv::ParseRow("\"abc", prow8, pcn8);
ulong pu1 = 0, pu2 = 0;
const bool max_ok = (CReportBuilder::ParseCsvUlong("18446744073709551615", pu1) &&
!CReportBuilder::ParseCsvUlong("18446744073709551616", pu2));
AddCheck("READER-07", "csv-strict-parsing", "WARNING",
"незакрытая кавычка => отказ; диапазон ulong проверяется (R8-S1)",
StringFormat("unclosed=%s max=%s", (unclosed8 ? "1" : "0"), (max_ok ? "1" : "0")),
unclosed8 && max_ok, "audit-8-R8-S1");
//--- R8-S2: samples несёт market_window_id/seq; market_windows ticks_checksum
CRequestTracker tw8;
2026-09-12 22:14:24 +03:00
tw8.Init(8, "audit8-win");
RequestPlan pw8;
pw8.Zero();
pw8.sequence = 1;
pw8.symbol = "X";
pw8.magic = 1;
pw8.condition_id = "A";
pw8.operation = LAB_OP_MARKET_OPEN;
ulong sw8 = 0;
tw8.Reserve(pw8, sw8);
SendObservation ws8;
ws8.Zero();
ws8.send_ok = true;
ws8.result_known = true;
ws8.result.retcode = TRADE_RETCODE_DONE;
ws8.result.request_id = 1;
tw8.MarkSendStart(1, 1000000UL);
tw8.MarkSendReturn(1, ws8);
tw8.SetMarketRegime(1, LAB_REGIME_QUIET, "w00000001_0001", "cal-r8", 7);
const string win_csv = CCsvReport::BuildSamples(&tw8, "C1", "cfg");
const bool win_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_win.csv",
win_csv, ue);
RebuiltSample win_r[2];
2026-09-12 22:14:24 +03:00
int win_n = -9;
if(win_saved)
2026-09-12 22:14:24 +03:00
win_n = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_win.csv",
win_r, 2, ue);
const bool win_link = (win_n == 1 &&
win_r[0].market_window_id == "w00000001_0001" &&
win_r[0].market_window_seq == 7);
MarketWindow ww;
ww.Zero();
ww.window_id = "wT";
ww.sequence = 3;
ww.ticks_checksum = 12345;
MarketWindow wwa[1];
wwa[0] = ww;
const string mw_csv = CCsvReport::BuildMarketWindows(wwa, 1, "s8");
const bool mw_ok = (StringFind(mw_csv, "ticks_checksum") >= 0 &&
StringFind(mw_csv, "12345") >= 0);
AddCheck("WINDOW-01", "e3-window-linkage", "WARNING",
"samples: market_window_id/seq round-trip; market_windows: ticks_checksum (R8-S2)",
StringFormat("n=%d link=%s mw=%s", win_n, (win_link ? "1" : "0"), (mw_ok ? "1" : "0")),
win_link && mw_ok, "audit-8-R8-S2");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R9 |
//+------------------------------------------------------------------+
void TestAudit9Regressions(void)
{
LabError a9e;
2026-09-12 22:14:24 +03:00
//--- R9-B1: ReleaseReservation — откат НЕОТПРАВЛЕННОГО резерва
CRequestTracker trr;
2026-09-12 22:14:24 +03:00
trr.Init(8, "audit9-res");
RequestPlan prr;
prr.Zero();
prr.sequence = 1;
prr.symbol = "X";
prr.magic = 1;
prr.condition_id = "A";
prr.operation = LAB_OP_MARKET_OPEN;
ulong srr = 0;
const bool r1 = trr.Reserve(prr, srr);
const bool rrel = trr.ReleaseReservation(1);
const bool r2 = trr.Reserve(prr, srr); // повтор того же слота
const int rn = trr.Count();
const string rr_csv = CCsvReport::BuildSamples(&trr, "C1", "cfg");
int rr_rows = 0;
string rr_lines[];
int rrc2 = 0;
const int rn2 = StringSplit(rr_csv, '\n', rr_lines);
for(int i = 1; i < rn2; i++)
{
2026-09-12 22:14:24 +03:00
string t = rr_lines[i];
StringReplace(t, "\r", "");
if(StringLen(t) > 0)
rr_rows++;
}
2026-09-12 22:14:24 +03:00
AddCheck("REL-01", "reserve-release-reuse", "WARNING",
"ReleaseReservation отпускает слот: повтор той же sequence возможен, фантом не экспортируется (R9-B1)",
StringFormat("r1=%s rel=%s r2=%s n=%d rows=%d",
(r1 ? "1" : "0"), (rrel ? "1" : "0"), (r2 ? "1" : "0"), rn, rr_rows),
r1 && rrel && r2 && rn == 1 && rr_rows == 1, "audit-9-R9-B1");
//--- R9-B2: единая политика закрытия — T2 без T3 не закрывает POSITION_CLOSE
CRequestTracker tcl;
2026-09-12 22:14:24 +03:00
tcl.Init(8, "audit9-clean");
RequestPlan pcl;
pcl.Zero();
pcl.sequence = 1;
pcl.symbol = "X";
pcl.magic = 1;
pcl.condition_id = "A";
pcl.operation = LAB_OP_POSITION_CLOSE;
ulong scl = 0;
tcl.Reserve(pcl, scl);
tcl.MarkRequest(1, 1200000UL, 1, TRADE_RETCODE_DONE, 0, 80, 0);
RequestMetadata mcl;
tcl.GetMetadata(1, mcl);
const bool cln_t2_no_t3 = (!CAppController::MeasurementPointsComplete(mcl));
tcl.MarkOrderAdd(1, 1300000UL, 80, "CLEANUP");
tcl.GetMetadata(1, mcl);
const bool cln_full = CAppController::MeasurementPointsComplete(mcl);
CRequestTracker tcl2;
2026-09-12 22:14:24 +03:00
tcl2.Init(8, "audit9-clean2");
ulong sc2 = 0;
tcl2.Reserve(pcl, sc2);
tcl2.SetFinal(1, 1500000UL, LAB_OUT_REJECTED, LAB_FINAL_TIMER_CHECK, 1);
tcl2.MarkRequest(1, 1200000UL, 1, TRADE_RETCODE_DONE, 0, 0, 0);
RequestMetadata mcl2;
tcl2.GetMetadata(1, mcl2);
const bool cln_rej = CAppController::MeasurementPointsComplete(mcl2);
AddCheck("CLN-01", "cleanup-measurement-points", "WARNING",
"cleanup: T2 без T3 НЕ закрывает collection (единая политика), REJECTED без ордера освобождает (R9-B2)",
StringFormat("t2_no_t3=%s full=%s rej=%s",
(cln_t2_no_t3 ? "1" : "0"), (cln_full ? "1" : "0"), (cln_rej ? "1" : "0")),
cln_t2_no_t3 && cln_full && cln_rej, "audit-9-R9-B2");
//--- R9-B3: сбой durable-flush => Confirm=false + BLOCKED; последующая
//--- отправка запрещена (code 7) — терминальное состояние не перезаписано
LabSettings f9s;
f9s.Zero();
f9s.campaign_id = "tests";
f9s.experiment_id = LAB_EXP_E1;
f9s.series_id = 1;
f9s.count_per_condition = 2;
f9s.warmup_per_condition = 0;
f9s.symbol = "X";
f9s.magic = 1;
f9s.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner rf9;
LabError fe9;
CRequestTracker tf9;
CEventJournal jf9;
CTimeSource cf9;
cf9.UseLive();
2026-09-12 22:14:24 +03:00
const bool f9init = (rf9.Init(f9s, fe9) && tf9.Init(16, "audit9-flush") &&
jf9.Init(256, "audit9-flush"));
CAppController af9;
2026-09-12 22:14:24 +03:00
af9.Configure(&tf9, &jf9, &rf9, &cf9);
af9.SetSettings(f9s);
2026-09-12 22:14:24 +03:00
bool flush_blocked = false;
if(f9init && af9.InitRun(fe9))
{
2026-09-12 22:14:24 +03:00
const string s9 = af9.SessionId();
const string dir9 = "RequestLatencyLab\\tests\\" + s9;
//--- intents.csv заменяется КАТАЛОГОМ: открыть каталог нельзя ->
//--- AppendDurableLine (FileOpen) отказ -> flush не удался.
2026-09-12 22:14:24 +03:00
FolderCreate(dir9 + "\\intents.csv");
af9.QueueDurableSendReturn(7, 1000100UL);
const bool cf = af9.ConfirmDurableSendReturns(); // R9-B3: возвращает успех
const bool st = (af9.State() == LAB_STATE_BLOCKED);
RequestPlan pl9;
pl9.Zero();
pl9.sequence = 1;
pl9.symbol = "X";
pl9.magic = 1;
pl9.condition_id = "A";
pl9.operation = LAB_OP_MARKET_OPEN;
ulong s9b = 0;
LabError pe;
const bool d9 = af9.DispatchPlan(pl9, false, s9b, pe);
flush_blocked = (!cf && st && !d9 && pe.code == 7);
}
2026-09-12 22:14:24 +03:00
AddCheck("DURA-07", "flush-failure-blocked", "WARNING",
"сбой durable flush => Confirm=false + BLOCKED, отправка запрещена code 7 (R9-B3)",
StringFormat("init=%s ok=%s state=%s", (f9init ? "1" : "0"),
(flush_blocked ? "1" : "0"), af9.StateName()),
f9init && flush_blocked, "audit-9-R9-B3");
//--- R9-S2: durable-журнал — INTENT_CANCELLED снимает неопределённость
CTimeSource cc9;
cc9.UseLive();
CRequestTracker tc9;
tc9.Init(16, "audit9-unc");
CEventJournal jc9;
jc9.Init(256, "audit9-unc");
CExperimentRunner rc9;
LabError xe9;
rc9.Init(f9s, xe9);
CAppController ac9;
2026-09-12 22:14:24 +03:00
ac9.Configure(&tc9, &jc9, &rc9, &cc9);
ac9.SetSettings(f9s);
2026-09-12 22:14:24 +03:00
const string pc1 = "RequestLatencyLab\\tests\\r9_cancel\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r9_cancel");
if(FileIsExist(pc1))
FileDelete(pc1);
2026-09-12 22:14:24 +03:00
const bool cs1 = CCsv::SaveUtf8(pc1,
"r9_cancel,5,INTENT,2000000\r\nr9_cancel,5,INTENT_CANCELLED,2000100\r\n", a9e);
const ulong cu1 = ac9.CountUncertainDurableIntents("r9_cancel");
const string pc2 = "RequestLatencyLab\\tests\\r9_plain\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r9_plain");
if(FileIsExist(pc2))
FileDelete(pc2);
2026-09-12 22:14:24 +03:00
const bool cs2 = CCsv::SaveUtf8(pc2, "r9_plain,5,INTENT,2000000\r\n", a9e);
const ulong cu2 = ac9.CountUncertainDurableIntents("r9_plain");
AddCheck("DURA-08", "intent-cancelled-not-uncertain", "WARNING",
"INTENT+INTENT_CANCELLED => 0 неопределённых; INTENT один => 1 (R9-S2)",
StringFormat("c1=%s canc=%I64u plain=%I64u", (cs1 ? "1" : "0"), cu1, cu2),
cs1 && cu1 == 0 && cs2 && cu2 == 1, "audit-9-R9-S2");
//--- R9-S1: заголовок samples.csv — проверяются ВСЕ 68 имён
CRequestTracker tr8;
tr8.Init(8, "audit9-hdr");
RequestPlan pr8;
pr8.Zero();
pr8.sequence = 1;
pr8.symbol = "X";
pr8.magic = 1;
pr8.condition_id = "A";
pr8.operation = LAB_OP_MARKET_OPEN;
ulong sr8 = 0;
tr8.Reserve(pr8, sr8);
const string good_csv = CCsvReport::BuildSamples(&tr8, "C1", "cfg");
const bool gs = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_hdr_good.csv",
good_csv, a9e);
RebuiltSample gr[2];
int gn = -9;
if(gs)
2026-09-12 22:14:24 +03:00
gn = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_hdr_good.csv",
gr, 2, a9e);
string bad_csv = good_csv;
StringReplace(bad_csv, "market_window_id", "bad_window_id");
StringReplace(bad_csv, "market_window_seq", "bad_window_seq");
const bool bs = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_hdr_bad.csv",
bad_csv, a9e);
RebuiltSample br[2];
int bn = -9;
LabError be9;
if(bs)
2026-09-12 22:14:24 +03:00
bn = CReportBuilder::LoadSamples("RequestLatencyLab\\tests\\samples_hdr_bad.csv",
br, 2, be9);
AddCheck("READER-08", "samples-header-68-columns", "WARNING",
"проверяются ВСЕ 68 имён заголовка: подмена window-колонок отклоняется (R9-S1)",
StringFormat("good=%d bad=%d bad_code=%d", gn, bn, be9.code),
gs && gn == 1 && bs && bn < 0 && be9.code == 32, "audit-9-R9-S1");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Регрессии аудита R10 |
//+------------------------------------------------------------------+
void TestAudit10Regressions(void)
{
LabError a10e;
2026-09-12 22:14:24 +03:00
//--- R10-B2: повторный INTENT после отмены - новая попытка
CTimeSource cx10;
cx10.UseLive();
CRequestTracker tx10;
2026-09-12 22:14:24 +03:00
tx10.Init(16, "audit10-unc");
CEventJournal jx10;
2026-09-12 22:14:24 +03:00
jx10.Init(256, "audit10-unc");
LabSettings x10s;
x10s.Zero();
2026-09-12 22:14:24 +03:00
x10s.campaign_id = "tests";
x10s.experiment_id = LAB_EXP_E1;
x10s.series_id = 1;
x10s.symbol = "X";
x10s.magic = 1;
x10s.run_mode = LAB_RUN_LOCAL_ONLY;
CExperimentRunner rx10;
2026-09-12 22:14:24 +03:00
rx10.Init(x10s, a10e);
CAppController ax10;
2026-09-12 22:14:24 +03:00
ax10.Configure(&tx10, &jx10, &rx10, &cx10);
ax10.SetSettings(x10s);
2026-09-12 22:14:24 +03:00
const string pr1 = "RequestLatencyLab\\tests\\r10cr\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r10cr");
if(FileIsExist(pr1))
FileDelete(pr1);
2026-09-12 22:14:24 +03:00
const bool r10s1 = CCsv::SaveUtf8(pr1,
"r10cr,7,INTENT,3000000\r\n"
"r10cr,7,INTENT_CANCELLED,3000100\r\n"
"r10cr,7,INTENT,4000000\r\n", a10e);
const ulong r10cr = ax10.CountUncertainDurableIntents("r10cr");
AddCheck("DURA-09", "cancel-retry-crash", "WARNING",
"INTENT+INTENT_CANCELLED+INTENT => 1 неопределённая (повторная попытка) (R10-B2)",
StringFormat("saved=%s u=%I64u", (r10s1 ? "1" : "0"), r10cr),
r10s1 && r10cr == 1, "audit-10-R10-B2");
const string pr2 = "RequestLatencyLab\\tests\\r10cs\\intents.csv";
FolderCreate("RequestLatencyLab\\tests\\r10cs");
if(FileIsExist(pr2))
FileDelete(pr2);
2026-09-12 22:14:24 +03:00
const bool r10s2 = CCsv::SaveUtf8(pr2,
"r10cs,7,INTENT,3000000\r\n"
"r10cs,7,INTENT_CANCELLED,3000100\r\n"
"r10cs,7,INTENT,4000000\r\n"
"r10cs,7,SEND_RETURN,4000100\r\n", a10e);
const ulong r10cs = ax10.CountUncertainDurableIntents("r10cs");
AddCheck("DURA-10", "cancel-retry-send-return", "WARNING",
"после отмены повторная попытка с SEND_RETURN => 0 неопределённых (R10-B2)",
StringFormat("saved=%s u=%I64u", (r10s2 ? "1" : "0"), r10cs),
r10s2 && r10cs == 0, "audit-10-R10-B2");
//--- R10-B3: закрытие коллекции cleanup ждёт DEAL-callback
RequestMetadata cm10a;
2026-09-12 22:14:24 +03:00
cm10a.outcome = LAB_OUT_UNKNOWN;
cm10a.order_ticket = 0;
cm10a.plan.operation = LAB_OP_POSITION_CLOSE;
cm10a.present_mask = (uint)LAB_MASK_T2 | (uint)LAB_MASK_T3;
cm10a.callback_coverage_complete = false;
const bool cln_a = !CAppController::CleanupCollectionReady(cm10a);
RequestMetadata cm10b;
2026-09-12 22:14:24 +03:00
cm10b = cm10a;
cm10b.callback_coverage_complete = true;
const bool cln_b = CAppController::CleanupCollectionReady(cm10b);
RequestMetadata cm10c;
2026-09-12 22:14:24 +03:00
cm10c.outcome = LAB_OUT_UNKNOWN;
cm10c.order_ticket = 0;
cm10c.plan.operation = LAB_OP_PENDING_DELETE;
cm10c.plan.operation = LAB_OP_PENDING_DELETE;
cm10c.present_mask = (uint)LAB_MASK_T2;
cm10c.callback_coverage_complete = false;
const bool cln_c = CAppController::CleanupCollectionReady(cm10c);
AddCheck("CLN-02", "cleanup-waits-deal-callback", "WARNING",
"cleanup POSITION_CLOSE без полноты DEAL-callback НЕ готов к закрытию; с покрытием готов; PENDING_DELETE (без сделки) не ждёт DEAL (R10-B3)",
StringFormat("pos_no_cb=%s pos_cb=%s del=%s",
(cln_a ? "1" : "0"), (cln_b ? "1" : "0"), (cln_c ? "1" : "0")),
cln_a && cln_b && cln_c, "audit-10-R10-B3");
//--- R10-B4: формат market_ticks.csv - заголовок, реальный CRLF, %.17g
const string hdr10 = CAppController::MarketTickHeader();
MqlTick mt10;
2026-09-12 22:14:24 +03:00
mt10.time_msc = 1234567890;
mt10.bid = 1.2345678901234567;
mt10.ask = 1.2345678901234999;
mt10.flags = 234;
mt10.volume = 777;
const string row10 = CAppController::FormatMarketTickLine("s1", "w1", 3, mt10);
const bool w_hdr = (hdr10 ==
"session_id,window_id,window_seq,time_msc,bid,ask,flags,volume\r\n");
const bool w_bad_crlf = (StringFind(row10, "\\r\\n") < 0); // НЕ текст-слеш
const bool w_real_crlf = (StringFind(row10, "\r\n") >= 0); // реальный CRLF
const bool w_prec = (StringFind(row10, "1.2345678901234567") >= 0);
const bool w_cols = (StringFind(row10, ",w1,3,1234567890,") >= 0 &&
StringFind(row10, ",234,777\r\n") >= 0);
AddCheck("WINDOW-02", "market-ticks-format", "WARNING",
"market_ticks: заголовок, реальный CRLF (не литеральный \\r\\n), точность %.17g для bid/ask (R10-B4)",
StringFormat("hdr=%s crlf=%s no_bs=%s prec=%s cols=%s",
(w_hdr ? "1" : "0"), (w_real_crlf ? "1" : "0"),
(w_bad_crlf ? "1" : "0"), (w_prec ? "1" : "0"), (w_cols ? "1" : "0")),
w_hdr && w_real_crlf && w_bad_crlf && w_prec && w_cols, "audit-10-R10-B4");
//--- R10-B1/R10-S1: StartRun LOCAL_ONLY -> FINISHED -> ResetForRestart ->
//--- повторный запуск; синтетический E1 создаёт intents.csv (пары
//--- INTENT/SEND_RETURN) и market_ticks.csv (заголовок). checkpoint.marker
//--- пишется EA при финализации (ExportCheckpoint, R10-B1) - проверка в
//--- демо-пилоте на графике.
CTimeSource cs10;
cs10.UseSynthetic(1000000);
CRequestTracker ts10;
2026-09-12 22:14:24 +03:00
ts10.Init(64, "audit10-start");
CEventJournal js10;
2026-09-12 22:14:24 +03:00
js10.Init(512, "audit10-start");
LabSettings s10;
s10.Zero();
2026-09-12 22:14:24 +03:00
s10.campaign_id = "tests";
s10.experiment_id = LAB_EXP_E1;
s10.series_id = 1;
s10.symbol = "X";
s10.magic = 1;
s10.run_mode = LAB_RUN_LOCAL_ONLY;
s10.count_per_condition = 2;
s10.warmup_per_condition = 0;
s10.pause_ms = 1;
s10.sample_capacity = 64;
s10.event_capacity = 1024;
CExperimentRunner rs10;
2026-09-12 22:14:24 +03:00
rs10.Init(s10, a10e);
CAppController as10;
2026-09-12 22:14:24 +03:00
as10.Configure(&ts10, &js10, &rs10, &cs10);
as10.SetSettings(s10);
2026-09-12 22:14:24 +03:00
const bool st1 = as10.InitRun(a10e);
const bool st2 = as10.StartRun(a10e);
const string ss10 = as10.SessionId();
const bool st3 = (as10.State() == LAB_STATE_FINISHED);
const string ipath10 = "RequestLatencyLab\\tests\\" + ss10 + "\\intents.csv";
string ic10 = "";
const bool il10 = CCsv::LoadUtf8(ipath10, ic10, a10e);
int iN = 0;
int srN = 0;
string ilines10[];
2026-09-12 22:14:24 +03:00
const int iln10 = StringSplit(ic10, '\n', ilines10);
for(int i = 0; i < iln10; i++)
{
2026-09-12 22:14:24 +03:00
string sl10 = ilines10[i];
StringReplace(sl10, "\\r", "");
if(StringFind(sl10, "INTENT") >= 0)
iN++;
2026-09-12 22:14:24 +03:00
if(StringFind(sl10, "SEND_RETURN") >= 0)
srN++;
}
2026-09-12 22:14:24 +03:00
const bool pairs_ok = (il10 && iN >= 1 && iN == srN);
const bool mt_ok = as10.EnsureMarketTicksFile();
const bool mt_ex = FileIsExist("RequestLatencyLab\\tests\\" + ss10 + "\\market_ticks.csv");
const bool st4 = as10.ResetForRestart();
const bool st5 = (as10.State() == LAB_STATE_READY);
const bool st6 = as10.StartRun(a10e);
const bool st7 = (as10.State() == LAB_STATE_FINISHED);
AddCheck("START-01", "start-run-restart-pilot", "WARNING",
"StartRun(LOCAL_ONLY): синтетический E1 завершается FINISHED, создаёт intents.csv (пары) и market_ticks.csv; после финализации ResetForRestart и повторный запуск работают (R10-B1/R10-S1)",
StringFormat("init=%s st2=%s fin=%s il=%s pairsInt=%d pairsSr=%d mt=%s mtex=%s rest=%s ready=%s st6=%s fin2=%s",
(st1 ? "1" : "0"), (st2 ? "1" : "0"), (st3 ? "1" : "0"),
(il10 ? "1" : "0"), iN, srN, (mt_ok ? "1" : "0"), (mt_ex ? "1" : "0"),
(st4 ? "1" : "0"), (st5 ? "1" : "0"), (st6 ? "1" : "0"), (st7 ? "1" : "0")),
st1 && st2 && st3 && pairs_ok && mt_ok && mt_ex && st4 && st5 && st6 && st7, "audit-10-R10-B1");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| Регрессии этапа 12 (R11) |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestAudit11Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- RUNN-01 (R11-B1): CExperimentRunner::Init обнуляет счётчики прогона.
//--- Повторный Init (новый прогон) НЕ накапливает отправки прежней сессии.
LabError a11e;
LabSettings s11;
s11.Zero();
2026-09-12 22:14:24 +03:00
s11.campaign_id = "tests";
s11.experiment_id = LAB_EXP_E1;
s11.series_id = 1;
s11.symbol = "X";
s11.magic = 1;
s11.run_mode = LAB_RUN_LOCAL_ONLY;
s11.count_per_condition = 100;
s11.warmup_per_condition = 10;
s11.pause_ms = 1;
s11.sample_capacity = 64;
s11.event_capacity = 1024;
CExperimentRunner rr11;
2026-09-12 22:14:24 +03:00
const bool rinit1 = rr11.Init(s11, a11e);
rr11.CountDispatched(LAB_ROLE_MAIN);
rr11.CountDispatched(LAB_ROLE_WARMUP);
2026-09-12 22:14:24 +03:00
const ulong b_m = rr11.MainDispatched();
const ulong b_w = rr11.WarmupDispatched();
const bool rinit2 = rr11.Init(s11, a11e);
const bool reset_ok = (rinit1 && rinit2 && b_m == 1 && b_w == 1 &&
rr11.MainDispatched() == 0 && rr11.WarmupDispatched() == 0);
AddCheck("RUNN-01", "init-resets-counters", "WARNING",
"CExperimentRunner::Init обнуляет счётчики прогона - повторный запуск не удваивает MainDispatched/WarmupDispatched (R11-B1)",
StringFormat("init1=%s b_m=%I64u b_w=%I64u init2=%s m=%I64u w=%I64u",
2026-09-12 22:14:24 +03:00
(rinit1 ? "1" : "0"), b_m, b_w, (rinit2 ? "1" : "0"),
rr11.MainDispatched(), rr11.WarmupDispatched()),
reset_ok, "audit-11-R11-B1");
//--- START-02 (R11-B1): два самостоятельных запуска LOCAL_ONLY через
//--- StartRun/FINISHED/ResetForRestart - счётчики второй сессии независимы
//--- (200 MAIN / 20 WARMUP, а не накопленные 400/40); обе сессии FINISHED.
CTimeSource cs11;
cs11.UseSynthetic(1000000);
CRequestTracker ts11;
2026-09-12 22:14:24 +03:00
ts11.Init(64, "audit11-start");
CEventJournal js11;
2026-09-12 22:14:24 +03:00
js11.Init(512, "audit11-start");
LabSettings s11b;
s11b.Zero();
2026-09-12 22:14:24 +03:00
s11b.campaign_id = "tests";
s11b.experiment_id = LAB_EXP_E1;
s11b.series_id = 1;
s11b.symbol = "X";
s11b.magic = 1;
s11b.run_mode = LAB_RUN_LOCAL_ONLY;
s11b.count_per_condition = 100;
s11b.warmup_per_condition = 10;
s11b.pause_ms = 1;
s11b.sample_capacity = 256;
s11b.event_capacity = 4096;
CExperimentRunner rs11;
2026-09-12 22:14:24 +03:00
rs11.Init(s11b, a11e);
CAppController as11;
2026-09-12 22:14:24 +03:00
as11.Configure(&ts11, &js11, &rs11, &cs11);
as11.SetSettings(s11b);
2026-09-12 22:14:24 +03:00
const bool st_i1 = as11.InitRun(a11e);
const bool st_r1 = as11.StartRun(a11e);
const ulong st_m1 = as11.Runner().MainDispatched();
const ulong st_w1 = as11.Runner().WarmupDispatched();
const bool st_f1 = (as11.State() == LAB_STATE_FINISHED);
const bool st_rr = as11.ResetForRestart();
const bool st_r2 = as11.StartRun(a11e);
const ulong st_m2 = as11.Runner().MainDispatched();
const ulong st_w2 = as11.Runner().WarmupDispatched();
const bool st_f2 = (as11.State() == LAB_STATE_FINISHED);
const bool ok2 = (st_i1 && st_r1 && st_f1 && st_rr && st_r2 && st_f2 &&
st_m1 == 200 && st_w1 == 20 && st_m2 == 200 && st_w2 == 20);
AddCheck("START-02", "restart-independent-counters", "WARNING",
"повторный StartRun после финализации даёт независимые счётчики (200 MAIN / 20 WARMUP), а не накопленные 400/40 (R11-B1)",
StringFormat("i1=%s r1=%s m1=%I64u w1=%I64u f1=%s rr=%s r2=%s m2=%I64u w2=%I64u f2=%s",
2026-09-12 22:14:24 +03:00
(st_i1 ? "1" : "0"), (st_r1 ? "1" : "0"), st_m1, st_w1, (st_f1 ? "1" : "0"),
(st_rr ? "1" : "0"), (st_r2 ? "1" : "0"), st_m2, st_w2, (st_f2 ? "1" : "0")),
ok2, "audit-11-R11-B1");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| Регрессии этапа 13 (R13) |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestAudit12Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- START-03 (R13-B1): отказ ПЕРВОЙ durable-записи синтетического прогона
//--- не стирает контекст сессии: StartRun=true (направляет в финализацию
//--- частичного отчёта), FINISHED + termination_reason, session_id сохранён,
//--- фантомный резерв до T0 снят (не «выполненное наблюдение»); после
//--- ResetForRestart повторный запуск без инъекции завершается FINISHED.
LabError a13e;
LabSettings s13;
s13.Zero();
2026-09-12 22:14:24 +03:00
s13.campaign_id = "tests";
s13.experiment_id = LAB_EXP_E1;
s13.series_id = 1;
s13.symbol = "X";
s13.magic = 1;
s13.run_mode = LAB_RUN_LOCAL_ONLY;
s13.count_per_condition = 10;
s13.warmup_per_condition = 0;
s13.pause_ms = 1;
s13.sample_capacity = 64;
s13.event_capacity = 1024;
CTimeSource c13;
c13.UseSynthetic(1000000);
CRequestTracker t13;
2026-09-12 22:14:24 +03:00
t13.Init(64, "audit13-start03");
CEventJournal j13;
2026-09-12 22:14:24 +03:00
j13.Init(512, "audit13-start03");
CExperimentRunner r13;
2026-09-12 22:14:24 +03:00
r13.Init(s13, a13e);
CAppController a13;
2026-09-12 22:14:24 +03:00
a13.Configure(&t13, &j13, &r13, &c13);
a13.SetSettings(s13);
2026-09-12 22:14:24 +03:00
const bool s13_i = a13.InitRun(a13e);
a13.TestSetFailIntentAfter(1); // отказ ПЕРВОГО INTENT
2026-09-12 22:14:24 +03:00
const bool s13_r1 = a13.StartRun(a13e); // true: финализация частичного отчёта
const bool s13_f = (a13.State() == LAB_STATE_FINISHED);
const bool s13_sid = (StringLen(a13.SessionId()) > 0);
const bool s13_tr = (StringLen(a13.TerminationReason()) > 0);
const bool s13_empty = (t13.Count() == 0); // резерв до T0 снят
const bool s13_rr = a13.ResetForRestart();
a13.TestSetFailIntentAfter(0); // отказ снят - вторая сессия чистая
2026-09-12 22:14:24 +03:00
const bool s13_r2 = a13.StartRun(a13e);
const bool s13_f2 = (a13.State() == LAB_STATE_FINISHED);
const bool s13_ok = (s13_i && s13_r1 && s13_f && s13_sid && s13_tr &&
s13_empty && s13_rr && s13_r2 && s13_f2);
AddCheck("START-03", "abort-preserves-session", "WARNING",
"отказ SyntheticDispatch (первый INTENT) сохраняет контекст сессии: StartRun=true, FINISHED+reason, резерв до T0 снят; после ResetForRestart повторный запуск чист (R13-B1)",
StringFormat("i=%s r1=%s fin=%s sid=%s reason=%s empty=%s rr=%s r2=%s fin2=%s",
2026-09-12 22:14:24 +03:00
(s13_i ? "1" : "0"), (s13_r1 ? "1" : "0"), (s13_f ? "1" : "0"),
(s13_sid ? "1" : "0"), (s13_tr ? "1" : "0"), (s13_empty ? "1" : "0"),
(s13_rr ? "1" : "0"), (s13_r2 ? "1" : "0"), (s13_f2 ? "1" : "0")),
s13_ok, "audit-13-R13-B1");
//--- START-04 (R13-B1): отказ записи SEND_RETURN (после T1) — прерванная
//--- попытка СОХРАНЯЕТСЯ как неопределённая запись (T0+T1, без T2), а не
//--- стирается и не считается выполненной сделкой; контекст сессии жив.
LabSettings s14;
s14.Zero();
2026-09-12 22:14:24 +03:00
s14.campaign_id = "tests";
s14.experiment_id = LAB_EXP_E1;
s14.series_id = 1;
s14.symbol = "X";
s14.magic = 1;
s14.run_mode = LAB_RUN_LOCAL_ONLY;
s14.count_per_condition = 10;
s14.warmup_per_condition = 0;
s14.pause_ms = 1;
s14.sample_capacity = 64;
s14.event_capacity = 1024;
CTimeSource c14;
c14.UseSynthetic(1000000);
CRequestTracker t14;
2026-09-12 22:14:24 +03:00
t14.Init(64, "audit13-start04");
CEventJournal j14;
2026-09-12 22:14:24 +03:00
j14.Init(512, "audit13-start04");
CExperimentRunner r14;
2026-09-12 22:14:24 +03:00
r14.Init(s14, a13e);
CAppController a14;
2026-09-12 22:14:24 +03:00
a14.Configure(&t14, &j14, &r14, &c14);
a14.SetSettings(s14);
2026-09-12 22:14:24 +03:00
const bool s14_i = a14.InitRun(a13e);
a14.TestSetFailIntentAfter(2); // отказ ПЕРВОГО SEND_RETURN (после T1)
2026-09-12 22:14:24 +03:00
const bool s14_r1 = a14.StartRun(a13e);
const bool s14_f = (a14.State() == LAB_STATE_FINISHED);
const bool s14_cnt = (t14.Count() == 1); // прерванная попытка сохранена
RequestMetadata s14_md;
2026-09-12 22:14:24 +03:00
const bool s14_got = t14.GetByIndex(0, s14_md);
const bool s14_t0 = (s14_got && (s14_md.present_mask & (uint)LAB_MASK_T0) != 0);
const bool s14_t1 = (s14_got && (s14_md.present_mask & (uint)LAB_MASK_T1) != 0);
const bool s14_no2 = (s14_got && (s14_md.present_mask & (uint)LAB_MASK_T2) == 0);
const bool s14_sid = (StringLen(a14.SessionId()) > 0);
const bool s14_tr = (StringLen(a14.TerminationReason()) > 0);
const bool s14_ok = (s14_i && s14_r1 && s14_f && s14_cnt && s14_t0 &&
s14_t1 && s14_no2 && s14_sid && s14_tr);
AddCheck("START-04", "abort-keeps-uncertain", "WARNING",
"отказ записи SEND_RETURN (после T1) оставляет прерванную попытку DISPATCH_UNCERTAIN (T0+T1, без T2), а не стирает её и не считает выполненной (R13-B1)",
StringFormat("i=%s r1=%s fin=%s cnt=%s t0=%s t1=%s not2=%s sid=%s reason=%s",
2026-09-12 22:14:24 +03:00
(s14_i ? "1" : "0"), (s14_r1 ? "1" : "0"), (s14_f ? "1" : "0"),
(s14_cnt ? "1" : "0"), (s14_t0 ? "1" : "0"), (s14_t1 ? "1" : "0"),
(s14_no2 ? "1" : "0"), (s14_sid ? "1" : "0"), (s14_tr ? "1" : "0")),
s14_ok, "audit-13-R13-B1");
//--- RESTART-01 (R13-B3): окна E3 прежней сессии НЕ переносятся в новый
//--- прогон — ResetForRestart и StartRun очищают массив/счётчик окон
//--- (иначе BuildMarketWindows пометил бы старые окна новой session_id).
LabSettings s15;
s15.Zero();
2026-09-12 22:14:24 +03:00
s15.campaign_id = "tests";
s15.experiment_id = LAB_EXP_E1;
s15.series_id = 1;
s15.symbol = "X";
s15.magic = 1;
s15.run_mode = LAB_RUN_LOCAL_ONLY;
s15.count_per_condition = 10;
s15.warmup_per_condition = 0;
s15.pause_ms = 1;
s15.sample_capacity = 64;
s15.event_capacity = 1024;
CTimeSource c15;
c15.UseSynthetic(1000000);
CRequestTracker t15;
2026-09-12 22:14:24 +03:00
t15.Init(64, "audit13-restart");
CEventJournal j15;
2026-09-12 22:14:24 +03:00
j15.Init(512, "audit13-restart");
CExperimentRunner r15;
2026-09-12 22:14:24 +03:00
r15.Init(s15, a13e);
CAppController a15;
2026-09-12 22:14:24 +03:00
a15.Configure(&t15, &j15, &r15, &c15);
a15.SetSettings(s15);
2026-09-12 22:14:24 +03:00
const bool s15_i = a15.InitRun(a13e);
const bool s15_start1 = a15.StartRun(a13e); // первая сессия FINISHED
MarketWindow s15_w;
s15_w.Zero();
2026-09-12 22:14:24 +03:00
s15_w.window_id = "old_window_e3";
s15_w.sequence = 1;
s15_w.valid = true;
a15.TestAddMarketWindow(s15_w); // имитация сохранённого окна
2026-09-12 22:14:24 +03:00
const bool s15_has1 = (a15.MarketWindowCount() == 1);
const bool s15_rr = a15.ResetForRestart();
const bool s15_clear = (a15.MarketWindowCount() == 0);
const bool s15_start2 = a15.StartRun(a13e); // новая сессия
const bool s15_clean = (a15.MarketWindowCount() == 0);
a15.TestAddMarketWindow(s15_w); // новое окно новой сессии
2026-09-12 22:14:24 +03:00
const bool s15_has1b = (a15.MarketWindowCount() == 1);
const bool s15_ok = (s15_i && s15_start1 && s15_has1 && s15_rr && s15_clear &&
s15_start2 && s15_clean && s15_has1b);
AddCheck("RESTART-01", "e3-windows-reset", "WARNING",
"ResetForRestart и StartRun очищают окна E3 прежней сессии - старые окна не переносятся в новый market_windows.csv (R13-B3)",
StringFormat("i=%s s1=%s has1=%s rr=%s clear=%s s2=%s clean=%s has1b=%s",
2026-09-12 22:14:24 +03:00
(s15_i ? "1" : "0"), (s15_start1 ? "1" : "0"), (s15_has1 ? "1" : "0"),
(s15_rr ? "1" : "0"), (s15_clear ? "1" : "0"), (s15_start2 ? "1" : "0"),
(s15_clean ? "1" : "0"), (s15_has1b ? "1" : "0")),
s15_ok, "audit-13-R13-B3");
//--- CHECK-01 (R13-B2): контракт контроллера, на который опирается gate
//--- финализации в слое советника: BlockRun НЕ меняет уже FINISHED-состояние,
//--- а признак сохранности checkpoint доступен через CheckpointOk().
a15.BlockRun("checkpoint write failed");
2026-09-12 22:14:24 +03:00
const bool ck1 = (a15.State() == LAB_STATE_FINISHED);
a15.SetCheckpointState(false);
2026-09-12 22:14:24 +03:00
const bool ck2 = !a15.CheckpointOk();
a15.SetCheckpointState(true);
2026-09-12 22:14:24 +03:00
const bool ck3 = a15.CheckpointOk();
const bool ck_ok = (ck1 && ck2 && ck3);
AddCheck("CHECK-01", "finalize-gate-contract", "WARNING",
"контракт слоя финализации: BlockRun не меняет FINISHED; сохранность checkpoint видна через CheckpointOk() (R13-B2)",
StringFormat("stays_finished=%s cp_false=%s cp_true=%s",
2026-09-12 22:14:24 +03:00
(ck1 ? "1" : "0"), (ck2 ? "1" : "0"), (ck3 ? "1" : "0")),
ck_ok, "audit-13-R13-B2");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| Регрессии этапа 14 (R14) |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestAudit13Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- SESS-01 (R14-B1): переустановка EA с теми же настройками и ТЕМИ ЖЕ
//--- синтетическими часами (1 000 000 = 0x000F4240) НЕ повторяет
//--- session_id — кандидат проверяется по существованию каталога;
//--- прежний отчёт (intents.csv первой сессии) не перезаписан.
LabError a14e;
LabSettings s16;
s16.Zero();
2026-09-12 22:14:24 +03:00
s16.campaign_id = "tests";
s16.experiment_id = LAB_EXP_E1;
s16.series_id = 1;
s16.symbol = "X";
s16.magic = 1;
s16.run_mode = LAB_RUN_LOCAL_ONLY;
s16.count_per_condition = 10;
s16.warmup_per_condition = 0;
s16.pause_ms = 1;
s16.sample_capacity = 64;
s16.event_capacity = 1024;
//--- первая "установка"
CTimeSource c16;
c16.UseSynthetic(1000000);
CRequestTracker t16;
2026-09-12 22:14:24 +03:00
t16.Init(64, "audit14-sess01a");
CEventJournal j16;
2026-09-12 22:14:24 +03:00
j16.Init(512, "audit14-sess01a");
CExperimentRunner r16;
2026-09-12 22:14:24 +03:00
r16.Init(s16, a14e);
CAppController a16;
2026-09-12 22:14:24 +03:00
a16.Configure(&t16, &j16, &r16, &c16);
a16.SetSettings(s16);
2026-09-12 22:14:24 +03:00
const bool s16_i1 = a16.InitRun(a14e);
const bool s16_r1 = a16.StartRun(a14e);
const string s16_sidA = a16.SessionId();
//--- вторая "установка": новые объекты/часы при тех же настройках
CTimeSource c16b;
c16b.UseSynthetic(1000000);
CRequestTracker t16b;
2026-09-12 22:14:24 +03:00
t16b.Init(64, "audit14-sess01b");
CEventJournal j16b;
2026-09-12 22:14:24 +03:00
j16b.Init(512, "audit14-sess01b");
CExperimentRunner r16b;
2026-09-12 22:14:24 +03:00
r16b.Init(s16, a14e);
CAppController a16b;
2026-09-12 22:14:24 +03:00
a16b.Configure(&t16b, &j16b, &r16b, &c16b);
a16b.SetSettings(s16);
2026-09-12 22:14:24 +03:00
const bool s16_i2 = a16b.InitRun(a14e);
const bool s16_r2 = a16b.StartRun(a14e);
const string s16_sidB = a16b.SessionId();
const bool s16_diff = (StringLen(s16_sidA) > 0 && StringLen(s16_sidB) > 0 &&
s16_sidA != s16_sidB);
const bool s16_keep = FileIsExist("RequestLatencyLab\\tests\\" + s16_sidA +
"\\intents.csv");
const bool s16_ok = (s16_i1 && s16_r1 && s16_i2 && s16_r2 && s16_diff &&
s16_keep);
AddCheck("SESS-01", "reattach-unique-session", "WARNING",
"переустановка EA с теми же часами/настройками даёт НОВЫЙ session_id и не перезаписывает каталог прежней сессии (R14-B1)",
StringFormat("i1=%s r1=%s sidA=%s i2=%s r2=%s diff=%s keep=%s",
2026-09-12 22:14:24 +03:00
(s16_i1 ? "1" : "0"), (s16_r1 ? "1" : "0"), s16_sidA,
(s16_i2 ? "1" : "0"), (s16_r2 ? "1" : "0"),
(s16_diff ? "1" : "0"), (s16_keep ? "1" : "0")),
s16_ok, "audit-14-R14-B1");
//--- SESS-02 (R14-B1): повторный запуск ПОСЛЕ отказа первого INTENT
//--- (частичная сессия, часы не двигались) получает НОВЫЙ session_id —
//--- каталог неполной сессии не маскируется следующей сессией.
CTimeSource c17;
c17.UseSynthetic(1000000);
CRequestTracker t17;
2026-09-12 22:14:24 +03:00
t17.Init(64, "audit14-sess02");
CEventJournal j17;
2026-09-12 22:14:24 +03:00
j17.Init(512, "audit14-sess02");
CExperimentRunner r17;
2026-09-12 22:14:24 +03:00
r17.Init(s16, a14e);
CAppController a17;
2026-09-12 22:14:24 +03:00
a17.Configure(&t17, &j17, &r17, &c17);
a17.SetSettings(s16);
2026-09-12 22:14:24 +03:00
const bool s17_i = a17.InitRun(a14e);
a17.TestSetFailIntentAfter(1); // отказ ПЕРВОГО INTENT
2026-09-12 22:14:24 +03:00
const bool s17_r1 = a17.StartRun(a14e); // частичная сессия FINISHED
const string s17_sidX = a17.SessionId();
const bool s17_f1 = (a17.State() == LAB_STATE_FINISHED);
const bool s17_rr = a17.ResetForRestart();
a17.TestSetFailIntentAfter(0); // отказ снят
2026-09-12 22:14:24 +03:00
const bool s17_r2 = a17.StartRun(a14e); // полная сессия
const string s17_sidY = a17.SessionId();
const bool s17_f2 = (a17.State() == LAB_STATE_FINISHED);
const bool s17_diff = (StringLen(s17_sidX) > 0 && StringLen(s17_sidY) > 0 &&
s17_sidX != s17_sidY);
const bool s17_ok = (s17_i && s17_r1 && s17_f1 && s17_rr && s17_r2 &&
s17_f2 && s17_diff);
AddCheck("SESS-02", "retry-after-fail-unique-session", "WARNING",
"повторный запуск после отказа первого INTENT получает НОВЫЙ session_id - неполная сессия не маскируется (R14-B1)",
StringFormat("i=%s r1=%s sidX=%s f1=%s rr=%s r2=%s sidY=%s f2=%s diff=%s",
2026-09-12 22:14:24 +03:00
(s17_i ? "1" : "0"), (s17_r1 ? "1" : "0"), s17_sidX,
(s17_f1 ? "1" : "0"), (s17_rr ? "1" : "0"), (s17_r2 ? "1" : "0"),
s17_sidY, (s17_f2 ? "1" : "0"), (s17_diff ? "1" : "0")),
s17_ok, "audit-14-R14-B1");
//--- WIN-02 (R14-S1): признак открытого измерительного окна учитывает и
//--- активный служебный cleanup — не только основную последовательность
//--- (иначе SyncPanelPhase перерисовывает график внутри окна cleanup).
CTimeSource c18;
c18.UseSynthetic(1000000);
CRequestTracker t18;
2026-09-12 22:14:24 +03:00
t18.Init(64, "audit14-win02");
CEventJournal j18;
2026-09-12 22:14:24 +03:00
j18.Init(512, "audit14-win02");
CExperimentRunner r18;
2026-09-12 22:14:24 +03:00
r18.Init(s16, a14e);
CAppController a18;
2026-09-12 22:14:24 +03:00
a18.Configure(&t18, &j18, &r18, &c18);
a18.SetSettings(s16);
2026-09-12 22:14:24 +03:00
const bool w18_i = a18.InitRun(a14e);
const bool w18_idle = !a18.InMeasurementWindow(); // READY - окна нет
a18.TestSetCleanupWindow(true);
2026-09-12 22:14:24 +03:00
const bool w18_cleanup = a18.InMeasurementWindow(); // cleanup - окно открыто
a18.TestSetCleanupWindow(false);
2026-09-12 22:14:24 +03:00
const bool w18_closed = !a18.InMeasurementWindow();
const bool w18_ok = (w18_i && w18_idle && w18_cleanup && w18_closed);
AddCheck("WIN-02", "measurement-window-includes-cleanup", "WARNING",
"InMeasurementWindow учитывает и активный cleanup - принудительная перерисовка не выполняется внутри окна (R14-S1)",
StringFormat("i=%s idle=%s cleanup=%s closed=%s",
2026-09-12 22:14:24 +03:00
(w18_i ? "1" : "0"), (w18_idle ? "1" : "0"),
(w18_cleanup ? "1" : "0"), (w18_closed ? "1" : "0")),
w18_ok, "audit-14-R14-S1");
}
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
//| Регрессии этапа 15 (R15) |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
void TestAudit14Regressions(void)
{
2026-09-12 22:14:24 +03:00
//--- SESS-03 (R15-B2): каталог с ЧАСТИЧНЫМИ остатками прерванного
//--- экспорта (только samples.csv/events.csv, БЕЗ intents/manifest/marker)
//--- не выдаётся новой сессии — прежняя пара побайтово неизменна.
LabSettings s20;
s20.Zero();
2026-09-12 22:14:24 +03:00
s20.campaign_id = "audit15";
s20.experiment_id = LAB_EXP_E1;
s20.series_id = 1;
s20.symbol = "X";
s20.magic = 1;
s20.run_mode = LAB_RUN_LOCAL_ONLY;
s20.count_per_condition = 10;
s20.warmup_per_condition = 0;
s20.pause_ms = 1;
s20.sample_capacity = 64;
s20.event_capacity = 1024;
//--- первая установка: InitRun только назначает sid (StartRun не вызываем)
LabError s20e;
CTimeSource c20;
c20.UseSynthetic(1000000);
CRequestTracker t20;
2026-09-12 22:14:24 +03:00
t20.Init(64, "audit15-sess03");
CEventJournal j20;
2026-09-12 22:14:24 +03:00
j20.Init(512, "audit15-sess03");
CExperimentRunner r20;
2026-09-12 22:14:24 +03:00
r20.Init(s20, s20e);
CAppController a20;
2026-09-12 22:14:24 +03:00
a20.Configure(&t20, &j20, &r20, &c20);
a20.SetSettings(s20);
2026-09-12 22:14:24 +03:00
const bool s20_i = a20.InitRun(s20e);
const string s20_sidA = a20.SessionId();
//--- имитация прерванного экспорта: в каталоге sidA ТОЛЬКО пара
const string s20_dir = "RequestLatencyLab\\audit15\\" + s20_sidA;
FolderCreate(s20_dir);
2026-09-12 22:14:24 +03:00
const string s20_payload = "KEEP_SAMPLES\r\n";
const bool s20_mk = (CCsv::SaveUtf8(s20_dir + "\\samples.csv", s20_payload, s20e) &&
CCsv::SaveUtf8(s20_dir + "\\events.csv", "KEEP_EVENTS\r\n", s20e) &&
!FileIsExist(s20_dir + "\\intents.csv") &&
!FileIsExist(s20_dir + "\\manifest.csv") &&
!FileIsExist(s20_dir + "\\checkpoint.marker"));
//--- повторное подключение с теми же часами — sidB должен отличаться
CTimeSource c20b;
c20b.UseSynthetic(1000000);
CRequestTracker t20b;
2026-09-12 22:14:24 +03:00
t20b.Init(64, "audit15-sess03b");
CEventJournal j20b;
2026-09-12 22:14:24 +03:00
j20b.Init(512, "audit15-sess03b");
CExperimentRunner r20b;
2026-09-12 22:14:24 +03:00
r20b.Init(s20, s20e);
CAppController a20b;
2026-09-12 22:14:24 +03:00
a20b.Configure(&t20b, &j20b, &r20b, &c20b);
a20b.SetSettings(s20);
2026-09-12 22:14:24 +03:00
const bool s20_i2 = a20b.InitRun(s20e);
const string s20_sidB = a20b.SessionId();
const bool s20_diff = (StringLen(s20_sidB) > 0 && s20_sidA != s20_sidB);
//--- прежняя пара побайтово неизменна
string s20_back;
LabError s20r;
2026-09-12 22:14:24 +03:00
const bool s20_load = CCsv::LoadUtf8(s20_dir + "\\samples.csv", s20_back, s20r);
const bool s20_keep = (s20_load && s20_back == s20_payload);
const bool s20_ok = (s20_i && s20_mk && s20_i2 && s20_diff && s20_keep);
AddCheck("SESS-03", "partial-dir-not-reused", "WARNING",
"каталог с частичными остатками прерванного экспорта (только samples/events) не выдаётся новой сессии; прежняя пара побайтово неизменна (R15-B2)",
StringFormat("i=%s mk=%s i2=%s sidA=%s sidB=%s diff=%s load=%s keep=%s",
2026-09-12 22:14:24 +03:00
(s20_i ? "1" : "0"), (s20_mk ? "1" : "0"), (s20_i2 ? "1" : "0"),
s20_sidA, s20_sidB, (s20_diff ? "1" : "0"),
(s20_load ? "1" : "0"), (s20_keep ? "1" : "0")),
s20_ok, "audit-15-R15-B2");
}
2026-09-12 22:14:24 +03:00
//+------------------------------------------------------------------+
//| Старт тестов |
//+------------------------------------------------------------------+
void OnStart(void)
{
TestDataStructures();
TestStatistics();
TestParkMiller();
TestCsv();
TestMarket();
TestTrackerBasics();
TestAuditRegressions();
TestAudit4Regressions();
TestAudit5Regressions();
TestAudit6Regressions();
TestAudit7Regressions();
TestAudit8Regressions();
TestAudit9Regressions();
TestAudit10Regressions();
TestAudit11Regressions();
TestAudit12Regressions();
TestAudit13Regressions();
TestAudit14Regressions();
2026-09-12 22:14:24 +03:00
//--- сохранение checks.csv
LabError err;
2026-09-12 22:14:24 +03:00
const string path = "RequestLatencyLab\\tests\\checks.csv";
const string out = CCsvReport::BuildChecks(ExtChecks, ExtCheckCount);
if(CCsv::SaveUtf8(path, out, err) &&
CCsv::ReadBackVerifyFile(path, ExtCheckCount + 1, err))
Print(StringFormat("RequestLatencyTests: checks.csv сохранён (%d проверок)",
ExtCheckCount));
else
2026-09-12 22:14:24 +03:00
Print("RequestLatencyTests: SAVE FAILED " + err.message);
PrintFormat("RequestLatencyTests: PASS=%d FAIL=%d TOTAL=%d",
2026-09-12 22:14:24 +03:00
ExtPassCount, ExtFailCount, ExtCheckCount);
if(ExtFailCount > 0)
Print("RequestLatencyTests: есть FAIL — блокирующие проверки см. checks.csv");
2026-09-12 22:14:24 +03:00
}
//+------------------------------------------------------------------+