MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性
简介
在本文中,我们会修改来自上一篇文章"MQL5 Cookbook: 自定义信息面板上的仓位属性"的EA交易,并且解决以下的问题:
- 在当前交易品种中检查新柱事件;
- 从柱中获取数据;
- 在文件中包含标准库中的交易类;
- 创建一个函数来搜索交易信号;
- 创建一个函数来执行交易操作;
- 在OnTrade()函数中判断交易事件。
事实上,上面提到的每个问题都应该用它们自己的文章解决,但是我觉得这样做只会使语言的学习更加复杂。
我将会使用很简单的例子来向你展示这些特性如何实现。换句话说,实现以上列表中的每个任务都将被放到一个简单而直接的函数中。当我们在本系列未来的文章中产生某个主意时,我们就逐步使这些函数更加复杂,因为需要它们解决手边的任务。
首先,让我们从上一篇文章复制EA交易,因为我们需要它所有的功能。
开发一个EA交易
在我们的文件中,我们以包含标准库中的CTrade 类开始,这个类中有所有执行交易操作必需的函数。开始的时候,我们可以简单使用它,不用看它的内部,我们就这么做。
为了包含这个类,我们需要写如下代码:
//--- 包含标准库中的一个类 #include <Trade/Trade.mqh>您可以把这些代码放到文件的最开始,这样以后很好找,比如放到#define 命令之后。#include 命令表明 Trade.mqh 文件需要从<MetaTrader 5 terminal directory>\MQL5\Include\Trade\目录下读取。可以用同样的方法来包含其他含有函数的任何文件,当项目代码变多并且难以浏览的时候,这是非常有用的。
现在我们需要创建这个类的一个实例来访问它的函数,您可以在类的名称之后写下实例的名字来做到这一点:
//--- 载入类
CTrade trade;
在这个版本的EA交易中,我们准备只使用CTrade类中所有可用函数中的一个交易函数,即用于建仓的 PositionOpen() 函数,它也可以用于对已有持仓进行反向操作,怎样从类中调用这个函数将在这篇文章的晚些时候展示,我们那时会创建一个函数来负责交易操作的执行。
进一步,我们在全局范围内增加两个动态数组,这些数组用于保存柱的数值。
//--- 价格数据数组 double close_price[]; // 收盘价 (柱的收盘价) double open_price[]; // 开盘价 (柱的开盘价)下面,创建一个CheckNewBar() 函数用于程序检查新柱事件,因为交易行为只针对已经完成的柱执行,
下面是带有详细注释的CheckNewBar() 函数的代码:
//+------------------------------------------------------------------+ //| 检查新柱 | //+------------------------------------------------------------------+ bool CheckNewBar() { //--- 用于保存当前柱开启时间的变量 static datetime new_bar=NULL; //--- 用于读取当前柱开启时间的数组 static datetime time_last_bar[1]={0}; //--- 读取当前柱的开启时间 // 如果读取时间出错,打印相关信息 if(CopyTime(_Symbol,Period(),0,1,time_last_bar)==-1) { Print(__FUNCTION__,": 复制柱开启时间出错: "+IntegerToString(GetLastError())+""); } //--- 如果是第一次函数调用 if(new_bar==NULL) { // 设置时间 new_bar=time_last_bar[0]; Print(__FUNCTION__,": 初始化 ["+_Symbol+"][TF: "+TimeframeToString(Period())+"][" +TimeToString(time_last_bar[0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]"); return(false); // 返回 false 并退出 } //--- 如果时间不同了 if(new_bar!=time_last_bar[0]) { new_bar=time_last_bar[0]; // 设置时间并退出 return(true); // 保存时间并返回true } //--- 如果我们到了这一行,说明没有新柱,返回 false return(false); }从以上代码中您可以看到,如果柱是新的,CheckNewBar() 函数返回 true,如果还没有新柱,函数返回 false,使用这种方法您可以在交易/测试时控制局势,只针对已经完成的柱执行交易操作。
在函数的最开始,我们声明了一个静态 变量和一个datetime类型的静态数组,静态局部变量即使在函数退出以后也保持它们的值,在以后的每次函数调用中,这样的局部变量将包含它们在上一次函数调用中的数值。
进一步,请注意CopyTime()函数,它帮助我们在time_last_bar数组中取得最新柱的时间。请在MQL5参考中查阅它的用法规则。
您也许会注意前文中都没有提到的用户定义的 TimeframeToString() 函数,它把时间区段的值转换为字符串,对用户来说更加清楚:
string TimeframeToString(ENUM_TIMEFRAMES timeframe) { string str=""; //--- 如果传入的值不正确,使用当前图表的时间区段 if(timeframe==WRONG_VALUE || timeframe == NULL) timeframe = Period(); switch(timeframe) { case PERIOD_M1 : str="M1"; break; case PERIOD_M2 : str="M2"; break; case PERIOD_M3 : str="M3"; break; case PERIOD_M4 : str="M4"; break; case PERIOD_M5 : str="M5"; break; case PERIOD_M6 : str="M6"; break; case PERIOD_M10 : str="M10"; break; case PERIOD_M12 : str="M12"; break; case PERIOD_M15 : str="M15"; break; case PERIOD_M20 : str="M20"; break; case PERIOD_M30 : str="M30"; break; case PERIOD_H1 : str="H1"; break; case PERIOD_H2 : str="H2"; break; case PERIOD_H3 : str="H3"; break; case PERIOD_H4 : str="H4"; break; case PERIOD_H6 : str="H6"; break; case PERIOD_H8 : str="H8"; break; case PERIOD_H12 : str="H12"; break; case PERIOD_D1 : str="D1"; break; case PERIOD_W1 : str="W1"; break; case PERIOD_MN1 : str="MN1"; break; } //--- return(str); }当我们把所有其他函数写好以后,在本文的晚些时候我们会看到CheckNewBar() 函数怎样被使用。让我们现在看GetBarsData() 函数,从一定数量的柱中取得数据。
//+------------------------------------------------------------------+ //| 取得柱的数值 | //+------------------------------------------------------------------+ void GetBarsData() { //--- 读取数组中数据的柱的数量 int amount=2; //--- 像时间序列一样倒序 ... 3 2 1 0 ArraySetAsSeries(close_price,true); ArraySetAsSeries(open_price,true); //--- 读取柱的收盘价 // 如果获取数值的数量少于所请求的,打印相关信息 if(CopyClose(_Symbol,Period(),0,amount,close_price)<amount) { Print("复制数据到收盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+")!" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } //--- 读取柱的开盘价 // 如果获取数值的数量少于所请求的,打印相关信息 if(CopyOpen(_Symbol,Period(),0,amount,open_price)<amount) { Print("复制数据到开盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+") !" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } }
让我们仔细看一下以上代码,首先,在amount变量上, 我们指定了我们需要取得的柱的数量,然后我们使用ArraySetAsSeries() 函数设置数组的索引顺序,这样最新(当前)柱的值就在数组的0索引中了,打个比方,如果您想在您的计算中使用最新柱的值,以开盘价为例,它可以这样写:open_price[0]. 第二新的柱可以类似地标记为:open_price[1].
取得收盘价以及开盘价的机制和CheckNewBar() 函数中我们取得最新柱时间很类似,只是我们在这种情况下使用CopyClose()和CopyOpen()函数,类似地, CopyHigh()和CopyLow()分别用于取得柱的最高价和最低价。
让我们继续并考虑一个简单的例子来演示怎样判断建仓/平仓的信号。价格数组保存两个柱的数据(当前柱和前一个已经完成的柱),我们会使用已完成柱的数据。
- 当收盘价高于开盘价时(牛市柱形),产生一个买入信号;
- 当收盘价低于开盘价时(熊市柱形),产生一个卖出信号.
实现这些简单条件的代码如下:
//+------------------------------------------------------------------+ //| 判断交易信号 | //+------------------------------------------------------------------+ int GetTradingSignal() { //--- 买入信号 (0) : if(close_price[1]>open_price[1]) return(0); //--- 卖出信号 (1) : if(close_price[1]<open_price[1]) return(1); //--- 没有信号 (3): return(3); }您可以看到,它非常简单,很容易想到怎样使用类似的方式处理更加复杂的条件。如果一个完成的柱是向上的,此函数返回0,如果完成柱向下则返回1,如果由于某种原因没有信号,函数将返回3。
现在我们需要创建一个TradingBlock() 函数来实现交易行为。. 以下是带有详细注释的函数代码:
//+------------------------------------------------------------------+ //| 交易模块 | //+------------------------------------------------------------------+ void TradingBlock() { int signal=-1; // 取得信号的变量 string comment="hello :)"; // 仓位注释 double start_lot=0.1; // 仓位初始交易量 double lot=0.0; // 反向持仓情况下计算仓位的交易量 double ask=0.0; // 买价 double bid=0.0; // 卖价 //--- 取得信号 signal=GetTradingSignal(); //--- 查找已有持仓 pos_open=PositionSelect(_Symbol); //--- 如果是买入信号 if(signal==0) { //--- 取得买价 ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); //--- 如果没有持仓 if(!pos_open) { //--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,start_lot,ask,0,0,comment)) { Print("开启买入仓位失败: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } //--- 如果已有持仓 else { //--- 读取仓位类型 pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- 如果是卖出仓位 if(pos_type==POSITION_TYPE_SELL) { //--- 取得仓位交易量 pos_volume=PositionGetDouble(POSITION_VOLUME); //--- 调整交易量 lot=NormalizeDouble(pos_volume+start_lot,2); //--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,lot,ask,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } //--- return; } //--- 如果是卖出信号 if(signal==1) { //-- 取得卖价 bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); //--- 如果没有持仓 if(!pos_open) { //--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,start_lot,bid,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } //--- 如果已有持仓 else { //--- 读取仓位类型 pos_type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- 如果是买入仓位 if(pos_type==POSITION_TYPE_BUY) { //--- 取得仓位交易量 pos_volume=PositionGetDouble(POSITION_VOLUME); //--- 调整交易量 lot=NormalizeDouble(pos_volume+start_lot,2); //--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,lot,bid,0,0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } //--- return; } }我相信直到开启仓位的地方一切都很清楚,在以上的代码中您可以看到,(trade) 指针之后有一个点,之后是PositionOpen() 方法,这就是您如何从类中调用某个方法,在你输入一个点以后,你会看到一个列表中包含所有的类方法,你所要做的就是从列表中选取所需的方法:
图 1. 调用一个类方法.
在TradingBlock() 函数中主要分两块 - 买入和卖出。紧接判断信号方向之后, 在买入信号情况下我们读取买价,在卖出信号情况下我们读取卖价。
所有在交易订单中使用的价位必须使用NormalizeDouble()函数规范化,否则试图开启或者修改仓位都会引起错误,在计算手数的时候也建议使用这个函数。进一步,请注意止损价位和获利价位参数是零值。更多有关设置交易参数的信息将会在本系列后面一篇文章中提供。
就这样现在所有的用户定义函数都写好了,我们可以按照正确的顺序安排它们了:
//+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 初始化新柱 CheckNewBar(); //--- 取得仓位信息并在面板上更新它们的值 GetPositionProperties(); //--- return(0); } //+------------------------------------------------------------------+ //| EA去初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- 在日志中打印去初始化原因 Print(GetDeinitReasonText(reason)); //--- 当从图表上移除时 if(reason==REASON_REMOVE) //--- 从图表上删除所有有关信息面板的对象 DeleteInfoPanel(); } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- 如果柱不是新的,退出 if(!CheckNewBar()) return; //--- 如果有新柱 else { GetBarsData(); // 取得柱数据 TradingBlock(); // 检查条件并交易 } //--- 读取属性并更新面板上的数值 GetPositionProperties(); }现在只剩下一件事情要考虑了 - 使用OnTrade函数判断交易事件。现在我们只是简要接触一下,给您一个大致的印象。在我们的实例中,我们需要实现如下的场景:当人工开启/关闭/修改仓位时, 在面板上的仓位属性信息列表上的数值需要在操作后立即更新,而不是收到一个新订单以后,为了这个目标,我们只需要增加以下代码:
//+------------------------------------------------------------------+ //| 交易事件 | //+------------------------------------------------------------------+ void OnTrade() { //--- 取得仓位信息并在面板上更新它们的值 GetPositionProperties(); }
基本上,所有事情都已完成,我们可以进行测试了。 策略测试器允许您在可视模式下快速运行测试并发现任何错误,使用策略测试器还有别的好处,您甚至可以在周末或者市场关闭的情况下开发程序。
设置好策略测试器,启用可视化模式并点击开始,EA交易将在策略测试器中开始交易,您将会看到类似下面的图片:
图 2. MetaTrader 5 策略测试器中的可视化模式
在可视化模式下,您可以在任何时候暂停,按F12键继续分步测试,如果您在策略测试器中设置了仅开盘价模式,一步等于一个柱, 而选择每一订单模式,每一步等于一个订单。您也可以控制测试速度。
为了确保信息面板上的数值能够在人工开启/关闭仓位或者增加/修改止损/获利价位时立即更新, EA交易应该在实时下测试。不要等待太久了,我们简单地在1分钟时间周期下运行EA交易,这样交易操作每分钟都执行一次。
除此之外,我还增加了另外一个数组用于信息面板上仓位属性的名字:
// 仓位属性名称数组 string pos_prop_texts[INFOPANEL_SIZE]= { "交易品种 :", "幻数 :", "注释:", "库存费 :", "手续费 :", "开盘价格 :", "当前价格 :", "利润 :", "交易量 :", "止损 :", "获利 :", "时间 :", "编号 :", "类型 :" };在前一篇文章中,我提到我们将需要这个数组来减少SetInfoPanel() 函数的代码,如果您自己还没有实现或者想好,您现在可以看到这是怎样做的,创建有关仓位属性对象列表的新实现方法如下:
//--- 仓位属性名称和数值的列表 for(int i=0; i<INFOPANEL_SIZE; i++) { //--- 属性名称 CreateLabel(0,0,pos_prop_names[i],pos_prop_texts[i],anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[i],2); //--- 属性值 CreateLabel(0,0,pos_prop_values[i],GetPropertyValue(i),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[i],2); }
在SetInfoPanel() 函数的开始, 您可以注意到下面的代码行:
//--- 在可视模式下测试 if(MQL5InfoInteger(MQL5_VISUAL_MODE)) { y_bg=2; y_property=16; }它告诉程序,如果程序当前在可视化模式下测试,信息面板上对象的纵坐标需要被调整,这是因为在策略测试器的可视化模式下做测试时,EA交易的名字不会像实时条件下那样显示在图表的右上角,这样,不必要的缩进就可以被去掉了。
结论
我们现在结束了。在下一篇文章中,我们会集中在设置和修改交易参数上,以下您可以下载EA交易的源代码, PositionPropertiesTesterEN.mq5.
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/642