624 righe
Nessun fine linea
26 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+ |