2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| ExperimentRunner.mqh |
|
2026-09-16 15:37:55 +03:00 | | | //| Copyright 2026, MetaQuotes Ltd. |
|
2026-09-10 16:43:28 +03:00 | | | //| www.mql5.com |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
| | | #ifndef REQUEST_LATENCY_LAB_EXPERIMENT_RUNNER_MQH
|
| | | #define REQUEST_LATENCY_LAB_EXPERIMENT_RUNNER_MQH
|
| | |
|
| | | #include "..\..\Include\RequestLatencyLab\Models.mqh"
|
| | | #include "..\..\Include\RequestLatencyLab\Configuration.mqh"
|
| | | //+------------------------------------------------------------------+
|
| | | //| Park-Miller PRNG (ТЗ §8.1): state=(16807*state) mod 2147483647. |
|
| | | //+------------------------------------------------------------------+
|
| | | class CParkMiller
|
| | | {
|
| | | private:
|
2026-09-12 22:14:24 +03:00 | | | ulong m_state;
|
2026-09-09 01:19:31 +03:00 | | | //--- M = 2147483647 (модуль Park-Miller)
|
| | |
|
| | | public:
|
2026-09-12 22:14:24 +03:00 | | | CParkMiller() { m_state = 1; }
|
| | | void Seed(const ulong s) { m_state = (s % 2147483647ul == 0 ? 1 : s % 2147483647ul); }
|
| | | ulong Next(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | m_state = (16807ul * m_state) % 2147483647ul;
|
2026-09-10 16:43:28 +03:00 | | | return(m_state);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //--- равномерный индекс [0,b): x=next-1; L=2147483646-(2147483646 mod b);
|
| | | //--- отклонить x>=L; индекс = x mod b.
|
2026-09-16 15:37:55 +03:00 | | | int NextUniform(const int b);
|
2026-09-09 01:19:31 +03:00 | | | };
|
| | |
|
| | | struct ScheduleSlot
|
| | | {
|
| | | RequestPlan plan;
|
| | | ENUM_LAB_SLOT_STATUS status;
|
| | | string reason_code;
|
| | | ulong first_local_us;
|
| | | ulong last_local_us;
|
2026-09-10 22:54:26 +03:00 | | | int retry_count; // R4-B7: попытки без фактического send
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-16 15:37:55 +03:00 | | | void Zero(void);
|
2026-09-09 01:19:31 +03:00 | | | };
|
| | |
|
| | | class CExperimentRunner
|
| | | {
|
| | | private:
|
2026-09-12 22:14:24 +03:00 | | | LabSettings m_settings;
|
| | | CParkMiller m_prng_main;
|
| | | CParkMiller m_prng_warmup;
|
| | | ScheduleSlot m_slots[]; // основные слоты пары ветвей
|
| | | ScheduleSlot m_warmup[]; // прогревочные слоты
|
| | | int m_slot_count;
|
| | | int m_warmup_count;
|
| | | int m_next_warmup_idx;
|
| | | int m_next_main_idx;
|
| | | ulong m_next_sequence; // глобальный счётчик попыток сессии
|
| | | ulong m_n_main_dispatched;
|
| | | ulong m_n_warmup_dispatched;
|
| | | ulong m_n_cleanup_dispatched;
|
| | | ulong m_n_precheck_rejected;
|
| | | ulong m_n_dispatch_uncertain;
|
2026-09-09 01:19:31 +03:00 | | | //--- квоты E3 (по режиму)
|
2026-09-12 22:14:24 +03:00 | | | int m_regime_quota; // на условие
|
| | | int m_regime_used_a;
|
| | | int m_regime_used_b;
|
2026-09-09 01:19:31 +03:00 | | |
|
| | | public:
|
2026-09-16 15:37:55 +03:00 | | | CExperimentRunner(void);
|
| | | bool Init(const LabSettings &s, LabError &error);
|
2026-09-12 22:14:24 +03:00 | | | ulong NextSequence() { return(m_next_sequence++); }
|
| | | ulong MainDispatched() const { return(m_n_main_dispatched); }
|
| | | ulong WarmupDispatched() const { return(m_n_warmup_dispatched); }
|
| | | ulong CleanupDispatched() const { return(m_n_cleanup_dispatched); }
|
| | | ulong PrecheckRejected() const { return(m_n_precheck_rejected); }
|
| | | ulong DispatchUncertain() const { return(m_n_dispatch_uncertain); }
|
2026-09-11 04:09:32 +03:00 | | | //--- слоты: каждая ветвь получает ровно count_per_condition запросов,
|
| | | //--- стороны Buy/Sell чередуются (баланс +/-1); блоки сохраняются как
|
| | | //--- метки для schedule.csv (block_len = per_condition/10 при стандарте).
|
| | | //--- R5-S1: малые чётные пилоты (4/10/40) строятся корректно — каждый
|
| | | //--- слот получает валидный план, нулевые планы не создаются.
|
2026-09-16 15:37:55 +03:00 | | | bool BuildScheduleMain(LabError &error);
|
| | | bool BuildWarmup(LabError &error);
|
| | | bool NextWarmup(RequestPlan &plan);
|
| | | bool NextMain(RequestPlan &plan);
|
| | | //--- попытка отправить слот только при совпадении режима (E3)
|
| | | bool TryDispatchByRegime(const ENUM_LAB_MARKET_REGIME regime, RequestPlan &plan);
|
| | | void CountDispatched(const ENUM_LAB_ROLE role);
|
| | | //--- R4-B7: учёт фактической отправки слота (DISPATCHED) и E3-квоты условия.
|
| | | //--- Вызывается из контроллера сразу после входа в OrderSend/OrderSendAsync.
|
| | | void OnSlotDispatched(const ulong sequence);
|
| | | //--- B9: статус слота по sequence (фактическая отправка / отказ / wait)
|
| | | bool SetSlotStatusBySequence(const ulong sequence, const ENUM_LAB_SLOT_STATUS st,
|
| | | const string reason);
|
| | | //--- B9: зафиксировать фактическую отправку. R4-B7: только здесь слот
|
| | | //--- становится DISPATCHED; затем квота условия учитывается в OnSlotDispatched.
|
| | | void MarkDispatchedSlot(const ulong sequence)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | SetSlotStatusBySequence(sequence, LAB_SLOT_DISPATCHED, "");
|
| | | OnSlotDispatched(sequence);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R4-B7: вернуть слот в очередь планировщика (не расходует слот/квоту).
|
| | | //--- Индекс возвращается назад, чтобы слот был выдан снова.
|
| | | bool RequeueSlotBySequence(const ulong sequence);
|
| | | //--- precheck-reject: ограниченное число повторов, затем терминальный
|
| | | //--- статус (слот не считается отправленным; серия станет INSUFFICIENT_DATA).
|
| | | void MarkRejectedSlot(const ulong sequence);
|
| | | //--- WAIT_DATA: слот возвращается в очередь (не расходуется).
|
| | | void MarkWaitSlot(const ulong sequence)
|
| | | {
|
| | | RequeueSlotBySequence(sequence);
|
| | | }
|
| | | //--- R4-B7: требуемое число фактических отправок (только MAIN-слоты)
|
| | | ulong RequiredDispatched(void) const;
|
| | | void MarkPrecheckRejected(void)
|
| | | {
|
| | | m_n_precheck_rejected++;
|
| | | }
|
| | | //--- R6-B4: прогревочные слоты имеют собственный lifecycle статусов
|
| | | bool RequeueWarmupBySequence(const ulong sequence);
|
| | | void MarkWarmupDispatched(const ulong sequence);
|
| | | void MarkWarmupRejected(const ulong sequence);
|
| | | void MarkCleanupDispatched(void)
|
| | | {
|
| | | m_n_cleanup_dispatched++;
|
| | | }
|
| | | void MarkDispatchUncertain(void)
|
| | | {
|
| | | m_n_dispatch_uncertain++;
|
| | | }
|
| | | int SlotCount() const { return(m_slot_count); }
|
| | | //--- R4-B7: E3-квоты заполнены фактическими отправками по обоим условиям
|
| | | bool E3QuotaComplete() const
|
| | | {
|
| | | return(m_regime_used_a >= m_regime_quota && m_regime_used_b >= m_regime_quota);
|
| | | }
|
| | | bool GetSlot(const int index, ScheduleSlot &out) const;
|
| | | bool MainComplete() const;
|
| | | bool WarmupComplete() const;
|
| | | //--- R7-B3: квота прогрева выполнена ТОЛЬКО фактическими отправками
|
| | | //--- (отдельно от «расписание исчерпано»; PRECHECK_REJECTED — НЕ успех).
|
| | | bool WarmupQuotaMet() const
|
| | | {
|
| | | return(m_n_warmup_dispatched >= WarmupRequired());
|
| | | }
|
| | | //--- R7-B3: расписание прогрева исчерпано — нет слотов, ожидающих выдачи
|
| | | //--- (PLANNED/INTENT_SAVED). Это НЕ равно выполнению квоты: исчерпанное
|
| | | //--- расписание с недобором фактических отправок — неполный исход,
|
| | | //--- а не успешно завершённый прогрев (см. AppController::ReturnToSchedule).
|
| | | bool WarmupExhausted() const;
|
| | | ulong WarmupRequired() const { return((ulong)m_warmup_count); } // R6-B4
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-16 15:37:55 +03:00 | | | private:
|
| | | void SwapBlocks(RequestPlan &arr[], const int a, const int b, const int len);
|
| | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Внешние определения методов. |
|
| | | //+------------------------------------------------------------------+
|
| | | int CParkMiller::NextUniform(const int b)
|
| | | {
|
| | | if(b <= 1)
|
| | | return(0);
|
| | | const ulong L = 2147483646ul - ((2147483646ul) % ((ulong)b));
|
| | | for(int guard = 0; guard < 1000; guard++)
|
| | | {
|
| | | const ulong x = Next() - 1;
|
| | | if(x >= L)
|
| | | continue;
|
| | | return(int)(x % ((ulong)b));
|
| | | }
|
| | | return(int)(Next() % ((ulong)b));
|
| | | }
|
| | | void ScheduleSlot::Zero(void)
|
| | | {
|
| | | plan.Zero();
|
| | | status = LAB_SLOT_PLANNED;
|
| | | reason_code = "";
|
| | | first_local_us = 0;
|
| | | last_local_us = 0;
|
| | | retry_count = 0;
|
| | | }
|
| | | CExperimentRunner::CExperimentRunner(void)
|
| | | {
|
| | | m_slot_count = 0;
|
| | | m_warmup_count = 0;
|
| | | m_next_warmup_idx = 0;
|
| | | m_next_main_idx = 0;
|
| | | m_next_sequence = 1;
|
| | | m_n_main_dispatched = 0;
|
| | | m_n_warmup_dispatched = 0;
|
| | | m_n_cleanup_dispatched = 0;
|
| | | m_n_precheck_rejected = 0;
|
| | | m_n_dispatch_uncertain = 0;
|
| | | m_regime_quota = 0;
|
| | | m_regime_used_a = 0;
|
| | | m_regime_used_b = 0;
|
| | | }
|
| | | bool CExperimentRunner::Init(const LabSettings &s, LabError &error)
|
| | | {
|
| | | error.Reset();
|
| | | error.component = LAB_COMP_PLANNER;
|
| | | m_settings = s;
|
| | | m_prng_main.Seed(s.seed);
|
| | | m_prng_warmup.Seed(s.seed ^ 2654435769ul);
|
| | | const int per_condition = s.count_per_condition;
|
| | | const int total = per_condition * 2;
|
| | | const int warmup_total = s.warmup_per_condition * 2;
|
| | | //--- R5-S1: малые сбалансированные пилоты допустимы, но нечётные/нулевые
|
| | | //--- значения явно отвергаются ДО создания расписания.
|
| | | if(per_condition < 2 || (per_condition % 2) != 0)
|
| | | {
|
| | | error.code = 7;
|
| | | error.severity = LAB_SEV_BLOCKER;
|
| | | error.message = "count_per_condition must be even and >=2";
|
| | | return(false);
|
| | | }
|
| | | if(s.warmup_per_condition < 0 || (s.warmup_per_condition % 2) != 0)
|
| | | {
|
| | | error.code = 8;
|
| | | error.severity = LAB_SEV_BLOCKER;
|
| | | error.message = "warmup_per_condition must be even (0 allowed)";
|
| | | return(false);
|
| | | }
|
| | | if(ArrayResize(m_slots, total) != total ||
|
| | | ArrayResize(m_warmup, warmup_total) != warmup_total)
|
| | | {
|
| | | error.code = 1;
|
| | | error.severity = LAB_SEV_BLOCKER;
|
| | | error.message = "cannot allocate schedule";
|
| | | return(false);
|
| | | }
|
| | | m_slot_count = total;
|
| | | m_warmup_count = warmup_total;
|
| | | m_next_warmup_idx = 0;
|
| | | m_next_main_idx = 0;
|
| | | m_next_sequence = 1;
|
| | | //--- R11-B1: счётчики прогона независимы - новый Init не накапливает
|
| | | //--- отправки предыдущей сессии (иначе повторный запуск удваивал бы их).
|
| | | m_n_main_dispatched = 0;
|
| | | m_n_warmup_dispatched = 0;
|
| | | m_n_cleanup_dispatched = 0;
|
| | | m_n_precheck_rejected = 0;
|
| | | m_n_dispatch_uncertain = 0;
|
| | | m_regime_quota = s.count_per_condition;
|
| | | m_regime_used_a = 0;
|
| | | m_regime_used_b = 0;
|
| | | return(BuildScheduleMain(error) && BuildWarmup(error));
|
| | | }
|
| | | bool CExperimentRunner::BuildScheduleMain(LabError &error)
|
| | | {
|
| | | const int per_condition = m_settings.count_per_condition;
|
| | | const int total = m_slot_count;
|
| | | RequestPlan raw[];
|
| | | if(ArrayResize(raw, total) != total)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | error.code = 2;
|
| | | return(false);
|
| | | }
|
| | | //--- R6-B4: сбалансированный блочный дизайн. Состав КАЖДОГО блока
|
| | | //--- формируется ДО перемешивания: block_len слотов ветви A (Buy/Sell
|
| | | //--- чередуются) + block_len слотов ветви B; затем Fisher-Yates только
|
| | | //--- внутри этого блока (глобальный shuffle разрушал 10A+10B).
|
| | | //--- block_len=per_condition/10 для штатных 100 (5Buy+5Sell на ветвь
|
| | | //--- блока); малые чётные пилоты получают один полный блок.
|
| | | const int block_len = (per_condition % 10 == 0 && per_condition >= 10 ?
|
| | | per_condition / 10 : per_condition);
|
| | | const int blocks = (block_len > 0 ? per_condition / block_len : 1);
|
| | | int idx = 0;
|
| | | for(int b = 0; b < blocks && idx < total; b++)
|
| | | {
|
| | | //--- при нечётном block_len знак сдвигается между блоками, чтобы
|
| | | //--- глобальный баланс ветви сохранялся (5Buy+5Sell на 10 слотов)
|
| | | const int parity = (block_len % 2 == 0 ? 0 : b);
|
| | | for(int c = 0; c < 2 && idx < total; c++) // условие A, B
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | const string cond = (c == 0 ? "A" : "B");
|
2026-09-16 15:37:55 +03:00 | | | for(int k = 0; k < block_len && idx < total; k++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int s = ((k + parity) % 2 == 0 ? 0 : 1); // Buy/Sell чередуются
|
2026-09-11 04:09:32 +03:00 | | | RequestPlan p;
|
| | | p.Zero();
|
2026-09-12 22:14:24 +03:00 | | | p.slot_id = (ulong)idx + 1;
|
| | | p.experiment_id = m_settings.experiment_id;
|
| | | p.series_id = (uint)m_settings.series_id;
|
| | | p.condition_id = cond;
|
2026-09-16 15:37:55 +03:00 | | | p.block_id = (uint)(b + 1);
|
| | | p.role = LAB_ROLE_MAIN;
|
2026-09-12 22:14:24 +03:00 | | | p.operation = (m_settings.experiment_id == LAB_EXP_E2 ?
|
| | | (cond == "A" ? LAB_OP_MARKET_OPEN : LAB_OP_PENDING_CREATE) :
|
| | | (m_settings.run_mode == LAB_RUN_PENDING_CREATE_DELETE ?
|
| | | LAB_OP_PENDING_CREATE : LAB_OP_MARKET_OPEN));
|
| | | p.side = (s == 0 ? LAB_SIDE_BUY : LAB_SIDE_SELL);
|
| | | p.mode = (m_settings.experiment_id == LAB_EXP_E1 ?
|
| | | (cond == "A" ? LAB_MODE_SYNC : LAB_MODE_ASYNC) : LAB_MODE_ASYNC);
|
2026-09-16 15:37:55 +03:00 | | | if(m_settings.experiment_id == LAB_EXP_E5)
|
| | | p.logging_mode = (cond == "A" ? LAB_LOG_MINIMAL : LAB_LOG_VERBOSE);
|
| | | else
|
| | | p.logging_mode = LAB_LOG_MINIMAL;
|
2026-09-12 22:14:24 +03:00 | | | p.sequence = NextSequence();
|
| | | p.symbol = m_settings.symbol;
|
2026-09-16 15:37:55 +03:00 | | | p.volume = 0.0; // заполняется контроллером
|
2026-09-12 22:14:24 +03:00 | | | p.distance_ticks = (double)m_settings.pending_distance_ticks;
|
| | | p.magic = m_settings.magic;
|
2026-09-16 15:37:55 +03:00 | | | raw[idx] = p;
|
2026-09-11 04:09:32 +03:00 | | | idx++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-B4: перемешивание только внутри блока — состав блока
|
| | | //--- (10A+10B c 5Buy+5Sell на ветвь при стандарте) не разрушается.
|
| | | const int from = b * block_len * 2;
|
| | | const int to = (from + block_len * 2 < total ? from + block_len * 2 : total);
|
| | | for(int i = to - 1; i > from; i--)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int j = from + m_prng_main.NextUniform(i - from + 1);
|
| | | if(j != i)
|
| | | {
|
| | | const RequestPlan tmp = raw[i];
|
| | | raw[i] = raw[j];
|
| | | raw[j] = tmp;
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | for(int i = 0; i < total; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_slots[i].Zero();
|
| | | m_slots[i].plan = raw[i];
|
| | | m_slots[i].plan.planned_order = (ulong)(i + 1);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(true);
|
| | | }
|
| | | bool CExperimentRunner::BuildWarmup(LabError &error)
|
| | | {
|
| | | const int per_condition = m_settings.warmup_per_condition;
|
| | | const int total = m_warmup_count;
|
| | | int idx = 0;
|
| | | for(int c = 0; c < 2 && idx < total; c++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const string cond = (c == 0 ? "A" : "B");
|
| | | for(int k = 0; k < per_condition && idx < total; k++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int s = (k % 2 == 0 ? 0 : 1);
|
| | | RequestPlan p;
|
| | | p.Zero();
|
| | | p.slot_id = (ulong)idx + 1;
|
| | | p.experiment_id = m_settings.experiment_id;
|
| | | p.series_id = (uint)m_settings.series_id;
|
| | | p.condition_id = cond;
|
| | | p.role = LAB_ROLE_WARMUP;
|
| | | p.operation = (m_settings.experiment_id == LAB_EXP_E2 ?
|
| | | (cond == "A" ? LAB_OP_MARKET_OPEN : LAB_OP_PENDING_CREATE) :
|
| | | (m_settings.run_mode == LAB_RUN_PENDING_CREATE_DELETE ?
|
| | | LAB_OP_PENDING_CREATE : LAB_OP_MARKET_OPEN));
|
| | | p.side = (s == 0 ? LAB_SIDE_BUY : LAB_SIDE_SELL);
|
| | | p.mode = (m_settings.experiment_id == LAB_EXP_E1 ?
|
| | | (cond == "A" ? LAB_MODE_SYNC : LAB_MODE_ASYNC) : LAB_MODE_ASYNC);
|
| | | p.logging_mode = LAB_LOG_MINIMAL;
|
| | | p.sequence = NextSequence();
|
| | | p.symbol = m_settings.symbol;
|
| | | p.distance_ticks = (double)m_settings.pending_distance_ticks;
|
| | | p.magic = m_settings.magic;
|
| | | m_warmup[idx].Zero();
|
| | | m_warmup[idx].plan = p;
|
| | | idx++;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
2026-09-16 15:37:55 +03:00 | | | m_warmup_count = idx;
|
| | | return(true);
|
| | | }
|
| | | bool CExperimentRunner::NextWarmup(RequestPlan &plan)
|
| | | {
|
| | | while(m_next_warmup_idx < m_warmup_count)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int i = m_next_warmup_idx;
|
| | | m_next_warmup_idx++;
|
| | | if(m_warmup[i].status != LAB_SLOT_PLANNED)
|
| | | continue;
|
| | | m_warmup[i].status = LAB_SLOT_INTENT_SAVED;
|
| | | plan = m_warmup[i].plan;
|
| | | return(true);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(false);
|
| | | }
|
| | | bool CExperimentRunner::NextMain(RequestPlan &plan)
|
| | | {
|
| | | //--- E3: режим окна должен совпадать с квотой условия; управляется
|
| | | //--- контроллером через TryDispatchByRegime; здесь — следующий слот.
|
| | | while(m_next_main_idx < m_slot_count)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const int i = m_next_main_idx;
|
| | | m_next_main_idx++;
|
| | | if(m_slots[i].status != LAB_SLOT_PLANNED)
|
| | | continue;
|
| | | //--- B9 (audit-3): DISPATCHED ставится только после фактического
|
| | | //--- входа в OrderSend*; здесь лишь INTENT_SAVED (намерение)
|
| | | m_slots[i].status = LAB_SLOT_INTENT_SAVED;
|
| | | m_slots[i].first_local_us = 0;
|
| | | plan = m_slots[i].plan;
|
| | | return(true);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(false);
|
| | | }
|
| | | bool CExperimentRunner::TryDispatchByRegime(const ENUM_LAB_MARKET_REGIME regime, RequestPlan &plan)
|
| | | {
|
| | | if(regime != LAB_REGIME_QUIET && regime != LAB_REGIME_FAST)
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | return(false);
|
2026-09-16 15:37:55 +03:00 | | | //--- R5-B6: выбирается ПЕРВАЯ подходящая возможность нужной ветви,
|
| | | //--- слот другой ветви впереди не блокирует отправку FAST/QUIET.
|
| | | int first_pending = -1;
|
| | | for(int i = 0; i < m_slot_count; i++)
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_slots[i].status != LAB_SLOT_PLANNED)
|
| | | continue;
|
| | | if(first_pending < 0)
|
| | | first_pending = i;
|
| | | //--- квота по условию уже использована
|
| | | const bool is_a = (m_slots[i].plan.condition_id == "A");
|
| | | const int used = (is_a ? m_regime_used_a : m_regime_used_b);
|
| | | if(used >= m_regime_quota)
|
| | | continue;
|
| | | //--- A/B для E3 — это QUIET/FAST
|
| | | const ENUM_LAB_MARKET_REGIME need = (is_a ? LAB_REGIME_QUIET : LAB_REGIME_FAST);
|
| | | if(regime != need)
|
| | | continue;
|
| | | //--- R4-B7: квота условия растёт ТОЛЬКО при фактической отправке
|
| | | //--- (OnSlotDispatched); выдача intent квоту не расходует.
|
| | | m_slots[i].status = LAB_SLOT_INTENT_SAVED;
|
| | | plan = m_slots[i].plan;
|
| | | if(i + 1 > m_next_main_idx)
|
| | | m_next_main_idx = i + 1;
|
| | | return(true);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- подходящего слота нет: курсор остаётся на первом ожидающем
|
| | | if(first_pending >= 0 && first_pending < m_next_main_idx)
|
| | | m_next_main_idx = first_pending;
|
| | | return(false);
|
| | | }
|
| | | void CExperimentRunner::CountDispatched(const ENUM_LAB_ROLE role)
|
| | | {
|
| | | if(role == LAB_ROLE_MAIN)
|
| | | m_n_main_dispatched++;
|
| | | else
|
| | | if(role == LAB_ROLE_WARMUP)
|
| | | m_n_warmup_dispatched++;
|
| | | }
|
| | | void CExperimentRunner::OnSlotDispatched(const ulong sequence)
|
| | | {
|
| | | for(int i = 0; i < m_slot_count; i++)
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_slots[i].plan.sequence != sequence)
|
| | | continue;
|
| | | if(m_slots[i].status != LAB_SLOT_DISPATCHED)
|
2026-09-10 22:54:26 +03:00 | | | return;
|
2026-09-16 15:37:55 +03:00 | | | const bool is_a = (m_slots[i].plan.condition_id == "A");
|
| | | if(is_a)
|
| | | m_regime_used_a++;
|
| | | else
|
| | | m_regime_used_b++;
|
| | | return;
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | bool CExperimentRunner::SetSlotStatusBySequence(const ulong sequence,
|
| | | const ENUM_LAB_SLOT_STATUS st, const string reason)
|
| | | {
|
| | | //--- только MAIN-слоты имеют sequence в плане; cleanup/warmup не слота
|
| | | for(int i = 0; i < m_slot_count; i++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_slots[i].plan.sequence != sequence)
|
| | | continue;
|
| | | m_slots[i].status = st;
|
| | | if(StringLen(reason) > 0)
|
| | | m_slots[i].reason_code = reason;
|
| | | return(true);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(false);
|
| | | }
|
| | | bool CExperimentRunner::RequeueSlotBySequence(const ulong sequence)
|
| | | {
|
| | | for(int i = 0; i < m_slot_count; i++)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_slots[i].plan.sequence != sequence)
|
| | | continue;
|
| | | m_slots[i].status = LAB_SLOT_PLANNED;
|
| | | if(i < m_next_main_idx)
|
| | | m_next_main_idx = i;
|
| | | return(true);
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-B4: прогревочные слоты возвращаются так же (слот не расходуется)
|
| | | return(RequeueWarmupBySequence(sequence));
|
| | | }
|
| | | void CExperimentRunner::MarkRejectedSlot(const ulong sequence)
|
| | | {
|
| | | for(int i = 0; i < m_slot_count; i++)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_slots[i].plan.sequence != sequence)
|
| | | continue;
|
| | | m_slots[i].retry_count++;
|
| | | if(m_slots[i].retry_count >= 3)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_slots[i].status = LAB_SLOT_PRECHECK_REJECTED;
|
| | | m_slots[i].reason_code = "precheck_rejected";
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_slots[i].status = LAB_SLOT_PLANNED;
|
| | | if(i < m_next_main_idx)
|
| | | m_next_main_idx = i;
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return;
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | //--- R6-B4: отказ прогревочного слота также ограничен повторами
|
| | | MarkWarmupRejected(sequence);
|
| | | }
|
| | | ulong CExperimentRunner::RequiredDispatched(void) const
|
| | | {
|
| | | ulong req = 0;
|
| | | for(int i = 0; i < m_slot_count; i++)
|
| | | if(m_slots[i].plan.role == LAB_ROLE_MAIN)
|
| | | req++;
|
| | | return(req);
|
| | | }
|
| | | bool CExperimentRunner::RequeueWarmupBySequence(const ulong sequence)
|
| | | {
|
| | | for(int j = 0; j < m_warmup_count; j++)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_warmup[j].plan.sequence != sequence)
|
| | | continue;
|
| | | m_warmup[j].status = LAB_SLOT_PLANNED;
|
| | | if(j < m_next_warmup_idx)
|
| | | m_next_warmup_idx = j;
|
| | | return(true);
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(false);
|
| | | }
|
| | | void CExperimentRunner::MarkWarmupDispatched(const ulong sequence)
|
| | | {
|
| | | for(int j = 0; j < m_warmup_count; j++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_warmup[j].plan.sequence != sequence)
|
| | | continue;
|
| | | m_warmup[j].status = LAB_SLOT_DISPATCHED;
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | void CExperimentRunner::MarkWarmupRejected(const ulong sequence)
|
| | | {
|
| | | for(int j = 0; j < m_warmup_count; j++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | if(m_warmup[j].plan.sequence != sequence)
|
| | | continue;
|
| | | m_warmup[j].retry_count++;
|
| | | if(m_warmup[j].retry_count >= 3)
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_warmup[j].status = LAB_SLOT_PRECHECK_REJECTED;
|
| | | m_warmup[j].reason_code = "precheck_rejected";
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | else
|
2026-09-11 09:06:39 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | m_warmup[j].status = LAB_SLOT_PLANNED;
|
| | | if(j < m_next_warmup_idx)
|
| | | m_next_warmup_idx = j;
|
2026-09-11 09:06:39 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
| | | bool CExperimentRunner::GetSlot(const int index, ScheduleSlot &out) const
|
| | | {
|
| | | if(index < 0 || index >= m_slot_count)
|
| | | return(false);
|
| | | out = m_slots[index];
|
| | | return(true);
|
| | | }
|
| | | bool CExperimentRunner::MainComplete() const
|
| | | {
|
| | | //--- R6-B4: завершение НЕ по high-watermark индекса, а по фактическим
|
| | | //--- отправкам и терминальным состояниям слотов (иначе E3 объявляет
|
| | | //--- MainComplete при неотправленных слотах другой ветви режима).
|
| | | if(m_n_main_dispatched >= RequiredDispatched())
|
| | | return(true);
|
| | | for(int i = 0; i < m_slot_count; i++)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ENUM_LAB_SLOT_STATUS st = m_slots[i].status;
|
| | | if(st == LAB_SLOT_PLANNED || st == LAB_SLOT_INTENT_SAVED)
|
| | | return(false);
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(true);
|
| | | }
|
| | | bool CExperimentRunner::WarmupComplete() const
|
| | | {
|
| | | //--- R6-B4: прогрев считается выполненным только по фактическим
|
| | | //--- отправкам; терминально исчерпанные слоты не объявляют успех.
|
| | | if(m_n_warmup_dispatched >= WarmupRequired())
|
| | | return(true);
|
| | | for(int i = 0; i < m_warmup_count; i++)
|
2026-09-11 15:17:38 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const ENUM_LAB_SLOT_STATUS st = m_warmup[i].status;
|
| | | if(st == LAB_SLOT_PLANNED || st == LAB_SLOT_INTENT_SAVED)
|
| | | return(false);
|
| | | }
|
| | | return(true);
|
| | | }
|
| | | bool CExperimentRunner::WarmupExhausted() const
|
| | | {
|
| | | if(m_n_warmup_dispatched >= WarmupRequired())
|
2026-09-11 15:17:38 +03:00 | | | return(true);
|
2026-09-16 15:37:55 +03:00 | | | for(int i = 0; i < m_warmup_count; i++)
|
| | | {
|
| | | const ENUM_LAB_SLOT_STATUS st = m_warmup[i].status;
|
| | | if(st == LAB_SLOT_PLANNED || st == LAB_SLOT_INTENT_SAVED)
|
| | | return(false);
|
2026-09-11 15:17:38 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | return(true);
|
| | | }
|
| | | void CExperimentRunner::SwapBlocks(RequestPlan &arr[], const int a, const int b, const int len)
|
| | | {
|
| | | for(int k = 0; k < len; k++)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | const RequestPlan tmp = arr[a + k];
|
| | | arr[a + k] = arr[b + k];
|
| | | arr[b + k] = tmp;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | }
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-12 22:14:24 +03:00 | | | #endif // REQUEST_LATENCY_LAB_EXPERIMENT_RUNNER_MQH
|
| | | //+------------------------------------------------------------------+
|