//+------------------------------------------------------------------+ //| MarketRegime.mqh | //| Copyright 2026, MetaQuotes Ltd. | //| www.mql5.com | //+------------------------------------------------------------------+ #ifndef REQUEST_LATENCY_LAB_MARKET_REGIME_MQH #define REQUEST_LATENCY_LAB_MARKET_REGIME_MQH #include "..\..\Include\RequestLatencyLab\Models.mqh" #include "..\..\Include\RequestLatencyLab\Legacy\BenchmarkStatistics.mqh" #define LAB_WINDOW_SECONDS 60 class CMarketRegime { public: //--- R8-S2: FNV-1a 64 — лёгкая контрольная сумма набора тиков (целостность //--- и однозначность диапазона [from_msc,to_msc] при повторном чтении). static ulong LabFnv1a(ulong h, const ulong v) { h ^= v; h *= 1099511628211UL; return(h); } //--- R9-S3: вызовы, которым НЕ нужны сырые тики окна (калибратор): //--- делегируют основной реализации с временным массивом. static bool ComputeWindow(const string symbol, const long anchor_msc, const ulong fetch_start_us, MarketWindow &w, LabError &error) { MqlTick tmp[]; return(ComputeWindowRawTicks(symbol, anchor_msc, fetch_start_us, w, error, tmp)); } //--- чтение причинного окна тиков до опорной котировки static bool ComputeWindowRawTicks(const string symbol, const long anchor_msc, const ulong fetch_start_us, MarketWindow &w, LabError &error, MqlTick &raw_ticks[]); //--- классификация по порогам калибровки (СП §8) static void Classify(const MarketWindow &w, const Calibration &cal, ENUM_LAB_MARKET_REGIME ®ime, bool &valid); //--- аккумулирование признаков минутных окон для калибровки static bool AggregateWindow(const MarketWindow &w, double &freq[], double &rng[], int &count, const int capacity); //--- пороги по набору валидных окон (p50/p90 для f и r) static bool ComputeThresholds(const double &freq[], const double &rng[], const int count, Calibration &cal, LabError &error); }; //+------------------------------------------------------------------+ //| Внешние определения методов. | //+------------------------------------------------------------------+ bool CMarketRegime::ComputeWindowRawTicks(const string symbol, const long anchor_msc, const ulong fetch_start_us, MarketWindow &w, LabError &error, MqlTick &raw_ticks[]) { error.Reset(); error.component = LAB_COMP_MARKET; w.Zero(); w.symbol = symbol; w.anchor_tick_msc = anchor_msc; //--- R4-B5: единый контракт окна [anchor-60000, anchor-1] (60000 мс //--- включительно); runtime и калибратор используют один и тот же anchor //--- (time_msc опорной котировки) без смещений. w.from_msc = anchor_msc - (long)LAB_WINDOW_SECONDS * 1000; w.to_msc = anchor_msc - 1; w.window_id = StringFormat("w%08X_%04X", (uint)(anchor_msc & 0xFFFFFFFF), (uint)(fetch_start_us & 0xFFFF)); w.fetch_start_us = fetch_start_us; MqlTick ticks[]; ResetLastError(); const int got = CopyTicksRange(symbol, ticks, COPY_TICKS_INFO, w.from_msc, w.to_msc); w.copy_last_error = GetLastError(); w.fetch_end_us = GetMicrosecondCount(); if(got <= 0) { //--- неполный/пустой диапазон не считается полноценным окном w.range_complete = false; w.n_returned = 0; w.valid = false; w.quality_codes = "EMPTY_RANGE"; error.code = 1; error.mql_error = w.copy_last_error; error.message = "no ticks returned"; return(false); } w.n_returned = (ulong)got; //--- R9-S3: сырые тики диапазона [from_msc,to_msc] целиком — для //--- автономного offline-пересчёта признаков окна (boundaries+checksum //--- служат проверкой, а не источником данных). if(ArrayResize(raw_ticks, got) != got) { w.range_complete = false; w.valid = false; w.quality_codes = "RAW_TICKS_OOM"; error.code = 2; error.message = "window raw ticks allocation failed"; return(false); } for(int i = 0; i < got; i++) raw_ticks[i] = ticks[i]; //--- валидные двусторонние INFO-обновления Bid/Ask ulong n_valid = 0; double mid_min = DBL_MAX; double mid_max = -DBL_MAX; bool crossed = false; long last_msc = 0; ulong ticks_hash = 14695981039346656037UL; // R8-S2: FNV-1a offset basis for(int i = 0; i < got; i++) { const MqlTick t = ticks[i]; if(((t.flags & TICK_FLAG_BID) == 0) && ((t.flags & TICK_FLAG_ASK) == 0)) continue; //--- R4-B5: MqlTick содержит актуальные значения всех полей; flags //--- сообщают только что изменилось. Полные t.bid/t.ask берутся напрямую. const double bid = t.bid; const double ask = t.ask; if(bid <= 0.0 || ask <= 0.0 || ask < bid) { crossed = true; continue; } const double mid = (bid + ask) * 0.5; if(mid < mid_min) mid_min = mid; if(mid > mid_max) mid_max = mid; n_valid++; //--- R8-S2: контрольная сумма валидного набора (детерминирована //--- содержимым окна [from_msc,to_msc]; повторное CopyTicksRange //--- того же диапазона даёт ту же сумму). ticks_hash = LabFnv1a(ticks_hash, (ulong)t.time_msc); ticks_hash = LabFnv1a(ticks_hash, (ulong)(bid * 1000000.0)); ticks_hash = LabFnv1a(ticks_hash, (ulong)(ask * 1000000.0)); if(t.time_msc < last_msc) { w.quality_codes = "UNORDERED_TICKS"; } last_msc = t.time_msc; } w.n_valid_info = n_valid; w.ticks_checksum = (n_valid > 0 ? ticks_hash : 0); // R8-S2 if(crossed) w.quality_codes = (StringLen(w.quality_codes) > 0 ? w.quality_codes + "," : "") + "CROSSED_QUOTES"; w.tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); w.mid_min = (n_valid > 0 ? mid_min : 0.0); w.mid_max = (n_valid > 0 ? mid_max : 0.0); //--- N>=2, отсутствие ошибок загрузки даёт пригодность окна if(w.n_valid_info >= 2 && w.copy_last_error == 0 && !crossed && w.tick_size > 0.0) { w.range_complete = true; w.valid = true; w.quote_frequency_hz = (double)n_valid / (double)LAB_WINDOW_SECONDS; w.mid_range_ticks = (w.mid_max - w.mid_min) / w.tick_size; } else { if(w.copy_last_error != 0) w.quality_codes = (StringLen(w.quality_codes) > 0 ? w.quality_codes + "," : "") + "COPY_ERROR"; if(got == 0) w.quality_codes = (StringLen(w.quality_codes) > 0 ? w.quality_codes + "," : "") + "EMPTY_RANGE"; w.range_complete = false; w.valid = false; } return(true); } void CMarketRegime::Classify(const MarketWindow &w, const Calibration &cal, ENUM_LAB_MARKET_REGIME ®ime, bool &valid) { regime = LAB_REGIME_UNKNOWN; valid = false; if(!w.valid || !cal.valid) return; //--- вырожденные пороги: два класса неразличимы if(cal.frequency_p90 <= cal.frequency_p50 || cal.range_p90 <= cal.range_p50) return; valid = true; const bool quiet_f = w.quote_frequency_hz <= cal.frequency_p50; const bool quiet_r = w.mid_range_ticks <= cal.range_p50; const bool fast_f = w.quote_frequency_hz >= cal.frequency_p90; const bool fast_r = w.mid_range_ticks >= cal.range_p90; if(quiet_f && quiet_r) regime = LAB_REGIME_QUIET; else if(fast_f && fast_r) regime = LAB_REGIME_FAST; else regime = LAB_REGIME_INTERMEDIATE; } bool CMarketRegime::AggregateWindow(const MarketWindow &w, double &freq[], double &rng[], int &count, const int capacity) { if(!w.valid || count >= capacity) return(false); freq[count] = w.quote_frequency_hz; rng[count] = w.mid_range_ticks; count++; return(true); } bool CMarketRegime::ComputeThresholds(const double &freq[], const double &rng[], const int count, Calibration &cal, LabError &error) { error.Reset(); error.component = LAB_COMP_MARKET; if(count < 1000) { //--- менее 1000 окон => калибровка непригодна (ТЗ §6(E3)) cal.valid = false; cal.quality_codes = StringFormat("TOO_FEW_WINDOWS_%d", count); error.code = 2; error.message = cal.quality_codes; return(false); } if(!LabPercentile(freq, count, 50.0, cal.frequency_p50) || !LabPercentile(freq, count, 90.0, cal.frequency_p90) || !LabPercentile(rng, count, 50.0, cal.range_p50) || !LabPercentile(rng, count, 90.0, cal.range_p90)) { cal.valid = false; cal.quality_codes = "PERCENTILE_FAILED"; error.code = 3; return(false); } cal.valid_window_count = count; //--- вырожденные пороги признаются невалидными if(cal.frequency_p90 <= cal.frequency_p50 || cal.range_p90 <= cal.range_p50) { cal.valid = false; cal.quality_codes = "DEGENERATE_THRESHOLDS"; error.code = 4; return(false); } cal.valid = true; cal.quality_codes = "OK"; return(true); } #endif // REQUEST_LATENCY_LAB_MARKET_REGIME_MQH //+------------------------------------------------------------------+