外汇EA编写教程: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. 调用一个类方法.

图 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 Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/642

附加的文件 |

下载ZIP
positionpropertiestesteren.mq5
(90.22 KB)

 

 


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

 

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

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

風險提示

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

邁投公眾號

聯繫我們

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

MyFxtops 邁投