739 lines
No EOL
38 KiB
MQL5
739 lines
No EOL
38 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| RequestLatencyLab.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 "Замер client-observed request latency"
|
|
#property description "Проект: mql5-execution-microstructure-02-request-latency."
|
|
#property description "Режимы: LOCAL_ONLY, PENDING_CREATE_DELETE,"
|
|
#property description "MARKET_ORDER (демо, явный Start оператора)."
|
|
|
|
#include "Include\RequestLatencyLab\Models.mqh"
|
|
#include "Include\RequestLatencyLab\Legacy\BenchmarkStatistics.mqh"
|
|
#include "Include\RequestLatencyLab\Legacy\TradeRequestTools.mqh"
|
|
#include "Include\RequestLatencyLab\Legacy\LifecycleAdapter.mqh"
|
|
#include "Include\RequestLatencyLab\Configuration.mqh"
|
|
#include "Include\RequestLatencyLab\TimeSource.mqh"
|
|
#include "Include\RequestLatencyLab\Statistics.mqh"
|
|
#include "Include\RequestLatencyLab\EventJournal.mqh"
|
|
#include "Include\RequestLatencyLab\MarketRegime.mqh"
|
|
#include "Include\RequestLatencyLab\StateReader.mqh"
|
|
#include "Include\RequestLatencyLab\CompletionPolicy.mqh"
|
|
#include "Include\RequestLatencyLab\RequestTracker.mqh"
|
|
#include "Include\RequestLatencyLab\ExperimentRunner.mqh"
|
|
#include "Include\RequestLatencyLab\CsvStorage.mqh"
|
|
#include "Include\RequestLatencyLab\ReportBuilder.mqh"
|
|
#include "Include\RequestLatencyLab\LatencyPanel.mqh"
|
|
#include "Include\RequestLatencyLab\AppController.mqh"
|
|
//+------------------------------------------------------------------+
|
|
//| Входные параметры |
|
|
//+------------------------------------------------------------------+
|
|
input group "Параметры лаборатории"
|
|
input string InpCampaignId = "RLL-C1"; // идентификатор кампании (префикс сессии и отчёта)
|
|
input ENUM_LAB_EXPERIMENT InpExperiment = LAB_EXP_E1; // эксперимент E1..E5
|
|
input int InpSeries = 1; // серия 1..5
|
|
input ENUM_LAB_RUN_MODE InpRunMode = LAB_RUN_LOCAL_ONLY; // режим: LOCAL_ONLY / PENDING_CREATE_DELETE / MARKET_ORDER
|
|
input string InpSymbol = "EURUSD"; // торговый символ
|
|
input long InpMagic = 20260201; // магический номер (фильтр сделок лаборатории)
|
|
|
|
input group "Торговые параметры"
|
|
input int InpDeviationPoints = 20; // допустимое отклонение цены исполнения, пункты
|
|
input int InpPendingDistanceTicks = 100; // дистанция отложенного ордера, тики (D)
|
|
|
|
input group "Протокол"
|
|
input int InpCountPerCondition = 100; // замеров на условие (5 условий × N)
|
|
input int InpWarmupPerCondition = 10; // прогрев на условие, замеры (чётное; 0 — без прогрева)
|
|
input int InpOutcomeTimeoutMs = 5000; // таймаут ожидания исхода запроса, мс
|
|
input int InpLateGraceMs = 1000; // допустимая задержка позднего исхода, мс
|
|
input int InpPauseMs = 1000; // пауза между замерами, мс
|
|
input int InpTimerMs = 20; // период таймера советника, мс
|
|
input int InpFeatureWindowSec = 60; // окно сбора признаков, сек
|
|
input int InpFeatureMaxAgeMs = 1000; // максимальный возраст признака, мс
|
|
input string InpCalibrationId = ""; // id калибровки (CSV из MarketCalibration; пусто — без калибровки)
|
|
input long InpSeed = 1; // зерно ГПСЧ (воспроизводимость)
|
|
input int InpCleanupMaxAttempts = 1; // попыток очистки старых записей (1..10)
|
|
input int InpE3CampaignLimitMin = 30; // E3: окно кампании, мин (0 — без лимита)
|
|
|
|
input group "Ресурсы"
|
|
input int InpSampleCapacity = 2048; // ёмкость буфера сэмплов (замеров)
|
|
input int InpEventCapacity = 65536; // ёмкость журнала событий
|
|
//--- глобальные компоненты
|
|
CTimeSource ExtClock;
|
|
CEventJournal ExtJournal;
|
|
CRequestTracker ExtTracker;
|
|
CExperimentRunner ExtRunner;
|
|
CAppController ExtController;
|
|
CLatencyPanel ExtPanel;
|
|
LabSettings ExtSettings;
|
|
bool ExtInitialized = false;
|
|
bool ExtStartPressed = false;
|
|
ulong ExtLastTimerUs = 0;
|
|
bool ExtExportDone = false;
|
|
bool ExtCheckpointDone = false; // R13-B2
|
|
//+------------------------------------------------------------------+
|
|
//| Заполнение настроек из входов |
|
|
//+------------------------------------------------------------------+
|
|
void FillSettings(void)
|
|
{
|
|
ExtSettings.Zero();
|
|
ExtSettings.campaign_id = InpCampaignId;
|
|
ExtSettings.session_prefix = InpCampaignId;
|
|
ExtSettings.experiment_id = InpExperiment;
|
|
ExtSettings.series_id = InpSeries;
|
|
ExtSettings.symbol = InpSymbol;
|
|
ExtSettings.magic = (ulong)InpMagic;
|
|
ExtSettings.deviation_points = InpDeviationPoints;
|
|
ExtSettings.pending_distance_ticks = InpPendingDistanceTicks;
|
|
ExtSettings.count_per_condition = InpCountPerCondition;
|
|
ExtSettings.warmup_per_condition = InpWarmupPerCondition;
|
|
ExtSettings.outcome_timeout_ms = InpOutcomeTimeoutMs;
|
|
ExtSettings.late_grace_ms = InpLateGraceMs;
|
|
ExtSettings.pause_ms = InpPauseMs;
|
|
ExtSettings.timer_requested_ms = InpTimerMs;
|
|
ExtSettings.feature_window_sec = InpFeatureWindowSec;
|
|
ExtSettings.feature_max_age_ms = InpFeatureMaxAgeMs;
|
|
ExtSettings.calibration_id = InpCalibrationId;
|
|
ExtSettings.seed = (ulong)InpSeed;
|
|
ExtSettings.cleanup_max_attempts = InpCleanupMaxAttempts;
|
|
ExtSettings.e3_campaign_limit_min = InpE3CampaignLimitMin;
|
|
ExtSettings.sample_capacity = InpSampleCapacity;
|
|
ExtSettings.event_capacity = InpEventCapacity;
|
|
ExtSettings.error_reserve_capacity = 4096;
|
|
ExtSettings.volume_units_tolerance = 1e-7;
|
|
ExtSettings.address_read_budget = 20000;
|
|
ExtSettings.history_scan_budget = 100000;
|
|
ExtSettings.run_mode = InpRunMode;
|
|
ExtSettings.data_origin = (ExtSettings.run_mode == LAB_RUN_LOCAL_ONLY ? "SYNTHETIC" : "DEMO");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Экспорт отчёта в песочницу Files\RequestLatencyLab\... |
|
|
//+------------------------------------------------------------------+
|
|
void ExportRun(void)
|
|
{
|
|
if(ExtExportDone)
|
|
return;
|
|
LabError err;
|
|
const string session = ExtController.SessionId();
|
|
if(StringLen(session) == 0)
|
|
return;
|
|
const string base = StringFormat("%s\\%s\\%s\\", LAB_DATA_ROOT,
|
|
ExtSettings.campaign_id, session);
|
|
FolderCreate(StringSubstr(base, 0, StringLen(base) - 1)); // R13-B1: каталог сессии
|
|
//--- R14-B2: основной экспорт ПЕРЕЗАПИСЫВАЕТ пару samples/events —
|
|
//--- опубликованный checkpoint-маркер снимается ДО изменения данных,
|
|
//--- чтобы обрыв между стадиями не оставил действующий «успех» над
|
|
//--- повреждённой парой; новый маркер публикуется только после записи
|
|
//--- и контрольного чтения всех таблиц (см. конец функции).
|
|
//--- R15-B1: отказ инвалидации ПРОВЕРЯЕТСЯ (как в ExportCheckpoint):
|
|
//--- при неуспехе удаления пары НЕ открываются на перезапись — старый
|
|
//--- marker не остаётся над повреждёнными данными; экспорт повторяется
|
|
//--- на следующем таймере (ExtExportDone не ставится).
|
|
if(FileIsExist(base + "checkpoint.marker"))
|
|
{
|
|
if(!FileDelete(base + "checkpoint.marker"))
|
|
{
|
|
Print("RequestLatencyLab: export marker invalidation FAILED");
|
|
ExtController.SetCheckpointState(false);
|
|
return;
|
|
}
|
|
}
|
|
ExtCheckpointDone = false;
|
|
//--- R8-B3: safe-фаза — до экспорта дописываются накопленные SEND_RETURN
|
|
//--- durable (после закрытия измерительных окон); сбой — повторная
|
|
//--- попытка экспорта на следующем таймере.
|
|
LabError ferr;
|
|
if(!ExtController.FlushPendingSendReturns(ferr))
|
|
{
|
|
Print("RequestLatencyLab: durable SEND_RETURN flush FAILED: " + ferr.message);
|
|
return;
|
|
}
|
|
//--- R9-S3: safe-фаза — после SEND_RETURN дописываются сырые тики
|
|
//--- окна E3, оставшиеся в памяти (в т.ч. при остановке до закрытия
|
|
//--- sample).
|
|
LabError terr;
|
|
if(!ExtController.FlushWindowTicks(terr))
|
|
{
|
|
Print("RequestLatencyLab: window ticks flush FAILED: " + terr.message);
|
|
return;
|
|
}
|
|
//--- R10-B4: контракт экспорта - market_ticks.csv существует для сессии
|
|
//--- (заголовок; строки тиков окна дописаны выше). Fail-closed при сбое.
|
|
if(!ExtController.EnsureMarketTicksFile())
|
|
{
|
|
Print("RequestLatencyLab: market_ticks.csv ensure FAILED");
|
|
return;
|
|
}
|
|
RunManifest m;
|
|
m.Zero();
|
|
m.campaign_id = ExtSettings.campaign_id;
|
|
m.session_id = session;
|
|
m.experiment_id = ExtSettings.experiment_id;
|
|
m.series_id = (uint)ExtSettings.series_id;
|
|
m.data_origin = ExtSettings.data_origin;
|
|
m.seed = ExtSettings.seed;
|
|
m.run_status = ExtController.StateName();
|
|
m.stop_reason = ExtController.TerminationReason();
|
|
m.n_main_dispatched = ExtRunner.MainDispatched();
|
|
m.n_warmup_dispatched = ExtRunner.WarmupDispatched();
|
|
m.n_cleanup_dispatched = ExtRunner.CleanupDispatched();
|
|
m.n_dispatch_uncertain = ExtRunner.DispatchUncertain();
|
|
//--- R6-B6: неопределённые попытки из durable-журнала сессии
|
|
//--- (INTENT без SEND_RETURN) учитываются в манифесте качества.
|
|
//--- R7-S3: нечитаемость журнала даёт sentinel ULONG_MAX; сумма не
|
|
//--- переполняется молча (значение остаётся «неопределённость»).
|
|
const ulong du_now = ExtController.CountUncertainDurableIntents(session);
|
|
if(du_now >= ULONG_MAX || m.n_dispatch_uncertain > ULONG_MAX - du_now)
|
|
m.n_dispatch_uncertain = ULONG_MAX;
|
|
else
|
|
m.n_dispatch_uncertain += du_now;
|
|
//--- R4-S5: машинно читаемый статус: недобор/отказ = INCOMPLETE,
|
|
//--- а не завершённая серия.
|
|
m.run_status = (StringLen(ExtController.TerminationReason()) > 0 ?
|
|
"INCOMPLETE" : ExtController.StateName());
|
|
string path = base + "manifest.csv";
|
|
bool export_ok = true;
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildManifest(m, ExtSettings, ExtController.Rules()), err))
|
|
export_ok = false;
|
|
path = base + "samples.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildSamples(&ExtTracker, ExtSettings.campaign_id,
|
|
ExtSettings.configuration_id), err))
|
|
export_ok = false;
|
|
path = base + "events.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildEvents(&ExtJournal), err))
|
|
export_ok = false;
|
|
path = base + "schedule.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildSchedule(&ExtRunner, session), err))
|
|
export_ok = false;
|
|
path = base + "deals.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildDeals(&ExtTracker, session), err))
|
|
export_ok = false;
|
|
path = base + "summary.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildSummary(&ExtTracker, session, ExtSettings), err))
|
|
export_ok = false;
|
|
path = base + "histogram.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildHistogram(&ExtTracker, session, ExtSettings), err))
|
|
export_ok = false;
|
|
//--- R4-B8: знаковая метрика T2-T4 и сравнение A/B (отдельные файлы)
|
|
path = base + "offset_signs.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildOffsetSigns(&ExtTracker, session, ExtSettings), err))
|
|
export_ok = false;
|
|
path = base + "comparisons.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildComparisons(&ExtTracker, session, ExtSettings), err))
|
|
export_ok = false;
|
|
path = base + "market_windows.csv";
|
|
MarketWindow wins[];
|
|
const int nwin = ExtController.MarketWindowCount();
|
|
if(ArrayResize(wins, nwin) != nwin)
|
|
Print("RequestLatencyLab: market windows array resize failed");
|
|
else
|
|
{
|
|
for(int i = 0; i < nwin; i++)
|
|
ExtController.GetMarketWindow(i, wins[i]);
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildMarketWindows(wins, nwin, session), err))
|
|
export_ok = false;
|
|
}
|
|
path = base + "checks.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildRunChecks(&ExtTracker, &ExtJournal, &ExtRunner, ExtSettings), err))
|
|
export_ok = false;
|
|
path = base + "experiment_manifest.csv";
|
|
if(!CCsv::SaveUtf8(path, CCsvReport::BuildExperimentManifest(&ExtRunner, session, ExtSettings), err))
|
|
export_ok = false;
|
|
//--- R4-S5: контрольное чтение участвует в итоговом статусе экспорта;
|
|
//--- при любой ошибке записи/чтения ExtExportDone НЕ ставится, чтобы
|
|
//--- повторная попытка (следующий OnTimer) была возможна.
|
|
if(export_ok && !CCsv::ReadBackVerifyFile(base + "manifest.csv", 39, err))
|
|
{
|
|
Print("RequestLatencyLab: manifest read-back FAILED: " + err.message);
|
|
export_ok = false;
|
|
}
|
|
if(export_ok && !CCsv::ReadBackVerifyFile(base + "samples.csv",
|
|
ExtTracker.Count() + 1, err))
|
|
{
|
|
Print("RequestLatencyLab: samples read-back FAILED: " + err.message);
|
|
export_ok = false;
|
|
}
|
|
if(export_ok && !CCsv::ReadBackVerifyFile(base + "deals.csv",
|
|
ExtTracker.DealCount() + 1, err))
|
|
{
|
|
Print("RequestLatencyLab: deals read-back FAILED: " + err.message);
|
|
export_ok = false;
|
|
}
|
|
if(!export_ok)
|
|
{
|
|
Print("RequestLatencyLab: EXPORT WRITE FAILED: " + err.message);
|
|
return; // повторная попытка на следующем таймере
|
|
}
|
|
//--- R14-B2: подтверждение полного экспорта — финальный marker
|
|
//--- публикуется ПОСЛЕДНИМ (после успешной записи и контрольного
|
|
//--- чтения всех таблиц); при сбое публикации экспорт НЕ завершён.
|
|
LabError merr;
|
|
if(!PublishCheckpointMarker(base, merr))
|
|
{
|
|
Print("RequestLatencyLab: final marker publish FAILED: " + merr.message);
|
|
ExtController.SetCheckpointState(false);
|
|
return;
|
|
}
|
|
ExtController.SetCheckpointState(true);
|
|
ExtCheckpointDone = true;
|
|
ExtExportDone = true;
|
|
PrintFormat("RequestLatencyLab: отчёт сохранён в MQL5\\Files\\%s",
|
|
base);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Публикация маркера согласованности (R8-S3/R13-B2/R14-B2) |
|
|
//+------------------------------------------------------------------+
|
|
//--- Пишется ТОЛЬКО ПОСЛЕ полной записи пары samples/events; отсутствие
|
|
//--- или смена поколения marker = пара файлов записана не целиком.
|
|
//--- Контрольное чтение подтверждает публикацию (R13-B2), иначе false.
|
|
//--- Единственная точка записи marker: ни одна стадия финализации НЕ
|
|
//--- перезаписывает уже опубликованную пару повторно (R14-B2).
|
|
bool PublishCheckpointMarker(const string base, LabError &err)
|
|
{
|
|
err.Reset();
|
|
err.component = LAB_COMP_CSV;
|
|
static ulong cp_gen = 0; // R8-S3: поколение checkpoint
|
|
cp_gen++;
|
|
const ulong now = ExtClock.NowUs();
|
|
if(!CCsv::SaveUtf8(base + "checkpoint.marker",
|
|
StringFormat("samples_and_events_ok\r\ngen=%I64u\r\n%I64u\r\n",
|
|
cp_gen, now), err))
|
|
return(false);
|
|
return(CCsv::ReadBackVerifyFile(base + "checkpoint.marker", 3, err));
|
|
}
|
|
//+--------------------------------------------------------------------+
|
|
//| Синхронизация фазы панели (R11): WARMUP->MAIN и счётчики за лёгкое |
|
|
//| обновление (перерисовка только при смене фазы) |
|
|
//+--------------------------------------------------------------------+
|
|
void SyncPanelPhase(void)
|
|
{
|
|
if(!ExtInitialized || !ExtPanel.IsCreated() || !ExtController.IsRunning())
|
|
return;
|
|
static string g_last_phase = "";
|
|
string disp = ExtController.StateName();
|
|
if(disp == "WARMUP")
|
|
disp = StringFormat("WARMUP disp=%I64u", ExtRunner.WarmupDispatched());
|
|
else
|
|
if(disp == "MAIN")
|
|
disp = StringFormat("MAIN disp=%I64u", ExtRunner.MainDispatched());
|
|
if(disp == g_last_phase)
|
|
return;
|
|
g_last_phase = disp;
|
|
ExtPanel.SetStatus(disp);
|
|
//--- R13-S2: внутри открытого измерительного окна принудительная
|
|
//--- перерисовка (ChartRedraw) запрещена — обновляется только текст
|
|
//--- панели; отрисовка откладывается до безопасной фазы (паузы).
|
|
ExtPanel.Update(ExtController.InMeasurementWindow());
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Контрольное сохранение (R5-B2): периодический checkpoint |
|
|
//+------------------------------------------------------------------+
|
|
bool ExportCheckpoint(const bool force)
|
|
{
|
|
//--- R6-B6: нет открытого окна MAIN и незавершённой очистки — иначе
|
|
//--- пропускаем (продолжается пауза); тяжёлый экспорт вне измерения.
|
|
//--- R13-B2: force-финализация не подтверждена, пока safe-фаза недостижима
|
|
//--- (повтор на следующем таймере); периодический пропуск штатен.
|
|
if(!ExtController.CanCheckpoint())
|
|
return(!force);
|
|
const ulong now = ExtClock.NowUs();
|
|
//--- R11-S1: периодическое сохранение имеет порог 60 с; ФИНАЛЬНЫЙ
|
|
//--- checkpoint (force) пишется всегда - короткий пилот не зависит
|
|
//--- от накопления виртуального времени.
|
|
static ulong last_cp = 1;
|
|
if(!force)
|
|
{
|
|
if(last_cp != 0 && now - last_cp < 60000000UL)
|
|
return(true);
|
|
last_cp = now;
|
|
}
|
|
LabError err;
|
|
const string session = ExtController.SessionId();
|
|
if(StringLen(session) == 0)
|
|
return(false);
|
|
const string base = StringFormat("%s\\%s\\%s\\", LAB_DATA_ROOT,
|
|
ExtSettings.campaign_id, session);
|
|
FolderCreate(StringSubstr(base, 0, StringLen(base) - 1)); // R13-B1: (см. ExportRun)
|
|
//--- R6-B6: тяжёлое сохранение ТОЛЬКО в безопасной фазе (см. CanCheckpoint);
|
|
//--- внутри открытого измерительного окна checkpoint не пишется.
|
|
bool ok = true;
|
|
//--- R9-B4: старый marker инвалидируется ДО ЛЮБЫХ перезаписей данных:
|
|
//--- пока новая пара файлов не записана целиком, устаревшего успеха
|
|
//--- не существует (прерывание после samples/events не оставляет
|
|
//--- ложный marker старого поколения); при неуспехе удаления файлы
|
|
//--- НЕ трогаются, сохранность неподтверждена — отправки блокируются.
|
|
if(FileIsExist(base + "checkpoint.marker"))
|
|
{
|
|
if(!FileDelete(base + "checkpoint.marker"))
|
|
{
|
|
Print("RequestLatencyLab: checkpoint marker invalidation FAILED");
|
|
ExtController.SetCheckpointState(false);
|
|
ExtController.BlockRun("checkpoint marker invalidate failed");
|
|
return(false);
|
|
}
|
|
}
|
|
if(ok)
|
|
ok = CCsv::SaveUtf8(base + "samples.csv",
|
|
CCsvReport::BuildSamples(&ExtTracker, ExtSettings.campaign_id,
|
|
ExtSettings.configuration_id), err);
|
|
if(ok)
|
|
ok = CCsv::SaveUtf8(base + "events.csv", CCsvReport::BuildEvents(&ExtJournal), err);
|
|
//--- R8-S3: старый marker удаляется ДО перезаписи — при сбое между
|
|
//--- фазами остаётся ОТСУТСТВУЮЩИЙ marker (несогласованная пара), а не
|
|
//--- устаревший «успех» из прежнего поколения.
|
|
if(FileIsExist(base + "checkpoint.marker"))
|
|
FileDelete(base + "checkpoint.marker");
|
|
//--- R14-B2: marker публикуется ОДИН раз после полной записи пары
|
|
//--- (общий helper с финальным экспортом) — ни одна стадия не
|
|
//--- перезаписывает уже опубликованную пару без снятия marker.
|
|
if(ok)
|
|
ok = PublishCheckpointMarker(base, err);
|
|
if(!ok)
|
|
{
|
|
Print("RequestLatencyLab: CHECKPOINT WRITE FAILED: " + err.message);
|
|
//--- R6-B6: неподтверждённая сохранность останавливает новые отправки
|
|
ExtController.SetCheckpointState(false);
|
|
ExtController.BlockRun("checkpoint write failed");
|
|
return(false);
|
|
}
|
|
ExtController.SetCheckpointState(true);
|
|
return(true);
|
|
}
|
|
//+-------------------------------------------------------------------+
|
|
//| Единая финализация (R13-B2): финальный checkpoint + основной |
|
|
//| экспорт под ОДНИМ проверяемым результатом; повторяется именно |
|
|
//| неуспешная стадия. FINISHED/кнопка - только после полного набора. |
|
|
//+-------------------------------------------------------------------+
|
|
bool FinalizeRun(void)
|
|
{
|
|
if(ExtExportDone)
|
|
return(true);
|
|
if(!ExtCheckpointDone)
|
|
{
|
|
if(!ExportCheckpoint(true))
|
|
{
|
|
const string crank = ExtController.BlockReason();
|
|
ExtPanel.SetStatus(crank == "" ?
|
|
"CHECKPOINT_FAILED: финальный checkpoint не сохранён" :
|
|
"CHECKPOINT_FAILED: " + crank);
|
|
ExtPanel.SetStarted(true);
|
|
ExtPanel.Update(false);
|
|
return(false);
|
|
}
|
|
ExtCheckpointDone = true;
|
|
}
|
|
ExportRun();
|
|
if(!ExtExportDone)
|
|
{
|
|
ExtPanel.SetStatus("EXPORT_FAILED: повторная попытка на таймере");
|
|
ExtPanel.SetStarted(true);
|
|
ExtPanel.Update(false);
|
|
return(false);
|
|
}
|
|
const string freason = ExtController.TerminationReason();
|
|
ExtPanel.SetStatus(freason == "" ? "FINISHED" : "FINISHED: " + freason);
|
|
ExtPanel.SetStarted(false);
|
|
ExtPanel.Update(false);
|
|
return(true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Инициализация |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit(void)
|
|
{
|
|
FillSettings();
|
|
ExtController.SetSettings(ExtSettings);
|
|
ExtController.Configure(&ExtTracker, &ExtJournal, &ExtRunner, &ExtClock);
|
|
if(ExtSettings.run_mode == LAB_RUN_LOCAL_ONLY)
|
|
{
|
|
//--- синтетические часы отделены от live-счётчика
|
|
ExtClock.UseSynthetic(1000000);
|
|
}
|
|
else
|
|
{
|
|
ExtClock.UseLive();
|
|
}
|
|
LabError err;
|
|
if(!ExtController.Preflight(err))
|
|
{
|
|
PrintFormat("RequestLatencyLab: PREFLIGHT FAILED [%s]",
|
|
LabResultString(false, err));
|
|
return(INIT_PARAMETERS_INCORRECT);
|
|
}
|
|
//--- R5-S2: configuration_id вычислен внутри Preflight (копия настроек);
|
|
//--- переносим в глобальные настройки для экспорта manifest/samples.
|
|
ExtSettings.configuration_id = ExtController.ConfigurationId();
|
|
if(!ExtController.InitRun(err))
|
|
{
|
|
PrintFormat("RequestLatencyLab: INIT FAILED [%s]", LabResultString(false, err));
|
|
return(INIT_FAILED);
|
|
}
|
|
//--- E3: calibration load (Scripts/MarketCalibration.mq5)
|
|
if(ExtSettings.experiment_id == LAB_EXP_E3 && ExtSettings.run_mode != LAB_RUN_LOCAL_ONLY)
|
|
{
|
|
LabError cerr;
|
|
Calibration cal;
|
|
if(CCsvReport::LoadCalibration(ExtSettings.calibration_id, cal, cerr))
|
|
{
|
|
ExtController.SetCalibration(cal);
|
|
PrintFormat("RequestLatencyLab: calibration loaded id=%s windows=%d",
|
|
cal.calibration_id, cal.valid_window_count);
|
|
}
|
|
else
|
|
PrintFormat("RequestLatencyLab: calibration NOT loaded: %s", cerr.message);
|
|
}
|
|
ExtPanel.Create(ChartID());
|
|
ExtPanel.SetStatus("READY");
|
|
ExtPanel.Update(false);
|
|
if(!EventSetMillisecondTimer((uint)ExtSettings.timer_requested_ms))
|
|
{
|
|
PrintFormat("RequestLatencyLab: EventSetMillisecondTimer FAILED err=%d",
|
|
GetLastError());
|
|
return(INIT_FAILED);
|
|
}
|
|
ExtInitialized = true;
|
|
PrintFormat("RequestLatencyLab: ready. mode=%s symbol=%s config=%s",
|
|
(ExtSettings.run_mode == LAB_RUN_LOCAL_ONLY ? "LOCAL_ONLY" : "TRADING"),
|
|
ExtSettings.symbol, ExtSettings.configuration_id);
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Деинициализация |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
EventKillTimer();
|
|
ExtController.StopRun();
|
|
//--- R5-B2: штатное снятие советника не теряет журнал и незавершённые
|
|
//--- записи — последний доступный снимок экспортируется.
|
|
ExportRun();
|
|
ExtPanel.Destroy();
|
|
ExtInitialized = false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| OnTick: только сигнал котировки; расчёт не выполняется |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick(void)
|
|
{
|
|
//--- R6-S2/R7-B5: фиксация момента наблюдения котировки КОНКРЕТНОГО
|
|
//--- рабочего символа (InpSymbol), а не символа графика: NewTick
|
|
//--- относится к символу графика [API3] и при другом рабочем символе
|
|
//--- не доказывает обновления. Свежесть считается по новым time_msc.
|
|
if(ExtInitialized)
|
|
{
|
|
MqlTick qt;
|
|
if(SymbolInfoTick(ExtSettings.symbol, qt) && qt.time_msc > 0)
|
|
ExtController.NoteQuoteObserved((long)qt.time_msc);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| OnTimer: резервная сверка, сроки, разрешённые шаги контроллера |
|
|
//+------------------------------------------------------------------+
|
|
void OnTimer(void)
|
|
{
|
|
if(!ExtInitialized)
|
|
return;
|
|
const ulong now = ExtClock.NowUs();
|
|
const ulong elapsed = (ExtLastTimerUs == 0 ? 0 : now - ExtLastTimerUs);
|
|
ExtLastTimerUs = now;
|
|
LabError err;
|
|
//--- R13-S1: терминальный BLOCKED отображается СРАЗУ — без зависимости
|
|
//--- от признака ожидающей команды (латч уже мог быть потреблён); причина
|
|
//--- в статусе, кнопка отпущена, новая торговля до устранения запрещена.
|
|
if(ExtController.State() == LAB_STATE_BLOCKED)
|
|
{
|
|
ExtStartPressed = false;
|
|
ExtPanel.SetStarted(false);
|
|
ExtPanel.SetStatus("BLOCKED: " + ExtController.BlockReason());
|
|
ExtPanel.Update(false);
|
|
return;
|
|
}
|
|
//--- R5-B2: периодическое контрольное сохранение (с порогом 60 с)
|
|
if(ExtController.IsRunning())
|
|
ExportCheckpoint(false);
|
|
//--- R11-B2: таймер ОДНОКРАТНО потребляет сохранённую команду Start.
|
|
//--- Latch ставит OnChartEvent СИНХРОННО по имени объекта; независимый
|
|
//--- опрос OBJPROP_STATE для запуска убран (один физический клик = один
|
|
//--- StartRun). Источник истины - внутренний ExtStartPressed.
|
|
if(ExtStartPressed)
|
|
{
|
|
ExtStartPressed = false; // команда потреблена один раз
|
|
ExtExportDone = false; // R11-B1: новый прогон - новый экспорт
|
|
ExtCheckpointDone = false; // R13-B2: новая сессия - новый финальный checkpoint
|
|
ExtPanel.SetStarted(true); // кнопка остаётся нажатой/заблокированной
|
|
if(ExtController.StartRun(err))
|
|
{
|
|
//--- R13-B1/B2: синхронный финал (и прерванный синтетический
|
|
//--- прогон с сохранённым контекстом) завершается ЕДИНОЙ
|
|
//--- финализацией: checkpoint + экспорт + статус/кнопка.
|
|
if(ExtController.State() == LAB_STATE_FINISHED)
|
|
{
|
|
FinalizeRun();
|
|
return;
|
|
}
|
|
SyncPanelPhase(); // R11: фаза WARMUP/MAIN + счётчик
|
|
}
|
|
else
|
|
{
|
|
ExtPanel.SetStatus("START_FAILED: " + err.message);
|
|
ExtPanel.SetStarted(false);
|
|
}
|
|
ExtPanel.Update(false);
|
|
if(ExtController.State() == LAB_STATE_FINISHED)
|
|
return;
|
|
}
|
|
if(ExtController.State() == LAB_STATE_FINISHED)
|
|
{
|
|
//--- R5-B2/R11-B3/R13-B2: повтор НЕУСПЕШНОЙ стадии финализации
|
|
//--- (финальный checkpoint или основной экспорт) на каждом таймере;
|
|
//--- FINISHED и разблокировка кнопки - только после полного набора.
|
|
if(!ExtExportDone)
|
|
FinalizeRun();
|
|
else
|
|
if(ExtStartPressed)
|
|
{
|
|
ExtStartPressed = false;
|
|
ExtPanel.SetStarted(false);
|
|
}
|
|
return;
|
|
}
|
|
//--- резервный таймер
|
|
ExtController.OnTimer(elapsed, err);
|
|
if(ExtController.IsRunning())
|
|
{
|
|
//--- отложенный запуск следующего шага, если пауза прошла
|
|
ExtController.DriveNext(err);
|
|
if(ExtController.State() == LAB_STATE_FINISHED)
|
|
{
|
|
//--- формальное завершение (напр. E3 INSUFFICIENT_DATA)
|
|
FinalizeRun();
|
|
return;
|
|
}
|
|
if(ExtController.State() == LAB_STATE_DRAINING)
|
|
{
|
|
//--- сбор завершён: очистка при известном торговом состоянии
|
|
LabError cerr;
|
|
if(ExtController.EndMainCycle(cerr))
|
|
{
|
|
ExtController.RequirePause();
|
|
//--- R5-B1/B2: завершение серии ТОЛЬКО без незавершённого cleanup;
|
|
//--- состояние контроллера переводится в FINISHED (manifest
|
|
//--- run_status=FINISHED), а не остаётся DRAINING.
|
|
if(!ExtController.CleanupInFlight() &&
|
|
ExtController.Runner().MainComplete() &&
|
|
ExtController.Runner().WarmupComplete())
|
|
{
|
|
ExtController.StopRun();
|
|
ExtController.MarkFinished();
|
|
FinalizeRun();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
SyncPanelPhase(); // R11: фаза WARMUP/MAIN + счётчик
|
|
ExtPanel.Update(true);
|
|
return;
|
|
}
|
|
ExtPanel.Update(false);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| OnTradeTransaction: вход, сохранение полей, быстрая корреляция |
|
|
//+------------------------------------------------------------------+
|
|
void OnTradeTransaction(const MqlTradeTransaction &trans,
|
|
const MqlTradeRequest &request,
|
|
const MqlTradeResult &result)
|
|
{
|
|
if(!ExtInitialized)
|
|
return;
|
|
TransactionObservation obs;
|
|
obs.Zero();
|
|
obs.entry_us = ExtClock.NowUs();
|
|
obs.trans = trans;
|
|
obs.request = request;
|
|
obs.result = result;
|
|
obs.has_request = (trans.type == TRADE_TRANSACTION_REQUEST);
|
|
obs.has_result = true;
|
|
LabError err;
|
|
ExtController.OnTransaction(obs, err);
|
|
obs.handler_exit_us = ExtClock.NowUs();
|
|
ExtController.FinalizeTransaction(obs, err);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| OnChartEvent: кнопка панели (R10-B1 - latch команды Start) |
|
|
//+------------------------------------------------------------------+
|
|
void OnChartEvent(const int id, const long &lparam, const double &dparam,
|
|
const string &sparam)
|
|
{
|
|
if(!ExtInitialized)
|
|
return;
|
|
if(id == CHARTEVENT_OBJECT_CLICK && ExtPanel.IsCreated())
|
|
{
|
|
const string obj = sparam;
|
|
//--- R10-B1/R11-B2: команда Start захватывается СИНХРОННО в OnChartEvent
|
|
//--- по имени объекта (latch ExtStartPressed). Кнопка остаётся
|
|
//--- нажатой/заблокированной весь прогон; фаза и итог выводятся в
|
|
//--- Status; исполняет команду РОВНО один раз OnTimer (независимый
|
|
//--- опрос OBJPROP_STATE убран - один клик = один StartRun).
|
|
if(obj == LAB_PANEL_PREFIX + "Start")
|
|
{
|
|
//--- терминальная блокировка: причина в статусе, кнопка не нажимается
|
|
if(ExtController.State() == LAB_STATE_BLOCKED)
|
|
{
|
|
ExtPanel.SetStatus("BLOCKED: " + ExtController.BlockReason());
|
|
ExtPanel.SetStarted(false);
|
|
ExtPanel.Update(false);
|
|
ChartRedraw();
|
|
return;
|
|
}
|
|
//--- R11-B3: повторный запуск после финализации разрешён ТОЛЬКО
|
|
//--- если отчёт успешно сохранён (иначе - ошибка, перезапуск запрещён).
|
|
if(ExtController.State() == LAB_STATE_FINISHED)
|
|
{
|
|
if(!ExtExportDone)
|
|
{
|
|
ExtPanel.SetStatus("EXPORT_FAILED: отчёт не сохранён; перезапуск запрещён");
|
|
ExtPanel.SetStarted(true);
|
|
ExtPanel.Update(false);
|
|
ChartRedraw();
|
|
return;
|
|
}
|
|
ExtController.ResetForRestart(); // R10-B1: подготовка нового прогона
|
|
}
|
|
//--- R11-B2: повторный/двойной клик и клики во время прогона
|
|
//--- игнорируются (одна команда на прогон; текущую не отменяет).
|
|
if(ExtStartPressed ||
|
|
(ExtController.State() != LAB_STATE_READY &&
|
|
ExtController.State() != LAB_STATE_IDLE))
|
|
{
|
|
//--- R13-S1/R14-S1: OBJ_BUTTON сам отжал кнопку кликом —
|
|
//--- наглядное состояние восстанавливается из намеренного, но
|
|
//--- БЕЗ принудительной перерисовки внутри открытого
|
|
//--- измерительного окна (ChartRedraw откладывается до паузы).
|
|
ExtPanel.RestoreStartedState();
|
|
const bool in_win = ExtController.InMeasurementWindow();
|
|
ExtPanel.Update(in_win);
|
|
if(!in_win)
|
|
ChartRedraw();
|
|
return;
|
|
}
|
|
//--- команда принята: latch, кнопка нажата и заблокирована
|
|
ExtStartPressed = true;
|
|
ExtPanel.SetStarted(true);
|
|
ExtPanel.SetStatus("Run…");
|
|
ExtPanel.Update(false);
|
|
ChartRedraw();
|
|
return;
|
|
}
|
|
//--- R15-S1: клик по ЧУЖОМУ объекту графика (не кнопке панели) не
|
|
//--- вызывает принудительную перерисовку внутри открытого окна
|
|
//--- измерения — тот же безопасный режим, что в ветви занятого Start
|
|
//--- (ChartRedraw откладывается до паузы).
|
|
const bool owin = ExtController.InMeasurementWindow();
|
|
ExtPanel.Update(owin);
|
|
if(!owin)
|
|
ChartRedraw();
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+ |