简介
为了让 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. 详情报告的外观
我们可以看出,此报告会呈现一个余额图和一些交易数据。有关计算此类指标的方法的更多详情,请见《专家测试报告中的数字有何含义》一文。
因为此面板仅作信息用途,不执行任何交易操作,所以最好将其实现为某独立窗口中的一个指标,以避免关闭实际图表。而且,将其放入一个子窗口中还方便缩放,甚至只要一个简单的鼠标动作就可以关闭面板。
您可能还想要利用一个饼图来补充此报告,用其来展示在此工具上完成的、相对于交易总数的交易数量。
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. 此表的最终视图
总结
亲爱的读者朋友,我希望您能通过阅读本文学到一些新东西。我努力在您面前展示这个作为“标准库”的奇妙工具的所有可能性,因为它会提供便利、速度以及优质性能。当然,您需要懂一点 OOP。
祝您好运!
先将 MQL5.rar 档案解压缩到终端文件夹,并允许使用 DLL。DLL_Sources.zip 档案文件中包含库 String_Metrics.dll PNG_to_BMP.dll 的源代码,它们都是我在一个已配 GDI 的 Borland C++ Builder 环境中编写的。