以 delta 指标为例开发股票交易量控制指标

26 九月 2018, 08:25
Alexey Kozitsyn
0
1 887

内容

概述

正如我们所知道的,MetaTrader 5 广播两类交易量。:

  • tick(逐笔报价) 交易量, 即柱线形成过程中收到的逐笔报价 (报价数据变化) 的次数;
  • real(实际) 交易量, 即柱线形成期间抵达得的交易次数。

在终端中,实际交易量被简单地表示为交易量。 它才是我们感兴趣的。 由于终端历史记录的特点,以及时间和买卖,现在有可能将其开发为股票指标。 它们令我们能够看到“幕后”正在发生的事情,即实际成交量由什么组成:成交量和交易频率,以及特定时期买卖双方的相关性。 这意味着我们现在可以将交易量扩展为组件。 这些数据可以显著提高我们的交易预测准确性。 同时,与常规方法相比,开发这样的指标更困难。 本文详细介绍了股票指标开发的先后顺序和细微之处,以及它们的工作特点和测试特点。 作为一个示例,我们将依据实际交易量开发买入和卖出交易量的 delta(差值)指标。 所开发的指标,其依据逐笔报价的工作规则也会一同说明。

然而,我们应该记住,只有在集中式(交易所)市场上才有真正的交易量。 这意味着它不能用于外汇市场,因为那是场外交易市场。 我们将通过莫斯科交易所衍生品市场(FRTS)的示例来研究实际成交量。 如果您对 FORTS 不熟悉,我强烈推荐您阅读有关 兑换定价 的文章。

目标读者

有关逐笔报价数据的问题近来在 MQL5.com 论坛 中已相当普遍。 这种功能相对来说是新出现的,并且在不断改进。 首先,本文旨在帮助那些已经知道如何编写指标,并愿意提高 MetaTrader 5 应用程序开发技能的程序员。 那些希望掌握股票市场,以及对涉及 delta 指标 和/或 类似逐笔报价指标分析的交易员,都会对本文感兴趣。

1. 准备。 选择服务器

矛盾的是,开始开发指标时应从选择交易服务器起始。 股票指标精准运行的必要条件:券商的服务器应可持续更新数据。 不幸的是,券商的服务器版本没有广播,并且难以立即解释数据是否准确。

希望市场深度能帮助我们解决这个问题。 若要打开它,请单击左上角金融产品名称附近的表格图标(如果没有显示,请检查“查看”选项卡上的“显示快速交易按钮”(F8)选项是否被选中)或按 Alt+B。 在市场深度窗口中,点击“显示时间和买卖”按钮。 此外,请确保未使用右键单击表格来设置最小交易量过滤器。

如果服务器没有更新数据,它会向市场深度广播所谓的“不确定方向”成交。 我们来更详细地查验它们。 每笔业务都有一个发起人:买方或卖方。 这意味着业务属性(买入或卖出)应可清晰地指明。 如果成交方向没有确定(在市场深度中标记为 N/A),这会影响由该指标计算的 delta(买卖交易量之间的差值)构造精度。 以下提供了更新和未更新的市场深度(图例 1):

  


图例 1. 更新的 (左侧) 和旧的 (右侧) 市场深度

规则 1. 检查服务器是否更新。

此外,我强烈建议选择一个响应速度快的服务器。 ping 值越低,终端就能够更快地与券商服务器交换数据。 如果我们深入观察图例 1,MetaTrader 5 广播精度为毫秒级,因此 ping 值越小,您获得并处理成交数据的速度就越快。 在终端的右下角检查连接当前服务器(如果需要的话更改服务器)的 ping 值:


图例 2. 选定服务器的延迟为 30.58 毫秒。

此外,请注意,客户终端应该更新为构建 1881 或更高版本,因为这个构建修复了关于逐笔报价数据的所有当前已知错误。

2. 获取逐笔报价的方法。 MqlTick 格式

假设我们选择了一台服务器为我们提供正确的逐笔报价历史数据。 我们如何获得历史数据? MQL5 语言具有两个函数:

  • CopyTicks() 意指从某个日期获得必要大小的逐笔报价;
  • CopyTicksRange() 是指在某个日期范围内获取逐笔报价。

我们的指标将需要全部两个函数。 它们将允许我们按照 MqlTick 格式获取逐笔报价。 这种结构存储数据的时间、价格、交易量,以及新逐笔报价中哪些数据已被明确改变。 我们可以得到三种类型的逐笔报价历史。 此类型由标志定义。:

  • COPY_TICKS_INFO – 返回竞买 和/或 竞卖价格变化造成的逐笔报价;
  • COPY_TICKS_TRADE – 返回上次价格和交易量变化造成的逐笔报价;
  • COPY_TICKS_ALL – 返回所有变化造成的逐笔报价。

我们需要一个交易逐笔报价流(COPY_TICKS_TRADE)来达到我们的目的。 在 CopyTicks 函数描述中找到有关逐笔报价类型的更多信息。

MqlTick 结构可令我们分析以下字段的值:

  • volume - 目前的最后一笔成交价格的成交量。 我们指标中的逐笔价格没啥用,不像该逐笔报价上的交易量,它会被重用;
  • time_msc - 最后更新时间,以毫秒为单位。 我们将使用此参数来确定逐笔报价所属的蜡烛并获取下一个逐笔报价的请求时间;
  • flags - 逐笔报价标记,变化数据的 ID。 我们将使用标志来摘选买入(TICK_FLAG_BUY 和 卖出(TICK_FLAG_SELL)类型。
简言之,该指标执行以下操作:它获取每根蜡烛的所有逐笔交易价格,跟踪买入和卖出交易量,并将其差值(delta)显示为直方图。 如果蜡烛的买方更多,则直方图柱线为蓝色。 否则,它是红色的。 一切都很简单!

3. 首次启动。 历史计算

CTicks _ticks (Ticks_article.mqh 文件) 在指标中作为主要对象来处理逐笔报价。 它将用于执行逐笔报价的所有操作。

指标操作将分为两个主要模块:历史计算和实时计算

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- 检查首次启动
   if(prev_calculated>0)                    // 若非首次启动
     {
      // 模块 2
     }
   else                                     // 如果是首次启动
     {
      // 模块 1
     }
//---
   return( rates_total );
  }

在终端首次启动或单击终端中的刷新按钮(模块1)时,我们应该依据历史记录计算指标。 最初,我计划制作一个通用计算函数,用于历史记录和实时计算。 然而,我最终决定改变观念,以便提高简单性并加快计算速度。 首先,依据完整的柱线计算历史(CalculateHistoryBars())。 之后计算当前柱线(CalculateCurrentBar())。 所有这些动作如下所述:

//--- 1. 按初始值初始化指标缓冲区
BuffersInitialize(EMPTY_VALUE);
//--- 2. 重置重复控制参数的值
_repeatedControl=false;
_controlNum=WRONG_VALUE;
//--- 3. 重置保存逐笔报价的柱线时间(单击“刷新”按钮)
_ticks.SetTime(0);
//--- 4. 设置已形成柱线的逐笔报价的下载开始时刻
_ticks.SetFrom(inpHistoryDate);
//--- 5. 检查下载开始的时刻 
if(_ticks.GetFrom()<=0)                 // 如果没有设定时刻
   return(0);                           // 退出
//--- 6. 设置已形成柱线结束历史下载的时刻
_ticks.SetTo( long( time[ rates_total-1 ]*MS_KOEF - 1 ) );
//--- 7. 下载形成的柱线历史
if(!_ticks.GetTicksRange())             // 如果不成功
   return(0);                           // 错误推出
//--- 8. 依据历史成形柱线计算
CalculateHistoryBars( rates_total, time, volume );
//--- 9. 重置保存逐笔报价的柱线时间
_ticks.SetTime(0);
//--- 10. 设置最后一根柱线开始下载逐笔报价的时刻
_ticks.SetFrom( long( time[ rates_total-1 ]*MS_KOEF ) );
//--- 11. 设置最后一根柱线结束下载逐笔报价的时刻
_ticks.SetTo( long( TimeCurrent()*MS_KOEF ) );
//--- 12. 下载当前柱线历史
if(!_ticks.GetTicksRange())             // 如果不成功
   return(0);                           // 错误推出
//--- 13. 复位复制结束时刻
_ticks.SetTo( ULONG_MAX );
//--- 14. 记住所获得的最后一次逐笔报价历史的时刻
_ticks.SetFrom();
//--- 15. 当前柱线计算
CalculateCurrentBar( true, rates_total, time, volume );
//--- 16. 设置后续实时复制逐笔报价的数量
_ticks.SetCount(4000);

指标代码有大量的注释,所以我将只关注要点。

第 3 点. “重置保存逐笔报价的柱线时间”。 处理逐笔报价的对象包含烛台的开盘时间,相应的逐笔报价保存在其中。 单击刷新时,终端会从开头重新计算指标。 为了在烛台中正确地保存逐笔报价,它的时间应被重置。

第 4 点. "设置已形成柱线的逐笔报价的下载开始时刻"。 获取逐笔报价历史可能是相当耗时的操作。 因此,您需要给用户机会来指定下载的开始日期。 IpistRoReDATE 参数就是用于此目的。 在零值的情况下,从当前日期开始下载历史记录。 在 SetFrom(datetime) 方法中,传递的时间为秒。 如上所述,指标首先计算已成形的主线。

第 5 点. “检查开始下载时刻是否正确”。 检查第 4 点中接收的数值。

第 6 点. “设置已形成柱线历史完成下载的时刻”。 已形成柱线历史完成下载的时刻是距当前烛台(rates_total- 1)开盘时间的毫秒值。 在这种情况下,完成下载的时刻是“long(长整数)”类型。 将参数传递给方法时,我们需要显式地指明正在传递“long”类型,以便防止将参数传递给类中含有“datetime”类型的方法。 在 SetTo() 方法的情况下,类不会用“datetime”类型参数来重载它。 无论如何,出于安全,我建议显式地传递“long”类型参数。

第 7 点. “通过已成型柱线下载历史”。 使用 GetTicksRange() 函数获得的历史记录,它是 CopyTicksRange() 函数的包装器,并添加了可能的错误检查。 如果下载过程中发生错误,则在下一次逐笔报价中重复请求整个历史。 下面附带的 Ticks_article.mqh 文件包含有关此函数以及用于处理逐笔报价的其它函数的更多细节。 

第 8 点. “依据已成型柱线历史计算”。 关于已成形柱线的计算将在相应的章节中详述。

第 9-12 点. 依据完整柱线计算。 现在是计算当前烛台的时候了。 设置复制范围,并得到当前烛台逐笔报价。

第 13 点. “重置复制结束时刻”。 此外,我们将继续使用 _ticks 对象获取逐笔报价,下载范围从最后一个到达的逐笔报价之时到整个可用历史的结束时间,而非从一个时刻到另一个时刻。 因此,最好重置复制结束时刻 — 我们在实时计算时不再需要它。

第 14 点. “记住所获历史最后一次逐笔报价的时间”。 我们需要将获得的历史最后一次逐笔报价时间作为开始实时数据复制的时刻。

第 15 点. “当前柱线计算”。 当前柱线的计算也将在本文的一个单独章节中描述,它与计算已成形柱线的方法有重大区别。

第 16 点. “设置后续实时复制逐笔报价的数量”。 以前,我们使用包装在 GetTicksRange() 方法中的 CopyTicksRange() 函数来获取逐笔报价。 然而,实时当中,我们将使用在 GetTicks() 方法中封装的 CopyTicks() 函数。 SetCount() 方法设置后续请求的逐笔报价数量。 我们选择 4000 个,因为出于快速访问,终端为每个品种存储 4096 个逐笔报价。 对这些逐笔报价的请求会以最高速度执行。 设置值不影响逐笔报价的获取速度(~1 毫秒),无论是 100 还是 4000。

我们来仔细查看计算函数。

4. 按所形成的柱线计算历史的函数

函数本身如下:

//+------------------------------------------------------------------+
//| 计算已成形历史柱线的函数                                             |
//+------------------------------------------------------------------+
bool CalculateHistoryBars(const int rates_total,    // 已计算柱线数量
                          const datetime& time[],   // 柱线开盘时间数组 
                          const long& volume[]      // 实际交易量数组
                          )
  {
//--- 总交易量
   long sumVolBuy=0;
   long sumVolSell=0;
//--- 写入缓冲区的柱线索引
   int bNum=WRONG_VALUE;
//--- 获取数组中的逐笔报价数量
   const int limit=_ticks.GetSize();
//--- 循环遍历所有逐笔报价Loop by all ticks
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- 定义写入逐笔报价的烛台
      if(_ticks.IsNewCandle(i))                         // 如果下一根烛台开始成形
        {
         //--- 检查已形成的(完整的)烛台的索引是否已保存
         if(bNum>=0) // If saved
           {
            //--- 检查交易量数值是否已保存
            if(sumVolBuy>0 || sumVolSell>0) // 如果所有参数都已保存
              {
               //--- 监测烛台总交易量
               VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
            //--- 将值添加到缓冲区
            DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
           }
         //--- 重置先前烛台的交易量
         sumVolBuy=0;
         sumVolSell=0;
         //--- 根据其开盘时间设定烛台索引
         bNum=_ticks.GetNumByTime(false);
         //--- 检查索引是否正确
         if(bNum>=rates_total || bNum<0) // 如果索引不正确
           {
            //--- 退出且不计算历史
            return( false );
           }
        }
      //--- 在必要的分量上添加一次交易量
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
     }
//--- 检查最后已成形烛台的交易量是否已保存
   if(sumVolBuy>0 || sumVolSell>0) // 如果所有参数都已保存
     {
      //--- 跟踪烛台的总交易量
      VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
     }
//--- 向缓冲区中输入数值
   DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
//--- 计算完成
   return( true );
  }

该函数背后的思路是通过当前时间帧中已形成烛台的逐笔报价进行排序,获得每根烛台买、卖交易量的差值,并将获得的交易量和增量值输入到指标缓冲区。

如早前所述,最初计划是将它构造为历史和实时计算的共有函数。 然而,增加在已形成柱线杆上计算历史的函数,我们有若干目标:

  • 简化算法。 由于柱线已经形成,没有新的逐笔报价会被添加到它们之中。 这意味着我们可以删除下面描述的过度检查,而这是实时计算所必需的;
  • 加速算法。 除了没有额外的检查之外,您也无需在每次逐笔报价之后更新缓冲区中的数值。 仅当处理新烛台的第一个逐笔报价时才执行对缓冲器的写入;
  • 增加变异性。 不在需要实时计算。 例如,如果在 EA 中使用的指标基于已成形柱线,那么仅对每根烛台执行此函数计算一次就足够了。

计算算法,以及处理逐笔报价历史的完整描述如下。

5. 计算当前烛条的函数

注意指标代码中的 CalculateCurrentBar() 函数。

//+------------------------------------------------------------------+
//| 当前烛台计算函数                                                    |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // 函数首次启动标志
                         const int rates_total,    // 已计算数标志
                         const datetime& time[],   // 柱线开盘时间数组 
                         const long& volume[]      // 实际交易量数组
                         )
  {
//--- 总交易量
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- 写入缓冲区的柱线索引
   static int bNum=WRONG_VALUE;
//--- 检查首次启动标志
   if(firstLaunch)                                 // 首次启动的情况
     {
      //--- 复位静态参数
      sumVolBuy=0;
      sumVolSell=0;
      bNum=WRONG_VALUE;
     }
//--- 获取数组中倒数第二个逐笔报价的索引
   const int limit=_ticks.GetSize()-1;
//--- 'limit' 逐笔报价时间
   const ulong limitTime=_ticks.GetFrom();
//--- 循环遍历所有逐笔报价(除最后一个)
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- 1. 取第 i 个逐笔报价与第 limit 个逐笔报价比较(检查循环完成)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // 如果逐笔报价时间等于 limit 逐笔报价时间
         return;                                   // 退出
      //--- 2. 检查图表上不存在的烛台是否开始成形
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // 如果烛台开始形成
        {
         //--- 检查日志是否已维护
         if(inpLog)
            Print(__FUNCTION__,": ATTENTION! Future tick ["+GetMsToStringTime(_ticks.GetTickTimeMs(i))+"]. Tick time "+TimeToString(_ticks.GetTickTime(i))+
                  ", time[ rates_total-1 ]+PerSec() = "+TimeToString(time[rates_total-1]+PeriodSeconds()));
         //--- 2.1. 设置(校正)下一个逐笔报价的请求时间
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- 退出
         return;
        }
      //--- 3. 定义保存逐笔报价的烛台
      if(_ticks.IsNewCandle(i))                    // 如果下一根烛台开始成形
        {
         //--- 3.1. 检查已形成的(完整的)烛台索引是否已保存
         if(bNum>=0)                               // 如果索引已保存
           {
            //--- 检查交易量数值是否已保存
            if(sumVolBuy>0 || sumVolSell>0)        // 如果所有参数已保存
              {
               //--- 3.1.1. 管理烛台的总交易量
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. 重置先前的烛台交易量
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. 记住当前的烛台索引
         bNum=rates_total-1;
        }
      //--- 4. 将逐笔报价的交易量加入必要的分量
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
      //--- 5. 将数值输入到缓冲区
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
     }
  }

它类似于以前的函数 CalculateHistoryBars(),但它有自己的特点。 我们来更详细地检查它们。 函数原型提供如下:

//+------------------------------------------------------------------+
//| 当前烛台计算函数                                                    |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // 函数首次启动标志
                         const int rates_total,    // 已计算柱线数量
                         const datetime& time[],   // 柱线开盘时间数组 
                         const long& volume[]      // 实际交易量数组
                         )

注意,CalculateCurrentBar() 用于两种情况:在首次启动期间计算当前烛台历史,并执行实时计算。 首次启动标志允许选择计算模式。 模式之间的唯一区别在于,在首次启动期间,包含买卖总和的静态变量、缓冲区中包含这些总和的的烛台索引,以及差值异 (delta) 均被重置为零。 我要再次强调,在指标中只用实际交易量!

//--- 总交易量
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- 写入缓冲区的柱线索引
   static int bNum=WRONG_VALUE;
//--- 检查首次启动标志
   if(firstLaunch)                                 // 首次启动的情况
     {
      //--- 重置交易量合计
      sumVolBuy=0;
      sumVolSell=0;
      //--- 重置烛台索引
      bNum=WRONG_VALUE;
     }

在声明静态变量之后,获取数组中最后一个逐笔报价的索引和时间。:

//--- 获取数组中最后一个逐笔报价的索引
   const int limit=_ticks.GetSize()-1;
//--- 'limit' 逐笔报价时间
   const ulong limitTime=_ticks.GetFrom();

该索引用作逐笔报价迭代循环的定界符。 我们来建立一个条件:在计算中不包括最后一个逐笔报价,以及与其时间匹配的逐笔报价。 为什么? 因为不同交易参与者的若干限价订单可以合并为单个逐笔报价到达。 逐笔报价集簇(成交)包括同时执行的交易(毫秒级精度),并且具有相同的类型(买或卖)(图例 3)。 注意,由于广播成交的时间为纳秒,因此在终端中可以显示多个逐笔报价集簇,因为它们是在同一毫秒内到达的。 若要查看它自己,启动下面的 test_getTicksRange 脚本。

图例 3. 逐笔报价集簇(市场买入订单,初始 4 笔成交,包括 26 手)

为了正确取用交易量,一个逐笔报价集簇只有在完全传递到终端时才会计算一次,即,随后包含的成交变为可用的时刻(图例 4)。


图例 4. 在 373 执行成交,计算在 334 绑定的逐笔报价。

我们不能确定绑定的逐笔报价已经完全到达终端,直到集簇之后的成交变为可用,因为集簇可能只有部分到达。 在此我不会详述这一点,只要相信我的话。 因此,我们可以定义规则 2。:

规则 2. 只有收到集簇之后 的逐笔报价时,才会计算逐笔报价集簇。

我们在首次启动算法 p. 13 中节省了获得最后逐笔报价的时间。 现在,我们把它写入变量的 limitTime。

现在,我们直接移动到逐笔报价计算循环。:

//--- 1. 取第 i 个逐笔报价与第 limit 个逐笔报价比较(检查循环完成)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // 如果逐笔报价时间等于 limit 逐笔报价
         return;                                   // 退出

第 1 点. “该逐笔报价时间与最后一个逐笔报价比较”。 如上所述,在计算中不考虑最后的逐笔报价,因为计算仅依据已形成的逐笔报价执行。 但是我们也知道最后一个逐笔报价也许已部分复制。 这意味着,我们应该从计算中排除所有的逐笔报价集簇(如果有的话)。

//--- 2. 检查图表上不存在的烛台是否开始成形
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())

第 2 点. “检查图表上烛台是否开始形成”。 这听起来可能有点奇怪。 图表上没有烛台怎么形成? 若要回答这个问题,您需要了解终端中处理/接收逐笔报价数据的特殊性。 这些特点是通过服务台与开发者进行广泛交流而得到澄清的。 我会在此描述它们: 

终端在单独的流中收集逐笔报价,与指标和 EA 的执行无关。 烛台是建立在另一个流上 — 那个执行的指标。 这些流彼此不同步。 将逐笔报价应用在烛台之后,才会计算指标。 不会错过一个逐笔报价。 这意味着,通过调用 CopyTicks() 函数,与应用到柱线的数据相比,您可以获得更多最近的逐笔报价数据。

在实际操作中,这意味着以下几点。 在计算 rates_total-1 根烛台时,指标可以获得下一个蜡烛的逐笔报价,而该烛台尚未完成(尚未应用该逐笔报价)。 为了避免这种情况(以及数组超出范围错误),我们需要添加这个检查。

规则 3. 请注意,也许会提前获取烛条图表上尚未出现的逐笔报价。

如果检测到“未来”的逐笔报价(与已形成烛台不对应),我们应该在下一个逐笔报价请求发生时重写时间(第 2.1 点)。 此外,我们应该立即退出循环和函数,同时等待新的逐笔报价,以及图表上形成新的烛台:

//--- 2. 检查图表上不存在的烛台是否开始成形
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // 如果烛台开始形成
        {
         //--- 2.1. 设置(校正)下一个逐笔报价的请求时间
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- 退出
         return;
        }

下面的算法几乎完全匹配 CalculateHistoryBars() 函数。 我们来更详尽地研究一下。

//--- 3. 定义保存逐笔报价的烛台
      if(_ticks.IsNewCandle(i))

第 3 点. 定义保存逐笔报价的烛台。 在此,我们将第 i 个逐笔报价的时间与保存逐笔报价的烛台的开盘时间进行比较。 如果第 i 个逐笔报价的时间超过烛台边界,则改变烛台开盘时间,并触发分析后续烛台的预备算法:

//--- 3. 定义保存逐笔报价的烛台
      if(_ticks.IsNewCandle(i))                    // 如果下一根烛台开始成形
        {
         //--- 3.1. 检查已形成的(完整的)烛台索引是否已保存
         if(bNum>=0)                               // 如果索引已保存
           {
            //--- 检查交易量数值是否已保存
            if(sumVolBuy>0 || sumVolSell>0)        // 如果所有参数均已保存
              {
               //--- 3.1.1. 管理烛台的总交易量
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. 重置先前的烛台交易量
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. 记住当前的烛台索引
         bNum=rates_total-1;
        }

第 3.1 点. 检查已形成的烛台索引是否已保存。 在历史计算模式(首次启动)中,该检查防止访问不正确索引(-1)的时间和交易量数组。 接下来,我们检查交易是否在烛台上进行。 在没有成交的情况下,不需要交易量控制。

第 3.1.1 点. 总交易量控制。 在 VolumeControl() 过程中,指标累积每根烛台的买、卖交易量,并与“引用”交易量比较,即,直接从兑换传递的实际交易量(通过已形成烛台的 Volume[] 数组数值)。 如果兑换交易量与累积交易量匹配,则继续进行下一步的计算。 但是若是没有呢? 您可能想知道它是怎么回事。 总交易量是一样的。 唯一的区别是,我们在指标中计算了其一,而另一个则来自交易所。 交易量应绝对重合! 

嗯,您是对的。 它们应当这样。 这条规则应该适用于所有烛台。 我们的指标究竟是做什么的:

  • 指标接收所有逐笔报价;
  • 零号逐笔报价时间用于定义计算烛台时间 (例如,在 M1 上,零号逐笔报价时间是 10:00:00.123,因此,烛台的开盘时间是 10:00,且作为计算的 delta);
  • 检查每个逐笔报价的时间;
  • 将每一个逐笔报价的交易量加入到买、卖交易量;
  • 等待直到逐笔报价离开已计算烛台边界 (时间超过 10:00:59.999),以便显示 10:00 烛台的 delta;
  • 逐笔报价超过已计算烛台的时间(例如,10:01:0.46),用于新烛台开盘(10:01)。 下一个 delta 用于计算烛台。 因此,重复整个过程。

到目前为止似乎很容易。 然而,在实时(对于第 rates_total-1 根烛台),我们应该记得前面提到的“未来”逐笔报价(规则 3)出现时,新烛台的逐笔报价到达,而烛台尚未形成。 这个特性也影响交易量控制! 当处理逐笔报价时,指标仍然包含 volume[] 数组中的过时交易量数值(该值仍然保持不变)。 这意味着,我们无法正确地将指标收集的交易量与 volume[] 数组进行比较。 在实际操作中,volume[rates_total-1] 参考交易量有时与指标收集的累计交易量 (sumVolBuy+sumVolSell) 不符。 VolumeControl() 过程提供两个解决方案:

  1. 重新计算烛台交易量,并将其与通过 CopyRealVolume() 函数获得的参考值进行比较;
  2. 如果第一选项不能解决该问题,则当形成新烛台时设置交易量控制标志。

因此,第一种方法尝试在新烛台形成之前解决控制问题,而第二种方法保证在新烛台形成之后解决控制问题。

第 3.2 点. “重置以前的蜡台交易量”。 在形成新烛台之后,将交易量计数器重置为零。

第 3.3 点. “记住当前烛台索引”。 将计算函数分离的另一个优点,一个函数计算已成形柱线,而另一个函数计算当前烛台。 当前烛台索引总是等于 rates_total-1。

//--- 4. 将逐笔报价的交易量加入必要的分量
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);

第 4 点. 将逐笔报价交易量添加到总交易量中。 首先,使用已分析逐笔报价的标志来找出什么数据已经改变:

//+------------------------------------------------------------------+
//| 将逐笔报价交易量添加到总交易量中                                       |
//+------------------------------------------------------------------+
void AddVolToSum(const MqlTick &tick,        // 检查逐笔报价参数
                 long& sumVolBuy,            // 总计买入交易量 (out)
                 long& sumVolSell            // 总计卖出交易量 (out)
                )
  {
//--- 检查逐笔报价方向
   if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY && ( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // 如果逐笔报价是双向的
        Print(__FUNCTION__,": ERROR! Tick '"+GetMsToStringTime(tick.time_msc)+"' is of unknown direction!");
   else if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY)   // 万一是买入
        sumVolBuy+=(long)tick.volume;
   else if(( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // 万一是卖出
        sumVolSell+=(long)tick.volume;
   else                                                  // 如果逐笔报价并非交易
        Print(__FUNCTION__,": ERROR! Tick '"+GetMsToStringTime(tick.time_msc)+"' is not a trading one!");
  }

在此,我再次强调的是 规则 1。 如果处理发生在广播未知方向交易的服务器上,则无法确定是谁发起了交易 - 买方还是卖方。 因此,流水账将继续相应的错误。 如果确定了发起者,则将交易量加到买入或卖出的总交易量中。 如果该标志不包含有关业务的发起人数据,则也会收到错误。

//--- 5. 向缓冲区输入数值
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);

第 5 点. 将数值添加到缓冲区。 DisplayValues() 过程跟踪指标缓冲区的索引(我们将调用字符串索引传递给函数来达成此目的),计算 delta 并向缓冲区写入 delta 以及买、卖交易量。

6. 实时计算

我们阐述实时计算的模块:

//--- 1. 检查新柱线成形
if(rates_total>prev_calculated) // 如果新柱线情况
  {
   //--- 用空值初始化 rates_total-1 缓冲区索引
   BuffersIndexInitialize(rates_total-1,EMPTY_VALUE);
   //--- 2. 检查 rates_total-2 的柱线交易量是否应该被跟踪
   if(_repeatedControl && _controlNum==rates_total-2)
     {
      //--- 3. 重检查
      RepeatedControl(false,_controlNum,time[_controlNum]);
     }
   //--- 4. 重置再次检查数值
   _repeatedControl=false;
   _controlNum=WRONG_VALUE;
  }
//--- 5. 下载新的逐笔报价
if(!_ticks.GetTicks() )               // 如果不成功
   return( prev_calculated );         // 出错退出
//--- 6. 记住所获得的最后一次逐笔报价历史的时刻
_ticks.SetFrom();
//--- 7. 实时计算
CalculateCurrentBar(false,rates_total,time,volume);

第 1 点. 检查新柱线成形。 这个检查很重要。 正如我们在第 2.1.1 中发现的,如果在主计算函数的交易量控制过程(实时计算)中没有通过检查,那么应该在新柱线形成时通过检查。 这的确是合适的时机!

第 2 点. 检查 rates_total-2 柱线上的交易量是否应该被跟踪。 如果在新形成的 rates_total-2 烛台上查到重复控制标志,则执行重新检查。 (p. 3).

第 3 点. 执行重新检查。 正如已经提到的,在重新检查期间,接收每根烛台的所有逐笔报价。 此外,我们还定义了买、卖交易量,计算了 delta,并将其和参考值进行了比较。

第 5 点. 下载新的逐笔报价。 获取上次指标启动前的最后一个逐笔报价到达后的逐笔报价。 在实时计算时,我们使用 GetTicks() 函数 和 CopyTicks() 函数得到逐笔报价。

第 6 点. 记住最后一次逐笔报价。 最后一次逐笔报价的时间在 p. 5 或历史计算之后。 在下一次指标启动的那一刻请求逐笔报价历史。

第 7 点. 实时计算。 正如早前提到的,在历史和实时计算中都使用 CalculateCurrentBar() 过程。 firstLaunch 标志为此负责。 在这种情况下,它被设置为 'false'。

7. 策略测试器的工作特点

在使用策略测试器时,我们应该始终牢记这是一个具有自己功能的独立程序。 即使测试器可以做与终端相同的事情,也不意味着它会遵照终端相同的方式来运作。 使用逐笔报价数据的程序会出现类似的情况(在测试器开发的这一阶段)。 尽管指标计算正确(交易量控制成功),但测试器中的指标计算稍有不同。 原因再次出于逐笔报价集簇的处理。

与终端不同,在终端中,多笔业务可能发生在单个逐笔报价内(即,我们可能得到逐笔报价集簇),在测试器中,即使单个逐笔报价集簇中的多个逐笔报价相继到达,每个逐笔报价也将被单独接收。 您可以通过从应用程序启动 test_tickPack 测试指标来了解这一点。 近似结果如下:

2018.07.13 10:00:00   OnCalculate: Received ticks 4. [0] = 2018.07.13 10:00:00.564, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 2. [0] = 2018.07.13 10:00:00.571, [1] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 3. [0] = 2018.07.13 10:00:00.571, [2] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 4. [0] = 2018.07.13 10:00:00.571, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 5. [0] = 2018.07.13 10:00:00.571, [4] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 6. [0] = 2018.07.13 10:00:00.571, [5] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Received ticks 7. [0] = 2018.07.13 10:00:00.571, [6] = 2018.07.13 10:00:00.572 FLAG_BUY

您可以自行尝试。 一定要按 F12 来设置“基于真实逐笔报价”模式。 逐笔报价将严格地逐次添加! 然而在现实中,这个集簇可以部分或一个片段进入终端,但很可能不是“一次一个”模式。 这样既不好也不坏。 只是要记住这一特性。

结束语

在本文中,我阐述了许多用户(包括我)在开发逐笔报价指标时遇到的一些微妙之处,以及最常见的困难。 我希望,我的经验能够为社区众多使用逐笔报价数据的应用铺平道路,并推动 MetaTrader 5 平台的进一步发展。 如果您知道其它一些交易逐笔报价的特性,或者如果您发现本文有误,请随时联系我。 我很乐意讨论这个话题。

最后的结果如下所示。 蓝条表示买方在某根烛台上占主导地位,而红色 — 卖方占主导地位。


图例 5. RTS-6.18 上的 Delta 指标

实际交易量的估计为股票市场分析开辟了新的视野,从而能够更好地理解价格走势。 基于逐笔报价数据分析可以开发很多应用,这个指标仅是其中的一小部分。 基于真实交易量创建股票指标是一个可行性相当高的任务。 我希望,这篇文章将帮助您创造这样的指标,并改进您的交易。

如果您对指标本身感兴趣,它的改进版本将会很快发表在我的个人资料当中的产品板块。 祝交易好运!

文章中使用的文件

文件名 类型 描述
 1. Delta_article.mq5  指标文件  delta 指标的实现
 2. Ticks_article.mqh  类文件  处理逐笔报价数据的辅助类
 3. test_getTicksRange.mq5  脚本文件  在一毫秒内检查接收多个逐笔报价集簇的测试脚本
 4. test_tickPack.mq5  指标文件  检查测试器是否收到逐笔报价的测试指标


本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/3708

附加的文件 |
MQL5.zip (16.21 KB)
同时双向工作的通用 RSI 指标 同时双向工作的通用 RSI 指标

当开发交易算法时,我们经常遇到这样一个难题:如何确定趋势/盘整从哪里开始和结束?在本文中,我们尝试创建一个通用指标,在其中我们会尝试组合几种不同类型策略的信号。在 EA 交易中,我们将尝试尽可能简化取得交易信号的过程,并将给出一个把几个指标组合为一的实例。

已有950个网站提供来自MetaQuotes的经济日历 已有950个网站提供来自MetaQuotes的经济日历

该小工具为网站提供了一个详细的发布时间表,列出了全球大型经济体的500个指标及指数。因此,除了主要的网站内容之外,交易者还能够迅速收到关于所有重要事件的最新消息及其解释和图表。

基于 CGraphic  用于分析数据数组(时间序列)之间相互关联的 PairPlot  图 基于 CGraphic 用于分析数据数组(时间序列)之间相互关联的 PairPlot 图

在技术分析中比较几个时间序列是一种很常用的任务,需要合适的工具。在本文中,我提出开发一种用于图形化分析的工具,可以侦测两个或者多个时间序列之间的相互关联。

在MQL5.com自由职业者服务中已完成50,000个订单 在MQL5.com自由职业者服务中已完成50,000个订单

截至2018年10月,MetaTrader官方自由职业者服务的成员已完成超过50,000个订单。这是全球最大的MQL程序员自由职业网站:超过1000名开发人员,每天几十个新订单以及7种语言本地化。