238 lignes
Pas d'EOL
10 Kio
MQL5
238 lignes
Pas d'EOL
10 Kio
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|
|
//+------------------------------------------------------------------+ |