353 строки
Без EOL
16 КиБ
MQL5
353 строки
Без EOL
16 КиБ
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:
|
|
//--- чтение активного ордера по тикету
|
|
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
|
|
//+------------------------------------------------------------------+ |