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