mql5-execution-microstructu.../RequestLatencyLab.mq5

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