//+------------------------------------------------------------------+ //| 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 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"); } //+------------------------------------------------------------------+