MetaTrader 5 中的多元品种余额图

Anatoli Kazharski | 25 五月, 2018


内容

概述

之前的一篇文章 当中, 我们曾研究过多元品种余额图的可视化。 自那以来,已经出现很多 MQL 函数库,无需使用第三方程序即可在 MetaTrader 5 平台上完全实现这种可视化。

在本文中,我将展示一款带有图形界面的应用程序示例,其中包含多元品种余额图和最后测试结果的资金回撤图。 完成 EA 测试后,交易记录将被写入文件。 这些数据可以读取并显示在图表上。

此外,本文还介绍了 EA 的一个版本,其在交易过程中以及在可视化测试模式期间,在图形界面上显示并更新多元品种余额图。


开发图形界面

在文章 "MetaTrader 5 里的可视化交易策略优化" 中,我们详细研究了如何包含和使用 EasyAndFast 函数库,以及它如何有助您开发 MQL 应用程序的图形化界面。 所以,我们在这里先从相应的图形界面开始。 

我们来列出在图形界面中使用的元素。

  • 控件的窗体。
  • 用最后一次测试结果来更新图表的按钮。
  • 多元品种余额图。
  • 资金回撤图。
  • 用来显示额外摘要信息的状态栏。

下列代码提供了创建这些元素的方法声明。 在单独的 包含文件 中实现方法。

//+------------------------------------------------------------------+
//| 用于创建应用程序的类                                                 |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 窗体
   CWindow           m_window1;
   //--- 状态栏
   CStatusBar        m_status_bar;
   //--- 图形
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- 按钮
   CButton           m_update_graph;
   //---
public:
   //--- 创建图形界面
   bool              CreateGUI(void);
   //---
private:
   //--- 窗体
   bool              CreateWindow(const string text);
   //--- 状态栏
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- 图形
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- 按钮
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| 创建控件元素的方法                                                   |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

在此情况下,创建图形界面的主要方法如下所示:

//+------------------------------------------------------------------+
//| 创建图形界面                                                       |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- 为控件元素创建窗体
   if(!CreateWindow("Expert panel"))
      return(false);
//--- 创建控件元素
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateGraph1(1,50))
      return(false);
   if(!CreateGraph2(1,159))
      return(false);
   if(!CreateUpdateGraph(7,25,"Update data"))
      return(false);
//--- 图形界面创建完毕
   CWndEvents::CompletedGUI();
   return(true);
  }

结果就是,如果您现在编译 EA 并在终端中下载其图形,则当前结果如下所示:

 图例 1. EA 图形界面

图例 1. EA 图形界面

现在,我们来研究测试后将数据写入文件。


测试的多元品种 EA

为了进行测试,我们将使用来自标准发行版的 MACD Sample EA,使其成为多符号。 此版本中使用的多元品种结构不精确。 使用相同的参数,依据执行测试的品种 (在测试器设置中所选择) 结果将有所不同。 因此,本 EA 仅用于测试和展示本主题工作架构内获得的结果。

创建多元品种 EA 的全新可能性将在近期的 MetaTrader 5 更新中呈现。 之后,可以考虑为这种类型的 EA 开发最终的通用版本。 如果您迫切需要快速、准确的多元品种结构,您可以尝试在 论坛上提议的选项

我们再添加一个字符串参数 for specifying symbols,测试依据外部参数进行:

//--- 外部参数
sinput string Symbols           ="EURUSD,USDJPY,GBPUSD,EURCHF"; // 品种
input  double InpLots           =0.1;                           // 手数
input  int    InpTakeProfit     =167;                           // 止盈 (点数)
input  int    InpTrailingStop   =97;                            // 尾随停止级别 (点数)
input  int    InpMACDOpenLevel  =16;                            // MACD 开仓级别 (点数)
input  int    InpMACDCloseLevel =19;                            // MACD 平仓级别 (点数)
input  int    InpMATrendPeriod  =14;                            // MA 趋势周期

品种用逗号分隔。 程序类 (CProgram) 实现读取此参数的方法,以及在服务器列表之一的市场观察中检查并设置品种的方法。 亦或,您可以通过文件中预先准备的清单指定交易品种,如文章 "MQL5 酷宝书: 开发无限数量参数的多币种智能交易系统" 所示。 此外,您可以制作多个列表供用户选择。 文章 "MQL5 酷宝书: 减少过度拟合和处理缺失报价的影响" 中提供了一个这样的例子。 使用图形界面能够带来更多选择品种及其列表的方法。 我将在后续文章之一中展示一个可能的选项。 

在测试公共列表中的字符之前,我们需要将它们保存到数组中。 之后传递 数组 (source_array[])CProgram::CheckTradeSymbols() 方法。 此处,在第一个循环中,我们传递外部参数中指定的品种。 在第二个循环中,我们检查这个品种是否在经纪商服务器上的列表中。 如果是, 将其添加到市场观察 和已检查品种的数组。 

如果未检测到品种,则仅使用 EA 启动时的当前品种。

class CProgram : public CWndEvents
  {
private:
   //--- 检查所传递数组中的交易品种并返回可用数组之一
   void              CheckTradeSymbols(string &source_array[],string &checked_array[]);
  };
//+------------------------------------------------------------------+
//| 检查所传递数组中的交易品种                                            |
//| 并返回可用数组之一                                                   |
//+------------------------------------------------------------------+
void CProgram::CheckTradeSymbols(string &source_array[],string &checked_array[])
  {
   int symbols_total     =::SymbolsTotal(false);
   int size_source_array =::ArraySize(source_array);
//--- 在总列表中查找指定的品种
   for(int i=0; i<size_source_array; i++)
     {
      for(int s=0; s<symbols_total; s++)
        {
         //--- 获取公共列表中当前品种的名称
         string symbol_name=::SymbolName(s,false);
         //--- 如果匹配
         if(symbol_name==source_array[i])
           {
            //--- 在市场观察中设置一个品种
            ::SymbolSelect(symbol_name,true);
            //--- 添加到确认的品种数组
            int size_array=::ArraySize(checked_array);
            ::ArrayResize(checked_array,size_array+1);
            checked_array[size_array]=symbol_name;
            break;
           }
        }
     }
//--- 如果未检测到品种,则仅使用当前品种
   if(::ArraySize(checked_array)<1)
     {
      ::ArrayResize(checked_array,1);
      checked_array[0]=_Symbol;
     }
  }

CProgram::CheckSymbols() 方法用于读取外部字符串参数中指定的品种。 此处,字符串被切分为一个数组,用 ',' 作为分隔符。 彼此间的间隙都是由此产生的。 之后,数组 将被发送给上面曾研究过的 CProgram::CheckTradeSymbols() 方法进行验证。

class CProgram : public CWndEvents
  {
private:
   //--- 检查来自字符串的交易品种并包含到数组
   int               CheckSymbols(const string symbols_enum);
  };
//+-------------------------------------------------------------------------+
//| 检查来自字符串的交易品种并包含到数组                                           |
//+-------------------------------------------------------------------------+
int CProgram::CheckSymbols(const string symbols_enum)
  {
   if(symbols_enum!="")
      ::Print(__FUNCTION__," > 输入交易品种: ",symbols_enum);
//--- 从字符串中获取品种
   string symbols[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(symbols_enum,u_sep,symbols);
//--- 从两侧剔除空白
   int elements_total=::ArraySize(symbols);
   for(int e=0; e<elements_total; e++)
     {
      ::StringTrimLeft(symbols[e]);
      ::StringTrimRight(symbols[e]);
     }
//--- 检查品种
   ::ArrayFree(m_symbols);
   CheckTradeSymbols(symbols,m_symbols);
//--- 获取交易品种的数量
   return(::ArraySize(m_symbols));
  }

含有交易策略类的文件通过应用程序类连接到文件。 CStrategy-类型的动态数组被创建。 

#include "Strategy.mqh"
//+------------------------------------------------------------------+
//| 用于创建应用程序的类                                                 |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 策略数组
   CStrategy         m_strategy[];
  };

此处,我们在程序初始化过程中从外部参数中获取品种数组和数量。 接着,将策略数组的大小设置为品种数量,然后 初始化所有策略实例,将品种名称传递给每个策略实例。

class CProgram : public CWndEvents
  {
private:
   //--- 品种总数
   int               m_symbols_total;
  };
//+------------------------------------------------------------------+
//| 初始化                                                            |
//+------------------------------------------------------------------+
bool CProgram::OnInitEvent(void)
  {
//--- 获取交易品种
   m_symbols_total=CheckSymbols(Symbols);
//--- 调整数组大小
   ::ArrayResize(m_strategy,m_symbols_total);
//--- 初始化
   for(int i=0; i<m_symbols_total; i++)
     {
      if(!m_strategy[i].OnInitEvent(m_symbols[i]))
         return(false);
     }
//--- 初始化成功
   return(true);
  }

接着,我们研究将最后的测试数据写入文件。


将数据写入文件

我们将把最后的测试数据保存在终端的公共数据文件夹中。 因此,可以从任意 MetaTrader 5 平台访问该文件。 在构造函数里 指定文件夹和文件名:

class CProgram : public CWndEvents
  {
private:
   //--- 最后的测试结果的保存文件路径
   string            m_last_test_report_path;
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_symbols_total(0)
  {
//--- 最后的测试结果的保存文件路径
   m_last_test_report_path=::MQLInfoString(MQL_PROGRAM_NAME)+"\\LastTest.csv";
  }

我们研究用于写入文件的 CProgram::CreateSymbolBalanceReport() 方法。 为了运用这种方法 (以及后面将要研究的另一种方法),我们需要 品种余额数组

//--- 所有品种的余额数组
struct CReportBalance { double m_data[]; };
//+------------------------------------------------------------------+
//| 用于创建应用程序的类                                                 |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 所有品种的余额数组
   CReportBalance    m_symbol_balance[];
   //---
private:
   //--- 创建 CSV 格式的交易测试报告
   void              CreateSymbolBalanceReport(void);
  };
//+------------------------------------------------------------------+
//| 创建 CSV 格式的交易测试报告                                          |
//+------------------------------------------------------------------+
void CProgram::CreateSymbolBalanceReport(void)
  {
   ...
  }

在方法伊始,在终端的公共文件夹中打开文件以便工作 (FILE_COMMON):

...
//--- 在终端公共文件夹中创建一个用于写入数据的文件
   int file_handle=::FileOpen(m_last_test_report_path,FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- 如果句柄有效 (文件已创建/已打开)
   if(file_handle==INVALID_HANDLE)
     {
      ::Print(__FUNCTION__," > 创建文件错误: ",::GetLastError());
      return;
     }
...

一些辅助变量将会形成一些报告参数。 我们将下表中提供的完整历史成交数据写入文件:

  • 成交时间
  • 品种
  • 类型
  • 方向
  • 交易量
  • 价格
  • 掉期利率
  • 结果 (盈利/亏损)
  • 回撤
  • 余额。 此列显示余额总数,而后面的列包含测试中使用的各品种余额

在此, 我们 形成第一行 数据题头:

...
   double max_drawdown    =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";
...

如果涉及多元品种,标题行应补充其名称。 之后,标题 (第一行) 应被写入文件。 

...
//--- 如果涉及多元品种,则补充标题行
   int symbols_total=::ArraySize(m_symbols);
   if(symbols_total>1)
     {
      for(int s=0; s<symbols_total; s++)
         ::StringAdd(headers,delimeter+m_symbols[s]);
     }
//--- 写报告标题
   ::FileWrite(file_handle,headers);
...

接下来,我们会收到整个成交历史记录及其编号,设置数组大小:

...
//--- 获取整个历史
   ::HistorySelect(0,LONG_MAX);
//--- 找出成交数量
   int deals_total=::HistoryDealsTotal();
//--- 通过品种数量设置余额数组的数量
   ::ArrayResize(m_symbol_balance,symbols_total);
//--- 为每个品种设置成交数组的大小
   for(int s=0; s<symbols_total; s++)
      ::ArrayResize(m_symbol_balance[s].m_data,deals_total);
...

在主循环中,传递整个历史记录并形成字符串以便写入文件。 当计算利润时,请考虑掉期利率和佣金。 如果有多元品种,我们在第二个循环中传递它们,并为每个品种形成余额。

数据以字符串形式写入文件。 在该方法结束时关闭该文件。
...
//--- 沿循环移动并写入数据
   for(int i=0; i<deals_total; i++)
     {
      //--- 获得成交单号
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- 找出价格中的小数位数
      int digits=(int)::SymbolInfoInteger(m_deal_info.Symbol(),SYMBOL_DIGITS);
      //--- 计算总余额
      balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
      //--- 形成连接写入的行
      ::StringConcatenate(string_to_write,
                          ::TimeToString(m_deal_info.Time(),TIME_DATE|TIME_MINUTES),delimeter,
                          m_deal_info.Symbol(),delimeter,
                          m_deal_info.TypeDescription(),delimeter,
                          m_deal_info.EntryDescription(),delimeter,
                          ::DoubleToString(m_deal_info.Volume(),2),delimeter,
                          ::DoubleToString(m_deal_info.Price(),digits),delimeter,
                          ::DoubleToString(m_deal_info.Swap(),2),delimeter,
                          ::DoubleToString(m_deal_info.Profit(),2),delimeter,
                          MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                          ::DoubleToString(balance,2));
      //--- 如果有多元品种,请写入其余额值
      if(symbols_total>1)
        {
         //--- 沿着所有品种移动
         for(int s=0; s<symbols_total; s++)
           {
            //--- 如果品种匹配并且成交结果不为零
            if(m_deal_info.Symbol()==m_symbols[s] && m_deal_info.Profit()!=0)
               //--- 显示此品种一笔交易的余额。 考虑掉期利率和佣金
               m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
            //--- 否则,写入之前的值
            else
              {
               //--- 如果是 "资金余额" 交易 (第一笔交易),所有品种的余额是相同的
               if(m_deal_info.DealType()==DEAL_TYPE_BALANCE)
                  m_symbol_balance[s].m_data[i]=balance;
               //--- 否则,将以前的值写入当前索引
               else
                  m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1];
              }
            //--- 将品种余额添加到字符串
            ::StringAdd(string_to_write,delimeter+::DoubleToString(m_symbol_balance[s].m_data[i],2));
           }
        }
      //--- 写入形成的字符串
      ::FileWrite(file_handle,string_to_write);
      //--- 强制将下一个字符串的变量设置为零
      string_to_write="";
     }
//--- 关闭文件
   ::FileClose(file_handle);
...

在形成字符串时 (见下面的代码),使用 CProgram::MaxDrawdownToString() 方法写入文件,以便计算总余额的回撤。 在首次调用时,回撤等于零。 当前余额保存为本地最大值/最小值。 在以下方法调用期间,根据以前的值计算回撤,如果余额超过保存的值,则更新局部最大值。 否则,将更新本地最小值并返回零值 (空字符串)。

class CProgram : public CWndEvents
  {
private:
   //--- 从局部最大值获得最大回撤
   string            MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown);
  };
//+------------------------------------------------------------------+
//| 从局部最大值获得最大回撤                                              |
//+------------------------------------------------------------------+
string CProgram::MaxDrawdownToString(const int deal_number,const 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
        {
         //--- 获得零回撤并更新最小值
         max_drawdown=0.0;
         min=fmin(min,balance);
        }
     }
//--- 为报告定义字符串
   str=(max_drawdown==0)? "" : ::DoubleToString(max_drawdown,2);
   return(str);
  }

文件结构允许在 Excel 中打开它 (请参阅下面的屏幕截图):

 图例 2. 报告文件结构

图例 2. 在 Excel 中的报告文件结构

结果就是,在测试结束时 调用 CProgram::CreateSymbolBalanceReport() 方法准备测试报告:

//+------------------------------------------------------------------+
//| 测试完成事件                                                       |
//+------------------------------------------------------------------+
double CProgram::OnTesterEvent(void)
  {
//--- 只有在测试后才写入报告
   if(::MQLInfoInteger(MQL_TESTER) && !::MQLInfoInteger(MQL_OPTIMIZATION) && 
      !::MQLInfoInteger(MQL_VISUAL_MODE) && !::MQLInfoInteger(MQL_FRAME_MODE))
     {
      //--- 形成报告并写入文件
      CreateSymbolBalanceReport();
     }
//---
   return(0.0);
  }

现在,我们来研究读取报告数据。


从文件中提取数据

毕竟我们上面已经实现了,在测试器中每个策略结束检查后都向文件写入报告。 接下来,我们来研究从报告中读取数据的方法。 首先,我们需要读取文件并将其内容插入到数组中以便处理。 为达此目的,我们使用 CProgram::ReadFileToArray() 方法。 在此,我们打开 EA 测试结束时的成交历史记录文件。 在循环中,读取文件直到最后一个字符串并用源数据填充数组。 

class CProgram : public CWndEvents
  {
private:
   //--- 来自文件数据的数组
   string            m_source_data[];
   //--- 
private:
   //--- 将文件读取到传递的数组
   bool              ReadFileToArray(const int file_handle);
  };
//+------------------------------------------------------------------+
//| 将文件读取到传递的数组                                               |
//+------------------------------------------------------------------+
bool CProgram::ReadFileToArray(const int file_handle)
  {
//--- 打开文件
   int file_handle=::FileOpen(m_last_test_report_path,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- 如果文件尚未打开,则退出
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- 释放数组
   ::ArrayFree(m_source_data);
//--- 将文件读取到数组
   while(!::FileIsEnding(file_handle))
     {
      int size=::ArraySize(m_source_data);
      ::ArrayResize(m_source_data,size+1,RESERVE);
      m_source_data[size]=::FileReadString(file_handle);
     }
//--- 关闭文件
   ::FileClose(file_handle);
   return(true);
  }

我们需要辅助的 CProgram::GetStartIndex() 方法来定义 BALANCE 列索引。 您可以使用 ',' 分隔符和标题字符串作为参数传递给它的动态数组。 在此字符串中,执行搜索列名称。  

class CProgram : public CWndEvents
  {
private:
   //--- 报告中的初始 baLalnce 指数
   bool              GetBalanceIndex(const string headers);
  };
//+------------------------------------------------------------------+
//| 定义开始复制数据的索引                                               |
//+------------------------------------------------------------------+
bool CProgram::GetBalanceIndex(const string headers)
  {
//--- 通过分隔符获取字符串元素
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(headers,u_sep,str_elements);
//--- 搜索 'BALANCE' 列
   int elements_total=::ArraySize(str_elements);
   for(int e=elements_total-1; e>=0; e--)
     {
      string str=str_elements[e];
      ::StringToUpper(str);
      //--- 如果找到含有必要标题的列
      if(str=="BALANCE")
        {
         m_balance_index=e;
         break;
        }
     }
//--- 如果找不到 'BALANCE' 列,则显示消息
   if(m_balance_index==WRONG_VALUE)
     {
      ::Print(__FUNCTION__," > 在报告文件里, 未有 \'BALANCE\' 题头! ");
      return(false);
     }
//--- 成功
   return(true);
  }

成交编号显示在两根图表上的 X 轴。 日期范围将作为额外信息显示在余额图页脚中。 CProgram::GetDateRange() 方法用于定义成交历史的开始和结束日期。 传递给它的两个参数是成交历史的开始和结束日期的引用字符串。

class CProgram : public CWndEvents
  {
private:
   //--- 日期范围
   void              GetDateRange(string &from_date,string &to_date);
  };
//+------------------------------------------------------------------+
//| 获取测试范围的开始和结束日期                                          |
//+------------------------------------------------------------------+
void CProgram::GetDateRange(string &from_date,string &to_date)
  {
//--- 如果少于三个字符串则退出
   int strings_total=::ArraySize(m_source_data);
   if(strings_total<3)
      return;
//--- 获取报告的开始和结束日期
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
//---
   ::StringSplit(m_source_data[1],u_sep,str_elements);
   from_date=str_elements[0];
   ::StringSplit(m_source_data[strings_total-1],u_sep,str_elements);
   to_date=str_elements[0];
  }

CProgram::GetReportDataToArray() 和 CProgram::AddDrawDown() 方法用于获取余额和回撤数据。 第二个调用第一个,它的代码非常短 (见下面的清单)。 此处传递交易指数和回撤值。 将索引和值插入适当的数组中,然后将其值显示在图上。 绘制的值保存在 m_dd_y[],而显示此值的索引保存在 m_dd_x[]。 因此,索引处没有数值则在图上不显示任何内容 (空值)。

class CProgram : public CWndEvents
  {
private:
   //--- 总余额回撤
   double            m_dd_x[];
   double            m_dd_y[];
   //--- 
private:
   //--- 将回撤添加到数组中
   void              AddDrawDown(const int index,const double drawdown);
  };
//+------------------------------------------------------------------+
//| 将回撤添加到数组中                                                   |
//+------------------------------------------------------------------+
void CProgram::AddDrawDown(const int index,const double drawdown)
  {
   int size=::ArraySize(m_dd_y);
   ::ArrayResize(m_dd_y,size+1,RESERVE);
   ::ArrayResize(m_dd_x,size+1,RESERVE);
   m_dd_y[size] =drawdown;
   m_dd_x[size] =(double)index;
  }

首先在 CProgram::GetReportDataToArray() 方法中定义数组大小和余额图的序列数量。 之后 初始化题头数组。 然后,在循环中按分隔符提取字符串元素,并将数据放置到回撤和余额数组中。  

class CProgram : public CWndEvents
  {
private:
   //--- 从报告中获取品种数据
   int               GetReportDataToArray(string &headers[]);
  };
//+------------------------------------------------------------------+
//| 从报告中获取品种数据                                                 |
//+------------------------------------------------------------------+
int CProgram::GetReportDataToArray(string &headers[])
  {
//--- 获取标题字符串元素
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- 数组大小
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- 释放数组
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- 获取序列的数量
   int curves_total=elements_total-m_balance_index;
   curves_total=(curves_total<3)? 1 : curves_total;
//--- 按序列数量设置数组的大小
   ::ArrayResize(headers,curves_total);
   ::ArrayResize(m_symbol_balance,curves_total);
//--- 设置序列的大小
   for(int i=0; i<curves_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,strings_total,RESERVE);
//--- 如果有若干个品种 (接题头)
   if(curves_total>2)
     {
      for(int i=0,e=m_balance_index; e<elements_total; e++,i++)
         headers[i]=str_elements[e];
     }
   else
      headers[0]=str_elements[m_balance_index];
//--- 获取数据
   for(int i=1; i<strings_total; i++)
     {
      ::StringSplit(m_source_data[i],u_sep,str_elements);
      //--- 收集数据至数组
      if(str_elements[m_balance_index-1]!="")
         AddDrawDown(i,double(str_elements[m_balance_index-1]));
      //--- 如果有若干个品种
      if(curves_total>2)
         for(int b=0,e=m_balance_index; e<elements_total; e++,b++)
            m_symbol_balance[b].m_data[i]=double(str_elements[e]);
      else
         m_symbol_balance[0].m_data[i]=double(str_elements[m_balance_index]);
     }
//--- 第一个序列的值
   for(int i=0; i<curves_total; i++)
      m_symbol_balance[i].m_data[0]=(strings_total<2)? 0 : m_symbol_balance[i].m_data[1];
//--- 获取序列的数量
   return(curves_total);
  }

接下来,我们将研究如何在图上显示获得的数据。


在图形上显示数据

上一节中研究的辅助方法在 CProgram::UpdateBalanceGraph() 方法的开始处调用。 然后,从图表中删除当前序列,因为参与上次测试的品种数量可能会发生变化。 之后通过 CProgram::GetReportDataToArray() 方法中定义的当前品种数量在循环中添加新的余额数据序列,并通过 Y 轴定义最小值和最大值。 

在此,我们还会在类字段中记住序列的 大小和 X 轴回撤间距。 这些值也是格式化回撤图所需的值。 针对 Y 轴计算 图表极值点的缩进等于 5%。 结果就是,所有这些值都应用于余额图,并更新图形以便显示近期的变化。 

class CProgram : public CWndEvents
  {
private:
   //--- 序列中的数据总数
   double            m_data_total;
   //--- X 轴上的刻度间距
   double            m_default_step;
   //--- 
private:
   //--- 更新余额图上的数据
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| 更新余额图                                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- 获取测试范围日期
   string from_date=NULL,to_date=NULL;
   GetDateRange(from_date,to_date);
//--- 定义开始复制数据的索引
   if(!GetBalanceIndex(m_source_data[0]))
      return;
//--- 从报告中获取品种数据
   string headers[];
   int curves_total=GetReportDataToArray(headers);

//--- 使用新数据更新所有图表序列
   CColorGenerator m_generator;
   CGraphic *graph=m_graph1.GetGraphicPointer();
//--- 清除图表
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- 图表高点和低点
   double y_max=0.0,y_min=m_symbol_balance[0].m_data[0];
//--- 添加数据
   for(int i=0; i<curves_total; i++)
     {
      //--- 定义 Y 轴的高点/低点
      y_max=::fmax(y_max,m_symbol_balance[i].m_data[::ArrayMaximum(m_symbol_balance[i].m_data)]);
      y_min=::fmin(y_min,m_symbol_balance[i].m_data[::ArrayMinimum(m_symbol_balance[i].m_data)]);
      //--- 将序列添加到图表
      CCurve *curve=graph.CurveAdd(m_symbol_balance[i].m_data,m_generator.Next(),CURVE_LINES,headers[i]);
     }
//--- 数值数量和 X 轴网格步长
   m_data_total   =::ArraySize(m_symbol_balance[0].m_data)-1;
   m_default_step =(m_data_total<10)? 1 : ::MathFloor(m_data_total/5.0);
//--- 范围和缩进
   double range  =::fabs(y_max-y_min);
   double offset =range*0.05;
//--- 第一个序列的颜色
   graph.CurveGetByIndex(0).Color(::ColorToARGB(clrCornflowerBlue));
//--- 横轴属性
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
   x_axis.Name(from_date+" - "+to_date);
//--- 纵轴属性
   CAxis *y_axis=graph.YAxis();
   y_axis.AutoScale(false);
   y_axis.Min(y_min-offset);
   y_axis.Max(y_max+offset);
   y_axis.MaxGrace(0);
   y_axis.MinGrace(0);
   y_axis.DefaultStep(range/10.0);
//--- 更新图形
   graph.CurvePlotAll();
   graph.Update();
  }

CProgram::UpdateDrawdownGraph() 方法用于更新回撤图。 由于数据已在 CProgram::UpdateBalanceGraph() 方法中计算,此处我们仅需应用它们并刷新图形。

class CProgram : public CWndEvents
  {
private:
   //--- 更新回撤图上的数据
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| 更新回撤图                                                         |
//+------------------------------------------------------------------+
void CProgram::UpdateDrawdownGraph(void)
  {
//--- 更新回撤图
   CGraphic *graph=m_graph2.GetGraphicPointer();
   CCurve *curve=graph.CurveGetByIndex(0);
   curve.Update(m_dd_x,m_dd_y);
   curve.PointsFill(false);
   curve.PointsSize(6);
   curve.PointsType(POINT_CIRCLE);
//--- 横轴属性
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
//--- 更新图形
   graph.CalculateMaxMinValues();
   graph.CurvePlotAll();
   graph.Update();
  }

CProgram::UpdateBalanceGraph() 和 CProgram::UpdateDrawdownGraph() 方法在 CProgram::UpdateGraphs() 方法中调用。 在调用它们之前,首先调用 CProgram::ReadFileToArray() 方法。 它从文件里接收 EA 上次测试结果数据。 

class CProgram : public CWndEvents
  {
private:
   //--- 更新最后测试结果图上的数据
   void              UpdateGraphs(void);
  };
//+------------------------------------------------------------------+
//| 更新图形                                                           |
//+------------------------------------------------------------------+
void CProgram::UpdateGraphs(void)
  {
//--- 用文件中的数据填充数组
   if(!ReadFileToArray())
     {
      ::Print(__FUNCTION__," > 无法打开测试结果文件!");
      return;
     }
//--- 刷新余额和回撤图
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

显示获得的结果

要在界面图上显示上次测试的结果,请单击一次按钮。 相应的事件在 CProgram::OnEvent() 方法中处理:

//+------------------------------------------------------------------+
//| 事件处理器                                                         |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 按钮点击事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- 按下 '更新数据'
      if(lparam==m_update_graph.Id())
        {
         //--- 更新图形
         UpdateGraphs();
         return;
        }
      //---
      return;
     }
  }

如果 EA 在点击按钮之前已完成过测试,我们会看到类似这样的内容:

图例 3. EA 的最后测试结果 

图例 3. EA 的最后测试结果

所以,如果 EA 已经上传到图表中,在参数优化后,您可以在查看多个测试结果的同时,立即看到多元品种余额图上的变化。 

交易和测试期间的多元品种余额图

现在,我们来研究第二个 EA 版本,显示并更新交易期间的多元交易品种余额图。 

图形界面与上述版本几乎相同。 仅有的区别是刷新按钮被替换为下拉式日历,允许您指定日期,交易结果将显示在图表上。

我们将在 OnTrade() 方法中检查事件到达后的历史变化。 CProgram::IsLastDealTicket() 方法用于确保新交易已添加到历史记录中。 在此方法中,我们将自上次调用后保存在内存中的时间处获取历史记录。 然后,检查最后一笔成交的单号和存储在内存中的单号。 如果单号不同,更新保存的单号和最后的交易时间 以便进行下一次检查,并获取 "true" 属性,通知历史记录已更改。

class CProgram : public CWndEvents
  {
private:
   //--- 最后更改的成交时间和单号
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //--- 
private:
   //--- 检查新成交
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
  }
//+------------------------------------------------------------------+
//| 获取指定品种的最后成交事件                                            |
//+------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- 如果尚未收到,则退出
   if(!::HistorySelect(m_last_deal_time,LONG_MAX))
      return(false);
//--- 从获取的列表中得到成交数量
   int total_deals=::HistoryDealsTotal();
//--- 从尾至头遍历接收列表中的所有成交
   for(int i=total_deals-1; i>=0; i--)
     {
      //--- 获得成交单号
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- 如果单号相同,退出
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- 如果单号不相同,通知
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- 保存最后一笔成交的时间和单号
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- 另一个品种的单号
   return(false);
  }

在遍历成交历史记录并用数据填充数组之前,我们应该定义历史记录中的品种以及设置数组大小的数字。 为达此目的,我们使用 CProgram::GetHistorySymbols() 方法。 在调用它之前,请选择历史记录的所需范围。 然后,将历史中的品种添加到字符串中。 为确保品种不重复,检查指定的子字符串。 之后,将历史中检测到的品种添加到数组 中并获取品种数量。

class CProgram : public CWndEvents
  {
private:
   //--- 来自历史的品种数组
   string            m_symbols_name[];
   //--- 
private:
   //--- 从帐户历史记录中获取品种并返回它们的数量
   int               GetHistorySymbols(void);
  };
//+------------------------------------------------------------------+
//| 从帐户历史记录中获取品种并返回它们的数量                                 |
//+------------------------------------------------------------------+
int CProgram::GetHistorySymbols(void)
  {
   string check_symbols="";
//--- 首次循环遍历并获得交易的品种
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     {
      //--- 获得成交单号
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- 如果有品种名称
      if(m_deal_info.Symbol()=="")
         continue;
      //--- 如果没有这样的字符串,则添加它
      if(::StringFind(check_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(check_symbols,(check_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
//--- 按分隔符获取字符串元素
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(check_symbols,u_sep,m_symbols_name);
//--- 返回品种的数量
   return(symbols_total);
  }

若要获得多元品种余额,请调用 CProgram::GetHistorySymbolsBalance() 方法:

class CProgram : public CWndEvents
  {
private:
   //--- 分别为每个品种获取余额总数和余额
   void              GetHistorySymbolsBalance(void);
  };
//+------------------------------------------------------------------+
//| 分别为每个品种获取总余额和余额                                         |
//+------------------------------------------------------------------+
void CProgram::GetHistorySymbolsBalance(void)
  {
   ...
  }

在此,我们应该从一开始就获得初始帐户余额。 获取首笔交易的历史。 它将被用作初始余额。 假定可以在日历中指定交易结果开始显示的日期。 因此,请再次选择历史记录。 然后,使用 CProgram::GetHistorySymbols() 方法获取所选历史记录中的品种及其数量。 之后,设置数组的大小。 定义显示历史结果范围的开始日期和结束日期。 

...
//--- 初始存款金额
   ::HistorySelect(0,LONG_MAX);
   double balance=(m_deal_info.SelectByIndex(0))? m_deal_info.Profit() : 0;
//--- 从指定的日期获取历史记录
   ::HistorySelect(m_from_trade.SelectedDate(),LONG_MAX);
//--- 获取品种的数量
   int symbols_total=GetHistorySymbols();
//--- 释放数组
   ::ArrayFree(m_dd_x);
   ::ArrayFree(m_dd_y);
//--- 为总余额设置余额数组的大小为品种数 + 1
   ::ArrayResize(m_symbols_balance,(symbols_total>1)? symbols_total+1 : 1);
//--- 设置每个品种成交数组的大小
   int deals_total=::HistoryDealsTotal();
   for(int s=0; s<=symbols_total; s++)
     {
      if(symbols_total<2 && s>0)
         break;
      //---
      ::ArrayResize(m_symbols_balance[s].m_data,deals_total);
      ::ArrayInitialize(m_symbols_balance[s].m_data,0);
     }
//--- 余额曲线的数量
   int balances_total=::ArraySize(m_symbols_balance);
//--- 历史记录的开始和结束
   m_begin_date =(m_deal_info.SelectByIndex(0))? m_deal_info.Time() : m_from_trade.SelectedDate();
   m_end_date   =(m_deal_info.SelectByIndex(deals_total-1))? m_deal_info.Time() : ::TimeCurrent();
...

品种和回撤余额在下一个循环中计算。 获得的数据被放置到数组中。 前面几节中介绍的方法也可用于计算回撤

...
//--- 最大回撤
   double max_drawdown=0.0;
//--- 将余额数组写入传递的数组
   for(int i=0; i<deals_total; i++)
     {
      //--- 获取成交数组
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- 初始化首笔交易
      if(i==0 && m_deal_info.DealType()==DEAL_TYPE_BALANCE)
         balance=0;
      //--- 从指定日期开始
      if(m_deal_info.Time()>=m_from_trade.SelectedDate())
        {
         //--- 计算总余额
         balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
         m_symbols_balance[0].m_data[i]=balance;
         //--- 计算回撤
         if(MaxDrawdownToString(i,balance,max_drawdown)!="")
            AddDrawDown(i,max_drawdown);
        }
      //--- 如果使用多元品种,则写入品种的余额值
      if(symbols_total<2)
         continue;
      //--- 仅从指定日期
      if(m_deal_info.Time()<m_from_trade.SelectedDate())
         continue;
      //--- 遍历所有品种
      for(int s=1; s<balances_total; s++)
        {
         int prev_i=i-1;
         //--- 如果是 "余额存款" 交易 (首笔交易) ...
         if(prev_i<0 || m_deal_info.DealType()==DEAL_TYPE_BALANCE)
           {
            //--- ... 所有品种的余额是相同的
            m_symbols_balance[s].m_data[i]=balance;
            continue;
           }
         //--- 如果品种相同且交易结果不为零
         if(m_deal_info.Symbol()==m_symbols_name[s-1] && m_deal_info.Profit()!=0)
           {
            //--- 在余额中反映这一品种的成交。 考虑掉期利率和佣金。
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
           }
         //--- 否则,写入之前的值
         else
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i];
        }
     }
...

使用 CProgram::UpdateBalanceGraph() 和 CProgram::UpdateDrawdownGraph() 方法将这些数据添加到图形中。 它们的代码几乎与前面章节中研究的第一个 EA 版本中的代码完全相同,因此我们可以立即调用它们。

首先,在创建图形界面时调用这些方法,以便用户立即看到成交结果。 之后,当在 OnTrade() 方法中接收交易事件时,图表会更新。 

class CProgram : public CWndEvents
  {
private:
   //--- 初始化图形
   void              UpdateBalanceGraph(const bool update=false);
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| 交易操作事件                                                       |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- 更新余额和回撤图
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

另外,在图形界面中,用户可以指定余额图将自哪个日期构建。 若要强制刷新图表而不检查最后一笔成交单号,将 true 传递给 CProgram::UpdateBalanceGraph() 方法。

日历中更改日期 (ON_CHANGE_DATE) 的事件以下列方式处理:

//+------------------------------------------------------------------+
//| 事件处理器                                                         |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 在日历中选择日期的事件
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
     {
      if(lparam==m_from_trade.Id())
        {
         UpdateBalanceGraph(true);
         UpdateDrawdownGraph();
         m_from_trade.ChangeComboBoxCalendarState();
        }
      //---
      return;
     }
  }

下面,您可在测试器中看它如何在可视化模式下工作:

图例 4. 在可视化模式下显示测试器结果

图例 4. 在可视化模式下显示测试器结果

来自信号服务的直观报告

作为对用户的另一个有益补充,我们将创建一款 EA,能够可视化来自 信号 服务报告的交易结果。

进入必要信号的页面并选择 "交易历史":


图例 5. 信号交易历史

下载 CSV 文件的交易历史链接可以在列表下方找到:

 图例 6. 将交易历史导出到 CSV 文件

图例 6. 将交易历史导出到 CSV 文件

对于当前实现的 EA,这些文件应放置到 \MQL5\Files。 在 EA 里添加一个外部参数。 它将显示报告文件的名称,其数据应会在图形上可视化。

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                                版权所有 2018, MetaQuotes 软件公司 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- 外部参数
input string PathToFile=""; // 文件路径
...

图例 7. 用于指定报告文件的外部参数

图例 7. 用于指定报告文件的外部参数

该 EA 版本的图形界面仅包含两个图形。 在终端图表上启动 EA 时,它会尝试打开设置中指定的文件。 如果没有找到这样的文件,程序会在 流水账 中显示一条消息。 这里的方法集合与上述版本大致相同。 有些地方有细微的差别,但主要原则是一样的。 我们只考虑那些已发生了很大变化的方法。

所以,文件已经被读取,并且它的字符串已经被放置到数据源中。 现在,您需要将这些数据分配到一个二维数组中,就像在表格中完成一样。 这对于按照交易开单时间从最早到最后进行数据排序尤为方便。 我们为此需要一个单独的数组。 

//--- 从文件中提取数据
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| 用于创建应用程序的类                                                 |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 报表
   CReportTable      m_columns[];
   //--- 字符串和列的数量
   uint              m_rows_total;
   uint              m_columns_total;
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_rows_total(0),
                           m_columns_total(0)
  {
...
  }

数组排序需要以下方法:

class CProgram : public CWndEvents
  {
private:
   //--- 快速排序方法
   void              QuickSort(uint beg,uint end,uint column);
   //--- 检查排序条件
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
   //--- 在指定单元格中交换数值
   void              Swap(uint r1,uint r2);
  };

所有这些方法都在 之前的一篇文章 中进行了深入讨论。 

所有基本操作都在 CProgram::GetData() 方法中执行。 我们来详尽地讨论它。 

class CProgram : public CWndEvents
  {
private:
   //--- 获取数据到数组
   int               GetData(void);
  };
//+------------------------------------------------------------------+
//| 从报告中获取品种数据                                                 |
//+------------------------------------------------------------------+
int CProgram::GetData(void)
  {
...
  }

首先,我们以 ';' 分隔符来定义字符串和字符串元素的数量。 然后将报告中的品种名称和它们的数量分别放入单独的数组中。 之后,准备数组并填充报告数据。

...
//--- 获取标题字符串元素
   string str_elements[];
   ushort u_sep=::StringGetCharacter(";",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- 字符串和字符串元素的数量
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- 获取品种
   if((m_symbols_total=GetHistorySymbols())==WRONG_VALUE)
     return;
//--- 释放数组
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- 数据序列大小
   ::ArrayResize(m_columns,elements_total);
   for(int i=0; i<elements_total; i++)
      ::ArrayResize(m_columns[i].m_rows,strings_total-1);
//--- 用文件中的数据填充数组
   for(int r=0; r<strings_total-1; r++)
     {
      ::StringSplit(m_source_data[r+1],u_sep,str_elements);
      for(int c=0; c<elements_total; c++)
         m_columns[c].m_rows[r]=str_elements[c];
     }
...

数据排序全部就绪。 这里,我们需要在填充之前 设置品种余额数组 的大小:

...
//--- 序列和列的数量
   m_rows_total    =strings_total-1;
   m_columns_total =elements_total;
//--- 按第一列中的时间排序
   QuickSort(0,m_rows_total-1,0);
//--- 序列大小
   ::ArrayResize(m_symbol_balance,m_symbols_total);
   for(int i=0; i<m_symbols_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,m_rows_total);
...

然后,填写总余额和回撤数组。 所有与追加资金相关的交易都会被跳过

...
//--- 余额和最大回撤
   double balance      =0.0;
   double max_drawdown =0.0;
//--- 获取总余额数据
   for(uint i=0; i<m_rows_total; i++)
     {
      //--- 初始余额
      if(i==0)
        {
         balance+=(double)m_columns[elements_total-1].m_rows[i];
         m_symbol_balance[0].m_data[i]=balance;
        }
      else
        {
         //--- 跳过追加资金
         if(m_columns[1].m_rows[i]=="Balance")
            m_symbol_balance[0].m_data[i]=m_symbol_balance[0].m_data[i-1];
         else
           {
            balance+=(double)m_columns[elements_total-1].m_rows[i]+(double)m_columns[elements_total-2].m_rows[i]+(double)m_columns[elements_total-3].m_rows[i];
            m_symbol_balance[0].m_data[i]=balance;
           }
        }
      //--- 计算回撤
      if(MaxDrawdownToString(i,balance,max_drawdown)!="")
         AddDrawDown(i,max_drawdown);
     }
...

然后为每个品种填写余额数组。 

...
//--- 获取品种余额数据
   for(int s=1; s<m_symbols_total; s++)
     {
      //--- 初始余额
      balance=m_symbol_balance[0].m_data[0];
      m_symbol_balance[s].m_data[0]=balance;
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- 如果品种不匹配,则返回先前的值
         if(m_symbols_name[s]!=m_columns[m_symbol_index].m_rows[r])
           {
            if(r>0)
               m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
            //---
            continue;
           }
         //--- 如果成交结果不为零
         if((double)m_columns[elements_total-1].m_rows[r]!=0)
           {
            balance+=(double)m_columns[elements_total-1].m_rows[r]+(double)m_columns[elements_total-2].m_rows[r]+(double)m_columns[elements_total-3].m_rows[r];
            m_symbol_balance[s].m_data[r]=balance;
           }
         //--- 否则,写入之前的值
         else
            m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
        }
     }
...

之后,数据显示在图形界面的图形上。 下面显示了来自各种信号提供者的几个示例:

 图例 8. 显示结果 (示例 1)

图例 8. 显示结果 (示例 1)

 图例 9. 显示结果 (示例 2)

图例 9. 显示结果 (示例 2)

 图例 10. 显示结果 (示例 3)

图例 10. 显示结果 (示例 3)

 图例 11. 显示结果 (示例 4)

图例 11. 显示结果 (示例 4)

结束语

本文展示了用于查看多元品种余额图的 MQL 应用程序的时尚版本。 以前,您必须使用第三方程序才能获得此结果。 如今,所有功能仅需使用 MQL 即能实现,而无需离开 MetaTrader 5

您可以从下面给出的链接下载文件以便进行测试,并详细研究文章中提供的代码。 每个程序版本都有以下文件结构: 

文件名 注释
MacdSampleMultiSymbols.mq5 自标准发行 MACD Sample 的改编版 EA
Program.mqh 程序类的文件
CreateGUI.mqh 文件实现来自 Program.mqh 文件的程序类方法
Strategy.mqh MACD Sample 策略类的改编文件 (多元品种版本)
FormatString.mqh 字符串格式化辅助函数的文件