外汇EA编写教程:利用MQL5和MQL4开发品种选择及导航应用

总结

经验丰富的交易者很清楚,在交易中最痛苦的事情不是开单和跟踪头寸,而是选择交易类型并找到切入点。

当然,如果你只关注一个或两个品种,那么这不是一个大问题。但是,如果你的交易工具涵盖了数百只股票和数十种外汇交易,可能需要几个小时才能找到正确的切入点。

在本文中,我们将开发一个简化股票搜索的EA。EA将从三个方面提供帮助:

  • 它预先过滤库存并为我们提供合格的清单。
  • 生成的库存清单可以简化导航。
  • 它可以显示决策所需的其他数据。

初始EA模板

最初,我们打算使用MQL5来开发EA。但是,由于许多经纪商仍然不提供MetaTrader 5帐户,因此我们必须在文章末尾重新开发EA,以便它也可用于MetaTrader 4。

因此,让我们先准备一个模板,它几乎与MQL5向导生成的模板相同:

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                                  版权所有 2018, MetaQuotes 软件公司 |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- 创建定时器
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 智能系统逆初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| 定时器函数                                                         |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }
//+------------------------------------------------------------------+

void OnChartEvent(const int id,         // 事件 ID   
                  const long& lparam,   // 长整数型事件参数 
                  const double& dparam, // 双精度型事件参数 
                  const string& sparam) // 字符串型事件参数 
  { 

}

在这个模板中,我们在创建EA的开始注册计时器。我们的计时器一秒钟激活一次。这意味着OnTimeStand标准函数每秒调用一次。

唯一不同于MQL5向导生成的典型模板的字符串是严格的属性。此字符串对于EA在Meta AtRADER 4中正常运行是必需的。由于它对Meta Atter 5没有显著影响,我们将提前添加到模板中。

我们将应用以下标准功能:

  • OnInit:在图表上显示符合我们要求的贸易产品按钮;
  • ondeinit:删除计时器和ea创建的所有图形对象;
  • 定时器用于确定ea在图形对象上创建的点击次数。
  • on chart event:ea为启动图表上创建的图形对象创建一个单击事件响应。

满足我们条件的品种列表将存储在carraystring类型对象中。因此,包含对象描述的MQH文件嵌入到EA中:

#include <Arrays/ArrayString.mqh>

使用图表时,我们还需要图表类型对象。我们还需要包括其定义:

#include <Charts/Chart.mqh>

所有这些都应该从我们的模板开始,在属性字符串块之后,在任何函数之外。

接下来,我们需要确定EA创建的所有按钮的宽度和高度。将这些值设置为常量,并在include字符串块之后指定它们:

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

输入

EA将根据输入进行控制。让我们看看它们,这样我们就可以立即定义将在本文后面实现的功能:

sinput string        delimeter_01="";        // --- 过滤设置 ---
input bool           noSYMBmarketWath=true;  // 是否不在市场观察中隐藏
input bool           noSYMBwithPOS=true;     // 若有持仓则隐藏
input ValueOfSpread  hide_SPREAD=spread_b1;  // 在点差情况下隐藏
input uint           hide_PRICE_HIGH=0;      // 若价格太高则隐藏
input uint           hide_PRICE_LOW=0;       // 若价格太低则隐藏
input bool           hideProhibites=true;    // 若禁止交易则隐藏
input bool           hideClosed=true;        // 若市场收市则隐藏
input StartHour      hide_HOURS=hour_any;    // 若有开放时间则显示
input double         hideATRcents=0.00;      // 如果 ATR 小于设定的美元值则隐藏
sinput string        delimeter_02="";        // --- 图表设置 ---
input bool           addInfoWatch=false;     // 将图表添加到市场观察中
input bool           viewCandle=true;        // 打开烛条图表
input bool           viewVolumes=true;       // 显示逐笔报价交易量
input bool           showInfoSymbol=true;    // 显示走势方向
input bool           showNameSymbol=true;    // 显示品名

我们可以立即注意到两个输入是自定义类型。因此,我们在进入之前添加这些类型的定义。两种自定义类型都是枚举。

ValueFo流传枚举定义了EA所要显示的多样性的点差条件:

enum ValueOfSpread
  {
   spread_no,//无
   spread_b05,// > 0.05%
   spread_b1,// > 0.1% 
   spread_b15,// > 0.15% 
   spread_l15,// < 0.15% 
   spread_l1,// < 0.1% 
  }; 

超过0.1%的价格被认为是增加了。因此,在一个弹簧参数值的情况下的隐藏值为gt;0.1%。但是,如果代理的此类项列表太小,则可以为此参数选择其他值。

starthour枚举包含一些市场开放期间的主要周期列表:

enum StartHour
  {
   hour_any, //任何时间
   hour_9am, // 9 am
   hour_10am,// 10 am 
   hour_4pm, // 4 pm 
   hour_0am, // 午夜
  }; 

上午9点(或任何其他数值)并不意味着只显示在指定时间开具发票的品种。相反,它意味着显示此时要计费的品种(例如,9:05)。

因此,下午4点意味着只显示美国股市16:30开盘的股票。

上午10点主要与俄罗斯和欧洲股市有关。

上午9点是一些指数的开放时间。

最后,午夜是外汇市场的开放时间,因为它全天都在工作。

全局变量

在开始使用标准函数的内容之前,我们只需要声明一系列在整个EA中可见的变量。我们在输入后添加它们:

// 在 EA 创建的所有图形对象名称中添加的前缀:
string exprefix="finder";
// 符合我们条件的品种数组:
CArrayString arrPanel1;
// arrPanel1 数组中当前品种的索引:
int panel1val;
// 用于存储 EA 创建的图表的数组(此刻此处只有一个图表):
CChart charts[];
// 用于存储指向 EA 创建的图表的指针的数组(此刻此处只有一个指针):
long curChartID[];

阐明为什么我们需要对这些变量进行评论。

所有准备工作都已就绪。我们现在准备开始开发EA。但首先,让我们看看结果:

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                                  版权所有 2018, MetaQuotes 软件公司 |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
#property strict

#include <Arrays/ArrayString.mqh>
#include <Charts/Chart.mqh>

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

enum ValueOfSpread
  {
   spread_no,//无
   spread_b05,// > 0.05 %
   spread_b1,// > 0.1 %
   spread_b15,// > 0.15 %
   spread_l15,// < 0.15 %
   spread_l1,// < 0.1 %
  }; 
enum StartHour
  {
   hour_any,//任何时间
   hour_9am,// 9 am
   hour_10am,// 10 am 
   hour_4pm,// 4 pm 
   hour_0am,// 午夜
  }; 

input bool           noSYMBmarketWath=true; // 在市场观察中隐藏品种<
input bool           noSYMBwithPOS=true;    // 若有持仓则隐藏
input ValueOfSpread  hide_SPREAD=spread_b1; // 隐藏有点差品种
input uint           hide_PRICE_HIGH=0;     // 隐藏价格太高品种 (0 - 不隐藏)
input uint           hide_PRICE_LOW=0;      // 隐藏价格太低品种 (0 - 不隐藏)
input bool           hideProhibites=true;   // 隐藏不可交易品种
input StartHour      hide_HOURS=hour_any;   // 显示仅在某时刻开放的品种
input bool           viewCandle=true;       // 打开烛条图表

// 在 EA 创建的所有图形对象名称中添加的前缀:
string exprefix="finder";
// 符合我们条件的品种数组:
CArrayString arrPanel1;
// arrPanel1 数组中当前品种的索引:
int panel1val;
// 用于存储 EA 创建的图表的数组(此刻此处只有一个图表):
CChart charts[];
// 用于存储指向 EA 创建的图表的指针的数组(此刻此处只有一个指针):
long curChartID[];

//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 创建定时器
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
  
//+------------------------------------------------------------------+
//| 智能系统逆初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(reason!=REASON_CHARTCHANGE){
      ObjectsDeleteAll(0, exprefix);
   }
   EventKillTimer();
  }

//+------------------------------------------------------------------+
//| 定时器函数                                                         |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }

void OnChartEvent(const int id,         // 事件 ID   
                  const long& lparam,   // 长整数型事件参数 
                  const double& dparam, // 双精度型事件参数 
                  const string& sparam) // 字符串型事件参数 
  { 

}

品种过滤功能

我们从一个函数开始,该函数显示符合我们标准的变体按钮。让我们命名这个函数开始符号。函数在OnInit函数中调用。结果是OnInit函数的最终形式:

//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   start_symbols();

//--- 创建定时器
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }

如果一切都可以在OnInit中实现,为什么我们需要一个函数?一切都很简单。我们不仅在启动EA时调用此函数,而且在按下R键时调用此函数。因此,我们可以轻松地更新品种列表,而无需从图表中删除EA并重新打开它。

由于变化点的差异,我们必须不时更新品种列表。此外,一些品种的位置也在变化。因此,在再次处理先前启动的EA之前,不要忘记更新品种列表(按R键)以查看当前数据。

让我们看看start_符号的功能。它还可以用作包装器来启动其他功能:

void start_symbols(){
   // 将列表中当前品种的索引设置为零(数组中的第一个品种):
   panel1val=0;
   // 准备品种列表:
   prepare_symbols();
   // 从图表中删除以前创建的品种按钮:
   ObjectsDeleteAll(0, exprefix);
   // 显示品种列表:
   show_symbols();
   // 更新图表来查看变化:
   ChartRedraw(0);
}

我们还遇到了另外两个自定义函数:准备符号和显示符号。首先是形成一系列符合我们要求的品种。第二个显示了在EA中运行的图表上这些品种的按钮。

在图表上显示按钮很容易。首先,我们找到显示按钮的x和y坐标,这样它们就不会与其他按钮重叠。那么我们应该展示一下:

void show_symbols(){
   
   // 初始化定义 X 和 Y 坐标的变量
   int btn_left=0;
   int btn_line=1;
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   
   // 在图表中显示数组中每个品种的按钮
   // 在按钮上书写品种名称
   for( int i=0; i<arrPanel1.Total(); i++ ){
      if( btn_left>btn_right-BTN_WIDTH ){
         btn_line++;
         btn_left=0;
      }
      
      ObjectCreate(0, exprefix+"btn"+(string) i, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_COLOR,clrBlack); 
      ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TEXT,arrPanel1.At(i));    
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false);
      
      
      btn_left+=BTN_WIDTH;
   }
   
}

结果是,符合我们要求的品种将显示在图表上。

在图表上显示品种按钮

现在,我们关注的是品种选择的形成条件(准备符号功能)。首先,我们将所有品种添加到列表中:

void prepare_symbols(){
   // 临时存储品种名称的变量
   string name;
   // 存储最后品种报价的变量
   MqlTick lastme;
   
   // 如果品种数组已包含任何数值,则重置品种数组
   arrPanel1.Resize(0);
   
   // 形成临时 tmpSymbols 数组
   // 它将包含所有可用的品种
   CArrayString tmpSymbols;
   for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){
      tmpSymbols.Add(SymbolName(i, noSYMBmarketWath));
   }
   
   // 条件在这里检查
   // 并且品种将包含在品种列表中
   // 如果它符合条件
   for( int i=0; i<tmpSymbols.Total(); i++ ){
      name=tmpSymbols[i];
      
      // 从品种名称中删除多余的空格,
      // 因为我们不确定它来自何处
      StringTrimLeft(name);
      StringTrimRight(name);
      if( !StringLen(name) ){
         continue;
      }
      
      // 进一步进行品种的主要过滤
      // ...

      
      // 如果品种符合我们的所有条件,则将其添加到列表中
      arrPanel1.Add(name);
   }
}

首先,将所有品种放入临时数组中。此时,市场观察面板参数定义中缺少隐藏符号的初始过滤已经发生。

不需要把品种放在临时数组中。相反,我们可以把必要的品种放在主目录中。但在这种情况下,我们需要重写代码。例如,我们需要添加一个输入,只添加需要显示在列表中的项目。换言之,我们应该按照必要的顺序,用定制品种替换经纪人提供的所有品种。

出于同样的原因,首先,临时数组中的所有品种都从循环中枚举出来。如果要实现上述输入,则不能省略筛选自定义输入。

现在,我们根据已经得到的输入对获得的品种进行排序。下面的代码模块是在主品种过滤器的注释字符串的进一步执行的基础上,根据准备符号功能(在循环中向列表中添加品种)添加的。

隐藏现有交易头寸:

      // 隐藏已有交易持仓的品种
      bool isskip=false;
      if( noSYMBwithPOS ){
         // 查看所有持仓清单
         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // 如果当前品种有持仓,则跳过
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }
      }

首先,检查品种是否有库存。如果没有未平仓合约,请检查是否有限价清单。如果有一个位置或价格限制,跳过品种。

隐藏了一点坏品种:

      // 如果品种当前价格值的输入有效,
      // 尝试获取当前值
      if(hide_PRICE_HIGH>0 || hide_PRICE_LOW>0 || hide_SPREAD>0 ){
         SymbolInfoTick(name, lastme);
         if( lastme.bid==0 ){
            Alert("Failed to get BID value. Some filtration functions may not work.");
         }
      }
      if(hide_SPREAD>0 && lastme.bid>0){
         switch(hide_SPREAD){
            // 如果当前点差超过价格的 0.05%,则跳过该品种
            case spread_b05:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.05 ){
                  isskip=true;
               }
               break;
            // 如果当前点差超过价格的 0.1%,则跳过该品种
            case spread_b1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.1 ){
                  isskip=true;
               }
               break;
            // 如果当前点差超过价格的 0.15%,则跳过该品种
            case spread_b15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.15 ){
                  isskip=true;
               }
               break;
            // 如果当前点差低于价格的 0.15%,则跳过该品种
            case spread_l15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.15 ){
                  isskip=true;
               }
               break;
            // 如果当前点差低于价格的 0.1%,则跳过该品种
            case spread_l1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.1 ){
                  isskip=true;
               }
               break;
         }
      }
      if(isskip){
         continue;
      }

差别越小越好。从这个角度看,使用点差小于0.05%的品种比较好。并非所有的经纪人都能提供这样好的条件,尤其是在股票市场交易时。

隐藏价格太高的品种(0-不隐藏):

      // 隐藏价格太高品种 (0 - 不隐藏)
      if(hide_PRICE_HIGH>0 && lastme.bid>0 && lastme.bid>hide_PRICE_HIGH){
         continue;
      }

隐藏价格太低的品种(0-无隐藏):

      if(hide_PRICE_LOW>0 && lastme.bid>0 && lastme.bid<hide_PRICE_LOW){
         continue;
      }

隐藏的非交易品种:

      if(hideProhibites){
         // 如果品种的最小仓量为 0,则跳过
         if( SymbolInfoDouble(name, SYMBOL_VOLUME_MIN)==0 ) continue;
         // 如果品种禁止开仓,则跳过
         if(SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){
            continue;
         }
      }

当然,可以在没有任何输入条件的情况下隐藏禁止的产品。但你可能仍然想把这些品种包括在名单中。这就是为什么我们添加了这个输入。

只显示在指定时间开放的品种:

      // 在 curDay 变量中获取当前日期
      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      // 如果市场开放时间有限制 
      // 并且我们设法获得了当日当前股票的开放时间,然后......
      if( hide_HOURS!=hour_any && SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

如果市场关闭,就把它藏起来。如果你在星期日启动EA,你可能不想分析股票市场。最有可能的是,你想选择一个星期日品种,如TA25指数或加密货币。这个输入参数允许我们这样做。

当然,它只能显示今天的可交易品种,而不能引入单独的输入。但如果是星期天,我们要为下一个交易日做准备。我们如何选择合适的存货?我们将此函数作为输入参数来实现。

要确定市场今天是否开放,我们需要符号信息集中功能。如果它返回错误,很明显这个品种今天不能交易。为了避免两次调用函数,我们需要重写代码以仅显示在时间段内打开的变量:

      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      
      bool sessionData=SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto);

      // 如果市场今天关闭,则隐藏一个品种
      if( hideClosed && !sessionData ){
         continue;
      }
      
      // 只显示在时段开放的品种
      // 在 curDay 变量中获取当前日期
      // 如果市场开放时间有限定, 
      // 并且我们设法获得了当日当前股票的开放时间,然后......
      if( hide_HOURS!=hour_any && sessionData){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

如果ATR小于设置的美元值,则隐藏。如果你在一天内进行交易,等待价格至少上涨50-90美分,那么你就不可能需要计算每天不超过30美分的品种。此参数允许我们通过指定每日价格趋势所需的最小大小来选择品种:

      // 如果 ATR 小于设定的美元值则隐藏
      if(hideATRcents>0){
         MqlRates rates[];
         ArraySetAsSeries(rates, true);
         double atr;
         if(CopyRates(name, PERIOD_D1, 1, 5, rates)==5){
            atr=0;
            for(int j=0; j<5; j++){
               atr+=rates[j].high-rates[j].low;
            }
            atr/=5;
            if( atr>0 && atr<hideATRcents ){
               continue;
            }
         }
      }

在我看来,一些参数足以完全过滤。但是,如果您需要其他过滤条件,则可以将它们添加到Prepare_symbols函数的循环中。

开放式海图

我们已经学会了如何绘制符合我们要求的各种按钮。我们可以到此为止,手动打开获得的品种图。但是不方便。幸运的是,我们可以简化流程,并在单击按钮时打开必要的图表。

要在单击按钮时执行此操作,应在MQL语言标准函数onChartEvent中进行描述。此函数截取任何图表事件。换句话说,当图表上发生任何事情时,都会调用它。

onchartevent函数有四个输入。第一个参数(id)包含onchartevent函数当前截获的事件id。要真正了解单击图表按钮后如何调用onChartEvent函数,请将参数值与所需值进行比较。

button click事件具有chartevent_object_click id。因此,我们将以下代码添加到onchartevent函数以处理图表上的按钮单击:

void OnChartEvent(const int id,         // 事件 ID   
                  const long& lparam,   // 长整数型事件参数 
                  const double& dparam, // 双精度型事件参数 
                  const string& sparam) // 字符串型事件参数 
  { 
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // 单击按钮时执行的代码
         break;
   }

}

我们如何准确地理解图表上按了哪个按钮?onchartevent函数的第二个参数(sparam)可以帮助我们。对于chartevent_object_click事件,它包含用户单击的按钮的名称。我们只需要将这个名称与EA生成的名称进行比较。如果这是EA按钮,请打开必要的品种表。打开的品种名称取自按钮上的文本。因此,我们在case chartevent_object_click条件中放入以下代码:

         // 如果按钮名称包含由您的 EA 创建的所有图形对象之一,
         // 则...
         if( StringFind(sparam, exprefix+"btn")>=0 ){
            // 放置当前按钮索引
            // (列表中当前品种的位置)至 panel1val 变量
            string tmpme=sparam;
            StringReplace(tmpme, exprefix+"btn", "");
            panel1val=(int) tmpme;
            
            // 打开当前品种的图表
            showcharts(ObjectGetString(0,sparam,OBJPROP_TEXT));
         }

使用ShowCharts自定义功能打开所选物种的图表。此功能不仅可以打开必要的图表,还可以打开:

  • 关闭前由EA打开的图表;
  • 如果市场观察面板上没有,则添加品种。
  • 如有必要,将图表切换到蜡烛模式。
  • 更改图表比例(我添加此功能只是因为我使用自定义比例而不是默认比例)。
void showcharts(string name){
   // 如果图表已经打开,则关闭它们
   closecharts();
   
   // 如果在“市场观察”面板中不存在,则添加该品种
   // 如果 "Add chart to Market Watch" 输入为 'true'
   if( addInfoWatch ){
      SymbolSelect(name, true);
   }
   
   // 打开图表并将图表 ID 放在 curChartID 数组中
   curChartID[ArrayResize(curChartID,ArraySize(curChartID)+1)-1]=charts[(uchar) ArrayResize(charts,ArraySize(charts)+1)-1].Open( name, PERIOD_D1 );
   
   // 如果 "Open candlestick charts" 输入为 'true',
   // 将图表切换到烛条模式
   if(viewCandle){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_MODE, CHART_CANDLES);
   }
   // 如果 "Show tick volumes" 输入为 'true',
   // 显示逐笔报价交易量
   if(viewVolumes){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SHOW_VOLUMES, CHART_VOLUME_TICK);
   }
   // 将图表比例更改为最方便的
   ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SCALE, 2);
      
   // 等待三分之一秒,直到所有更改都得到实施
   Sleep(333);
   // 更新打开的图表以便进行所有更改
   ChartRedraw(curChartID[ArraySize(curChartID)-1]);
   
}

closecharts 函数关闭以前由 EA 打开的所有图表。 它的代码非常简单:

void closecharts(){
   // 如果 EA 已打开图表得数组不为空,那么...
   if(ArraySize(charts)){
      // 持续关闭所有图表
      for( int i=0; i<ArraySize(charts); i++ ){
         charts[i].Close();
      }
      // 清除图表数组
      ArrayFree(charts);
   }
   // 如果 EA 已打开图表的 ID 数组不为空,则清除它
   if(ArraySize(curChartID)){
      ArrayFree(curChartID);
   }
}

显示其他品种信息

它不仅具有打开品种图的优点,而且还可以显示辅助数据,如描述(有些经纪人使用品种名称,难以理解,似乎是某种密码)和品种在最后一天的最后一个小时的方向。

可以使用图形对象显示此信息。但是,我们将使用更简单的方法。我们将在图表注释中显示此信息。

为此,在调用休眠函数之前,将下面的代码添加到StudiCar函数中:

   // 在图表上显示附加信息
   string msg="";
   if(showNameSymbol){
      StringAdd(msg, getmename_symbol(name)+"/r/n");
   }
   if(showInfoSymbol){
      StringAdd(msg, getmeinfo_symbol(name, false)+"/r/n");
   }
   if( StringLen(msg)>0 ){
      ChartSetString(curChartID[ArraySize(curChartID)-1], CHART_COMMENT, msg);
   }

如果SuNoMeSyMyPoT输入为true,则调用GeMeMeNeMuleMulk函数返回其中的品种名称所在的行。如果StfIfSysMyBOL输入为true,则调用GeMeNoFixFielm函数返回该类名称所在的行:

string getmename_symbol(string symname){
   return SymbolInfoString(symname, SYMBOL_DESCRIPTION);
}
string getmeinfo_symbol(string symname, bool show=true){
   MqlRates rates2[];
   ArraySetAsSeries(rates2, true);
   string msg="";

   if(CopyRates(symname, PERIOD_D1, 0, 1, rates2)>0){
      if(show){
         StringAdd(msg, (string) symname+": ");
      }
      StringAdd(msg, "D1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2) +"%");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2) +"%");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   if(CopyRates(symname, PERIOD_H1, 0, 1, rates2)>0){
      StringAdd(msg, ", H1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2)+"% (+"+DoubleToString(rates2[0].close-rates2[0].open, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2)+"% (-"+DoubleToString(rates2[0].open-rates2[0].close, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   
   return msg;
}

因此,我们将在新打开的图表上看到以下信息:

显示附加品种信息

通过键盘管理控制EA

我们仍在onchartevent函数中,因此我们添加一个键响应:

  • 当按下R键时,根据我们的条件过滤的物种列表将更新:
  • 按下X键时,EA将从图表中删除。

事件具有可用于处理击键的ChartEvent_KeyDown ID。密钥代码在前面提到的SPARAM参数中传递。因此,我们只需向开关操作员添加以下条件:

      case CHARTEVENT_KEYDOWN:
         switch((int) sparam){
            case 45: //x
               ExpertRemove();
               break;
            case 19: //r
               start_symbols();
               break;
         }
         break;

如您所见,当按下R键时,我们只需要调用前面创建的start_symbols函数。

添加图表导航

我们不仅已经学会了如何显示商品种类按钮,还了解了如何在点击按钮时打开必要商品种类的图表。我们的实用工具仍然不方便使用。打开品种图后,我们需要手动关闭它,然后单击下一个图表按钮。每次我们需要转移到下一个产品时都应该这样做,这使得操作非常麻烦。让我们添加品种列表导航按钮以打开图表。

我们只需要添加三个按钮:移动到下一个图表,移动到上一个图表,然后关闭图表。

剩下的就是决定如何实现它们。我们可以在创建新图表时直接向ShowCharts功能添加按钮。但是按钮的数量将来可能会增加。创建按钮和其他图形对象可能会减慢图表的打开速度,这与预期相反。

因此,我们将在标准的ontimer函数中创建按钮。我们将定期检查EA是否打开图表,如果图表打开,是否有按钮。如果没有按钮,请创建它们:

void OnTimer()
  {
   // 如果已打开图表 ID 数组包含数值,则...
   uchar tmpCIDcnt=(uchar) ArraySize(curChartID);
   if(tmpCIDcnt>0 ){
      // 如果数组中的最后一个 ID 没有损坏,那么...
      if(curChartID[tmpCIDcnt-1]>0){
         // 如果 ID 对应的图表没有按钮,则创建它们
         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }
      }
   }
   
  }

图表上的按钮是在“创建BTN自定义”功能中创建的。它的代码非常简单:

void createBTNS(long CID){
   ObjectCreate(CID, exprefix+"_p_btn_prev", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YDISTANCE,90); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_prev",OBJPROP_TEXT,"Prev chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_next", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YDISTANCE,65); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_next",OBJPROP_TEXT,"Next chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_close", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YDISTANCE,40); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_close",OBJPROP_TEXT,"Close chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_SELECTABLE,false); 
   
   // 更新图表以便查看已实施的变化
   ChartRedraw(CID);
}

因此,新图表采用以下形式:

品种列表导航按钮

添加按钮响应

到目前为止,添加到图表中的按钮只是装饰。没有任何反应就按压它们。我们向他们展示如何回应新闻界。

不幸的是,标准OnCARTTEVER函数在这一点上无济于事,因为它只响应在启动EA的图表上的事件,并且按钮被添加到新的图表中。

也许还有一些更方便的方法。我想出了一种方法来应对另一张图表上的更改。它涉及到ontimer标准函数。如果图表上有按钮,我们将检查是否按了其中一些按钮。如果是,执行必要的操作。因此,条件如下:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }

…重写如下:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }else{
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_prev",OBJPROP_STATE)==true ){
               prevchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_next",OBJPROP_STATE)==true ){
               nextchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_close",OBJPROP_STATE)==true ){
               closecharts();
               return;
            }
         }

当按下“上一图表”按钮时,将调用“上一图表”函数。按下下一图表按钮时,将调用NextChart函数。当按下“关闭图表”按钮时,将调用上面的“关闭图表”功能。PrevChart和NextChart功能类似:

void nextchart(){
   // 如果品种列表具有下一个品种,则打开其图表
   // 否则,关闭当前图表
   if(arrPanel1.Total()>(panel1val+1)){
      panel1val++;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}
void prevchart(){
   // 如果品种列表包含上一个符号,则打开其图表
   // 否则,关闭当前图表
   if(arrPanel1.Total()>(panel1val-1) && (panel1val-1)>=0){
      panel1val--;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}

结束语

就是这样。正如您所看到的,代码的总体数量并不是很大,但是其优点是显而易见的。在关闭图表之前,我们不需要一次又一次地打开它们。相反,我们可以点击必要的按钮,为我们做一切。

当然,还有更多的方法可以改进我们的EA。但就目前的形式而言,它已经是一个成熟的产品,可以大大简化股票的选择。

将此实用程序移植到MQL4

现在,让我们尝试将实用程序移植到MQL4。令人惊讶的是,我们只需要重写一个代码模块。大约需要五分钟。

首先,在元编辑器4中创建一个新的EA。然后,将mql5 ea的源代码移动到此位置。

编译EA。当然,尝试以错误结束。但结果,我们得到了一个要修复的错误列表。只有三个:

  • “positionstotal”-函数未定义
  • “positionGetSymbol”-函数未定义
  • ‘ordergetticket’-函数未定义

双击第一个错误并移动到相应的EA字符串。

“positionsTotal”-函数未定义。在“准备符号”功能代码的以下模块中检测到错误:

         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // 如果当前品种有持仓,则跳过
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }

mql4和mql5语言之间的显著区别在于处理仓库和订单。因此,我们应该按照以下方式重写代码块,以便EA可以在MetaTrader 4中正常工作:

         int cntMyPos=OrdersTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol() == name ){
               isskip=true;
               break;
            }
         }

因为MQL4位置和订单之间没有区别,所以生成的代码要小得多。

其余的错误将自动修复,因为它们发生在我们修复的代码块中。

本文由MetaQuotes Software Corp.翻译自俄语原文
,网址为https://www.mql5.com/ru/articles/5348。

附加文件下载zip finder.mq5(40.88 kb)find er4.mq4(40.09 kb)finder.ex5(56.21 kb)find er4.ex4(30.97 kb)

 

 


MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

 

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

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

風險提示

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

邁投公眾號

聯繫我們

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

MyFxtops 邁投