214 lines
No EOL
8.8 KiB
MQL5
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 "e, 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
|
|
//+------------------------------------------------------------------+ |