下载MetaTrader 5

MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误

18 十月 2013, 10:49
Anatoli Kazharski
0
1 032

简介

作为我们在系列前一篇文章,"MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性"中EA交易工作的继续, 我们将使用很多有用的函数,以及提高和优化已有的函数来增强它。

在MQL编程论坛上,初学者的很多问题都是有关设置/修改交易参数(止损,获利还有挂单等)时的错误,我相信你们中的很多人都会熟悉日志中以[invalid stops]结尾的消息。在本文中,我们将创建在开启/修改仓位之前规范化和检查交易参数值正确性的函数。

这一次EA交易有可以在MetaTrader 5策略测试器中优化的外部参数,并且在某些方面组成了一个简单的交易系统。当然,离能够开发一个真正的交易系统我们还有很长的路要走,但是罗马不是一天建成的,所以我们还有很多事情要做。

在展开文章的时候会考虑已有函数的代码优化,信息面板部分还不会处理这方面,因为我们还需要看一些无法使用标准标识符获得的仓位属性(需要使用成交历史)。尽管如此,这个主题还是会在系列中以后的文章中包括。

EA 交易开发

让我们开始吧,和通常一样,我们从在文件开头部分插入额外的枚举,变量,数组以及辅助函数开始,我们需要一个函数可以使我们简单地读取交易品种属性,读取仓位属性也将有必要采用相同的方法。

在前文中我们看到,全局变量在GetPositionProperties函数中被一次以仓位属性赋值,这一次,我们尝试一下独立地读取每一个属性。下面是为实现以上功能的两个枚举,函数本身我们会晚一些看。

//--- 仓位属性的枚举
enum ENUM_POSITION_PROPERTIES
  {
   P_SYMBOL        = 0,
   P_MAGIC         = 1,
   P_COMMENT       = 2,
   P_SWAP          = 3,
   P_COMMISSION    = 4,
   P_PRICE_OPEN    = 5,
   P_PRICE_CURRENT = 6,
   P_PROFIT        = 7,
   P_VOLUME        = 8,
   P_SL            = 9,
   P_TP            = 10,
   P_TIME          = 11,
   P_ID            = 12,
   P_TYPE          = 13,
   P_ALL           = 14
  };
//--- 交易品种属性的枚举
enum ENUM_SYMBOL_PROPERTIES
  {
   S_DIGITS       = 0,
   S_SPREAD       = 1,
   S_STOPSLEVEL   = 2,
   S_POINT        = 3,
   S_ASK          = 4,
   S_BID          = 5,
   S_VOLUME_MIN   = 6,
   S_VOLUME_MAX   = 7,
   S_VOLUME_LIMIT = 8,
   S_VOLUME_STEP  = 9,
   S_FILTER       = 10,
   S_UP_LEVEL     = 11,
   S_DOWN_LEVEL   = 12,
   S_ALL          = 13
  };
ENUM_SYMBOL_PROPERTIES枚举并不包含所有的交易品种属性,但是它们可以在任何有需要的时候添加,这个枚举还包含着基于其他交易品种属性计算的用户定义的属性(10, 11, 12),还有一个标识符和仓位属性的枚举一样,用于从枚举中一次读取所有属性。

之后是EA交易的外部参数:

//--- EA交易的外部参数
input int            NumberOfBars=2;     // 用于买入/卖出的牛市/熊市柱形
input double         Lot         =0.1;   // 手数
input double         StopLoss    =50;    // 止损
input double         TakeProfit  =100;   // 获利
input double         TrailingStop=10;    // 跟踪止损
input bool           Reverse     =true;  // 反向持仓
让我们仔细看一下这些外部参数:
  • NumberOfBars - 这个参数设置用于开启仓位的同方向柱数;
  • Lot - 仓位交易量;
  • TakeProfit - 获利价位点数. 0值表示不需要设置获利值。
  • StopLoss - 止损价位点数. 0值表示不需要设置止损值。
  • TrailingStop - 跟踪止损点数,对于一个买入仓位,计算是基于柱的最小值的(最小值减去止损参数的点数),对于一个卖出仓位,计算是基于柱的最大值的(最大值加上止损参数的点数),零值表示跟踪止损被关闭。
  • Reverse 启用/禁用反向仓位,

只有NumberOfBars参数需要进一步说明了,一般不应该把这个参数设置超过5,因为这非常少见并且在这种价格移动下开启仓位可能已经太晚了。我们将需要一个变量来帮我们调整此参数的值:

//--- 用于检查 NumberOfBars 外部参数的值
int                  AllowedNumberOfBars=0;
这个参数也用于判断价格数组中存储的柱数据的数量,当我们将要修改其自定义函数的时候我们将会讨论。

和仓位属性一样,我们也在全局范围内声明交易品种的属性一边任何函数都能够访问它们。

//--- 交易品种属性
int                  sym_digits=0;           // 小数位数
int                  sym_spread=0;           // 点差的点数
int                  sym_stops_level=0;      // 止损
double               sym_point=0.0;          // 每一点的值
double               sym_ask=0.0;            // 买价
double               sym_bid=0.0;            // 卖价
double               sym_volume_min=0.0;     // 交易的最小交易量
double               sym_volume_max=0.0;     // 交易的最大交易量
double               sym_volume_limit=0.0;   // 在相同方向上仓位和订单的最大允许交易量
double               sym_volume_step=0.0;    // 交易中交易量改变的最小步长
double               sym_offset=0.0;         // 针对一个事务最大可能价格的偏移
double               sym_up_level=0.0;       // 止损上限价格
double               sym_down_level=0.0;     // 止损下限价格
因为跟踪止损必须基于柱的最高价和最低价计算,我们需要保存这些数据的数组:
//--- 价格数据数组
double               close_price[]; // 收盘价 (柱的收盘价)
double               open_price[];  // 开盘价 (柱的开盘价)
double               high_price[];  // 最高价 (柱的最高价)
double               low_price[];   // 最低价 (柱的最低价)

让我们现在继续修改和创建函数,我们已经有了把开盘价和收盘价复制到价格数组的GetBarsData函数,现在,我们也需要复制最高价和最低价。另外,从NumberOfBars参数获得的数值也要进行调整,这就是函数修改之后的样子:

//+------------------------------------------------------------------+
//| 获得柱的数值                                                       |
//+------------------------------------------------------------------+
void GetBarsData()
  {
//--- 调整开启仓位条件中柱的数量
   if(NumberOfBars<=1)
      AllowedNumberOfBars=2;              // 至少需要两个柱
   if(NumberOfBars>=5)
      AllowedNumberOfBars=5;              // 但是不要超过5
   else
      AllowedNumberOfBars=NumberOfBars+1; // 而且只能多一个
//--- 倒转索引顺序 (... 3 2 1 0)
   ArraySetAsSeries(close_price,true);
   ArraySetAsSeries(open_price,true);
   ArraySetAsSeries(high_price,true);
   ArraySetAsSeries(low_price,true);
//--- 读取柱的收盘价
//    如果获得数值的数量少于请求数,打印相关信息
   if(CopyClose(_Symbol,Period(),0,AllowedNumberOfBars,close_price)<AllowedNumberOfBars)
     {
      Print("复制数据到收盘价数组失败 ("
            +_Symbol+", "+TimeframeToString(Period())+")!"
            "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- 读取柱的开盘价
//    如果获取数值的数量少于所请求的,打印相关信息
   if(CopyOpen(_Symbol,Period(),0,AllowedNumberOfBars,open_price)<AllowedNumberOfBars)
     {
      Print("复制数值到开盘价数组失败 ("
            +_Symbol+", "+TimeframeToString(Period())+") !"
            "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- 取得柱的最高价
//    如果获取数值的数量少于所请求的,打印相关信息
   if(CopyHigh(_Symbol,Period(),0,AllowedNumberOfBars,high_price)<AllowedNumberOfBars)
     {
      Print("复制数值到最高价数组失败 ("
            +_Symbol+", "+TimeframeToString(Period())+") to the High price array! "
            "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
//--- 取得柱的最低价
//    如果获取数值的数量少于所请求的,打印相关信息
   if(CopyLow(_Symbol,Period(),0,AllowedNumberOfBars,low_price)<AllowedNumberOfBars)
     {
      Print("复制数值到数组失败 ("
            +_Symbol+", "+TimeframeToString(Period())+")"
            "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError()));
     }
  }

条件要求至少需要两个柱,并且还要有多一个柱因为我们只处理以index[1]开始的已经完成的柱。实际上,这个实例中的调整可以认为是不必要的,因为复制起点的索引可以通过CopyOpen, CopyClose, CopyHighCopyLow函数的第三个参数制定,5个柱的限制也可以根据您自己的判断进行调整(向上/向下)。

GetTradingSignal函数现在变得复杂一些了,因为条件使用不同的方式生成,是根据NumberOfBars参数设置的柱数,进一步,我们现在使用了更加准确的返回值类型 - 订单类型:

//+------------------------------------------------------------------+
//| 判断交易信号                                                       |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE GetTradingSignal()
  {
//--- 买入信号 (ORDER_TYPE_BUY) :
   if(AllowedNumberOfBars==2 && 
      close_price[1]>open_price[1])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==3 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==4 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars==5 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3] && 
      close_price[4]>open_price[4])
      return(ORDER_TYPE_BUY);
   if(AllowedNumberOfBars>=6 && 
      close_price[1]>open_price[1] && 
      close_price[2]>open_price[2] && 
      close_price[3]>open_price[3] && 
      close_price[4]>open_price[4] && 
      close_price[5]>open_price[5])
      return(ORDER_TYPE_BUY);
//--- 卖出信号 (ORDER_TYPE_SELL) :
   if(AllowedNumberOfBars==2 && 
      close_price[1]<open_price[1])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==3 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==4 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars==5 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3] && 
      close_price[4]<open_price[4])
      return(ORDER_TYPE_SELL);
   if(AllowedNumberOfBars>=6 && 
      close_price[1]<open_price[1] && 
      close_price[2]<open_price[2] && 
      close_price[3]<open_price[3] && 
      close_price[4]<open_price[4] && 
      close_price[5]<open_price[5])
      return(ORDER_TYPE_SELL);
//--- 没有信号 (WRONG_VALUE):
   return(WRONG_VALUE);
  }
让我们现在修改GetPositionProperties函数,在前一篇文章中,它允许我们一次取得所有属性,然而,有的时候您可能只需要取得一个属性,为了做到这一点,您当然可以使用语言中提供的标准函数,然而这没有我们想要的那么方便,下面是修改后的GetPositionProperties函数的代码,现在,当一个ENUM_POSITION_PROPERTIES枚举类型的标识符传入后,您可以取得某一个单独的仓位属性或者一次取得所有的属性。
//+------------------------------------------------------------------+
//| 取得仓位属性                                                       |
//+------------------------------------------------------------------+
void GetPositionProperties(ENUM_POSITION_PROPERTIES position_property)
  {
//--- 检查是否有持仓
   pos_open=PositionSelect(_Symbol);
//--- 如果已经有持仓,取得其属性
   if(pos_open)
     {
      switch(position_property)
        {
         case P_SYMBOL        : pos_symbol=PositionGetString(POSITION_SYMBOL);                  break;
         case P_MAGIC         : pos_magic=PositionGetInteger(POSITION_MAGIC);                   break;
         case P_COMMENT       : pos_comment=PositionGetString(POSITION_COMMENT);                break;
         case P_SWAP          : pos_swap=PositionGetDouble(POSITION_SWAP);                      break;
         case P_COMMISSION    : pos_commission=PositionGetDouble(POSITION_COMMISSION);          break;
         case P_PRICE_OPEN    : pos_price=PositionGetDouble(POSITION_PRICE_OPEN);               break;
         case P_PRICE_CURRENT : pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);           break;
         case P_PROFIT        : pos_profit=PositionGetDouble(POSITION_PROFIT);                  break;
         case P_VOLUME        : pos_volume=PositionGetDouble(POSITION_VOLUME);                  break;
         case P_SL            : pos_sl=PositionGetDouble(POSITION_SL);                          break;
         case P_TP            : pos_tp=PositionGetDouble(POSITION_TP);                          break;
         case P_TIME          : pos_time=(datetime)PositionGetInteger(POSITION_TIME);           break;
         case P_ID            : pos_id=PositionGetInteger(POSITION_IDENTIFIER);                 break;
         case P_TYPE          : pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); break;
         case P_ALL           :
            pos_symbol=PositionGetString(POSITION_SYMBOL);
            pos_magic=PositionGetInteger(POSITION_MAGIC);
            pos_comment=PositionGetString(POSITION_COMMENT);
            pos_swap=PositionGetDouble(POSITION_SWAP);
            pos_commission=PositionGetDouble(POSITION_COMMISSION);
            pos_price=PositionGetDouble(POSITION_PRICE_OPEN);
            pos_cprice=PositionGetDouble(POSITION_PRICE_CURRENT);
            pos_profit=PositionGetDouble(POSITION_PROFIT);
            pos_volume=PositionGetDouble(POSITION_VOLUME);
            pos_sl=PositionGetDouble(POSITION_SL);
            pos_tp=PositionGetDouble(POSITION_TP);
            pos_time=(datetime)PositionGetInteger(POSITION_TIME);
            pos_id=PositionGetInteger(POSITION_IDENTIFIER);
            pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);                     break;
         default: Print("The passed position property is not listed in the enumeration!");               return;
        }
     }
//--- 如果没有开启的仓位,把仓位属性变量清零
   else
      ZeroPositionProperties();
  }

类似地,我们实现GetSymbolProperties函数用于取得交易品种属性:

//+------------------------------------------------------------------+
//| 取得交易品种属性                                                   |
//+------------------------------------------------------------------+
void GetSymbolProperties(ENUM_SYMBOL_PROPERTIES symbol_property)
  {
   int lot_offset=1; // 距离止损水平的偏移点数
//---
   switch(symbol_property)
     {
      case S_DIGITS        : sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);                   break;
      case S_SPREAD        : sym_spread=(int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);                   break;
      case S_STOPSLEVEL    : sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);   break;
      case S_POINT         : sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);                           break;
      //---
      case S_ASK           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);                       break;
      case S_BID           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);                       break;
         //---
      case S_VOLUME_MIN    : sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);                 break;
      case S_VOLUME_MAX    : sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);                 break;
      case S_VOLUME_LIMIT  : sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);             break;
      case S_VOLUME_STEP   : sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);               break;
      //---
      case S_FILTER        :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);        break;
         //---
      case S_UP_LEVEL      :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
         sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits);                     break;
         //---
      case S_DOWN_LEVEL    :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
         sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits);                   break;
         //---
      case S_ALL           :
         sym_digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
         sym_spread=(int)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD);
         sym_stops_level=(int)SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
         sym_point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
         sym_ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),sym_digits);
         sym_bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),sym_digits);
         sym_volume_min=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
         sym_volume_max=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
         sym_volume_limit=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_LIMIT);
         sym_volume_step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
         sym_offset=NormalizeDouble(CorrectValueBySymbolDigits(lot_offset*sym_point),sym_digits);
         sym_up_level=NormalizeDouble(sym_ask+sym_stops_level*sym_point,sym_digits);
         sym_down_level=NormalizeDouble(sym_bid-sym_stops_level*sym_point,sym_digits);                   break;
         //---
      default: Print("The passed symbol property is not listed in the enumeration!"); return;
     }
  }
请注意,一些交易品种的属性可能要求您先取得其他属性。

我们有了个新函数, CorrectValueBySymbolDigits,它根据价格中的小数点位数返回相关的值,可以给这个函数传入整数或者实数。传入数据的类型决定使用哪个版本的函数,这个特性被称为函数重载

//+------------------------------------------------------------------+
//| 根据价格小数点位数调整数值 (整数形)                                   |
//+------------------------------------------------------------------+
int CorrectValueBySymbolDigits(int value)
  {
   return (sym_digits==3 || sym_digits==5) ? value*=10 : value;
  }
//+------------------------------------------------------------------+
//| 根据价格小数点位数调整数值 (双精度型)                                 |
//+------------------------------------------------------------------+
double CorrectValueBySymbolDigits(double value)
  {
   return (sym_digits==3 || sym_digits==5) ? value*=10 : value;
  }
我们的EA交易将有一个外部参数用来指定建仓的交易量(手数),让我们创建一个函数来根据交易品种特性来调整交易手数 - CalculateLot:
//+------------------------------------------------------------------+
//| 计算仓位手数                                                       |
//+------------------------------------------------------------------+
double CalculateLot(double lot)
  {
//--- 根据步长调整
   double corrected_lot=0.0;
//---
   GetSymbolProperties(S_VOLUME_MIN);  // 取得最小可能手数
   GetSymbolProperties(S_VOLUME_MAX);  // 取得最大可能手数
   GetSymbolProperties(S_VOLUME_STEP); // 取得手数增加/减少步长
//--- 根据步长调整
   corrected_lot=MathRound(lot/sym_volume_step)*sym_volume_step;
//--- 如果比最小值小,返回最小值
   if(corrected_lot<sym_volume_min)
      return(NormalizeDouble(sym_volume_min,2));
//--- 如果比最大值大,返回最大值
   if(corrected_lot>sym_volume_max)
      return(NormalizeDouble(sym_volume_max,2));
//---
   return(NormalizeDouble(corrected_lot,2));
  }

现在让我们前进到与本文标题直接相关的函数,它们非常简单而直接,您可以使用代码中的注释,没有任何困难地了解它们的目标。

CalculateTakeProfit函数用于计算获利值:

//+------------------------------------------------------------------+
//| 计算获利值                                                        |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_ORDER_TYPE order_type)
  {
//--- 如果需要获利值
   if(TakeProfit>0)
     {
      //--- 计算获利值
      double tp=0.0;
      //--- 如果您需要计算卖出仓位的值
      if(order_type==ORDER_TYPE_SELL)
        {
         //--- 计算数据
         tp=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         //--- 如果计算的数值比止损价位下限低则返回计算的数值
         //    如果数值高或者相等,返回调整后的值
         return(tp<sym_down_level ? tp : sym_down_level-sym_offset);
        }
      //--- 如果您需要计算买入仓位的值
      if(order_type==ORDER_TYPE_BUY)
        {
         //--- 计算数据
         tp=NormalizeDouble(sym_ask+CorrectValueBySymbolDigits(TakeProfit*sym_point),sym_digits);
         //--- 如果计算的数值比止损价位上限高则返回计算的数值
         //    如果数值低或者相等,返回调整后的值
         return(tp>sym_up_level ? tp : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }
CalculateStopLoss 函数用于计算止损值:
//+------------------------------------------------------------------+
//| 计算止损值                                                        |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE order_type)
  {
//--- 如果需要止损值
   if(StopLoss>0)
     {
      //--- 计算止损值
      double sl=0.0;
      //--- 如果您需要计算买入仓位的值
      if(order_type==ORDER_TYPE_BUY)
        {
         // 计算数据
         sl=NormalizeDouble(sym_ask-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- 如果计算的值比止损水平下限低则返回计算值
         //    如果数值高或者相等,返回调整后的值
         return(sl<sym_down_level ? sl : sym_down_level-sym_offset);
        }
      //--- 如果您需要计算卖出仓位的值
      if(order_type==ORDER_TYPE_SELL)
        {
         //--- 计算数据
         sl=NormalizeDouble(sym_bid+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- 如果计算值比止损水平上限高则返回计算值
         //    如果数值低或者相等,返回调整后的值
         return(sl>sym_up_level ? sl : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }

CalculateTrailingStop 函数用于计算跟踪止损值:

//+------------------------------------------------------------------+
//| 计算追踪止损值                                                     |
//+------------------------------------------------------------------+
double CalculateTrailingStop(ENUM_POSITION_TYPE position_type)
  {
//--- 计算所需变量
   double            level       =0.0;
   double            buy_point   =low_price[1];    // 用于买入的低价
   double            sell_point  =high_price[1];   // 用于卖出的高价
//--- 计算买入仓位的水平
   if(position_type==POSITION_TYPE_BUY)
     {
      //--- 柱的最低价减去指定的点数
      level=NormalizeDouble(buy_point-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
      //--- 如果计算值比止损价位的下限低, 
      //   计算完成,返回当前止损价位
      if(level<sym_down_level)
         return(level);
      //--- 如果不低,根据卖价计算
      else
        {
         level=NormalizeDouble(sym_bid-CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- 如果计算的价位比下限低,返回当前价位
         //    否则设为最接近的可能价位
         return(level<sym_down_level ? level : sym_down_level-sym_offset);
        }
     }
//--- 计算卖出仓位的数据
   if(position_type==POSITION_TYPE_SELL)
     {
      // 柱的最高价加上指定的点数
      level=NormalizeDouble(sell_point+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
      //--- 如果计算的价位比止损价位上限高,
      //   计算完成,返回当前止损价位
      if(level>sym_up_level)
         return(level);
      //---如果不高,基于买价计算
      else
        {
         level=NormalizeDouble(sym_ask+CorrectValueBySymbolDigits(StopLoss*sym_point),sym_digits);
         //--- 如果计算的价位比上限高,返回计算的价位
         //    否则设为最接近的可能值
         return(level>sym_up_level ? level : sym_up_level+sym_offset);
        }
     }
//---
   return(0.0);
  }

现在我们有了返回交易操作正确数值需要的全部函数,让我们创建一个函数来检查修改跟踪止损的条件,如果指定条件符合则进行修改 - ModifyTrailingStop. 下面是带有详细注释的这个函数的代码。

请注意其中所有上面创建/修改过的函数的用法,switch 开关根据当前仓位类型判断条件,而条件的结果储存在condition变量中,要修改仓位的话,我们使用来自标准库中CTrade类的PositionModify方法。

//+------------------------------------------------------------------+
//| 修改跟踪止损价位                                                   |
//+------------------------------------------------------------------+
void ModifyTrailingStop()
  {
//--- 如果追踪止损和止损都设置了
   if(TrailingStop>0 && StopLoss>0)
     {
      double         new_sl=0.0;       // 计算新的止损价位
      bool           condition=false;  // 检查修改条件
      //--- 获取持仓存在/缺席的标志。
      pos_open=PositionSelect(_Symbol);
      //--- 如果有持仓
      if(pos_open)
        {
         //--- 取得交易品种属性
         GetSymbolProperties(S_ALL);
         //--- 取得仓位属性
         GetPositionProperties(P_ALL);
         //--- 取得止损价位
         new_sl=CalculateTrailingStop(pos_type);
         //--- 根据仓位类型,检查追踪止损修改的相关条件
         switch(pos_type)
           {
            case POSITION_TYPE_BUY  :
               //--- 如果新的止损值比
               //    当前值加步长高
               condition=new_sl>pos_sl+CorrectValueBySymbolDigits(TrailingStop*sym_point);
               break;
            case POSITION_TYPE_SELL :
               //--- 如果新的止损值比
               //   当前值减步长低
               condition=new_sl<pos_sl-CorrectValueBySymbolDigits(TrailingStop*sym_point);
               break;
           }
         //--- 如果有止损,修改前比较数值
         if(pos_sl>0)
           {
            //--- 如果订单修改条件符合,比如新值比当前值更低/更高
            //    修改仓位的跟踪止损
            if(condition)
              {
               if(!trade.PositionModify(_Symbol,new_sl,pos_tp))
                  Print("修改仓位出错: ",GetLastError()," - ",ErrorDescription(GetLastError()));
              }
           }
         //--- 如果没有止损,简单设置它
         if(pos_sl==0)
           {
            if(!trade.PositionModify(_Symbol,new_sl,pos_tp))
               Print("修改仓位出错: ",GetLastError()," - ",ErrorDescription(GetLastError()));
           }
        }
     }
  }
为了和以上修改保持一致,现在让我们调整TradingBlock 函数,和ModifyTrailingStop函数一样, 所有交易订单变量的数值都是用switch开关进行判断,这很大程度减少了代码量,并且让未来的修改更加简单,因为没有了分支或者两种仓位的处理。
//+------------------------------------------------------------------+
//| 交易模块                                                          |
//+------------------------------------------------------------------+
void TradingBlock()
  {
   ENUM_ORDER_TYPE      signal=WRONG_VALUE;                 // 用于取得信号的变量
   string               comment="hello :)";                 // 仓位注释
   double               tp=0.0;                             // 获利
   double               sl=0.0;                             // 止损
   double               lot=0.0;                            // 如有仓位反转用于计算仓位交易量
   double               position_open_price=0.0;            // 仓位开盘价
   ENUM_ORDER_TYPE      order_type=WRONG_VALUE;             // 建仓的订单类型
   ENUM_POSITION_TYPE   opposite_position_type=WRONG_VALUE; // 反向仓位类型
//--- 取得信号
   signal=GetTradingSignal();
//--- 如果没有信号,则退出
   if(signal==WRONG_VALUE)
      return;
//--- 查找已有持仓
   pos_open=PositionSelect(_Symbol);
//--- 取得所有交易品种属性
   GetSymbolProperties(S_ALL);
//--- 计算交易变量值
   switch(signal)
     {
      //--- 给买入订单变量赋值
      case ORDER_TYPE_BUY  :
         position_open_price=sym_ask;
         order_type=ORDER_TYPE_BUY;
         opposite_position_type=POSITION_TYPE_SELL;
         break;
         //--- 给卖出订单变量赋值
      case ORDER_TYPE_SELL :
         position_open_price=sym_bid;
         order_type=ORDER_TYPE_SELL;
         opposite_position_type=POSITION_TYPE_BUY;
         break;
     }
//--- 计算获利和止损价位
   sl=CalculateStopLoss(order_type);
   tp=CalculateTakeProfit(order_type);
//--- 如果没有持仓
   if(!pos_open)
     {
      //--- 调整交易量
      lot=CalculateLot(Lot);
      //--- 开启仓位
      //    如果开启仓位失败,打印相关信息
      if(!trade.PositionOpen(_Symbol,order_type,lot,position_open_price,sl,tp,comment))
        {
         Print("开启仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError()));
        }
     }
//--- 如果有持仓
   else
     {
      //--- 读取仓位类型
      GetPositionProperties(P_TYPE);
      //--- 如果信号和仓位方向相反并且启用了反向仓位操作
      if(pos_type==opposite_position_type && Reverse)
        {
         //--- 取得仓位交易量
         GetPositionProperties(P_VOLUME);
         //--- 调整交易量
         lot=pos_volume+CalculateLot(Lot);
         //--- 开启仓位. 如果开启仓位失败,打印相关信息
         if(!trade.PositionOpen(_Symbol,order_type,lot,position_open_price,sl,tp,comment))
           {
            Print("开启仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError()));
           }
        }
     }
//---
   return;
  }

我们也需要在SetInfoPanel函数中做另一个重要修改,但我们首先要准备几个辅助函数来指明程序怎样/在哪里使用:

//+------------------------------------------------------------------+
//| 返回测试标志                                                       |
//+------------------------------------------------------------------+
bool IsTester()
  {
   return(MQL5InfoInteger(MQL5_TESTER));
  }
//+------------------------------------------------------------------+
//| 返回优化标志                                                       |
//+------------------------------------------------------------------+
bool IsOptimization()
  {
   return(MQL5InfoInteger(MQL5_OPTIMIZATION));
  }
//+------------------------------------------------------------------+
//| 返回可视化测试模式标志                                              |
//+------------------------------------------------------------------+
bool IsVisualMode()
  {
   return(MQL5InfoInteger(MQL5_VISUAL_MODE));
  }
//+------------------------------------------------------------------+
//| 如果所有条件都满足,返回                                             |
//| 策略测试器之外的实时模式                                            |
//+------------------------------------------------------------------+
bool IsRealtime()
  {
   if(!IsTester() && !IsOptimization() && !IsVisualMode())
      return(true);
   else
      return(false); 
  }

我们唯一需要增加到SetInfoPanel函数中的是一个条件,它向程序指出信息面板只应该在可视化和实时模式下显示,如果忽略了这个,测试时间要长4-5倍,这对优化参数来说尤为重要。

//+------------------------------------------------------------------+
//| 设置信息面板                                                       |
//|------------------------------------------------------------------+
void SetInfoPanel()
  {
//--- 可视化或实时模式
   if(IsVisualMode() || IsRealtime())
     {
     // SetInfoPanel() 函数其余的代码
     // ...
     }
  }
现在,我们只需要对主程序函数做出修改,然后就可以进行参数优化和EA交易测试了。
//+------------------------------------------------------------------+
//| EA初始化函数                                                       |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 初始化新柱
   CheckNewBar();
//--- 读取属性并设置面板
   GetPositionProperties(P_ALL);
//--- 设置信息面板
   SetInfoPanel();
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| EA订单函数                                                        |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 如果柱不是新的,退出
   if(!CheckNewBar())
      return;
//--- 如果有新柱
   else
     {
      GetBarsData();          // 取得柱数据
      TradingBlock();         // 检查条件和交易
      ModifyTrailingStop();   // 修改跟踪止损价位
     }
//--- 读取属性并更新面板上的数值
   GetPositionProperties(P_ALL);
//--- 更新信息面板
   SetInfoPanel();
  }
//+------------------------------------------------------------------+
//| 交易事件                                                          |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- 取得仓位信息并在面板上更新它们的值
   GetPositionProperties(P_ALL);
//--- 更新信息面板
   SetInfoPanel();
  }

优化参数和测试EA交易

现在让我们优化参数,我们使策略测试器如下显示:

图 1. 策略测试器参数优化设置

图 1. 策略测试器参数优化设置

EA 交易的参数会有大范围的值:

图 2. EA交易参数优化设置

图 2. EA交易参数优化设置

在一个双核处理器上(Intel Core2 Duo P7350 @ 2.00GHz)优化使用大约7分钟,最大采收率测试结果如下:

图 3. 最大采收率测试结果

图 3. 最大采收率测试结果

结论

这就是所有内容,学习,测试,优化,实验和欢呼。本文中EA交易的源代码可以使用以下链接下载,用于进一步学习。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/643

附加的文件 |
MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性 MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性

我们将会展示一个来自前一篇文章,"MQL5 Cookbook: 自定义信息面板上的仓位属性"的修改版的EA交易。我们将会解决一些问题,包括从柱中获得数据,在当前交易品种中检查新柱事件,在文件中包含标准库中的交易类,创建一个函数来搜索交易信号,还有一个执行交易操作的函数以及在OnTrade()函数中判断交易事件。

MQL5 Cookbook: 自定义信息面板上的仓位属性 MQL5 Cookbook: 自定义信息面板上的仓位属性

这一次我们创建一个简单的EA交易,它可以取得当前交易品种的仓位属性并且在人工交易的时候在自定义信息面板上显示它们。信息面板将使用图形对象创建,显示的信息在每当有订单时都会刷新,这将比系列文章的前一篇 - "MQL5 Cookbook: 获取仓位属性"中提到的每次必须人工运行脚本要方便得多。

自定义图形控件。第一部分:创建简单控件 自定义图形控件。第一部分:创建简单控件

本文介绍开发图形控件的一般原则。我们将准备若干用于快速和方便地处理图形对象的工具,分析一个创建用于输入文本或数字的简单控件的例子以及使用该控件的方法。

MQL5 快速上手 MQL5 快速上手

您已决定学习 MQL5 交易策略的编程语言,但却对其一无所知?我们尝试从新人的视角来看待 MQL5 和 MetaTrader 5 终端,并撰写了此篇简短的介绍性文章。本文中简要地讲述了该语言的多种可能性,还包含有关使用 MetaEditor 5 及此终端的一些小贴士。