夏令时(本地)

为确定本地时钟是否已切换至夏令时,MQL5 提供了 TimeDaylightSavings 函数。该函数会获取你的操作系统的设置。

确定服务器上的夏令时时间并非易事。为此,你需要对 报价财经日历 事件或展期交割/掉期时间(在 账户交易历史记录中)实施 MQL5 分析。在下面的示例中,我们将显示其中一种方案。

int TimeDaylightSavings()

该函数返回秒数修正(如果已应用了夏令时)。冬季时间是每个时区的标准时间,因此该期间的修正为零。条件性形式的获取修正的公式可编写如下:

TimeDaylightSavings() = TimeLocal winter() - TimeLocal summer()

例如,如果标准时区 (winter) 等于 UTC+3(即时区时间比 UTC 早 3 小时),则在转换到夏令时 (summer) 的过程中,我们加 1 小时,得到 UTC+4。其中 TimeDaylightSavings 将返回 -3600。

该函数的一个使用示例在 TimeSummer.mq5 脚本中提供,其中还给出了一种识别服务器上适当模式的可能实证方法。

void OnStart()
{
   PRTF(TimeLocal());          // local time of the terminal
   PRTF(TimeCurrent());        // last known server time
   PRTF(TimeTradeServer());    // estimated server time
   PRTF(TimeGMT());            // GMT time (calculation from local via time zone shift)
   PRTF(TimeGMTOffset());      // time zone shift compare to GMT, in seconds
   PRTF(TimeDaylightSavings());// correction for summer time in seconds
   ...

首先,我们显示所有类型的时间以及 MQL5 提供的修正(函数 TimeGMTTimeGMTOffset 将在关于 世界时间的下一节中探讨,但从前面的描述中我们应该已经大致清楚它们的含义了)。

该脚本应在交易日运行。日志中的条目将对应于你的计算机和经纪商的服务器的设置。

TimeLocal()=2021.09.09 22:06:17 / ok
TimeCurrent()=2021.09.09 22:06:10 / ok
TimeTradeServer()=2021.09.09 22:06:17 / ok
TimeGMT()=2021.09.09 19:06:17 / ok
TimeGMTOffset()=-10800 / ok
TimeDaylightSavings()=0 / ok

在本例中,客户的时区与 GMT 相差 3 小时 (UTC+3),没有夏令时调整。

现在我们看看服务器的情况。基于 TimeCurrent 函数的值,我们可以确定服务器的当前时间,但不能确定其标准时区,因为该时间可能涉及夏令时转换(MQL5 不提供关于是否使用夏令时以及是否当前已启用夏令时的信息)。

为确定服务器的真实时区和夏令时,我们将利用服务器时间转换会影响报价这一事实。与大部分用于解决问题的实证方法一样,这一实证方法在某些情况下可能无法给出完全正确的结果。如果与其它来源的比较显示不一致,则应选择不同的方法。

外汇市场在 UTC 时间星期六 22:00 点开盘(这对应于亚太地区早盘开始),在星期五 22:00 点关闭(美洲交易收盘)。这表示在 UTC+2 时区(东欧)的服务器上,第一柱线将在星期一 0 时 0 分准时出现。根据中欧时间(对应于 UTC+1),交易周在星期六 23:00 点开始。

在计算了每周末暂停后第一柱线 H1 的当天偏移统计之后,我们将获得服务器时区的估计结果。当然,为此最好使用流动性最强的外汇金融工具 EURUSD。

如果在年度期间的统计中找到两个最大当日偏移,并且它们的位置彼此相邻,这就表示经纪商正在切换到夏令时,反之亦然。

请注意,夏季和冬季时间周期不相等。因此,若在三月初切换到夏季时间而在十一月初返回冬季时间时,我们得到约 8 个月的夏季时间。这将影响统计中最大值的比率。

有了两个时区之后,我们就可以轻松确定当前处于活动状态的时区,进而确定当前是否存在夏令时修正。

将时钟切换到夏令时,经纪商的时区将从 UTC+2 变更为 UTC+3,将把周开始时间从 22:00 点偏移到 21:00 点。这将影响 H1 柱线的结构:在图表上,我们将看到在星期天晚上有三个柱线,而不是两个。

EURUSD H1 图表上从冬季时间 (UTC+2) 到夏季 (UTC+3) 时间的变化

EURUSD H1 图表上从冬季时间 (UTC+2) 到夏季 (UTC+3) 时间的变化

为实现这点,我们有一个单独函数 ServerTimeZone。对内置函数 CopyTime 的调用负责获取报价,或者更准确的说,获取柱线时间戳(我们将在关于 访问时间序列的章节学习该函数)。

ServerTime ServerTimeZone(const string symbol = NULL)
{
  const int year = 365 * 24 * 60 * 60;
  datetime array[];
  if(PRTF(CopyTime(symbolPERIOD_H1TimeCurrent() - yearTimeCurrent(), array)) > 0)
  {
     // here we get about 6000 bars in the array
     const int n = ArraySize(array);
     PrintFormat("Got %d H1 bars, ~%d days"nn / 24);
     // (-V-) loop through H1 bars
     ...
  }
}

CopyTime 函数接收工作金融工具、H1 时间范围以及去年的日期范围作为参数。代替金融工具的 NULL 值表示脚本放置位置的当前图表交易品种,因此,建议选择有 EURUSD 的窗口。你可能猜到了,PERIOD_H1 常量对应于 H1。我们已经熟悉了 TimeCurrent 函数:返回当前最新的已知服务器时间。如果我们从其减去一年中的秒数(该秒数放在 year 变量中),我们将得到正好一年前的时期和时间。结果将进入 array 中。

为统计每周某跟柱线在特定小时开盘的次数,我们保留了 hours[24] 数组。将通过遍历生成的 array 来执行计算,即,按从过去到现在的柱线计算。每次迭代时,被查看的该周的开盘时间将存储在 current 变量中。当循环结束时,服务器的当前时区将保持在 current 中,因为当前周将最后处理。

     // (-v-) cycle through H1 bars
     int hours[24] = {};
     int current = 0;
     for(int i = 0i < n; ++i)
     {
     // (-V-) processing of the i-th bar H1
        ...
     }
     
     Print("Week opening hours stats:");
     ArrayPrint(hours);

在天数循环中,我们将使用来自头文件 MQL5Book/DateTime.mqhdatetime 类(参见 日期和时间)。

        // (-v-) processing the i-th bar H1
        // find the day of the week of the bar
        const ENUM_DAY_OF_WEEK weekday = TimeDayOfWeek(array[i]);
        // skip all days except Sunday and Monday
        if(weekday > MONDAYcontinue;
        // analyze the first bar H1 of the next trading week
        // find the hour of the first bar after the weekend
        current = _TimeHour();
        // calculate open hours statistics
        hours[current]++;
        
        // skip next 2 days
        // (because the statistics for the beginning of this week have already been updated)
        i += 48;

建议的算法不是最优的,但不要求我们理解时间序列组织方式的技术细节,这些内容我们尚未学习。

有些周没有设置格式(在节假日之后开始)。如果此情况发生在最后一周,则 current 变量将包含异常偏移。这可通过统计来验证:对于得到的小时数,记录的每周“开盘”次数将非常少。在此情况下,在测试脚本中,会直接在日志中显示一条消息。在实践中,应核实前一到两周的标准开盘时间。

     // (-V-) cycle through H1 bars
     ...
     if(hours[current] <= 52 / 4)
     {
        // TODO: check for previous weeks
        Print("Extraordinary week detected");
     }

如果经纪商未切换到夏令时,则统计数据中将出现一个最大值,其将包含所有或几乎所有的周。如果经纪商实行时区更改,则在统计中将会有两个高值。

     // find the most frequent time shift
     int max = ArrayMaximum(hours);
     // then check if there is another regular shift
     hours[max] = 0;
     int sub = ArrayMaximum(hours);

我们需要确定第二个极高值的重要性(即区别于可能导致周初开盘时间偏移的随机假期)。为此,我们评估一年的一个季度(52 周/4)的统计数据。如果超过了该限值,则经纪商支持夏令时。

     int DST = 0;
     if(hours[sub] > 52 / 4)
     {
        // basically, DST is supported 
        if(current == max || current == sub)
        {
           if(current == MathMin(maxsub))
              DST =fabs(max -sub); // DST is enabled now
        }
     }

如果当前周的开盘偏移(存储在当前变量中)与两个极高值之一重合,则当前周正常开盘,可据此推断时区(这一保护性条件是必要的,因为我们对于非标准周没有修正,而是仅会发出一个警告)。

现在一切就绪,可以确定我们的函数回应:服务器时区和启用夏令时的标志。

 current +=2 +DST;// +2 to get offset from UTC
     current %= 24;
 // timezones are always in the range [UTC-12,UTC+12]
     if(current > 12current = current - 24;

由于我们需要从函数返回两个特性(currentDST),并且除此之外,我们可以告知调用代码,经纪商是否使用夏令时(即使现在是冬季),因此声明一个具有所有要求字段的特殊结构体 ServerTime 是合理的。

struct ServerTime
{
 intoffsetGMT;      // timezone in seconds relative to UTC/GMT
 intoffsetDST;      // DST correction in seconds (included in offsetGMT)
 boolsupportDST;    // DST correction detected in quotes in principle
 stringdescription// result description
};

然后,在 ServerTimeZone 函数中,我们可以填充和返回由此得到的这样一个结构体。

     ServerTime st = {};
     st.description = StringFormat("Server time offset: UTC%+d, including DST%+d"currentDST);
     st.offsetGMT = -current * 3600;
     st.offsetDST = -DST * 3600;
     return st;

如果因某些原因该函数不能获取到报价,则返回一个空结构体。

ServerTime ServerTimeZone(const string symbol = NULL)
{
  const int year = 365 * 24 * 60 * 60;
  datetime array[];
  if(PRTF(CopyTime(symbolPERIOD_H1TimeCurrent() - yearTimeCurrent(), array)) > 0)
  {
     ...
     return st;
  }
  ServerTime empty = {-INT_MAX, -INT_MAXfalse};
  return empty;
}

我们来以实操方式检查新函数,我们为该新函数在 OnStart 中添加以下指令:

   ...
   ServerTime st = ServerTimeZone();
   Print(st.description);
   Print("ServerGMTOffset: "st.offsetGMT);
   Print("ServerTimeDaylightSavings: "st.offsetDST);
}

我们来看看可能的结果。

CopyTime(symbol,PERIOD_H1,TimeCurrent()-year,TimeCurrent(),array)=6207 / ok
Got 6207 H1 bars, ~258 days
Week opening hours stats:
52  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
Server time offset: UTC+2, including DST+0
ServerGMTOffset: -7200
ServerTimeDaylightSavings: 0

根据为 H1 柱线收集的统计数据,该经纪商的周开盘严格在星期一 00:00。因此,真实时区等于 UTC+2,,并且没有夏季时间修正,即服务器时间必须匹配 EET (UTC+2)。然而在实践中,如同我们在日志第一部分中看到的,服务器上的时间与 GMT 相差 3 小时。

这里我们可以假定我们碰到了一个全年以夏季时间工作的服务器。在此情况下,函数 ServerTimeZone 便无法区分修正和“时区”中的额外小时:结果,DST 模式将等于零,而根据服务器报价计算得出的 GMT 时间将比真实时间向右偏移一小时。我们最初假设的“报价从星期天 22:00 点开始”与该服务器的运行模式不符。此类问题应通过经纪商的支持服务进行澄清。