mql5-execution-microstructu.../Include/RequestLatencyLab/StateReader.mqh

353 lines
16 KiB
MQL5

//+------------------------------------------------------------------+
//| StateReader.mqh |
//| Copyright 2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#ifndef REQUEST_LATENCY_LAB_STATE_READER_MQH
#define REQUEST_LATENCY_LAB_STATE_READER_MQH
#include "..\..\Include\RequestLatencyLab\Models.mqh"
#include "..\..\Include\RequestLatencyLab\TimeSource.mqh"
class CStateReader
{
public:
//--- чтение активного ордера по тикету
2026-09-12 22:14:24 +03:00
static bool ReadActiveOrder(const ulong ticket, long &state, double &volume_current,
double &volume_initial, string &symbol, long &type,
long &magic, ENUM_LAB_READ_STATUS &status);
//--- чтение ордера из истории (требует HistorySelect с диапазоном)
2026-09-12 22:14:24 +03:00
static bool ReadHistoryOrder(const ulong ticket, long &state, double &volume_current,
double &volume_initial, string &symbol, long &type,
ENUM_LAB_READ_STATUS &status);
//--- сбор уникальных сделок ордера в пределах выбранной истории
2026-09-12 22:14:24 +03:00
static int CollectDealsByOrder(const ulong order_ticket, const string symbol,
ulong &tickets[], double &exec_units,
const double volume_step, const double tolerance,
int &api_errors, const int budget, bool &truncated);
//--- позиция по тикету (именно тикет, не identifier)
static bool ReadPositionFacts(const ulong ticket, PositionFacts &pf);
//--- полный снимок для одной записи (T6 первый успешный)
static void BuildSnapshot(const ulong sequence, const ENUM_LAB_OPERATION expected_op,
const ulong order_ticket, const double requested_units,
const double volume_step, const double tolerance,
CTimeSource *clock, const ulong position_ticket_hint,
const int history_scan_budget, ConfirmationSnapshot &snap,
const bool light = false,
const double callback_volume_units = 0.0,
const int callback_deal_count = 0);
//--- R4-B2 (audit-4): публичный перевод лотов в units (единая точка
//--- конвертации для событийной сверки; аналог VolumeToUnitsSafe).
static bool LotsToUnits(const double lots, const double step, const double tolerance,
double &units);
private:
static bool VolumeToUnitsSafe(const double volume, const double step,
const double tolerance, double &units);
};
//+------------------------------------------------------------------+
//| Внешние определения методов. |
//+------------------------------------------------------------------+
bool CStateReader::ReadActiveOrder(const ulong ticket, long &state, double &volume_current,
double &volume_initial, string &symbol, long &type,
long &magic, ENUM_LAB_READ_STATUS &status)
{
status = LAB_READ_UNKNOWN;
if(ticket == 0)
return(false);
ResetLastError();
if(!OrderSelect(ticket))
{
const int e = GetLastError();
//--- B4 (audit-3): 4754 ERR_TRADE_ORDER_NOT_FOUND — ожидаемый
//--- «ордер не найден» -> ABSENT_CONFIRMED; прочие ошибки (в т.ч.
//--- 4756 SEND_FAILED) остаются FAILED (не доказанное отсутствие).
status = (e == 4754 ? LAB_READ_ABSENT_CONFIRMED : LAB_READ_FAILED);
return(false);
}
state = OrderGetInteger(ORDER_STATE);
volume_current = OrderGetDouble(ORDER_VOLUME_CURRENT);
volume_initial = OrderGetDouble(ORDER_VOLUME_INITIAL);
symbol = OrderGetString(ORDER_SYMBOL);
type = OrderGetInteger(ORDER_TYPE);
magic = OrderGetInteger(ORDER_MAGIC);
status = LAB_READ_FOUND;
return(true);
}
bool CStateReader::ReadHistoryOrder(const ulong ticket, long &state, double &volume_current,
double &volume_initial, string &symbol, long &type,
ENUM_LAB_READ_STATUS &status)
{
status = LAB_READ_UNKNOWN;
if(ticket == 0)
return(false);
ResetLastError();
if(!HistoryOrderSelect(ticket))
{
const int e = GetLastError();
//--- B4 (audit-3): только 4754 считаем подтверждённым отсутствием.
status = (e == 4754 ? LAB_READ_ABSENT_CONFIRMED : LAB_READ_FAILED);
return(false);
}
state = HistoryOrderGetInteger(ticket, ORDER_STATE);
volume_current = HistoryOrderGetDouble(ticket, ORDER_VOLUME_CURRENT);
volume_initial = HistoryOrderGetDouble(ticket, ORDER_VOLUME_INITIAL);
symbol = HistoryOrderGetString(ticket, ORDER_SYMBOL);
type = HistoryOrderGetInteger(ticket, ORDER_TYPE);
status = LAB_READ_FOUND;
return(true);
}
int CStateReader::CollectDealsByOrder(const ulong order_ticket, const string symbol,
ulong &tickets[], double &exec_units,
const double volume_step, const double tolerance,
int &api_errors, const int budget, bool &truncated)
{
int found = 0;
exec_units = 0.0;
truncated = false;
//--- аудит-2 B1: детерминированная выборка истории сделок до перебора;
//--- не зависит от ранее выбранного контекста (HistoryOrderSelect и пр.)
//--- B6 (audit-3): проверяем bool HistorySelect; при неуспехе возвращаем
//--- -1 (ошибка API), положительное завершение не допускается.
ResetLastError();
if(!HistorySelect((datetime)0, TimeCurrent()))
{
api_errors++;
return(-1);
}
const int total = HistoryDealsTotal();
if(total < 0)
{
api_errors++;
return(-1);
}
int scanned = 0;
for(int i = total - 1; i >= 0 && scanned < budget; i--, scanned++)
{
const ulong dt = HistoryDealGetTicket(i);
if(dt == 0)
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
{
api_errors++;
continue;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
}
if((ulong)HistoryDealGetInteger(dt, DEAL_ORDER) != order_ticket)
continue;
if(StringLen(symbol) > 0 && HistoryDealGetString(dt, DEAL_SYMBOL) != symbol)
continue;
//--- уникальность тикета в собранном наборе
bool dup = false;
for(int k = 0; k < found; k++)
if(tickets[k] == dt)
{
dup = true;
break;
}
if(dup)
continue;
//--- R5-S4: буфер реестра исчерпан — явный неполный исход,
//--- а не молчаливый обрыв (сигнал «не все данные» != «данных нет»)
if(found >= ArraySize(tickets))
{
2026-09-12 22:14:24 +03:00
truncated = true;
continue;
}
tickets[found] = dt;
found++;
const double vol = HistoryDealGetDouble(dt, DEAL_VOLUME);
double units = 0;
if(VolumeToUnitsSafe(vol, volume_step, tolerance, units))
exec_units += units;
}
//--- R5-S4: достижение бюджета сканирования — история, возможно, неполна
if(scanned >= budget && budget > 0)
truncated = true;
return(found);
}
bool CStateReader::ReadPositionFacts(const ulong ticket, PositionFacts &pf)
{
pf.Zero();
if(ticket == 0)
return(false);
ResetLastError();
if(!PositionSelectByTicket(ticket))
return(false);
pf.ticket = ticket;
pf.identifier = (ulong)PositionGetInteger(POSITION_IDENTIFIER);
pf.type = PositionGetInteger(POSITION_TYPE);
pf.volume = PositionGetDouble(POSITION_VOLUME);
pf.magic = (ulong)PositionGetInteger(POSITION_MAGIC);
pf.symbol = PositionGetString(POSITION_SYMBOL);
pf.read_status = true;
return(true);
}
void CStateReader::BuildSnapshot(const ulong sequence, const ENUM_LAB_OPERATION expected_op,
const ulong order_ticket, const double requested_units,
const double volume_step, const double tolerance,
CTimeSource *clock, const ulong position_ticket_hint,
const int history_scan_budget, ConfirmationSnapshot &snap,
const bool light, const double callback_volume_units,
const int callback_deal_count)
{
snap.Zero();
snap.sequence = sequence;
snap.expected_operation = expected_op;
snap.order_ticket = order_ticket;
snap.requested_volume_units = requested_units;
snap.light_snapshot = light; // R6-B1: вид сверки — явный признак (не deal_count)
snap.check_start_us = clock.NowUs(); // начало построения снимка
if(order_ticket == 0)
return;
long state = 0;
double vol_cur = 0, vol_ini = 0;
string sym = "";
long type = 0, magic = 0;
ENUM_LAB_READ_STATUS st = LAB_READ_UNKNOWN;
if(ReadActiveOrder(order_ticket, state, vol_cur, vol_ini, sym, type, magic, st))
{
snap.active_select_status = LAB_READ_FOUND;
snap.active_select_error = 0;
snap.order_state_before = state;
//--- R4-S3: поля *_volume_units всегда в units (лоты -> шаги объёма)
double ini_u = 0.0, rem_u = 0.0;
VolumeToUnitsSafe(vol_ini, volume_step, tolerance, ini_u);
VolumeToUnitsSafe(vol_cur, volume_step, tolerance, rem_u);
snap.initial_volume_units = ini_u;
snap.remaining_volume_units = rem_u;
//--- R4-S2: факты ордера для сверки ожидаемого контракта
snap.order_symbol = OrderGetString(ORDER_SYMBOL);
snap.order_type = OrderGetInteger(ORDER_TYPE);
snap.order_magic = (ulong)OrderGetInteger(ORDER_MAGIC);
snap.order_volume_lots = OrderGetDouble(ORDER_VOLUME_INITIAL);
snap.order_price = OrderGetDouble(ORDER_PRICE_OPEN);
snap.trading_state_known = true;
}
else
{
snap.active_select_status = st;
snap.active_select_error = GetLastError();
//--- проверка истории для терминальных состояний
if(ReadHistoryOrder(order_ticket, state, vol_cur, vol_ini, sym, type, st))
{
snap.history_select_status = LAB_READ_FOUND;
snap.history_order_error = 0;
2026-09-12 22:14:24 +03:00
snap.order_state_before = state;
//--- R4-S3: единицы объёма как units
2026-09-12 22:14:24 +03:00
double ini_u = 0.0, rem_u = 0.0;
VolumeToUnitsSafe(vol_ini, volume_step, tolerance, ini_u);
VolumeToUnitsSafe(vol_cur, volume_step, tolerance, rem_u);
snap.initial_volume_units = ini_u;
snap.remaining_volume_units = rem_u;
//--- R4-S2: факты ордера из истории
snap.order_symbol = HistoryOrderGetString(order_ticket, ORDER_SYMBOL);
snap.order_type = HistoryOrderGetInteger(order_ticket, ORDER_TYPE);
snap.order_magic = (ulong)HistoryOrderGetInteger(order_ticket, ORDER_MAGIC);
snap.order_volume_lots = HistoryOrderGetDouble(order_ticket, ORDER_VOLUME_INITIAL);
snap.order_price = HistoryOrderGetDouble(order_ticket, ORDER_PRICE_OPEN);
2026-09-12 22:14:24 +03:00
snap.trading_state_known = true;
}
else
{
snap.history_select_status = st;
snap.history_order_error = GetLastError();
}
}
//--- сделки ордера
int api_errors = 0;
if(light)
{
//--- B5 (audit-3): событийная сверка в callback выполняется адресно
//--- и коротко: объём берётся из накопленных callback-сделок записи,
//--- без полного прохода истории (запрещено в OnTradeTransaction).
snap.deal_count = callback_deal_count;
snap.executed_volume_units = callback_volume_units;
snap.history_scan_failed = false;
snap.api_errors = 0;
}
else
{
bool truncated = false;
snap.deal_count = CollectDealsByOrder(order_ticket, sym, snap.deal_tickets,
snap.executed_volume_units, volume_step,
tolerance, api_errors, history_scan_budget,
truncated);
if(snap.deal_count < 0)
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
{
snap.deal_count = 0;
snap.history_scan_failed = true;
snap.api_errors = api_errors;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
}
else
{
//--- R5-S4: прерванный скан (бюджет/ёмкость) = неполный исход
snap.history_scan_failed = truncated;
snap.api_errors = api_errors;
fix(audit3): блокеры B1-B10 и существенные S1-S6 этапа 3 аудита Блокеры: - B1: cleanup как полная запись tracker (role=CLEANUP, lifecycle, DriveCleanup, нулевая экспозиция, однократный dispatch, отказ -> BLOCKED) - B2: отложенная корреляция ранних ORDER_ADD/DEAL_ADD (PushDeferred/ReconcileDeferred) - B3: collection не закрывается до ожидаемого REQUEST (T2) для market - B4: 4754 = not found; 4756/FAILED не = absence; no_active без FAILED - B5: событийная сверка light (без полного HistorySelect в callback) - B6: проверка bool HistorySelect; history_scan_budget вместо 100000 - B7: финальный reject подключён к T6 (IsFinalRejection); неоднозначные — нет - B8: E4 — reuse-анализ без торгового runtime; reuse=1 - B9: PLANNED->INTENT_SAVED->DISPATCHED; счётчик после OrderSend*; один PrecheckRejected - B10: write/timer/journal errors блокирующие (export check, integrity), timer check Существенные: - S1: фактические deviation/comment из настроек в RequestPlan/запросе - S2: bid/ask/spread/tick_time_msc + actual fields в samples.csv - S3: пустые статистики summary при n<2 - S4: histogram_rebuilt.csv в LatencyReport; experiment/series из выборки - S5: калибровка E3: символ + >=5 торговых сессий (trading_session_count) - S6: market cleanup: type_filling/deviation + PreCheck Тесты: DEFER-01, NACT-01, REJ-01, AMB-01. Компиляция 4 exe: 0 errors / 0 warnings. EOL CRLF.
2026-09-10 21:54:49 +03:00
}
}
//--- покрытие объёма: полное исполнение (для B3/B4 и CompletionPolicy)
snap.coverage_complete = (requested_units > 0.0 && snap.deal_count > 0 &&
MathAbs(snap.executed_volume_units - requested_units) <= tolerance);
//--- позиция (аудит-2 S7): адресная связь по переданному тикету
//--- либо по DEAL_POSITION_ID сделок ордера; PositionGetTicket(0)
//--- по символу не используется (ненадёжно на hedging-счёте)
ulong pos_ticket = position_ticket_hint;
ulong pos_identifier = 0;
if(pos_ticket == 0)
{
for(int k = 0; k < snap.deal_count; k++)
{
const ulong dt = snap.deal_tickets[k];
const ulong pid = (ulong)HistoryDealGetInteger(dt, DEAL_POSITION_ID);
if(pid != 0)
pos_identifier = pid;
}
if(pos_identifier != 0)
{
ResetLastError();
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong tt = PositionGetTicket(i);
if(tt == 0)
continue;
if(PositionGetString(POSITION_SYMBOL) != sym)
continue;
if((ulong)PositionGetInteger(POSITION_IDENTIFIER) == pos_identifier)
{
pos_ticket = tt;
break;
}
}
}
}
PositionFacts pf;
if(pos_ticket > 0 && ReadPositionFacts(pos_ticket, pf))
{
snap.position_found = true;
snap.position_ticket = pos_ticket;
snap.position_volume = pf.volume;
snap.position_identifier = pf.identifier;
snap.position_type = pf.type;
snap.trading_state_known = true;
snap.position_facts_last = pf;
}
snap.check_end_us = clock.NowUs(); // конец построения снимка; T6 ставит CompletionPolicy (п.7 аудита)
}
bool CStateReader::LotsToUnits(const double lots, const double step, const double tolerance,
double &units)
{
if(step <= 0.0)
return(false);
const double ratio = lots / step;
const double q = MathRound(ratio);
if(MathAbs(ratio - q) > tolerance)
return(false);
units = q;
return(true);
}
bool CStateReader::VolumeToUnitsSafe(const double volume, const double step,
const double tolerance, double &units)
{
return(LotsToUnits(volume, step, tolerance, units));
}
2026-09-12 22:14:24 +03:00
#endif // REQUEST_LATENCY_LAB_STATE_READER_MQH
//+------------------------------------------------------------------+