//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+