外汇EA编写教程:MQL5 Cookbook: 交易历史和取得仓位信息的函数库

简介

现在是时候简单总结一下之前关于仓位属性文章的内容了,在本文中,我们会额外创建几个函数来取得只能通过访问交易历史才能获得的属性,我们也会对数据结构更加熟悉,这使我们可以用更加方便的方法访问仓位和交易品种属性。

交易系统中如果仓位在它们的存续期间都保持相同的交易量,则不需要本文中将要提供的函数,但是如果您计划实现一个资金管理系统,晚些时候在您的交易策略中控制仓位的手数,这些函数是不可缺少的。

在我们开始之前,我想建议第一次访问这个网站或者才开始学习MQL5语言,链接到这篇文章的读者们从”MQL Cookbook”系列早些的文章开始。

EA 交易开发

为了能够看到修改过的EA交易中新函数的操作(EA交易来自前文,叫做“MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误”), 在已有持仓,而建仓信号再度出现的时候,我们将增加继续增加仓位交易量的功能,

仓位历史中可能有几个交易,而且如果在交易过程中有仓位交易量的改变,当前仓位价格必然也有所改变。为了找出第一次的进场价格,我们需要访问指定仓位的交易历史,下图演示了仓位仅有一个交易(进场点)的实例:

图 1. 仓位的第一个交易

图 1. 仓位的第一个交易

下面的图显示了随第二个交易改变的仓位价格:

图 2. 仓位的第二个交易

图 2. 仓位的第二个交易

如前文中演示的,您使用标准标识符只能取得当前的仓位价格(POSITION_PRICE_OPEN)和持仓的交易品种的当前价格(POSITION_PRICE_CURRENT)。

然而,在一些交易系统中我们需要知道当前价格和第一个进场点价格,以及最后一笔交易价格之间的距离,所有这些信息存在于账户的交易/订单历史中。以下是和上图关联的交易列表:

图 3. 账户中的交易历史

图 3. 账户中的交易历史

我相信,现在状况非常清楚了,所有目标都已设定,让我们继续修改前文中EA交易。首先,我们会在仓位属性枚举中增加新的数值为0,6,9,12和16的标识符:

//--- 仓位属性的枚举
enum ENUM_POSITION_PROPERTIES
  {
   P_TOTAL_DEALS     = 0,
   P_SYMBOL          = 1,
   P_MAGIC           = 2,
   P_COMMENT         = 3,
   P_SWAP            = 4,
   P_COMMISSION      = 5,
   P_PRICE_FIRST_DEAL= 6,
   P_PRICE_OPEN      = 7,
   P_PRICE_CURRENT   = 8,
   P_PRICE_LAST_DEAL = 9,
   P_PROFIT          = 10,
   P_VOLUME          = 11,
   P_INITIAL_VOLUME  = 12,
   P_SL              = 13,
   P_TP              = 14,
   P_TIME            = 15,
   P_DURATION        = 16,
   P_ID              = 17,
   P_TYPE            = 18,
   P_ALL             = 19
  };

关于每个属性的注释我们会在稍下面的一个结构中看到,

让我们增加外部参数的数量,现在,我们可以设置:

  • MagicNumber – EA交易的唯一编号 (幻数);
  • Deviation – 滑点;
  • VolumeIncrease – 仓位交易量增加值;
  • InfoPanel – 一个让您启用/禁用信息面板显示的参数

这是如何实现:

//--- EA交易的外部参数
sinput   long        MagicNumber=777;     // 幻数
sinput   int         Deviation=10;        // 滑点
input    int         NumberOfBars=2;      // 用于买/卖的牛市/熊市柱数
input    double      Lot=0.1;             // 手数
input    double      VolumeIncrease=0.1;  // 仓位交易量增加量
input    double      StopLoss=50;         // 止损
input    double      TakeProfit=100;      // 获利
input    double      TrailingStop=10;     // 跟踪止损
input    bool        Reverse=true;        // 反向仓位
sinput   bool        ShowInfoPanel=true;  // 是否显示信息面板

请注意使用sinput修饰符的变量,这个修饰符允许您在策略测试器中禁用其优化。事实上,当开发您自己使用的程序时,您会很清楚哪些参数会影响最终结果,所以您从优化中排除它们,但是当有很多参数时,这个方法使您直观地把他们从其他参数中分开,因为它们是灰色标识的:

图 4. 禁用优化的参数是灰色标识的

图 4. 禁用优化的参数时灰色标识的.

我们现在用数据结构(struct)来替代用于保存仓位和交易品种属性值的全局变量:

//--- 仓位属性
struct position_properties
  {
   uint              total_deals;      // 交易数量
   bool              exists;           // 有没有开启仓位的标志
   string            symbol;           // 交易品种
   long              magic;            // 幻数
   string            comment;          // 注释
   double            swap;             // 库存费
   double            commission;       // 手续费   
   double            first_deal_price; // 仓位中第一笔交易的价格
   double            price;            // 仓位当前价格
   double            current_price;    // 仓位交易品种的当前价格      
   double            last_deal_price;  // 仓位最后一笔交易的价格
   double            profit;           // 仓位的利润/亏损
   double            volume;           // 当前仓位交易量
   double            initial_volume;   // 初始仓位交易量
   double            sl;               // 仓位的止损价位
   double            tp;               // 仓位的获利价位
   datetime          time;             // 仓位建仓时间
   ulong             duration;         // 持仓以秒数计算的时间
   long              id;               // 仓位编号
   ENUM_POSITION_TYPE type;            // 仓位类型
  };
//--- 交易品种属性
struct symbol_properties
  {
   int               digits;        // 价格的小数点位数
   int               spread;        // 点差点数
   int               stops_level;   // 止损价位
   double            point;         // 点值
   double            ask;           // 买价
   double            bid;           // 卖价
   double            volume_min;    // 交易的最小交易量
   double            volume_max;    // 交易的最大交易量
   double            volume_limit;  // 仓位以及一个方向上订单允许的最大交易量
   double            volume_step;   // 交易量改变的最小步长
   double            offset;        // 一个事务中最大价格偏移量
   double            up_level;      // 止损价格上限
   double            down_level;    // 止损价格下限
  }

现在,为了访问结构中的某个元素,我们需要建立此结构类型的一个变量,这个过程和创建一个交易类的对象类似,就像前一篇叫做“MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性”的文章中提到的那样。

//--- 仓位和交易品种属性的变量
position_properties  pos;
symbol_properties    symb;

您可以像使用类方法同样的方式访问其中的元素,换句话说,只要在结构变量的后面加一个点,然后此结构包含的元素列表就显示出来了,这非常方便,如果为结构中的字段提供单行注释(像我们例子中那样),他们会在列表右边的提示信息中显示。

图 5a. 结构中的仓位属性字段列表 图 5b. 结构中的交易品种属性字段列表

图 5. 结构栏位列表.

另外一个重点,在修改EA交易的过程中,我们已经把在很多函数中用到的全局变量修改了,所以现在我们需要用对应交易品种和仓位属性结构的字段来替代它们,比如,用于保存是否有开启仓位标志的pos_open全局变量已经被position_properties结构类型中的exists字段替代,这样的话,哪里使用了pos_open变量,哪里都要用pos.exists来替代。

如果您人工做的话会很慢而且很累,所以我们最好使用MetaQuotes语言编辑器的特性来自动化这个任务:在编辑菜单下的查找和替换 -> 替换或者Ctrl+H组合键:

图 6. 查找和替换文字

图 6. 查找和替换文字.

我们需要查找和替换所有用于仓位和交易品种属性的全局变量,然后编译文件再运行一个测试。如果没有发现错误,说明我们已经做好了所有事情。我在这里就不提供代码了,以免把文章变得太长而没有必要。另外,准备好的代码已经附在文章末尾以便下载。

现在我们已经把变量的问题解决了,让我们继续修改已有的函数以及创建新函数。

在外部参数中,您现在可以设置幻数和滑点点数,我们则需要在EA交易代码中做相关修改。我们会创建一个用户定义的辅助函数OpenPosition(), 这些参数将使用CTrade类的函数在发送订单或者开启仓位之前设置。

//+------------------------------------------------------------------+
//| 开启一个仓位                                                       |
//+------------------------------------------------------------------+
void OpenPosition(double lot,
                  ENUM_ORDER_TYPE order_type,
                  double price,
                  double sl,
                  double tp,
                  string comment)
  {
   trade.SetExpertMagicNumber(MagicNumber); // 设置交易结构中的幻数
   trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation)); // 设置滑点点数
//--- 如果开启仓位失败,打印相关信息
   if(!trade.PositionOpen(_Symbol,order_type,lot,price,sl,tp,comment))
     { Print("开启仓位错误: ",GetLastError()," - ",ErrorDescription(GetLastError())); }
  }

在EA交易的主要交易函数 – TradingBlock() 的代码中,我们只需要做些很小的修改. 以下是改动过的部分程序代码:

//--- 如果没有持仓
   if(!pos.exists)
     {
      //--- 调整交易量
      lot=CalculateLot(Lot);
      //--- 开启仓位
      OpenPosition(lot,order_type,position_open_price,sl,tp,comment);
     }
//--- 如果有持仓
   else
     {
      //--- 读取仓位类型
      GetPositionProperties(P_TYPE);
      //--- 如果仓位和信号方向相反并且反向仓位被启用
      if(pos.type==opposite_position_type && Reverse)
        {
         //--- 取得仓位交易量
         GetPositionProperties(P_VOLUME);
         //--- 调整交易量
         lot=pos.volume+CalculateLot(Lot);
         //--- 反转仓位
         OpenPosition(lot,order_type,position_open_price,sl,tp,comment);
         return;
        }
      //--- 如果信号与仓位方向相同并且启用了增加交易量,则增加仓位交易量
      if(!(pos.type==opposite_position_type) && VolumeIncrease>0)
        {
         //--- 取得当前仓位的止损
         GetPositionProperties(P_SL);
         //--- 取得当前仓位的获利
         GetPositionProperties(P_TP);
         //--- 调整交易量
         lot=CalculateLot(Increase);
         //--- 增加仓位交易量
         OpenPosition(lot,order_type,position_open_price,pos.sl,pos.tp,comment);
         return;
        }

以上代码在检查当前仓位方向和信号方向模块部分已经增强,如果它们的方向一致,并且已经通过外部变量启用了增加仓位交易量(VolumeIncrease 参数大于零), 我们检查/调整指定的手数然后发送相关的订单。现在,如果您需要发送一个订单,用于开启或反转一个仓位或者增加仓位交易量,只需要写下一行代码。

让我们创建函数来从交易历史中获取仓位属性,我们从CurrentPositionTotalDeals() 函数开始,它返回当前仓位交易的数量:

//+------------------------------------------------------------------+
//| 返回当前仓位的交易数量                                              |
//+------------------------------------------------------------------+
uint CurrentPositionTotalDeals()
  {
   int    total       =0;  // 选择的历史列表中总的交易数量
   int    count       =0;  // 根据仓位交易品种的交易计数器
   string deal_symbol =""; // 交易的交易品种
//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      //--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      //--- 遍历获得列表中所有的交易
      for(int i=0; i<total; i++)
        {
            //--- 取得交易的交易品种
            deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
            //--- 如果交易的交易品种和当前交易品种相同,增加计数器
            if(deal_symbol==_Symbol)
               count++;
        }
     }
//---
   return(count);
  }

以上代码已经提供了比较详细的注释,但是我们还是应该说一下历史是怎样选择的。在我们的实例中,我们使用HistorySelect()函数以当前仓位开启时间为起点,获得直到当前时间点的历史列表,在历史选择好以后,我们可以使用HistoryDealsTotal()函数来获得列表中的交易数量。其他部分看注释就应该很清楚了。

特定仓位的历史也可以根据其编号使用HistorySelectByPosition()函数进行选择,这里,您需要考虑到,仓位反转的时候仓位编号还是相同的,就像在我们的EA交易中出现的那样,然而,仓位开启时间并不会因为反转而改变,所以这种方法更容易实现。但是如果您还必须处理当前开启仓位以外的交易历史,您需要使用仓位编号。我们会在将来的文章中再回头讨论交易历史。

让我们继续创建一个CurrentPositionFirstDealPrice()函数用来返回仓位第一笔交易的价格,也就是仓位开启时的交易价格。

//+------------------------------------------------------------------+
//| 返回当前仓位第一笔交易的价格                                         |
//+------------------------------------------------------------------+
double CurrentPositionFirstDealPrice()
  {
   int      total       =0;    // 所选历史列表中的交易总数
   string   deal_symbol ="";   // 交易的交易品种
   double   deal_price  =0.0;  // 交易的价格
   datetime deal_time   =NULL; // 交易的时间
//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      //--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      //--- 遍历获得列表中所有的交易
      for(int i=0; i<total; i++)
        {
         //--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         //--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         //--- 取得交易的时间
         deal_time=(datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME);
         //--- 如果交易和仓位开启时间相同, 
         //    并且交易的交易品种和当前交易品种相同,退出循环
         if(deal_time==pos.time && deal_symbol==_Symbol)
            break;
        }
     }
//---
   return(deal_price);
  }

这里的原则和前一函数相同,我们从仓位开启时开始读取历史,然后在每个迭代中检查仓位开启的时间和交易的时间,我们读取交易品种名称,交易的时间,以及交易的价格,当交易的时间和仓位开启时间一致时,即是第一笔交易,因为价格已经赋值给相关的变量,我们只需要返回其数值。

让我们继续,有时,您可能需要取得当前仓位的最后一笔交易的价格,为了这个目的,我们将要创建一个CurrentPositionLastDealPrice() 函数:

//+------------------------------------------------------------------+
//| 返回当前仓位的最后一笔交易的价格                                     |
//+------------------------------------------------------------------+
double CurrentPositionLastDealPrice()
  {
   int    total       =0;   // 所选历史列表的交易总数
   string deal_symbol ="";  // 交易的交易品种 
   double deal_price  =0.0; // 价格
//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      //--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      //--- 在获得的列表中从第一个交易开始遍历所有交易
      for(int i=total-1; i>=0; i--)
        {
         //--- 取得交易的价格
         deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE);
         //--- 取得交易的交易品种
         deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL);
         //--- 如果交易的交易品种和当前交易品种相同,退出循环
         if(deal_symbol==_Symbol)
            break;
        }
     }
//---
   return(deal_price);
  }

这一次循环从列表的最后一笔交易开始,通常情况所需的交易在循环的第一个迭代就找到了,但是如果您同时交易几个交易品种,循环将继续,直到交易的交易品种和当前交易品种相匹配。

当前仓位的交易量可以使用POSITION_VOLUME标准标识符获得. 为了找到初始仓位交易量(第一笔交易的交易量), 我们将要创建一个CurrentPositionInitialVolume() 函数:

//+------------------------------------------------------------------+
//| 返回当前仓位的初始交易量                                            |
//+------------------------------------------------------------------+
double CurrentPositionInitialVolume()
  {
   int             total       =0;           // 所选历史列表的交易总数
   ulong           ticket      =0;           // 交易的订单编号
   ENUM_DEAL_ENTRY deal_entry  =WRONG_VALUE; // 仓位修改方法
   bool            inout       =false;       // 仓位反转标志
   double          sum_volume  =0.0;         // 所有交易累计交易量的计数器,第一笔交易除外
   double          deal_volume =0.0;         // 交易的交易量
   string          deal_symbol ="";          // 交易的交易品种 
   datetime        deal_time   =NULL;        // 交易执行时间
//--- 如果获得了仓位历史
   if(HistorySelect(pos.time,TimeCurrent()))
     {
      //--- 取得获得列表中的交易数量
      total=HistoryDealsTotal();
      //--- 在获得的列表中从最后一笔交易到第一笔交易迭代所有交易
      for(int i=total-1; i>=0; i--)
        {
         //--- 如果根据仓位得到了订单编号,那么...
         if((ticket=HistoryDealGetTicket(i))>0)
           {
            //--- 取得交易的交易量
            deal_volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            //--- 取得仓位修改方法
            deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
            //--- 取得交易执行时间
            deal_time=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
            //--- 取得交易的交易品种
            deal_symbol=HistoryDealGetString(ticket,DEAL_SYMBOL);
            //--- 当仓位执行时间小于或者等于仓位开启时间,退出循环
            if(deal_time<=pos.time)
               break;
            //--- 否则根据仓位交易品种计算交易的累积交易量,第一笔交易除外
            if(deal_symbol==_Symbol)
               sum_volume+=deal_volume;
           }
        }
     }
//--- 如果仓位修改方法是反转
   if(deal_entry==DEAL_ENTRY_INOUT)
     {
      //--- 如果仓位交易量曾经被增加/减少
      //    比如交易数量大于1
      if(fabs(sum_volume)>0)
        {
         //--- 当前仓位交易量减去除第一笔外的所有交易的交易量
         double result=pos.volume-sum_volume;
         //--- 如果结果大于0,返回该结果,否则返回当前仓位交易量       
         deal_volume=result>0 ? result : pos.volume;
        }
      //--- 如果除了进场交易外没有更多交易,
      if(sum_volume==0)
         deal_volume=pos.volume; // 返回当前仓位交易量
     }
//--- 返回初始仓位交易量
   return(NormalizeDouble(deal_volume,2));
  }

这个函数比前面的要复杂一些,我已经尝试考虑到所有可能得出错误结果的状况,通过仔细测试也没有发现任何问题。代码中提供的详细注释应该帮助您了解到这一点。

一个返回仓位持仓时间的函数也是很有用的,我们会安排它并且允许用户选择返回值的适当格式:秒,分钟,小时或者天。为了这个目标,让我们创建另外一个枚举:

//--- 仓位持仓时间
enum ENUM_POSITION_DURATION
  {
   DAYS     = 0, // 天
   HOURS    = 1, // 小时
   MINUTES  = 2, // 分钟
   SECONDS  = 3  // 秒
  };

以下是CurrentPositionDuration()函数所有相关计算的代码:

//+------------------------------------------------------------------+
//| 返回当前仓位的持仓时间                                              |
//+------------------------------------------------------------------+
ulong CurrentPositionDuration(ENUM_POSITION_DURATION mode)
  {
   ulong     result=0;   // 最终结果
   ulong     seconds=0;  // 秒数
//--- 计算持仓时间的秒数
   seconds=TimeCurrent()-pos.time;
//---
   switch(mode)
     {
      case DAYS      : result=seconds/(60*60*24);   break; // 计算天数
      case HOURS     : result=seconds/(60*60);      break; // 计算小时数
      case MINUTES   : result=seconds/60;           break; // 计算分钟数
      case SECONDS   : result=seconds;              break; // 无计算 (秒数)
      //---
      default        :
         Print(__FUNCTION__,"(): 未知传入持仓时间模式!");
         return(0);
     }
//--- 返回结果
   return(result);
  }

让我们为显示仓位属性的信息面板创建一个CurrentPositionDurationToString()函数. 这个函数将把持仓时间的描述转换为易于被用户理解的格式,秒数会传给函数,而此函数会返回包含仓位持仓时间的天数,小时数,分钟数和秒数的字符串:

//+------------------------------------------------------------------+
//| 把仓位持仓时间转换为字符串                                           |
//+------------------------------------------------------------------+
string CurrentPositionDurationToString(ulong time)
  {
//--- 如果没有仓位则返回横线
   string result="-";
//--- 如果有持仓
   if(pos.exists)
     {
      //--- 用于计算结果的变量
      ulong days=0;
      ulong hours=0;
      ulong minutes=0;
      ulong seconds=0;
      //--- 
      seconds=time%60;
      time/=60;
      //---
      minutes=time%60;
      time/=60;
      //---
      hours=time%24;
      time/=24;
      //---
      days=time;
      //--- 以指定的格式 DD:HH:MM:SS 生成字符串
      result=StringFormat("%02u d: %02u h : %02u m : %02u s",days,hours,minutes,seconds);
     }
//--- 返回结果
   return(result);
  }

现在每件事情都完成了,我就不再提供GetPositionProperties() 和GetPropertyValue() 函数为适应以上改变所修改的代码了,如果您阅读了本系列前面的文章,您可以没有任何困难地自己去做,不管怎样,源代码文件在本文附件中。

最终结果,信息面板显示如下:

图 7. 在信息面板上演示所有仓位属性

图 7. 在信息面板上演示所有仓位属性.

这样,我们现在有了获得仓位属性的函数库,在将来的文章中如果我们需要还会继续做工作。

优化参数和测试EA交易

作为试验,让我们尝试优化EA交易的参数,尽管我们现在拥有的还不能叫做全功能交易系统,我们所获得的结果将可以让我们大开眼界,并且增加我们作为交易系统开发者的经验。

我们使策略测试器如下显示:

图 8. 用于参数优化的策略测试器设置

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

EA交易外部参数设置应该如下:

图 9. EA交易用于优化的参数设置

图 9. EA交易用于优化的参数设置

在优化之后,我们根据最大采收率将获得的结果排序:

图 10. 根据最大采收率排序的结果

图 10. 根据最大采收率排序的结果.

让我们现在测试最上面的一组参数,其采收率等于4.07. 即使我们只是在EURUSD上做了优化, 我们在其他许多交易品种上也能得到正面结果:

EURUSD的结果:

图 11. EURUSD的结果

图 11. EURUSD的结果.

AUDUSD的结果:

图 12. AUDUSD的结果

图 12. AUDUSD的结果.

NZDUSD的结果:

图13. NZDUSD的结果

图13. Results for NZDUSD.

结论

理论上任何想法都可以开发和提高,每一个交易系统在认为有缺陷而摒弃之前都应该仔细测试,在未来的文章中,我们会看一下在自定义和适应几乎任何交易系统时起到重要作用的各种不同的机制和规划。

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

 

 


MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投