mql5-execution-microstructu.../Scripts/MarketCalibration.mq5

201 lines
9.9 KiB
MQL5

//+------------------------------------------------------------------+
//| MarketCalibration.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 "Калибровочный набор p50/p90 для QUIET/FAST"
#property description "классификатора (ТЗ §6(E3)). Без торговых запросов."
#property description "Не менее 1000 валидных минутных окон из 5 сессий."
#property description "Результат — calibration.csv в Common\\Files\\RequestLatencyLab\\."
#property script_show_inputs
#include "..\Include\RequestLatencyLab\Models.mqh"
#include "..\Include\RequestLatencyLab\Configuration.mqh"
#include "..\Include\RequestLatencyLab\MarketRegime.mqh"
#include "..\Include\RequestLatencyLab\CsvStorage.mqh"
#include "..\Include\RequestLatencyLab\Legacy\BenchmarkStatistics.mqh"
//--- символ калибровки
input string InpSymbol="EURUSD";
//--- 0=live: TimeCurrent()-InpDaysBack; тестер: начало периода
input datetime InpCalibrationFrom=0;
//--- 0=live: TimeCurrent(); тестер: конец периода
input datetime InpCalibrationTo=0;
//--- период калибровки назад от сегодня
input int InpDaysBack=30;
//--- идентификатор калибровки
input string InpCalibrationId="cal-eur-usd-01";
//+------------------------------------------------------------------+
//| Построение калибровки рыночного режима |
//+------------------------------------------------------------------+
void OnStart(void)
{
LabError err;
//--- R5-B6: ёмкость увеличена — для плотного потока 4096 окон = ~3 суток
//--- (< 5 календарных сессий); теперь генератор добирает сессии.
const int cap = 20000;
double freq[];
double rng[];
long win_msc[];
if(ArrayResize(freq, cap) != cap || ArrayResize(rng, cap) != cap ||
ArrayResize(win_msc, cap) != cap)
{
Print("MarketCalibration: cannot allocate");
return;
}
int count = 0;
int invalid = 0;
//--- R4-B4: отдельные торговые сессии = уникальные календарные даты,
//--- в которых наблюдалось хотя бы одно валидное минутное окно
//--- (дата берётся по серверному времени начала минуты).
int session_dates[64];
int session_count = 0;
int cur_session_day = -1;
const datetime now = TimeCurrent();
const datetime from = (InpCalibrationFrom > 0 ? InpCalibrationFrom : now - InpDaysBack * 86400);
const datetime to = (InpCalibrationTo > 0 ? InpCalibrationTo : now);
//--- проход по минутам выбранного периода; R5-B6: цикл продолжается,
//--- пока не набраны >=1000 окон И >=5 сессий (либо исчерпан период/ёмкость)
datetime cursor = from;
while(cursor < to && count < cap && !(session_count >= 5 && count >= 1000))
{
//--- границы минутного окна по календарной шкале
MqlDateTime mdt;
TimeToStruct(cursor, mdt);
mdt.sec = 0;
const datetime minute_start = StructToTime(mdt);
MqlTick anchor;
if(!SymbolInfoTick(InpSymbol, anchor))
{
//--- пропуск минут без котировки (не валидное окно)
invalid++;
cursor = minute_start + 60;
continue;
}
const long b = (long)minute_start * 1000; // начало текущей минуты
MarketWindow w;
//--- R4-B5: единый контракт окна [b-60000, b-1] с anchor=b (как в runtime)
if(CMarketRegime::ComputeWindow(InpSymbol, b, GetMicrosecondCount(), w, err))
{
if(w.valid)
{
freq[count] = w.quote_frequency_hz;
rng[count] = w.mid_range_ticks;
win_msc[count] = (long)minute_start * 1000; // R5-B6: исходное окно
count++;
//--- R4-B4: смена календарной даты валидного окна = новая сессия
MqlDateTime sdt;
TimeToStruct(minute_start, sdt);
const int day = (int)sdt.year * 10000 + (int)sdt.mon * 100 + (int)sdt.day;
if(cur_session_day != day)
{
cur_session_day = day;
if(session_count < 64)
{
session_dates[session_count] = day;
session_count++;
}
}
}
else
invalid++;
}
else
invalid++;
cursor = minute_start + 60;
}
Calibration cal;
cal.Zero();
cal.calibration_id = InpCalibrationId;
cal.symbol = InpSymbol;
cal.calendar_time_scale = "SERVER";
cal.period_from_msc = (long)from * 1000;
cal.period_to_msc = (long)to * 1000;
cal.invalid_window_count = invalid;
cal.trading_session_count = session_count; // R4-B4: реальное число сессий
//--- R5-B6: набор, который loader отвергнет, не сохраняется —
//--- явный INSUFFICIENT_DATA вместо невалидной калибровки.
if(session_count < 5 || count < 1000)
{
PrintFormat("MarketCalibration: INSUFFICIENT_DATA windows=%d sessions=%d",
count, session_count);
return;
}
if(CMarketRegime::ComputeThresholds(freq, rng, count, cal, err))
{
PrintFormat("MarketCalibration: windows=%d p50(f)=%.4g p90(f)=%.4g p50(r)=%.4g p90(r)=%.4g",
count, cal.frequency_p50, cal.frequency_p90, cal.range_p50, cal.range_p90);
//--- calibration.csv
const string path = StringFormat("%s\\%s.csv", LAB_DATA_ROOT, InpCalibrationId);
string out = CCsv::HeaderKeyValue();
out += KeyValue(cal.calibration_id, cal.symbol, cal.frequency_p50, cal.frequency_p90,
cal.range_p50, cal.range_p90, count, cal.invalid_window_count,
cal.trading_session_count, cal.period_from_msc, cal.period_to_msc);
if(CCsv::SaveUtf8(path, out, err, true))
Print("MarketCalibration: saved Common\\Files\\" + path + " (read-back ok)");
else
Print("MarketCalibration: SAVE FAILED " + err.message);
//--- R5-B6: исходные окна выгружаются для независимой проверки
//--- (отсутствие пересечения калибровки и эксперимента, пересчёт порогов)
string wh[] = {"minute_start_msc", "quote_frequency_hz", "mid_range_ticks"};
string wout = CCsv::Header(wh);
string wc[3];
for(int i = 0; i < count; i++)
{
wc[0] = IntegerToString(win_msc[i]);
wc[1] = DoubleToString(freq[i], 12);
wc[2] = DoubleToString(rng[i], 12);
wout += CCsv::BuildRow(wc, 3);
}
const string wpath = StringFormat("%s\\%s_windows.csv", LAB_DATA_ROOT, InpCalibrationId);
if(CCsv::SaveUtf8(wpath, wout, err, true))
Print("MarketCalibration: windows saved Common\\Files\\" + wpath);
else
Print("MarketCalibration: WINDOWS SAVE FAILED " + err.message);
PrintFormat("MarketCalibration: FINISHED id=%s windows=%d sessions=%d",
InpCalibrationId, count, session_count);
}
else
PrintFormat("MarketCalibration: недобор валидных окон: %d (требуется >=1000): %s",
count, err.message);
}
//+------------------------------------------------------------------+
//| Вспомогательные функции записи calibration.csv |
//+------------------------------------------------------------------+
string KeyValue(const string id, const string symbol, const double fp50, const double fp90,
const double rp50, const double rp90, const int valid_windows,
const int invalid_windows, const int sessions, const long from_msc,
const long to_msc)
{
string out = "";
string cols[5];
AddPair(out, cols, "calibration_id", id);
AddPair(out, cols, "symbol", symbol);
AddPair(out, cols, "frequency_p50", DoubleToString(fp50, 10));
AddPair(out, cols, "frequency_p90", DoubleToString(fp90, 10));
AddPair(out, cols, "range_p50", DoubleToString(rp50, 10));
AddPair(out, cols, "range_p90", DoubleToString(rp90, 10));
AddPair(out, cols, "valid_window_count", IntegerToString(valid_windows));
AddPair(out, cols, "invalid_window_count", IntegerToString(invalid_windows));
AddPair(out, cols, "trading_session_count", IntegerToString(sessions));
//--- R5-B6: границы калибровочного периода (перекрытие с экспериментом)
AddPair(out, cols, "period_from_msc", IntegerToString(from_msc));
AddPair(out, cols, "period_to_msc", IntegerToString(to_msc));
return(out);
}
//+------------------------------------------------------------------+
//| Добавление пары ключ/значение в CSV-строку |
//+------------------------------------------------------------------+
void AddPair(string &out, string &cols[], const string key, const string value)
{
cols[0] = key;
cols[1] = value;
cols[2] = "string";
cols[3] = "KNOWN";
cols[4] = "DERIVED";
out += CCsv::BuildRow(cols, 5);
}
//+------------------------------------------------------------------+