English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性

MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性

MetaTrader 5示例 | 18 十月 2013, 09:53
2 261 0
Anatoli Kazharski
Anatoli Kazharski

简介

在本文中,我们会修改来自上一篇文章"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. 调用一个类方法.

图 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 策略测试器中的可视化模式

图 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

附加的文件 |
MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误 MQL5 Cookbook: 怎样在设置/修改交易参数时避免错误
作为我们在系列前一篇文章,"MQL Cookbook: 在MetaTrader 5策略测试器中分析仓位属性"中EA交易工作的继续,我们将使用很多有用的函数,以及提高和优化已有的函数来增强它。这一次EA交易有可以在MetaTrader 5策略测试器中优化的外部参数,并且在某些方面组成了一个简单的交易系统。
MQL5 Cookbook: 自定义信息面板上的仓位属性 MQL5 Cookbook: 自定义信息面板上的仓位属性
这一次我们创建一个简单的EA交易,它可以取得当前交易品种的仓位属性并且在人工交易的时候在自定义信息面板上显示它们。信息面板将使用图形对象创建,显示的信息在每当有订单时都会刷新,这将比系列文章的前一篇 - "MQL5 Cookbook: 获取仓位属性"中提到的每次必须人工运行脚本要方便得多。
自定义图形控件。第一部分:创建简单控件 自定义图形控件。第一部分:创建简单控件
本文介绍开发图形控件的一般原则。我们将准备若干用于快速和方便地处理图形对象的工具,分析一个创建用于输入文本或数字的简单控件的例子以及使用该控件的方法。
MQL5 Cookbook: 获取仓位属性 MQL5 Cookbook: 获取仓位属性
在本文中,我们将创建一个脚本来获得所有的仓位属性,并用对话框向用户显示它们。通过运行这个脚本,您可以从外部参数下拉列表的两种模式中选择:只看当前交易品种的仓位属性,或者查看所有交易品种的属性。