请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364705 新帖:26

MQL5 Cookbook: 把交易历史写入文件以及为每个交易品种在Excel中创建余额图表

牛市来了发表于:4 月 17 日 17:13回复(1)

简介

当在各种论坛做沟通时,我经常使用我自己的测试结果作为例子,这些结果显示为Microsoft Excel中的图表截图。很多时候都有人问我这些图表是怎样创建的,Excel提供了很多创建图表的功能,也有很多介绍这方面的书,为了在书中找到所需的信息,可能需要全部读一遍。最终,现在我有时间在本文中解释其中的全部了。

在前面两篇文章, MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径 和 MQL5 Cookbook: 使用不限数量的参数开发多币种EA交易中, 我们使用MQL5开发了多币种的EA交易。我们知道,MetaTrader 5中的测试结果显示为通用的余额/净值曲线,比如,如果您需要独立查看每个交易品种的结果,您需要一次次在EA交易的外部参数中禁用除了所需交易品种之外的所有其他交易品种,然后再次运行测试,这很不方便。

所以今天我将向您展示一种简单的方法,您只要轻点鼠标,就可以在一个Excel数据表中获得多币种EA交易的全部交易品种的余额图表以及综合结果。为了重建实例,我们将直接使用前文的多币种EA交易. 它将使用一个函数来增强,该函数会在测试完成之后把历史交易和所有交易品种的余额曲线写到一个.csv文件中,而且,我们会在报告中增加另外一列用于显示所有本地最大的回撤。

让我们创建一个Excel工作簿,设置后就能够连接数据文件了,工作簿可以一直都打开,所以在运行另外一次测试之前不需要关闭,在测试完成之后,您只需要按某个键刷新数据就可以看到图表中报告的改变。

EA 交易开发

我们的EA交易不会有任何大的改变,我们只是增加几个函数。让我们开始,先在主文件中增加针对交易品种余额的结构和数组。

//--- 余额数组
struct Balance
  {
   double            balance[];
  };
//--- 所有交易品种的余额数组
Balance symbol_balance[];

然后,我们创建一个独立的Report.mqh 包含文件用于生成测试报告,并且在EA交易的主文件中包含它 (参见以下高亮部分的代码):

//--- 包含自定义库
#include "Include/Auxiliary.mqh"
#include "Include/Enums.mqh"
#include "Include/Errors.mqh"
#include "Include/FileFunctions.mqh"
#include "Include/InitializeArrays.mqh"
#include "Include/Report.mqh"
#include "Include/ToString.mqh"
#include "Include/TradeFunctions.mqh"
#include "Include/TradeSignals.mqh"

让我们首先创建一个交易属性结构,就像我们在项目中已经有的针对仓位和交易品种的属性那样,为之我们在Enums.mqh文件中增加属性标识符的枚举:

//+------------------------------------------------------------------+
//| 交易属性枚举                                                       |
//+------------------------------------------------------------------+
enum ENUM_DEAL_PROPERTIES
  {
   D_SYMBOL     = 0, // 交易的交易品种
   D_COMMENT    = 1, // 交易的注释
   D_TYPE       = 2, // 交易的类型
   D_ENTRY      = 3, // 交易的进场类型 - 入, 出, 反转
   D_PRICE      = 4, // 交易的价格
   D_PROFIT     = 5, // 交易的结果 (利润/亏损)
   D_VOLUME     = 6, // 交易的交易量
   D_SWAP       = 7, // 到平仓的累积库存费
   D_COMMISSION = 8, // 交易的手续费
   D_TIME       = 9, // 交易时间
   D_ALL        = 10 // 以上所有的交易属性
  };

进而, 在Report.mqh文件中 我们创建交易属性结构和 GetHistoryDealProperties() 函数用于返回一个交易属性. 此函数接收两个参数: 交易的订单编号和属性标识符。

以下,您可以看到结构和GetHistoryDealProperties()函数的代码:

//--- 历史中的交易属性
struct HistoryDealProperties
  {
   string            symbol;     // 交易品种
   string            comment;    // 注释
   ENUM_DEAL_TYPE    type;       // 交易类型
   ENUM_DEAL_ENTRY   entry;      // 方向
   double            price;      // 价格
   double            profit;     // 利润/亏损
   double            volume;     // 交易量
   double            swap;       // 库存费
   double            commission; // 手续费
   datetime          time;       // 时间
  };
//--- 交易属性变量
HistoryDealProperties  deal;
//+------------------------------------------------------------------+
//| 根据订单编号取得交易属性                                             |
//+------------------------------------------------------------------+
void GetHistoryDealProperties(ulong ticket_number,ENUM_DEAL_PROPERTIES history_deal_property)
  {
   switch(history_deal_property)
     {
      case D_SYMBOL     : deal.symbol=HistoryDealGetString(ticket_number,DEAL_SYMBOL);                 break;
      case D_COMMENT    : deal.comment=HistoryDealGetString(ticket_number,DEAL_COMMENT);               break;
      case D_TYPE       : deal.type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket_number,DEAL_TYPE);    break;
      case D_ENTRY      : deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_number,DEAL_ENTRY); break;
      case D_PRICE      : deal.price=HistoryDealGetDouble(ticket_number,DEAL_PRICE);                   break;
      case D_PROFIT     : deal.profit=HistoryDealGetDouble(ticket_number,DEAL_PROFIT);                 break;
      case D_VOLUME     : deal.volume=HistoryDealGetDouble(ticket_number,DEAL_VOLUME);                 break;
      case D_SWAP       : deal.swap=HistoryDealGetDouble(ticket_number,DEAL_SWAP);                     break;
      case D_COMMISSION : deal.commission=HistoryDealGetDouble(ticket_number,DEAL_COMMISSION);         break;
      case D_TIME       : deal.time=(datetime)HistoryDealGetInteger(ticket_number,DEAL_TIME);          break;
      case D_ALL        :
         deal.symbol=HistoryDealGetString(ticket_number,DEAL_SYMBOL);
         deal.comment=HistoryDealGetString(ticket_number,DEAL_COMMENT);
         deal.type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket_number,DEAL_TYPE);
         deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_number,DEAL_ENTRY);
         deal.price=HistoryDealGetDouble(ticket_number,DEAL_PRICE);
         deal.profit=HistoryDealGetDouble(ticket_number,DEAL_PROFIT);
         deal.volume=HistoryDealGetDouble(ticket_number,DEAL_VOLUME);
         deal.swap=HistoryDealGetDouble(ticket_number,DEAL_SWAP);
         deal.commission=HistoryDealGetDouble(ticket_number,DEAL_COMMISSION);
         deal.time=(datetime)HistoryDealGetInteger(ticket_number,DEAL_TIME);                           break;
         //---
      default: Print("传入的交易属性没有在枚举中列出!");                          return;
     }
  }

我们也将需要几个函数用于把一些交易属性转换为字符串值,如果传入的数值为空或者为零,这些简单的函数会返回一个横线("-")。让我们在ToString.mqh文件中写下这些函数:

//+------------------------------------------------------------------+
//| 返回交易品种名称,否则 - 横线                                        |
//+------------------------------------------------------------------+
string DealSymbolToString(string deal_symbol)
  {
   return(deal_symbol=="" ? "-" : deal_symbol);
  }
//+------------------------------------------------------------------+
//| 把交易类型转换为字符串                                              |
//+------------------------------------------------------------------+
string DealTypeToString(ENUM_DEAL_TYPE deal_type)
  {
   string str="";
//---
   switch(deal_type)
     {
      case DEAL_TYPE_BUY                      : str="买";                      break;
      case DEAL_TYPE_SELL                     : str="卖";                     break;
      case DEAL_TYPE_BALANCE                  : str="余额";                  break;
      case DEAL_TYPE_CREDIT                   : str="信用";                   break;
      case DEAL_TYPE_CHARGE                   : str="收费";                   break;
      case DEAL_TYPE_CORRECTION               : str="更正";               break;
      case DEAL_TYPE_BONUS                    : str="奖励";                    break;
      case DEAL_TYPE_COMMISSION               : str="手续费";               break;
      case DEAL_TYPE_COMMISSION_DAILY         : str="每日手续费";         break;
      case DEAL_TYPE_COMMISSION_MONTHLY       : str="每月手续费";       break;
      case DEAL_TYPE_COMMISSION_AGENT_DAILY   : str="每日代理手续费";   break;
      case DEAL_TYPE_COMMISSION_AGENT_MONTHLY : str="每月代理手续费"; break;
      case DEAL_TYPE_INTEREST                 : str="利息";                 break;
      case DEAL_TYPE_BUY_CANCELED             : str="买取消";             break;
      case DEAL_TYPE_SELL_CANCELED            : str="卖取消";            break;
      //--- 未知交易类型
      default : str="未知";
     }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| 把交易方向转换为字符串                                              |
//+------------------------------------------------------------------+
string DealEntryToString(ENUM_DEAL_ENTRY deal_entry)
  {
   string str="";
//---
   switch(deal_entry)
     {
      case DEAL_ENTRY_IN    : str="入";            break;
      case DEAL_ENTRY_OUT   : str="出";           break;
      case DEAL_ENTRY_INOUT : str="入/出";        break;
      case DEAL_ENTRY_STATE : str="状态记录"; break;
      //--- 未知方向类型
      default : str="未知";
     }
//---
   return(str);
  }
//+------------------------------------------------------------------+
//| 把交易量转换为字符串                                                |
//+------------------------------------------------------------------+
string DealVolumeToString(double deal_volume)
  {
   return(deal_volume<=0 ? "-" : DoubleToString(deal_volume,2));
  }
//+------------------------------------------------------------------+
//| 把价格转换为字符串                                                  |
//+------------------------------------------------------------------+
string DealPriceToString(double deal_price,int digits)
  {
   return(deal_price<=0 ? "-" : DoubleToString(deal_price,digits));
  }
//+------------------------------------------------------------------+
//| 把交易结果转换为字符串                                              |
//+------------------------------------------------------------------+
string DealProfitToString(string deal_symbol,double deal_profit)
  {
   return((deal_profit==0 || deal_symbol=="") ? "-" : DoubleToString(deal_profit,2));
  }
//+------------------------------------------------------------------+
//| 把库存费转换为字符串                                                |
//+------------------------------------------------------------------+
string DealSwapToString(double deal_swap)
  {
   return(deal_swap<=0 ? "-" : DoubleToString(deal_swap,2));
  }

现在,所有事情都准备好了,我们可以写CreateSymbolBalanceReport()函数了,它会准备报告的数据并写到LastTest.csv文件中. 它非常简单:首先我们写抬头(请注意当测试运行于多个交易品种时字符串怎样调整), 然后报告中所需的交易属性以字符串的形式接着连续写到文件中,

以下是CreateSymbolBalanceReport()函数的代码:

//+------------------------------------------------------------------+
//| 创建 .csv 格式的交易测试报告                                        |
//+------------------------------------------------------------------+
void CreateSymbolBalanceReport()
  {
   int    file_handle =INVALID_HANDLE; // 文件句柄
   string path        ="";             // 文件路径

//---如果在创建/读取文件夹时出错,退出
   if((path=CreateInputParametersFolder())=="")
      return;
//--- 在终端的通用文件夹下创建写入数据的文件
   file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- 如果句柄有效 (文件已创建/打开)
   if(file_handle>0)
     {
      int    digits          =0;   // 价格中的小数点位数
      int    deals_total     =0;   // 制定历史中的交易数量
      ulong  ticket          =0;   // 交易订单号
      double drawdown_max    =0.0; // 最大回撤
      double balance         =0.0; // 余额
      //---
      string delimeter       =","; // 分隔符
      string string_to_write ="";  // 生成写下的字符串

      //--- 生成抬头字符串
      string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE";
      //--- 如果选了多个交易品种, 修改抬头字符串
      if(SYMBOLS_COUNT>1)
        {
         for(int s=0; s<SYMBOLS_COUNT; s++)
            StringAdd(headers,","+InputSymbols[s]);
        }
      //--- 写报告抬头
      FileWrite(file_handle,headers);
      //--- 取得完整历史
      HistorySelect(0,TimeCurrent());
      //--- 取得交易数量
      deals_total=HistoryDealsTotal();
      //---根据交易品种数量修改余额数组大小
      ArrayResize(symbol_balance,SYMBOLS_COUNT);
      //--- 修改每个交易品种的交易数组大小
      for(int s=0; s<SYMBOLS_COUNT; s++)
         ArrayResize(symbol_balance[s].balance,deals_total);
      //--- 在循环中迭代并写入数据
      for(int i=0; i<deals_total; i++)
        {
         //--- 取得交易订单号码
         ticket=HistoryDealGetTicket(i);
         //--- 取得所有的交易属性
         GetHistoryDealProperties(ticket,D_ALL);
         //--- 取得价格中的小数位数
         digits=(int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS);
         //--- 计算总体余额
         balance+=deal.profit+deal.swap+deal.commission;
         //--- 通过连接生成用于写入的字符串
         StringConcatenate(string_to_write,
                           deal.time,delimeter,
                           DealSymbolToString(deal.symbol),delimeter,
                           DealTypeToString(deal.type),delimeter,
                           DealEntryToString(deal.entry),delimeter,
                           DealVolumeToString(deal.volume),delimeter,
                           DealPriceToString(deal.price,digits),delimeter,
                           DealSwapToString(deal.swap),delimeter,
                           DealProfitToString(deal.symbol,deal.profit),delimeter,
                           MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                           DoubleToString(balance,2));

         //--- 如果选择了多个交易品种,写下它们的余额
         if(SYMBOLS_COUNT>1)
           {
            //--- 迭代所有交易品种
            for(int s=0; s<SYMBOLS_COUNT; s++)
              {
               //--- 如果交易品种相同而交易结果不等于零
               if(deal.symbol==InputSymbols[s] && deal.profit!=0)
                 {
                  //--- 显示对应交易品种的交易余额
                  //    考虑库存费和手续费
                  symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]+
                                               deal.profit+
                                               deal.swap+
                                               deal.commission;
                  //--- 加到字符串上
                  StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                 }
               //--- 否则写下前值
               else
                 {
                  //--- 如果交易类型为 "余额" (第一笔交易)
                  if(deal.type==DEAL_TYPE_BALANCE)
                    {
                     //--- 余额对所有交易品种都相同
                     symbol_balance[s].balance[i]=balance;
                     StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                    }
                  //--- 否则在当前索引写下前值
                  else
                    {
                     symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1];
                     StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2));
                    }
                 }
              }
           }
         //--- 写下生成的字符串
         FileWrite(file_handle,string_to_write);
         //--- 强制把变量清零,为下个字符串做准备
         string_to_write="";
        }
      //--- 关闭文件
      FileClose(file_handle);
     }
//--- 如果文件无法创建/打开,打印相关信息
   else
      Print("Error creating file: "+IntegerToString(GetLastError())+"");
  }

在以上代码中高亮标出的MaxDrawdownToString()函数根据局部最大值计算回撤并返回新的局部最大值时的字符串表现形式。在其他情况下,此函数返回一个包含 "-" (一条横线)的字符串。

//+------------------------------------------------------------------+
//| 返回局部极值的最大回撤                                              |
//+------------------------------------------------------------------+
string MaxDrawdownToString(int deal_number,double balance,double &max_drawdown)
  {
//--- 报告中显示的字符串
   string str="";
//--- 计算局部最大值和回撤
   static double max=0.0;
   static double min=0.0;
//--- 如果是第一笔交易
   if(deal_number==0)
     {
      //--- 还没有回撤
      max_drawdown=0.0;
      //--- 设置起点为局部最大值
      max=balance;
      min=balance;
     }
   else
     {
      //--- 如果当前余额大于内存中的值
      if(balance>max)
        {
         //--- 使用之前的值计算回撤
         max_drawdown=100-((min/max)*100);
         //--- 更新局部最大值
         max=balance;
         min=balance;
        }
      else
        {
         //--- 返回0值做为回撤
         max_drawdown=0.0;
         //--- 更新最小值
         min=fmin(min,balance);
        }
     }
//--- 决定报告中的字符串
   if(max_drawdown==0)
      str="-";
   else
      str=DoubleToString(max_drawdown,2);
//--- 返回结果
   return(str);
  }

这样所有的报告生成函数都写好了,我们只需要看一下怎样使用这些函数了,这将需要在测试结束时调用的OnTester()函数。请确保您已经在MQL5参考中查阅了这个函数的详细说明。

只需要在OnTester()函数的函数体中写下几行简单的代码,指定报告生成的条件,对应的代码段提供如下:

//+------------------------------------------------------------------+
//| 测试结束事件的处理函数                                              |
//+------------------------------------------------------------------+
double OnTester()
  {
//--- 只在测试后写报告
   if(IsTester() && !IsOptimization() && !IsVisualMode())
      //--- 生成报告并写入文件
      CreateSymbolBalanceReport();
//---
   return(0.0);
  }

现在,如果您在策略测试器下运行EA交易,在测试末尾您会看见EA交易在终端的通用文件夹C:\ProgramData\MetaQuotes\Terminal\Common\Files之下创建了一个目录。而且在EA交易文件夹还生成了LastTest.csv报告文件。如果您使用记事本程序打开文件,您会看到类似的画面:

图 1.  .csv 格式的报告文件

图 1. .csv 格式的报告文件.

在Excel中创建图表

我们可以在Excel中打开创建的文件,就可以在独立的列中看到每种数据类型。使用这种方法数据看起来更加方便。在此,我们已经从技术上准备好创建图表并将其保存为Excel工作簿的*.xlsx格式。然而,如果我们以后再运行测试打开工作簿,我们还是会看到旧的数据。

如果我们尝试刷新数据,因为 LastTest.csv 文件已经在Excel中被使用,文件不会更新,因为EA交易无法在其他程序使用文件的时候打开并写入它。

图 2. Excel 2010 中 .csv格式的报告文件

图 2. Excel 2010 中 .csv格式的报告文件.

在我们这种情况下有一个解决方案,首先在任何您想要的目录下创建一个*.xlsx格式的Excel工作簿,然后打开它并移动到数据页面。

图 3. Excel 2010 的数据页面

图 3. Excel 2010 的数据页面.

在页面工具带上,选择自文本选项,倒入文本文件对话框将会弹出,您需要选择"LastTest.csv"文件,选择文件并点击打开按钮,文本导入向导 - 第1步,共3步 对话框将会弹出如下显示:

图 4. "文本导入向导 - 第1步,共3步" 对话框

图 4. "文本导入向导 - 第1步,共3步" 对话框.

按以上显示调整设置并点击下一步 >。这里,(第2步,共3步) 您需要指定数据文件中的分隔符,在我们的文件中, 它是"," (逗号)。

图 5. "文本导入向导 - 第2步,共3步" 对话框

图 5. "文本导入向导 - 第2步,共3步" 对话框.

点击Next > 移动到文本导入向导 - 第3步,共3步,在此,为所有列保留通用为数据格式,您可以晚些时候改变格式。

图 6. "文本导入向导 - 第3步,共3步"对话框

图 6. "文本导入向导 - 第3步,共3步"对话框.

在点击完成按钮后,导入数据窗口将会出现,您需要在那里指定工作页面以及用于数据导入的单元格。

图 7. 在 Excel 2010 中选择用于数据导入的单元格

图 7. 在 Excel 2010 中选择用于数据导入的单元格.

通常情况下,我们选择左上角的单元格A1。在点击确定以后,点击属性...按钮来设置外部数据范围属性,您将会看到如下显示的对话框。

图 8. 在 Excel 2010 中从文本文件导入数据时的外部数据范围属性

图 8. 在 Excel 2010 中从文本文件导入数据时的外部数据范围属性.

在当前和下一个窗口中按照以上所示调整设置并点击确定

结果,您的数据就会像载入.csv文件那样出现了。但是现在您可以在MetaTrader 5中重复运行测试,而不需要关闭Excel工作簿了。你所要做的只是在运行完测试以后,简单地使用Ctrl+Alt+F5快捷键或者数据页面工作带上的全部刷新按钮来刷新数据。

使用开始页面工具带上的条件格式选项,您可以设置数据表现形式上所需的可视化属性。

图 9. Excel 2010 中的条件格式

图 9. Excel 2010 中的条件格式.

现在我们需要在Excel图表上显示数据了,一个图表将显示所有的余额图表,另一个会以柱状图显示所有的局部最大值的回撤。

让我们首先为余额图表创建数据图。选择所有余额部分的抬头以及从上到下的整个数据数组(按住Shift键,按End键,然后按向下箭头键)。在插入页面,选择想要的图表类型。

图 10. 在 Excel 2010 中选择图表类型

图 10. 在 Excel 2010 中选择图表类型.

这样,图表就创建好了,为了方便可以把它移动到其他的工作页面上,如果要做到这个,选择它并按下Ctrl+X (剪切)。然后到新创建的工作页面上,选择A1单元格并按Ctrl+V (粘贴)。

默认设置下的创建好的图表显示如下图:

图 11. 默认设置下图表的模样

图 11. 默认设置下图表的模样.

您可以自定义图表中的任何元素:修改它的大小,颜色,风格等。

在上图中,水平轴显示交易的数量,让我们修改它以显示日期。为了这个目标,用鼠标右键点击图表并从右键菜单中选择选择数据选项. 选择数据源对话框将会弹出,点击编辑按钮,然后在时间列选择所需的数据范围并点击确定

图 12. "选择数据源" 对话框

图 12. "选择数据源" 对话框.

您可以尝试一下自己创建回撤图表并把它放到第一个图表之下,现在,如有必要它们的显示属性可以自定义。我个人通常这样做:

图 13. 在 Excel 2010 自定义图表

图 13. 在 Excel 2010 自定义图表.

结论

这样,我们在Excel中得到了看起来很不错的测试结果,在我未来的一篇文章中,我将向您展示怎样创建更有信息性的报告。文章的附件中是可以下载的EA交易的文件存档。

在从存档中展开文件后,把ReportInExcel文件夹放到MetaTrader 5\MQL5\Experts目录下。另外, EventsSpy.mq5 指标必须放在MetaTrader 5\MQL5\Indicators目录下。

全部回复

0/140

量化课程

    移动端课程