下载MetaTrader 5

使用标准库类和Google Chart API 创建信息板

25 九月 2013, 13:57
Евгений
0
1 195

简介

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

1. “标准库”类概述

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

包含所有类代码的文件位于 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

附加的文件 |
dll_sources.zip (1.62 KB)
infoboard-doc.zip (757.48 KB)
infoboard.zip (203.03 KB)
如何很快地制作一个交易机器人 如何很快地制作一个交易机器人

于金融市场中交易存在许多风险,其中就包括最为严重的一种 - 做出错误交易决策的风险。每一位交易者都梦寐以求有一个交易机器人,它能始终保持良好状态,而且不会受制于人类的诸多弱点 - 恐惧、贪婪和没耐心。

采用跟踪止损的赚钱算法 采用跟踪止损的赚钱算法

本文旨在研究带有不同的交易进入和采用跟踪止损的退出的算法盈利能力。待用的条目类型为随机进场与反向进场交易。止损订单用于跟踪止损与跟踪止盈。本文讲述的是年盈利能力约达30%的赚钱算法。

EA 交易中的资金管理函数 EA 交易中的资金管理函数

交易策略的开发主要着重于搜索进入和退出市场的模式以及维持仓位。如果我们能够将某些模式公式化为自动交易规则,则交易者面临计算持仓量、预付款数额等问题,以及在自动模式中维持抵押资金的安全水平以保证未平仓位的问题。在本文中,我们将使用 MQL5 语言构建几个进行这些计算的简单例子。

如果您不是卖家或供应商,要如何从MetaTrader应用商店以及交易信号服务赚钱 如果您不是卖家或供应商,要如何从MetaTrader应用商店以及交易信号服务赚钱

今年夏天我们已经发布了一个新的伙伴计划来帮助MQL5.community成员推销他们的产品和信号。然而,它不仅有利于MetaTrader市 场卖家和付费交易信号供应商,还有利于普通用户。现在,您无需亲自编写市场应用或在MQL5.com出售您信号的订阅赚钱。您只是简单地帮忙推销这些产 品,获得您的利润份额。