Skip to content

时间序列

时间序列是数据数组,其中元素的索引对应于有序的时间样本。由于交易终端的应用特性,交易者所需的几乎所有信息都以时间序列的形式提供。具体来说,这些信息包括报价数组、报价点数组、技术指标读数数组等。绝大多数的MQL程序也处理这些数据,因此在MQL5 API中有一组专门用于处理它们的函数,我们将在本节中进行探讨。

在MQL5中,访问数组的方式使开发者能够设置两种索引方向之一:

  • 正常(正向):元素的编号从数组的开头到结尾(从旧的数据到新的数据)。
  • 反向(时间序列):编号从数组的结尾到开头(从新的数据到旧的数据)。

我们已经在“数组索引方向(如时间序列)”部分讨论过这个问题。

使用ArraySetAsSeries函数来更改索引模式,这不会影响数组在内存中的物理布局。只是通过编号访问元素的方式发生了变化:在正常索引中,我们通过array[i]获取第i个元素,而在时间序列模式下,等效的公式是array[N - i - 1],其中N是数组的大小(之所以称为“等效”,是因为应用程序开发者无需在各处进行这样的重新计算,因为如果为数组设置了时间序列索引模式,终端会自动进行此操作)。下表(对于一个包含10个元素的字符数组)对此进行了说明。

数组元素ABCDEFGHIJ
常规索引0123456789
时间序列索引9876543210

请记住,数组索引总是从0开始。

当涉及到报价数组和其他不断更新的数据时,新元素会物理地附加到数组的末尾。然而,从交易的角度来看,在分析历史数据时,应该考虑最新的数据并将其作为起点。这就是为什么将当前(最后一个)K线置于索引0下,并从它开始往过去计算之前的K线是很方便的。这样,我们就得到了时间序列索引。

默认情况下,数组是从左到右进行索引的。如果我们想象这样一个数组显示在标准的MetaTrader 5图表上,那么从视觉上看,索引为0的元素将位于最左边的位置,而最后一个元素位于最右边的位置。在反向索引的时间序列中,第0个元素对应于最右边的位置,而最后一个元素对应于最左边的位置。由于时间序列存储了金融工具价格数据相对于时间的历史记录,其中最新的数据总是在旧数据的右边。

时间序列数组中索引为零的元素包含有关最新品种报价的信息。零号K线通常是不完整的,因为它还在继续形成中。

报价时间序列的另一个特征是其周期,即相邻读数之间的时间间隔。这个周期也称为“时间框架”,可以更精确地表述。时间框架是形成一根报价K线的时间段,其开始和结束在绝对时间上以相同的步长对齐。例如,在“1小时”(H1)时间框架中,K线严格在每天每小时的0分钟开始。每个这样的时间段的开始包含在当前K线中,而结束属于下一根K线。

“品种和时间框架”章节提供了标准时间框架的完整列表。

通常,在时间序列概念的框架内,技术指标的缓冲区也会起作用,但我们将在后面研究它们的特点。

如有必要,在任何MQL程序中,您都可以请求任何品种和时间框架的时间序列值,以及为任何品种和时间框架计算的指标值。通过使用Copy函数来获取这些数据,其中有几个函数可以分别读取不同类型的价格数组(例如,开盘价、最高价、最低价、收盘价),或者读取包含每根K线所有特征的MqlRates结构数组。

K线和报价点

除了带有报价的K线之外,MetaTrader 5还为用户和MQL程序提供了分析报价点的能力,报价点是基本的价格变化,K线就是基于这些报价点构建的。每个报价点包含精确到毫秒的时间、几种类型的价格(买价、卖价、最新价)、描述变化本质的标志以及交易的成交量。我们将在稍后的“处理真实报价点数组”章节中研究相应的MqlTick结构。

根据交易品种的类型,K线可以基于买价或最新价来构建。特别是,对于交易所交易品种,有最新价可用,这些品种还会广播市场深度价格。对于非交易所交易品种,如外汇或差价合约,使用买价。

没有价格变化的时间段不会生成K线。这就是MetaTrader 5中价格的呈现方式。例如,如果时间框架等于1天(D1),那么通常周末没有几根K线,星期五之后直接就是星期一。

如果在相应的时间间隔内至少发生了一个报价点,就会出现一根报价K线。同时,K线的开盘时间总是严格与时间段边界对齐,即使第一个报价点到达的时间较晚(通常就是这样)。例如,如果在午夜后的4分钟内没有报价点,然后在00:05:15(即第五分钟的第15秒)发生了价格变化,那么当天的第一根M1 K线可能会在00:05形成。因此,一个报价点根据以下时间戳的关系被包含在特定的K线中:Topen <=Ttick < Topen + P,其中Topen是K线的开盘时间,Ttick是报价点的时间,Topen + P是在时间段P之后下一个潜在K线的开盘时间(之所以称为“潜在”K线,是因为它的存在取决于其他报价点)。

交易品种和时间框架

带有报价的时间序列由两个参数来确定:交易品种名称(金融工具)和时间框架(周期)。

用户可以在“市场报价”窗口中查看交易品种列表,并根据经纪商提供的总列表(“交易品种”对话框)对其进行编辑。对于MQL程序,有一组函数可用于执行相同的操作:在所有交易品种中搜索、了解它们的属性,以及在“市场报价”中添加或删除交易品种。这些功能将在单独的章节中进行讨论。

然而,要请求时间序列,只需知道交易品种的名称即可——这是一个包含现有金融工具标识的字符串。例如,它可以由用户在输入变量中设置。此外,当前图表的交易品种可以通过内置变量_Symbol(或Symbol函数)找到,但为了方便起见,所有时间序列函数都支持这样的约定:NULL值也对应于当前图表的交易品种。

现在让我们来谈谈时间框架。系统中定义了21种标准时间框架:每种时间框架都由特殊枚举ENUM_TIMEFRAMES中的一个元素指定。

标识符值(十六进制)描述
PERIOD_CURRENT0当前图表周期
PERIOD_M11 (0x1)1分钟
PERIOD_M22 (0x2)2分钟
PERIOD_M33 (0x3)3分钟
PERIOD_M44 (0x4)4分钟
PERIOD_M55 (0x5)5分钟
PERIOD_M66 (0x6)6分钟
PERIOD_M1010 (0xA)10分钟
PERIOD_M1212 (0xC)12分钟
PERIOD_M1515 (0xF)15分钟
PERIOD_M2020 (0x14)20分钟
PERIOD_M3030 (0x1E)30分钟
PERIOD_H116385 (0x4001)1小时
PERIOD_H216386 (0x4002)2小时
PERIOD_H316387 (0x4003)3小时
PERIOD_H416388 (0x4004)4小时
PERIOD_H616390 (0x4006)6小时
PERIOD_H816392 (0x4008)8小时
PERIOD_H1216396 (0x400C)12小时
PERIOD_D116408 (0x4018)1天
PERIOD_W132769 (0x8001)1周
PERIOD_MN149153 (0xC001)1个月

正如我们在“预定义变量”部分中看到的,程序可以通过内置变量_Period(或Period函数)了解当前图表的周期。从值这一列中很容易看出,将零传递给接受时间框架的内置函数意味着当前图表的周期。

对于分钟时间框架,其值与其中的分钟数相同(例如,30表示M30)。对于小时时间框架,设置了0x4000位,并且低字节包含小时数(例如,H3为0x4003)。日周期D1编码为24小时,即0x4018(0x18等于24)。最后,周时间框架和月时间框架分别有自己的区分位0x8000和0xC000作为单位指示符,并且在这两种情况下计数(在低字节中)均为1。

为了方便地将枚举元素转换为字符串以及反向转换,本书附带了一个头文件Periods.mqh(我们已经在文件操作示例中使用过它,并且将在未来的示例中继续使用)。它的一个函数StringToPeriod在其算法中使用了上述枚举元素内部位表示的特征。

c
#define PERIOD_PREFIX_LENGTH 7 // StringLen("PERIOD_")

// 获取不带“PERIOD_”前缀的周期缩写名称
string PeriodToString(const ENUM_TIMEFRAMES tf = PERIOD_CURRENT)
{
   const static int prefix = StringLen("PERIOD_");
   return StringSubstr(EnumToString(tf == PERIOD_CURRENT? _Period : tf),
      PERIOD_PREFIX_LENGTH);
}

// 通过完整名称(如PERIOD_H4)或短名称(如H4)获取周期值
ENUM_TIMEFRAMES StringToPeriod(string name)
{
   if(StringLen(name) < 2) return 0;
   // 如果需要,将完整名称“PERIOD_TN”转换为短名称“TN”
   if(StringLen(name) > PERIOD_PREFIX_LENGTH)
   {
     name = StringSubstr(name, PERIOD_PREFIX_LENGTH);
   }
   // 将数字结尾“N”转换为数字,跳过“T”
   const int count = (int)StringToInteger(StringSubstr(name, 1));
   // 清除可能的错误WRONG_STRING_PARAMETER(5040)
   // 例如,如果输入字符串是“MN1”,那么对于StringToInteger来说,N1不是一个数字
   ResetLastError();
   switch(name[0])
   {
      case 'M':
         if(!count) return PERIOD_MN1;
         return (ENUM_TIMEFRAMES)count;
      case 'H':
         return (ENUM_TIMEFRAMES)(0x4000 + count);
      case 'D':
         return PERIOD_D1;
      case 'W':
         return PERIOD_W1;
   }
   return 0;
}

请注意,_Symbol_Period变量仅在在图表上运行的MQL程序(包括脚本、智能交易系统和指标)中包含实际数据。在服务中,这些变量为空,因此,要访问时间序列,必须显式设置交易品种名称和周期,或者以某种方式从外部获取它们。

时间框架的定义属性是其持续时间(K线持续时间)。MQL5允许使用PeriodSeconds函数获取构成特定时间框架一根K线的秒数。

c
int PeriodSeconds(ENUM_TIMEFRAMES period = PERIOD_CURRENT)

period参数将周期指定为ENUM_TIMEFRAMES枚举的一个元素。如果未指定该参数,则返回程序正在运行的当前图表周期的秒数。

我们将在“等待数据和管理可见性”部分的指标IndDeltaVolume.mq5中,以及“使用内置指标”部分的指标UseM1MA.mq5中考虑使用该函数的示例。

为了生成未包含在指定列表中的非标准持续时间的时间框架,MQL5 API提供了自定义交易品种,但是,如果不修改智能交易系统,它们不允许像在标准图表上那样进行交易。

此外,需要注意的是,在MetaTrader 5中,特定时间序列内或图表上的K线持续时间始终是相同的。因此,要构建K线不是根据时间形成,而是随着其他参数(特别是成交量(等量图))的积累或价格沿一个方向以固定步长移动(砖形图)而形成的图表,可以基于指标(例如,使用DRAW_CANDLESDRAW_BARS渲染类型)或使用自定义交易品种开发自己的解决方案。

时间序列组织和存储的技术方面

在着手使用MQL5 API中用于处理时间序列的函数的实际问题之前,我们应该先了解从服务器获取报价数据并将其存储在MetaTrader 5中的技术基础。

在价格数据可在终端中用于在图表上显示并传输给MQL程序之前,它们会从服务器下载并以特殊方式进行准备。从服务器获取数据的机制,并不取决于请求是如何发起的——无论是用户在浏览图表时发起的,还是通过MQL5语言以编程方式发起的。

数据以压缩格式从服务器传输过来:这些是经过高效打包的分钟K线数据块,但它们并不是通常意义上的M1周期K线。

从服务器接收到的数据会自动解压缩,并以特殊的HCC中间格式保存。每个交易品种的数据都会写入一个单独的文件夹{terminal_dir}/bases/{server_name}/history/{symbol_name}中。例如,来自MetaQuotes-Demo交易服务器的欧元兑美元(EURUSD)数据可能位于文件夹C:/Program Files/MetaTrader 5/bases/MetaQuotes-Demo/history/EURUSD/中。

数据会写入扩展名为*.hcc的文件中:每个文件存储一年的一分钟K线数据。例如,EURUSD文件夹中的2021.hcc文件包含2021年的欧元兑美元分钟K线数据。这些文件用于为所有时间框架准备价格数据,并不用于直接访问。

HCC格式的服务文件充当为特定时间框架绘制价格数据的数据源。它们仅在图表或MQL程序请求时创建,并以*.hc扩展名的文件形式保存以供后续使用。

对于每个时间框架,数据的准备独立于其他时间框架。数据生成和可用的规则对所有时间框架都是相同的,包括M1时间框架。也就是说,尽管HCC格式中数据存储的单位是分钟K线,但它们的存在并不意味着在HC格式中以相同数量存在和可用的M1时间框架数据。

为了节省资源,时间框架数据仅在必要时加载并存储在随机存取存储器(RAM)中:如果长时间没有对数据进行访问,它们会从RAM中卸载(但仍保留在文件中)。如果某个时间序列长时间未被使用,这可能会导致下一次请求该时间序列时执行时间增加。如果计算机有足够的资源,所有常用的时间序列,特别是那些已打开其图表的时间序列,几乎可以立即获取。

从服务器接收新数据会导致自动更新所有时间框架的HC格式的已用价格数据,并重新计算所有相关指标。

当MQL程序访问特定交易品种和时间框架的数据时,有可能所需的时间序列尚未生成,或者尚未与交易服务器同步(例如,服务器上出现了更新的价格)。在这种情况下,应该以某种形式实现等待数据准备就绪的机制。

对于脚本来说,唯一的解决方案是使用循环,因为由于缺乏事件处理功能,它们没有其他选择。对于指标而言,绝对不建议使用这样的算法(与任何其他等待循环一样),因为它们会导致给定交易品种的所有指标计算以及价格数据的其他处理暂停。

对于智能交易系统(EA)和指标,最好使用事件处理模型。如果在处理OnTickOnCalculate事件时,未能获取所需时间序列的所有必要数据,那么应该退出事件处理程序,并等待在下次调用该处理程序时数据出现。

最大K线数量

需要注意的是,对于每个请求的交易品种/时间框架对,将计算的最大K线数量不会超过终端“选项”对话框中“图表中最大K线数”(Max. bars in chart)参数的值。因此,该参数不仅对任何时间框架的图表施加了限制,也对所有MQL程序施加了限制。

此限制主要是为了节省资源。在设置该参数的较大值时,应该记住,如果较低时间框架的价格数据历史记录足够深,存储时间序列和指标缓冲区的内存消耗可能会达到数百兆字节,并占用所有的RAM。

更改K线限制仅在重新启动客户端终端后才会生效。它会影响从服务器请求的数据量,以便构建所需数量的工作时间框架的K线。

该参数设置的限制并非绝对严格,在某些情况下可能会被超过。例如,如果在会话开始时,特定时间框架的报价历史记录足以达到整个限制数量,那么随着新K线的形成,它们的数量可能会超过当前参数值。可用K线的实际数量由Bars/iBars函数返回。

获取价格数组的特征

在读取时间序列数组之前,我们应该确保它们是可用的,并且具备所需的特征。SeriesInfoInteger函数用于获取基本属性,例如终端和服务器上可用历史数据的深度、针对特定交易品种/周期组合构建的K线数量,以及终端与服务器之间报价是否存在差异。

该函数有两种形式:第一种直接返回请求的值(类型为long),第二种使用通过引用传递的第四个参数result。在这种情况下,第二种形式返回成功标志(true)或错误标志(false)。无论哪种情况,都可以使用GetLastError函数找到错误代码。

c
long SeriesInfoInteger(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_SERIES_INFO_INTEGER property)

bool SeriesInfoInteger(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_SERIES_INFO_INTEGER property, long &result)

该函数允许您查询指定交易品种和时间框架的时间序列属性之一,或者查询整个交易品种历史的属性。所请求的属性由ENUM_SERIES_INFO_INTEGER类型的第三个参数来标识。此枚举包含所有可用属性:

标识符描述属性类型
SERIES_BARS_COUNT按交易品种/周期的K线数量,见Bars函数long
SERIES_FIRSTDATE按交易品种/周期的最早日期datetime
SERIES_LASTBAR_DATE按交易品种/周期的最后一根K线的开盘时间datetime
SERIES_SYNCHRONIZED终端和服务器上按交易品种/周期的数据同步标志bool
SERIES_SERVER_FIRSTDATE服务器上按交易品种的历史最早日期,与周期无关datetime
SERIES_TERMINAL_FIRSTDATE客户端终端上按交易品种的历史最早日期,与周期无关datetime

根据属性的本质,结果值应转换为特定类型的值(请参阅“属性类型”列)。

所有属性均返回当前时刻的值。

脚本SeriesInfo.mq5提供了查询所有属性的示例:

c
void OnStart()
{
   PRTF(SeriesInfoInteger(NULL, 0, SERIES_BARS_COUNT));
   PRTF((datetime)SeriesInfoInteger(NULL, 0, SERIES_FIRSTDATE));
   PRTF((datetime)SeriesInfoInteger(NULL, 0, SERIES_LASTBAR_DATE));
   PRTF((bool)SeriesInfoInteger(NULL, 0, SERIES_SYNCHRONIZED));
   PRTF((datetime)SeriesInfoInteger(NULL, 0, SERIES_SERVER_FIRSTDATE));
   PRTF((datetime)SeriesInfoInteger(NULL, 0, SERIES_TERMINAL_FIRSTDATE));
   PRTF(SeriesInfoInteger("ABRACADABRA", 0, SERIES_BARS_COUNT));
}

以下是在MQ Demo服务器上针对欧元兑美元(EURUSD)、H1时间框架获得的结果示例:

SeriesInfoInteger(NULL,0,SERIES_BARS_COUNT)=10001 / ok
(datetime)SeriesInfoInteger(NULL,0,SERIES_FIRSTDATE)=2020.03.02 10:00:00 / ok
(datetime)SeriesInfoInteger(NULL,0,SERIES_LASTBAR_DATE)=2021.10.08 14:00:00 / ok
(bool)SeriesInfoInteger(NULL,0,SERIES_SYNCHRONIZED)=false / ok
(datetime)SeriesInfoInteger(NULL,0,SERIES_SERVER_FIRSTDATE)=1971.01.04 00:00:00 / ok
(datetime)SeriesInfoInteger(NULL,0,SERIES_TERMINAL_FIRSTDATE)=2016.06.01 00:00:00 / ok
SeriesInfoInteger(ABRACADABRA,0,SERIES_BARS_COUNT)=0 / MARKET_UNKNOWN_SYMBOL(4301)

可用K线数量(Bars/iBars)

函数BarsiBars提供了一种更简便的方法来查询按交易品种/周期划分的时间序列中K线的总数(它们之间没有区别,iBars的存在是为了与MQL4兼容)。

c
int Bars(const string symbol, ENUM_TIMEFRAMES timeframe)        

int iBars(const string symbol, ENUM_TIMEFRAMES timeframe)

这些函数返回MQL程序针对给定交易品种和周期可获取的K线数量。该值受终端“选项”中“图表中最大K线数”(Max. bars in chart)参数的影响(请参阅“时间序列组织和存储的技术特点”部分的注释)。例如,如果下载到终端的某个特定时间框架的历史数据有20,000根K线,但在设置中限制为10,000根K线,那么第二个值将起决定作用。在终端启动后,这些函数将立即返回10,000根K线的数量,但随着新K线的形成,该数量会增加(如果有足够的可用内存)。在MQL5中,可以通过调用TerminalInfoInteger(TERMINAL_MAXBARS)来获取这个限制值。

此外,Bars函数还有第二种形式,它允许查询两个日期之间范围内的K线数量。

c
int Bars(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, datetime stop)

这样的请求仅查询开盘时间落在从startstop(包括两端)范围内的那些K线。startstop的指定顺序无关紧要:函数将从较小的时间到较大的时间来分析报价。

如果在调用Bars/iBars函数时,具有指定参数的时间序列数据尚未生成,或者尚未与交易服务器同步,该函数将返回null。在这种情况下,_LastError中的错误属性也将为0(没有错误,因为数据只是尚未下载或准备好)。在得到返回值0之后,可以使用SeriesInfoInteger(..., SERIES_SYNCHRONIZED)检查特定时间框架的同步情况,或者使用特殊的SymbolIsSynchronized函数检查交易品种的同步情况。

下一节中的脚本SeriesBars.mq5将展示如何使用这些函数的示例,同时还会介绍相关的iBarShift函数。

按时间搜索K线索引(iBarShift)

iBarShift函数可提供指定时间对应的K线编号。在这种情况下,K线的编号始终按照时间序列的方式进行,即索引0对应最右边、最新的K线,并且从右向左(向过去的方向)数值逐渐增大。

c
int iBarShift(const string symbol, ENUM_TIMEFRAMES timeframe, datetime time, bool exact = false)

该函数返回在指定交易品种/时间框架参数对的时间序列中,time参数值所属K线的索引。每根K线都由一个开盘时间和该序列中所有K线共有的持续时间(即周期)来表征。例如,在小时时间框架下,开盘时间标记为13:00的K线持续时间是从13:00:00到13:59:59(包括最后一分钟和一秒)。

如果不存在与指定时间对应的K线(例如,该时间处于非交易时间或非交易日),那么函数的行为会根据exact参数而有所不同:如果exact = true,函数将返回-1;如果exact = false,它将返回开盘时间小于指定时间的最近K线的索引。当不存在这样的K线时,也就是说,在指定时间之前没有历史数据,函数将返回-1。但这里有一个细微之处。

**注意!**如果iBarShift函数返回一个特定的K线编号,即一个不等于-1的值,这并不意味着随后通过该索引访问时间序列就一定能获取到该K线的价格或其他特征。特别是当请求的K线索引超过终端窗口中的K线限制(TerminalInfoInteger(TERMINAL_MAXBARS))时,就可能出现这种情况。随着新K线的形成,这种情况可能会发生:较旧的K线可能会向左移动超出限制范围,处于可见窗口之外,尽管名义上它们可能会在内存中保留一段时间。开发者应该始终检查这种情况。

让我们使用脚本SeriesBars.mq5来检验Bars/iBars函数(见上一节)和iBarShift函数的性能。

c
void OnStart()
{
   const datetime target = PRTF(ChartTimeOnDropped());
   PRTF(iBarShift(NULL, 0, target));
   PRTF(iBarShift(NULL, 0, target, true));
   PRTF(iBarShift(NULL, 0, TimeCurrent()));
   PRTF(Bars(NULL, 0, target, TimeCurrent()));
   PRTF(Bars(NULL, 0, TimeCurrent(), target));
   PRTF(iBars(NULL, 0));
   PRTF(Bars(NULL, 0));
   PRTF(Bars(NULL, 0, 0, TimeCurrent()));
   PRTF(Bars(NULL, 0, TimeCurrent(), TimeCurrent()));
}

在这里我们遇到了另一个不熟悉的函数ChartTimeOnDropped(我们稍后会描述它):它返回(在活动图表上)从导航器中用鼠标拖放到的特定K线的时间。首先,让我们将脚本拖放到图表上有报价的区域。

日志中会创建以下记录(根据您的设置、操作和当前时间,数字会有所不同):

ChartTimeOnDropped()=2021.10.01 09:00:00 / ok
iBarShift(NULL,0,target)=125 / ok
iBarShift(NULL,0,target,true)=125 / ok
iBarShift(NULL,0,TimeCurrent())=0 / ok
Bars(NULL,0,target,TimeCurrent())=126 / ok
Bars(NULL,0,TimeCurrent(),target)=126 / ok
iBars(NULL,0)=10004 / ok
Bars(NULL,0)=10004 / ok
Bars(NULL,0,0,TimeCurrent())=10004 / ok
Bars(NULL,0,TimeCurrent(),TimeCurrent())=0 / ok

在这种情况下,脚本被拖放到时间为2021.10.01 09:00的K线(使用的是小时时间框架)上。根据iBarShift函数,这个时间对应的是第125根K线。

从鼠标所在的K线到最后一根(当前时间)K线的数量是126。这与K线编号125是相符的,因为编号是从0开始的。

通过不同方式(iBars、不带日期范围的Bars以及从0到当前时刻TimeCurrent的完整范围的Bars)得到的图表上的K线总数都等于10004。终端设置的限制是10000,但在会话期间又形成了额外的4根小时K线。

对于现有的交易品种和时间框架,当exact = false时,当前时间所在K线的编号iBarShift(..., TimeCurrent())始终为0。如果exact = true,那么有时我们可能会得到-1,因为当所有市场工具的报价点到达时,服务器时间会增加,并且当前交易品种可能暂时不进行交易。然后服务器时间可能会比一根K线的时间提前更多,并且对于TimeCurrent来说,没有新的K线能正好与之对应。

如果我们将脚本拖放到当前最后一根K线右边的空白区域(即未来的时间),我们会得到类似这样的结果:

ChartTimeOnDropped()=2021.10.09 02:30:00 / ok
iBarShift(NULL,0,target)=0 / ok
iBarShift(NULL,0,target,true)=-1 / ok
Bars(NULL,0,target,TimeCurrent())=0 / ok
Bars(NULL,0,TimeCurrent(),target)=0 / ok
iBars(NULL,0)=10004 / ok
Bars(NULL,0)=10004 / ok
Bars(NULL,0,0,TimeCurrent())=10004 / ok
Bars(NULL,0,TimeCurrent(),TimeCurrent())=0 / ok

在搜索任何先前K线的模式下(exact = false),iBarShift函数返回0,因为当前K线是离未来最近的。然而,精确搜索(exact = true)得到的结果是-1。此外,计算从当前时间到“目标”未来时间范围内K线数量的Bars函数现在返回0(那里还没有K线)。

iBarShift函数对于编写多货币MQL程序特别有用。不同金融工具的交易时间表常常不一致,所以对于特定的时间,某一交易品种可能存在一根K线,而另一交易品种则不存在。使用iBarShift函数的最近(先前)K线搜索模式,您总是可以获取在同一时刻对于不同交易品种相关的带有价格的K线索引。通常,即使对于外汇交易品种,同一时间的历史K线索引也可能不同。

例如,以下指令将记录在一小时时间框架下(MQ Demo服务器),三个交易品种(欧元兑美元(EURUSD)、黄金兑美元(XAUUSD)、美元兑卢布(USDRUB))在相同日期范围内的不同K线数量及其编号:

c
PRTF(Bars("EURUSD", PERIOD_H1, D'2021.05.01', D'2021.09.01')); // 2087
PRTF(Bars("XAUUSD", PERIOD_H1, D'2021.05.01', D'2021.09.01')); // 1991
PRTF(Bars("USDRUB", PERIOD_H1, D'2021.05.01', D'2021.09.01')); // 694
PRTF(iBarShift("EURUSD", PERIOD_H1, D'2021.09.01')); // 671
PRTF(iBarShift("XAUUSD", PERIOD_H1, D'2021.09.01')); // 638
PRTF(iBarShift("USDRUB", PERIOD_H1, D'2021.09.01')); // 224

用于获取报价数组的Copy函数概述

MQL5 API中有几个函数可用于将报价时间序列读取到数组中。以下表格列出了这些函数及其作用:

函数作用
CopyRates将报价历史数据获取到MqlRates结构体数组中
CopyTime将K线开盘时间历史数据获取到datetime类型的数组中
CopyOpen将K线开盘价历史数据获取到double类型的数组中
CopyHigh将K线最高价历史数据获取到double类型的数组中
CopyLow将K线最低价历史数据获取到double类型的数组中
CopyClose将K线收盘价历史数据获取到double类型的数组中
CopyTickVolume将报价成交量历史数据获取到long类型的数组中
CopyRealVolume将交易所成交量历史数据获取到long类型的数组中
CopySpread将点差历史数据获取到int类型的数组中

所有这些函数的前两个参数都是所需交易品种的名称和时间周期,可用以下伪代码表示:

c
int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, ...)

此外,所有函数都有三种原型变体,它们的区别在于请求范围的设置方式:

  1. 起始K线索引和K线数量Copy***(..., int offset, int count, ...)
  2. 范围起始时间和K线数量Copy***(..., datetime start, int count, ...)
  3. 范围起始时间和结束时间Copy***(..., datetime start, datetime stop, ...)

同时,参数表示方式意味着请求的数据采用时间序列的索引方向,即索引为0的偏移位置存储当前未完成K线的数据,索引增加对应向价格历史更深处移动。因此,特别是对于第二种方式,指定的K线数量count将从偏移范围的起始位置开始反向计数,也就是时间递减的方向。

第三种方式提供了额外的灵活性:起始日期和结束日期(start/stop)的指定顺序无关紧要,因为函数无论如何都会返回从较小日期到较大日期范围内的数据。合适的K线是这样选择的:其开盘时间在时间计数start/stop之间,或者等于其中之一,即考虑范围[start; stop]包括边界。

选择哪种函数选项由开发者根据更重要的因素来决定:是获取保证数量的元素(例如,用于机器学习算法),还是覆盖特定的日期区间(例如,具有预定的统一市场行为)。

datetime类型的时间表示精度为1秒。start/stop值不必四舍五入到周期大小。例如,从14:59到16:01的范围将允许在H1时间框架下选择15:00和16:00的两根K线。具有相等且四舍五入标签的退化范围,例如H1报价中的15:00,对应一根K线。

即使start/stop参数中包含非零的小时/分钟/秒,也可以请求日线时间框架的K线(尽管D1时间框架的K线标签时间为00:00)。在这种情况下,仅选择开盘时间在start/stop最小值之后且到start/stop最大值之前的D1 K线(在这种情况下,与日线K线标签相等是不可能的,因为所需时间包含小时/分钟/秒)。例如,在D'2021.09.01 12:00'和D'2021.09.03 07:00'之间,有两个D1 K线的开盘时间——D'2021.09.02'和D'2021.09.03'。这些K线将包含在结果中。D'2021.09.01' K线的开盘时间为00:00,早于范围的开始时间,因此被丢弃。D'2021.09.03' K线包含在结果中,尽管该日只有上午7个小时落入该范围。另一方面,请求一天内的几个小时,例如,在D'2021.09.01 12:00'和D'2021.09.01 15:00'之间,将不会覆盖任何日线K线(D'2021.09.01' K线的开盘时间不在此范围内),因此接收数组将为空。

表格中所有函数的唯一区别在于接收数据的数组类型,该数组作为最后一个参数通过引用传递。例如,CopyRates函数将请求的数据放入MqlRates结构体数组中,CopyTime函数将K线开盘时间放入datetime类型的数组中,依此类推。

因此,通用的函数原型可以表示如下:

c
int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, int offset, int count, type &result[])
int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, int count, type &result[])
int Copy***(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, datetime stop, type &result[])

这里,type根据具体函数匹配MqlRatesdatetimedoublelongint中的任何一种类型。

这些函数返回复制到数组中的元素数量,出错时返回 -1。特别是,如果服务器上请求的区间没有数据,或者该区间超出了图表上的最大K线数量(TerminalInfoInteger(TERMINAL_MAXBARS)),我们将得到 -1。

重要的是要注意,在接收数组中,接收到的数据总是按时间顺序从过去到未来物理放置。因此,如果对接收数组使用标准索引(即函数ArraySetAsSeries),则索引为0的元素将是最旧的,最后一个元素将是最新的。如果对数组执行了ArraySetAsSeries(result, true)指令,则编号将按时间序列的反向顺序进行:第0个元素将是范围内最新的,最后一个元素将是最旧的。这在以下图中得到说明。

当成功执行时,将从终端自身(内部)时间序列中复制指定数量的元素到目标数组。当按日期范围(start/stop)请求数据时,结果数组中的元素数量将根据该范围内的历史内容间接确定。因此,为了复制先前未知数量的值,建议使用动态数组:复制函数会自动分配目标数组所需的大小(大小可以增加或减少)。

如果需要复制已知数量的元素,或者频繁进行复制操作,例如在智能交易系统的每次OnTick调用或指标的每次OnCalculate调用中,最好使用静态分配的数组。事实上,动态数组的内存分配操作需要额外的时间,并且可能会影响性能,特别是在测试和优化期间。

对于不同类型的MQL程序,如果请求的数据尚未准备好,访问时间序列的方式不同。例如,在自定义指标中,Copy函数会立即返回错误,因为指标在终端的公共界面线程中执行,不能等待数据接收(假设指标将在其事件处理程序的下一次调用中请求数据,届时时间序列将已下载并构建)。此外,在指标章节中,我们将了解到,要访问指标所在“原生”图表的报价,不需要使用Copy函数,因为所有时间序列都会通过OnCalculate处理程序的数组参数自动传递。

当从智能交易系统和脚本访问时,会尝试多次接收数据,并进行短暂暂停(函数内部有等待),这为加载和计算缺失的时间序列提供了时间。函数将返回在超时到期时准备好的数据量,但历史加载将继续,下一次类似的请求将返回更多数据。

无论如何,您应该做好准备,Copy函数可能会返回错误而不是数据(原因有很多:连接失败、请求的数据缺失、如果并行请求许多新的时间序列会导致处理器负载过高):在代码中分析问题的原因(_LastError),稍后再试,调整设置,或者通知用户。

使用Copy函数请求时间序列时,交易品种是否在“市场报价”窗口中并非必要条件。然而,对于包含在该窗口中的交易品种,查询往往运行得更快,因为已经从服务器下载了一些数据,并且可能已经针对请求的周期进行了计算。我们将在“编辑市场报价列表”部分学习如何以编程方式将交易品种添加到“市场报价”窗口中。

为了说明这些函数在实践中的工作原理,让我们考虑脚本SeriesCopy.mq5。它包含对CopyTime函数的多次调用,这使得我们可以直观地看到时间戳和K线编号之间的关联。

脚本定义了一个动态数组times来接收数据。所有请求都是针对“EURUSD”交易品种和H1时间框架进行的。

c
void OnStart()
{
    datetime times[];

    // 首先,请求从2021年9月5日开始向过去的10根K线
    PRTF(CopyTime("EURUSD", PERIOD_H1, D'2021.09.05', 10, times)); // 10 / ok
    ArrayPrint(times);
    /*
    [0] 2021.09.03 14:00 2021.09.03 15:00 2021.09.03 16:00 2021.09.03 17:00 2021.09.03 18:00
    [5] 2021.09.03 19:00 2021.09.03 20:00 2021.09.03 21:00 2021.09.03 22:00 2021.09.03 23:00
    */

    // 默认情况下,数组输出按时间顺序进行(尽管函数参数设置为时间序列的反向坐标系)。让我们更改接收数组的索引顺序并再次输出。
    PRTF(ArraySetAsSeries(times, true)); // true / ok
    ArrayPrint(times);
    /*
    [0] 2021.09.03 23:00 2021.09.03 22:00 2021.09.03 21:00 2021.09.03 20:00 2021.09.03 19:00
    [5] 2021.09.03 18:00 2021.09.03 17:00 2021.09.03 16:00 2021.09.03 15:00 2021.09.03 14:00
    */

    // 对于接下来的实验,我们将恢复通常的顺序。
    PRTF(ArraySetAsSeries(times, false)); // true / ok

    // 现在让我们请求两个时间点之间不确定数量的K线(数量未知,因为范围内可能有假期等)。我们将通过两种方式进行:第一种情况,我们指定从未来到过去的范围,第二种情况,从过去到未来。结果相同。
    //                                      FROM                 TO
    PRTF(CopyTime("EURUSD", PERIOD_H1, D'2021.09.06 03:00', D'2021.09.05 03:00', times));
    ArrayPrint(times);
    //                   FROM                 TO
    PRTF(CopyTime("EURUSD", PERIOD_H1, D'2021.09.05 03:00', D'2021.09.06 03:00', times));
    ArrayPrint(times);
    /*
    CopyTime(EURUSD,PERIOD_H1,D'2021.09.06 03:00',D'2021.09.05 03:00',times)=4 / ok
    2021.09.06 00:00 2021.09.06 01:00 2021.09.06 02:00 2021.09.06 03:00
    CopyTime(EURUSD,PERIOD_H1,D'2021.09.05 03:00',D'2021.09.06 03:00',times)=4 / ok
    2021.09.06 00:00 2021.09.06 01:00 2021.09.06 02:00 2021.09.06 03:00
    */

    // 通过打印数组,我们可以看到它们是相同的。让我们回到时间序列索引模式并讨论另一个要点。
    PRTF(ArraySetAsSeries(times, true)); // true / ok
    ArrayPrint(times);
    // 2021.09.06 03:00 2021.09.06 02:00 2021.09.06 01:00 2021.09.06 00:00

    // 虽然两个时间戳相差24小时,这意味着数组中应该有25个元素(记住起始和结束是包含在内的),但结果只包含4根K线。事实是,9月5日是周日,因此,在整个范围内,仅在6日上午进行了交易。
    // 此外,注意接收数组的大小已自动从10个元素减少到4个元素。

    // 最后,我们将请求从第100根K线开始的10根K线(获得的结果将取决于您的当前时间和可用历史数据)。
    PRTF(CopyTime("EURUSD", PERIOD_H1, 100, 10, times)); // 10 / ok
    ArrayPrint(times);
    /*
    [0] 2021.10.04 19:00 2021.10.04 18:00 2021.10.04 17:00 2021.10.04 16:00 2021.10.04 15:00
    [5] 2021.10.04 14:00 2021.10.04 13:00 2021.10.04 12:00 2021.10.04 11:00 2021.10.04 10:00
    */
}

由于采用时间序列的索引方式,数组按逆时间顺序显示。

将报价作为MqlRates结构体数组获取

要请求包含所有K线特征的报价数组,请使用具有多个重载形式的CopyRates函数。

c
int CopyRates(const string symbol, ENUM_TIMEFRAMES timeframe, int offset, int count, MqlRates &rates[])

int CopyRates(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, int count, MqlRates &rates[])

int CopyRates(const string symbol, ENUM_TIMEFRAMES timeframe, datetime start, datetime stop, MqlRates &rates[])

该函数将指定参数(交易品种、时间框架,以及通过K线编号或datetime类型的start/stop值指定的时间范围)的历史数据获取到rates数组中。

函数返回复制的数组元素数量,如果出错则返回 -1,错误代码可从_LastError中获取。特别是,如果指定了不存在的交易品种、服务器上的区间不包含数据,或者该区间超出了图表上K线数量的限制(TerminalInfoInteger (TERMINAL_MAXBARS)),就会发生错误。

使用此函数的基本要点与所有Copy函数相同,已在“用于获取报价数组的Copy函数概述”部分进行了概述。

内联类型结构体MqlRates描述如下:

c
struct MqlRates
{
   datetime time;         // K线开盘时间
   double   open;         // 开盘价
   double   high;         // 每根K线的最高价
   double   low;          // 每根K线的最低价
   double   close;        // 收盘价
   long     tick_volume;  // 每根K线的报价成交量
   int      spread;       // 每根K线的最小点差(以点数为单位)
   long     real_volume;  // 每根K线的交易所成交量
};

让我们尝试在脚本SeriesStats.mq5中应用此函数来计算K线的平均大小。在输入变量中,我们将提供选择工作交易品种、时间框架、分析的K线数量以及向过去的初始偏移量(0表示从当前K线开始分析)的功能。

c
input string WorkSymbol = NULL; // 交易品种(留空表示当前品种)
input ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT;
input int BarOffset = 0;
input int BarCount = 10000;
 
void OnStart()
{
   MqlRates rates[];
   double range = 0, move = 0; // 计算K线的波动范围和价格变动
   
   PrintFormat("Requesting %d bars on %s %s", 
      BarCount, StringLen(WorkSymbol) > 0 ? WorkSymbol : _Symbol, 
      EnumToString(TimeFrame == PERIOD_CURRENT ? _Period : TimeFrame));
   
   // 请求关于BarCount根K线的所有信息并存储到MqlRates数组中
   const int n = PRTF(CopyRates(WorkSymbol, TimeFrame, BarOffset, BarCount, rates));
   
   // 在循环中计算波动范围和价格变动的平均值
   for(int i = 0; i < n; ++i)
   {
      range += (rates[i].high - rates[i].low) / n;
      move += (fmax(rates[i].open, rates[i].close)
             - fmin(rates[i].open, rates[i].close)) / n;
   }
   
   PrintFormat("Stats per bar: range=%f, movement=%f", range, move);
   PrintFormat("Dates: %s - %s", 
      TimeToString(rates[0].time), TimeToString(rates[n - 1].time));
}

将此脚本拖放到欧元兑美元(EURUSD)、H1时间框架的图表上,我们大约会得到以下结果:

Requesting 100000 bars on EURUSD PERIOD_H1
CopyRates(WorkSymbol,TimeFrame,BarOffset,BarCount,rates)=20018 / ok
Stats per bar: range=0.001280, movement=0.000621
Dates: 2018.07.19 15:00 - 2021.10.11 17:00

由于终端的K线数量限制为20,000根,请求100,000根K线只能返回20018根(限制数量以及会话开始后新形成的K线)。数组的第一个元素(索引为0)包含时间为2018.07.19 15:00的K线,最后一个元素包含时间为2021.10.11 17:00的K线。

根据统计数据,在此期间K线的平均波动范围为128个点,开盘价和收盘价之间的变动为62个点。

当使用起始日期和结束日期(start/stop)请求信息时,请记住两个边界都是包含在内的。因此,要设置与更高时间框架的任何一根K线相对应的区间,应该从右边界减去1秒。我们将在“按K线索引读取价格、成交量、点差和时间”部分的脚本SeriesSpread.mq5示例中应用此技巧。

分别请求价格、成交量、点差和时间数组

你可以选择将特定字段(价格、成交量、点差或时间)的数据读取到单独的数组中,而不是将所有K线特征作为MqlRates数组进行查询。为此,定义了几个函数,它们遵循“用于获取报价数组的Copy函数概述”部分中讨论的通用原则。

下面的脚本SeriesRates.mq5使用复制开盘价(Open)、最高价(High)、最低价(Low)和收盘价(Close)(OHLC)的函数,并将结果与调用CopyRates的结果进行比较。

c
void OnStart()
{
    const int N = 10;
    MqlRates rates[];

    // 请求并显示MqlRates数组中N根K线的所有信息
    PRTF(CopyRates("EURUSD", PERIOD_D1, D'2021.10.01', N, rates));
    ArrayPrint(rates);

    // 现在分别请求OHLC价格
    double open[], high[], low[], close[];
    PRTF(CopyOpen("EURUSD", PERIOD_D1, D'2021.10.01', N, open));
    PRTF(CopyHigh("EURUSD", PERIOD_D1, D'2021.10.01', N, high));
    PRTF(CopyLow("EURUSD", PERIOD_D1, D'2021.10.01', N, low));
    PRTF(CopyClose("EURUSD", PERIOD_D1, D'2021.10.01', N, close));

    // 比较通过不同方法获取的价格
    for(int i = 0; i < N; ++i)
    {
        if(rates[i].open != open[i]
        || rates[i].high != high[i]
        || rates[i].low != low[i]
        || rates[i].close != close[i])
        {
            // 不应该出现这种情况
            Print("Data mismatch at ", i);
            return;
        }
    }

    Print("Copied OHLC arrays match MqlRates array"); // 成功:数据匹配
}

运行脚本后,日志中会出现以下记录:

CopyRates(EURUSD,PERIOD_D1,D'2021.10.01',N,rates)=10 / ok
                 [time]  [open]  [high]   [low] [close] [tick_volume] [spread] [real_volume]
[0] 2021.09.20 00:00:00 1.17272 1.17363 1.17004 1.17257         58444        0             0
[1] 2021.09.21 00:00:00 1.17248 1.17486 1.17149 1.17252         58514        0             0
[2] 2021.09.22 00:00:00 1.17240 1.17555 1.16843 1.16866         72571        0             0
[3] 2021.09.23 00:00:00 1.16860 1.17501 1.16835 1.17381         68536        0             0
[4] 2021.09.24 00:00:00 1.17379 1.17476 1.17007 1.17206         51401        0             0
[5] 2021.09.27 00:00:00 1.17255 1.17255 1.16848 1.16952         57807        0             0
[6] 2021.09.28 00:00:00 1.16940 1.17032 1.16682 1.16826         64793        0             0
[7] 2021.09.29 00:00:00 1.16825 1.16901 1.15894 1.15969         68964        0             0
[8] 2021.09.30 00:00:00 1.15963 1.16097 1.15626 1.15769         68517        0             0
[9] 2021.10.01 00:00:00 1.15740 1.16075 1.15630 1.15927         66777        0             0
CopyOpen(EURUSD,PERIOD_D1,D'2021.10.01',N,open)=10 / ok
CopyHigh(EURUSD,PERIOD_D1,D'2021.10.01',N,high)=10 / ok
CopyLow(EURUSD,PERIOD_D1,D'2021.10.01',N,low)=10 / ok
CopyClose(EURUSD,PERIOD_D1,D'2021.10.01',N,close)=10 / ok
Copied OHLC arrays match MqlRates array

需要注意的是,tick_volume字段中的报价成交量是一个周期内报价点的简单计数器。对于非交易所交易工具(如这里的欧元兑美元),real_volume字段中的交易所成交量为零。

另外,在“用于获取报价数组的Copy函数概述”部分的脚本SeriesCopy.mq5中给出了使用CopyTime函数的示例。

按K线索引读取价格、成交量、点差和时间

有时,我们需要获取的不是一系列K线的信息,而仅仅是某一根K线的信息。从理论上讲,这可以通过使用前面讨论过的Copy函数来实现,在这些函数中指定数量(参数count)为1,但这样做不是很方便。下面的这些函数提供了一种更简单的方式,它们可以根据时间序列中K线的编号返回某一特定类型的一个值。

所有这些函数都有相似的原型,但名称和返回类型不同。从历史角度看,函数名以前缀i开头,即形如iValue(这些函数属于一大类内置技术指标:毕竟,报价特征是技术分析的主要来源,几乎所有指标都是其衍生出来的,所以用字母i表示)。

c
type iValue(const string symbol, ENUM_TIMEFRAMES timeframe, int offset)

这里的type根据具体函数对应datetimedoublelongint中的一种类型。symboltimeframe用于确定所请求的时间序列。所需的K线索引offset按照时间序列的表示方法传递:0表示最新的、最右边的K线(通常尚未完成),数值越大表示的K线越旧。与Copy函数的情况一样,NULL和0可用于将交易品种和周期设置为与当前图表的属性相同。

由于i系列函数等同于调用Copy函数,“用于获取报价数组的Copy函数概述”部分中描述的从不同类型程序请求时间序列的所有特点也适用于它们。

函数描述
iTimeK线开盘时间
iOpenK线开盘价
iHighK线最高价
iLowK线最低价
iCloseK线收盘价
iTickVolumeK线报价成交量(类似于iVolume
iVolumeK线报价成交量(类似于iTickVolume
iRealVolumeK线的实际交易量
iSpreadK线最小点差(以点为单位)

这些函数返回请求的值,如果出错则返回0(遗憾的是,在某些情况下0可能是一个真实的值)。要获取关于错误的更多信息,请调用GetLastError函数。

这些函数不会缓存结果。每次调用时,它们都会从指定交易品种/周期的时间序列中返回实际数据。这意味着在没有准备好的数据时(首次调用,或同步丢失后),函数可能需要一些时间来准备结果。

例如,让我们尝试对每根K线的点差大小进行一个较为现实的估计。最小点差值存储在报价中,这可能会在设计交易策略时导致不合理的过高预期。要获取每根K线的平均、中位数或最大点差的绝对准确值,需要分析实际的报价点,但我们还没有学习如何处理它们。此外,这将是一个非常耗费资源的过程。一个更合理的方法是分析较低的M1时间框架的点差:对于较高时间框架的K线,只需在M1的内部K线中寻找最大点差就足够了。当然,严格来说,这不是真正的最大值,而是最小值中的最大值,但考虑到分钟数据的短暂性,我们可以希望至少在某些M1 K线中检测到特征性的点差扩大情况,这对于获得可接受的分析精度和速度的比例已经足够了。

脚本SeriesSpread.mq5实现了该算法的一个版本。在输入变量中,你可以设置交易品种、时间框架以及要分析的K线数量。默认情况下,处理当前图表的交易品种及其周期(应该大于M1)。

c
input string WorkSymbol = NULL; // 交易品种(留空表示当前品种)
input ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT;
input int BarCount = 100;

由于对于每根K线来说,只有其时间和点差信息是重要的,所以定义了一个有两个字段的特殊结构体。我们本可以使用标准的MqlRates结构体,并将“最大”点差添加到一些未使用的字段中(例如,对于外汇交易品种的real_volume字段),但这样的话,大多数字段的数据都会被复制,从而浪费内存。

c
struct SpreadPerBar
{
   datetime time;
   int spread;
};

使用新的结构体类型,我们准备了peaks数组来计算指定数量K线的数据。

c
void OnStart()
{
   SpreadPerBar peaks[];
   ArrayResize(peaks, BarCount);
   ZeroMemory(peaks);
   ...

接下来,算法的主要部分在K线循环中执行。对于每根K线,我们使用iTime函数来确定定义该K线边界的两个时间戳。实际上,这是第i根K线和相邻的第(i + 1)根K线的开盘时间。根据索引原则,我们可以说第(i + 1)根K线是前一根K线(更旧,见变量prev),第i根K线是后一根K线(更新,见变量next)。K线开盘时间只属于一根K线,也就是说,标签prev包含在第(i + 1)根K线中,标签next在第i根K线中。因此,在处理每根K线时,其右边界应从区间[prev; next)中排除。

我们关注的是一分钟时间框架的点差,因此对于PERIOD_M1时间框架,我们将使用CopySpread函数。在这种情况下,通过将start/stop参数设置为精确的prev值和减少1秒的next值来实现半开区间。点差信息被复制到动态数组spreads中(其内存由函数自身分配)。

c
   for(int i = 0; i < BarCount; ++i)
   {
      int spreads[]; // 用于接收第i根K线内部M1点差的数组
      const datetime next = iTime(WorkSymbol, TimeFrame, i);
      const datetime prev = iTime(WorkSymbol, TimeFrame, i + 1);
      const int n = CopySpread(WorkSymbol, PERIOD_M1, prev, next - 1, spreads);
      const int m = ArrayMaximum(spreads);
      if(m > -1)
      {
         peaks[i].spread = spreads[m];
         peaks[i].time = prev;
      }
   }

然后,我们在这个数组中找到最大值,并将其与K线时间一起保存到相应的SpreadPerBar结构体中。请注意,未完成的第0根K线不包含在分析中(如有必要,可以补充算法)。

循环完成后,我们将结构体数组输出到日志中。

c
   PrintFormat("Maximal speeds per intraday bar\nProcessed %d bars on %s %s", 
      BarCount, StringLen(WorkSymbol) > 0 ? WorkSymbol : _Symbol, 
      EnumToString(TimeFrame == PERIOD_CURRENT ? _Period : TimeFrame));
   ArrayPrintM(peaks);

在欧元兑美元(EURUSD)、H1时间框架的图表上运行该脚本,我们将得到每小时K线内部的点差统计信息(节选):

Maximal speeds per intraday bar
Processed 100 bars on EURUSD PERIOD_H1
[ 0] 2021.10.12 14:00        1
[ 1] 2021.10.12 13:00        1
[ 2] 2021.10.12 12:00        1
[ 3] 2021.10.12 11:00        1
[ 4] 2021.10.12 10:00        0
[ 5] 2021.10.12 09:00        1
[ 6] 2021.10.12 08:00        2
[ 7] 2021.10.12 07:00        2
[ 8] 2021.10.12 06:00        1
[ 9] 2021.10.12 05:00        1
[10] 2021.10.12 04:00        1
[11] 2021.10.12 03:00        1
[12] 2021.10.12 02:00        4
[13] 2021.10.12 01:00       16
[14] 2021.10.12 00:00       65
[15] 2021.10.11 23:00       15
[16] 2021.10.11 22:00        2
[17] 2021.10.11 21:00        1
[18] 2021.10.11 20:00        1
[19] 2021.10.11 19:00        2
[20] 2021.10.11 18:00        1
[21] 2021.10.11 17:00        1
[22] 2021.10.11 16:00        1
[23] 2021.10.11 15:00        2
[24] 2021.10.11 14:00        1

很明显,夜间点差会增加:例如,接近午夜时,报价中的点差为7 - 15点,而在我们的测量中,它们为15 - 65点。然而,在其他时间段也发现了非零值,尽管每小时K线的指标通常包含零值。

在时间序列中查找最大值和最小值

在用于处理报价时间序列的函数组中,有两个函数提供了最简单的聚合处理功能:分别用于在给定区间内搜索序列的最大值和最小值,即iHighestiLowest

c
int iHighest(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_SERIESMODE type, int count = WHOLE_ARRAY, int offset = 0)

int iLowest(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_SERIESMODE type, int count = WHOLE_ARRAY, int offset = 0)

这些函数返回特定时间序列类型的最大/最小值的索引,该时间序列由交易品种/时间框架参数对以及ENUM_SERIESMODE枚举元素(它描述了我们已经熟悉的报价字段)指定。

标识符描述
MODE_OPEN开盘价
MODE_LOW最低价
MODE_HIGH最高价
MODE_CLOSE收盘价
MODE_VOLUME报价成交量
MODE_REAL_VOLUME实际成交量
MODE_SPREAD点差

offset参数指定开始搜索的索引。编号方式与时间序列中的一致,即offset值增加表示向过去的方向移动,索引0表示当前K线(这是默认值)。分析的K线数量由count参数指定(默认值为WHOLE_ARRAY)。

如果出错,这些函数返回 -1。使用GetLastError函数可以找到错误代码。

为了演示其中一个函数(iHighest)的工作方式,让我们修改上一节中关于估计每根K线实际点差大小的示例,并比较结果。当然,它们必须匹配。新版本的脚本包含在文件SeriesSpreadHighest.mq5中。

这些更改影响了SpreadPerBar结构体以及OnStart函数内部的工作循环。

向结构体中添加了一些字段,以便理解新函数的工作方式。由于算法的特性,这些字段不是必需的。

c
struct SpreadPerBar
{
   datetime time;
   int spread;
   int max; // 具有最大点差值的M1 K线的索引,该点差值在当前较高时间框架K线内的所有M1 K线中最大
   int num; // 当前较高时间框架K线内的M1 K线数量
   int pos; // 当前较高时间框架K线内M1 K线的初始索引
};

主要的修改影响了OnStart函数,但它们都局限在循环内部(所有其他代码片段保持不变)。

c
   for(int i = 0; i < BarCount; ++i)
   {
      const datetime next = iTime(WorkSymbol, TimeFrame, i);
      const datetime prev = iTime(WorkSymbol, TimeFrame, i + 1);
     ...

与之前一样,定义了当前K线的边界prevnext。然而,我们不是将这些标签之间的时间序列元素复制到自己的数组spreads中,然后对其调用ArrayMaximum函数,而是确定构成当前较高时间框架K线的M1 K线的索引和数量。具体做法如下。

iBarShift函数可以让我们找到在M1历史数据中时间为next - 1的K线右边界所在的偏移量(变量p)。Bars函数计算落在prevnext - 1之间的M1 K线数量(变量n)。这两个值成为调用iHighest函数的参数,用于在从索引p开始的n根M1 K线中查找MODE_SPREAD类型的最大值。如果顺利找到最大值(m > -1),我们只需使用iSpread获取相应的值并将其放入结构体中。

c
      const int p = iBarShift(WorkSymbol, PERIOD_M1, next - 1);
      const int n = Bars(WorkSymbol, PERIOD_M1, prev, next - 1);
      const int m = iHighest(WorkSymbol, PERIOD_M1, MODE_SPREAD, n, p);
      if(m > -1)
      {
         peaks[i].spread = iSpread(WorkSymbol, PERIOD_M1, m);
         peaks[i].time = prev;
         peaks[i].max = m;
         peaks[i].num = n;
         peaks[i].pos = p;
      }
   }

当将结果数组输出到日志中时,我们现在还会额外看到M1 K线的索引,即较高时间框架K线“开始”的位置以及在其中找到最大点差的位置。“开始”这个词加上引号是因为随着新的M1 K线到来,这些索引会增加,每个K线的虚拟“开始”位置会移动,尽管历史K线的开盘时间当然是保持不变的。

Maximal speeds per intraday bar
Processed 100 bars on EURUSD PERIOD_H1
               [time] [spread] [max] [num] [pos]
[ 0] 2021.10.12 15:00        0     7    60     7
[ 1] 2021.10.12 14:00        1    89    60    67
[ 2] 2021.10.12 13:00        1   181    60   127
[ 3] 2021.10.12 12:00        1   213    60   187
[ 4] 2021.10.12 11:00        1   248    60   247
[ 5] 2021.10.12 10:00        0   307    60   307
[ 6] 2021.10.12 09:00        1   385    60   367
[ 7] 2021.10.12 08:00        2   469    60   427
[ 8] 2021.10.12 07:00        2   497    60   487
[ 9] 2021.10.12 06:00        1   550    60   547
[10] 2021.10.12 05:00        1   616    60   607
[11] 2021.10.12 04:00        1   678    60   667
[12] 2021.10.12 03:00        1   727    60   727
[13] 2021.10.12 02:00        4   820    60   787
[14] 2021.10.12 01:00       16   906    60   847
[15] 2021.10.12 00:00       65   956    60   907
[16] 2021.10.11 23:00       15   967    60   967
[17] 2021.10.11 22:00        2  1039    60  1027
[18] 2021.10.11 21:00        1  1090    60  1087
[19] 2021.10.11 20:00        1  1148    60  1147
[20] 2021.10.11 19:00        2  1210    60  1207
[21] 2021.10.11 18:00        1  1313    60  1267
[22] 2021.10.11 17:00        1  1345    60  1327
[23] 2021.10.11 16:00        1  1411    60  1387
[24] 2021.10.11 15:00        2  1461    60  1447
[25] 2021.10.11 14:00        1  1526    60  1507
...

例如,在脚本启动时,标签为2021.10.12 14:00的K线从第67根M1 K线开始(即它是在67分钟前开盘的),并且在这个H1 K线内部具有最大点差的M1 K线的索引为89。显然,这个索引应该小于前一个H1 K线(2021.10.12 13:00)开始的M1 K线的编号:它是在127分钟前标记的。反过来,在这个H1 K线中,找到了索引为181的最大点差。并且这个索引小于更早的K线(2021.10.12 12:00)的索引187。

posmax列中的索引不断增加,因为我们是按从当前到过去的顺序遍历K线的。num列几乎总是60,因为大多数H1 K线由60根M1 K线组成。但并非总是如此。例如,下面是不完整的小时K线,由较少的分钟组成:这可能是由于假期安排导致市场提前收盘的结果,或者是交易活动的实际缺口(缺乏流动性)。

...
[38] 2021.10.11 01:00       20  2346    60  2287
[39] 2021.10.11 00:00       85  2404    58  2347
[40] 2021.10.08 23:00       15  2406    55  2405
[41] 2021.10.08 22:00        2  2463    60  2460
...

使用MqlTick结构体处理真实报价点数组

MetaTrader 5不仅提供了处理报价(K线)历史数据的能力,还支持处理真实报价点的历史数据。从用户界面来看,所有历史数据都可以在“交易品种”对话框中获取。该对话框有三个选项卡:规格、K线和报价点。当在第一个选项卡的树状交易品种列表中选择特定的交易品种后,切换到“K线”和“报价点”选项卡时,你可以分别以K线或报价点的形式请求相应的报价数据。

在MQL程序中,也可以使用CopyTicksCopyTicksRange函数来获取真实报价点的历史数据。

c
int CopyTicks(const string symbol, MqlTick &ticks[], uint flags = COPY_TICKS_ALL, ulong from = 0, uint count = 0)

int CopyTicksRange(const string symbol, MqlTick &ticks[], uint flags = COPY_TICKS_ALL, ulong from = 0, ulong to = 0)

这两个函数都将指定交易品种的报价点数据请求到通过引用传递的ticks数组中。MqlTick结构体包含了关于一个报价点的所有信息,在MQL5中其描述如下:

c
struct MqlTick
{
   datetime time;        // 此价格更新的时间
   double   bid;         // 当前买价
   double   ask;         // 当前卖价
   double   last;        // 最后成交价
   ulong    volume;      // 最后成交价的成交量
   long     time_msc;    // 此价格更新的时间(以毫秒为单位)
   uint     flags;       // 标志位(结构体中哪些字段发生了变化)
   double   volume_real; // 最后成交价的更精确成交量
};

flags字段用于存储标志位的位掩码,标识报价点结构体中哪些字段包含已更改的值。

常量描述
TICK_FLAG_BID2买价已更改
TICK_FLAG_ASK4卖价已更改
TICK_FLAG_LAST8最后成交价已更改
TICK_FLAG_VOLUME16成交量已更改
TICK_FLAG_BUY32报价点是由买入交易产生的
TICK_FLAG_SELL64报价点是由卖出交易产生的

之所以需要这样,是因为每个报价点总是会填充所有字段,无论与前一个报价点相比数据是否发生了变化。这使得你在任何时候都能获取到价格的当前状态,而无需在报价点历史记录中查找先前的值。例如,一个报价点可能仅买价发生了变化,但除了新的价格之外,结构体中还会指示其他参数:之前的卖价、最后成交价、成交量等等。

同时,你应该记住,根据交易品种的类型,报价点中的某些字段可能始终为零(并且相应的掩码位永远不会为它们设置)。特别是对于外汇交易品种,通常lastvolumevolume_real字段会保持为空。

接收报价点的数组可以是固定大小的,也可以是动态的。无论请求的时间间隔(由CopyTicksRange函数中的from/to参数指定)或CopyTicks函数中的count参数中实际的报价点数量是多少,这些函数复制到固定数组中的报价点数量都不会超过数组的大小。在ticks数组中,最旧的报价点放在最前面,最新的报价点放在最后面。

在这两个函数的参数中,时间读数被指定为自1970年1月1日00:00:00以来的毫秒数。在CopyTicks函数中,请求的报价点范围由起始时间from和报价点数量count设置,而在CopyTicksRange函数中,由fromto设置(两个值都包含在内)。

换句话说,CopyTicksRange函数用于获取特定时间间隔内的报价点,并且事先不知道这些报价点的数量。CopyTicks函数保证不超过count个报价点,但不允许事先确定这些报价点将覆盖的时间间隔。

CopyTicksRange函数中fromto值的时间顺序并不重要:该函数无论如何都会给出从两个值中的最小值开始,到最大值结束的报价点。

CopyTicks函数将from参数视为最小时间的左边界,并从它开始向未来计数count个报价点。然而,有一个重要的例外:from = 0(默认值)被视为当前时刻,并且从它开始向过去计数报价点。这使得始终可以获取指定数量的最新报价点。当count = 0(默认值)时,该函数最多复制2000个报价点。

这两个函数都返回复制的报价点数量,如果出错则返回 -1。特别是,GetLastError可能返回以下错误代码:

  • ERR_HISTORY_TIMEOUT — 报价点同步超时,函数返回了已有的所有数据。
  • ERR_HISTORY_SMALL_BUFFER — 静态缓冲区太小,因此它给出了数组中能容纳的数据量。
  • ERR_NOT_ENOUGH_MEMORY — 未能分配所需的内存量,以便将指定范围内的报价点历史数据获取到动态数组中。

flags参数定义了请求的报价点类型。

常量描述
COPY_TICKS_INFO1由买价和/或卖价变化引起的报价点(TICK_FLAG_BIDTICK_FLAG_ASK
COPY_TICKS_TRADE2最后成交价和成交量发生变化的报价点(TICK_FLAG_LASTTICK_FLAG_VOLUMETICK_FLAG_BUYTICK_FLAG_SELL
COPY_TICKS_ALL3所有报价点

对于任何请求类型,MqlTick结构体中与标志位不匹配的其余字段将包含先前的实际值。例如,如果仅请求了信息报价点(COPY_TICKS_INFO),其余字段仍将被填充。这意味着如果仅买价发生了变化,卖价和成交量字段将写入最后已知的值。要了解报价点中发生了哪些变化,请分析其flags字段(可能是TICK_FLAG_BIDTICK_FLAG_ASK或两者的组合)。如果一个报价点的买价和卖价为零,并且标志位表明这些价格已发生变化(flags == TICK_FLAG_BID | TICK_FLAG_ASK),则表示订单簿已清空。

类似地,如果请求了交易报价点(COPY_TICKS_TRADE),最后已知的价格值将记录在买价和卖价字段中。在这种情况下,flags字段可能是TICK_FLAG_LASTTICK_FLAG_VOLUMETICK_FLAG_BUYTICK_FLAG_SELL的组合。

当请求COPY_TICKS_ALL时,将返回所有报价点。

调用CopyTicks/CopyTicksRange函数中的任何一个都会检查存储在硬盘上的给定交易品种的报价点库的同步情况。如果本地数据库中的报价点不足,缺失的报价点将自动从交易服务器下载。在这种情况下,将根据查询参数中的最早日期到当前时刻进行报价点同步。之后,该交易品种的所有传入报价点都将进入报价点数据库,并使其保持同步的最新状态。

报价点数据比分钟报价数据大得多。当首次请求报价点历史数据或通过真实报价点开始测试时,下载它们可能需要很长时间。报价点数据的历史记录以内部TKC格式存储在文件中,路径为{terminal_dir}/bases/{server_name}/ticks/{symbol_name}。每个文件包含一个月的信息。

在指标中,这些函数会立即返回结果,即它们按交易品种复制可用的报价点,并在数据不足时启动报价点库同步的后台进程。一个交易品种上的所有指标都在一个公共线程中工作,所以它们无权等待同步完成。同步结束后,函数的下一次调用将返回所有请求的报价点。

在智能交易系统和脚本中,函数最多可以等待45秒以获取结果:与指标不同,每个智能交易系统和脚本都在自己的线程中运行,因此可以在超时时间内等待同步完成。如果在此期间仍未按要求同步足够的报价点,则仅返回可用的报价点,并且同步将在后台继续进行。

回想一下,实时报价点作为事件广播到图表上:指标在OnCalculate处理程序中接收新报价点的通知,而智能交易系统在OnTick处理程序中接收它们。应该记住,系统不能保证交付所有事件。如果在程序处理当前OnCalculate/OnTick事件时,新的报价点到达终端,针对这个“繁忙”程序的新事件可能不会添加到其队列中(请参阅“事件处理函数概述”部分)。此外,多个报价点可能同时到达,但每个MQL程序只会生成一个事件:当前市场状态事件。在这种情况下,可以使用CopyTicks函数请求自上次处理事件以来到达的所有报价点。以下是此算法的伪代码:

c
void processAllTicks()
{
   static ulong prev = 0;
   if(!prev)
   {
      MqlTick ticks[];
      const int n = CopyTicks(_Symbol, ticks, COPY_TICKS_ALL, prev + 1, 1000000);
      if(n > 0)
      {
         prev = ticks[n - 1].time_msc;
         ... // 处理所有错过的报价点
      }
   }
   else
   {
      MqlTick tick;
      SymbolInfoTick(_Symbol, tick);
      prev = tick.time_msc;
      ... // 处理第一个报价点
   }
}

这里使用的SymbolInfoTick函数会将最后一个报价点数据填充到通过引用传递的单个MqlTick结构体中。我们将在单独的部分中学习它。

请注意,在调用CopyTicks时,会在旧的时间戳prev上增加一毫秒。这确保了不会再次处理先前的报价点。然而,如果在与prev对应的一毫秒内有多个报价点,此算法将跳过它们。如果你想涵盖绝对所有的报价点,则应在更新prev变量时记住prev时间的可用报价点数量。在下一次调用CopyTicks时,从prev时刻请求报价点,并跳过(在数组中忽略)“旧”报价点的数量。

然而,请注意,并非每个MQL程序都需要上述算法。大多数程序不会分析每个报价点,而与最后已知报价点对应的当前价格状态会在事件模型中快速广播到图表上,并可通过交易品种和图表属性获取。

为了演示这些函数的使用,让我们考虑两个示例,每个函数对应一个示例。对于这两个示例,开发了一个通用的头文件TickEnum.mqh,其中将上述请求报价点标志和报价点状态标志的常量汇总到两个枚举中。

c
enum COPY_TICKS
{
   ALL_TICKS = /* -1 */ COPY_TICKS_ALL,    // 所有报价点
   INFO_TICKS = /* 1 */ COPY_TICKS_INFO,   // 信息报价点
   TRADE_TICKS = /* 2 */ COPY_TICKS_TRADE, // 交易报价点
};
 
enum TICK_FLAGS
{
   TF_BID = /* 2 */ TICK_FLAG_BID, 
   TF_ASK = /* 4 */ TICK_FLAG_ASK, 
   TF_BID_ASK = TICK_FLAG_BID | TICK_FLAG_ASK, 
   
   TF_LAST = /* 8 */ TICK_FLAG_LAST, 
   TF_BID_LAST = TICK_FLAG_BID | TICK_FLAG_LAST, 
   TF_ASK_LAST = TICK_FLAG_ASK | TICK_FLAG_LAST, 
   TF_BID_ASK_LAST = TF_BID_ASK | TICK_FLAG_LAST, 
   
   TF_VOLUME = /* 16 */ TICK_FLAG_VOLUME, 
   TF_LAST_VOLUME = TICK_FLAG_LAST | TICK_FLAG_VOLUME, 
   TF_BID_VOLUME = TICK_FLAG_BID | TICK_FLAG_VOLUME, 
   TF_BID_ASK_VOLUME = TF_BID_ASK | TICK_FLAG_VOLUME, 
   TF_BID_ASK_LAST_VOLUME = TF_BID_ASK | TF_LAST_VOLUME, 
   
   TF_BUY = /* 32 */ TICK_FLAG_BUY, 
   TF_SELL = /* 64 */ TICK_FLAG_SELL, 
   TF_BUY_SELL = TICK_FLAG_BUY | TICK_FLAG_SELL, 
   TF_LAST_VOLUME_BUY = TF_LAST_VOLUME | TICK_FLAG_BUY, 
   TF_LAST_VOLUME_SELL = TF_LAST_VOLUME | TICK_FLAG_SELL, 
   TF_LAST_VOLUME_BUY_SELL = TF_BUY_SELL | TF_LAST_VOLUME, 
   ...
};

使用枚举使得在源代码中的类型检查更加严格,并且使用EnumToString函数将值的含义显示为字符串也更加容易。此外,TICK_FLAGS枚举中添加了最常用的标志组合,以优化报价点的可视化或过滤。不能给枚举元素与内置常量相同的名称,否则会发生名称冲突。

第一个脚本SeriesTicksStats.mq5使用CopyTicks函数来统计在给定历史深度内设置了不同标志位的报价点数量。

在输入参数中,你可以设置工作交易品种(默认为图表交易品种)、分析的报价点数量以及来自COPY_TICKS的请求模式。

c
input string WorkSymbol = NULL; // 交易品种(留空表示当前品种)
input int TickCount = 10000;
input COPY_TICKS TickType = ALL_TICKS;

TickFlagStats结构体中收集每个标志位(位掩码中的每个位)在报价点属性中出现的统计信息。

c
struct TickFlagStats
{
   TICK_FLAGS flag; // 带有位(一个或多个)的掩码
   int count;       // 在flags字段中具有此位的报价点数量 
   string legend;   // 位的描述
};

OnStart函数定义了一个大小为8个元素的TickFlagStats结构体数组:其中6个(从1到6,包括1和6)用于相应的TICK_FLAG位,另外两个用于位组合(见下文)。使用一个简单的循环,将各个标准位/标志的元素填充到数组中,循环结束后,填充两个组合掩码(在第0个元素中,将统计买价和卖价同时发生变化的报价点,在第7个元素中,统计同时有买入和卖出交易的报价点)。

c
void OnStart()
{
   TickFlagStats stats[8] = {};
   for(int k = 1; k < 7; ++k)
   {
      stats[k].flag = (TICK_FLAGS)(1 << k);
      stats[k].legend = EnumToString(stats[k].flag);
   }
   stats[0].flag = TF_BID_ASK;  // 买价和卖价的组合
   stats[7].flag = TF_BUY_SELL; // 买入和卖出的组合
   stats[0].legend = "TF_BID_ASK (COMBO)";
   stats[7].legend = "TF_BUY_SELL (COMBO)";
  ...

我们将所有主要工作委托给辅助函数CalcTickStats,将输入参数和准备好的统计数组传递给它。之后,只需在日志中显示统计的数量。

   const int count = CalcTickStats(TickType, 0, TickCount, stats);
   PrintFormat("%s stats requested: %d (got: %d) on %s", 
      EnumToString(TickType),
      TickCount, count, StringLen(WorkSymbol) > 0 ? WorkSymbol : _Symbol);
   ArrayPrint(stats);
}

代码功能概述

这段代码主要围绕MetaTrader 5中处理真实报价点数据展开,包含两个脚本:SeriesTicksStats.mq5SeriesTicksDeltaVolume.mq5。下面为你详细介绍这两个脚本的功能及实现细节。

脚本1:SeriesTicksStats.mq5

此脚本借助CopyTicks函数统计在给定历史深度内设置了不同标志位的报价点数量。

主要功能:

  1. 请求报价点数据:调用CopyTicks函数请求指定交易品种、类型和数量的报价点数据。
  2. 记录时间范围:若请求成功,记录接收到的报价点的时间范围。
  3. 统计标志位:遍历所有报价点,统计每个标志位在报价点属性中出现的次数。

代码示例:

c
int CalcTickStats(const string symbol, const COPY_TICKS type, 
   const datetime start, const int count, 
   TickFlagStats &stats[])
{
   MqlTick ticks[];
   ResetLastError();
   const int nf = ArraySize(stats);
   const int nt = CopyTicks(symbol, ticks, type, start * 1000, count);
   if(nt > -1 && _LastError == 0)
   {
      PrintFormat("Ticks range: %s'%03d - %s'%03d", 
         TimeToString(ticks[0].time, TIME_DATE | TIME_SECONDS),
         ticks[0].time_msc % 1000, 
         TimeToString(ticks[nt - 1].time, TIME_DATE | TIME_SECONDS),
         ticks[nt - 1].time_msc % 1000);
      
      // loop through ticks
      for(int j = 0; j < nt; ++j)
      {
         // loop through TICK_FLAGs (2 4 8 16 32 64) and combinations
         for(int k = 0; k < nf; ++k)
         {
            if((ticks[j].flags & stats[k].flag) == stats[k].flag)
            {
               stats[k].count++;
            }
         }
      }
   }
   return nt;
}

运行结果示例:

Ticks range: 2021.10.11 07:39:53'278 - 2021.10.13 11:51:29'428
ALL_TICKS stats requested: 100000 (got: 100000) on YNDX.MM
    [flag] [count]              [legend]
[0]      6   11323 "TF_BID_ASK (COMBO)" 
[1]      2   26700 "TF_BID"             
[2]      4   33541 "TF_ASK"             
[3]      8   51082 "TF_LAST"            
[4]     16   51082 "TF_VOLUME"          
[5]     32   25654 "TF_BUY"             
[6]     64   28802 "TF_SELL"            
[7]     96    3374 "TF_BUY_SELL (COMBO)"

脚本2:SeriesTicksDeltaVolume.mq5

该脚本使用CopyTicksRange函数计算每个K线的成交量差值。

主要功能:

  1. 获取K线时间范围:遍历每个K线,获取其时间范围。
  2. 请求报价点数据:调用CopyTicksRange函数请求该时间范围内的报价点数据。
  3. 计算成交量差值:根据报价点的标志位和价格变动,分别累计买入和卖出成交量,并计算差值。

代码示例:

c
void OnStart()
{
   DeltaVolumePerBar deltas[];
   ArrayResize(deltas, BarCount);
   ZeroMemory(deltas);

   for(int i = 0; i < BarCount; ++i)
   {
      MqlTick ticks[];
      const datetime next = iTime(WorkSymbol, TimeFrame, i);
      const datetime prev = iTime(WorkSymbol, TimeFrame, i + 1);
      ResetLastError();
      const int n = CopyTicksRange(WorkSymbol, ticks, COPY_TICKS_ALL, 
         prev * 1000, next * 1000 - 1);
      if(n > -1 && _LastError == 0)
      {
         deltas[i].time = prev; // remember the bar time
         for(int j = 0; j < n; ++j)
         {
            // when real volumes can be available, take them from ticks
            if(TickType == TRADE_TICKS)
            {
               // separately accumulate volumes for buy and sell deals
               if((ticks[j].flags & TICK_FLAG_BUY) != 0)
               {
                  deltas[i].buy += ticks[j].volume;
               }
               if((ticks[j].flags & TICK_FLAG_SELL) != 0)
               {
                  deltas[i].sell += ticks[j].volume;
               }
            }
            // when there are no real volumes, we evaluate them by the price movement up/down
            else
            if(TickType == INFO_TICKS && j > 0)
            {
               if((ticks[j].flags & (TICK_FLAG_ASK | TICK_FLAG_BID)) != 0)
               {
                  const long d = (long)(((ticks[j].ask + ticks[j].bid)
                               - (ticks[j - 1].ask + ticks[j - 1].bid)) / _Point);
                  if(d > 0) deltas[i].buy += d;
                  else deltas[i].sell += -d;
               }
            }
         }
         deltas[i].delta = (long)(deltas[i].buy - deltas[i].sell);
      }
   }

   PrintFormat("Delta volumes per intraday bar\nProcessed %d bars on %s %s %s", 
      BarCount, StringLen(WorkSymbol) > 0 ? WorkSymbol : _Symbol, 
      EnumToString(TimeFrame == PERIOD_CURRENT ? _Period : TimeFrame),
      EnumToString(TickType));
   ArrayPrint(deltas);
}

运行结果示例:

Delta volumes per intraday bar
Processed 100 bars on YNDX.MM PERIOD_H1 TRADE_TICKS
                  [time] [buy] [sell] [delta]
[ 0] 2021.10.13 11:00:00  7912  14169   -6257
[ 1] 2021.10.13 10:00:00  8470  11467   -2997
[ 2] 2021.10.13 09:00:00 10830  13047   -2217
[ 3] 2021.10.13 08:00:00 23682  19478    4204
[ 4] 2021.10.13 07:00:00 14538  11600    2938
[ 5] 2021.10.12 20:00:00  2132   4786   -2654
[ 6] 2021.10.12 19:00:00  9173  13775   -4602
[ 7] 2021.10.12 18:00:00  1297   1719    -422
[ 8] 2021.10.12 17:00:00  3803   2995     808
[ 9] 2021.10.12 16:00:00  6743   7045    -302
[10] 2021.10.12 15:00:00 17286  37286  -20000
[11] 2021.10.12 14:00:00 33263  54157  -20894
[12] 2021.10.12 13:00:00 56060  52659    3401
[13] 2021.10.12 12:00:00 12832  10489    2343
[14] 2021.10.12 11:00:00  7530   6092    1438
[15] 2021.10.12 10:00:00  6268  25201  -18933
...

总结

这两个脚本借助MetaTrader 5提供的CopyTicksCopyTicksRange函数,对真实报价点数据进行处理和分析,为交易决策提供了有价值的信息。