交易机器人在市场发布前必须经过的检验·进阶篇
盯紧单品种的手数天花板
在 MT5 里下单一品种前,最容易被忽略的是该品种的总交易量封顶值。MQL5 提供 SYMBOL_VOLUME_LIMIT 这个属性,若经纪商设了限,则该品种已开仓+挂单的总手数不能突破它;返回 0 代表无限制,此时只能退而参考 SYMBOL_VOLUME_MAX 单笔上限。 对于支持对冲的账户,一个品种可能叠加多个同向或反向仓位,必须遍历所有 Position 和 Pending 求和,否则会低估已占用额度。下面这段代码给出三个可直接拷进 EA 的辅助函数。 [CODE] //+------------------------------------------------------------------+
| // | 返回指定交易品种的仓位大小 |
|---|
//+------------------------------------------------------------------+ double PositionVolume(string symbol) { //--- 尝试根据交易品种选择仓位 bool selected=PositionSelect(symbol); //--- 有仓位 if(selected) //--- 返回仓位交易量 return(PositionGetDouble(POSITION_VOLUME)); else { //--- 选择仓位出错报告 Print(__FUNCTION__," 执行 PositionSelect() 失败,交易品种为 ", symbol," 错误编号 ",GetLastError()); return(-1); } } double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); if(max_volume==0) volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); //+------------------------------------------------------------------+
| // | 返回交易品种当前挂单的交易量 |
|---|
//+------------------------------------------------------------------+ double PendingsVolume(string symbol) { double volume_on_symbol=0; ulong ticket; //--- 获得全部交易品种当前下单数量 int all_orders=OrdersTotal(); //--- 恢复范围内的全部订单 for(int i=0;i<all_orders;i++) { //--- 获得列表中持仓的订单号 if(ticket=OrderGetTicket(i)) { //--- 如果订单中指定我们的交易品种,增加该订单的交易量 if(symbol==OrderGetString(ORDER_SYMBOL)) volume_on_symbol+=OrderGetDouble(ORDER_VOLUME_INITIAL); } } //--- 返回指定交易品种当前已下挂单的总交易量 return(volume_on_symbol); } //+------------------------------------------------------------------+
| // | 返回交易品种中一个订单允许的最大交易量 |
|---|
//+------------------------------------------------------------------+ double NewOrderAllowedVolume(string symbol) { double allowed_volume=0; //--- 取得订单最大交易量限制 double symbol_max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); //--- 取得一个交易品种的交易量限制 double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); //--- 取得一个交易品种已建仓位的交易量 double opened_volume=PositionVolume(symbol); if(opened_volume>=0) { //--- 如果我们已经用完了交易量 if(max_volume-opened_volume<=0) return(0); //--- 如果已建仓位的交易量没有超过 max_volume double orders_volume_on_symbol=PendingsVolume(symbol); [/CODE] 逐行拆一下:PositionVolume() 先调 PositionSelect(symbol) 选中该品种仓位,成功就用 PositionGetDouble(POSITION_VOLUME) 吐出当前手数,失败返回 -1 并打印错误码。PendingsVolume() 用 OrdersTotal() 拿全部订单数,循环里用 OrderGetTicket(i) 取订单句柄,再比对 ORDER_SYMBOL 是否等于目标品种,是就把 ORDER_VOLUME_INITIAL 累加。NewOrderAllowedVolume() 里先抓 SYMBOL_VOLUME_MAX 和 SYMBOL_VOLUME_LIMIT,再算已开仓手数,若限额减已开仓小于等于 0 直接返回 0 禁止再加。 外汇与贵金属杠杆高、品种限额因经纪商而异,实际跑之前请在策略测试器用真实品种符号验证 SYMBOL_VOLUME_LIMIT 的返回值,可能某些品种返回 0 让你误以为能无限加仓。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回指定交易品种的仓位大小 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double PositionVolume(class="type">class="kw">string symbol) { class=class="str">"cmt">//--- 尝试根据交易品种选择仓位 class="type">bool selected=PositionSelect(symbol); class=class="str">"cmt">//--- 有仓位 if(selected) class=class="str">"cmt">//--- 返回仓位交易量 class="kw">return(PositionGetDouble(POSITION_VOLUME)); else { class=class="str">"cmt">//--- 选择仓位出错报告 Print(__FUNCTION__," 执行 PositionSelect() 失败,交易品种为 ", symbol," 错误编号 ",GetLastError()); class="kw">return(-class="num">1); } } class="type">class="kw">double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); if(max_volume==class="num">0) volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回交易品种当前挂单的交易量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double PendingsVolume(class="type">class="kw">string symbol) { class="type">class="kw">double volume_on_symbol=class="num">0; class="type">class="kw">ulong ticket; class=class="str">"cmt">//--- 获得全部交易品种当前下单数量 class="type">int all_orders=OrdersTotal(); class=class="str">"cmt">//--- 恢复范围内的全部订单 for(class="type">int i=class="num">0;i<all_orders;i++) { class=class="str">"cmt">//--- 获得列表中持仓的订单号 if(ticket=OrderGetTicket(i)) { class=class="str">"cmt">//--- 如果订单中指定我们的交易品种,增加该订单的交易量 if(symbol==OrderGetString(ORDER_SYMBOL)) volume_on_symbol+=OrderGetDouble(ORDER_VOLUME_INITIAL); } } class=class="str">"cmt">//--- 返回指定交易品种当前已下挂单的总交易量 class="kw">return(volume_on_symbol); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回交易品种中一个订单允许的最大交易量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double NewOrderAllowedVolume(class="type">class="kw">string symbol) { class="type">class="kw">double allowed_volume=class="num">0; class=class="str">"cmt">//--- 取得订单最大交易量限制 class="type">class="kw">double symbol_max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); class=class="str">"cmt">//--- 取得一个交易品种的交易量限制 class="type">class="kw">double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); class=class="str">"cmt">//--- 取得一个交易品种已建仓位的交易量 class="type">class="kw">double opened_volume=PositionVolume(symbol); if(opened_volume>=class="num">0) { class=class="str">"cmt">//--- 如果我们已经用完了交易量 if(max_volume-opened_volume<=class="num">0) class="kw">return(class="num">0); class=class="str">"cmt">//--- 如果已建仓位的交易量没有超过 max_volume class="type">class="kw">double orders_volume_on_symbol=PendingsVolume(symbol);
◍ 账户层风控下的可开仓手数计算
在 EA 里动态算可开仓手数,不能只盯策略信号,还得先扣掉已占用的仓位和挂单。上面这段从账户总允许量里减去已开仓与同品种挂单占用,就是实盘里防止超仓的直接做法。 若算出来的值大于品种上限 symbol_max_volume,就强制截断到品种上限,避免 broker 拒单。这个判断在多点触发的网格或马丁类策略里尤其关键,否则一次重算偏差就可能把账户推到保证金临界。 外汇与贵金属杠杆高,超仓带来的强平风险远大于少赚几单,建议把这段代码直接塞进 OrderSend 前的校验函数里,在 MT5 用策略测试器跑一遍极端行情看截断是否生效。
allowed_volume=max_volume-opened_volume-orders_volume_on_symbol; if(allowed_volume>symbol_max_volume) allowed_volume=symbol_max_volume; } class="kw">return(allowed_volume); }
「TP/SL 距离市价的最小点数门槛」
动态计算止损止盈时,方向判断要和报价源错开看:买入按 Ask 成交,但 TP/SL 必须对照 Bid 去比;卖出按 Bid 成交,TP/SL 则对照 Ask。错配报价源会直接把校验逻辑写反,订单可能被经纪商拒掉。 交易品种属性里有个 SYMBOL_TRADE_STOPS_LEVEL,定义了止损获利离当前平仓价的最小点数距离。若该值为 0,代表不限制最小距离;非 0 时,市价单必须满足:买方向下 SL 距 Bid 至少 N 点、向上 TP 距 Bid 至少 N 点,卖单则对称地距 Ask 至少 N 点。 挂单反而简单——以挂单激活价 Open 为基准,限价/止损买卖都要求 TP 与 SL 距 Open 大于 SYMBOL_TRADE_STOPS_LEVEL 个点,不用再区分 Ask/Bid。 下面这段校验函数把上述规则落地了,注意它用 _Point 把整数点数换算成绝对价格,并在不通过时打印具体边界值,方便在 MT5 策略测试器里直接排错。外汇与贵金属杠杆高,点差突变时最小距离可能临时扩大,校验失败概率会上升,实盘前务必在模拟账户跑一遍。
<span class="keyword">class="type">bool</span> CheckStopLoss_Takeprofit(<span class="macro">ENUM_ORDER_TYPE</span> type,<span class="keyword">class="type">class="kw">double</span> SL,<span class="keyword">class="type">class="kw">double</span> TP) { <span class="comment">class=class="str">"cmt">//--- 取得 SYMBOL_TRADE_STOPS_LEVEL 水平</span> <span class="keyword">class="type">int</span> stops_level=(<span class="keyword">class="type">int</span>)<span class="functions">SymbolInfoInteger</span>(<span class="predefines">_Symbol</span>,<span class="macro">SYMBOL_TRADE_STOPS_LEVEL</span>); <span class="keyword">if</span>(stops_level!=<span class="number">class="num">0</span>) { <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"SYMBOL_TRADE_STOPS_LEVEL=%d: 止损和获利必须"</span>+ <span class="class="type">class="kw">string">" 距离当前价格大于 %d 个点"</span>,stops_level,stops_level); } <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">bool</span> SL_check=<span class="macro">class="kw">false</span>,TP_check=<span class="macro">class="kw">false</span>; <span class="comment">class=class="str">"cmt">//--- 只检查两种订单类型</span> <span class="keyword">class="kw">switch</span>(type) { <span class="comment">class=class="str">"cmt">//--- 买入操作</span> <span class="keyword">case</span> <span class="macro">ORDER_TYPE_BUY</span>: { <span class="comment">class=class="str">"cmt">//--- 检验止损</span> SL_check=(<span class="predefines">Bid</span>-SL>stops_level*<span class="predefines">_Point</span>); <span class="keyword">if</span>(!SL_check) <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"对于订单 %s 止损=%.5f 必须小于 %.5f"</span>+ <span class="class="type">class="kw">string">" (Bid=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)"</span>, <span class="functions">EnumToString</span>(type),SL,<span class="predefines">Bid</span>-stops_level*<span class="predefines">_Point</span>,<span class="predefines">Bid</span>,stops_level); <span class="comment">class=class="str">"cmt">//--- 检验获利</span> TP_check=(TP-<span class="predefines">Bid</span>>stops_level*<span class="predefines">_Point</span>); <span class="keyword">if</span>(!TP_check) <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"对于 %s 获利=%.5f 必须大于 %.5f"</span>+ <span class="class="type">class="kw">string">" (Bid=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)"</span>, <span class="functions">EnumToString</span>(type),TP,<span class="predefines">Bid</span>+stops_level*<span class="predefines">_Point</span>,<span class="predefines">Bid</span>,stops_level); <span class="comment">class=class="str">"cmt">//--- 返回检验结果</span> <span class="keyword">class="kw">return</span>(SL_check&&TP_check); } <span class="comment">class=class="str">"cmt">//--- 卖出操作</span> <span class="keyword">case</span> <span class="macro">ORDER_TYPE_SELL</span>: { <span class="comment">class=class="str">"cmt">//--- 检验止损</span>
用代码卡死止损获利的最小距离
在 MT5 发单前,经纪商规定的 SYMBOL_TRADE_STOPS_LEVEL 是一道硬门槛:止损和获利离市价必须超过该点数乘 _Point,否则订单会被拒。下面这段校验逻辑针对市价买单,用 SL_check 和 TP_check 两个布尔量把规则钉死。 SL_check=(SL-Ask>stops_level*_Point); if(!SL_check) PrintFormat("对于订单 %s 止损=%.5f 必须大于 %.5f "+ " (Ask=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)", EnumToString(type),SL,Ask+stops_level*_Point,Ask,stops_level); TP_check=(Ask-TP>stops_level*_Point); if(!TP_check) PrintFormat("对于订单 %s 获利=%.5f 必须小于 %.5f "+ " (Ask=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)", EnumToString(type),TP,Ask-stops_level*_Point,Ask,stops_level); return(TP_check&&SL_check); 逐行看:SL_check 要求止损价低于 Ask 且差距大于 stops_level 点;不达标就打印具体差值。TP_check 则要求获利价高于 Ask 且距离同样过点数门槛。两者皆真才 return true,否则函数返回 false,订单不应继续发送。 脚本里故意用 Bid+2*_Point 设 SL、Bid-2*_Point 设 TP 来触发报错——在多数品种上 2 点远小于 stops_level(常见 10~50 点),终端会直接吐出“止损或者获利水平是不对的!”。外汇和贵金属杠杆高,这种距离校验能帮你避开无谓的 rejected 回报,实盘前先在策略测试器跑一遍看打印。
SL_check=(SL-Ask>stops_level*_Point); if(!SL_check) PrintFormat("对于订单 %s 止损=%.5f 必须大于 %.5f "+ " (Ask=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)", EnumToString(type),SL,Ask+stops_level*_Point,Ask,stops_level); class=class="str">"cmt">//--- 检验获利 TP_check=(Ask-TP>stops_level*_Point); if(!TP_check) PrintFormat("对于订单 %s 获利=%.5f 必须小于 %.5f "+ " (Ask=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)", EnumToString(type),TP,Ask-stops_level*_Point,Ask,stops_level); class=class="str">"cmt">//--- 返回检验结果 class="kw">return(TP_check&&SL_check);
◍ 止损止盈被拒的层级真相
在 MT5 里挂单或下市价单,SL/TP 不是你想设多近都行。经纪商通过 SYMBOL_TRADE_STOPS_LEVEL 强制限定止损获利与当前价的最小距离,EURUSD H1 下该值常为 30 点(_Point=0.00001 时即 0.00030)。 上面这段 MQL5 片段在卖单场景把 SL 放在 Ask-2*_Point、TP 放在 Ask+2*_Point,显然远远小于 30 点限制,于是 CheckStopLoss_Takeprofit 返回 false,Print 提示“止损或者获利水平是不对的”,随后 Sell 调用仍触发了 OrderSend 错误 4756、retcode=10016,订单与成交均为 0。 回测日志给得更直白:Buy at 1.11433 时 SL=1.11425、TP=1.11421,而 Bid=1.11423,系统要求买入单 SL 必须 < 1.11393(Bid-30点)、TP 必须 > 1.11453(Bid+30点),否则直接判错。 还有个隐藏雷区是 SYMBOL_TRADE_FREEZE_LEVEL=20:当价格距已激活单仅 20 点时禁止修改。日志里两次“立即买入 1.00 EURUSD sl:1.11404”都标 [无效止损],因为 StopLoss-Bid 分别为 -26 和 -27 点,未跨过 30 点门槛。外汇与贵金属杠杆高,实盘前务必用 SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL) 读出真实限制再算挂单价。
class="type">class="kw">double SL=NormalizeDouble(Ask-class="num">2*_Point,_Digits); class="type">class="kw">double TP=NormalizeDouble(Ask+class="num">2*_Point,_Digits); class=class="str">"cmt">//--- 进行检查 PrintFormat("Sell at %.5f SL=%.5f TP=%.5f Ask=%.5f",price,SL,TP,Ask); if(!CheckStopLoss_Takeprofit(ORDER_TYPE_SELL,SL,TP)) Print("止损或者获利水平是不对的!"); class=class="str">"cmt">//--- try to sell anyway, in order to see the execution result Sell(price,SL,TP); }
「挂单止损距离踩到 Stops Level 的边界」
在即时成交模式下,SYMBOL_TRADE_STOPS_LEVEL=30 意味着止损或获利必须离当前 Bid 至少 30 个点。上面那组市价买入测试里,SL 距 Bid 为 -28、-29 点时全部被拒,报「无效止损」;直到第 5 笔 SL=1.11398、距 Bid 1.11428 正好 -30 点才成交,实际止损宽度 41 点(点差 12 + Stops Level 30)。 换到获利测试,TP 距 Bid 26~29 点连续四笔被拒,第 5 笔 TP 距 Bid 刚好 30 点通过,成交后 TakeProfit 显示为 20 点——因为 Stops Level 30 减去点差 10,净保护空间被压缩。EURUSD 这类高杠杆品种,点差跳动会直接吃掉你预设的止损缓冲,实盘可能比回测更频繁撞墙。 挂单这边逻辑一致:止损买入单 Open 价 1.11019,SL 距 Open 26、27 点时同样吃「无效止损」,临界点就是 Stops Level 30。写 EA 时别硬编码 30,要用 SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL) 动态读,否则换品种就废。