2026-09-09 01:19:31 +03:00 | | | //+------------------------------------------------------------------+
|
2026-09-10 16:43:28 +03:00 | | | //| MarketRegime.mqh |
|
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 | | | //+------------------------------------------------------------------+
|
| | | #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:
|
2026-09-11 18:16:32 +03:00 | | | //--- R8-S2: FNV-1a 64 — лёгкая контрольная сумма набора тиков (целостность
|
| | | //--- и однозначность диапазона [from_msc,to_msc] при повторном чтении).
|
2026-09-12 22:14:24 +03:00 | | | static ulong LabFnv1a(ulong h, const ulong v)
|
2026-09-11 18:16:32 +03:00 | | | {
|
2026-09-12 22:14:24 +03:00 | | | h ^= v;
|
| | | h *= 1099511628211UL;
|
2026-09-11 18:16:32 +03:00 | | | return(h);
|
| | | }
|
2026-09-11 21:20:36 +03:00 | | | //--- R9-S3: вызовы, которым НЕ нужны сырые тики окна (калибратор):
|
| | | //--- делегируют основной реализации с временным массивом.
|
2026-09-12 22:14:24 +03:00 | | | static bool ComputeWindow(const string symbol, const long anchor_msc,
|
| | | const ulong fetch_start_us, MarketWindow &w,
|
2026-09-09 01:19:31 +03:00 | | | LabError &error)
|
2026-09-11 21:20:36 +03:00 | | | {
|
| | | MqlTick tmp[];
|
2026-09-12 22:14:24 +03:00 | | | return(ComputeWindowRawTicks(symbol, anchor_msc, fetch_start_us, w, error, tmp));
|
2026-09-11 21:20:36 +03:00 | | | }
|
| | | //--- чтение причинного окна тиков до опорной котировки
|
2026-09-12 22:14:24 +03:00 | | | static bool ComputeWindowRawTicks(const string symbol, const long anchor_msc,
|
| | | const ulong fetch_start_us, MarketWindow &w,
|
2026-09-16 15:37:55 +03:00 | | | LabError &error, MqlTick &raw_ticks[]);
|
2026-09-09 01:19:31 +03:00 | | | //--- классификация по порогам калибровки (СП §8)
|
2026-09-12 22:14:24 +03:00 | | | static void Classify(const MarketWindow &w, const Calibration &cal,
|
2026-09-16 15:37:55 +03:00 | | | ENUM_LAB_MARKET_REGIME ®ime, bool &valid);
|
2026-09-09 01:19:31 +03:00 | | | //--- аккумулирование признаков минутных окон для калибровки
|
2026-09-12 22:14:24 +03:00 | | | static bool AggregateWindow(const MarketWindow &w, double &freq[], double &rng[],
|
2026-09-16 15:37:55 +03:00 | | | int &count, const int capacity);
|
2026-09-09 01:19:31 +03:00 | | | //--- пороги по набору валидных окон (p50/p90 для f и r)
|
2026-09-12 22:14:24 +03:00 | | | static bool ComputeThresholds(const double &freq[], const double &rng[],
|
| | | const int count, Calibration &cal,
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | //--- неполный/пустой диапазон не считается полноценным окном
|
| | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | crossed = true;
|
| | | continue;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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)
|
2026-09-09 01:19:31 +03:00 | | | {
|
2026-09-16 15:37:55 +03:00 | | | w.quality_codes = "UNORDERED_TICKS";
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | last_msc = t.time_msc;
|
2026-09-09 01:19:31 +03:00 | | | }
|
2026-09-16 15:37:55 +03:00 | | | 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);
|
| | | }
|
2026-09-09 01:19:31 +03:00 | | |
|
2026-09-12 22:14:24 +03:00 | | | #endif // REQUEST_LATENCY_LAB_MARKET_REGIME_MQH
|
| | | //+------------------------------------------------------------------+
|