同步多个相同交易品种而时段不同的图表

Dmitriy Gizlyk | 12 六月, 2018


简介

从以前到现今,交易者在做出交易决定时会在不同时段分析图表,我想,你们中的很多人已经对这种情形相当熟悉,就是显示全局趋势的对象是应用于更高时段的图表中的,随后,再在较低时段分析对象看价格行为,在这种分析中,之前创建的对象可能会有改变。现有的 MetaTrader 5 工具可以使您在一个图表上修改时段而同时保留所应用的对象,来执行这种工作。. 但是如果您需要同时在多个图表中跟踪价格的话又该怎么办呢?

您可以为此使用模板,然而,就算修改一个对象,您也需要重新保存模板并把它应用到所有的图表中。在本文中,我提出一种自动化这个过程的方法,并且把同步图标的功能放置在一个指标中。


1. 设定任务

我们指标的主要任务就是同步在 MetaTrader 5 中打开的图表,该程序应当通过一个交易品种名称来定义所需的图表。同时,程序应当持续监控所有所需图表中全部图形对象的状态,每当一个图表中有一个对象发生变化的时候,程序应当把变化复制到其他图表中。

图形对象的状态可以使用两种方法跟踪,第一种方法: 定义周期性,即检查和同步所有应用对象的时间段。这种方法的优点是我们只需要在每个图表中带有程序的一个实例,但是有两个问题:

  • 因为有更新间隔,会造成同步的延迟;
  • 很难确定哪个对象的状态应当作为最新状态。

乍一看来,这两个问题都可以通过增加同步频率,然后把最后同步的对象数据保存到程序变量或者磁盘文件中来简单地加以解决,但是随着图表中对象的数量增加,这样在每个循环中执行所花费的时间以及存储的数据量都会增加。与 EA 和脚本程序不同,指标是在 MetaTrader 的总流程中运行的,所以,指标中有过多负载的话可能会引起其他指标执行的效率,并且终端也是作为一个整体。

第二种方法是通过终端事件来进行对象变化的跟踪和对象的同步,就在 OnChartEvent 函数中处理它们。这种方法可以使程序在创建或者修改对象后立刻做出反应,这样可以最小化延迟。所以,我们不需要保存所有同步的对象,也不需要定时检查所有图表中它们的状态,这样明显减少了程序的负载。

看起来第二个选项对我们非常完美,但是,它也有一个小缺点:OnChartEvent 函数只能在载入程序的图表中调用,如果我们能够定义主图表来管理全部计算的话,这样没什么问题,这种情况下只要一个指标实例就足够了。但是我们不想被局限在一个图表上修改对象,我们就需要在每个图表中运行指标实例。这项工作可以人工完成,也可以自动做,这要感谢 ChartIndicatorAdd 函数。

考虑到上面这些因素,我选择了第二个选项来实现程序。这样,指标的操作可以被分成两大块,

  1. 当指标载入时,打开的图表按照交易品种排序,检查对应交易品种打开的图表中是否已经有这个指标,当前图表的所有对象克隆到选中的图表中。
  2. 初始化部分图表

  3. 处理图表事件. 当创建或者修改图形对象的时候,程序读取被修改对象的数据,并且把它传递到所有之前构建的列表中。

事件处理

当程序在运行时,用户可以打开和关闭图表,所以,为了维护适当的图表列表,我建议使用计时器在一定时间间隔内运行第一个过程。


2. 操作图表的处理

第一个过程是把指标克隆到图表上,为了解决这个问题,我们创建了 CCloneIndy 类。我们把交易品种和指标的名称,以及调用指标的路径保存到它的变量中,这个类有一个公有函数 (SearchCharts) 用于选择所需的图表,这个函数读取一个源图表的 ID 而返回选中图表的数组。

class CCloneIndy
  {
private:
   string            s_Symbol;
   string            s_IndyName;
   string            s_IndyPath;

public:
                     CCloneIndy();
                    ~CCloneIndy();
   bool              SearchCharts(long chart,long &charts[]);

protected:
   bool              AddChartToArray(const long chart,long &charts[]);
   bool              AddIndicator(const long master_chart,const long slave_chart);
  };

当初始化类时,把源数据保存到变量中并为指标设置一个短名称,我们将需要它来取得载入指标拷贝所需的参数。

CCloneIndy::CCloneIndy()
  {
   s_Symbol=_Symbol;
   s_IndyName=MQLInfoString(MQL_PROGRAM_NAME);
   s_IndyPath=MQLInfoString(MQL_PROGRAM_PATH);
   int pos=StringFind(s_IndyPath,"\\Indicators\\",0);
   if(pos>=0)
     {
      pos+=12;
      s_IndyPath=StringSubstr(s_IndyPath,pos);
     }
   IndicatorSetString(INDICATOR_SHORTNAME,s_IndyName);
  }

2.1. 用于根据交易品种名称选择图表的函数

让我们详细探讨用于选择所需图表的函数是如何工作的,首先,我们将在函数参数中检查主图表的 ID。如果它是无效的,函数会立即返回 ‘false’。如果没有设置 ID,就把当前图表的 ID 赋给参数。然后检查保存的交易品种名称是否与主图表相对应,如果不是,重写交易品种名称来对图表排序。

bool CCloneIndy::SearchCharts(long chart,long &charts[])
  {
   switch((int)chart)
     {
      case -1:
        return false;
        break;
      case 0:
        chart=ChartID();
        break;
      default:
        if(s_Symbol!=ChartSymbol(chart))
           s_Symbol=ChartSymbol(chart);
        break;
     }

下一步,在所有打开的图表中迭代。如过所验证的图表的 ID 等于主图表ID,就转到下面一个。

   long check_chart=ChartFirst();
   while(check_chart!=-1)
     {
      if(check_chart==chart)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

然后检查是否锁验证图表的交易品种与我们在寻找的部分一致,如果交易品种名称没有满足搜索,就转到下个图表。

      if(ChartSymbol(check_chart)!=s_Symbol)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

在那之后,检查所验证的图表上是否存在指标,如果是,就把图表ID保存到数组中。

      int handl=ChartIndicatorGet(check_chart,0,s_IndyName);
      if(handl!=INVALID_HANDLE)
        {
         AddChartToArray(check_chart,charts);
         check_chart=ChartNext(check_chart);
         continue;
        }

否则,运行指标调用函数,并且设置主和部分图表的 ID,如果不成功,就转到下一个图表中。将会在下一次函数调用中尝试把指标附加到之前的图表中。

      if(!AddIndicator(chart,check_chart))
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

如果指标成功附加到图表,就把图表 ID 加到数组中。运行把主图表上所有对象克隆到已验证图表中的函数。

      AddChartToArray(check_chart, charts);
      check_chart=ChartNext(check_chart);
     }
//---
   return true;
  }

循环结束后,就退出函数并返回 'true'.

2.2. 调用指标的函数

让我们探讨一下把指标绑定到图表的函数。这个函数的参数是主图表和接受图表的 ID,它们的有效性会在函数的开始处做检查: ID应该是有效地,并且不能相同。

bool CCloneIndy::AddIndicator(const long master_chart,const long slave_chart)
  {
   if(master_chart<0 || slave_chart<=0 || master_chart==slave_chart)
      return false;

返回主图表中的指标句柄。如果是无效的,就退出函数并在结果中返回 'false' 。

   int master_handle=ChartIndicatorGet(master_chart,0,s_IndyName);
   if(master_handle==INVALID_HANDLE)
      return false;

如果句柄是有效的,我们就得到了用于在新图表上运行类似指标的参数。如果没有得到参数,就退出函数并在结果中返回 'false'。

   MqlParam params[];
   ENUM_INDICATOR type;
   if(IndicatorParameters(master_handle,type,params)<0)
      return false;

下一步,写下指标初始化时保存的,调用的路径,而指标要被处理的图表的 ID 写到第二个位置,定义接收图表的时段并调用指标。如果指标调用失败,就退出函数并返回 'false' 的结果。

   params[0].string_value=s_IndyPath;
   params[1].integer_value=slave_chart;
   ENUM_TIMEFRAMES Timeframe=ChartPeriod(slave_chart);
   int slave_handle=IndicatorCreate(s_Symbol,Timeframe,type,ArraySize(params),params);
   if(slave_handle<0)
      return false;

为了完成函数,通过得到的句柄把指标加到接受图表中。

   return ChartIndicatorAdd(slave_chart,0,slave_handle);
  }

您可能会问,为什么我们不能立刻通过主图表的句柄来把指标立即附加到接收图表中,答案很简单:指标和图表可能在交易品种和时段上并不匹配。根据我们的任务,图表上的时段将是不同的。

您可以分析附件中的类源代码。

3. 用于操作图形对象的类

我们程序的下一个过程就是处理事件以及把关于图形对象的数据传递到其他图表中。在写代码之前,我们应当定义数据怎样在图表之间传递。

MQL5 工具可以使程序在一个图表中使用函数根据图表 ID 来在其他图表中创建和修改对象,就可以操作图形对象,这在图表和图形对象数量较少的时候是合适的,

但是还有另外一个选项。之前,我们决定使用事件来跟踪图表中对象的变化,我们甚至写了代码来在所有感兴趣的图表中添加指标,我们可以使用事件模型来把改变的对象的数据传给其他不同图表中的指标。这项操作对象的任务就交给图表中的指标了,这使我们可以把操作对象的工作分布到所有的指标中,从而创建一种异步模式。

这个计划看起来不错,但是我们知道 OnChartEvent 函数只有四个参数:

  • 事件 ID;
  • 长整数型事件参数;
  • 双精度型事件参数;
  • 字符串型事件参数.

怎样把所有有关对象的数据放到这四个参数中呢?我们将简单传递一个事件ID,而所有关于对象的信息都写到字符串类型的参数中,我们将使用文章 "使用云存储服务来在终端之间交换数据" 中的方法来把有关对象的数据收集到一个字符串类型的变量中。

让我们创建 CCloneObjects 类来收集图形对象的数据并在图表上显示它们。

class CCloneObjects
  {
private:
   string            HLineToString(long chart, string name, int part);
   string            VLineToString(long chart, string name, int part);
   string            TrendToString(long chart, string name, int part);
   string            RectangleToString(long chart, string name, int part);
   bool              CopySettingsToObject(long chart,string name,string &settings[]);

public:
                     CCloneObjects();
                    ~CCloneObjects();
//---
   string            CreateMessage(long chart, string name, int part);
   bool              DrawObjects(long chart, string message);
  };

这个类中函数的操作 已经详细介绍过: 我想这里就不需要重复这些描述了。但是,要注意这一点: 当使用 EventChartCustom 函数来生成自定义事件时,'sparam' 参数的长度被限制在63个字符之内,所以,当您把相关对象的数据传递到其他图表时,我们要把消息分成两个部分,为此,消息创建函数要有设置所需部分数据的参数,用于收集一条趋势线数据的函数代码作为实例如下.

string CCloneObjects::TrendToString(long chart,string name, int part)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   switch(part)
     {
      case 0:
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,0))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=1="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,1),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=1="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,1))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_LEFT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_LEFT))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_RIGHT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_RIGHT))+"|";
        break;
      default:
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"="+ObjectGetString(chart,name,OBJPROP_TEXT)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"="+ObjectGetString(chart,name,OBJPROP_TOOLTIP);
        break;
     }
   return result;
  }

所有函数的代码都在下面的附件中。


4. 汇总为指标

一切都准备好了,现在,是时候汇总我们的指标来跟踪和复制图形对象了,指标将只有一个参数 — 图表 ID.

sinput long    Chart =  0;

当运行指标时,参数值总是等于0,您可能会问,为什么要包含一个从来不变的参数呢?

当从程序中在其他图表中调用指标的时候,它的值就变了。

ChartID 函数返回调用指标的图表的 ID(而不是它被附加到的图表),这样做是因为在 MetaTrader 5 中处理指标有一些特点。如果一个指标以相同的交易品种和时段调用了多次,它只会被载入一次 - 在第一次调用时。所有随后对它的调用 (即使是从其它图表) 也会被定向到已经载入的指标中,然后,指标在它的图表中工作,再返回有关它的信息。这样,当您在 CCloneIndy 类中调用指标实例的时候,新的指标拷贝将可以工作并返回它第一次运行的图表上的数据。为了避免这一点,我们应当准备一个特殊的图表用于每个指标实例,

让我们仔细看看指标代码。在全局变量中声明了下面的项目:

  • 之前创建的类的拷贝,
  • 用于保存工作图表 ID 的变量
  • 以及用于保存图表中将要克隆的对象 ID 的数组。
CCloneIndy    *CloneIndy;
CCloneObjects *CloneObjects;
long           l_Chart;
long           ar_Charts[];

在 OnInit 函数中, 初始化类实例来用于图表和对象.

int OnInit()
  {
//--- 创建指标类
   CloneIndy   =  new   CCloneIndy();
   if(CheckPointer(CloneIndy)==POINTER_INVALID)
      return INIT_FAILED;
   CloneObjects=  new CCloneObjects();
   if(CheckPointer(CloneObjects)==POINTER_INVALID)
      return INIT_FAILED;

初始化工作图表 ID.

   l_Chart=(Chart>0 ? Chart : ChartID());

让我们寻找根据交易品种打开图表,如果有需要,指标拷贝会被加到侦测到的图表中。

   CloneIndy.SearchCharts(l_Chart,ar_Charts);

在函数的末尾,初始化间隔为10秒的计时器,它的唯一目的是更新用于克隆对象的图表列表。

   EventSetTimer(10);
//---
   return(INIT_SUCCEEDED);
  }

OnCalculate 函数不进行任何操作,上面已经提到过,我们的指标是基于事件模型的,这里的意思是我们指标的全部功能就集中于 OnChartEvent 函数中。在函数的开始声明辅助局部变量。

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   string message1=NULL;
   string message2=NULL;
   int total=0;

下面,根据进入的事件使用'switch'操作符构建运行分支,

运行的第一块收集和传递用于在其他图表中创建和修改对象的数据,它根据对象在图表上被创建,修改或者移动的事件来调用。如果发生了这些事件,指标根据对象的状态创建两条消息,然后在循环中运行,把它们发送到我们数组中ID对应的所有图表中。

   switch(id)
     {
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DRAG:
        message1=CloneObjects.CreateMessage(l_Chart,sparam,0);
        message2=CloneObjects.CreateMessage(l_Chart,sparam,1);
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
          {
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message1);
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message2);
          }
        break;

下一块是当对象被从图表上删除时调用的,在这种情况下,不需要另外的消息,因为只要根据它的名称就可以删除对象,我们在 'sparam' 变量中已经有这个信息了。所以,立即运行循环,发送消息到其它图表中。

      case CHARTEVENT_OBJECT_DELETE:
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,sparam);
        break;

下面两块处理从其它图表接收到的消息的处理,当有关对象创建或者修改的信息传来的时候,我们调用函数来在图表上显示对象,把工作图表 ID 和函数参数中的输入消息传递过来。

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DRAG:
        CloneObjects.DrawObjects(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;

当接收到删除对象的数据时,调用函数来在工作图表中删除类似的对象。

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DELETE:
        ObjectDelete(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;
     }
  }

指标的代码和使用的类的代码都在附件中有提供。


结论

在本文中,我们已经提出了方法来开发指标,可以自动在终端图表之间实时自动复制图形对象,这种方法实现了在终端打开的图表之间做双向数据交换的机制,它对用户没有同步图表数量的限制,同时,用户可以在任何同步的图表上创建,修改和删除图形对象。指标的操作在视频中做了展示:



参考

  1. 使用云存储服务来进行终端之间的数据交换

本文中使用的程序

#
 名称
类型 
描述 
1 ChartObjectsClone.mq5  指标  图表之间交换数据的指标
2 CloneIndy.mqh  类库  根据交易品种名称选择图表的类
3 CloneObjects.mqh  类库  用于操作图形对象的类
4 ChartObjectsClone.mqproj    项目描述文件