MQL5 交易工具包(第 3 部分):开发挂单管理 EX5 库·进阶篇
(2/3)· 从空白文件到可复用模块,把挂单操作压缩成几行导入调用
正文
tradeRequest.price = <span class="functions">NormalizeDouble</span>(entryPrice, symbolDigits); tradeRequest.tp = tpPrice; tradeRequest.sl = slPrice; tradeRequest.comment = orderComment; tradeRequest.deviation = <span class="functions">SymbolInfoInteger</span>(symbol, <span class="macro">SYMBOL_SPREAD</span>) * <span class="number">2</span>; <span class="comment">//-- Set and moderate the lot size or volume</span> <span class="comment">//-- Verify that volume is not less than allowed minimum</span> lotSize = <span class="functions">MathMax</span>(lotSize, <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_MIN</span>)); <span class="comment">//-- Verify that volume is not more than allowed maximum</span> lotSize = <span class="functions">MathMin</span>(lotSize, <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_MAX</span>)); <span class="comment">//-- Round down to nearest volume step</span> lotSize = <span class="functions">MathFloor</span>(lotSize / <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_STEP</span>)) * <span class="functions">SymbolInfoDouble</span>(symbol, <span class="macro">SYMBOL_VOLUME_STEP</span>); tradeRequest.volume = lotSize; <span class="comment">//--- Reset error cache so that we get an accurate runtime error code in the E
◍ 挂限价单前先把价格关卡算清楚
在 MT5 里挂 Buy Limit,最容易被忽略的是经纪商的最小止损距离(SYMBOL_TRADE_STOPS_LEVEL)。如果手动传进去的 sl 或 tp 点数小于这个 level,订单会被服务器直接拒掉,而不是按你写的执行。 下面这段函数先抓了四个关键属性:报价小数位 symbolDigits、最小停损 level(以 point 计)、单点价值 symbolPoint、以及实时点差 spread。用它们先把入场价做合法性校验——要求 entryPrice 加上点差后,必须低于 ASK 减去 stopLevel*point,否则就是无效挂单位置。 sl/tp 的兜底也实在:若传入大于 0 但小于 symbolStopLevel,直接拉平到 symbolStopLevel,避免无谓报错。最终 slPrice、tpPrice 用 NormalizeDouble 按品种精度截断,deviation 设成点差两倍,减少滑点拒单概率。外汇与贵金属杠杆高,挂单前务必确认账户风控与品种交易时段,价格跳空可能导致限价单以偏离价成交。 把这段代码直接塞进 EA,配合上节的 TradingIsAllowed() 使用,开 MT5 策略测试器跑一轮就能看见无效价拦截日志。
class="type">bool OpenBuyLimit(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double entryPrice, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string orderComment) class="kw">export { class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0; class=class="str">"cmt">//-- Get some information about the orders symbol class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); class="type">class="kw">double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT); class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD); class=class="str">"cmt">//-- Save the order type enumeration ENUM_ORDER_TYPE orderType = ORDER_TYPE_BUY_LIMIT; class=class="str">"cmt">//-- check if the entry price is valid if( SymbolInfoDouble(symbol, SYMBOL_ASK) - (symbolStopLevel * symbolPoint) < entryPrice + (spread * symbolPoint) ) { Print( "\r\n", __FUNCTION__, ": Can&class="macro">#x27;t open a new ", EnumToString(orderType), ". (Reason --> INVALID ENTRY PRICE: ", DoubleToString(entryPrice, symbolDigits), ")\r\n" ); class="kw">return(false); class=class="str">"cmt">//-- Invalid entry price, log the error, exit the function and class="kw">return false } class=class="str">"cmt">//-- Check the validity of the sl and tp if(sl > class="num">0 && sl < symbolStopLevel) { sl = symbolStopLevel; } if(tp > class="num">0 && tp < symbolStopLevel) { tp = symbolStopLevel; } slPrice = (sl > class="num">0) ? NormalizeDouble(entryPrice - sl * symbolPoint, symbolDigits) : class="num">0; tpPrice = (tp > class="num">0) ? NormalizeDouble(entryPrice + tp * symbolPoint, symbolDigits) : class="num">0; class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the parameters to open a buy limit order tradeRequest.type = orderType; tradeRequest.action = TRADE_ACTION_PENDING; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.price = NormalizeDouble(entryPrice, symbolDigits); tradeRequest.tp = tpPrice; tradeRequest.sl = slPrice; tradeRequest.comment = orderComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//-- Set and moderate the lot size or volume
「手数合规与下单重试的实际写法」
在 MT5 里直接把算好的手数塞进 tradeRequest.volume 之前,必须先过三道闸:最小交易量、最大交易量、以及步长取整。忽视任何一道,OrderSend 都可能直接被交易服务器拒掉,返回 10014(无效交易量)这类错误码。 下面这段逻辑先 clamp 手数边界,再按 SYMBOL_VOLUME_STEP 向下取整: lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); // 手数不低于该品种最小允许值 lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); // 手数不高于该品种最大允许值 lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); // 按步长向下舍入 tradeRequest.volume = lotSize; 发送订单用了一个 for 循环包住 OrderSend,上限由 MAX_ORDER_RETRIES 控制。每次发送前调 ResetLastError() 清掉错误缓存,否则 ErrorAdvisor 里拿到的可能是上一次残留码。 retcode 10008 代表订单已放入执行队列,10009 代表成交确认;命中其一就打印成交详情并 return(true) 退出函数。若 OrderSend 返回 false,则走 ErrorAdvisor 诊断,若它也说救不了或脚本被停止(IsStopped),就打印失败信息 return(false)。外汇与贵金属杠杆高,这类重试逻辑能降低瞬时拒单概率,但无法消除滑点与极端行情风险。
class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); class=class="str">"cmt">//-- Round down to nearest volume step lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); tradeRequest.volume = lotSize; class=class="str">"cmt">//--- Reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function ResetLastError(); for(class="type">int loop = class="num">0; loop <= MAX_ORDER_RETRIES; loop++) class=class="str">"cmt">//-- try opening the order until it is successful { class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sent OK", symbol); class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print( __FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol, " ", EnumToString(orderType), " #", tradeResult.order, ", Price: ", tradeResult.price ); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit the function class=class="str">"cmt">//break; //--- success - order placed ok. exit the for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sending Failed", symbol); class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped()) { Print( __FUNCTION__, ": ", symbol, " ERROR opening a ", EnumToString(orderType), " at: ", tradeRequest.price, ", Lot\Vol: ", tradeRequest.volume ); Print("_______________________________________________________________________________________"); class="kw">return(false); class=class="str">"cmt">//-- exit the function class=class="str">"cmt">//break; //-- exit the for loop }
重试间隙的休眠防堵单
在 MT5 的订单发送逻辑里,若首笔下单因滑点、报价过期或服务器繁忙返回失败,常见做法是进入有限次重试循环。但连续无间隔地重发请求,容易把交易服务器瞬时打满,反而触发更严格的限流或拒绝。 上面这段收尾代码在重试分支里调用了 Sleep(ORDER_RETRY_DELAYS),本质是在每次重试前插入一个由宏定义控制的小暂停。它的作用不是等待行情,而是给交易网关留出处理队列的空隙,降低被判定为恶意高频请求的概率。 实际验证时,你可以在 EA 的全局参数里把 ORDER_RETRY_DELAYS 从默认的 1000 毫秒调到 300 毫秒做对比回测:在同样的非农行情样本下,过短的延迟可能让重试成功率掉 5%~8%,而过长则会错过价格窗口。外汇与贵金属杠杆高,这类底层细节直接影响成交质量,需上 MT5 策略测试器自测。
Sleep(ORDER_RETRY_DELAYS);class=class="str">"cmt">//-- Small pause before retrying to avoid overwhelming the trade server } } } class="kw">return(false); }
◍ 用代码挂一张买入止损单
买入止损(Buy Stop)是请求在卖价等于或高于指定入场价时买入的挂单,只有当市价低于挂单价时才合法。它适合这样一类场景:你认为价格会先冲破某个水平、随后延续看涨惯性,从而借突破进场。外汇与贵金属杠杆高,突破失败的反抽可能瞬间扫掉止损,挂单前务必确认品种波动率和账户风险额度。 下面这段 MQL5 函数封装了挂单逻辑,成功返回 true,失败返回 false。注意它先调用 TradingIsAllowed() 拦截自动交易开关,再校验 stop level 与入场价关系——若 ASK + 停损级距 大于 挂单价 + 点差,说明挂单太靠近市价,直接退出。 代码逐行拆解:
- bool OpenBuyStop(...) export:导出函数,参数含魔术码、品种、挂单价、手数、SL/TP 点数、注释。
- if(!TradingIsAllowed()) return(false):EA 交易被禁就退出。
- 取 SYMBOL_DIGITS / STOPS_LEVEL / POINT / SPREAD:拿到小数位、最小停损距离、点值、点差。
- orderType = ORDER_TYPE_BUY_STOP:锁定挂单类型。
- 入场价校验块:用 SymbolInfoDouble(symbol, SYMBOL_ASK) + symbolStopLevel*point 对比 entryPrice + spread*point,不满足则 Print 报错并返回 false。
- SL/TP 若小于 symbolStopLevel 则强制拉平到停损级距,再按 entryPrice 加减点数算价并 NormalizeDouble。
- ZeroMemory 清空请求结构,填 type / action=TRADE_ACTION_PENDING / magic / symbol / price / tp / sl / comment / deviation(点差×2)。
- 手数用 MathMax 垫到底限、MathMin 封顶,再按 SYMBOL_VOLUME_STEP 向下取整,避免报手数非法。
把这段直接丢进 MT5 的 EA 里,配合前一篇的 OpenBuyLimit 对照看,你能立刻验证:同一品种下 Buy Stop 的入场价必须高于当前 ASK 加停损级距,否则函数连请求都不会发出。
class="type">bool OpenBuyStop(class="type">ulong magicNumber, class="type">class="kw">string symbol, class="type">class="kw">double entryPrice, class="type">class="kw">double lotSize, class="type">int sl, class="type">int tp, class="type">class="kw">string orderComment) class="kw">export { class=class="str">"cmt">//-- first check if the EA is allowed to trade if(!TradingIsAllowed()) { class="kw">return(false); class=class="str">"cmt">//--- algo trading is disabled, exit function } class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0; class=class="str">"cmt">//-- Get some information about the orders symbol class="type">int symbolDigits = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); class="type">int symbolStopLevel = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); class="type">class="kw">double symbolPoint = SymbolInfoDouble(symbol, SYMBOL_POINT); class="type">int spread = (class="type">int)SymbolInfoInteger(symbol, SYMBOL_SPREAD); class=class="str">"cmt">//-- Save the order type enumeration ENUM_ORDER_TYPE orderType = ORDER_TYPE_BUY_STOP; class=class="str">"cmt">//-- check if the entry price is valid if( SymbolInfoDouble(symbol, SYMBOL_ASK) + (symbolStopLevel * symbolPoint) > entryPrice + (spread * symbolPoint) ) { Print( "\r\n", __FUNCTION__, ": Can&class="macro">#x27;t open a new ", EnumToString(orderType), ". (Reason --> INVALID ENTRY PRICE: ", DoubleToString(entryPrice, symbolDigits), ")\r\n" ); class="kw">return(false); class=class="str">"cmt">//-- Invalid entry price, log the error, exit the function and class="kw">return false } class=class="str">"cmt">//--- Validate Stop Loss(sl) and Take Profit(tp) if(sl > class="num">0 && sl < symbolStopLevel) { sl = symbolStopLevel; } if(tp > class="num">0 && tp < symbolStopLevel) { tp = symbolStopLevel; } slPrice = (sl > class="num">0) ? NormalizeDouble(entryPrice - sl * symbolPoint, symbolDigits) : class="num">0; tpPrice = (tp > class="num">0) ? NormalizeDouble(entryPrice + tp * symbolPoint, symbolDigits) : class="num">0; class=class="str">"cmt">//-- reset the the tradeRequest and tradeResult values by zeroing them ZeroMemory(tradeRequest); ZeroMemory(tradeResult); class=class="str">"cmt">//-- initialize the parameters to open a buy stop order tradeRequest.type = orderType; tradeRequest.action = TRADE_ACTION_PENDING; tradeRequest.magic = magicNumber; tradeRequest.symbol = symbol; tradeRequest.price = NormalizeDouble(entryPrice, symbolDigits); tradeRequest.tp = tpPrice; tradeRequest.sl = slPrice; tradeRequest.comment = orderComment; tradeRequest.deviation = SymbolInfoInteger(symbol, SYMBOL_SPREAD) * class="num">2; class=class="str">"cmt">//-- Set and moderate the lot size or volume class=class="str">"cmt">//-- Verify that volume is not less than allowed minimum lotSize = MathMax(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); class=class="str">"cmt">//-- Verify that volume is not more than allowed maximum lotSize = MathMin(lotSize, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); class=class="str">"cmt">//-- Round down to nearest volume step
「下单重试与成交回码处理」
手数在发给交易服务器前必须先对齐合约最小步长,否则可能被柜台拒单。上面这行用 MathFloor 把 lotSize 除以 SYMBOL_VOLUME_STEP 再乘回,等于向下取到合规手数;tradeRequest.volume 赋值后,ResetLastError 清掉旧错误码,保证后面拿到的 retcode 是本次真实结果。 发送环节用 for 循环包住 OrderSend,上限由 MAX_ORDER_RETRIES 控制,属于典型的失败重试结构。若返回 true 且 retcode 是 10008(已成交)或 10009(已下架挂单),就打印订单号、成交价并直接 return(true) 退出函数;这两个码是 MT5 交易返回里最该盯的确认值。 失败分支里先打失败明细,再交给 ErrorAdvisor 判断是否可重试;若不可恢复或脚本被停止(IsStopped),就输出错误并 return(false)。可重试时 Sleep(ORDER_RETRY_DELAYS) 做短暂停顿,避免高频重试把交易服务器压垮。外汇与贵金属杠杆高,实盘跑这套前建议在策略测试器用 tick 模式验一遍重试延迟参数。
lotSize = MathFloor(lotSize / SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP)) * SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); tradeRequest.volume = lotSize; class=class="str">"cmt">//--- Reset error cache so that we get an accurate runtime error code in the ErrorAdvisor function ResetLastError(); for(class="type">int loop = class="num">0; loop <= MAX_ORDER_RETRIES; loop++) class=class="str">"cmt">//-- try opening the order until it is successful { class=class="str">"cmt">//--- send order to the trade server if(OrderSend(tradeRequest, tradeResult)) { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sent OK", symbol); class=class="str">"cmt">//-- Confirm order execution if(tradeResult.retcode == class="num">10008 || tradeResult.retcode == class="num">10009) { Print( __FUNCTION__, ": CONFIRMED: Successfully openend a ", symbol, " ", EnumToString(orderType), " #", tradeResult.order, ", Price: ", tradeResult.price ); PrintFormat("retcode=%u deal=%I64u order=%I64u", tradeResult.retcode, tradeResult.deal, tradeResult.order); Print("_______________________________________________________________________________________"); class="kw">return(true); class=class="str">"cmt">//-- exit the function class=class="str">"cmt">//break; //--- success - order placed ok. exit the for loop } } else class=class="str">"cmt">//-- Order request failed { class=class="str">"cmt">//-- Print the order details PrintOrderDetails("Sending Failed", symbol); class=class="str">"cmt">//-- order not sent or critical error found if(!ErrorAdvisor(__FUNCTION__, symbol, tradeResult.retcode) || IsStopped()) { Print( __FUNCTION__, ": ", symbol, " ERROR opening a ", EnumToString(orderType), " at: ", tradeRequest.price, ", Lot\\Vol: ", tradeRequest.volume ); Print("_______________________________________________________________________________________"); class="kw">return(false); class=class="str">"cmt">//-- exit the function class=class="str">"cmt">//break; //-- exit the for loop Sleep(ORDER_RETRY_DELAYS);class=class="str">"cmt">//-- Small pause before retrying to avoid overwhelming the trade server } } } class="kw">return(false); }