mql5-execution-microstructu.../Include/RequestLatencyLab/ExperimentRunner.mqh

624 righe
Nessun fine linea
26 KiB
MQL5

//+------------------------------------------------------------------+
//| ExperimentRunner.mqh |
//| Copyright 2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#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:
ulong m_state;
//--- M = 2147483647 (модуль Park-Miller)
public:
CParkMiller() { m_state = 1; }
void Seed(const ulong s) { m_state = (s % 2147483647ul == 0 ? 1 : s % 2147483647ul); }
ulong Next(void)
{
m_state = (16807ul * m_state) % 2147483647ul;
return(m_state);
}
//--- равномерный индекс [0,b): x=next-1; L=2147483646-(2147483646 mod b);
//--- отклонить x>=L; индекс = x mod b.
int NextUniform(const int b);
};
struct ScheduleSlot
{
RequestPlan plan;
ENUM_LAB_SLOT_STATUS status;
string reason_code;
ulong first_local_us;
ulong last_local_us;
int retry_count; // R4-B7: попытки без фактического send
void Zero(void);
};
class CExperimentRunner
{
private:
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;
//--- квоты E3 (по режиму)
int m_regime_quota; // на условие
int m_regime_used_a;
int m_regime_used_b;
public:
CExperimentRunner(void);
bool Init(const LabSettings &s, LabError &error);
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); }
//--- слоты: каждая ветвь получает ровно count_per_condition запросов,
//--- стороны Buy/Sell чередуются (баланс +/-1); блоки сохраняются как
//--- метки для schedule.csv (block_len = per_condition/10 при стандарте).
//--- R5-S1: малые чётные пилоты (4/10/40) строятся корректно — каждый
//--- слот получает валидный план, нулевые планы не создаются.
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)
{
SetSlotStatusBySequence(sequence, LAB_SLOT_DISPATCHED, "");
OnSlotDispatched(sequence);
}
//--- 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
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)
{
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
{
const string cond = (c == 0 ? "A" : "B");
for(int k = 0; k < block_len && idx < total; k++)
{
const int s = ((k + parity) % 2 == 0 ? 0 : 1); // Buy/Sell чередуются
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.block_id = (uint)(b + 1);
p.role = LAB_ROLE_MAIN;
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);
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;
p.sequence = NextSequence();
p.symbol = m_settings.symbol;
p.volume = 0.0; // заполняется контроллером
p.distance_ticks = (double)m_settings.pending_distance_ticks;
p.magic = m_settings.magic;
raw[idx] = p;
idx++;
}
}
//--- 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--)
{
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;
}
}
}
for(int i = 0; i < total; i++)
{
m_slots[i].Zero();
m_slots[i].plan = raw[i];
m_slots[i].plan.planned_order = (ulong)(i + 1);
}
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++)
{
const string cond = (c == 0 ? "A" : "B");
for(int k = 0; k < per_condition && idx < total; k++)
{
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++;
}
}
m_warmup_count = idx;
return(true);
}
bool CExperimentRunner::NextWarmup(RequestPlan &plan)
{
while(m_next_warmup_idx < m_warmup_count)
{
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);
}
return(false);
}
bool CExperimentRunner::NextMain(RequestPlan &plan)
{
//--- E3: режим окна должен совпадать с квотой условия; управляется
//--- контроллером через TryDispatchByRegime; здесь — следующий слот.
while(m_next_main_idx < m_slot_count)
{
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);
}
return(false);
}
bool CExperimentRunner::TryDispatchByRegime(const ENUM_LAB_MARKET_REGIME regime, RequestPlan &plan)
{
if(regime != LAB_REGIME_QUIET && regime != LAB_REGIME_FAST)
return(false);
//--- R5-B6: выбирается ПЕРВАЯ подходящая возможность нужной ветви,
//--- слот другой ветви впереди не блокирует отправку FAST/QUIET.
int first_pending = -1;
for(int i = 0; i < m_slot_count; i++)
{
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);
}
//--- подходящего слота нет: курсор остаётся на первом ожидающем
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++)
{
if(m_slots[i].plan.sequence != sequence)
continue;
if(m_slots[i].status != LAB_SLOT_DISPATCHED)
return;
const bool is_a = (m_slots[i].plan.condition_id == "A");
if(is_a)
m_regime_used_a++;
else
m_regime_used_b++;
return;
}
}
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++)
{
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);
}
return(false);
}
bool CExperimentRunner::RequeueSlotBySequence(const ulong sequence)
{
for(int i = 0; i < m_slot_count; i++)
{
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);
}
//--- R6-B4: прогревочные слоты возвращаются так же (слот не расходуется)
return(RequeueWarmupBySequence(sequence));
}
void CExperimentRunner::MarkRejectedSlot(const ulong sequence)
{
for(int i = 0; i < m_slot_count; i++)
{
if(m_slots[i].plan.sequence != sequence)
continue;
m_slots[i].retry_count++;
if(m_slots[i].retry_count >= 3)
{
m_slots[i].status = LAB_SLOT_PRECHECK_REJECTED;
m_slots[i].reason_code = "precheck_rejected";
}
else
{
m_slots[i].status = LAB_SLOT_PLANNED;
if(i < m_next_main_idx)
m_next_main_idx = i;
}
return;
}
//--- 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++)
{
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);
}
return(false);
}
void CExperimentRunner::MarkWarmupDispatched(const ulong sequence)
{
for(int j = 0; j < m_warmup_count; j++)
{
if(m_warmup[j].plan.sequence != sequence)
continue;
m_warmup[j].status = LAB_SLOT_DISPATCHED;
return;
}
}
void CExperimentRunner::MarkWarmupRejected(const ulong sequence)
{
for(int j = 0; j < m_warmup_count; j++)
{
if(m_warmup[j].plan.sequence != sequence)
continue;
m_warmup[j].retry_count++;
if(m_warmup[j].retry_count >= 3)
{
m_warmup[j].status = LAB_SLOT_PRECHECK_REJECTED;
m_warmup[j].reason_code = "precheck_rejected";
}
else
{
m_warmup[j].status = LAB_SLOT_PLANNED;
if(j < m_next_warmup_idx)
m_next_warmup_idx = j;
}
return;
}
}
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++)
{
const ENUM_LAB_SLOT_STATUS st = m_slots[i].status;
if(st == LAB_SLOT_PLANNED || st == LAB_SLOT_INTENT_SAVED)
return(false);
}
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++)
{
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())
return(true);
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);
}
return(true);
}
void CExperimentRunner::SwapBlocks(RequestPlan &arr[], const int a, const int b, const int len)
{
for(int k = 0; k < len; k++)
{
const RequestPlan tmp = arr[a + k];
arr[a + k] = arr[b + k];
arr[b + k] = tmp;
}
}
#endif // REQUEST_LATENCY_LAB_EXPERIMENT_RUNNER_MQH
//+------------------------------------------------------------------+