mql5-execution-microstructu.../Include/RequestLatencyLab/Legacy/TradeRequestTools.mqh

214 lines
8.8 KiB
MQL5

//+------------------------------------------------------------------+
//| TradeRequestTools.mqh |
//| Copyright 2026, MetaQuotes Ltd. |
//| www.mql5.com |
//+------------------------------------------------------------------+
#ifndef REQUEST_LATENCY_LAB_LEGACY_TRADE_REQUEST_TOOLS_MQH
#define REQUEST_LATENCY_LAB_LEGACY_TRADE_REQUEST_TOOLS_MQH
#include "..\..\..\Include\RequestLatencyLab\Models.mqh"
//+------------------------------------------------------------------+
//| Нормализация цены по SYMBOL_DIGITS (алгоритм исходника). |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
bool LabNormalizePrice(const string symbol, const double price, double &result)
{
2026-09-12 22:14:24 +03:00
if(StringLen(symbol) == 0)
return(false);
2026-09-12 22:14:24 +03:00
const int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
result = NormalizeDouble(price, digits);
return(MathIsValidNumber(result));
}
//+------------------------------------------------------------------+
//| Нормализация объёма: диапазон SYMBOL_VOLUME_MIN..MAX и шаг STEP, |
//| с явной проверкой шага и диапазона. Тихо не меняет объём: |
2026-09-12 22:14:24 +03:00
//| выход за диапазон даёт false (требование ТЗ §2.4). |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
bool LabNormalizeVolume(const string symbol, const double volume, double &result)
{
2026-09-12 22:14:24 +03:00
if(StringLen(symbol) == 0)
return(false);
2026-09-12 22:14:24 +03:00
const double min_v = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
const double max_v = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
const double step_v = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
if(step_v <= 0.0)
return(false);
2026-09-12 22:14:24 +03:00
if(volume < min_v || volume > max_v)
return(false);
2026-09-12 22:14:24 +03:00
double v = MathRound(volume / step_v) * step_v;
int vdigits = (int)MathCeil(-MathLog10(step_v));
if(vdigits < 0)
vdigits = 0;
v = NormalizeDouble(v, vdigits);
if(v < min_v || v > max_v)
return(false);
2026-09-12 22:14:24 +03:00
result = v;
return(MathIsValidNumber(result));
}
//+------------------------------------------------------------------+
//| Перевод объёма в целые шаги: q=round(volume/step), допуск |
//| abs(volume/step-q)<=tolerance (ТЗ §6.4, предлагаемый 1e-7). |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
bool VolumeToUnits(const double volume, const double step, const double tolerance,
double &units)
{
2026-09-12 22:14:24 +03:00
if(step <= 0.0)
return(false);
2026-09-12 22:14:24 +03:00
const double ratio = volume / step;
const double q = MathRound(ratio);
if(MathAbs(ratio - q) > tolerance)
return(false);
2026-09-12 22:14:24 +03:00
units = q;
return(true);
}
//+------------------------------------------------------------------+
//| Выбор режима исполнения (алгоритм исходника): для рыночной |
//| заявки — первый допустимый FOK/IOC, иначе RETURN. Для отложенных |
2026-09-12 22:14:24 +03:00
//| ордеров протокол закрепляет RETURN (ТЗ §2.4). |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
ENUM_ORDER_TYPE_FILLING SelectFilling(const string symbol, const bool is_pending)
{
if(is_pending)
return(ORDER_FILLING_RETURN);
2026-09-12 22:14:24 +03:00
const long mode = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((mode & SYMBOL_FILLING_FOK) != 0)
return(ORDER_FILLING_FOK);
2026-09-12 22:14:24 +03:00
if((mode & SYMBOL_FILLING_IOC) != 0)
return(ORDER_FILLING_IOC);
return(ORDER_FILLING_RETURN);
}
//+------------------------------------------------------------------+
//| Построение MqlTradeRequest из плана, правил символа и котировки. |
//| Buy Limit — ниже Bid на D шагов (округление вниз), Sell Limit — |
//| выше Ask на D шагов (округление вверх), ТЗ §7. |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
bool LabBuildRequest(const RequestPlan &plan, const SymbolRules &rules,
const MqlTick &quote, MqlTradeRequest &request,
LabError &error)
{
error.Reset();
ZeroMemory(request);
if(!rules.IsValid())
{
2026-09-12 22:14:24 +03:00
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 1;
error.severity = LAB_SEV_BLOCKER;
error.message = "invalid symbol rules";
return(false);
}
2026-09-12 22:14:24 +03:00
const bool is_market = (plan.operation == LAB_OP_MARKET_OPEN ||
plan.operation == LAB_OP_POSITION_CLOSE);
request.action = (is_market ? TRADE_ACTION_DEAL : TRADE_ACTION_PENDING);
request.magic = (long)plan.magic;
request.symbol = plan.symbol;
request.volume = plan.volume;
//--- S1: фактическое отклонение из плана (не хардкод 0)
request.deviation = plan.deviation_points;
request.type_time = ORDER_TIME_GTC;
request.type_filling = SelectFilling(rules.symbol, !is_market);
//--- S1: фактический comment (или дефолт)
if(StringLen(plan.comment) > 0)
request.comment = plan.comment;
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
2026-09-12 22:14:24 +03:00
request.comment = StringFormat("RLL_%s_%I64u", plan.condition_id, plan.sequence);
if(plan.operation == LAB_OP_PENDING_DELETE)
{
2026-09-12 22:14:24 +03:00
request.action = TRADE_ACTION_REMOVE;
request.order = plan.request_price > 0.0 ? (ulong)MathRound(plan.request_price) : 0;
return(true);
}
switch(plan.operation)
{
case LAB_OP_MARKET_OPEN:
case LAB_OP_POSITION_CLOSE:
{
2026-09-12 22:14:24 +03:00
if(plan.side == LAB_SIDE_BUY)
{
2026-09-12 22:14:24 +03:00
request.type = ORDER_TYPE_BUY;
request.price = quote.ask;
}
else
2026-09-12 22:14:24 +03:00
if(plan.side == LAB_SIDE_SELL)
{
request.type = ORDER_TYPE_SELL;
request.price = quote.bid;
}
else
{
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 2;
error.message = "market operation requires side";
return(false);
}
break;
}
case LAB_OP_PENDING_CREATE:
{
2026-09-12 22:14:24 +03:00
if(plan.side == LAB_SIDE_BUY)
{
2026-09-12 22:14:24 +03:00
request.type = ORDER_TYPE_BUY_LIMIT;
request.price = quote.bid - plan.distance_ticks * rules.tick_size;
}
else
2026-09-12 22:14:24 +03:00
if(plan.side == LAB_SIDE_SELL)
{
request.type = ORDER_TYPE_SELL_LIMIT;
request.price = quote.ask + plan.distance_ticks * rules.tick_size;
}
else
{
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 3;
error.message = "pending create requires side";
return(false);
}
break;
}
default:
{
2026-09-12 22:14:24 +03:00
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 4;
error.message = "unsupported operation";
return(false);
}
}
2026-09-12 22:14:24 +03:00
//--- нормализация цены; для лимитных ордеров округление в сторону от рынка
double price = request.price;
if(!LabNormalizePrice(rules.symbol, price, price))
{
2026-09-12 22:14:24 +03:00
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 5;
error.message = "price normalization failed";
return(false);
}
2026-09-12 22:14:24 +03:00
if(request.type == ORDER_TYPE_BUY_LIMIT)
price = MathFloor(price / rules.tick_size + 0.00000001) * rules.tick_size;
else
if(request.type == ORDER_TYPE_SELL_LIMIT)
price = MathCeil(price / rules.tick_size - 0.00000001) * rules.tick_size;
request.price = NormalizeDouble(price, rules.digits);
return(true);
}
//+------------------------------------------------------------------+
//| PreCheck — обёртка OrderCheck без журналирования результата |
//| (журналирование — на усмотрение вызывающего компонента). |
//+------------------------------------------------------------------+
2026-09-12 22:14:24 +03:00
bool LabPreCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result,
LabError &error)
{
error.Reset();
ZeroMemory(result);
2026-09-12 22:14:24 +03:00
if(!OrderCheck(request, result))
{
2026-09-12 22:14:24 +03:00
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 10;
error.mql_error = GetLastError();
error.severity = LAB_SEV_BLOCKER;
error.message = StringFormat("OrderCheck refused retcode=%u", result.retcode);
return(false);
}
2026-09-12 22:14:24 +03:00
result.retcode = TRADE_RETCODE_DONE;
return(true);
}
2026-09-12 22:14:24 +03:00
#endif // REQUEST_LATENCY_LAB_LEGACY_TRADE_REQUEST_TOOLS_MQH
//+------------------------------------------------------------------+