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

214 lines
No EOL
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 (алгоритм исходника). |
//+------------------------------------------------------------------+
bool LabNormalizePrice(const string symbol, const double price, double &result)
{
if(StringLen(symbol) == 0)
return(false);
const int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
result = NormalizeDouble(price, digits);
return(MathIsValidNumber(result));
}
//+------------------------------------------------------------------+
//| Нормализация объёма: диапазон SYMBOL_VOLUME_MIN..MAX и шаг STEP, |
//| с явной проверкой шага и диапазона. Тихо не меняет объём: |
//| выход за диапазон даёт false (требование ТЗ §2.4). |
//+------------------------------------------------------------------+
bool LabNormalizeVolume(const string symbol, const double volume, double &result)
{
if(StringLen(symbol) == 0)
return(false);
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);
if(volume < min_v || volume > max_v)
return(false);
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);
result = v;
return(MathIsValidNumber(result));
}
//+------------------------------------------------------------------+
//| Перевод объёма в целые шаги: q=round(volume/step), допуск |
//| abs(volume/step-q)<=tolerance (ТЗ §6.4, предлагаемый 1e-7). |
//+------------------------------------------------------------------+
bool VolumeToUnits(const double volume, const double step, const double tolerance,
double &units)
{
if(step <= 0.0)
return(false);
const double ratio = volume / step;
const double q = MathRound(ratio);
if(MathAbs(ratio - q) > tolerance)
return(false);
units = q;
return(true);
}
//+------------------------------------------------------------------+
//| Выбор режима исполнения (алгоритм исходника): для рыночной |
//| заявки — первый допустимый FOK/IOC, иначе RETURN. Для отложенных |
//| ордеров протокол закрепляет RETURN (ТЗ §2.4). |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING SelectFilling(const string symbol, const bool is_pending)
{
if(is_pending)
return(ORDER_FILLING_RETURN);
const long mode = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((mode & SYMBOL_FILLING_FOK) != 0)
return(ORDER_FILLING_FOK);
if((mode & SYMBOL_FILLING_IOC) != 0)
return(ORDER_FILLING_IOC);
return(ORDER_FILLING_RETURN);
}
//+------------------------------------------------------------------+
//| Построение MqlTradeRequest из плана, правил символа и котировки. |
//| Buy Limit — ниже Bid на D шагов (округление вниз), Sell Limit — |
//| выше Ask на D шагов (округление вверх), ТЗ §7. |
//+------------------------------------------------------------------+
bool LabBuildRequest(const RequestPlan &plan, const SymbolRules &rules,
const MqlTick &quote, MqlTradeRequest &request,
LabError &error)
{
error.Reset();
ZeroMemory(request);
if(!rules.IsValid())
{
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 1;
error.severity = LAB_SEV_BLOCKER;
error.message = "invalid symbol rules";
return(false);
}
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;
else
request.comment = StringFormat("RLL_%s_%I64u", plan.condition_id, plan.sequence);
if(plan.operation == LAB_OP_PENDING_DELETE)
{
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:
{
if(plan.side == LAB_SIDE_BUY)
{
request.type = ORDER_TYPE_BUY;
request.price = quote.ask;
}
else
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:
{
if(plan.side == LAB_SIDE_BUY)
{
request.type = ORDER_TYPE_BUY_LIMIT;
request.price = quote.bid - plan.distance_ticks * rules.tick_size;
}
else
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:
{
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 4;
error.message = "unsupported operation";
return(false);
}
}
//--- нормализация цены; для лимитных ордеров округление в сторону от рынка
double price = request.price;
if(!LabNormalizePrice(rules.symbol, price, price))
{
error.component = LAB_COMP_TRADE_TOOLS;
error.code = 5;
error.message = "price normalization failed";
return(false);
}
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 без журналирования результата |
//| (журналирование — на усмотрение вызывающего компонента). |
//+------------------------------------------------------------------+
bool LabPreCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result,
LabError &error)
{
error.Reset();
ZeroMemory(result);
if(!OrderCheck(request, result))
{
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);
}
result.retcode = TRADE_RETCODE_DONE;
return(true);
}
#endif // REQUEST_LATENCY_LAB_LEGACY_TRADE_REQUEST_TOOLS_MQH
//+------------------------------------------------------------------+