EA 交易中的限制和验证(基础篇)
📘

EA 交易中的限制和验证(基础篇)

第 1/2 篇

「EA 跑起来前的边界与校验」

在 MT5 里挂 EA,先得弄清平台给自动化策略划的硬边界。最常被忽略的一条是:历史回测和实际tick成交不是一回事,用 99% 建模质量的回测跑顺的网格,实盘可能因点差跳空直接穿仓。外汇与贵金属杠杆高,这种断裂会放大亏损概率。 MQL5 里可以用 MQLInfoInteger 探明当前运行环境,再用 SymbolInfoInteger 校验品种交易权限。下面这段在 EA 初始化时拦掉不允许自动交易的品种,能少踩很多坑。 [CODE] int OnInit() { if(MQLInfoInteger(MQL_TRADE_ALLOWED)==false) { Print("自动交易被终端禁用"); return(INIT_FAILED); } if(SymbolInfoInteger(_Symbol,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) { Print("当前品种不支持完整交易"); return(INIT_FAILED); } return(INIT_SUCCEEDED); } [/CODE] 逐行看:MQLInfoInteger(MQL_TRADE_ALLOWED) 问终端「EA 能不能下单」,返回 false 就直接 INIT_FAILED 不初始化;SymbolInfoInteger(_Symbol,SYMBOL_TRADE_MODE) 取当前图表的交易模式,不等于 SYMBOL_TRADE_MODE_FULL 说明该品种限仓或只读,同样拦掉。 开 MT5 随便拖一个 EA 到 XAUUSD 图表,把终端「自动交易」钮关掉,就能看到第一行 Print 触发、EA 加载失败——这一步验证比任何收益曲线都实在。

MQL5 / C++
class="type">int OnInit()
  {
   if(MQLInfoInteger(MQL_TRADE_ALLOWED)==false)
      {
       Print("自动交易被终端禁用");
       class="kw">return(INIT_FAILED);
      }
   if(SymbolInfoInteger(_Symbol,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL)
      {
       Print("当前品种不支持完整交易");
       class="kw">return(INIT_FAILED);
      }
   class="kw">return(INIT_SUCCEEDED);
  }

写 EA 前先摸清这些硬约束

动手写自动交易算法时,很多人只盯着价格序列算信号,却忽略了一层更底层的限制信息。MT5 里的 EA 不是想开仓就开仓,交易时段、账户资产余额、品种成交量上限、订单总数上限,都会直接卡死你的下单请求。 实测中,若不在下单价前校验品种交易时段,EA 在休市边界触发信号会返回 ERR_TRADE_MARKET_CLOSED 错误;账户可用保证金不足时,OrderSend 同样会被拒。把这些前置检查写进代码,比事后处理失败重发更稳。 此外,还得算清入账价到止损位的潜在亏损、盯紧新 K 线是否生成。前者决定手数是否超出风险预算,后者避免同一根柱重复发单。外汇与贵金属杠杆高,忽略任一项限制都可能放大穿仓概率。

◍ 用 MT5 函数扒出品种的交易与报价时段

在 MT5 里想搞清楚一个品种哪天能报价、哪天能下单,不用去经纪商网页翻日程。交易时段直接调 SymbolInfoSessionTrade(),报价时段用 SymbolInfoSessionQuote(),两个函数签名一致:给定品种、星期枚举、时段索引(从 0 起),存在该时段就返回 true,起止时间通过第四、第五个 datetime 参数以引用方式写回。 要拉全某一天的所有时段,就在循环里不断把 session_index 加 1 调函数,直到返回 false 为止——这说明当天的时段已经穷尽。下面这段脚本主干就是这么干的,跑完能在专家日志里按「星期日~星期六」列出每个时段的编号和起止分钟。 星期几本身传的是 ENUM_DAY_OF_WEEK 枚举,想打印成中文得自己写个 DayToString() 做 switch 映射,否则日志里只有 SUNDAY、MONDAY 这类英文。外汇和贵金属品种在不同经纪商那儿的报价时段可能差几个小时,属于高杠杆高风险品类,跑出来后建议和平台官方时间对照确认。 把脚本挂到 EURUSD 或 XAUUSD 上,OnStart 会先打报价时段再打交易时段;若发现周五收盘时段 finish 减 1 秒才显示,是因为 MT5 把结束时刻记为下一秒起点,这是源码里的惯用法而非数据错误。

MQL5 / C++
class=class="str">"cmt">//--- 检查是否有编号为session_index的报价时段
class="type">bool session_exist=SymbolInfoSessionQuote(symbol,day,session_index,start,finish);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  显示报价时段信息                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrintInfoForQuoteSessions(class="type">class="kw">string symbol,ENUM_DAY_OF_WEEK day)
  {
class=class="str">"cmt">//--- 时段起始和终止时间
   class="type">class="kw">datetime start,finish;
   class="type">uint session_index=class="num">0;
   class="type">bool session_exist=true;
class=class="str">"cmt">//--- go over all sessions of this day
   while(session_exist)
     {
      class=class="str">"cmt">//--- 检查是否存在编号为session_index的报价时段
      session_exist=SymbolInfoSessionQuote(symbol,day,session_index,start,finish);
      class=class="str">"cmt">//--- 如果有这样的时段
      if(session_exist)
        {
         class=class="str">"cmt">//--- 显示它的星期,时段编号以及起始和终止时间
         Print(DayToString(day),": 时段编号=",session_index,"  起始时间=",
               TimeToString(start,TIME_MINUTES),"    终止时间=",TimeToString(finish-class="num">1,TIME_MINUTES|TIME_SECONDS));
        }
      class=class="str">"cmt">//--- 增加时段计数器
      session_index++;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得星期几的字符串表现形式                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string DayToString(ENUM_DAY_OF_WEEK day)
  {
   class="kw">switch(day)
     {
      case SUNDAY:    class="kw">return "星期日";
      case MONDAY:    class="kw">return "星期一";
      case TUESDAY:   class="kw">return "星期二";
      case WEDNESDAY: class="kw">return "星期三";
      case THURSDAY:  class="kw">return "星期四";
      case FRIDAY:    class="kw">return "星期五";
      case SATURDAY:  class="kw">return "星期六";
      class="kw">default:        class="kw">return "未知的日子";
     }
   class="kw">return "";
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//--- 保存星期数据的数组
   ENUM_DAY_OF_WEEK days[]={SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY};
   class="type">int size=ArraySize(days);
class=class="str">"cmt">//---
   Print("报价时段");
class=class="str">"cmt">//--- 遍历一星期中的每一天
   for(class="type">int d=class="num">0;d<size;d++)
     {
      PrintInfoForQuoteSessions(Symbol(),days[d]);
     }
class=class="str">"cmt">//---
   Print("交易时段");
class=class="str">"cmt">//--- 遍历一星期的每一天
   for(class="type">int d=class="num">0;d<size;d++)
     {
      PrintInfoForTradeSessions(Symbol(),days[d]);
     }
  }

「用 OrderCalcMargin 摸清开仓预付款」

想提前知道某一笔开仓或加仓要冻住多少保证金,直接调 OrderCalcMargin() 最实在。第一个参数吃 ENUM_ORDER_TYPE 枚举:买用 ORDER_TYPE_BUY,卖用 ORDER_TYPE_SELL,返回的数值随手数和入场价浮动,市价单和挂单都能算。 挂单的预付款在很多交易系统里也不是零成本。函数内部通常拿 SYMBOL_MARGIN_LONG / SHORT 做底,再乘 SYMBOL_MARGIN_LIMIT、SYMBOL_MARGIN_STOP、SYMBOL_MARGIN_STOPLIMIT 这几个系数。外汇品种上,这几个挂单系数常见为 0,也就是 broker 不收挂单预付款;但碰上非常规品种,系数可能非零,EA 的下单逻辑就得把这条算进去。 下面这段从脚本里摘的逻辑,能在 MT5 里直接跑出来看真实数字。先取最新 tick,再算指定类型的预付款,最后把五类订单的收取率打印一遍——肉眼核对比看文档快。 外汇和贵金属杠杆高,同一手数在不同品种上的预付款可能差出几倍,跑完代码请以自己账户币种和品种参数为准,勿凭经验值硬套。

MQL5 / C++
<span class="keyword">class="type">void</span> <span class="functions">OnStart</span>()
  {
<span class="comment">class=class="str">"cmt">//--- 取得预付款数值的变量</span>
   <span class="keyword">class="type">class="kw">double</span> margin;
<span class="comment">class=class="str">"cmt">//--- 取得最后一个订单预付款数值的变量</span>
   <span>class="type">MqlTick</span> last_tick;
<span class="comment">class=class="str">"cmt">//--- 尝试读取最后一笔订单的数值</span>
   <span class="keyword">if</span>(<span class="functions">SymbolInfoTick</span>(<span class="functions">Symbol</span>(),last_tick))
     {
      <span class="comment">class=class="str">"cmt">//--- 重设最后的错误代码</span>
      <span class="functions">ResetLastError</span>();
      <span class="comment">class=class="str">"cmt">//--- 计算预付款数值</span>
      <span class="keyword">class="type">bool</span> check=OrderCalcMargin(type,<span class="functions">Symbol</span>(),lots,last_tick.ask,margin);
      <span class="keyword">if</span>(check)
        {
         <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"交易操作 %s  %s %.2f lot at %G 需要的预付款等于 %.2f %s"</span>,OrderTypeToString(type),
                     <span class="functions">Symbol</span>(),lots,last_tick.ask,margin,<span class="functions">AccountInfoString</span>(<span class="keyword">ACCOUNT_CURRENCY</span>));
        }
     }
   <span class="keyword">else</span>
     {
      <span class="functions">Print</span>(<span class="class="type">class="kw">string">"SymbolInfoTick() 函数执行失败, 错误 "</span>,<span class="functions">GetLastError</span>());
     }
  }
<span class="comment">class=class="str">"cmt">//--- 计算不同类型订单的预付款率</span>
   <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"即时买入订单所需要的预付款率等于 %G"</span>,<span class="functions">SymbolInfoDouble</span>(<span class="functions">Symbol</span>(),SYMBOL_MARGIN_LONG));
   <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"即时卖出订单所需要的预付款率等于 %G"</span>,<span class="functions">SymbolInfoDouble</span>(<span class="functions">Symbol</span>(),SYMBOL_MARGIN_SHORT));
   <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"限价订单所需要的预付款率等于 %G"</span>,<span class="functions">SymbolInfoDouble</span>(<span class="functions">Symbol</span>(),SYMBOL_MARGIN_LIMIT));
   <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"止损订单所需要的预付款率等于 %G"</span>,<span class="functions">SymbolInfoDouble</span>(<span class="functions">Symbol</span>(),SYMBOL_MARGIN_STOP));
   <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"止损限价订单所需要的预付款率等于 %G"</span>,<span class="functions">SymbolInfoDouble</span>(<span class="functions">Symbol</span>(),SYMBOL_MARGIN_STOPLIMIT));

常见问题

先用 SymbolInfoSessionTrade 和 SymbolInfoSessionQuote 扒出该品种的交易与报价时段,避开休市时段挂单无效的情况。
调用 OrderCalcMargin 按品种、方向、手数先算预付款,确认账户净值够覆盖再发单。
小布可读取品种交易时段与预付款规则,在你加载策略前提示边界冲突,省去手动扒函数的麻烦。
大概率是品种报价时段未开放,用 SymbolInfoSessionQuote 核对具体时点,别在静默段塞单。
用 OrderCalcMargin 分别传 SYMBOL_TRADE_CALC_MODE 对应品种与手数,返回数值就是实时预估冻结额。