3429 lines
No EOL
152 KiB
MQL5
3429 lines
No EOL
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];
|
|
int ExtCheckCount = 0;
|
|
int ExtPassCount = 0;
|
|
int ExtFailCount = 0;
|
|
//+------------------------------------------------------------------+
|
|
//| Добавление проверки в журнал теста |
|
|
//+------------------------------------------------------------------+
|
|
void AddCheck(const string id, const string case_key, const string sev,
|
|
const string expected, const string actual, const bool pass,
|
|
const string src)
|
|
{
|
|
if(ExtCheckCount >= TESTS_CAPACITY)
|
|
return;
|
|
int i = ExtCheckCount;
|
|
ExtChecks[i].Zero();
|
|
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++;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Сравнение двух значений с допуском |
|
|
//+------------------------------------------------------------------+
|
|
bool Near(const double a, const double b, const double tol)
|
|
{
|
|
return(MathAbs(a - b) <= tol);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| DATA-01: LatencySample — ровно десять редакторских полей |
|
|
//+------------------------------------------------------------------+
|
|
void TestDataStructures(void)
|
|
{
|
|
LatencySample s;
|
|
ZeroMemory(s);
|
|
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",
|
|
(s.send_start_us == 0 ? "0" : "?"), true, "ТЗ §3");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| STAT-01/02/03: статистика |
|
|
//+------------------------------------------------------------------+
|
|
void TestStatistics(void)
|
|
{
|
|
//--- [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",
|
|
mean, p50, p90, p999, sd), stat_ok, "ТЗ §7/СП §9");
|
|
//--- пустой массив и [5]
|
|
double empty[1];
|
|
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",
|
|
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",
|
|
IntegerToString(off), off == -20, "ТЗ §7/СП §9");
|
|
//--- STAT-04: границы корзин
|
|
long vals[10];
|
|
for(int i = 0; i < 10; i++)
|
|
vals[i] = (i == 0 ? 0 : (1000 << (i - 1)));
|
|
ulong bins[LAB_HISTOGRAM_BINS];
|
|
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",
|
|
StringFormat("used=%I64u/10", used_bins), hist_ok && used_bins == 10,
|
|
"ТЗ §7/СП §9");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| PLAN-01: Park-Miller seed=1 |
|
|
//+------------------------------------------------------------------+
|
|
void TestParkMiller(void)
|
|
{
|
|
CParkMiller rng;
|
|
rng.Seed(1);
|
|
const ulong expected[5] = {16807, 282475249, 1622650073, 984943658, 1144108930};
|
|
ulong got[5];
|
|
bool all = true;
|
|
string actual = "";
|
|
for(int i = 0; i < 5; i++)
|
|
{
|
|
got[i] = rng.Next();
|
|
if(i > 0)
|
|
actual += ",";
|
|
actual += IntegerToString(got[i]);
|
|
if(got[i] != expected[i])
|
|
all = false;
|
|
}
|
|
AddCheck("PLAN-01", "seed=1", "WARNING",
|
|
"16807,282475249,1622650073,984943658,1144108930",
|
|
actual, all, "ТЗ §5/СП §8.1");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| CSV-01/02: экранирование и ошибки |
|
|
//+------------------------------------------------------------------+
|
|
void TestCsv(void)
|
|
{
|
|
//--- кириллица, запятая, кавычки, CR, пустая строка
|
|
const string tricky = "Тест, \"кавычка\",\r\nи \"\"пусто\"\"";
|
|
string out = CCsv::Esc(tricky);
|
|
string cols[];
|
|
int cnt = 0;
|
|
CCsv::ParseRow(out, cols, cnt);
|
|
AddCheck("CSV-01", "escaping-roundtrip", "WARNING",
|
|
"exact UTF-8 round-trip",
|
|
(cnt == 1 && cols[0] == tricky ? "exact" : "broken"),
|
|
cnt == 1 && cols[0] == tricky, "ТЗ §8/CSV");
|
|
//--- содержимая пустая строка ("") и отсутствующее значение (пусто) различаются:
|
|
//--- BuildRow(одна пустая ячейка) -> строка ",,.."; ParseRow сохраняет "" из кавычек.
|
|
string one[1];
|
|
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[];
|
|
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",
|
|
(distinct ? "ok" : "broken"), distinct, "ТЗ §8/CSV");
|
|
//--- CSV-02: переполнение uint64 не через double
|
|
const ulong big = 18446744073709551615ul;
|
|
string big_one[1];
|
|
big_one[0] = IntegerToString(big);
|
|
string big_row = CCsv::BuildRow(big_one, 1);
|
|
StringReplace(big_row, "\r", "");
|
|
StringReplace(big_row, "\n", "");
|
|
string big_cols[];
|
|
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",
|
|
(big_ok ? "exact" : "narrowed"), big_ok, "ТЗ §8/CSV");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| MARKET-04/05: классификация |
|
|
//+------------------------------------------------------------------+
|
|
void TestMarket(void)
|
|
{
|
|
Calibration cal;
|
|
cal.Zero();
|
|
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();
|
|
w_quiet.valid = true;
|
|
w_quiet.quote_frequency_hz = 2.0;
|
|
w_quiet.mid_range_ticks = 4.0;
|
|
w_fast.Zero();
|
|
w_fast.valid = true;
|
|
w_fast.quote_frequency_hz = 10.0;
|
|
w_fast.mid_range_ticks = 20.0;
|
|
w_mid.Zero();
|
|
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",
|
|
StringFormat("%s;%s;%s", LabRegimeName(r1), LabRegimeName(r2),
|
|
LabRegimeName(r3)), m_ok, "ТЗ §6(E3)/СП §8");
|
|
//--- вырожденные пороги -> INVALID/UNKNOWN
|
|
Calibration bad;
|
|
bad.Zero();
|
|
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",
|
|
StringFormat("regime=%s valid=%s", LabRegimeName(rb), (vb ? "true" : "false")),
|
|
!vb && rb == LAB_REGIME_UNKNOWN, "ТЗ §6(E3)/СП §8");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| DEAL-01/CORR-05/DATA-03: реестр и корреляция |
|
|
//+------------------------------------------------------------------+
|
|
void TestTrackerBasics(void)
|
|
{
|
|
//--- DEAL-01: повтор deal_ticket
|
|
CRequestTracker tr;
|
|
if(!tr.Init(64, "test-session"))
|
|
return;
|
|
RequestPlan p;
|
|
p.Zero();
|
|
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;
|
|
tr.GetMetadata(1, r);
|
|
AddCheck("DEAL-01", "deal-dedupe", "WARNING",
|
|
"deal_count=1, volume counted once, T5 not shifted",
|
|
StringFormat("count=%u vol=%.2f t4=%I64u", r.deal_count, r.callback_volume,
|
|
r.sample.first_deal_us),
|
|
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;
|
|
tr2.Init(64, "s2");
|
|
RequestPlan p2;
|
|
p2.Zero();
|
|
p2.sequence = 1;
|
|
tr2.Reserve(p2, seq);
|
|
tr2.MarkSendStart(1, 0);
|
|
SendObservation so;
|
|
so.Zero();
|
|
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",
|
|
StringFormat("mask=%u", r.present_mask),
|
|
(r.present_mask & mask) == mask, "ТЗ §3/СП §4,9");
|
|
//--- CORR-05: общий order_ticket для create/delete — разные записи
|
|
CRequestTracker tr3;
|
|
tr3.Init(64, "s3");
|
|
RequestPlan pc, pd;
|
|
pc.Zero();
|
|
pc.sequence = 10;
|
|
pc.operation = LAB_OP_PENDING_CREATE;
|
|
tr3.Reserve(pc, seq);
|
|
pd.Zero();
|
|
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();
|
|
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",
|
|
StringFormat("create_t3=%s delete_t3=%s", (create_t3 ? "1" : "0"),
|
|
(delete_no_t3 ? "0" : "1")),
|
|
create_t3 && delete_no_t3, "ТЗ §4/СП §6");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| AUDIT-регрессии (п.4,7,8,9,10,11) |
|
|
//+------------------------------------------------------------------+
|
|
void TestAuditRegressions(void)
|
|
{
|
|
CRequestTracker tr;
|
|
tr.Init(128, "audit");
|
|
RequestPlan p;
|
|
p.Zero();
|
|
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();
|
|
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;
|
|
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);
|
|
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);
|
|
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;
|
|
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 корреляция после появления моста
|
|
CRequestTracker tr2;
|
|
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);
|
|
tr2.ReconcileDeferred(2);
|
|
//--- PushDeferred вручную после MarkSendReturn (order известен)
|
|
tr2.PushDeferred(30, TRADE_TRANSACTION_DEAL_ADD, 5050, 9090, 0, 0.1);
|
|
tr2.ReconcileDeferred(2);
|
|
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)
|
|
ConfirmationSnapshot sn;
|
|
sn.Zero();
|
|
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();
|
|
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))
|
|
{
|
|
int n_a = 0, n_b = 0, mix = 0;
|
|
for(int i = 0; i < rn.SlotCount(); i++)
|
|
{
|
|
ScheduleSlot ss;
|
|
if(!rn.GetSlot(i, ss))
|
|
continue;
|
|
if(ss.plan.condition_id == "A")
|
|
{ n_a++; if(ss.plan.operation != LAB_OP_MARKET_OPEN) mix++; }
|
|
else
|
|
{ n_b++; if(ss.plan.operation != LAB_OP_PENDING_CREATE) mix++; }
|
|
}
|
|
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
|
|
AddCheck("E2-01", "schedule-ops", "info", "init", "failed", false, "audit-8");
|
|
s.experiment_id = LAB_EXP_E5;
|
|
if(rn.Init(s, err))
|
|
{
|
|
int a_min = 0, b_verb = 0, bad = 0;
|
|
for(int i = 0; i < rn.SlotCount(); i++)
|
|
{
|
|
ScheduleSlot ss;
|
|
if(!rn.GetSlot(i, ss))
|
|
continue;
|
|
if(ss.plan.condition_id == "A")
|
|
{ if(ss.plan.logging_mode == LAB_LOG_MINIMAL) a_min++; else bad++; }
|
|
else
|
|
{ if(ss.plan.logging_mode == LAB_LOG_VERBOSE) b_verb++; else bad++; }
|
|
}
|
|
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
|
|
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;
|
|
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: режим другой ветви не блокируется первым ожидающим слотом —
|
|
//--- отправляется первая подходящая возможность своей ветви.
|
|
const bool ok_other = rn.TryDispatchByRegime(other, plan);
|
|
const bool other_branch = (ok_other && plan.condition_id != first.plan.condition_id);
|
|
//--- затем режим первого слота отдаёт именно первый ожидающий
|
|
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
|
|
AddCheck("E3-01", "regime-scan-forward", "WARNING", "failed", "init", false, "audit-9");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R4 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit4Regressions(void)
|
|
{
|
|
//--- 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;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
const string cal_id = "cal-audit4-ok";
|
|
string cal_out = CCsv::HeaderKeyValue();
|
|
string cv[5];
|
|
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();
|
|
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;
|
|
const bool have_first = rn4.NextMain(first);
|
|
rn4.MarkWaitSlot(first.sequence); // нет котировки — WAIT
|
|
RequestPlan again;
|
|
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);
|
|
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
|
|
AddCheck("QUOTA-01", "wait-requeue", "WARNING", "init", "failed", false,
|
|
"audit-4-R4-B7");
|
|
//--- 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;
|
|
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();
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R5 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit5Regressions(void)
|
|
{
|
|
//--- R5-B3: категория LATE по фактическому концу метрики (не по порядку таймера)
|
|
CRequestTracker tr5;
|
|
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;
|
|
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();
|
|
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;
|
|
bool pilot_ok = false;
|
|
if(rn5.Init(s5, p5e))
|
|
{
|
|
pilot_ok = (rn5.SlotCount() == 8);
|
|
for(int i = 0; i < rn5.SlotCount() && pilot_ok; i++)
|
|
{
|
|
ScheduleSlot ss;
|
|
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; }
|
|
}
|
|
}
|
|
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;
|
|
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;
|
|
trr.Init(128, "audit5-rebuild");
|
|
for(int k = 1; k <= 2; k++)
|
|
{
|
|
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);
|
|
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);
|
|
}
|
|
const string e2e_samples = CCsvReport::BuildSamples(&trr, "C1", "cfg");
|
|
const string e2e_path = "RequestLatencyLab\\tests\\samples_e2e.csv";
|
|
LabError e2e_err;
|
|
const bool e2e_saved = CCsv::SaveUtf8(e2e_path, e2e_samples, e2e_err);
|
|
FolderCreate("RequestLatencyLab\\tests\\rebuilt_e2e");
|
|
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",
|
|
rc, e2e_err))
|
|
{
|
|
string rl[];
|
|
const int rn = StringSplit(rc, '\n', rl);
|
|
int rrows = 0;
|
|
for(int i = 0; i < rn; i++)
|
|
{
|
|
string t = rl[i];
|
|
StringReplace(t, "\r", "");
|
|
if(StringLen(t) > 0)
|
|
rrows++;
|
|
}
|
|
//--- 3 группы x (A+B+combined) x 8 метрик = 72 строки + заголовок
|
|
rows_ok = (rrows == 73);
|
|
}
|
|
}
|
|
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];
|
|
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;
|
|
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];
|
|
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;
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R6 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit6Regressions(void)
|
|
{
|
|
//--- R6-B3: знаковая метрика T2-T4 — своевременность по ОБЕИМ точкам
|
|
CRequestTracker tr6s;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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];
|
|
arr2[0] = rw1;
|
|
arr2[1] = rw2;
|
|
StatSummaryRow sr6;
|
|
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);
|
|
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;
|
|
bool e3init = e3r.Init(e3s, e3e);
|
|
bool q1st = false;
|
|
if(e3init)
|
|
{
|
|
for(int k = 0; k < 40; k++)
|
|
{
|
|
RequestPlan pp;
|
|
if(!e3r.TryDispatchByRegime(LAB_REGIME_QUIET, pp))
|
|
break;
|
|
e3r.MarkDispatchedSlot(pp.sequence);
|
|
e3r.CountDispatched(pp.role);
|
|
}
|
|
//--- одна ветвь заполнена, другая нет => НЕ MainComplete (R6-B4)
|
|
q1st = !e3r.MainComplete() && e3r.MainDispatched() == 40 && !e3r.E3QuotaComplete();
|
|
bool all_fast = true;
|
|
for(int k = 0; k < 40; k++)
|
|
{
|
|
RequestPlan pp;
|
|
if(!e3r.TryDispatchByRegime(LAB_REGIME_FAST, pp))
|
|
{ all_fast = false; break; }
|
|
e3r.MarkDispatchedSlot(pp.sequence);
|
|
e3r.CountDispatched(pp.role);
|
|
}
|
|
q1st = q1st && all_fast && e3r.E3QuotaComplete();
|
|
}
|
|
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;
|
|
bool blocks_ok = false;
|
|
if(br.Init(bs, be))
|
|
{
|
|
blocks_ok = true;
|
|
for(int b = 1; b <= 10 && blocks_ok; b++)
|
|
{
|
|
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;
|
|
if(!br.GetSlot(i, ss) || ss.plan.block_id != (uint)b)
|
|
continue;
|
|
if(ss.plan.condition_id == "A")
|
|
{ a_n++; if(ss.plan.side == LAB_SIDE_BUY) a_buy++; else a_sell++; }
|
|
else
|
|
{ b_n++; if(ss.plan.side == LAB_SIDE_BUY) b_buy++; else b_sell++; }
|
|
}
|
|
if(!(a_n == 10 && b_n == 10 && a_buy == 5 && a_sell == 5 && b_buy == 5 && b_sell == 5))
|
|
blocks_ok = false;
|
|
}
|
|
}
|
|
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;
|
|
bool warm_rej = false, warm_done = false;
|
|
if(wr.Init(ws, we))
|
|
{
|
|
//--- все 20 прогревочных планов получают систематический отказ precheck
|
|
RequestPlan wp;
|
|
while(wr.NextWarmup(wp))
|
|
{
|
|
for(int t = 0; t < 3; t++)
|
|
wr.MarkWarmupRejected(wp.sequence);
|
|
}
|
|
warm_rej = (wr.WarmupComplete() && wr.WarmupDispatched() < wr.WarmupRequired());
|
|
//--- нормальный прогон: фактические отправки закрывают квоту
|
|
CExperimentRunner w2;
|
|
LabError we2;
|
|
if(w2.Init(ws, we2))
|
|
{
|
|
RequestPlan wp2;
|
|
while(w2.NextWarmup(wp2))
|
|
{
|
|
w2.MarkWarmupDispatched(wp2.sequence);
|
|
w2.CountDispatched(wp2.role);
|
|
}
|
|
warm_done = (w2.WarmupComplete() &&
|
|
w2.WarmupDispatched() == w2.WarmupRequired());
|
|
}
|
|
}
|
|
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;
|
|
trs.Init(8, "audit6-reader");
|
|
ulong sqs = 0;
|
|
trs.Reserve(prs, sqs);
|
|
const string good_csv = CCsvReport::BuildSamples(&trs, "C1", "cfg");
|
|
LabError rerr;
|
|
//--- нечисловой токен в present_mask (col 37)
|
|
string bl[];
|
|
const int bn = StringSplit(good_csv, '\n', bl);
|
|
string hh = bl[0];
|
|
StringReplace(hh, "\r", "");
|
|
string rr = bl[1];
|
|
StringReplace(rr, "\r", "");
|
|
string rc[];
|
|
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];
|
|
int nbad = -99;
|
|
if(bad_saved)
|
|
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];
|
|
int ndup = -99;
|
|
if(dup_saved)
|
|
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];
|
|
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;
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R7 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit7Regressions(void)
|
|
{
|
|
LabError a7e;
|
|
//--- R7-B1: durable-журнал — правильные байты, две записи, без NUL
|
|
FolderCreate("RequestLatencyLab\\tests");
|
|
const string dpath = "RequestLatencyLab\\tests\\intents_r7.csv";
|
|
if(FileIsExist(dpath))
|
|
FileDelete(dpath);
|
|
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;
|
|
bool no_nul = true;
|
|
const bool dloaded = CCsv::LoadUtf8(dpath, dcontent, a7e);
|
|
int dlc = 0;
|
|
string dlines[];
|
|
if(dloaded)
|
|
{
|
|
dlc = StringSplit(dcontent, '\n', dlines);
|
|
for(int k = 0; k < dlc && no_nul; k++)
|
|
{
|
|
const int l2 = StringLen(dlines[k]);
|
|
for(int c2 = 0; c2 < l2; c2++)
|
|
if(StringGetCharacter(dlines[k], c2) == 0)
|
|
{ no_nul = false; break; }
|
|
}
|
|
}
|
|
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();
|
|
const bool w7init = (r7.Init(w7, w7e) && t7.Init(64, "audit7-warm") &&
|
|
j7.Init(256, "audit7-warm"));
|
|
CAppController a7;
|
|
a7.Configure(&t7, &j7, &r7, &c7);
|
|
a7.SetSettings(w7);
|
|
bool w7mixed = false;
|
|
if(w7init)
|
|
{
|
|
int sent7 = 0;
|
|
RequestPlan wp7;
|
|
while(r7.NextWarmup(wp7))
|
|
{
|
|
if(sent7 < 19)
|
|
{
|
|
r7.MarkWarmupDispatched(wp7.sequence);
|
|
r7.CountDispatched(wp7.role);
|
|
}
|
|
else
|
|
{
|
|
for(int t = 0; t < 3; t++)
|
|
r7.MarkWarmupRejected(wp7.sequence);
|
|
}
|
|
sent7++;
|
|
}
|
|
a7.ReturnToSchedule();
|
|
w7mixed = (a7.State() == LAB_STATE_FINISHED &&
|
|
a7.TerminationReason() == "WARMUP_INSUFFICIENT_DATA");
|
|
}
|
|
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();
|
|
const bool w8init = (r8.Init(w7, w8e) && t8.Init(64, "audit7-warm2") &&
|
|
j8.Init(256, "audit7-warm2"));
|
|
CAppController a8;
|
|
a8.Configure(&t8, &j8, &r8, &c8);
|
|
a8.SetSettings(w7);
|
|
bool w8main = false;
|
|
if(w8init)
|
|
{
|
|
RequestPlan wp8;
|
|
while(r8.NextWarmup(wp8))
|
|
{
|
|
r8.MarkWarmupDispatched(wp8.sequence);
|
|
r8.CountDispatched(wp8.role);
|
|
}
|
|
a8.ReturnToSchedule();
|
|
w8main = (a8.State() == LAB_STATE_MAIN);
|
|
}
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
const bool multi_saved = CCsv::SaveUtf8("RequestLatencyLab\\tests\\samples_multi.csv",
|
|
multi_csv, merr);
|
|
RebuiltSample mr7s[8];
|
|
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;
|
|
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];
|
|
int mi_n = -9;
|
|
if(mi_saved)
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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++)
|
|
{
|
|
string sl = sum_lines[i];
|
|
StringReplace(sl, "\r", "");
|
|
if(StringLen(sl) == 0)
|
|
continue;
|
|
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");
|
|
}
|
|
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;
|
|
aq.Configure(&tq, &jq, &rq, &cq);
|
|
aq.SetSettings(qs);
|
|
aq.NoteQuoteObserved(1000);
|
|
const ulong q1 = aq.LastQuoteObservedUs(); // штамп NowUs()=1000000
|
|
cq.AdvanceUs(5000);
|
|
aq.NoteQuoteObserved(1000); // ТОТ ЖЕ тик, время ушло
|
|
const ulong q2 = aq.LastQuoteObservedUs(); // НЕ обновилось
|
|
cq.AdvanceUs(5000);
|
|
aq.NoteQuoteObserved(1001); // НОВЫЙ тик
|
|
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];
|
|
for(int d = 0; d < 5; d++)
|
|
{
|
|
const string od = StringFormat("RequestLatencyLab\\tests\\e4_orig_%d", d + 1);
|
|
FolderCreate(od);
|
|
e4o[d] = od;
|
|
LabError oe;
|
|
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;
|
|
const bool e4_rej = !CReportBuilder::RebuildE4(e4o, 5, "E4-TEST",
|
|
"RequestLatencyLab\\tests\\rebuilt_e4o\\", oe1);
|
|
LabError oe2;
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R8 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit8Regressions(void)
|
|
{
|
|
LabError a8e;
|
|
//--- 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();
|
|
const bool g8init = (rg8.Init(gs, ge8) && tg8.Init(16, "audit8-gate") &&
|
|
jg8.Init(256, "audit8-gate"));
|
|
CAppController ag8;
|
|
ag8.Configure(&tg8, &jg8, &rg8, &cg8);
|
|
ag8.SetSettings(gs);
|
|
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
|
|
const bool g1 = ag8.DispatchPlan(pg8, false, sg8, ge8);
|
|
const bool gate_blocked = (!g1 && ge8.code == 7);
|
|
ulong sg9 = 0;
|
|
ag8.MarkFinished(); // FINISHED
|
|
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;
|
|
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;
|
|
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();
|
|
const bool f8init = (fr8.Init(f8s, fe8) && tf8.Init(16, "audit8-flush") &&
|
|
jf8.Init(256, "audit8-flush"));
|
|
CAppController af8;
|
|
af8.Configure(&tf8, &jf8, &fr8, &cf8);
|
|
af8.SetSettings(f8s);
|
|
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))
|
|
{
|
|
const string sess8 = af8.SessionId();
|
|
const string ipath = "RequestLatencyLab\\tests\\" + sess8 + "\\intents.csv";
|
|
if(FileIsExist(ipath))
|
|
FileDelete(ipath);
|
|
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;
|
|
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);
|
|
}
|
|
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;
|
|
ax8.Configure(&tx8, &jx8, &rx8, &ccount);
|
|
ax8.SetSettings(f8s); // campaign_id=tests
|
|
//--- 440 полностью парных INTENT/SEND_RETURN => 0 неопределённых
|
|
const string p440 = "RequestLatencyLab\\tests\\r8_440\\intents.csv";
|
|
FolderCreate("RequestLatencyLab\\tests\\r8_440");
|
|
if(FileIsExist(p440))
|
|
FileDelete(p440);
|
|
string c440 = "";
|
|
for(int k = 1; k <= 440; k++)
|
|
{
|
|
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;
|
|
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);
|
|
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);
|
|
const bool esaved = CCsv::SaveUtf8(pe, "", ue);
|
|
const bool cempty = (esaved && ax8.CountUncertainDurableIntents("r8_empty") == 0);
|
|
//--- журнала нет (пустой каталог) => 0
|
|
FolderCreate("RequestLatencyLab\\tests\\r8_miss");
|
|
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;
|
|
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];
|
|
int cap_n = -9;
|
|
if(cap_saved)
|
|
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;
|
|
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];
|
|
int win_n = -9;
|
|
if(win_saved)
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R9 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit9Regressions(void)
|
|
{
|
|
LabError a9e;
|
|
//--- R9-B1: ReleaseReservation — откат НЕОТПРАВЛЕННОГО резерва
|
|
CRequestTracker trr;
|
|
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++)
|
|
{
|
|
string t = rr_lines[i];
|
|
StringReplace(t, "\r", "");
|
|
if(StringLen(t) > 0)
|
|
rr_rows++;
|
|
}
|
|
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;
|
|
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;
|
|
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();
|
|
const bool f9init = (rf9.Init(f9s, fe9) && tf9.Init(16, "audit9-flush") &&
|
|
jf9.Init(256, "audit9-flush"));
|
|
CAppController af9;
|
|
af9.Configure(&tf9, &jf9, &rf9, &cf9);
|
|
af9.SetSettings(f9s);
|
|
bool flush_blocked = false;
|
|
if(f9init && af9.InitRun(fe9))
|
|
{
|
|
const string s9 = af9.SessionId();
|
|
const string dir9 = "RequestLatencyLab\\tests\\" + s9;
|
|
//--- intents.csv заменяется КАТАЛОГОМ: открыть каталог нельзя ->
|
|
//--- AppendDurableLine (FileOpen) отказ -> flush не удался.
|
|
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);
|
|
}
|
|
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;
|
|
ac9.Configure(&tc9, &jc9, &rc9, &cc9);
|
|
ac9.SetSettings(f9s);
|
|
const string pc1 = "RequestLatencyLab\\tests\\r9_cancel\\intents.csv";
|
|
FolderCreate("RequestLatencyLab\\tests\\r9_cancel");
|
|
if(FileIsExist(pc1))
|
|
FileDelete(pc1);
|
|
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);
|
|
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)
|
|
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)
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии аудита R10 |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit10Regressions(void)
|
|
{
|
|
LabError a10e;
|
|
//--- R10-B2: повторный INTENT после отмены - новая попытка
|
|
CTimeSource cx10;
|
|
cx10.UseLive();
|
|
CRequestTracker tx10;
|
|
tx10.Init(16, "audit10-unc");
|
|
CEventJournal jx10;
|
|
jx10.Init(256, "audit10-unc");
|
|
LabSettings x10s;
|
|
x10s.Zero();
|
|
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;
|
|
rx10.Init(x10s, a10e);
|
|
CAppController ax10;
|
|
ax10.Configure(&tx10, &jx10, &rx10, &cx10);
|
|
ax10.SetSettings(x10s);
|
|
const string pr1 = "RequestLatencyLab\\tests\\r10cr\\intents.csv";
|
|
FolderCreate("RequestLatencyLab\\tests\\r10cr");
|
|
if(FileIsExist(pr1))
|
|
FileDelete(pr1);
|
|
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);
|
|
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;
|
|
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;
|
|
cm10b = cm10a;
|
|
cm10b.callback_coverage_complete = true;
|
|
const bool cln_b = CAppController::CleanupCollectionReady(cm10b);
|
|
RequestMetadata cm10c;
|
|
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;
|
|
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;
|
|
ts10.Init(64, "audit10-start");
|
|
CEventJournal js10;
|
|
js10.Init(512, "audit10-start");
|
|
LabSettings s10;
|
|
s10.Zero();
|
|
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;
|
|
rs10.Init(s10, a10e);
|
|
CAppController as10;
|
|
as10.Configure(&ts10, &js10, &rs10, &cs10);
|
|
as10.SetSettings(s10);
|
|
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[];
|
|
const int iln10 = StringSplit(ic10, '\n', ilines10);
|
|
for(int i = 0; i < iln10; i++)
|
|
{
|
|
string sl10 = ilines10[i];
|
|
StringReplace(sl10, "\\r", "");
|
|
if(StringFind(sl10, "INTENT") >= 0)
|
|
iN++;
|
|
if(StringFind(sl10, "SEND_RETURN") >= 0)
|
|
srN++;
|
|
}
|
|
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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии этапа 12 (R11) |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit11Regressions(void)
|
|
{
|
|
//--- RUNN-01 (R11-B1): CExperimentRunner::Init обнуляет счётчики прогона.
|
|
//--- Повторный Init (новый прогон) НЕ накапливает отправки прежней сессии.
|
|
LabError a11e;
|
|
LabSettings s11;
|
|
s11.Zero();
|
|
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;
|
|
const bool rinit1 = rr11.Init(s11, a11e);
|
|
rr11.CountDispatched(LAB_ROLE_MAIN);
|
|
rr11.CountDispatched(LAB_ROLE_WARMUP);
|
|
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",
|
|
(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;
|
|
ts11.Init(64, "audit11-start");
|
|
CEventJournal js11;
|
|
js11.Init(512, "audit11-start");
|
|
LabSettings s11b;
|
|
s11b.Zero();
|
|
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;
|
|
rs11.Init(s11b, a11e);
|
|
CAppController as11;
|
|
as11.Configure(&ts11, &js11, &rs11, &cs11);
|
|
as11.SetSettings(s11b);
|
|
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",
|
|
(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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии этапа 13 (R13) |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit12Regressions(void)
|
|
{
|
|
//--- START-03 (R13-B1): отказ ПЕРВОЙ durable-записи синтетического прогона
|
|
//--- не стирает контекст сессии: StartRun=true (направляет в финализацию
|
|
//--- частичного отчёта), FINISHED + termination_reason, session_id сохранён,
|
|
//--- фантомный резерв до T0 снят (не «выполненное наблюдение»); после
|
|
//--- ResetForRestart повторный запуск без инъекции завершается FINISHED.
|
|
LabError a13e;
|
|
LabSettings s13;
|
|
s13.Zero();
|
|
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;
|
|
t13.Init(64, "audit13-start03");
|
|
CEventJournal j13;
|
|
j13.Init(512, "audit13-start03");
|
|
CExperimentRunner r13;
|
|
r13.Init(s13, a13e);
|
|
CAppController a13;
|
|
a13.Configure(&t13, &j13, &r13, &c13);
|
|
a13.SetSettings(s13);
|
|
const bool s13_i = a13.InitRun(a13e);
|
|
a13.TestSetFailIntentAfter(1); // отказ ПЕРВОГО INTENT
|
|
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); // отказ снят - вторая сессия чистая
|
|
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",
|
|
(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();
|
|
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;
|
|
t14.Init(64, "audit13-start04");
|
|
CEventJournal j14;
|
|
j14.Init(512, "audit13-start04");
|
|
CExperimentRunner r14;
|
|
r14.Init(s14, a13e);
|
|
CAppController a14;
|
|
a14.Configure(&t14, &j14, &r14, &c14);
|
|
a14.SetSettings(s14);
|
|
const bool s14_i = a14.InitRun(a13e);
|
|
a14.TestSetFailIntentAfter(2); // отказ ПЕРВОГО SEND_RETURN (после T1)
|
|
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;
|
|
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",
|
|
(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();
|
|
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;
|
|
t15.Init(64, "audit13-restart");
|
|
CEventJournal j15;
|
|
j15.Init(512, "audit13-restart");
|
|
CExperimentRunner r15;
|
|
r15.Init(s15, a13e);
|
|
CAppController a15;
|
|
a15.Configure(&t15, &j15, &r15, &c15);
|
|
a15.SetSettings(s15);
|
|
const bool s15_i = a15.InitRun(a13e);
|
|
const bool s15_start1 = a15.StartRun(a13e); // первая сессия FINISHED
|
|
MarketWindow s15_w;
|
|
s15_w.Zero();
|
|
s15_w.window_id = "old_window_e3";
|
|
s15_w.sequence = 1;
|
|
s15_w.valid = true;
|
|
a15.TestAddMarketWindow(s15_w); // имитация сохранённого окна
|
|
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); // новое окно новой сессии
|
|
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",
|
|
(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");
|
|
const bool ck1 = (a15.State() == LAB_STATE_FINISHED);
|
|
a15.SetCheckpointState(false);
|
|
const bool ck2 = !a15.CheckpointOk();
|
|
a15.SetCheckpointState(true);
|
|
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",
|
|
(ck1 ? "1" : "0"), (ck2 ? "1" : "0"), (ck3 ? "1" : "0")),
|
|
ck_ok, "audit-13-R13-B2");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии этапа 14 (R14) |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit13Regressions(void)
|
|
{
|
|
//--- SESS-01 (R14-B1): переустановка EA с теми же настройками и ТЕМИ ЖЕ
|
|
//--- синтетическими часами (1 000 000 = 0x000F4240) НЕ повторяет
|
|
//--- session_id — кандидат проверяется по существованию каталога;
|
|
//--- прежний отчёт (intents.csv первой сессии) не перезаписан.
|
|
LabError a14e;
|
|
LabSettings s16;
|
|
s16.Zero();
|
|
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;
|
|
t16.Init(64, "audit14-sess01a");
|
|
CEventJournal j16;
|
|
j16.Init(512, "audit14-sess01a");
|
|
CExperimentRunner r16;
|
|
r16.Init(s16, a14e);
|
|
CAppController a16;
|
|
a16.Configure(&t16, &j16, &r16, &c16);
|
|
a16.SetSettings(s16);
|
|
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;
|
|
t16b.Init(64, "audit14-sess01b");
|
|
CEventJournal j16b;
|
|
j16b.Init(512, "audit14-sess01b");
|
|
CExperimentRunner r16b;
|
|
r16b.Init(s16, a14e);
|
|
CAppController a16b;
|
|
a16b.Configure(&t16b, &j16b, &r16b, &c16b);
|
|
a16b.SetSettings(s16);
|
|
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",
|
|
(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;
|
|
t17.Init(64, "audit14-sess02");
|
|
CEventJournal j17;
|
|
j17.Init(512, "audit14-sess02");
|
|
CExperimentRunner r17;
|
|
r17.Init(s16, a14e);
|
|
CAppController a17;
|
|
a17.Configure(&t17, &j17, &r17, &c17);
|
|
a17.SetSettings(s16);
|
|
const bool s17_i = a17.InitRun(a14e);
|
|
a17.TestSetFailIntentAfter(1); // отказ ПЕРВОГО INTENT
|
|
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); // отказ снят
|
|
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",
|
|
(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;
|
|
t18.Init(64, "audit14-win02");
|
|
CEventJournal j18;
|
|
j18.Init(512, "audit14-win02");
|
|
CExperimentRunner r18;
|
|
r18.Init(s16, a14e);
|
|
CAppController a18;
|
|
a18.Configure(&t18, &j18, &r18, &c18);
|
|
a18.SetSettings(s16);
|
|
const bool w18_i = a18.InitRun(a14e);
|
|
const bool w18_idle = !a18.InMeasurementWindow(); // READY - окна нет
|
|
a18.TestSetCleanupWindow(true);
|
|
const bool w18_cleanup = a18.InMeasurementWindow(); // cleanup - окно открыто
|
|
a18.TestSetCleanupWindow(false);
|
|
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",
|
|
(w18_i ? "1" : "0"), (w18_idle ? "1" : "0"),
|
|
(w18_cleanup ? "1" : "0"), (w18_closed ? "1" : "0")),
|
|
w18_ok, "audit-14-R14-S1");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Регрессии этапа 15 (R15) |
|
|
//+------------------------------------------------------------------+
|
|
void TestAudit14Regressions(void)
|
|
{
|
|
//--- SESS-03 (R15-B2): каталог с ЧАСТИЧНЫМИ остатками прерванного
|
|
//--- экспорта (только samples.csv/events.csv, БЕЗ intents/manifest/marker)
|
|
//--- не выдаётся новой сессии — прежняя пара побайтово неизменна.
|
|
LabSettings s20;
|
|
s20.Zero();
|
|
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;
|
|
t20.Init(64, "audit15-sess03");
|
|
CEventJournal j20;
|
|
j20.Init(512, "audit15-sess03");
|
|
CExperimentRunner r20;
|
|
r20.Init(s20, s20e);
|
|
CAppController a20;
|
|
a20.Configure(&t20, &j20, &r20, &c20);
|
|
a20.SetSettings(s20);
|
|
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);
|
|
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;
|
|
t20b.Init(64, "audit15-sess03b");
|
|
CEventJournal j20b;
|
|
j20b.Init(512, "audit15-sess03b");
|
|
CExperimentRunner r20b;
|
|
r20b.Init(s20, s20e);
|
|
CAppController a20b;
|
|
a20b.Configure(&t20b, &j20b, &r20b, &c20b);
|
|
a20b.SetSettings(s20);
|
|
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;
|
|
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",
|
|
(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");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Старт тестов |
|
|
//+------------------------------------------------------------------+
|
|
void OnStart(void)
|
|
{
|
|
TestDataStructures();
|
|
TestStatistics();
|
|
TestParkMiller();
|
|
TestCsv();
|
|
TestMarket();
|
|
TestTrackerBasics();
|
|
TestAuditRegressions();
|
|
TestAudit4Regressions();
|
|
TestAudit5Regressions();
|
|
TestAudit6Regressions();
|
|
TestAudit7Regressions();
|
|
TestAudit8Regressions();
|
|
TestAudit9Regressions();
|
|
TestAudit10Regressions();
|
|
TestAudit11Regressions();
|
|
TestAudit12Regressions();
|
|
TestAudit13Regressions();
|
|
TestAudit14Regressions();
|
|
//--- сохранение checks.csv
|
|
LabError err;
|
|
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
|
|
Print("RequestLatencyTests: SAVE FAILED " + err.message);
|
|
PrintFormat("RequestLatencyTests: PASS=%d FAIL=%d TOTAL=%d",
|
|
ExtPassCount, ExtFailCount, ExtCheckCount);
|
|
if(ExtFailCount > 0)
|
|
Print("RequestLatencyTests: есть FAIL — блокирующие проверки см. checks.csv");
|
|
}
|
|
//+------------------------------------------------------------------+ |