2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| RequestLatencyLab.mq5 |
|
2026-09-16 15:37:55 +03:00 | | | //| Copyright 2026, MetaQuotes Ltd. |
|
2026-09-10 16:43:28 +03:00 | | | //| www.mql5.com |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-16 15:37:55 +03:00 | | | #property copyright "Copyright 2026, MetaQuotes Ltd."
|
2026-09-10 16:43:28 +03:00 | | | #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 оператора)."
|
2026-09-09 01:19:31 +03:00 | | |
|
| | | #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"
|
2026-09-10 16:43:28 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| Входные параметры |
|
| | | //+------------------------------------------------------------------+
|
2026-09-09 01:19:31 +03:00 | | | input group "Параметры лаборатории"
|
2026-09-12 22:14:24 +03:00 | | | 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; // магический номер (фильтр сделок лаборатории)
|
2026-09-10 16:43:28 +03:00 | | |
|
2026-09-09 01:19:31 +03:00 | | | input group "Торговые параметры"
|
2026-09-12 22:14:24 +03:00 | | | input int InpDeviationPoints = 20; // допустимое отклонение цены исполнения, пункты
|
| | | input int InpPendingDistanceTicks = 100; // дистанция отложенного ордера, тики (D)
|
2026-09-10 16:43:28 +03:00 | | |
|
2026-09-09 01:19:31 +03:00 | | | input group "Протокол"
|
2026-09-12 22:14:24 +03:00 | | | 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 — без лимита)
|
2026-09-10 16:43:28 +03:00 | | |
|
2026-09-09 01:19:31 +03:00 | | | input group "Ресурсы"
|
2026-09-12 22:14:24 +03:00 | | | input int InpSampleCapacity = 2048; // ёмкость буфера сэмплов (замеров)
|
| | | input int InpEventCapacity = 65536; // ёмкость журнала событий
|
2026-09-09 01:19:31 +03:00 | | | //--- глобальные компоненты
|
2026-09-10 16:43:28 +03:00 | | | CTimeSource ExtClock;
|
| | | CEventJournal ExtJournal;
|
| | | CRequestTracker ExtTracker;
|
| | | CExperimentRunner ExtRunner;
|
| | | CAppController ExtController;
|
| | | CLatencyPanel ExtPanel;
|
| | | LabSettings ExtSettings;
|
2026-09-12 22:14:24 +03:00 | | | bool ExtInitialized = false;
|
| | | bool ExtStartPressed = false;
|
| | | ulong ExtLastTimerUs = 0;
|
| | | bool ExtExportDone = false;
|
| | | bool ExtCheckpointDone = false; // R13-B2
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| Заполнение настроек из входов |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | void FillSettings(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-10 16:43:28 +03:00 | | | ExtSettings.Zero();
|
2026-09-12 22:14:24 +03:00 | | | 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");
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| Экспорт отчёта в песочницу Files\RequestLatencyLab\... |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | void ExportRun(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-10 16:43:28 +03:00 | | | if(ExtExportDone)
|
2026-09-09 01:19:31 +03:00 | | | return;
|
2026-09-10 16:43:28 +03:00 | | | LabError err;
|
2026-09-12 22:14:24 +03:00 | | | const string session = ExtController.SessionId();
|
| | | if(StringLen(session) == 0)
|
2026-09-09 01:19:31 +03:00 | | | return;
|
2026-09-12 22:14:24 +03:00 | | | 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"))
|
2026-09-12 16:38:37 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | if(!FileDelete(base + "checkpoint.marker"))
|
2026-09-12 16:38:37 +03:00 | | | {
|
| | | Print("RequestLatencyLab: export marker invalidation FAILED");
|
| | | ExtController.SetCheckpointState(false);
|
| | | return;
|
| | | }
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | ExtCheckpointDone = false;
|
| | | //--- R8-B3: safe-фаза — до экспорта дописываются накопленные SEND_RETURN
|
| | | //--- durable (после закрытия измерительных окон); сбой — повторная
|
| | | //--- попытка экспорта на следующем таймере.
|
2026-09-11 18:16:32 +03:00 | | | LabError ferr;
|
| | | if(!ExtController.FlushPendingSendReturns(ferr))
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: durable SEND_RETURN flush FAILED: " + ferr.message);
|
2026-09-11 18:16:32 +03:00 | | | return;
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- R9-S3: safe-фаза — после SEND_RETURN дописываются сырые тики
|
| | | //--- окна E3, оставшиеся в памяти (в т.ч. при остановке до закрытия
|
| | | //--- sample).
|
2026-09-11 21:20:36 +03:00 | | | LabError terr;
|
| | | if(!ExtController.FlushWindowTicks(terr))
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: window ticks flush FAILED: " + terr.message);
|
2026-09-11 21:20:36 +03:00 | | | return;
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- R10-B4: контракт экспорта - market_ticks.csv существует для сессии
|
| | | //--- (заголовок; строки тиков окна дописаны выше). Fail-closed при сбое.
|
2026-09-12 02:21:14 +03:00 | | | if(!ExtController.EnsureMarketTicksFile())
|
| | | {
|
| | | Print("RequestLatencyLab: market_ticks.csv ensure FAILED");
|
| | | return;
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | RunManifest m;
|
| | | m.Zero();
|
2026-09-12 22:14:24 +03:00 | | | 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;
|
2026-09-11 15:17:38 +03:00 | | | else
|
2026-09-12 22:14:24 +03:00 | | | 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";
|
2026-09-10 16:43:28 +03:00 | | | MarketWindow wins[];
|
2026-09-12 22:14:24 +03:00 | | | const int nwin = ExtController.MarketWindowCount();
|
| | | if(ArrayResize(wins, nwin) != nwin)
|
2026-09-10 16:43:28 +03:00 | | | Print("RequestLatencyLab: market windows array resize failed");
|
| | | else
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | 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;
|
2026-09-10 16:43:28 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | 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))
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: manifest read-back FAILED: " + err.message);
|
| | | export_ok = false;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | if(export_ok && !CCsv::ReadBackVerifyFile(base + "samples.csv",
|
| | | ExtTracker.Count() + 1, err))
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: samples read-back FAILED: " + err.message);
|
| | | export_ok = false;
|
2026-09-10 22:54:26 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | if(export_ok && !CCsv::ReadBackVerifyFile(base + "deals.csv",
|
| | | ExtTracker.DealCount() + 1, err))
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: deals read-back FAILED: " + err.message);
|
| | | export_ok = false;
|
2026-09-10 22:54:26 +03:00 | | | }
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | if(!export_ok)
|
2026-09-10 22:54:26 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: EXPORT WRITE FAILED: " + err.message);
|
2026-09-10 22:54:26 +03:00 | | | return; // повторная попытка на следующем таймере
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- R14-B2: подтверждение полного экспорта — финальный marker
|
| | | //--- публикуется ПОСЛЕДНИМ (после успешной записи и контрольного
|
| | | //--- чтения всех таблиц); при сбое публикации экспорт НЕ завершён.
|
2026-09-12 12:06:55 +03:00 | | | LabError merr;
|
2026-09-12 22:14:24 +03:00 | | | if(!PublishCheckpointMarker(base, merr))
|
2026-09-12 12:06:55 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: final marker publish FAILED: " + merr.message);
|
2026-09-12 12:06:55 +03:00 | | | ExtController.SetCheckpointState(false);
|
| | | return;
|
| | | }
|
| | | ExtController.SetCheckpointState(true);
|
2026-09-12 22:14:24 +03:00 | | | ExtCheckpointDone = true;
|
| | | ExtExportDone = true;
|
2026-09-09 01:19:31 +03:00 | | | PrintFormat("RequestLatencyLab: отчёт сохранён в MQL5\\Files\\%s",
|
| | | base);
|
| | | }
|
2026-09-12 12:06:55 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| Публикация маркера согласованности (R8-S3/R13-B2/R14-B2) |
|
2026-09-12 12:06:55 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //--- Пишется ТОЛЬКО ПОСЛЕ полной записи пары samples/events; отсутствие
|
| | | //--- или смена поколения marker = пара файлов записана не целиком.
|
| | | //--- Контрольное чтение подтверждает публикацию (R13-B2), иначе false.
|
| | | //--- Единственная точка записи marker: ни одна стадия финализации НЕ
|
| | | //--- перезаписывает уже опубликованную пару повторно (R14-B2).
|
2026-09-12 22:14:24 +03:00 | | | bool PublishCheckpointMarker(const string base, LabError &err)
|
2026-09-12 12:06:55 +03:00 | | | {
|
| | | err.Reset();
|
2026-09-12 22:14:24 +03:00 | | | err.component = LAB_COMP_CSV;
|
| | | static ulong cp_gen = 0; // R8-S3: поколение checkpoint
|
2026-09-12 12:06:55 +03:00 | | | cp_gen++;
|
2026-09-12 22:14:24 +03:00 | | | const ulong now = ExtClock.NowUs();
|
| | | if(!CCsv::SaveUtf8(base + "checkpoint.marker",
|
2026-09-12 12:06:55 +03:00 | | | StringFormat("samples_and_events_ok\r\ngen=%I64u\r\n%I64u\r\n",
|
2026-09-12 22:14:24 +03:00 | | | cp_gen, now), err))
|
2026-09-12 12:06:55 +03:00 | | | return(false);
|
2026-09-12 22:14:24 +03:00 | | | return(CCsv::ReadBackVerifyFile(base + "checkpoint.marker", 3, err));
|
2026-09-12 12:06:55 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | //+--------------------------------------------------------------------+
|
2026-09-12 06:20:31 +03:00 | | | //| Синхронизация фазы панели (R11): WARMUP->MAIN и счётчики за лёгкое |
|
2026-09-12 22:14:24 +03:00 | | | //| обновление (перерисовка только при смене фазы) |
|
| | | //+--------------------------------------------------------------------+
|
| | | void SyncPanelPhase(void)
|
2026-09-12 06:20:31 +03:00 | | | {
|
| | | if(!ExtInitialized || !ExtPanel.IsCreated() || !ExtController.IsRunning())
|
| | | return;
|
2026-09-12 22:14:24 +03:00 | | | 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)
|
2026-09-12 06:20:31 +03:00 | | | return;
|
2026-09-12 22:14:24 +03:00 | | | g_last_phase = disp;
|
2026-09-12 06:20:31 +03:00 | | | ExtPanel.SetStatus(disp);
|
2026-09-12 22:14:24 +03:00 | | | //--- R13-S2: внутри открытого измерительного окна принудительная
|
| | | //--- перерисовка (ChartRedraw) запрещена — обновляется только текст
|
| | | //--- панели; отрисовка откладывается до безопасной фазы (паузы).
|
2026-09-12 11:19:45 +03:00 | | | ExtPanel.Update(ExtController.InMeasurementWindow());
|
2026-09-12 06:20:31 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| Контрольное сохранение (R5-B2): периодический checkpoint |
|
2026-09-12 06:20:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 11:19:45 +03:00 | | | bool ExportCheckpoint(const bool force)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | //--- R6-B6: нет открытого окна MAIN и незавершённой очистки — иначе
|
| | | //--- пропускаем (продолжается пауза); тяжёлый экспорт вне измерения.
|
| | | //--- R13-B2: force-финализация не подтверждена, пока safe-фаза недостижима
|
| | | //--- (повтор на следующем таймере); периодический пропуск штатен.
|
2026-09-11 09:06:39 +03:00 | | | if(!ExtController.CanCheckpoint())
|
2026-09-12 11:19:45 +03:00 | | | return(!force);
|
2026-09-12 22:14:24 +03:00 | | | const ulong now = ExtClock.NowUs();
|
| | | //--- R11-S1: периодическое сохранение имеет порог 60 с; ФИНАЛЬНЫЙ
|
| | | //--- checkpoint (force) пишется всегда - короткий пилот не зависит
|
| | | //--- от накопления виртуального времени.
|
| | | static ulong last_cp = 1;
|
2026-09-12 06:20:31 +03:00 | | | if(!force)
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | if(last_cp != 0 && now - last_cp < 60000000UL)
|
2026-09-12 11:19:45 +03:00 | | | return(true);
|
2026-09-12 22:14:24 +03:00 | | | last_cp = now;
|
2026-09-12 06:20:31 +03:00 | | | }
|
2026-09-11 04:09:32 +03:00 | | | LabError err;
|
2026-09-12 22:14:24 +03:00 | | | const string session = ExtController.SessionId();
|
| | | if(StringLen(session) == 0)
|
2026-09-12 11:19:45 +03:00 | | | return(false);
|
2026-09-12 22:14:24 +03:00 | | | 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"))
|
2026-09-11 21:20:36 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | if(!FileDelete(base + "checkpoint.marker"))
|
2026-09-11 21:20:36 +03:00 | | | {
|
| | | Print("RequestLatencyLab: checkpoint marker invalidation FAILED");
|
| | | ExtController.SetCheckpointState(false);
|
| | | ExtController.BlockRun("checkpoint marker invalidate failed");
|
2026-09-12 11:19:45 +03:00 | | | return(false);
|
2026-09-11 21:20:36 +03:00 | | | }
|
| | | }
|
2026-09-11 09:06:39 +03:00 | | | if(ok)
|
2026-09-12 22:14:24 +03:00 | | | ok = CCsv::SaveUtf8(base + "samples.csv",
|
| | | CCsvReport::BuildSamples(&ExtTracker, ExtSettings.campaign_id,
|
| | | ExtSettings.configuration_id), err);
|
2026-09-11 09:06:39 +03:00 | | | if(ok)
|
2026-09-12 22:14:24 +03:00 | | | 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.
|
2026-09-12 11:19:45 +03:00 | | | if(ok)
|
2026-09-12 22:14:24 +03:00 | | | ok = PublishCheckpointMarker(base, err);
|
2026-09-11 09:06:39 +03:00 | | | if(!ok)
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | Print("RequestLatencyLab: CHECKPOINT WRITE FAILED: " + err.message);
|
2026-09-11 09:06:39 +03:00 | | | //--- R6-B6: неподтверждённая сохранность останавливает новые отправки
|
| | | ExtController.SetCheckpointState(false);
|
| | | ExtController.BlockRun("checkpoint write failed");
|
2026-09-12 11:19:45 +03:00 | | | return(false);
|
2026-09-11 09:06:39 +03:00 | | | }
|
| | | ExtController.SetCheckpointState(true);
|
2026-09-12 11:19:45 +03:00 | | | return(true);
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | //+-------------------------------------------------------------------+
|
2026-09-12 11:19:45 +03:00 | | | //| Единая финализация (R13-B2): финальный checkpoint + основной |
|
| | | //| экспорт под ОДНИМ проверяемым результатом; повторяется именно |
|
2026-09-12 22:14:24 +03:00 | | | //| неуспешная стадия. FINISHED/кнопка - только после полного набора. |
|
| | | //+-------------------------------------------------------------------+
|
| | | bool FinalizeRun(void)
|
2026-09-12 11:19:45 +03:00 | | | {
|
| | | if(ExtExportDone)
|
| | | return(true);
|
| | | if(!ExtCheckpointDone)
|
| | | {
|
| | | if(!ExportCheckpoint(true))
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | const string crank = ExtController.BlockReason();
|
| | | ExtPanel.SetStatus(crank == "" ?
|
| | | "CHECKPOINT_FAILED: финальный checkpoint не сохранён" :
|
| | | "CHECKPOINT_FAILED: " + crank);
|
2026-09-12 11:19:45 +03:00 | | | ExtPanel.SetStarted(true);
|
| | | ExtPanel.Update(false);
|
| | | return(false);
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | ExtCheckpointDone = true;
|
2026-09-12 11:19:45 +03:00 | | | }
|
| | | ExportRun();
|
| | | if(!ExtExportDone)
|
| | | {
|
| | | ExtPanel.SetStatus("EXPORT_FAILED: повторная попытка на таймере");
|
| | | ExtPanel.SetStarted(true);
|
| | | ExtPanel.Update(false);
|
| | | return(false);
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | const string freason = ExtController.TerminationReason();
|
| | | ExtPanel.SetStatus(freason == "" ? "FINISHED" : "FINISHED: " + freason);
|
2026-09-12 11:19:45 +03:00 | | | ExtPanel.SetStarted(false);
|
| | | ExtPanel.Update(false);
|
| | | return(true);
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| Инициализация |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | int OnInit(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
| | | FillSettings();
|
2026-09-10 16:43:28 +03:00 | | | ExtController.SetSettings(ExtSettings);
|
2026-09-12 22:14:24 +03:00 | | | ExtController.Configure(&ExtTracker, &ExtJournal, &ExtRunner, &ExtClock);
|
| | | if(ExtSettings.run_mode == LAB_RUN_LOCAL_ONLY)
|
2026-09-09 01:19:31 +03:00 | | | {
|
| | | //--- синтетические часы отделены от live-счётчика
|
2026-09-10 16:43:28 +03:00 | | | ExtClock.UseSynthetic(1000000);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | else
|
| | | {
|
2026-09-10 16:43:28 +03:00 | | | ExtClock.UseLive();
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | LabError err;
|
2026-09-10 16:43:28 +03:00 | | | if(!ExtController.Preflight(err))
|
2026-09-09 01:19:31 +03:00 | | | {
|
| | | PrintFormat("RequestLatencyLab: PREFLIGHT FAILED [%s]",
|
2026-09-12 22:14:24 +03:00 | | | LabResultString(false, err));
|
2026-09-10 16:43:28 +03:00 | | | return(INIT_PARAMETERS_INCORRECT);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- R5-S2: configuration_id вычислен внутри Preflight (копия настроек);
|
| | | //--- переносим в глобальные настройки для экспорта manifest/samples.
|
| | | ExtSettings.configuration_id = ExtController.ConfigurationId();
|
2026-09-10 16:43:28 +03:00 | | | if(!ExtController.InitRun(err))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | PrintFormat("RequestLatencyLab: INIT FAILED [%s]", LabResultString(false, err));
|
2026-09-10 16:43:28 +03:00 | | | return(INIT_FAILED);
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- E3: calibration load (Scripts/MarketCalibration.mq5)
|
| | | if(ExtSettings.experiment_id == LAB_EXP_E3 && ExtSettings.run_mode != LAB_RUN_LOCAL_ONLY)
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | LabError cerr;
|
| | | Calibration cal;
|
2026-09-12 22:14:24 +03:00 | | | if(CCsvReport::LoadCalibration(ExtSettings.calibration_id, cal, cerr))
|
2026-09-10 16:43:28 +03:00 | | | {
|
| | | ExtController.SetCalibration(cal);
|
| | | PrintFormat("RequestLatencyLab: calibration loaded id=%s windows=%d",
|
2026-09-12 22:14:24 +03:00 | | | cal.calibration_id, cal.valid_window_count);
|
2026-09-10 16:43:28 +03:00 | | | }
|
| | | else
|
2026-09-12 22:14:24 +03:00 | | | PrintFormat("RequestLatencyLab: calibration NOT loaded: %s", cerr.message);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-10 16:43:28 +03:00 | | | ExtPanel.Create(ChartID());
|
| | | ExtPanel.SetStatus("READY");
|
| | | ExtPanel.Update(false);
|
 fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита
Блокеры:
- B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup,
нулевая экспозиция, однократный dispatch, отказ -> BLOCKED)
- B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred)
- B3: collection не закрывается до ожидаемого REQUEST (T2) для market
- B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED
- B5: событийная сверка light (без полного HistorySelect в callback)
- B6: проверка bool HistorySelect; history_scan_budget вместо 100000
- B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет
- B8: E4 — reuse-анализ без торгового runtime; reuse=1
- B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected
- B10: write/timer/journal errors блокирующие (export check, integrity), timer check
Существенные:
- S1: фактические deviation/comment из настроек в RequestPlan/запросе
- S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv
- S3: пустые статистики summary при n<2
- S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки
- S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count)
- S6: market cleanup: type_filling/deviation + PreCheck
Тесты: DEFER-01, NACT-01, REJ-01, AMB-01.
Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00 | | | if(!EventSetMillisecondTimer((uint)ExtSettings.timer_requested_ms))
|
| | | {
|
| | | PrintFormat("RequestLatencyLab: EventSetMillisecondTimer FAILED err=%d",
|
| | | GetLastError());
|
| | | return(INIT_FAILED);
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | ExtInitialized = true;
|
2026-09-09 01:19:31 +03:00 | | | PrintFormat("RequestLatencyLab: ready. mode=%s symbol=%s config=%s",
|
2026-09-12 22:14:24 +03:00 | | | (ExtSettings.run_mode == LAB_RUN_LOCAL_ONLY ? "LOCAL_ONLY" : "TRADING"),
|
| | | ExtSettings.symbol, ExtSettings.configuration_id);
|
2026-09-10 16:43:28 +03:00 | | | return(INIT_SUCCEEDED);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| Деинициализация |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
| | | void OnDeinit(const int reason)
|
| | | {
|
| | | EventKillTimer();
|
2026-09-10 16:43:28 +03:00 | | | ExtController.StopRun();
|
2026-09-12 22:14:24 +03:00 | | | //--- R5-B2: штатное снятие советника не теряет журнал и незавершённые
|
| | | //--- записи — последний доступный снимок экспортируется.
|
2026-09-11 04:09:32 +03:00 | | | ExportRun();
|
2026-09-10 16:43:28 +03:00 | | | ExtPanel.Destroy();
|
2026-09-12 22:14:24 +03:00 | | | ExtInitialized = false;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTick: только сигнал котировки; расчёт не выполняется |
|
| | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | void OnTick(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | //--- R6-S2/R7-B5: фиксация момента наблюдения котировки КОНКРЕТНОГО
|
| | | //--- рабочего символа (InpSymbol), а не символа графика: NewTick
|
| | | //--- относится к символу графика [API3] и при другом рабочем символе
|
| | | //--- не доказывает обновления. Свежесть считается по новым time_msc.
|
2026-09-11 09:06:39 +03:00 | | | if(ExtInitialized)
|
2026-09-11 15:17:38 +03:00 | | | {
|
| | | MqlTick qt;
|
2026-09-12 22:14:24 +03:00 | | | if(SymbolInfoTick(ExtSettings.symbol, qt) && qt.time_msc > 0)
|
2026-09-11 15:17:38 +03:00 | | | ExtController.NoteQuoteObserved((long)qt.time_msc);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTimer: резервная сверка, сроки, разрешённые шаги контроллера |
|
| | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | void OnTimer(void)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-10 16:43:28 +03:00 | | | if(!ExtInitialized)
|
2026-09-09 01:19:31 +03:00 | | | return;
|
2026-09-12 22:14:24 +03:00 | | | const ulong now = ExtClock.NowUs();
|
| | | const ulong elapsed = (ExtLastTimerUs == 0 ? 0 : now - ExtLastTimerUs);
|
| | | ExtLastTimerUs = now;
|
2026-09-12 02:21:14 +03:00 | | | LabError err;
|
2026-09-12 22:14:24 +03:00 | | | //--- R13-S1: терминальный BLOCKED отображается СРАЗУ — без зависимости
|
| | | //--- от признака ожидающей команды (латч уже мог быть потреблён); причина
|
| | | //--- в статусе, кнопка отпущена, новая торговля до устранения запрещена.
|
| | | if(ExtController.State() == LAB_STATE_BLOCKED)
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | ExtStartPressed = false;
|
2026-09-12 02:21:14 +03:00 | | | ExtPanel.SetStarted(false);
|
2026-09-12 22:14:24 +03:00 | | | ExtPanel.SetStatus("BLOCKED: " + ExtController.BlockReason());
|
2026-09-12 02:21:14 +03:00 | | | ExtPanel.Update(false);
|
2026-09-12 11:19:45 +03:00 | | | return;
|
2026-09-12 02:21:14 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- R5-B2: периодическое контрольное сохранение (с порогом 60 с)
|
2026-09-11 04:09:32 +03:00 | | | if(ExtController.IsRunning())
|
2026-09-12 06:20:31 +03:00 | | | ExportCheckpoint(false);
|
2026-09-12 22:14:24 +03:00 | | | //--- R11-B2: таймер ОДНОКРАТНО потребляет сохранённую команду Start.
|
| | | //--- Latch ставит OnChartEvent СИНХРОННО по имени объекта; независимый
|
| | | //--- опрос OBJPROP_STATE для запуска убран (один физический клик = один
|
| | | //--- StartRun). Источник истины - внутренний ExtStartPressed.
|
2026-09-12 06:20:31 +03:00 | | | if(ExtStartPressed)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | ExtStartPressed = false; // команда потреблена один раз
|
| | | ExtExportDone = false; // R11-B1: новый прогон - новый экспорт
|
| | | ExtCheckpointDone = false; // R13-B2: новая сессия - новый финальный checkpoint
|
2026-09-12 06:20:31 +03:00 | | | ExtPanel.SetStarted(true); // кнопка остаётся нажатой/заблокированной
|
2026-09-10 16:43:28 +03:00 | | | if(ExtController.StartRun(err))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 11:19:45 +03:00 | | | //--- R13-B1/B2: синхронный финал (и прерванный синтетический
|
| | | //--- прогон с сохранённым контекстом) завершается ЕДИНОЙ
|
| | | //--- финализацией: checkpoint + экспорт + статус/кнопка.
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_FINISHED)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 11:19:45 +03:00 | | | FinalizeRun();
|
| | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-12 11:19:45 +03:00 | | | SyncPanelPhase(); // R11: фаза WARMUP/MAIN + счётчик
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | else
|
| | | {
|
2026-09-12 22:14:24 +03:00 | | | ExtPanel.SetStatus("START_FAILED: " + err.message);
|
2026-09-12 06:20:31 +03:00 | | | ExtPanel.SetStarted(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-10 16:43:28 +03:00 | | | ExtPanel.Update(false);
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_FINISHED)
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_FINISHED)
|
2026-09-11 04:09:32 +03:00 | | | {
|
2026-09-12 11:19:45 +03:00 | | | //--- R5-B2/R11-B3/R13-B2: повтор НЕУСПЕШНОЙ стадии финализации
|
| | | //--- (финальный checkpoint или основной экспорт) на каждом таймере;
|
| | | //--- FINISHED и разблокировка кнопки - только после полного набора.
|
2026-09-11 04:09:32 +03:00 | | | if(!ExtExportDone)
|
2026-09-12 11:19:45 +03:00 | | | FinalizeRun();
|
2026-09-12 22:14:24 +03:00 | | | else
|
| | | if(ExtStartPressed)
|
| | | {
|
| | | ExtStartPressed = false;
|
| | | ExtPanel.SetStarted(false);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | | return;
|
2026-09-11 04:09:32 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | //--- резервный таймер
|
| | | ExtController.OnTimer(elapsed, err);
|
2026-09-10 16:43:28 +03:00 | | | if(ExtController.IsRunning())
|
2026-09-09 01:19:31 +03:00 | | | {
|
| | | //--- отложенный запуск следующего шага, если пауза прошла
|
2026-09-10 16:43:28 +03:00 | | | ExtController.DriveNext(err);
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_FINISHED)
|
2026-09-10 19:04:10 +03:00 | | | {
|
| | | //--- формальное завершение (напр. E3 INSUFFICIENT_DATA)
|
2026-09-12 11:19:45 +03:00 | | | FinalizeRun();
|
2026-09-10 19:04:10 +03:00 | | | return;
|
| | | }
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_DRAINING)
|
2026-09-09 01:19:31 +03:00 | | | {
|
| | | //--- сбор завершён: очистка при известном торговом состоянии
|
| | | LabError cerr;
|
2026-09-10 16:43:28 +03:00 | | | if(ExtController.EndMainCycle(cerr))
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-10 16:43:28 +03:00 | | | ExtController.RequirePause();
|
2026-09-11 04:09:32 +03:00 | | | //--- R5-B1/B2: завершение серии ТОЛЬКО без незавершённого cleanup;
|
| | | //--- состояние контроллера переводится в FINISHED (manifest
|
| | | //--- run_status=FINISHED), а не остаётся DRAINING.
|
| | | if(!ExtController.CleanupInFlight() &&
|
| | | ExtController.Runner().MainComplete() &&
|
2026-09-10 16:43:28 +03:00 | | | ExtController.Runner().WarmupComplete())
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-10 16:43:28 +03:00 | | | ExtController.StopRun();
|
2026-09-11 04:09:32 +03:00 | | | ExtController.MarkFinished();
|
2026-09-12 11:19:45 +03:00 | | | FinalizeRun();
|
2026-09-11 04:09:32 +03:00 | | | return;
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | }
|
| | | }
|
2026-09-12 06:20:31 +03:00 | | | SyncPanelPhase(); // R11: фаза WARMUP/MAIN + счётчик
|
2026-09-10 16:43:28 +03:00 | | | ExtPanel.Update(true);
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | }
|
2026-09-10 16:43:28 +03:00 | | | ExtPanel.Update(false);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| OnTradeTransaction: вход, сохранение полей, быстрая корреляция |
|
| | | //+------------------------------------------------------------------+
|
| | | void OnTradeTransaction(const MqlTradeTransaction &trans,
|
| | | const MqlTradeRequest &request,
|
| | | const MqlTradeResult &result)
|
| | | {
|
2026-09-10 16:43:28 +03:00 | | | if(!ExtInitialized)
|
2026-09-09 01:19:31 +03:00 | | | return;
|
| | | TransactionObservation obs;
|
| | | obs.Zero();
|
2026-09-12 22:14:24 +03:00 | | | 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;
|
2026-09-09 01:19:31 +03:00 | | | LabError err;
|
2026-09-12 22:14:24 +03:00 | | | ExtController.OnTransaction(obs, err);
|
| | | obs.handler_exit_us = ExtClock.NowUs();
|
| | | ExtController.FinalizeTransaction(obs, err);
|
2026-09-09 01:19:31 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | //| OnChartEvent: кнопка панели (R10-B1 - latch команды Start) |
|
2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-12 22:14:24 +03:00 | | | void OnChartEvent(const int id, const long &lparam, const double &dparam,
|
2026-09-09 01:19:31 +03:00 | | | const string &sparam)
|
| | | {
|
2026-09-12 02:21:14 +03:00 | | | if(!ExtInitialized)
|
| | | return;
|
2026-09-12 22:14:24 +03:00 | | | if(id == CHARTEVENT_OBJECT_CLICK && ExtPanel.IsCreated())
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | const string obj = sparam;
|
2026-09-12 06:20:31 +03:00 | | | //--- R10-B1/R11-B2: команда Start захватывается СИНХРОННО в OnChartEvent
|
| | | //--- по имени объекта (latch ExtStartPressed). Кнопка остаётся
|
| | | //--- нажатой/заблокированной весь прогон; фаза и итог выводятся в
|
| | | //--- Status; исполняет команду РОВНО один раз OnTimer (независимый
|
| | | //--- опрос OBJPROP_STATE убран - один клик = один StartRun).
|
2026-09-12 22:14:24 +03:00 | | | if(obj == LAB_PANEL_PREFIX + "Start")
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 06:20:31 +03:00 | | | //--- терминальная блокировка: причина в статусе, кнопка не нажимается
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_BLOCKED)
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | ExtPanel.SetStatus("BLOCKED: " + ExtController.BlockReason());
|
2026-09-12 02:21:14 +03:00 | | | ExtPanel.SetStarted(false);
|
| | | ExtPanel.Update(false);
|
| | | ChartRedraw();
|
| | | return;
|
| | | }
|
2026-09-12 06:20:31 +03:00 | | | //--- R11-B3: повторный запуск после финализации разрешён ТОЛЬКО
|
| | | //--- если отчёт успешно сохранён (иначе - ошибка, перезапуск запрещён).
|
2026-09-12 22:14:24 +03:00 | | | if(ExtController.State() == LAB_STATE_FINISHED)
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 06:20:31 +03:00 | | | if(!ExtExportDone)
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 06:20:31 +03:00 | | | ExtPanel.SetStatus("EXPORT_FAILED: отчёт не сохранён; перезапуск запрещён");
|
| | | ExtPanel.SetStarted(true);
|
| | | ExtPanel.Update(false);
|
| | | ChartRedraw();
|
| | | return;
|
2026-09-12 02:21:14 +03:00 | | | }
|
2026-09-12 06:20:31 +03:00 | | | ExtController.ResetForRestart(); // R10-B1: подготовка нового прогона
|
2026-09-12 02:21:14 +03:00 | | | }
|
2026-09-12 06:20:31 +03:00 | | | //--- R11-B2: повторный/двойной клик и клики во время прогона
|
| | | //--- игнорируются (одна команда на прогон; текущую не отменяет).
|
| | | if(ExtStartPressed ||
|
2026-09-12 22:14:24 +03:00 | | | (ExtController.State() != LAB_STATE_READY &&
|
| | | ExtController.State() != LAB_STATE_IDLE))
|
2026-09-12 02:21:14 +03:00 | | | {
|
2026-09-12 12:06:55 +03:00 | | | //--- R13-S1/R14-S1: OBJ_BUTTON сам отжал кнопку кликом —
|
| | | //--- наглядное состояние восстанавливается из намеренного, но
|
| | | //--- БЕЗ принудительной перерисовки внутри открытого
|
| | | //--- измерительного окна (ChartRedraw откладывается до паузы).
|
2026-09-12 11:19:45 +03:00 | | | ExtPanel.RestoreStartedState();
|
2026-09-12 22:14:24 +03:00 | | | const bool in_win = ExtController.InMeasurementWindow();
|
2026-09-12 12:06:55 +03:00 | | | ExtPanel.Update(in_win);
|
| | | if(!in_win)
|
| | | ChartRedraw();
|
2026-09-12 06:20:31 +03:00 | | | return;
|
2026-09-12 02:21:14 +03:00 | | | }
|
2026-09-12 06:20:31 +03:00 | | | //--- команда принята: latch, кнопка нажата и заблокирована
|
2026-09-12 22:14:24 +03:00 | | | ExtStartPressed = true;
|
2026-09-12 06:20:31 +03:00 | | | ExtPanel.SetStarted(true);
|
| | | ExtPanel.SetStatus("Run…");
|
2026-09-12 02:21:14 +03:00 | | | ExtPanel.Update(false);
|
| | | ChartRedraw();
|
| | | return;
|
| | | }
|
2026-09-12 16:38:37 +03:00 | | | //--- R15-S1: клик по ЧУЖОМУ объекту графика (не кнопке панели) не
|
| | | //--- вызывает принудительную перерисовку внутри открытого окна
|
| | | //--- измерения — тот же безопасный режим, что в ветви занятого Start
|
| | | //--- (ChartRedraw откладывается до паузы).
|
2026-09-12 22:14:24 +03:00 | | | const bool owin = ExtController.InMeasurementWindow();
|
2026-09-12 16:38:37 +03:00 | | | ExtPanel.Update(owin);
|
| | | if(!owin)
|
| | | ChartRedraw();
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-12 22:14:24 +03:00 | | | }
|
| | | //+------------------------------------------------------------------+
|