//+------------------------------------------------------------------+ //| 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: //--- чтение активного ордера по тикету 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 с диапазоном) static bool ReadHistoryOrder(const ulong ticket, long &state, double &volume_current, double &volume_initial, string &symbol, long &type, ENUM_LAB_READ_STATUS &status); //--- сбор уникальных сделок ордера в пределах выбранной истории 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) { api_errors++; continue; } 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)) { 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; snap.order_state_before = state; //--- R4-S3: единицы объёма как 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 = 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); 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) { snap.deal_count = 0; snap.history_scan_failed = true; snap.api_errors = api_errors; } else { //--- R5-S4: прерванный скан (бюджет/ёмкость) = неполный исход snap.history_scan_failed = truncated; snap.api_errors = api_errors; } } //--- покрытие объёма: полное исполнение (для 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)); } #endif // REQUEST_LATENCY_LAB_STATE_READER_MQH //+------------------------------------------------------------------+