外汇EA编写教程:使用标准库类和Google Chart API 创建信息板

简介

为了让 MQL5 语言的程序员更加轻松地编程,设计师们创建了一个“标准库”,其中涵盖了几乎所有的 API MQL5 函数,而且使用它们也更加简单、方便。本文会试着创建一个信息面板,其中包含该标准库所使用的最大数量的类。

1. “标准库”类概述

那么,这个库究竟是什么呢?本网站的“文档”版块如此描述其构成:

  • 基类 CObject
  • 数据类
  • 图形对象类
  • 使用图表类
  • 文件操作类
  • 字符串操作类
  • 使用指标与时间序列类
  • 交易类

包含所有类代码的文件位于 MQL5/Include 文件夹。查看库代码时您就会发现,它只提供类,而不是函数。所以,想要使用它,您必须懂点面向对象编程 (OOP) 知识。

所有的库类(交易类除外)都源自 CObject 基类。为了展示,我们会试着构造一个 类图,因为我们已拥有其所要求的一切 – 基类及其继承子类。因为 MQL5 语言基本上就是 C++ 的一个子集,所以我们采用 IBM Rational Rose 工具,为 C++ 项目提供逆向工程工具,为图解的自动构造提供工具。

图 1. “标准库”类图解

我们不会显示类属性与方法,因为那样会让图表繁琐笨重。我们还会忽略聚合,因为它们对我们不重要。结果,我们只剩下了泛化(继承),借此我们可以查找类获得了哪些属性和方法。

从图表中可以看出,使用行、文件、图表、图形对象和数组的每一个库组件,都拥有自己的基类(分别为 CString、 CFile、 CChart、 CChartObject 及 CArray) – 皆继承自 CObject。使用指标 CIndicator 及其 CIndicators 辅类的基类继承自 CArrayObj,而对指标缓冲区类 CIndicatorBuffer 的访问权限则继承自 CArrayDouble。

图表中标深红色表明在实际的类、指标、数组和 ChartObjects 中不存在 – 它们都是集,其中包含使用指标、数组和图形对象的类。因为数量庞大,它们又都继承自一个父类,所以,我考虑将其适当简化,从而让图表不那么乱。比如说,“指标”包含 CiDEMA、 CiStdDev 等

还值得一提的是,亦可利用 Doxygen 文档系统的自动创建构造类图。从某种程度上讲,在该系统中执行要比 Rational Rose (可视化建模工具)中简单一些。有关 Doxygen 的更多详情,请见 MQL5 代码自动生成文档。

2. 问题

我们试着创建一个包含最大数量“标准库”类的信息表。

面板会显示哪些内容?有点类似 MetaTrader 5 的详情报告,即:

图 2. 详情报告的外观

图 2. 详情报告的外观

我们可以看出,此报告会呈现一个余额图和一些交易数据。有关计算此类指标的方法的更多详情,请见《专家测试报告中的数字有何含义》一文。

因为此面板仅作信息用途,不执行任何交易操作,所以最好将其实现为某独立窗口中的一个指标,以避免关闭实际图表。而且,将其放入一个子窗口中还方便缩放,甚至只要一个简单的鼠标动作就可以关闭面板。

您可能还想要利用一个饼图来补充此报告,用其来展示在此工具上完成的、相对于交易总数的交易数量。

3. 设计界面

我们已经界定了自己的目标 – 我们需要主图表的子窗口中有一份详情报告。

我们将自己的信息面板作为一个类实现。开始吧:

//+------------------------------------------------------------------+
///面板类
//+------------------------------------------------------------------+
class Board
  {
//被保护的数据
protected:
///存储表格的子窗口编号
   int               wnd;             
///成交数量数组   
   CArrayObj        *Data;
///余额数据数组   
   CArrayDouble      ChartData;       
///界面元素数组   
   CChartObjectEdit  cells[10][6];    
///图表的工作对象   
   CChart            Chart;           
///余额图表的工作对象
   CChartObjectBmpLabel BalanceChart; 
///饼图的工作对象
   CChartObjectBmpLabel PieChart;     
///饼图的数据
   PieData          *pie_data;
//私有数据和方法
private:
   double            net_profit;      //这些变量将存储计算后的特征值 
   double            gross_profit;
   double            gross_loss;
   double            profit_factor;
   double            expected_payoff;
   double            absolute_drawdown;
   double            maximal_drawdown;
   double            maximal_drawdown_pp;
   double            relative_drawdown;
   double            relative_drawdown_pp;
   int               total;
   int               short_positions;
   double            short_positions_won;
   int               long_positions;
   double            long_positions_won;
   int               profit_trades;
   double            profit_trades_pp;
   int               loss_trades;
   double            loss_trades_pp;
   double            largest_profit_trade;
   double            largest_loss_trade;
   double            average_profit_trade;
   double            average_loss_trade;
   int               maximum_consecutive_wins;
   double            maximum_consecutive_wins_usd;
   int               maximum_consecutive_losses;
   double            maximum_consecutive_losses_usd;
   int               maximum_consecutive_profit;
   double            maximum_consecutive_profit_usd;
   int               maximum_consecutive_loss;
   double            maximum_consecutive_loss_usd;
   int               average_consecutive_wins;
   int               average_consecutive_losses;

   ///获取成交数据和余额的方法
   void              GetData();

   ///计算特征值的方法
   void              Calculate();
   
   ///构建图表的方法
   void              GetChart(int X_size,int Y_size,string request,string file_name);
   
   ///请求谷歌图表API的方法
   string            CreateGoogleRequest(int X_size,int Y_size,bool type);
   
  ///获取最佳字体大小的方法
   int               GetFontSize(int x,int y);
   string            colors[12];  //文本显示颜色的数组
//公共方法
public:
///构造函数
   void              Board();    
///析构函数         
   void             ~Board();    
///更新面板的方法
   void              Refresh();  
///创建界面元素的方法/   
   void              CreateInterface(); 
  };

受保护的类数据为界面元素以及交易、余额图和饼图数据(PieData 类将于下文讨论)。交易指标及一些方法属于私有。说它们私有,是因为用户没有直接访问它们的权限,它们只于类中计算,而且只能通过调用相应的公共方法才能计算。

界面创建与指标计算亦为私有方法,因为您在这里需要忍耐严格的方法调用顺序。比如说,没有用于计算的数据就不可能计算指标,不事先创建就不可能有要更新的界面。因此,我们可不会让用户“搬起石头砸自己的脚”。

我们立即来处理某个类的构造函数与析构函数,这样一来,我们一会就不必再返回来了:

//+------------------------------------------------------------------+
///构造函数
//+------------------------------------------------------------------+
void Board::Board()
  {
   Chart.Attach();                               //将当前图表加到类的实例中
   wnd=ChartWindowFind(Chart.ChartId(),"IT");    //查找指标窗口
   Data = new CArrayObj;                         //创建CArrayObj类的实例
   pie_data=new PieData;                         //创建PieData类的实例
   //填充颜色数组
   colors[0]="003366"; colors[1]="00FF66"; colors[2]="990066";
   colors[3]="FFFF33"; colors[4]="FF0099"; colors[5]="CC00FF";
   colors[6]="990000"; colors[7]="3300CC"; colors[8]="000033";
   colors[9]="FFCCFF"; colors[10]="CC6633"; colors[11]="FF0000";
  }
//+------------------------------------------------------------------+
///析构函数
//+------------------------------------------------------------------+
void Board::~Board()
  {
   if(CheckPointer(Data)!=POINTER_INVALID) delete Data;   //删除交易数据
   if(CheckPointer(pie_data)!=POINTER_INVALID) delete pie_data;
   ChartData.Shutdown();    //删除余额数据
   Chart.Detach();          //从图表中卸载
   for(int i=0;i<10;i++)    //删除所有界面元素
      for(int j=0;j<6;j++)
         cells[i][j].Delete();
   BalanceChart.Delete();   //删除余额图表
   PieChart.Delete();       //删除饼图表
  }

在构造函数中,我们会在 Attach() 方法的帮助下,将某 CChart 类型对象关联到当前图表。而于析构函数中调用的 Detach() 方法则会解除图表与对象的关联。作为 CArrayObj 类型对象指针的数据对象,会接收利用 new 操作动态创建的对象地址,并利用 delete 操作符于析构函数中移除。不要忘记在删除之前利用 CheckPointer() 检查有无对象存在,否则会出现错误。

有关 CArrayObj 类的更多详情,我们后文还有详述。同其它类一样,CArrayDouble 类的 Shutdown() 方法亦继承自 CArray 类 (参见类图解),会清理和释放被对象占用内存。CChartObject 类继承子类的 Delete() 方法会由图表中移除对象。

由此,构造函数分配内存、析构函数则释放内存,并移除由此类创建的图形对象。

现在,我们来处理界面。前面讲过,CreateInterface() 方法会创建一个面板界面:

//+------------------------------------------------------------------+
///CreateInterface函数
//+------------------------------------------------------------------+
void Board::CreateInterface()
  {
   //取指标窗口的宽度
   int x_size=Chart.WidthInPixels();
   //以及高度
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   
    //计算余额图会占据多少空间
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   if(Chart_ratio<100)//如果余额图占据了整个表格
     {
      for(int i=0;i<10;i++)//创建列
        {
         for(int j=0;j<6;j++)//创建行
           {
            cells[i][j].Create(Chart.ChartId(),"InfBoard "+IntegerToString(i)+" "+IntegerToString(j),
                               wnd,j*(x_size/6.0),i*(chart_border/10.0),x_size/6.0,chart_border/10.0);
            //设置可选属性为false
            cells[i][j].Selectable(false);
            //设置文本为只读
            cells[i][j].ReadOnly(true);
            //设置字体大小
            cells[i][j].FontSize(GetFontSize(x_size/6.0, chart_border/10.0));
            cells[i][j].Font("Arial");    //字体名称
            cells[i][j].Color(text_color);//字体颜色
           }
        }
     }

   if(Chart_ratio>0)//如果需要余额图
     {
      //创建余额图
      BalanceChart.Create(Chart.ChartId(), "InfBoard chart", wnd, 0, chart_border);
      //设置可选属性为false
      BalanceChart.Selectable(false);
      //创建饼图
      PieChart.Create(Chart.ChartId(), "InfBoard pie_chart", wnd, x_size*0.75, chart_border);
      PieChart.Selectable(false);//设置可选属性为false
     }

   Refresh();//刷新面板
  }

想要紧凑布置所有元素,首先,利用 CChart 类的 WidthInPixels() 与 GetInteger() 方法,查明面板所放置的指标子窗口的长度和宽度。然后我们创建包含指标值的单元格 – 利用 CChartObjectEdit 类的 Create() 方法(创建 “input field”),所有继承子类都拥有 CChartObject 的这种方法。

使用标准库执行此类型的操作有多方便,大家注意到没有?如果没有它,我们必须利用 ObjectCreate 函数创建每一个对象,并利用 ObjectSet 之类的函数设置各对象的属性,进而导致代码冗余。而且,当我们之后想要更改对象的属性时,就必须得仔细监管对象的名称才能避免混淆。现在我们可以轻松地创建一个图形对象数组,并可随时根据需要全盘查看。

此外,我们还可以利用一个函数获取/设置对象的属性 – 只要其为类的重载创造者,比如 CChartObject 类的 Color() 方法。如果利用其设置它们的参数调用或无参数 – 它会返回对象颜色。将饼图放在余额图旁边,会占用整个屏幕宽度的四分之一。

Refresh method() 会更新面板。更新都包括哪些内容呢?我们需要算出指标的总数,将其输入图形对象中,如果其所在的窗口尺寸已有改变,则还要重新调整面板的大小。此面板应占据窗口的全部闲余空间。

//+------------------------------------------------------------------+
///面板更新函数
//+------------------------------------------------------------------+
void Board::Refresh()
  {
   //检查服务器链接状态
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)) {Alert("No connection with the trading server!"); return;}
   //检查是否允许从动态链接库中引入函数
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) {Alert("DLLs are prohibited!"); return;}
   //计算特征值
   Calculate();
   //取指标窗口的宽度
   int x_size=Chart.WidthInPixels();
   //以及高度
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   //计算余额图会占据多大空间
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   string captions[10][6]= //界面元素名称数组
     {
        {"Total Net Profit:"," ","Gross Profit:"," ","Gross Loss:"," "},
        {"Profit Factor:"," ","Expected Payoff:"," ","",""},
        {"Absolute Drawdown:"," ","Maximal Drawdown:"," ","Relative Drawdown:"," "},
        {"Total Trades:"," ","Short Positions (won %):"," ","Long Positions (won %):"," "},
        {"","","Profit Trades (% of total):"," ","Loss trades (% of total):"," "},
        {"Largest","","profit trade:"," ","loss trade:"," "},
        {"Average","","profit trade:"," ","loss trade:"," "},
        {"Maximum","","consecutive wins ($):"," ","consecutive losses ($):"," "},
        {"Maximal","","consecutive profit (count):"," ","consecutive loss (count):"," "},
        {"Average","","consecutive wins:"," ","consecutive losses:"," "}
     };

   //将计算所得值存入数组
   captions[0][1]=DoubleToString(net_profit, 2);
   captions[0][3]=DoubleToString(gross_profit, 2);
   captions[0][5]=DoubleToString(gross_loss, 2);

   captions[1][1]=DoubleToString(profit_factor, 2);
   captions[1][3]=DoubleToString(expected_payoff, 2);

   captions[2][1]=DoubleToString(absolute_drawdown, 2);
   captions[2][3]=DoubleToString(maximal_drawdown, 2)+"("+DoubleToString(maximal_drawdown_pp, 2)+"%)";
   captions[2][5]=DoubleToString(relative_drawdown_pp, 2)+"%("+DoubleToString(relative_drawdown, 2)+")";

   captions[3][1]=IntegerToString(total);
   captions[3][3]=IntegerToString(short_positions)+"("+DoubleToString(short_positions_won, 2)+"%)";
   captions[3][5]=IntegerToString(long_positions)+"("+DoubleToString(long_positions_won, 2)+"%)";

   captions[4][3]=IntegerToString(profit_trades)+"("+DoubleToString(profit_trades_pp, 2)+"%)";
   captions[4][5]=IntegerToString(loss_trades)+"("+DoubleToString(loss_trades_pp, 2)+"%)";

   captions[5][3]=DoubleToString(largest_profit_trade, 2);
   captions[5][5]=DoubleToString(largest_loss_trade, 2);

   captions[6][3]=DoubleToString(average_profit_trade, 2);
   captions[6][5]=DoubleToString(average_loss_trade, 2);

   captions[7][3]=IntegerToString(maximum_consecutive_wins)+"("+DoubleToString(maximum_consecutive_wins_usd, 2)+")";
   captions[7][5]=IntegerToString(maximum_consecutive_losses)+"("+DoubleToString(maximum_consecutive_losses_usd, 2)+")";

   captions[8][3]=DoubleToString(maximum_consecutive_profit_usd, 2)+"("+IntegerToString(maximum_consecutive_profit)+")";
   captions[8][5]=DoubleToString(maximum_consecutive_loss_usd, 2)+"("+IntegerToString(maximum_consecutive_loss)+")";

   captions[9][3]=IntegerToString(average_consecutive_wins);
   captions[9][5]=IntegerToString(average_consecutive_losses);

   if(Chart_ratio<100) //如果余额图没有占据整个表格
     {
      for(int i=0;i<10;i++) //遍历界面元素
        {
         for(int j=0;j<6;j++)
           {
            //确定位置
            cells[i][j].X_Distance(j*(x_size/6.0));
            cells[i][j].Y_Distance(i*(chart_border/10.0));
            //大小
            cells[i][j].X_Size(x_size/6.0);
            cells[i][j].Y_Size(chart_border/10.0);
            //文本
            cells[i][j].SetString(OBJPROP_TEXT,captions[i][j]);
            //字体大小
            cells[i][j].FontSize(GetFontSize(x_size/6.0,chart_border/10.0));
           }
        }
     }

   if(Chart_ratio>0)//如果需要余额图
     {
      //刷新余额图
      int X=x_size*0.75,Y=y_size-chart_border;
      //获取图表
      GetChart(X,Y,CreateGoogleRequest(X,Y,true),"board_balance_chart");
      //设置它的位置
      BalanceChart.Y_Distance(chart_border);
      //确定文件名
      BalanceChart.BmpFileOn("board_balance_chart.bmp");
      BalanceChart.BmpFileOff("board_balance_chart.bmp");
      //刷新饼图
      X=x_size*0.25;
      //获取图表
      GetChart(X,Y,CreateGoogleRequest(X,Y,false),"pie_chart");
      //设置新位置
      PieChart.Y_Distance(chart_border);
      PieChart.X_Distance(x_size*0.75);
      //确定文件名
      PieChart.BmpFileOn("pie_chart.bmp");
      PieChart.BmpFileOff("pie_chart.bmp");
     }

   ChartRedraw(); //重绘图表
  }

代码量非常大,类似于 CreateInterface() 方法,首先用 Calculate() 函数计算指标,然后将其输入图形对象,同时利用 X_Size() 与 Y_Size() 方法将对象尺寸调节为适应窗口大小。X_Distance 与 Y_Distance 方法会改变对象的位置。

多多注意 GetFontSize() 函数,它会选择一种字号,让文本不会在重新调整之后“溢出”窗口边界,并且相反情况下也不会太小。

我们更近距离地来研究一下此函数:

//引入动态链接库函数,度量字符串
#import "String_Metrics.dll" 
void GetStringMetrics(int font_size,int &X,int &Y);
#import

//+------------------------------------------------------------------+
///确定最佳字体大小的函数
//+------------------------------------------------------------------+
int Board::GetFontSize(int x,int y)
  {
   int res=8;
   for(int i=15;i>=1;i--)//遍历不同的字体大小
     {
      int X,Y; //这里我们输入坐标线
      //确定坐标
      GetStringMetrics(i,X,Y);
      //如果坐标线适合已设定的边界,返回字体大小
      if(X<=x && Y<=y) return i;
     }
   return res;
  }

上面说过,GetStringMetrics() 函数由 DLL 导入,其代码可于档案 DLL_Sources.zip 中找到,必要时可以修改。我觉得如果您选择在项目中自行设计界面,它迟早有用。

我们已经完成了用户界面,现在开始着手交易指标的计算。

4. 交易指标的计算

Calculate() 方法会执行计算。

但我们还需要 GetData() 方法来接收必要数据:

//+------------------------------------------------------------------+
///接收成交和余额数据的函数
//+------------------------------------------------------------------+
void Board::GetData()
  {
   //删除旧数据
   Data.Shutdown();
   ChartData.Shutdown();
   pie_data.Shutdown();
   //准备所有成交历史
   HistorySelect(0,TimeCurrent()); 
   CAccountInfo acc_inf;   //对帐户进行操作的对象
   //计算余额
   double balance=acc_inf.Balance();
   double store=0; //余额
   long_positions=0;
   short_positions=0;
   long_positions_won=0;
   short_positions_won=0;
   for(int i=0;i<HistoryDealsTotal();i++) //遍历所有成交历史

     {
      CDealInfo deal;  //成交信息存储在此
      deal.Ticket(HistoryDealGetTicket(i));//获取成交单号
      //如果交易产生结果(退出市场)
      if(deal.Ticket()>=0 && deal.Entry()==DEAL_ENTRY_OUT)
        {
         pie_data.Add(deal.Symbol()); //向饼图中添加数据
         //检查交易品种
         if(!For_all_symbols && deal.Symbol()!=Symbol()) continue;
         double profit=deal.Profit(); //获取交易利润
         profit+=deal.Swap();         //库存费
         profit+=deal.Commission();   //手续费
         store+=profit;               //累积利润
         Data.Add(new CArrayDouble);  //将新的元素添加到数组
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(profit);  //以及数据
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(deal.Type());
        }
     }

   //计算初始入金
   double initial_deposit=(balance-store);
   for(int i=0;i<Data.Total();i++) //遍历交易
     {
      //计算余额
      initial_deposit+=((CArrayDouble *)Data.At(i)).At(0);
      ChartData.Add(initial_deposit); //存入数组中
     }
  }

首先,我们来看存储数据的方法。标准库会提供数据结构类,它们能让您避免使用数组。我们需要一个二维数组,其中会存储有关利润及历史交易类型的数据。但“标准库”不会提供组织二维数组的显式类,却有 CArrayDouble (双精度数据类型数组)和 CArrayObj 类(指向 CObject 类实例及其继承子类的指针动态数组)。即,我们可以创建一个双精度数组的数组,而这正是我们所做的。

当然, ((CArrayDouble *) Data.At (Data.Total () – 1 )).Add (profit) 之类的语句没有 data [i] [j] = profit 简洁,但这只是表面现象。毕竟,在只是简单地声明一个数组、且未使用标准库类的情况下,我们就不能享用诸如内置内存管理器、插入一个不同的数组的能力、对比数组、查找项目之类的好处。因此,存储器组织类的使用让我们无需控制数组的溢出,并为我们提供了许多有用的工具。

CArray 类的 Total() 方法(参见图 1)会返回数组中的元素数量,Add() 方法会添加它们,而 At() 方法则返回元素。

因为我们决定构建一个饼图来显示交易品种的交易量,所以我们需要收集必要的数据。

我们会编写一个辅类以收集此数据:

//+------------------------------------------------------------------+
///饼图表类
//+------------------------------------------------------------------+
class PieData
  {
protected:
///每个交易品种的交易笔数
   CArrayInt         val;   
///交易品种
   CArrayString      symb;  
public:
///删除数据
   bool Shutdown()          
     {
      bool res=true;
      res&=val.Shutdown();
      res&=symb.Shutdown();
      return res;
     }
///在数组中查找字符串
   int Search(string str)   
     {  //检查所有数组元素
      for(int i=0;i<symb.Total();i++)
         if(symb.At(i)==str) return i;
      return -1;
     }
///添加新数据
   void Add(string str)    
     {
      int symb_pos=Search(str);//确定数组中交易品种的位置
      if(symb_pos>-1)
         val.Update(symb_pos,val.At(symb_pos)+1);//更新交易数据
      else //如果没有找到
        {
         symb.Add(str); //添加
         val.Add(1);
        }
     }

   int Total() const {return symb.Total();}
   int Get_val(int pos) const {return val.At(pos);}
   string Get_symb(int pos) const {return symb.At(pos);}
  };

标准库类也并不能够始终为我们提供必要的工作方法。本例中,CArrayString 类的 Search() 方法就不适用,因为想要应用它,我们必须首先分拣数组,而这样又会违反数据结构。因此我们必须得编写自己的方法。

交易特性的计算采用 Calculate() 方法实现:

//+------------------------------------------------------------------+ 
///计算特征值
//+------------------------------------------------------------------+
void Board::Calculate()
  {
   //获取数据
   GetData();
   //置零
   gross_profit=0;
   gross_loss=0;
   net_profit=0;
   profit_factor=0;
   expected_payoff=0;
   absolute_drawdown=0;
   maximal_drawdown_pp=0;
   maximal_drawdown=0;
   relative_drawdown=0;
   relative_drawdown_pp=0;
   total=Data.Total();
   long_positions=0;
   long_positions_won=0;
   short_positions=0;
   short_positions_won=0;
   profit_trades=0;
   profit_trades_pp=0;
   loss_trades=0;
   loss_trades_pp=0;
   largest_profit_trade=0;
   largest_loss_trade=0;
   average_profit_trade=0;
   average_loss_trade=0;
   maximum_consecutive_wins=0;
   maximum_consecutive_wins_usd=0;
   maximum_consecutive_losses=0;
   maximum_consecutive_losses_usd=0;
   maximum_consecutive_profit=0;
   maximum_consecutive_profit_usd=0;
   maximum_consecutive_loss=0;
   maximum_consecutive_loss_usd=0;
   average_consecutive_wins=0;
   average_consecutive_losses=0;

   if(total==0) return; //如果没有成交记录,函数返回
   double max_peak=0,min_peak=0,tmp_balance=0;
   int max_peak_pos=0,min_peak_pos=0;
   int max_cons_wins=0,max_cons_losses=0;
   double max_cons_wins_usd=0,max_cons_losses_usd=0;
   int avg_win=0,avg_loss=0,avg_win_cnt=0,avg_loss_cnt=0;

   for(int i=0; i<total; i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0); //获取利润
      int deal_type=((CArrayDouble *)Data.At(i)).At(1); //以及成交类型
      switch(deal_type) //检查成交类型
        {
         //计算买入持仓和卖出持仓的数量
         case DEAL_TYPE_BUY: {long_positions++; if(profit>=0) long_positions_won++; break;}
         case DEAL_TYPE_SELL: {short_positions++; if(profit>=0) short_positions_won++; break;}
        }

      if(profit>=0)//如果是获利的
        {
         gross_profit+=profit; //净利润
         profit_trades++;      //获利交易的数量
         //最大获利交易和最大连续获利笔数
         if(profit>largest_profit_trade) largest_profit_trade=profit;

         if(maximum_consecutive_losses<max_cons_losses || 
            (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
           {
            maximum_consecutive_losses=max_cons_losses;
            maximum_consecutive_losses_usd=max_cons_losses_usd;
           }
         if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
            (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
           {
            maximum_consecutive_loss=max_cons_losses;
            maximum_consecutive_loss_usd=max_cons_losses_usd;
           }
         //均笔获利
         if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
         max_cons_losses=0;
         max_cons_losses_usd=0;
         max_cons_wins++;
         max_cons_wins_usd+=profit;
        }
      else //亏损的交易
        {
         gross_loss-=profit; //累积利润
         loss_trades++;      //亏损的成交笔数
         //最大亏损交易和最大连续亏损笔数
         if(profit<largest_loss_trade) largest_loss_trade=profit;
         if(maximum_consecutive_wins<max_cons_wins || 
            (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
           {
            maximum_consecutive_wins=max_cons_wins;
            maximum_consecutive_wins_usd=max_cons_wins_usd;
           }
         if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
            (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
           {
            maximum_consecutive_profit=max_cons_wins;
            maximum_consecutive_profit_usd=max_cons_wins_usd;
           }
         //均笔亏损
         if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
         max_cons_wins=0;
         max_cons_wins_usd=0;
         max_cons_losses++;
         max_cons_losses_usd+=profit;
        }

      tmp_balance+=profit; //计算绝对亏损
      if(tmp_balance>max_peak) {max_peak=tmp_balance; max_peak_pos=i;}
      if(tmp_balance<min_peak) {min_peak=tmp_balance; min_peak_pos=i;}
      if((max_peak-min_peak)>maximal_drawdown && min_peak_pos>max_peak_pos) maximal_drawdown=max_peak-min_peak;
     }
   //计算最大跌幅
   double min_peak_rel=max_peak;
   tmp_balance=0;
   for(int i=max_peak_pos;i<total;i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0);
      tmp_balance+=profit;
      if(tmp_balance<min_peak_rel) min_peak_rel=tmp_balance;
     }
   //计算相对跌幅
   relative_drawdown=max_peak-min_peak_rel;
   //净利润
   net_profit=gross_profit-gross_loss;
   //利润因子
   profit_factor=(gross_loss!=0) ?  gross_profit/gross_loss : gross_profit;
   //期望回报
   expected_payoff=net_profit/total;
   double initial_deposit=AccountInfoDouble(ACCOUNT_BALANCE)-net_profit;
   absolute_drawdown=MathAbs(min_peak); 
   //跌幅
   maximal_drawdown_pp=(initial_deposit!=0) ?(maximal_drawdown/initial_deposit)*100.0 : 0;
   relative_drawdown_pp=((max_peak+initial_deposit)!=0) ?(relative_drawdown/(max_peak+initial_deposit))*100.0 : 0;
   
   //盈利和亏损交易的占比
   profit_trades_pp=((double)profit_trades/total)*100.0;
   loss_trades_pp=((double)loss_trades/total)*100.0;
   
   //每笔平均盈利和每笔平均亏损
   average_profit_trade=(profit_trades>0) ? gross_profit/profit_trades : 0;
   average_loss_trade=(loss_trades>0) ? gross_loss/loss_trades : 0;
   
   //最大连续亏损
   if(maximum_consecutive_losses<max_cons_losses || 
      (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
     {
      maximum_consecutive_losses=max_cons_losses;
      maximum_consecutive_losses_usd=max_cons_losses_usd;
     }
   if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
      (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
     {
      maximum_consecutive_loss=max_cons_losses;
      maximum_consecutive_loss_usd=max_cons_losses_usd;
     }

   if(maximum_consecutive_wins<max_cons_wins || 
      (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
     {
      maximum_consecutive_wins=max_cons_wins;
      maximum_consecutive_wins_usd=max_cons_wins_usd;
     }
   if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
      (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
     {
      maximum_consecutive_profit=max_cons_wins;
      maximum_consecutive_profit_usd=max_cons_wins_usd;
     }
   //平均亏损和盈利
   if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
   if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
   average_consecutive_wins=(avg_win_cnt>0) ? round((double)avg_win/avg_win_cnt) : 0;
   average_consecutive_losses=(avg_loss_cnt>0) ? round((double)avg_loss/avg_loss_cnt) : 0;
   
   //获利的买入和卖出持仓数量
   long_positions_won=(long_positions>0) ?((double)long_positions_won/long_positions)*100.0 : 0;
   short_positions_won=(short_positions>0) ?((double)short_positions_won/short_positions)*100.0 : 0;
  }

5. 利用 Google Chart API 创建一个余额图

Google Chart API 允许开发人员即时创建各种类型的图解。Google Chart API 存储于指向 Google 网络服务器资源的链接 (URL),如果接收到一个格式正确的链接 (URL),就会以图像形式返回图解。

图解属性(颜色、标题、轴、图表上的点位等)均由此链接 (URL) 指定。生成的图像可存储于文件系统或数据库中。最令人高兴的是,Google Chart API 完全免费,无需创建账户或是经历注册流程。

GetChart() 方法会从 Google 接收图表并将其保存到磁盘:

#import "PNG_to_BMP.dll"//引入动态链接库函数,将png转换为bmp
bool Convert_PNG(string src,string dst);
#import

#import "wininet.dll"//引入动态链接库函数,实现和网络相关的功能
int InternetAttemptConnect(int x);
int InternetOpenW(string sAgent,int lAccessType,
                  string sProxyName="",string sProxyBypass="",
                  int lFlags=0);
int InternetOpenUrlW(int hInternetSession,string sUrl,
                     string sHeaders="",int lHeadersLength=0,
                     int lFlags=0,int lContext=0);
int InternetReadFile(int hFile,char &sBuffer[],int lNumBytesToRead,
                     int &lNumberOfBytesRead[]);
int InternetCloseHandle(int hInet);
#import

//+------------------------------------------------------------------+
///创建余额图的函数
//+------------------------------------------------------------------+
void Board::GetChart(int X_size,int Y_size,string request,string file_name)
  {
   if(X_size<1 || Y_size<1) return; //太小
   //尝试创建连接
   int rv=InternetAttemptConnect(0);
   if(rv!=0) {Alert("Error in call of the InternetAttemptConnect()"); return;}
   //初始化结构体
   int hInternetSession=InternetOpenW("Microsoft Internet Explorer", 0, "", "", 0);
   if(hInternetSession<=0) {Alert("Error in call of the InternetOpenW()"); return;}
   //发送请求
   int hURL=InternetOpenUrlW(hInternetSession, request, "", 0, 0, 0);
   if(hURL<=0) Alert("Error in call of the InternetOpenUrlW()");
   //记录结果的文件
   CFileBin chart_file;
   //让我们创建它
   chart_file.Open(file_name+".png",FILE_BIN|FILE_WRITE);
   int dwBytesRead[1]; //读取数据的数量
   char readed[1000];  //数据
   //读取请求后由服务器返回的数据
   while(InternetReadFile(hURL,readed,1000,dwBytesRead))
     {
      if(dwBytesRead[0]<=0) break; //若没有数据,退出
      chart_file.WriteCharArray(readed,0,dwBytesRead[0]); //将数据写入文件
     }
   InternetCloseHandle(hInternetSession);//关闭连接
   chart_file.Close();//关闭文件
   //******************************
   //为转换设置文件路径
   CString src;
   src.Assign(TerminalInfoString(TERMINAL_PATH));
   src.Append("/MQL5/Files//"+file_name+".png");
   src.Replace("//","////");
   CString dst;
   dst.Assign(TerminalInfoString(TERMINAL_PATH));
   dst.Append("/MQL5/Images//"+file_name+".bmp");
   dst.Replace("//","////");
   //转换文件
   if(!Convert_PNG(src.Str(),dst.Str())) Alert("Error in call of the Convert_PNG()");
  }
  

有关使用 API Windows 与 MQL5 在线工具的更多详情,请参阅《利用 WinInet.dll 通过 Internet 实现各终端间的数据交换》一文。于此我不再赘述。导入的函数 Convert_PNG() 是我编写的,目的是将 PNG 图像转换为 BMP。

此举完全必要,因为 Google Chart 会以 PNG 或 GIF 格式返回图表,但 “graphic label” 对象只接受 BMP 图像。PNG_to_BMP.dll 库函数所对应的代码,均载于档案 DLL_Sources.zip。

此函数还展示了几个利用标准库操作行与文件的示例。CString 类方法允许执行与字符串函数相同的操作。 CFile 类是 CFileBin 与 CFileTxt 类的基础。在它们的帮助下,我们可以分别生成阅读与记录的二进制与文本文件:方法与使用文件的函数类似。

最后,我们来讲讲 CreateGoogleRequest () 函数 – 它会由余额图上的数据创建查询:

//+------------------------------------------------------------------+
///创建谷歌图表服务器请求的函数
//+------------------------------------------------------------------+
string Board::CreateGoogleRequest(int X_size,int Y_size,bool type)
  {
   if(X_size>1000) X_size=1000; //检查图表大小
   if(Y_size>1000) Y_size=300;  //确保不会太大
   if(X_size<1) X_size=1;       //或太小//s18>
   if(Y_size<1) Y_size=1;
   if(X_size*Y_size>300000) {X_size=1000; Y_size=300;}//并且适合该区域
   CString res; //结果字符串
   if(type) //创建余额图请求
     {
      //准备请求体
      res.Assign("http://chart.apis.google.com/chart?cht=lc&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<ChartData.Total();i++)
         res.Append(DoubleToString(ChartData.At(i),2)+",");
      res.TrimRight(",");
      //数组排序
      ChartData.Sort();
      res.Append("&chxt=x,r&chxr=0,0,");
      res.Append(IntegerToString(ChartData.Total()));
      res.Append("|1,");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
      res.Append("&chg=10,10&chds=");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
     }
   else //创建饼图请求
     {
      //准备请求体
      res.Assign("http://chart.apis.google.com/chart?cht=p3&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(IntegerToString(pie_data.Get_val(i))+",");
      res.TrimRight(",");
      res.Append("&chdl=");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(pie_data.Get_symb(i)+"|");
      res.TrimRight("|");
      res.Append("&chco=");
      int cnt=0;
      for(int i=0;i<pie_data.Total();i++)
        {
         if(cnt>11) cnt=0;
         res.Append(colors[cnt]+"|");
         cnt++;
        }
      res.TrimRight("|");
     }
   return res.Str(); //返回结果
  }

注意:余额图与饼图的要求已分别收集完毕。Append() 方法会向现有行的末尾添加另一行,而 TrimRight() 方法则允许您移除该行末尾显示的多余字符。

6. 最终汇编与测试

类已准备就绪,我们来测试一下。从 OnInit () 指标开始吧:

Board *tablo;   //面板对象指针
int prev_x_size=0,prev_y_size=0,prev_deals=0;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓存映射
   //设置指标简称
   IndicatorSetString(INDICATOR_SHORTNAME,"IT");
   //加载计时器
   EventSetTimer(1); 
   //创建对象实例
   tablo=new Board;
   //以及界面
   tablo.CreateInterface(); 
   prev_deals=HistoryDealsTotal(); //交易数
   //当前窗口大小
   prev_x_size=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); 
   prev_y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
//---
   return(0);
  }

我们于此动态创建 Board 类实例,启动计时器,初始化辅变量。

我们马上置入OnDeinit()函数,并由此移除(自动调用析构函数的)对象,再令计时器停止:

void OnDeinit(const int reason)
{
   EventKillTimer(); //停止计时器
   delete table;    //删除面板
}

OnCalculate() 函数会一个订单号一个订单号地监测新交易的流动性,下述情况下还会更新显示:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   //准备历史数据
   HistorySelect(0,TimeCurrent());
   int deals=HistoryDealsTotal();
   //如果成交笔数变化则更新面板
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
//--- 返回prev_calculated的值用于下一次调用
   return(rates_total);
  }

OnTimer() 函数会监测窗口大小的变化,必要时可以自定义显示屏大小,如果订单号每秒少于一个,则亦监测 OnCalculate() 之类的交易。

void OnTimer()
  {
   int x_size=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //如果窗口大小改变则更新面板
   if(x_size!=prev_x_size || y_size!=prev_y_size) tablo.Refresh();
   prev_x_size=x_size;
   prev_y_size=y_size;
   //如果成交笔数变化则更新面板
   HistorySelect(0,TimeCurrent());
   int deals=HistoryDealsTotal();
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
  }

编译并运行此指标:

图 3. 此表的最终视图

图 3. 此表的最终视图

总结

亲爱的读者朋友,我希望您能通过阅读本文学到一些新东西。我努力在您面前展示这个作为“标准库”的奇妙工具的所有可能性,因为它会提供便利、速度以及优质性能。当然,您需要懂一点 OOP。

祝您好运!

先将 MQL5.rar 档案解压缩到终端文件夹,并允许使用 DLL。DLL_Sources.zip 档案文件中包含库 String_Metrics.dll PNG_to_BMP.dll 的源代码,它们都是我在一个已配 GDI 的 Borland C++ Builder 环境中编写的。

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

附加的文件 |

下载ZIP
dll_sources.zip
(1.62 KB)
infoboard-doc.zip
(757.48 KB)
infoboard.zip
(203.03 KB)

 

 


MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!

 

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

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

風險提示

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

邁投公眾號

聯繫我們

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

MyFxtops 邁投