下载MetaTrader 5

MQL5 编程基础:时间

10 三月 2014, 07:18
Dmitry Fedoseev
0
2 912

目录

简介

MQL5 提供了大量处理时间的简单函数,学习起来不难。需要用到日期与时间的任务范围却非常小。主要的任务包括:

  • 在某给定时间点执行特定操作(图 1)。可以是每天的同一时间、一天中某个给定的时间、一周中给定的某一天,或只是于某个给定的日期与时间执行。

    图 1. 时间点
    图 1. 时间点

  • 在某个给定时间范围内(时段)启用或禁用特定操作。如此则可以在一天中加入一个时段(每天从一个时间点到另一个时间点),于一周的某天启用/禁用特定操作,从一周中某一天的某个给定时间到一周中另一天的某个给定时间的时间段,以及只是属于某指定日期与时间范围中的操作。

    图 2. 时间范围
    图 2. 时间范围

实际情况中,时间的使用相当复杂。而困难,都是与时间测量的特殊性以及 EA 交易和指标操作的环境有关:

  • 图表上因缺少价格变动而缺少的柱。如时间框架较短,就尤其明显:M1、M5 甚至是 M15。较长的时间框架上也能发现缺少的柱。

  • 某些交易中心的报价还包括实际上属于周一的周日柱。

  • 周末。周一之前是周五,而不是周日。周五之后是周一,而不是周六。

  • 除周日柱外,有些交易中心还提供连续报价,其中包括整个周末。尽管价格活动与工作日相比很少,但整个周末确实是有活动的。

  • 交易服务器与本地计算机(交易者的计算机和交易终端)之间的时区差异。不同交易中心的服务器时间可能有所不同。

  • 夏令时。

本文将从一些关于时间的一般理论讲起。之后,我们继续研究处理时间的标准 MQL5 函数,同时讲解一些编程技巧,通过处理实际问题实现圆熟。

本文写得很长,所以刚刚开始研究 MQL5 的编程新手很难一次掌握。最好能够投入三天或更长的时间。

时间测量的特殊性

我们先暂且岔开这个话题,来谈谈天文学。众所周知,地球围绕着太阳转动,同时又绕着自身轴线转动。地轴相对其围绕着太阳的轨道有少许的倾斜。地球绕其轴在天文(天体、地球)坐标中转动完整一圈所需的时间,即所谓的天文或恒星日。

地球上的普通人(相对天文学家而言)都不太关心恒星日。更重要的是昼夜的更替。一天一夜周期所需的时间被称为太阳日。从地球北极的上方看太阳系(图 3),就能看到地球在绕着自身轴线旋转的同时,还逆时针围绕着太阳转动。因此,为了绕着地轴相对于太阳完成一整圈的旋转,地球就必须转动 360 度多一点。如此一来,太阳日就要比恒星日长一点。

图 3. 地球绕其自轴旋转与围绕太阳旋转的方向(从地球北极上方看)
图 3. 地球绕其自轴旋转与围绕太阳旋转的方向(从地球北极上方看)

出于方便和准确考虑,太阳日被作为时间测量的基础。一个太阳日划分为 24 小时,一个小时又持续 60 分钟,如此等等。而恒星日则是 23 时 56 分 4 秒长。地轴相对其轨道面的略微倾斜,使得地球生灵们感受到明显的四季变化。

一年内的太阳日数量并不是个整数。实际上是稍微多一点——365 天零 6 小时。正因如此,日历才要做出周期性调整,每四年加上 1 天,也就是 2 月 29 日(闰年)。但是,这一调整又不是完全精确(稍微多加了点时间),所以有些年份,尽管是 4 的倍数,也并非闰年。以 "00" 为结尾的年份(100 的倍数)不进行日历调整。还不止如此。

如果某年同时是 100 和 400 的倍数,那么该年份即被视为闰年,且日历必须调整。1900 年是 4 和 100 的倍数,但却不是 400 的倍数,所以它不是闰年。2000 年是 4、100 和 400 的倍数,所以它是闰年。是 4 和 100 倍数的下一个年份是 2100 年,但因为它不是 400 的倍数,所以也不会是闰年。之后,本文的读者也就不会认为每一个成 4 倍数的年份都是闰年了。下一个是 100 的倍数、同时又是闰年的年份为 2400 年。

时区

地球绕其轴线转动产生了日夜交替。地球的不同地方,可能是白天或黑夜,同时又可能是一天中的任何时间。因为一天有 24 个小时,将地球的周长划分为 24 部分,每部分 15 度,即所谓的时区。出于方便考虑,时区的界限并不总是遵循经线,而是按照行政地域划分的界限:国界、地区等。

参照点为本初子午线,即格林威治子午线。因其穿过伦敦格林威治区而得名。格林威治标准时间向格林威治子午线两侧各延伸 7.5 度。由格林威治标准时间向东测出 12 个时区(从 +1 到 +12),由格林威治标准时间向西亦测出 12 个时区(从 -1 到 -12)。事实上,时区 -12 与 +12 之间仅有 7.5 度宽,而非 15 度。时区 -12 与 +12 分别位于 180 度子午线的左右两侧,即所谓的“国际日期变更线”。

假设格林威治现在是正午 (12:00),它是 -12 时区的 00:00,即一天的开始;而 - 24:00 即 +12 时区次日的 00:00。任何其它时间也都一样——尽管钟表上的时间完全一致,但日历中的日期却不同。实际上,时区 -12 并不使用,而是以 +12 替代。同样,时区 -11 亦被时区 +13 取代。而这很可能与经济关系的特殊性相关:比如说,处于时区 +13 的萨摩亚独立国与日本有牢固的经济关系,所以与日本时间更接近被认为更加方便。

此外,还有 -04:30 和 +05:45 之类的非常规时区。如您感觉好奇,可以到 Windows 的时间设置里查看所有时区的列表。

夏令时

世界上有许多国家将其时钟前拨一小时,作为一项“夏令时”惯例,目的则是更有效地利用日光及节能。全球约有 80 个国家遵守“夏令时”,其它国家则并不如此。一些大规模采用“夏令时”的国家中,也有部分地区选择退出这一做法(包括美国)。遵守“夏令时”的多是重要、经济发达的国家和地区:几乎所有的欧洲国家(包括德国、英国、瑞士)、美国(纽约、芝加哥)、澳大利亚(悉尼)以及新西兰(威灵顿)。日本不遵守“夏令时”。2011 年 3 月 27 日,俄罗斯最后一次将其时钟前拨了一个小时,但却再也没有在 10 月份调回标准时间。自那以后,俄罗斯正式退出了“夏令时”。

更改为“夏令时”的流程各国都有不同。在美国,改时是在三月份第二个星期日当地时间 02:00 进行,并在十一月的第一个星期日的 02:00 改回。而在欧洲,改为夏令时是在三月份最后一个星期日的 02:00,而改回标准时间则是在十月份最后一个星期日的 03:00。在所有的欧洲国家中,改为夏令时都是同时进行,而不是设定为当地时间。比如伦敦 02:00 时、柏林 03:00 时,要根据时区确定。当伦敦 03:00、柏林 04:00 (如此等等)时,时钟就会改回标准时间。

澳大利亚和新西兰位于南半球,当其夏天来临时,北半球迎来冬天。所以,澳大利亚是在十月的第一个星期日改为“夏令时”,并在四月份的第一个星期日改回标准时间。很难更准确地说明澳大利亚改为“夏令时”的时间,因为该国不同地区一直都没能就开始和结束日期达成一致。新西兰是在九月份的最后一个星期日的 02:00 改为“夏令时”,并在四月份的第一个星期日的 03:00 改回标准时间。

时间标准

上文说过,太阳日被作为时间测量的基础采用,而格林威治标准时间则被作为时间标准采用,所以其它时区都以此时间为准。格林威治标准时间通常缩写为 GMT。

然而,由于已经确定地球转动时会有些微的不均匀,所以用原子钟来测量时间和 UTC (协调世界时)已成为了新的时间标准。当前,UTC 充当着全世界的主要时间标准,是所有时区时间测量的基础,且针对“夏令时”做出了必要的调整。UTC 不受“夏令时”约束。

由于基于太阳日的 GMT 与基于原子钟的 UTC 并不完全相同,所以 UTC 和 GMT 大约每 500 天就会积累出 1 秒钟左右的时间差。为此,有时会在6 月 30 日或 12 月 31 日进行一次 1 秒的调整。

日期与时间格式

日期格式因国而异。比如说,俄罗斯的习惯是先写日期,然后是月份和年。日期中的数字分别用圆点隔开,比如 01.12.2012 - 2012 年 12 月 1 日。而在美国,日期的格式就是月/日/年,日期中的数字是用一个斜杠 "/" 隔开。除了圆点和斜杠 "/" 外,有些格式标准还可能采用 破折号 "-" 来分隔日期中的数字。而表示时间时,使用冒号“:”分隔小时、分钟、秒,比如 12:15:30,表示 12 时 15 分 30 秒。

指明日期和时间格式有一种简单的方法。比如说,"dd.mm.yyyy" 是指先写日期(由两位数字构成的月份中的日期;如果是从 1 到 9 号,则在前面加一个 0),然后是月份(需要由两位数字构成),最后是由四位数字构成的年份。"d-m-yy" 是指先写日期(可以是一位的数字),然后是月份(允许一位数字),最后是两位数字构成的年份,比如 1/12/12 即指 2012 年 12 月 1 日。日、月、年的值中间都由一个破折号 "-" 隔开。

时间与日期之间用一个空格隔开。时间格式用 "h" 代表小时、"m" 代表分钟而 "s" 代表秒钟,同时指定所需的位数。比如说,"hh:mi:ss" 是指首先写小时(1 到 9 要在值前面加个 0),然后是分钟(需要 2 位),最后是秒(2 位),其中的小时、分钟和秒值之间都用一个冒号隔开。

以程序员的角度来看,被视为最准确的日期和时间格式则为 "yyyy.mm.dd hh:mi:ss"。对带有利用这种标记法写下的日期的字符串进行排序时,可以很方便地按时间顺序排列。假设您每天都将信息存储到文本文件中,并保存在同一个文件夹中。如果您用该格式命名文件,会方便文件夹中文件的分类和按序排列。

现在,我们已经搞定了理论部分,开始实施吧。

MQL5 中的时间

在 MQL5 中,时间按照从 1970 年 1 月 1 日(所谓的 Unix 时间戳)起流逝的秒数进行测量。要存储时间,我们采用 datetime 类型变量。datetime 型变量的最小值为 0 (对应着时间戳开始的日期),而最大值为 32 535 244 799 (对应着 3000 年 12 月 31 日的 23:59:59)。

确定当前服务器时间

要确定当前时间,我们采用 TimeCurrent() 函数。它会返回已知最新的服务器时间:

datetime tm=TimeCurrent();
//--- output result
Alert(tm);

同样的服务器时间被用于指定图表中各柱的时间。已知最新服务器时间,是在 Market Watch (市场报价)窗口中打开的任何交易品种价格方面最后一次变化的时间。如果 Market Watch 窗口中仅有 EURUSD,则 TimeCurrent() 函数将返回 EURUSD 的最后一次价格变化。由于 Market Watch 窗口通常会显示相当数量的交易品种,所以此函数基本上都是返回当前的服务器时间。但是,由于价格在周末不会变动,所以由此函数返回的值将与实际的服务器时间有很大不同。

如您需要按某特定交易品种查找服务器时间(最后一次价格变动的时间),您可以使用 SymbolInfoInteger() 函数,该函数带有SYMBOL_TIME 标识符:

datetime tm=(datetime)SymbolInfoInteger(_Symbol,SYMBOL_TIME);
//--- output result
Alert(tm);

确定当前本地时间

本地时间(由用户 PC 的时钟显示)由 TimeLocal() 函数确定:

datetime tm=TimeLocal();
//--- output result
Alert(tm);

实际情况中,在编制 EA 和指标程序时,大都会采用服务器时间。本地时间在提醒和日志项中十分方便:对于用户来讲,将某个消息或条目时间戳与 PC 时钟显示的时间进行对比,可更方便地查看该消息或条目是多久以前登记的。但是,此 MetaTrader 5 终端会利用 Alert()Print() 函数,自动向输出的消息和日志项添加一个时间戳。因此,只有极少见的情况下,才会产生使用 TimeLocal() 函数的需求。

时间输出

请注意,上述代码中的 tm 变量值,是利用 Alert() 函数输出的。也就是说,该值是以一种易读的格式显示,比如 "2012.12.05 22:31:57"。这是由于 Alert() 函数将传递给它的自变量转换成了 string 类型(使用 Print()、Comment() 函数和输出为文本与 csv 文件时也会出现这种情况)。在生成一条包含 datetime 类型变量值的文本信息的过程中,类型转换由您自己负责。如您需要得到格式化的时间,请转换为字符串类型;或者如果您需要的是数值,则先转换为 long 类型,再转换为字符串类型:

datetime tm=TimeCurrent();
//--- output result
Alert("Formatted: "+(string)tm+", in seconds: "+(string)(long)tm);

由于 long 和 ulong 类型变量的值范围涵盖了 datetime 型变量值的范围,所以亦可将其用于存储时间;但这种情况下,要输出格式化的时间,您需要将 long 类型转换为 datetime 类型,然后将其转换为 string 类型;而如果您要输出一个数值,则只要将其转换为 string 类型即可:

long tm=TimeCurrent();
//--- output result
Alert("Formatted: "+(string)(datetime)tm+", in seconds: "+(string)tm);

时间格式化

时间格式化曾在“MQL5 编程基础:字符串”专门讲解将各种变量转换为字符串的章节中研究过。我们在这里简单提几个要点。除了转换类型外,MQL5 还提供一个允许您在将日期和时间转换为字符串时指定其格式的函数 - TimeToString() 函数:

datetime tm=TimeCurrent();
string str1="Date and time with minutes: "+TimeToString(tm);
string str2="Date only: "+TimeToString(tm,TIME_DATE);
string str3="Time with minutes only: "+TimeToString(tm,TIME_MINUTES);
string str4="Time with seconds only: "+TimeToString(tm,TIME_SECONDS);
string str5="Date and time with seconds: "+TimeToString(tm,TIME_DATE|TIME_SECONDS);
//--- output results
Alert(str1);
Alert(str2);
Alert(str3);
Alert(str4);
Alert(str5);

TimeToString() 函数可被应用于 datetime 、long、ulong 类型变量以及其它的一些 integer 型变量,只是它们不能用于存储时间。

利用 StringFormat() 函数格式化某文本消息时,您需要注意类型转换。

  • 将时间存储为 datetime 类型变量时:

    datetime tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Formatted: %s, in seconds: %I64i",(string)tm,tm);
    //--- output result
    Alert(str);
  • 将时间存储为 long 类型变量时:

    long tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Formatted: %s, in seconds: %I64i",(string)(datetime)tm,tm);
    //--- output result
    Alert(str);
  • 或是采用 TimeToString() 函数:

    datetime tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Date: %s",TimeToString(tm,TIME_DATE));
    //--- output result
    Alert(str);

将时间转换为数字。加减时间

要将某格式化的日期(字符串)转换为数字(自时间戳记开始起流逝的秒数),则要利用 StringToTime() 函数:

datetime tm=StringToTime("2012.12.05 22:31:57");
//--- output result
Alert((string)(long)tm);

作为上述代码的结果,以秒计算的输出时间将是 "1354746717",与下述日期对应 "2012.12.05 22:31:57"。

只要时间以数字形式呈现,各种相关操作就都变得简单方便了,比如说,您可以查找过去和未来的日期和时间。由于时间按秒测量,您要添加一个以秒为单位的时间段。只要知道一分钟有 60 秒、一个小时为 60 分钟或 3600 秒,计算任何时间段的时长都不会太难。

在当前时间的基础上减去或加上一个小时(3600 秒),您就会得到一个小时之前或之后的时间:

datetime tm=TimeCurrent();
datetime ltm=tm-3600;
datetime ftm=tm+3600;
//--- output result
Alert("Current: "+(string)tm+", an hour ago: "+(string)ltm+", in an hour: "+(string)ftm);

传递给 StringToTime() 函数的数据无需是完整的。而且,您还可以传递不带时间的日期,或是不带日期的时间。如您传递不带时间的日期,此函数会返回指定日期 00:00:00 时的值:

datetime tm=StringToTime("2012.12.05");
//--- output result
Alert(tm);

如您只传递时间,此函数会返回与当前日期的指定时间对应的值:

datetime tm=StringToTime("22:31:57");
//--- output result
Alert((string)tm);

也可以不带秒数传递时间。如您传递日期,唯一能够传递的时间分量就是小时了。实际情况中很少有这种需求。但如果您感到好奇,请自行随意试验。

日期与时间分量

要确定各日期与时间分量(年、月、日等)的值,我们利用 TimeToStruct() 函数和 MqlDateTime 结构。此结构通过引用传递给该函数。执行此函数后,此结构会被传递给它的日期的各个分量值填充:

datetime    tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
//--- output date components
Alert("Year: "        +(string)stm.year);
Alert("Month: "      +(string)stm.mon);
Alert("Day: "      +(string)stm.day);
Alert("Hour: "        +(string)stm.hour);
Alert("Minute: "     +(string)stm.min);
Alert("Second: "    +(string)stm.sec);
Alert("Day of the week: "+(string)stm.day_of_week);
Alert("Day of the year: "  +(string)stm.day_of_year);

注意:除日期分量外,此结构还包含几个附加字段:星期几 (the day_of_week field) 和该年的第几天 (day_of_year)。星期几从 0 开始计数(0 - 周日,1 - 周一,以此类推)。该年的第几天也从零开始计数。其它值遵循普遍认可的计数顺序(月份从 1 开始计数,日数也是)。

还有另一种调用 TimeCurrent() 函数的方式。MqlDateTime 类型结构通过引用传递给该函数。执行此函数后,该结构则填有当前日期的各个分量:

MqlDateTime stm;
datetime tm=TimeCurrent(stm);
//--- output date components
Alert("Year: "        +(string)stm.year);
Alert("Month: "      +(string)stm.mon);
Alert("Day: "      +(string)stm.day);
Alert("Hour: "        +(string)stm.hour);
Alert("Minute: "     +(string)stm.min);
Alert("Second: "    +(string)stm.sec);
Alert("Day of the week: "+(string)stm.day_of_week);
Alert("Day of the year: "  +(string)stm.day_of_year);

TimeLocal() 函数亦可通过这种方式调用。

由其分量生成日期

您也可以反过来,把 MqlDateTime 结构转换为 datetime 类型。为此,我们使用 StructToTime() 函数。

我们来确定一下刚好是一个月前的时间。各月的天数有所不同。或者 30 天,或者 31 天,而且二月份还可能是 28 或 29 天长。之前讲过的加减时间法,也因此不太合适了。所以,我们将日期分解为多个分量,将月份值减 1;而且如果月份值为 1,我们则将其设置为 12,并将年份值减 1:

datetime tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
if(stm.mon==1)
  {
   stm.mon=12;
   stm.year--;
  }
else
  {
   stm.mon--;
  }
datetime ltm=StructToTime(stm);
//--- output result
Alert("Current: "+(string)tm+", a month ago: "+(string)ltm);

确定柱时间

开发某指标时,MetaEditor 会自动创建两个 OnCalculate() 函数版本中的一种。

版本 1:

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[])
  {
   return(rates_total);
  }

版本 2:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   return(rates_total);
  }

此函数的第一个版本的参数中包括 time[] 数组,而该数组中的元素则包含所有柱的时间。

使用第二个版本时,以及编制 EA 程序、安排对于从指标到其它时间框架上各柱时间的访问时,我们采用 CopyTime() 函数。此函数存在三种版本。在所有版本中,前两个函数参数都是定义交易品种,以及确定柱时间所本的图表时间框架。最后一个参数则定义一个数组,该数组根据所用的函数版本,存储返回值以及两个中间参数。

CopyTime - 版本 1 您需要指定柱索引及待复制元素的数量:

//--- variables for function parameters
int start = 0; // bar index
int count = 1; // number of bars
datetime tm[]; // array storing the returned bar time
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert(tm[0]);

此例所示为复制 D1 时间框架上最后一个柱的时间的实施。我们使用的函数版本,取决于传递给它的参数的类型。上例介绍了 int 类型变量的使用,这就意味着,我们需要按照指定柱数的柱编号获取时间。

采用 CopyTime() 函数时,各柱是从右到左、从零开始计数。针对所获值(最后一个参数)采用的一个动态数组,由 CopyTime() 函数自己即可缩放为所需大小。您也可以采用一个静态数组,但这种情况下,数组大小必须与所需元素数量(第 4 个参数的值)严格对应。

一次性由多个柱取得时间时,重要的是了解返回数组中的元素顺序。尽管图表中的柱是从指定柱开始从右到左计数,但数组元素却是从左到右排列的:

//--- variables for function parameters
int start = 0; // bar index
int count = 2; // number of bars
datetime tm[]; // array storing the returned bar time
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert("Today: "+(string)tm[1]+", yesterday: "+(string)tm[0]);

作为此代码的结果,昨日柱的时间会被存储到 tm 数组的 0 元素,而第 1 个元素中则将包含今日柱的时间。

某些情况下,让数组中的时间按照与图表中柱计数一样顺序来排列,似乎更方便一些。而这里的 ArraySetAsSeries() 函数可以达到这种效果:

//--- variables for function parameters
int start = 0; // bar index
int count = 2; // number of bars
datetime tm[]; // array storing the returned bar time
ArraySetAsSeries(tm,true); // specify that the array will be arranged in reverse order
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert("Today: "+(string)tm[0]+", yesterday: "+(string)tm[1]);

现在,今日柱的时间在索引为 0 的元素中,而昨日柱的时间则在索引为 1 的元素中。

CopyTime - 版本 2。这里,调用 CopyTime() 函数时,我们需要指定复制开始的柱时间,以及待复制的柱数。此版本将适用于确定某个包含较低时间框架柱的较高时间框架的时间:

//--- get the time of the last bar on M5
int m5_start=0; 
int m5_count=1;
datetime m5_tm[];
CopyTime(_Symbol,PERIOD_M5,m5_start,m5_count,m5_tm);
//--- determine the bar time on H1 that contains the bar on M5
int h1_count=1;
datetime h1_tm[];
CopyTime(_Symbol,PERIOD_H1,m5_tm[0],h1_count,h1_tm);
//--- output result
Alert("The bar on М5 with the time "+(string)m5_tm[0]+" is contained in the bar on H1 with the time "+(string)h1_tm[0]);

如果您需要确定某较低时间框架柱的时间,作为某较高时间框架柱的起点,则情况更加复杂。带有与较高时间框架柱相同时间的柱,在较低时间框架可能就会丢失。这种情况下,我们就会获取较高时间框架前一柱中包含的较低时间框架的最后一个柱的时间。所以,我们需要确定较低时间框架上柱的数量,并获取下一个柱的时间。

下面就是上述内容的一种待用型函数形式的实施:

bool LowerTFFirstBarTime(string aSymbol,
                         ENUM_TIMEFRAMES aLowerTF,
                         datetime aUpperTFBarTime,
                         datetime& aLowerTFFirstBarTime)
  {
   datetime tm[];
//--- determine the bar time on a lower time frame corresponding to the bar time on a higher time frame 
   if(CopyTime(aSymbol,aLowerTF,aUpperTFBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]<aUpperTFBarTime)
     {
      //--- we got the time of the preceding bar
      datetime tm2[];
      //--- determine the time of the last bar on a lower time frame
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      if(tm[0]<tm2[0])
        {
         //--- there is a bar following the bar of a lower time frame  that precedes the occurrence of the bar on a higher time frame
         int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0])-2;
         //--- the Bars() function returns the number of bars from the bar with time tm[0] to
         //--- the bar with time tm2[0]; since we need to determine the index of the bar following 
         //--- the bar with time tm2[2], subtract 2
         if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
           {
            return(false);
           }
        }
      else
        {
         //--- there is no bar of a lower time frame contained in the bar on a higher time frame
         aLowerTFFirstBarTime=0;
         return(true);
        }
     }
//--- assign the obtained value to the variable 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

函数参数:

  • aSymbol - 交易品种;
  • aLowerTF - 较低时间框架;
  • aUpperTFBarTime - 某较高时间框架上的柱时间;
  • aLowerTFFirstBarTime - 某较低时间框架的返回值。

在整个代码中,该函数都检查是否已成功调用 CopyTime() 函数;如出错,则返回 false。柱时间是利用 aLowerTFFirstBarTime 参数通过引用返回。

此函数的使用示例如下:

//--- time of the bar on the higher time frame H1
   datetime utftm=StringToTime("2012.12.10 15:00");
//--- variable for the returned value
   datetime val;
//--- function call
   if(LowerTFFirstBarTime(_Symbol,PERIOD_M5,utftm,val))
     {
      //--- output result in case of successful function operation
      Alert("val = "+(string)val);
     }
   else
     {
      //--- in case of an error, terminate the operation of the function from which the LowerTFFirstBarTime() function is called
      Alert("Error copying the time");
      return;
     }

如果此函数收到某较高时间框架某个不存在柱的时间,就会出现较高时间框架某个柱中包含的某个较低时间框架上没有柱的情况。这种情况下,此函数返回 true,且时间值 0 被写入 aLowerTFFirstBarTime 变量。如果某较高时间框架的某个柱存在,则每个较低时间框架中始终都至少有一个对应柱。

查找某较高时间框架某个柱中包含的某较低时间框架最后一个柱的时间,稍微简单一些。我们计算某较高时间框架下一个柱的时间,再用所得值来确定某较低时间框架对应柱的时间。如果作为得到的时间与计算得出的较高时间框架时间相等,则我们需要确定较低时间框架前一柱的时间。如果作为得到的时间较小,则我们得到了正确的时间。

下面就是上述内容的一种待用型函数形式的实施:

bool LowerTFLastBarTime(string aSymbol,
                        ENUM_TIMEFRAMES aUpperTF,
                        ENUM_TIMEFRAMES aLowerTF,
                        datetime aUpperTFBarTime,
                        datetime& aLowerTFFirstBarTime)
  {
//--- time of the next bar on a higher time frame
   datetime NextBarTime=aUpperTFBarTime+PeriodSeconds(aUpperTF);
   datetime tm[];
   if(CopyTime(aSymbol,aLowerTF,NextBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]==NextBarTime)
     {
      //--- There is a bar on a lower time frame corresponding to the time of the next bar on a higher time frame.
      //--- Determine the time of the last bar on a lower time frame
      datetime tm2[];
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      //--- determine the preceding bar index on a lower time frame
      int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0]);
      //--- determine the time of this bar
      if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
        {
         return(false);
        }
     }
//--- assign the obtain value to the variable 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

函数参数:

  • aSymbol - 交易品种;
  • aUpperTF - 较高时间框架;
  • aLowerTF - 较低时间框架;
  • aUpperTFBarTime - 某较高时间框架上的柱时间;
  • aLowerTFFirstBarTime - 某较低时间框架的返回值。

在整个代码中,该函数都检查是否已成功调用 CopyTime() 函数;如出错,则返回 false。柱时间是利用 aLowerTFFirstBarTime 参数通过引用返回。

此函数的使用示例如下:

//--- time of the bar on the higher time frame H1
datetime utftm=StringToTime("2012.12.10 15:00");
//--- variable for the returned value
datetime val;
//--- function call
if(LowerTFLastBarTime(_Symbol,PERIOD_H1,PERIOD_M5,utftm,val))
  {
//--- output result in case of successful function operation
   Alert("val = "+(string)val);
  }
else
  {
//--- in case of an error, terminate the operation of the function from which the LowerTFFirstBarTime() function is called
   Alert("Error copying the time");
   return;
  }

注意! 假设传递给此函数的时间就是某较高时间框架的精确时间。如果此柱的精确时间未知,我们只知道该柱或某个较高时间框架柱中包含的某个较低时间框架柱某特定价格变动的时间,那么就需要时间标准化。MetaTrader 5 终端中使用的时间框架将日分解为一个整数数量的柱。我们确定自时间戳记开始起的柱数,然后再乘以以秒数计的柱长度:

datetime BarTimeNormalize(datetime aTime,ENUM_TIMEFRAMES aTimeFrame)
  {
   int BarLength=PeriodSeconds(aTimeFrame);
   return(BarLength*(aTime/BarLength));
  }

函数参数:

  • aTime - 时间;
  • aTimeFrame - 时间框架。

此函数的使用示例如下:

//--- the time to be normalized
datetime tm=StringToTime("2012.12.10 15:25");
//--- function call
datetime tm1=BarTimeNormalize(tm,PERIOD_H1);
//--- output result
Alert(tm1);

然而,这不只是时间标准化的方法,也是通过较低时间框架的时间确定较高时间框架时间的另一种方法。

CopyTime - 版本 3。这种情况下,如果调用 CopyTime() 函数,我们则会指定需要由其复制的与柱相关的时间范围。此方法允许我们轻松地获取某较高时间框架柱包含的某较低时间框架的所有柱。

//--- time of the bar start on H1
datetime TimeStart=StringToTime("2012.12.10 15:00");
//--- the estimated time of the last bar on
//--- M5
datetime TimeStop=TimeStart+PeriodSeconds(PERIOD_H1)-PeriodSeconds(PERIOD_M5);
//--- copy time
datetime tm[];
CopyTime(_Symbol,PERIOD_M5,TimeStart,TimeStop,tm);
//--- output result 
Alert("Bars copied: "+(string)ArraySize(tm)+", first bar: "+(string)tm[0]+", last bar: "+(string)tm[ArraySize(tm)-1]);

确定日开始时间以及自日开始以来流逝的时间量

根据某给定时间来确定日开始时间最显而易见的方式,就是将时间分解成其各个分量,将小时、分钟和秒归零,然后再把它们加起来。但是,还有一种更简单的方式。一天有 86400 秒。我们需要获取一个时间除以一天秒数所得结果的整数,再乘以一天的秒数:

datetime tm=TimeCurrent();
tm=(tm/86400)*86400;
//--- output result
Alert("Day start time: "+(string)tm);

注意! 此技巧仅于整数变量有效。如果您的计算中出现任何双精度和浮点型变量,则需要利用 MathFloor() 函数截掉小数部分:

MathFloor(tm/86400)*86400

乘法运算之后,则应利用 NormalizeDouble() 函数完成值的正态化。因为结果必须为整数,所以您可以利用一个取整函数 MathRound()

MathRound(MathFloor(tm/86400)*86400)

采用 integer 变量时,余数会被自动截掉。处理时间的过程中,很少会用到双精度或浮点型变量;如果用到它们,极有可能表明方法从根本上错了。

要确定自日开始起流逝的秒数,我们只需取时间除以 86400 的余数:

datetime tm=TimeCurrent();
long seconds=tm%86400;
//--- output result
Alert("Time elapsed since the day start: "+(string)seconds+" sec.");

也可以用类似的方法,将获取的以秒计算的时间,转换为小时、分钟和秒。作为一个函数实施:

int TimeFromDayStart(datetime aTime,int &aH,int &aM,int &aS)
  {
//--- Number of seconds elapsed since the day start (aTime%86400),
//--- divided by the number of seconds in an hour is the number of hours
   aH=(int)((aTime%86400)/3600);
//--- Number of seconds elapsed since the last hour (aTime%3600),
//--- divided by the number of seconds in a minute is the number of minutes 
   aM=(int)((aTime%3600)/60);
//--- Number of seconds elapsed since the last minute 
   aS=(int)(aTime%60);
//--- Number of seconds since the day start
   return(int(aTime%86400));
  }

第一个传递的参数是时间。其它参数均用于返回值:aH - 小时,aM - 分钟,aS - 秒。此函数本身会返回自日开始起的总秒数。我们来看看这个函数:

datetime tm=TimeCurrent();
int t,h,m,s;
t=TimeFromDayStart(tm,h,m,s);
//--- output result
Alert("Time elapsed since the day start ",t," s, which makes ",h," h, ",m," m, ",s," s ");

您也可以计算确定日开始柱的指标中待使用的天数:

bool NewDay=(time[i]/86400)!=(time[i-1]/86400);

假设各柱由左至右索引,其中 time[i] 为当前柱的时间,而 time[i-1] 则为前一柱的时间。

确定周开始时间以及自周开始以来流逝的时间量

与确定日开始相比,确定周开始时间稍微复杂一些。尽管一周的天数是恒定的,而且也能计算出一周的秒数时长(604800 秒),但是只是计算出自时间戳记开始起流逝的整周数、再乘以周的持续时长是不够的。

问题在于,大多数国家的一周都是从周一开始,而有一些国家(美国、加拿大、以色列等)却是从周日开始。但我们还记得,时间测量的时间戳从周四开始。如果周四是一周的第一天,那么有这些简单的计算就足够了。

出于方便考虑,我们将以第一个时间戳日对应 0 值为例,来研究确定周开始的特殊性。我们需要找到这样一个值:当将添加到时间时,会改变时间戳的首日 (1970.01.01 00:00),从零开始计数,到第四日,即我们需要添加四日的持续时长。如果一周从周一开始,则周四是第四日,所以我们需要添加三日的持续时长。但如果一周从周日开始,则周四是第五日,所以我们需要添加四日的持续时长。

我们编写一个函数来计算周数:

long WeekNum(datetime aTime,bool aStartsOnMonday=false)
  {
//--- if the week starts on Sunday, add the duration of 4 days (Wednesday+Tuesday+Monday+Sunday),
//    if it starts on Monday, add 3 days (Wednesday, Tuesday, Monday)
   if(aStartsOnMonday)
     {
      aTime+=259200; // duration of three days (86400*3)
     }
   else
     {
      aTime+=345600; // duration of four days (86400*4)  
     }
   return(aTime/604800);
  }

此函数可在确定新一周的第一个柱的指标中发挥作用:

bool NewWeek=WeekNum(time[i])!=WeekNum(time[i-1]);

假设各柱由左至右索引,其中 time[i] 为当前柱的时间,而 time[i-1] 则为前一柱的时间。

现在,我们可以计算周开始的时间。由于为了计算一周的天数,我们假设时间戳的开始提前三(或四)天,现在我们需要执行反向的时间纠正:

long WeekStartTime(datetime aTime,bool aStartsOnMonday=false)
  {
   long tmp=aTime;
   long Corrector;
   if(aStartsOnMonday)
     {
      Corrector=259200; // duration of three days (86400*3)
     }
   else
     {
      Corrector=345600; // duration of four days (86400*4)
     }
   tmp+=Corrector;
   tmp=(tmp/604800)*604800;
   tmp-=Corrector;
   return(tmp);
  }  

此函数会返回一个 long 类型值,因为第一周的值可能是负数(在时间戳开始前三、四天)。此函数的第二个参数可确定此周从周日还是周一开始。

现在,我们拥有了周开始的时间,就可以计算自周开始起流逝的秒数了:

long SecondsFromWeekStart(datetime aTime,bool aStartsOnMonday=false)
  {
   return(aTime-WeekStartTime(aTime,aStartsOnMonday));
  }

秒数可被转换为日、小时、分钟和秒。尽管计算自日开始起的小时、分钟和秒不难,但像这种情况采用 TimeToStruct() 函数会更简单:

long sfws=SecondsFromWeekStart(TimeCurrent());
MqlDateTime stm;
TimeToStruct(sfws,stm);
stm.day--;
Alert("Time elapsed since the week start "+(string)stm.day+" d, "+(string)stm.hour+" h, "+(string)stm.min+" m, "+(string)stm.sec+" s");

请注意,stm.day 值被减了 1。月的号数是从 1 计数,因为我们需要确定所有日数。有些人可能觉得,从实用角度看,这部分内容没什么用处。但您对于上述函数的了解作为处理时间的一项经验,十分有价值。

确定自某个给定日期、年开始或月开始以来的周数

注意 MqlDateTime 结构的各个字段,尤其是 day_of_year 字段,有人会喜欢创建确定自年开始和月开始起周数的函数。最好是编写一个确定自某给定日期起周数的一般函数。此函数的运行原理,与确定自时间戳开始起的周数所使用的函数类似:

long WeekNumFromDate(datetime aTime,datetime aStartTime,bool aStartsOnMonday=false)
  {
   long Time,StartTime,Corrector;
   MqlDateTime stm;
   Time=aTime;
   StartTime=aStartTime;
//--- determine the beginning of the reference epoch
   StartTime=(StartTime/86400)*86400;
//--- determine the time that elapsed since the beginning of the reference epoch
   Time-=StartTime;
//--- determine the day of the week of the beginning of the reference epoch
   TimeToStruct(StartTime,stm);
//--- if the week starts on Monday, numbers of days of the week are decreased by 1,
//    and the day with number 0  becomes a day with number 6
   if(aStartsOnMonday)
     {
      if(stm.day_of_week==0)
        {
         stm.day_of_week=6;
        }
      else
        {
         stm.day_of_week--;
        }
     }
//--- calculate the value of the time corrector 
   Corrector=86400*stm.day_of_week;
//--- time correction
   Time+=Corrector;
//--- calculate and return the number of the week
   return(Time/604800);
  }

基于此函数,我们编写两个确定自年开始、自月开始起周数的函数。为此,我们首先需要确定年开始的时间和月开始的时间。将时间分解成其分量,调整某些字段的值,并将各分量重新转换为时间标记。

  • 确定年开始时间的函数:

    datetime YearStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.mon=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }
  • 确定月开始时间的函数:

    datetime MonthStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }

现在,下面就是确定自年开始、月开始起周数的函数。

  • 自年开始起:

    long WeekNumYear(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,YearStartTime(aTime),aStartsOnMonday));
          }
  • 自月开始起:

    long WeekNumMonth(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,MonthStartTime(aTime),aStartsOnMonday));
          }

最终,我们开始着手纯粹的实务。

创建试验工具集

前面提到过,有些交易中心的报价包含周日柱,也有的在整个周末都持续提供报价。我们要确保必要的函数在所有情况下都正常运行。当然,我们可以到互联网上找到一些适用的交易中心,并利用演示账户上的报价来测试各函数的运行情况。但是,除了寻找正确的交易中心外,我们还必须在图表中寻找适当的位置来运行所需测试。

我们来创建自己的测试函数测试区域。而周五、周末和周一则是我们的关注重点。我们将根据需要,创建一个包含周五、周一和周末柱时间的数组。共有 4 个选项:

  1. 无周末柱。
  2. 周日末尾的一些柱,即 4 个。
  3. 持续的周末报价。
  4. 周六柱,但没有周日柱。

为避免数组变得过大,我们将采用 H1 时间框架。数组最大尺寸将是 96 个元素(每天 24 个柱乘 4 天),而数组本身将在利用图形对象绘制时匹配图表。所以,我们会通过启动某指标时第一次执行 OnCalculate() 函数的类似方式,得到一种带有时间和可在循环中的数组上进行迭代的、类似指标缓冲区的东西。由此,我们将能够实现函数运行的可视化。

此工具以随附于本文的一个脚本的形式实施(sTestArea.mq5 文件)。准备工作则于脚本的 OnStart() 函数内执行。此函数代码最开头的 Variant 变量允许您选取上面列出的 4 个选项中的 1 个。而在 OnStart() 函数下方,您可以看到类似于指标的 OnCalculate() 函数的 LikeOnCalculate() 函数。此函数有两个参数:rates_total - 柱数,以及 time[] - 带柱时间的数组。

此外,我们会继续在此函数内工作,就像我们在编写一个指标一样。您可以通过调用 SetMarker() 函数,由此函数设定一个标记。传递给 SetMarker() 函数的参数分别为:柱索引、缓冲区索引(标记显示的行)及标记颜色。

图 4 所示为脚本性能结果,Variant 变量为 2,且在每个柱下设置两个标记行(用相关时间戳记标记柱)。所有图表元素的颜色设置均不可见。

图 4. sTestArea.mq5 脚本性能
图 4. sTestArea.mq5 脚本性能

柱时间戳记根据周几取色:周五 - 红色,周六 - 洋红色,周日 - 绿色,周一 - 蓝色。现在,我们可以继续编写对周末柱需要特殊方法、且能够可视化监控其工作的各种函数了。

支点指标 - 备选 1

我们首先尝试创建一个简单的支点指标。要计算支点线,我们需要清楚昨日的收盘价格,以及昨日的最高和最低价格。该指标值是作为上述三值的平均进行计算。我们会找出一日内的新高新低,计算新的一日开始时的支点值,并进一步绘制并显示全天的价格。

我们将提供该指标运行的两种版本:

  1. 新的每一天,都计算支点(假设没有周末柱)。如果有周末柱,则周六和周日柱会被区别对待。
  2. 周六柱属于周五,而周日柱则属于周一(整个周末都持续提供报价、以及仅存在周日柱的情况均适用)。这里,您要记住的是,周末很可能没有柱。

在第一种版本中,只确定新的一天的开始就足够了。我们将当前时间 (aTimeCur) 和前一时间 (aTimePre) 传递给此函数,计算自时间戳开始起的天数,而且如果它们不匹配,我们就推断新的一天已经开始:

bool NewDay1(datetime aTimeCur,datetime aTimePre)
  {
   return((aTimeCur/86400)!=(aTimePre/86400));
  }

第二种版本。如果周六已开始,则日开始应被忽略。如果周日已开始,我们则定义日开始(这自然只是另一天)。如果周一继周日后开始,则略过日开始。如果一周中其它任何一天(比如周六或周五)先于周一,则定义日开始。所得函数如下:

bool NewDay2(datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
//--- new day
   if(NewDay1(aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      switch(stm.day_of_week)
        {
         case 6: // Saturday
            return(false);
            break;
         case 0: // Sunday
            return(true);
            break;
         case 1: // Monday
            TimeToStruct(aTimePre,stm);
            if(stm.day_of_week!=0)
              { // preceded by any day of the week other than Sunday
               return(true);
              }
            else
              {
               return(false);
              }
            break;
         default: // any other day of the week
            return(true);
        }
     }
   return(false);
  }

下面则是取决于版本的通用函数:

bool NewDay(datetime aTimeCur,datetime aTimePre,int aVariant=1)
  {
   switch(aVariant)
     {
      case 1:
         return(NewDay1(aTimeCur,aTimePre));
         break;
      case 2:
         return(NewDay2(aTimeCur,aTimePre));
         break;
     }
   return(false);
  }

我们利用 "sTestArea" 工具来测试函数的运行(随附的 sTestArea_Pivot1.mq5 文件;日开始被标记为褐色)。您需要运行 8 次测试: 4 个柱生成选项的 2 个函数版本。确保函数正常运行后,我们可以安全地展开指标开发了。但是,由于指标开发并非本文重点,所以我们随附了一个即用型指标(Pivot1.mq5 文件),详细讲解此开发过程中最困难的部分。

确定时段

我们需要允许 EA 交易在当天的指定时间范围内、每天按相同的间隔进行交易。我们指定交易时段开始的时与分,以及交易时段结束的时与分。时与分分别指定(而不是将时间指定为 "14:00" 的字符串变量),如此一来,只要 EA 交易中采用了此函数,我们就可以在策略测试程序中执行优化。

要确定时段,则操作如下:

  1. 计算自日开始起的秒数时间,作为时间起始点;同样再算出时间结束点。
  2. 计算自日开始起以秒计的当前时间。
  3. 将当前时间与起始、结束时间进行对比。

交易时段从某天开始、另一天结束的情况也不是不可能,即如果某交易时段经过了午夜,则计算出的自日开始起的结束时间会小于开始时间。因此,我们需要执行两次检查。所得函数如下:

bool TimeSession(int aStartHour,int aStartMinute,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- session start time
   int StartTime=3600*aStartHour+60*aStartMinute;
//--- session end time
   int StopTime=3600*aStopHour+60*aStopMinute;
//--- current time in seconds since the day start
   aTimeCur=aTimeCur%86400;
   if(StopTime<StartTime)
     {
      //--- going past midnight
      if(aTimeCur>=StartTime || aTimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- within one day
      if(aTimeCur>=StartTime && aTimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

如果时段经过午夜,则当前时间应大于等于时段起始时间,或小于时段结束时间。如果时段是在当日内,则当前时间应大于等于起始时间,并小于结束时间。

本文末尾处附有一个创建用于测试函数运行的指标(Session.mq5 文件)。和任何其它应用指标一样,它不仅可被用于测试,还有其它的实用用途。

确定日内的一个时间点

简单地检查与指定时间的相等性不会有效,因为价格变动并不会定期出现,而且可能会有几秒到几分钟的延迟。在指定的时间内,很可能市场中根本就没有任何价格变动。我们需要检查给定的时间戳有无交叉。

当前时间应等于或大于指定时间,而前一时间则应小于指定时间。由于确定日内某时间点的需要,我们要将当前时间(及前一时间)转换为自日开始起的秒数。同样,给定的时间参数(时与分)亦应转换为秒数。前一时间很有可能属于前一天,即如果转换为自日开始起的秒数,它将大于当前时间。这种情况下,我们按照确定时段时的相同方式继续 - 执行两项检查。

所得函数如下:

bool TimeCross(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
//--- specified time since the day start
   datetime PointTime=aHour*3600+aMinute*60;
//--- current time since the day start
   aTimeCur=aTimeCur%86400;
//--- previous time since the day start
   aTimePre=aTimePre%86400;
   if(aTimeCur<aTimePre)
     {
      //--- going past midnight
      if(aTimeCur>=PointTime || aTimePre<PointTime)
        {
         return(true);
        }
     }
   else
     {
      if(aTimeCur>=PointTime && aTimePre<PointTime)
        {
         return(true);
        }
     }
   return(false);
  }

有一个基于此函数创建的指标(随附于本文的 TimePoint.mq5 文件)。

支点指标 - 备选 2

我们已经了解了如何确定一个时间点,现在我们再让支点指标复杂一些。不再是通常的 00:00,日现在是从任何给定的时间开始。我们将称之为用户定义日。要确定用户定义日的开始,我们将使用之前提到的 TimeCross() 函数。由于周末柱的不同生成选项,有些日必须忽略。要马上得到所有的检验规则可不容易,所以我们还是一步一步来。重要的是从何处着手,并就如何继续有多个选项。我们有一个测试脚本 - sTestArea.mq5,所以甚至可以试着找到正确的解决方案。

“无周末柱”的情况最简单:新的一天,从某给定时间戳时间交叉处开始。

如果周日末尾仅有几个柱,则不管函数参数如何,TimeCross() 函数都会将第一个周日柱定义为日开始。假设周末没有报价(周日柱属于周一),那么应当忽略周日。如果某给定时间位于一系列周日柱的中间某处,则亦应忽略,因为新的日开始已于周五注册。

持续的周末报价:如果某用户定义日的开始位于某日历日的中间(图 5),

图 5. 处于某日历日中间的某个用户定义日开始周五 - 红色,周六 - 洋红色,周日 - 绿色,周一 - 蓝色。
图 5. 处于某日历日中间的某个用户定义日开始
周五 - 红色,周六 - 洋红色,周日 - 绿色,周一 - 蓝色。

周六的一半作为周五,周日的一半作为周一。但是,还有从周六中间到周日中间的一些柱,不属于任何一天。当然,我们可以将周六到周日的间隔划分为多个等分,并将一半视为周五,另一半视为周一。但这样会将一个非常简单的指标严重复杂化,尽管周末报价并没有这么重要。

最合理的解决方案会是,将所有周六与周日柱视为从周五持续到周一的一个用户定义日。也就是说,周六和周日开始的用户定义日全被忽略。

所得函数如下:

bool NewCustomDay(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
   if(TimeCross(aHour,aMinute,aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      if(stm.day_of_week==0 || stm.day_of_week==6)
        {
         return(false);
        }
      else
        {
         return(true);
        }
     }
   return(false);
  }

有一个基于此函数创建的指标(随附于本文的 Pivot2.mq5 文件)。

确定一周的交易天数

要让某 EA 交易仅于特定日交易,实现起来相当简单。我们利用 TimeToStruct() 函数,将时间分解成其各个分量,并在 EA 交易的参数中,声明一周中每天的布尔型变量。根据是周几,此函数会返回对应变量的值。

这个可以通过一种更加优化的方式来完成。初始化某 EA 交易或指标时,利用允许或不允许在特定日交易的变量值来填充数组。之后,检查与周几对应的数组元素值。我们得到两个函数:一个在初始化期间被调用,另一个则按需调用。

变量:

input bool Sunday   =true; // Sunday
input bool Monday   =true; // Monday
input bool Tuesday  =true; // Tuesday 
input bool Wednesday=true; // Wednesday
input bool Thursday =true; // Thursday
input bool Friday   =true; // Friday
input bool Saturday =true; // Saturday

bool WeekDays[7];

初始化函数:

void WeekDays_Init()
  {
   WeekDays[0]=Sunday;
   WeekDays[1]=Monday;
   WeekDays[2]=Tuesday;
   WeekDays[3]=Wednesday;
   WeekDays[4]=Thursday;
   WeekDays[5]=Friday;
   WeekDays[6]=Saturday;
  }

主函数:

bool WeekDays_Check(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   return(WeekDays[stm.day_of_week]);
  }

本文末尾处附有一个基于此函数创建的指标(TradeWeekDays.mq5 文件)。

确定一周的交易时间

我们需要确定从一周中某天给定时间,到一周中另一天给定时间的交易时段。此函数与 TimeSession() 函数类似,仅有的区别在于计算是基于自周开始起流逝的时间。所得函数如下:

bool WeekSession(int aStartDay,int aStartHour,int aStartMinute,int aStopDay,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- session start time since the week start
   int StartTime=aStartDay*86400+3600*aStartHour+60*aStartMinute;
//--- session end time since the week start
   int StopTime=aStopDay*86400+3600*aStopHour+60*aStopMinute;
//--- current time in seconds since the week start
   long TimeCur=SecondsFromWeekStart(aTimeCur,false);
   if(StopTime<StartTime)
     {
      //--- passing the turn of the week
      if(TimeCur>=StartTime || TimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- within one week
      if(TimeCur>=StartTime && TimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

本文末尾处附有一个基于此函数创建的指标(SessionWeek.mq5 文件)。

我们已经完成了所有最常见时间相关任务的讲解,并研究了相关的编程技巧,以及解决它们所需的标准 MQL5 函数。

更多的 MQL5 函数

还有一些处理时间的 MQL5 函数:TimeTradeServer()TimeGMT(), TimeDaylightSavings()TimeGMTOffset()。而这些函数的主要特点,就是它们用于某用户 PC 的时钟和时间设置。

TimeTradeServer() 函数上文说过,TimeCurrent() 函数会在周末显示错误时间(周五最后一次价格变动的时间)。TimeTradeServer() 函数会计算正确的服务器时间:

datetime tm=TimeTradeServer();
//--- output result
Alert(tm);

TimeGMT() 函数此函数会根据某用户计算机的时钟值和时间设置,计算 GMT 时间:时区和“夏令时”:

datetime tm=TimeGMT();
//--- output result
Alert(tm);

为了更加精确,此函数返回 UTC 时间。

TimeDaylightSavings() 函数此函数会从用户计算机设置返回“夏令时”修正值。

int val=TimeDaylightSavings();
//--- output result
Alert(val);

要获取不带“夏令时”修正值的时间,您要将修正值添加到本地时间。

TimeGMTOffset() 函数此函数允许您获取某用户计算机的时区。该值会在几秒内返回,以备添加到本地时间来获取 GMT 时间。

int val=TimeGMTOffset();
//--- output result
Alert(val);

用户计算机上的时间将是 TimeGMT()-TimeGMTOffset()-TimeDaylightSavings():

datetime tm1=TimeLocal();
datetime tm2=TimeGMT()-TimeGMTOffset()-TimeDaylightSavings();
//--- output result
Alert(tm1==tm2);

更多的处理时间的有用函数

确定闰年的函数

bool LeapYear(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
//--- a multiple of 4 
   if(stm.year%4==0)
     {
      //--- a multiple of 100
      if(stm.year%100==0)
        {
         //--- a multiple of 400
         if(stm.year%400==0)
           {
            return(true);
           }
        }
      //--- not a multiple of 100 
      else
        {
         return(true);
        }
     }
   return(false);
  }

确定闰年的原理,已于上文的“时间测量的特殊性”部分中讲到。

确定月内天数的函数

int DaysInMonth(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   if(stm.mon==2)
     {
      //--- February
      if(LeapYear(aTime))
        {
         //--- February in a leap year 
         return(29);
        }
      else
        {
         //--- February in a non-leap year 
         return(28);
        }
     }
   else
     {
      //--- other months
      return(31-((stm.mon-1)%7)%2);
     }
  }

此函数会检查该年是否为闰年,以返回 2 月 28 日或 29 日的正确值,并计算其它月份的天数。前 7 个月的天数交替如下:31、30、31、30 等,以及剩余 5 个月的天数。因此,此函数会计算除以 7 的余数。然后我们执行奇数奇偶检验,并用 31 减去得到的修正值。

于策略测试程序中运行的时间函数的特殊性

策略测试程序会生成自己的报价流,而且 TimeCurrent() 函数的值与策略测试程序中的报价流相对应。TimeTradeServer() 函数值与 TimeCurrent() 值对应。同样,TimeLocal() 函数值亦与 TimeCurrent() 值对应。策略测试程序中的 TimeCurrent() 函数并未考虑时区和“夏令时”修正。EA 交易的运行基于价格变动,所以,如果您的 EA 交易需要处理时间,请使用 TimeCurrent() 函数。您便可以在策略测试程序中安全地测试自己的 EA 交易。

TimeGMT()、TimeDaylightSavings() 和 TimeGMTOffset() 函数完全基于用户计算机的当前设置运行(转为“夏令时”和改回标准时间并不在策略测试程序中模拟)。如果测试某 EA 交易时,您需要模拟改为“夏令时”和改回标准时间(如确有必要),自己要注意这一点。这就需要调整时钟的精确日期与时间的相关信息,以及一次全面的分析。

而此问题的解决方案,已经远远超出了一篇文章的论述范畴,所以这里亦不予考虑。如果某 EA 交易是在欧洲或美洲时段工作,尽管交易中心遵守“夏令时”,服务器时间与事件时间之间也不会有差异,这一点与亚洲时段不同(日本不实行“夏令时”,而且澳大利亚是在 11 月份改为“夏令时”)。

总结

本文讨论了处理时间的所有标准 MQL5 函数。讲述了处理时间相关任务时使用的编程技巧。此外,本文还演示了多种指标和一些有用函数的创建,且包含对于运行原理的详细描述。

处理时间的所有标准函数,均可划分为几个类别:

  1. TimeCurrent()TimeLocal() 是用于确定当前时间的主函数。
  2. TimeToString()StringToTime()TimeToStruct()StructToTime() 为时间处理函数。
  3. CopyTime() 是一种处理柱时间的函数。
  4. TimeTradeServer()TimeGMT()TimeDaylightSavings()TimeGMTOffset() 是取决于用户计算机设置的函数。

随附文件

  • sTestArea.mq5 - 测试复杂时间函数的一个脚本。
  • sTestArea_Pivot1.mq5 - sTestArea.mq5 脚本,用于测试 Pivot1.mq5 指标的时间函数。
  • Pivot1.mq5 - 一个采用标准日的支点指标,(NewDay 函数)。
  • Session.mq5 - 日的交易时段指标(TimeSession 函数)。
  • TimePoint.mq5 - 一种某给定时间点的指标(TimeCross 函数)。
  • Pivot2.mq5 - 一个采用用户定义日的支点指标(NewCustomDay 函数)。
  • TradeWeekDays.mq5 - 一个周交易日的指标(WeekDays_Check 函数)。
  • SessionWeek.mq5 - 周的交易时段指标(WeekSession 函数)。
  • TimeFunctions.mqh - 本文提供的所有时间函数,都在一个文件中。

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

附加的文件 |
stestarea.mq5 (4.96 KB)
pivot1.mq5 (4.5 KB)
session.mq5 (3.51 KB)
timepoint.mq5 (3.32 KB)
pivot2.mq5 (4.14 KB)
tradeweekdays.mq5 (3.59 KB)
sessionweek.mq5 (5.08 KB)
timefuncions.mqh (20.52 KB)
摆脱自制的 DLL 摆脱自制的 DLL

如果 MQL5 语言的功能性不足以完成任务,MQL5 程序员不得不诉诸于其他工具。他们必须转向其他编程语言并创建中间 DLL。MQL5 可提供各种数据类型并将它们传递至 API,但遗憾的是,MQL5 无法解决从收到的指针提取数据的相关问题。在本文中,我们将循规蹈矩,说明交换和使用复杂数据类型的简单机制。

Dr. Tradelove 或我如何不再担忧并创建一个自训练 EA 交易 Dr. Tradelove 或我如何不再担忧并创建一个自训练 EA 交易

一年前,joo 在他的《遗传算法 - 很简单!》一文中向我们提供了一种用 MQL5 实施遗传算法的工具。现在,利用这个工具,我们将创建一个 EA 交易,该 EA 将依据某些边界条件对其自己的参数进行遗传优化。

指标喷发整体特征的计算 指标喷发整体特征的计算

指标喷发是市场研究中较少涉及的一个领域。这主要是由于时变数据超大数组的处理造成的分析难度。现有的图形分析过于资源密集,并由此触发了一种采用喷发时间序列的俭省算法的发展。本文要论述的,就是如何利用喷发整体特征的研究,来替代可视(直观图像)分析。无论是交易者,还是自动化交易系统的开发者,都会感兴趣。

Mac OS 上的 MetaTrader 5 Mac OS 上的 MetaTrader 5

Apple 产品现已越来越深入人心。MetaQuotes Software Corp. 紧跟 IT 行业的脚步,并已发布了针对基于 iOS 设备的专门移动应用 - iPhone 版 MetaTrader 4 和 iPhone 版 MetaTrader 5。MQL5.community 论坛上也有许多话题,人们可以在此找到 Mac OS 操作系统下运行 MetaTrader 5 的解决方案。本文中,您将学会如何通过 Apple 的流行操作系统使用 MetaTrader 5。