交易机器人在市场发布前必须经过的检验

MetaQuotes | 6 九月, 2016


为什么产品在市场发布之前需要检验

任何产品在市场发布之前,它必须通过强制的预先检验,因为EA交易或者指标中的小小错误都可能导致交易账户的损失。这就是为什么我们开发了一系列基本的检验,以确保市场产品达到了所需的质量水平。

如果市场的管理人员在检验您产品的过程中发现了任何错误,您都将必须把它们全部修改好。本文家恶少了开发者们在他们的技术指标和交易机器人中最常犯下的错误,我们还推荐您阅读以下文章:


怎样快速捕捉和修复交易机器人中的错误

平台中集成的策略测试器不仅允许回测交易系统,而且可以用于发现交易机器人开发过程中的逻辑和算法错误,在测试中,所有有关交易操作的消息以及发现的错误都输出在测试器的日志(Journal)中。使用特别的记录阅读器就可以很方便地分析这些消息, 它可以使用上下文菜单的命令调用出来。


在EA交易的测试之后,打开阅读器并启用"只显示错误(Error only)"模式,如上图所示。如果您的交易机器人包含错误,您就能立即看到它们。如果第一次没有侦测到错误,可以在不同的交易品种/时段/输入参数以及不同数量的初始存款情况下进行一系列的测试。使用这些简单的技巧可以发现 99% 的错误,并且我们会在本文中讨论它们。

我们可以使用在 MetaEditor 中的在历史数据上做调试的功能来仔细研究发现的错误,通过这个方法,我们可以使用可视化测试模式,不仅监控价格图表和使用的指标,也能跟踪程序在每个时刻的变量数值。这样,您就能够调试您的交易策略了,而不必在实时模式下花费很长时间。

资金不足以进行交易操作

在发送每个交易订单之前,需要检查账户是否有足够的资金,缺乏资金以进行未来的建仓或者订单会被认为是疏忽大意的。

请一定要记住就算设置一个挂单也可能会要求担保 — 保证金


我们推荐特意使用很小的初始存款来测试交易机器人,例如,1美元或者1欧元。

如果检查显示,资金不足以进行交易操作,就有必要在记录中输出一个错误消息而不是调用 OrderSend() 函数。检验实例:

MQL5

bool CheckMoneyForTrade(string symb,double lots,ENUM_ORDER_TYPE type)
  {
//--- 取得建仓价格
   MqlTick mqltick;
   SymbolInfoTick(symb,mqltick);
   double price=mqltick.ask;
   if(type==ORDER_TYPE_SELL)
      price=mqltick.bid;
//--- 所需以及可用保证金的数值
   double margin,free_margin=AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   //--- 调用检验函数
   if(!OrderCalcMargin(type,symb,lots,price,margin))
     {
      //--- 出错了,发送报告并返回 false
      Print("有错误出现在 ",__FUNCTION__," 编号=",GetLastError());
      return(false);
     }
   //--- 如果资金不够进行操作
   if(margin>free_margin)
     {
      //--- 报告错误并返回 false
      Print("资金不足以进行 ",EnumToString(type)," ",lots," ",symb," 错误编号=",GetLastError());
      return(false);
     }
//--- 检验成功
   return(true);
  }

MQL4

bool CheckMoneyForTrade(string symb, double lots,int type)
  {
   double free_margin=AccountFreeMarginCheck(symb,type, lots);
   //-- 如果资金不够
   if(free_margin<0)
     {
      string oper=(type==OP_BUY)?"买入":"卖出";
      Print("资金不足以进行", oper," ",lots, " ", symb, " 错误编号",GetLastError());
      return(false);
     }
   //--- 检验成功
   return(true);
  }


交易操作中的无效交易量

在发送交易订单之前,也有必要检查在订单中指定的交易量的正确性,EA交易中订单设置的手数必须在调用 OrderSend() 函数之前进行检查,交易品种所允许的最小和最大交易量,以及交易量之间的步长是在规格说明中指定的。在 MQL5 中, 这些数值可以通过ENUM_SYMBOL_INFO_DOUBLE 枚举,在SymbolInfoDouble()函数的帮助下获得。

SYMBOL_VOLUME_MIN

交易的最小交易量

SYMBOL_VOLUME_MAX

交易的最大交易量

SYMBOL_VOLUME_STEP

执行交易时最小的交易量变化步长

检查交易量正确性函数的实例

//+------------------------------------------------------------------+
//| 检查订单交易量的正确性                                               |
//+------------------------------------------------------------------+
bool CheckVolumeValue(double volume,string &description)
  {
//--- 交易操作允许的最小交易量
   double min_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   if(volume<min_volume)
     {
      description=StringFormat("交易量小于允许的最小交易量,SYMBOL_VOLUME_MIN=%.2f",min_volume);
      return(false);
     }

//--- 交易操作允许的最大交易量 
   double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX);
   if(volume>max_volume)
     {
      description=StringFormat("交易量大于允许的最大交易量,SYMBOL_VOLUME_MAX=%.2f",max_volume);
      return(false);
     }

//--- 取得交易量变化的最小步长
   double volume_step=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP);

   int ratio=(int)MathRound(volume/volume_step);
   if(MathAbs(ratio*volume_step-volume)>0.0000001)
     {
      description=StringFormat("交易量不是最小交易步长的整数倍,SYMBOL_VOLUME_STEP=%.2f, 最接近的正确交易量是 %.2f",
                               volume_step,ratio*volume_step);
      return(false);
     }
   description="正确的交易量数值";
   return(true);
  }


挂单的限制数量

账户中允许同时设置的活动挂单的数量可能也会有所限制。IsNewOrderAllowed() 函数的实例, 它用来检查是否还允许继续设置挂单。

//+------------------------------------------------------------------+
//| 检查是否还允许设置订单                                               |
//+------------------------------------------------------------------+
bool IsNewOrderAllowed()
  {
//--- 取得账户中允许设置的挂单数量
   int max_allowed_orders=(int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS);

//--- 如果没有限制,返回 true; 您可以发送一个订单
   if(max_allowed_orders==0) return(true);

//--- 如果我们达到这一行,说明有限制; 找出已经设置了多少挂单
   int orders=OrdersTotal();

//--- 返回比较结果
   return(orders<max_allowed_orders);
  }


这个函数很简单: 取得允许的最大订单数并赋予max_allowed_orders变量; 如果它不等于0,把它与当前订单数量做比较。但是,这个函数没有考虑到另外的可能的限制 - 对某一特定交易品种开启仓位总交易量的限制和挂单数量的限制。


某特定交易品种的手数限制

为了取得某一特定交易品种的开启仓位的总交易量,首先您需要使用PositionSelect()函数来选择一个仓位,之后您就可以使用PositionGetDouble()来得到已建仓位的交易量, 它可以返回双精度类型的所选仓位的各种属性。让我们写一个 PostionVolume() 函数来取得所需交易品种的仓位交易量。

//+------------------------------------------------------------------+
//| 返回指定交易品种的仓位大小                                            |
//+------------------------------------------------------------------+
double PositionVolume(string symbol)
  {
//--- 尝试根据交易品种选择仓位
   bool selected=PositionSelect(symbol);
//--- 有仓位
   if(selected)
      //--- 返回仓位交易量
      return(PositionGetDouble(POSITION_VOLUME));
   else
     {
      //--- 选择仓位出错报告
      Print(__FUNCTION__," 执行 PositionSelect() 失败,交易品种为 ",
            symbol," 错误编号 ",GetLastError());
      return(-1);
     }
  }

对于支持对冲的账户,还需要迭代当前交易品种所有的仓位。

在根据交易品种生成交易请求以设置挂之前, 您应该检查在一个交易品种上的已开启仓位和挂单总交易量的限制 - SYMBOL_VOLUME_LIMIT,如果没有限制,那么挂单的总交易量不能超过使用SymbolInfoDouble()得到的最大交易量。

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);
      allowed_volume=max_volume-opened_volume-orders_volume_on_symbol;
      if(allowed_volume>symbol_max_volume) allowed_volume=symbol_max_volume;
     }
   return(allowed_volume);
  }


在 SYMBOL_TRADE_STOPS_LEVEL 最小水平之内设置获利(TakeProfit)和止损(StopLoss)水平

许多EA交易在进行买入或者卖出时动态计算获利和止损水平,订单的止损是在价格走向期望的方向时用于关闭仓位的,而止损是当价格走向不希望的方向时用于限制损失的。

因而,获利和止损水平应该与当前的价格相比较而进行反向的操作:

  • 买入是根据卖方报价(Ask)的 — 而止损和获利水平应该和买方报价(Bid)做比较。
  • 卖出是根据买方报价(Bid)的 - 获利和止损水平应该和卖方报价(Ask)做比较。
买入是根据卖方报价的
卖出是根据买方报价的
TakeProfit >= Bid
StopLoss <= Bid
TakeProfit <= Ask
StopLoss >= Ask



金融资产在交易品种设置中可能有SYMBOL_TRADE_STOPS_LEVEL参数,它决定了止损和获利水平距离当前已建仓位平仓价格的最小距离点数,如果这个属性的值是0,就没有设定止损/获利单距离买入和卖出价格的最小距离。

一般来说,检查获利和止损水平,看是否符合账户设置的最小距离SYMBOL_TRADE_STOPS_LEVEL的方法如下所示:

  • 买入是根据卖方报价的 — 获利和止损水平必须距离买方报价至少 SYMBOL_TRADE_STOPS_LEVEL 个点
  • 卖出是根据买方报价的 — 获利和止损水平必须距离卖方报价至少 SYMBOL_TRADE_STOPS_LEVEL 个点
买入是根据卖方报价的
卖出是根据买方报价的
TakeProfit - Bid >= SYMBOL_TRADE_STOPS_LEVEL
Bid - StopLoss >= SYMBOL_TRADE_STOPS_LEVEL
Ask - TakeProfit >= SYMBOL_TRADE_STOPS_LEVEL
StopLoss - Ask >= SYMBOL_TRADE_STOPS_LEVEL

这样,我们就能创建一个CheckStopLoss_Takeprofit()检验函数,它要求获利和止损水平距离收盘价至少 SYMBOL_TRADE_STOPS_LEVEL 个点:

bool CheckStopLoss_Takeprofit(ENUM_ORDER_TYPE type,double SL,double TP)
  {
//--- 取得 SYMBOL_TRADE_STOPS_LEVEL 水平
   int stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   if(stops_level!=0)
     {
      PrintFormat("SYMBOL_TRADE_STOPS_LEVEL=%d: 止损和获利必须"+
                  " 距离当前价格大于 %d 个点",stops_level,stops_level);
     }
//---
   bool SL_check=false,TP_check=false;
//--- 只检查两种订单类型
   switch(type)
     {
      //--- 买入操作
      case  ORDER_TYPE_BUY:
        {
         //--- 检验止损
         SL_check=(Bid-SL>stops_level*_Point);
         if(!SL_check)
            PrintFormat("对于订单 %s 止损=%.5f 必须小于 %.5f"+
                        " (Bid=%.5f - SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                        EnumToString(type),SL,Bid-stops_level*_Point,Bid,stops_level);
         //--- 检验获利
         TP_check=(TP-Bid>stops_level*_Point);
         if(!TP_check)
            PrintFormat("对于 %s 获利=%.5f 必须大于 %.5f"+
                        " (Bid=%.5f + SYMBOL_TRADE_STOPS_LEVEL=%d points)",
                        EnumToString(type),TP,Bid+stops_level*_Point,Bid,stops_level);
         //--- 返回检验结果
         return(SL_check&&TP_check);
        }
      //--- 卖出操作
      case  ORDER_TYPE_SELL:
        {
         //--- 检验止损
         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);
        }
      break;
     }
//--- 对于挂单有少许不同
   return false;
  }

检验本身如下所示:

//+----------------------------------------+
//| 脚本程序起始函数                          |
//+----------------------------------------+
void OnStart()
  {
//--- 随机取得操作类型
   int oper=(int)(GetTickCount()%2); // 被2除永远余0或者1
   switch(oper)
     {
      //--- buy
      case  0:
        {
         //--- 取得建仓价格并且故意设置无效的获利/止损/
         double price=Ask;
         double SL=NormalizeDouble(Bid+2*_Point,_Digits);
         double TP=NormalizeDouble(Bid-2*_Point,_Digits);
         //--- 进行检查
         PrintFormat("Buy at %.5f   SL=%.5f   TP=%.5f  Bid=%.5f",price,SL,TP,Bid);
         if(!CheckStopLoss_Takeprofit(ORDER_TYPE_BUY,SL,TP))
            Print("止损或者获利水平是不对的!");
         //--- 还是尝试买入,是为了看执行结果
         Buy(price,SL,TP);
        }
      break;
      //--- sell
      case  1:
        {
         //--- 取得建仓价格并且故意设置无效的获利/止损/
         double price=Bid;
         double SL=NormalizeDouble(Ask-2*_Point,_Digits);
         double TP=NormalizeDouble(Ask+2*_Point,_Digits);
         //--- 进行检查
         PrintFormat("Sell at %.5f   SL=%.5f   TP=%.5f  Ask=%.5f",price,SL,TP,Ask);
         if(!CheckStopLoss_Takeprofit(ORDER_TYPE_SELL,SL,TP))
            Print("止损或者获利水平是不对的!");
         //--- try to sell anyway, in order to see the execution result
         Sell(price,SL,TP);
        }
      break;
      //---
     }
  }

例子函数可以在附件中的脚本程序Check_TP_and_SL.mq4Check_TP_and_SL.mq5中找到。执行的实例:

MQL5
Check_TP_and_SL (EURUSD,H1) Buy at 1.11433   SL=1.11425   TP=1.11421  Bid=1.11423
Check_TP_and_SL (EURUSD,H1) SYMBOL_TRADE_STOPS_LEVEL=30: 止损和获利距离平仓价格不能少于30个点
Check_TP_and_SL (EURUSD,H1) For order ORDER_TYPE_BUY 止损=1.11425 必须小于 1.11393 (Bid=1.11423 - SYMBOL_TRADE_STOPS_LEVEL=30 个点)
Check_TP_and_SL (EURUSD,H1) For order ORDER_TYPE_BUY 获利=1.11421 必须大于 1.11453 (Bid=1.11423 + SYMBOL_TRADE_STOPS_LEVEL=30 个点)
Check_TP_and_SL (EURUSD,H1) 止损或者获利水平是不对的!
Check_TP_and_SL (EURUSD,H1) OrderSend 错误 4756
Check_TP_and_SL (EURUSD,H1) retcode=10016  deal=0  order=0
MQL4
Check_TP_and_SL EURUSD,H1:  卖出于1.11430   SL=1.11445   TP=1.11449  卖方报价=1.11447
Check_TP_and_SL EURUSD,H1:  SYMBOL_TRADE_STOPS_LEVEL=1: 止损和获利水平与收盘价的距离不能少于1个点
Check_TP_and_SL EURUSD,H1:  对于订单 ORDER_TYPE_SELL 止损=1.11445 必须大于 1.11448  (买方报价=1.11447 + SYMBOL_TRADE_STOPS_LEVEL=1 个点)
Check_TP_and_SL EURUSD,H1:  对于订单 ORDER_TYPE_SELL 获利=1.11449 必须小于 1.11446  (卖方报价=1.11447 - SYMBOL_TRADE_STOPS_LEVEL=1 个点)
Check_TP_and_SL EURUSD,H1:  止损或者获利水平是不对的!
Check_TP_and_SL EURUSD,H1:  OrderSend 错误 130 

为了模拟无效获利和止损值的情形,Test_Wrong_TakeProfit_LEVEL.mq5Test_Wrong_StopLoss_LEVEL.mq5 EA交易可以在本文的附件中找到。它们只能在模拟账户上运行。学习这些实例就可以看到可以成功买入时的条件。

Test_Wrong_StopLoss_LEVEL.mq5 例子EA交易的执行结果:

Test_Wrong_StopLoss_LEVEL.mq5
Point=0.00001 Digits=5
SYMBOL_TRADE_EXECUTION=SYMBOL_TRADE_EXECUTION_INSTANT
SYMBOL_TRADE_FREEZE_LEVEL=20: 如果距离激活的价格只有20个点,不允许修改订单或者仓位
SYMBOL_TRADE_STOPS_LEVEL=30: 止损和获利距离当前价格不能少于30个点
1. 买入1.0 EURUSD 价格1.11442 SL=1.11404 Bid=1.11430 ( StopLoss-Bid=-26 个点 ))
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11442 sl: 1.11404 [无效止损]
2. 买入1.0 EURUSD 价格1.11442 SL=1.11404 Bid=1.11431 ( StopLoss-Bid=-27 个点 ))
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11442 sl: 1.11404 [无效止损]
3. 买入 1.0 EURUSD 价格 1.11442 SL=1.11402 Bid=1.11430 ( StopLoss-Bid=-28 个点 ))
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11442 sl: 1.11402 [无效止损]
4. 买入 1.0 EURUSD 价格 1.11440 SL=1.11399 Bid=1.11428 ( StopLoss-Bid=-29 个点 ))
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11440 sl: 1.11399 [无效止损]
5. Buy 1.0 EURUSD at 1.11439 SL=1.11398 Bid=1.11428 ( StopLoss-Bid=-30 points ))
Buy 1.0 EURUSD done at 1.11439 with StopLoss=41 points (spread=12 + SYMBOL_TRADE_STOPS_LEVEL=30)


Example of the Test_Wrong_TakeProfit_LEVEL.mq5 EA execution:

Test_Wrong_TakeProfit_LEVEL.mq5
Point=0.00001 Digits=5
SYMBOL_TRADE_EXECUTION=SYMBOL_TRADE_EXECUTION_INSTANT
SYMBOL_TRADE_FREEZE_LEVEL=20: 如果距离激活的价格只有20个点,不允许修改订单或者仓位
SYMBOL_TRADE_STOPS_LEVEL=30: 止损和获利距离当前价格不能少于30个点
1. 买入1.0 EURUSD 价格 1.11461 TP=1.11478 Bid=1.11452 (TakeProfit-Bid=26 个点)
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11461 tp: 1.11478 [无效止损]
2. 买入 1.0 EURUSD 价格 1.11461 TP=1.11479 Bid=1.11452 (TakeProfit-Bid=27个点)
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11461 tp: 1.11479 [无效止损]
3. 买入1.0 EURUSD 价格 1.11461 TP=1.11480 Bid=1.11452 (TakeProfit-Bid=28个点)
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11461 tp: 1.11480 [无效止损]
4. 买入1.0 EURUSD 价格1.11461 TP=1.11481 Bid=1.11452 (TakeProfit-Bid=29 个点)
CTrade::OrderSend: 立即买入 1.00 EURUSD 价格 1.11461 tp: 1.11481 [无效止损]
5. 买入1.0 EURUSD 价格1.11462 TP=1.11482 Bid=1.11452 (TakeProfit-Bid=30个点)
买入1.0 EURUSD 完成,价格1.11462,TakeProfit=20 个点 (SYMBOL_TRADE_STOPS_LEVEL=30 - 点差=10)

在挂单中检查止损和获利水平简单多了,因为这些水平必须基于订单的建仓价格,也就是说,考虑SYMBOL_TRADE_STOPS_LEVEL最小距离来检查水平的方法如下: 获利和止损水平必须距离订单的激活价格大于 SYMBOL_TRADE_STOPS_LEVEL 个点。

限价买入和止损买入
限价卖出和止损卖出
TakeProfit - Open >= SYMBOL_TRADE_STOPS_LEVEL
Open - StopLoss >= SYMBOL_TRADE_STOPS_LEVEL
Open - TakeProfit >= SYMBOL_TRADE_STOPS_LEVEL
StopLoss - Open >= SYMBOL_TRADE_STOPS_LEVEL

Test_StopLoss_Level_in_PendingOrders.mq5 EA 交易进行了一系列的止损买入和限价买入尝试,直到操作成功。在每次成功的尝试中,止损或者获利水平都在正确方向上偏移一个点。此EA交易执行的实例:

Test_StopLoss_Level_in_PendingOrders.mq5
SYMBOL_TRADE_EXECUTION=SYMBOL_TRADE_EXECUTION_INSTANT
SYMBOL_TRADE_FREEZE_LEVEL=20: 如果距离激活的价格只有20个点,不允许修改订单或者仓位
SYMBOL_TRADE_STOPS_LEVEL=30: 止损和获利距离最新价格不能少于30个点
1. 止损买入 1.0 EURUSD 价格 1.11019 SL=1.10993 (Open-StopLoss=26 个点)
CTrade::OrderSend: 止损买入 1.00 EURUSD 价格 1.11019 sl: 1.10993 [无效止损]
2. 止损买入1.0 EURUSD 价格 1.11019 SL=1.10992 (Open-StopLoss=27 个点)
CTrade::OrderSend: 止损买入 1.00 EURUSD 价格 1.11019 sl: 1.10992 [无效止损]
3. 止损买入 1.0 EURUSD 价格 1.11020 SL=1.10992 (Open-StopLoss=28 个点)
CTrade::OrderSend: 止损买入 1.00 EURUSD 价格 1.11020 sl: 1.10992 [无效止损]
4. 止损买入 1.0 EURUSD 价格 1.11021 SL=1.10992 (Open-StopLoss=29 个点)
CTrade::OrderSend: 止损买入 1.00 EURUSD 价格 1.11021 sl: 1.10992 [无效止损]
5. 止损买入 1.0 EURUSD 价格 1.11021 SL=1.10991 (Open-StopLoss=30 个点)
止损买入 1.0 EURUSD 完成 价格 1.11021 止损=1.10991 (SYMBOL_TRADE_STOPS_LEVEL=30)
 --------- 
1. 限价买入 1.0 EURUSD 价格 1.10621 TP=1.10647 (TakeProfit-Open=26 个点)
CTrade::OrderSend: 限价买入1.00 EURUSD 价格 1.10621 tp: 1.10647 [无效止损]
2. 限价买入1.0 EURUSD 价格 1.10621 TP=1.10648 (TakeProfit-Open=27 个点)
CTrade::OrderSend: 限价买入1.00 EURUSD 价格1.10621 tp: 1.10648 [无效止损]
3. 限价买入1.0 EURUSD 价格1.10621 TP=1.10649 (TakeProfit-Open=28 个点)
CTrade::OrderSend: 限价买入1.00 EURUSD 价格1.10621 tp: 1.10649 [无效止损]
4. 限价买入1.0 EURUSD 价格1.10619 TP=1.10648 (TakeProfit-Open=29个点)
CTrade::OrderSend: 限价买入1.00 EURUSD 价格 1.10619 tp: 1.10648 [无效止损]
5. 限价买入 1.0 EURUSD 价格1.10619 TP=1.10649 (TakeProfit-Open=30个点)
限价买入 1.0 EURUSD 完成 价格1.10619 TakeProfit=1.10649 (SYMBOL_TRADE_STOPS_LEVEL=30)


在挂单中检验获利和止损水平的实例可以在附件的源文件中找到: Check_TP_and_SL.mq4Check_TP_and_SL.mq5


尝试在 SYMBOL_TRADE_FREEZE_LEVEL 水平范围之内修改订单或者仓位

SYMBOL_TRADE_FREEZE_LEVEL参数可以在交易品种的规格中设定,它显示了冻结挂单操作水平与建仓价格之间的距离。例如,如果某金融资产的交易被重定向到一个外部交易系统,而限价买入挂单可能现在距离当前的卖家报价太近了,另外,如果此时有请求来修改订单,并且距离卖家报价很接近,就可能出现订单被执行而无法修改。

所以,交易品种的规格对挂单和已建仓位指定了冻结距离,在距离之内它们就不能修改。一般来说,在尝试发送修改请求之前,需要考虑到SYMBOL_TRADE_FREEZE_LEVEL而进行检查:

订单/仓位的类型
激活价格
检查
限价买入订单
 卖家报价(Ask)
Ask-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL
止损买入订单 卖家报价(Ask)OpenPrice-Ask >= SYMBOL_TRADE_FREEZE_LEVEL
限价卖出订单 买家报价(Bid)OpenPrice-Bid >= SYMBOL_TRADE_FREEZE_LEVEL
止损卖出订单 买家报价(Bid)Bid-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL
买入仓位
 买家报价(Bid)TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL
Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL
卖出仓位
 卖家报价(Ask)Ask-TakeProfit >= SYMBOL_TRADE_FREEZE_LEVEL
StopLoss-Ask >= SYMBOL_TRADE_FREEZE_LEVEL

用于检查订单和仓位 SYMBOL_TRADE_FREEZE_LEVEL 水平的完整函数实例可以在附件中的 Check_FreezeLevel.mq5Check_FreezeLevel.mq4 脚本程序中找到。

//--- 检查订单类型
   switch(type)
     {
      //--- 限价买入挂单
      case  ORDER_TYPE_BUY_LIMIT:
        {
         //--- 检查当前价格与激活价格的距离
         check=((Ask-price)>freeze_level*_Point);
         if(!check)
            PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                        EnumToString(type),ticket,(int)((Ask-price)/_Point),freeze_level);
         return(check);
        }
      //--- 限价买入挂单
      case  ORDER_TYPE_SELL_LIMIT:
        {
         //--- 检查当前价格与激活价格的距离
         check=((price-Bid)>freeze_level*_Point);
         if(!check)
            PrintFormat("订单 %s #%d 不能被修改: Open-Bid=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                        EnumToString(type),ticket,(int)((price-Bid)/_Point),freeze_level);
         return(check);
        }
      break;
      //--- BuyStop 挂单
      case  ORDER_TYPE_BUY_STOP:
        {
         //--- 检查当前价格与激活价格的距离
         check=((price-Ask)>freeze_level*_Point);
         if(!check)
            PrintFormat("订单 %s #%d 不能被修改: Ask-Open=%d 个点 < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                        EnumToString(type),ticket,(int)((price-Ask)/_Point),freeze_level);
         return(check);
        }
      //--- 止损卖出挂单
      case  ORDER_TYPE_SELL_STOP:
        {
         //--- 检查当前价格与激活价格的距离
         check=((Bid-price)>freeze_level*_Point);
         if(!check)
            PrintFormat("订单 %s #%d 不能被修改: Bid-Open=%d points < SYMBOL_TRADE_FREEZE_LEVEL=%d 个点",
                        EnumToString(type),ticket,(int)((Bid-price)/_Point),freeze_level);
         return(check);
        }
      break;
     }

您可以模拟一种情形,尝试在冻结水平之内修改挂单。为此,开一个模拟账户,其中金融资产的 SYMBOL_TRADE_FREEZE_LEVEL 水平不为0, 然后把Test_SYMBOL_TRADE_FREEZE_LEVEL.mq5 (Test_SYMBOL_TRADE_FREEZE_LEVEL.mq4) EA 附加到图表上并人工设置任意的挂单。该 EA 交易将自动把挂单移动到距离当前尽可能近的位置而将开始进行非法的修改尝试。它将会使用PlaySound()函数发出声音提醒。


当操作缺少历史报价的交易品种时发生错误

如果一个EA交易或者指标在图表上运行,而历史数据不够,则会出现两种可能:

  1. 程序检查所需的历史是否足够长,如果柱数少于所需,程序会请求缺少的数据,并在下一个订单时刻来临之前结束操作。这种方法是最正确的,它可以帮助避免很多错误,例如超出数组返回或者除零错误;
  2. 程序不做任何检查,立即开始工作,就如同所有所需的交易品种和时段信息在请求时已经可用了,这种方法是有问题的,可能会带来许多无法预料的错误。

您可以尝试自己模拟这种情形。为此,在图表上运行测试的指标或者EA交易,然后关闭终端并删除历史,再重新打开终端。如果这样重启之后记录中没有错误,就尝试在运行程序的图表上改变交易品种和时段,许多指标会在每周或者每月的时段中出错,因为这些时段常常柱数有限。另外,立即改变图表的交易品种(例如,从 EURUSD 改为 CADJPY), 图表上的指标或者EA交易可能会因为缺少所需计算的历史而出错。


超出数组范围

当操作数组时,对它们元素的访问是通过索引编号进行的,它不能是负的,并且必须小于数组的大小。数组的大小可以通过使用ArraySize() 函数得到。

这个错误可能会在操作动态数组时遇到,它的大小可以明确使用ArrayResize()函数来设置, 或者当作为传入参数而在函数中间接设置了它的大小时也可能遇到。例如, CopyTicks() 函数尝试把所需数量的订单保存到数组中,但是如果复制的订单少于请求的数量,那么结果数组的大小将比索期待的要小。

另一个遇到这个错误的方法是尝试读取还没有初始化过的指标缓冲区。提醒一下,指标缓冲区也是动态数组,并且它们的大小是在图表初始化后由终端的执行系统定义的。所以,在 OnInit() 函数中尝试访问这种缓冲区的数据时会引起"数组超出范围"错误的。


在指标中生成这样错误的实例在附件的 Test_Out_of_range.mq5 文件中可以找到。


除以零

另一个严重错误是尝试除以零。在这种情况下,程序的执行会立即终止,策略测试器会在日志中显示出错函数的名称和代码中的行号。


从原则上看,除以零的错误都是因为遇到了程序开发人员没有预料到的情况,例如,读取属性或者计算表达式时使用了"损坏的"数据。

除以零可以使用 TestZeroDivide.mq5 EA交易简单重现, 它的源代码显示在屏幕截图中。另一个严重错误是使用不正确的对象指针在历史数据上调试对于找到引发错误的原因是很有用的。


发送请求来修改水平而并没有真正改变它们

如果交易系统的规则需要修改挂单或者已经建立的仓位,那么在发送交易请求进行这样的操作之前,还需要确认请求的操作会真正改变订单或者仓位的参数。ually change parameters of the order or position. 发送了交易请求而不能做任何改变将会被视为错误,而交易服务器会回应TRADE_RETCODE_NO_CHANGES=10025 的返回值 (MQL5) 或者 ERR_NO_RESULT=1 的代号 (MQL4)。

在 MQL5 做检查的实例在Check_OrderLevels.mq5脚本程序中提供:

//--- 用于进行交易操作的类
#include <Trade\Trade.mqh>
CTrade trade;
#include <Trade\Trade.mqh>
//--- 用于操作订单的类
#include <Trade\OrderInfo.mqh>
COrderInfo orderinfo;
//--- 用于操作仓位的类
#include <Trade\PositionInfo.mqh>
CPositionInfo positioninfo;
//+------------------------------------------------------------------+
//| 在修改订单之前检查水平新的数值                                         |
//+------------------------------------------------------------------+
bool OrderModifyCheck(ulong ticket,double price,double sl,double tp)
  {
//--- 根据编号选择订单
   if(orderinfo.Select(ticket))
     {
      //--- 用于设置挂单的交易品种的点位大小和名称
      string symbol=orderinfo.Symbol();
      double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      int digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      //--- 检查建仓价格有没有改变
      bool PriceOpenChanged=(MathAbs(orderinfo.PriceOpen()-price)>point);
      //--- 检查止损水平有没有改变
      bool StopLossChanged=(MathAbs(orderinfo.StopLoss()-sl)>point);
      //--- 检查获利水平有没有改变
      bool TakeProfitChanged=(MathAbs(orderinfo.TakeProfit()-tp)>point);
      //--- 如果水平有任何变化
      if(PriceOpenChanged || StopLossChanged || TakeProfitChanged)
         return(true);  // 订单可以修改      
      //--- 开盘价,止损和获利水平没有变化
      else
      //--- 通知错误
         PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",
                     ticket,orderinfo.PriceOpen(),orderinfo.StopLoss(),orderinfo.TakeProfit());
     }
//--- 结束,订单没有改变
   return(false);       // 不需要做修改 
  }
//+------------------------------------------------------------------+
//| 在修改订单之前检查水平新的数值                                        |
//+------------------------------------------------------------------+
bool PositionModifyCheck(ulong ticket,double sl,double tp)
  {
//--- 根据编号选择订单
   if(positioninfo.SelectByTicket(ticket))
     {
      //--- 用于设置挂单的交易品种的点位大小和名称
      string symbol=positioninfo.Symbol();
      double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      //--- 检查止损水平有没有改变
      bool StopLossChanged=(MathAbs(positioninfo.StopLoss()-sl)>point);
      //--- 检查获利水平有没有改变
      bool TakeProfitChanged=(MathAbs(OrderTakeProfit()-tp)>point);
      //--- 如果水平有任何变化
      if(StopLossChanged || TakeProfitChanged)
         return(true);  // 仓位可以修改      
      //--- 止损和获利水平没有变化
      else
      //--- 通知错误
         PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",
                     ticket,orderinfo.PriceOpen(),orderinfo.StopLoss(),orderinfo.TakeProfit());
     }
//--- 结束,订单没有改变
   return(false);       // 不需要做修改 
  }
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 订单和仓位的价格水平
   double priceopen,stoploss,takeprofit;
//--- 当前订单和仓位的编号
   ulong orderticket,positionticket;
/*
   ... 取得订单编号和新的止损/获利/建仓价格水平
*/
//--- 在修改挂单之前检查水平   
   if(OrderModifyCheck(orderticket,priceopen,stoploss,takeprofit))
     {
      //--- 检验成功
      trade.OrderModify(orderticket,priceopen,stoploss,takeprofit,
                        orderinfo.TypeTime(),orderinfo.TimeExpiration());
     }
/*
   ... 取得仓位的编号和新的止损/获利水平
*/
//--- 在修改仓位之前做检查
   if(PositionModifyCheck(positionticket,stoploss,takeprofit))
     {
      //--- 检验成功
      trade.PositionModify(positionticket,stoploss,takeprofit);
     }
//---
  }

使用 MQL4 语言作检查的实例可以在 Check_OrderLevels.mq4 脚本程序中找到:

#property strict
//+------------------------------------------------------------------+
//| 在修改订单之前检查水平新的数值                                         |
//+------------------------------------------------------------------+
bool OrderModifyCheck(int ticket,double price,double sl,double tp)
  {
//--- 根据编号选择订单
   if(OrderSelect(ticket,SELECT_BY_TICKET))
     {
      //--- 用于设置挂单的交易品种的点位大小和名称
      string symbol=OrderSymbol();
      double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      //--- 检查建仓价格有没有改变
      bool PriceOpenChanged=true;
      int type=OrderType();
      if(!(type==OP_BUY || type==OP_SELL))
        {
         PriceOpenChanged=(MathAbs(OrderOpenPrice()-price)>point);
        }
      //--- 检查止损水平有没有改变
      bool StopLossChanged=(MathAbs(OrderStopLoss()-sl)>point);
      //--- 检查获利水平有没有改变
      bool TakeProfitChanged=(MathAbs(OrderTakeProfit()-tp)>point);
      //--- 如果水平有任何变化
      if(PriceOpenChanged || StopLossChanged || TakeProfitChanged)
         return(true);  // 订单可以修改      
      //--- 开盘价,止损和获利水平没有变化
      else
      //--- 通知错误
         PrintFormat("订单 #%d 水平已经是 Open=%.5f SL=%.5f TP=%.5f",
                     ticket,OrderOpenPrice(),OrderStopLoss(),OrderTakeProfit());
     }
//--- 结束,订单没有改变
   return(false);       // 不需要做修改 
  }
//+----------------------------------------+
//| 脚本程序起始函数                          |
//+----------------------------------------+
void OnStart()
  {
//--- 订单和仓位的价格水平
   double priceopen,stoploss,takeprofit;
//--- 当前订单的编号 
   int orderticket;
/*
   ... 取得订单编号和新的止损/获利/建仓价格水平
*/
//--- 在修改订单之前做检查   
   if(OrderModifyCheck(orderticket,priceopen,stoploss,takeprofit))
     {
      //--- 检验成功
      OrderModify(orderticket,priceopen,stoploss,takeprofit,OrderExpiration());
     }
  }

推荐阅读的文章:

  1. 如何更简单地在EA交易的代码中发现和恢复错误
  2. 如何使用MQL4开发安全可信赖的交易机器人


尝试引入编译好的文件(例如 EX4/EX5)和 DLL

通过市场发布的程序必须保证对用户安全,所以,任何使用DLL或者来自编译好的EX4/EX5文件的函数都会被认为是错误的,这些产品将不能在市场上发布。

如果您的程序需要标准发布中没有的额外的指标,请使用资源(Resources)


使用 iCustom() 调用自定义指标

如果您程序的运行需要调用来自自定义指标的数据,所有所需的指标应该放到资源(Resources)中。市场中的产品需要在任何没有准备的环境下工作,所以它们应该在它们的 EX4/EX5 文件中包含所有它们所需的一切。推荐阅读的相关文章:


向函数传入无效参数(运行时错误)

这种类型的错误相对较少,它们中的大部分都有可用的代号, 用于帮助查找原因。

常数 数值 描述
ERR_INTERNAL_ERROR 4001 未料到的内部错误
ERR_WRONG_INTERNAL_PARAMETER 4002 使用错误参数调用客户终端内部函数
ERR_INVALID_PARAMETER 4003 使用错误参数调用系统函数
ERR_NOTIFICATION_WRONG_PARAMETER 4516 使用无效参数发送通知 — 把空字符串或者NULL值传给SendNotification() 函数
ERR_BUFFERS_WRONG_INDEX 4602 错误的指标缓冲区索引
ERR_INDICATOR_WRONG_PARAMETERS 4808 当创建指标时使用的参数数量错误
ERR_INDICATOR_PARAMETERS_MISSING 4809 创建指标时没有参数
ERR_INDICATOR_CUSTOM_NAME 4810 数组中的第一个参数必须是自定义指标的名称
ERR_INDICATOR_PARAMETER_TYPE 4811 当创建指标时数组中的参数类型不正确
ERR_NO_STRING_DATE 5030 字符串中没有日期
ERR_WRONG_STRING_DATE 5031 字符串中日期错误
ERR_TOO_MANY_FORMATTERS 5038 格式符号多于参数数量
ERR_TOO_MANY_PARAMETERS 5039 参数数量多于格式符号数量

上面的表格并没有列出程序运行过程和总所有可能遇到的错误。

 

访问冲突

这个错误发生于尝试访问拒绝访问的内存地址时,在这种情况下,有必要通过服务台或者联系人页面来联系开发人员。重现错误的详细步骤描述,以及附加上源代码将能大幅加快发现错误原因,并且有助于提高源代码编译器。




消耗CPU资源和内存

当写程序的时候,使用节约时间的算法是很关键的,否则其他运行在终端中的程序都可能变慢甚至无法运行。

需要记住的是终端会为每个在市场报价中的交易品种分配一个通用的线程,所有在图表上运行的指标都在那个交易品种的线程中处理,

这就是说,如果打开了5个EURUSD不同时段的图表并且那些图表上有15个指标,那么这些图表和指标的计算和在图表上显示信息都由一个线程来完成。所以,一个使用资源效率不高的指标在图表上运行时可能会减慢所有其他指标的运行,甚至使该交易品种其他图表上的价格无法绘制。

您可以在您的算法中简单地使用GetMicrosecondCount()函数来检查时间,很容易测量两行代码之间执行的时间微秒数,如果要把这时间转为毫秒(ms), 它应该除以 1000 (1 毫秒包含 1000 微秒)。通常,指标运行时的瓶颈是在OnCalculate()处理函数中,作为一项规则,指标的第一次计算很大程度上依赖于图表的最大柱数参数,把它设为"无限(Unlimited)"并在M1时段,拥有超过10年历史数据的交易品种上运行指标,如果第一次开始会花费很长时间(例如,超过 100 毫秒), 那么就需要优化代码。

这是一个在ROC指标中测量 OnCalculate() 处理函数的执行时间的实例, ROC指标的源代码是包含在终端的标准分发包中的。插入的代码用黄色高亮显示:

//+-----------------------------------------------+
//| 变化速率(Rate of Change, ROC)                   |
//+-----------------------------------------------+
int OnCalculate(const int rates_total,const int prev_calculated,const int begin,const double &price[])
  {
//--- 检查数据数量
   if(rates_total<ExtRocPeriod)
      return(0);
//--- 计算开始时间
   ulong start=GetMicrosecondCount();  
//--- 预先计算
   int pos=prev_calculated-1; // 设置计算位置
   if(pos<ExtRocPeriod)
      pos=ExtRocPeriod;
//--- 计算的主循环
   for(int i=pos;i<rates_total && !IsStopped();i++)
     {
      if(price[i]==0.0)
         ExtRocBuffer[i]=0.0;
      else
         ExtRocBuffer[i]=(price[i]-price[i-ExtRocPeriod])/price[i]*100;
     }
//--- 计算结束时间     
   ulong finish=GetMicrosecondCount();  
   PrintFormat("Function %s in %s took %.1f ms",__FUNCTION__,__FILE__,(finish-start)/1000.);
//--- OnCalculate 完成返回新的 prev_calculated.
   return(rates_total);
  }

内存的使用可以使用MQLInfoInteger(MQL_MEMORY_USED)函数来测量,并且,可以使用代码分析器(Profiler)来找到您程序中最消耗资源的函数,我们还推荐您阅读指标的经济计算原则以及调试 MQL5 程序等文章。

EA交易工作于它们自己的线程中,但是以上知识也同样应用于它们。写出优化的代码对于任何类型的程序都是重要的,不论是EA交易,指标,开发库或是脚本程序。


不能有过多的检验

以上的关于检验指标和EA交易的技巧不仅使用于在市场发布的产品,对于您写自己用的程序也同样适用。本文没有涵盖在真实帐户中交易可能遇到的所有错误,它也没有考虑到处理交易错误的原则,例如失去与交易服务器的链接,重新报价,交易被拒以及许多其他可能打断交易系统规则的问题。每个开发人员对于这些状况都有个人的解决方法,

推荐新手阅读所有关于错误处理的文章,并在论坛中和本文的评论中问问题。其他更加有经验的 MQL5.community 成员将会帮您解决困难,我们希望本文中收集的信息将会帮您在更短的时间内创建更加可靠的机器人。


推荐阅读的相关文章: