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