EA 交易中的限制和验证·进阶篇
📘

EA 交易中的限制和验证·进阶篇

第 2/2 篇

用 OrderCalcProfit 提前看清一手的盈亏边界

挂了止损就得假定它会被打掉,否则风控只是心理安慰。MT5 里 OrderCalcProfit() 就是干这个的:它和 OrderCalcMargin() 长得很像,但必须同时给开盘价和收盘价,才能反推出那笔平仓的浮盈或浮亏。 第一个参数只认 ORDER_TYPE_BUY 或 ORDER_TYPE_SELL,传别的订单类型直接报错;最后一个参数用引用传一个变量,函数算成了就把盈亏写进去。下面这段是算一手买入持仓、从 entry 到 exit 的盈亏示例,复制进 MT5 就能跑。 [CODE] //+------------------------------------------------------------------+

//计算买入一手的潜在利润/亏损

//+------------------------------------------------------------------+ double CalculateProfitOneLot(double entry_price,double exit_price) { //--- 取得利润值的变量 double profit=0; if(!OrderCalcProfit(ORDER_TYPE_BUY,Symbol(),1.0,entry_price,exit_price,profit)) { Print(__FUNCTION__," 计算 OrderCalcProfit() 失败. 错误 ",GetLastError()); } //--- return(profit); } [/CODE] 逐行拆一下:函数收两个价,内部先建 profit 变量清零;调 OrderCalcProfit 买一手当前品种,成交量写死 1.0,失败就打印错误码;正常则返回写好的 profit。外汇和贵金属杠杆高,同一点位波动下盈亏可能被放大,实际数值请以你账户品种和杠杆在 MT5 里跑出来的为准。 把 CalculateProfit_EA.mq5 里的 EA 加载到 XAUUSD 图表,改一下 entry 和 exit,就能在盘前看到这一手如果被打损会吞掉多少净值,而不是等成交了才心算。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算买入一手的潜在利润/亏损                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateProfitOneLot(class="type">class="kw">double entry_price,class="type">class="kw">double exit_price)
  {
class=class="str">"cmt">//--- 取得利润值的变量
   class="type">class="kw">double profit=class="num">0;
   if(!OrderCalcProfit(ORDER_TYPE_BUY,Symbol(),class="num">1.0,entry_price,exit_price,profit))
     {
      Print(__FUNCTION__,"  计算 OrderCalcProfit() 失败. 错误 ",GetLastError());
     }
class=class="str">"cmt">//---
   class="kw">return(profit);
  }

◍ 用 isNewBar 锁住每根新柱只触发一次

很多 EA 的逻辑默认只在每根 K 线开盘时算一次信号,并在该根内只下单一次。MT5 策略测试器里的“仅开盘价”模式正好适配这种写法:该模式下指标计算和 OnTick() 每根柱只跑一次,是所有回测模式里最快、对小幅杂波最不敏感的方案。不过测试器替你保证了“每柱一次”,实盘或模拟直播时这层保护没了,得自己盯住未成形柱的开启时间。 最轻量的做法是拿 SeriesInfoInteger() 取当前品种、周期下 SERIES_LASTBAR_DATE 属性,也就是最后一根柱的开启时间,再和静态变量里存的上次时间比。不一致就说明新柱来了。下面这段自定义函数直接可用: //+------------------------------------------------------------------+

//如果在交易品种/周期中出现新柱

//+------------------------------------------------------------------+ bool isNewBar() { //--- 在静态变量中记录上一个柱的开启时间 static datetime last_time=0; //--- 当前时间 datetime lastbar_time=SeriesInfoInteger(Symbol(),Period(),SERIES_LASTBAR_DATE); //--- 如果是第一次调用此函数 if(last_time==0) { //--- 设置时间并退出 last_time=lastbar_time; return(false); } //--- 如果时间不同 if(last_time!=lastbar_time) { //--- 记录时间并返回true last_time=lastbar_time; return(true); } //--- 如果我们运行到这一行,说明柱不是新的,返回false return(false); } 逐行拆一下:static datetime last_time=0 在 EA 生命周期内只初始化一次,专门存“上根柱开门时间”;SeriesInfoInteger 取的是最后一根柱的开启时间,M1 上就是最近那根 1 分钟 K 的开盘时刻。首次调用 last_time 为 0,直接记下时间并返回 false,避免起步误判。之后每次 tick 进来比一次,时间变了就是新柱,更新并返回 true,否则 false。 附带样本 CheckLastBar.mq5 里,M1 周期每出现新柱就打印一条消息,你可以直接挂模拟盘看它是否严格每分钟吐一条。外汇与贵金属杠杆高、滑点跳空频繁,实盘里若依赖此函数做信号闸门,建议先跑几天模拟确认 tick 流不会因休市或重连造成时间回退误触。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果在交易品种/周期中出现新柱                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool isNewBar()
  {
class=class="str">"cmt">//--- 在静态变量中记录上一个柱的开启时间
   class="kw">static class="type">class="kw">datetime last_time=class="num">0;
class=class="str">"cmt">//--- 当前时间
   class="type">class="kw">datetime lastbar_time=SeriesInfoInteger(Symbol(),Period(),SERIES_LASTBAR_DATE);
class=class="str">"cmt">//--- 如果是第一次调用此函数
   if(last_time==class="num">0)
     {
      class=class="str">"cmt">//--- 设置时间并退出
      last_time=lastbar_time;
      class="kw">return(false);
     }
class=class="str">"cmt">//--- 如果时间不同
   if(last_time!=lastbar_time)
     {
      class=class="str">"cmt">//--- 记录时间并返回true
      last_time=lastbar_time;
      class="kw">return(true);
     }
class=class="str">"cmt">//--- 如果我们运行到这一行,说明柱不是新的,返回false
   class="kw">return(false);
  }

「给挂单数量加一道闸」

想在 MT5 账户里压住同时挂着的待成交单数量,不必依赖券商默认逻辑,自己写个判断函数更直接。下面这个函数核心就一件事:先问账户允许的最大挂单数,再比当前活单量,不够名额才放行。 函数里用 AccountInfoInteger(ACCOUNT_LIMIT_ORDERS) 拿上限,若返回 0 代表经纪商不限制,直接 true;否则拿 OrdersTotal() 比大小。注意这版只管账户级总数,没算具体品种的开仓加挂单上限,实盘跑黄金或外汇前得自己补这段,否则可能触到品种规则被拒单。 外汇和贵金属杠杆高、滑点跳空频繁,挂单堆太多在重大数据前容易被一锅端,限制数量是风控而非多余动作。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查是否允许新开订单                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsNewOrderAllowed()
  {
class=class="str">"cmt">//--- 得到一个账户允许的最大挂单数
   class="type">int max_allowed_orders=(class="type">int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);
class=class="str">"cmt">//--- 如果没有限制,返回true,您可以发送一个订单
   if(max_allowed_orders==class="num">0) class="kw">return(true);
class=class="str">"cmt">//--- 如果我们运行到这一行,说明有限制,检测已经有多少个活动订单
   class="type">int orders=OrdersTotal();
class=class="str">"cmt">//--- 返回比较结果
   class="kw">return(orders<max_allowed_orders);
  }

查持仓量前先过品种限额这道关

想拿到某个交易品种当前持仓的手数,得先调 PositionSelect() 把该品种持仓置为“当前选中”,否则 PositionGetDouble(POSITION_VOLUME) 读出来的东西不可信。上面那段 PositionVolume() 就是干这个的:选中成功返回双精度手数,失败返回 -1 并打印错误码,方便你在 EA 日志里抓异常。 下发挂单之前,必须核对 SYMBOL_VOLUME_LIMIT——这是交易所或经纪商给单一品种的开仓+挂单总量硬上限。如果该字段返回 0,说明没总量限制,此时挂单量只需不超 SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX) 即可。但光看这个会漏掉已挂未成交的部分,所以得自己算净可开仓。 NewOrderAllowedVolume() 的逻辑很直接:用限额减去已开持仓再减同品种挂单量(PendingsVolume 需自行实现),结果若大于单笔上限就截断为 SYMBOL_VOLUME_MAX。外汇和贵金属杠杆高、跳空频繁,真把这些限额算错,可能在重大数据行情里直接被拒单或强平,实盘前务必在 MT5 策略测试器里用真实品种属性跑一遍。 文末附的 Check_Order_And_Volume_Limits.mq5 曾在 2010 自动交易锦标赛某参赛账户做过点对点验证,你可以直接拖进 MT5 看它怎么拦截超量请求。

MQL5 / C++
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 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">//--- 如果没有仓位则返回0
      class="kw">return(class="num">0);
     }
  }
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);
      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);
  }

◍ 交易量边界别靠拍脑袋

EA 下单前若不校验手数,MT5 会直接拒单或滑到异常仓位。核心不是资金风控,而是让委托量贴合品种本身的交易属性:最小手数、最大手数、最小步长,三者都由 SymbolInfoDouble 实时取,不是写死在代码里的常数。 常见坑是用了 0.13 手这类数值,而步长是 0.01 还好,但若步长为 0.02,0.13 就不在网格上,服务器会按就近规则处理,结果可能偏离策略意图。外汇与贵金属杠杆高,错手数可能瞬间放大敞口,务必在发单前拦一道。 下面这个函数把三道边界一次扫完,返回 false 时把原因写进 description,方便脚本直接打印: 别把 Symbol 属性当常量 很多老代码把最小手数硬编成 0.01,遇到黄金 XAUUSD 在部分经纪商最小 0.1 就崩。用 SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) 现取现用,切换品种不踩雷。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  检查订单量的正确性                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckVolumeValue(class="type">class="kw">double volume,class="type">class="kw">string &description)
  {
class=class="str">"cmt">//--- 交易操作中允许的最小订单量
   class="type">class="kw">double min_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   if(volume<min_volume)
     {
      description=StringFormat("交易量小于允许的最小值 SYMBOL_VOLUME_MIN=%.2f",min_volume);
      class="kw">return(false);
     }
class=class="str">"cmt">//--- 交易操作中允许的最大订单量
   class="type">class="kw">double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX);
   if(volume>max_volume)
     {
      description=StringFormat("交易量大于允许的最大值 SYMBOL_VOLUME_MAX=%.2f",max_volume);
      class="kw">return(false);
     }
class=class="str">"cmt">//--- 取得交易量允许的最小改变步长
   class="type">class="kw">double volume_step=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP);
   class="type">int ratio=(class="type">int)MathRound(volume/volume_step);
   if(MathAbs(ratio*volume_step-volume)>class="num">0.0000001)
     {
      description=StringFormat("交易量不是最小步长的倍数 SYMBOL_VOLUME_STEP=%.2f, 最接近的正确交易量是 %.2f",
                               volume_step,ratio*volume_step);
      class="kw">return(false);
     }
   description="正确的交易量 ";
   class="kw">return(true);
  }

「最后说句实在话」

这一节把 EA 在账户上跑之前要做的几类校验摊开了讲:资金够不够、手数是不是 SYMBOL_VOLUME_STEP 的整数倍、当前品种有没有超过 SYMBOL_VOLUME_LIMIT。示例里的六个 mq5 文件(checkvolumevalue 2.96 KB、symbolinfosession 4.05 KB 等)直接拖进 MT5 就能编译,初学者改两行参数就能套进自己的策略。 下面这段从原文评论区抠出来的限额计算函数,已经把挂单占用的额度逻辑简化掉,只算净可开仓。注意 max_volume 返回 0 时(MT5 2715 版有用户反馈过),说明该品种服务端没下发总限额,函数会直接 return(0),这时候别硬发单。 外汇和贵金属杠杆高、跳空频繁,所有校验都只是「降低报错概率」,不保证成交。把代码跑一遍,比你信任何教程都实在。

MQL5 / C++
class="type">class="kw">double orders_volume_on_symbol=PendingsVolume(symbol);
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=class="str">"cmt">//class="type">class="kw">double orders_volume_on_symbol=PendingsVolume(symbol);
      class=class="str">"cmt">//allowed_volume=max_volume-opened_volume-orders_volume_on_symbol;
      allowed_volume=max_volume-opened_volume;
      if(allowed_volume>symbol_max_volume) allowed_volume=symbol_max_volume;
     }
   class="kw">return(allowed_volume);
  }

常见问题

用 OrderCalcProfit 按当前品种、方向、手数和开平仓价预演,返回值为正即盈利边界,负值即亏损边界,不用等成交。
没做新柱判定。用 isNewBar 逻辑锁住每根新柱只跑一次下单判断,避免一根柱子内被 tick 反复触发。
可以。小布能按品种限额和自定义闸值做实时诊断,挂单堆太多或持仓超量时直接提示你,不用自己写校验代码。
是手数超出品种最小/最大交易限制。下单前先读品种规范里的 volume min/max/step,别靠经验拍脑袋填固定手数。
因为不同平台对黄金原油的单边持仓上限不同,先取限额再比对当前持仓,否则容易在下单接口被拒还查不出原因。