处理时间(第二部分):函数·进阶篇
(2/3)·手工核对经纪商时间错位既慢又易错,本篇拆解全局变量与核心函数的运作机制
◍ 周末时区切换怎么算才不漏
在遍历 EURUSD 历史K线前,先要把经纪商时间里的夏令时/冬令时切换点算准。核心是一个 nextDST(zone, t) 函数,zone 传 "EUR"、"USD" 或 "AUD",t 一般取 TimeCurrent();它先判断是否需要重算,比如 EUR 区若 t 小于已记录的 nxtSwitch_EUR 就直接返回,这也是为什么回测开头必须把 nxtSwitch_EUR 清零——不清零可能在整段测试里都不触发重算。 判定某月第几个周日的算法不是新发明,网上早有流传。以欧盟为例,3月最后一个周日的日期用 31 - MOD(ROUNDDOWN(5*年/4);7) 算,2021 年得出 25 号,即 2021.03.25 03:00 欧盟切到夏令时。美、欧、澳逻辑类似,只是月份和周序不同:美国是3月第2个周日与11月第1个周日,澳洲是11月第1个周日与3月最后周日(南半球季节相反)。 单次调用会扫完三个地区一年里的三个相关时段:切换前、两次切换之间、切换后。实测输出里,2019.02.05 欧区 DST=0、下个切换 2019.03.31 03:00;2019.06.05 DST=-3600、下个切换 2019.10.27 03:00;2019.11.20 又回 0、指向 2020.03.29。外汇与贵金属受此影响报价时间戳会跳变,属高风险品类,验证前先确认自己经纪商时区。 别把正态当圣经 澳洲的冬夏令颠倒容易看错:欧洲冬天时澳洲是夏天,所以 AUD 的 DST 在 EUR 的冬季反而生效,写跨半球品种逻辑时别照抄 EUR 分支。
class="type">void nextDST(class="type">class="kw">string zone, class="type">class="kw">datetime t) { if((zone == "EUR") && t < nxtSwitch_EUR) { if(IS_DEBUG_MODE) Print("no change as time < nxtSwitch_EUR"); class="kw">return; } ... d = (class="type">int)(class="num">31 - MathMod((class="num">4 + MathFloor(class="num">5*y/class="num">4)), class="num">7)); class=class="str">"cmt">// determine the last Sunday in March for the EU class="kw">switch spr = StringToTime(""+(class="type">class="kw">string)y+".class="num">03."+(class="type">class="kw">string)d+" class="num">03:class="num">00"); class=class="str">"cmt">// convert to class="type">class="kw">datetime format if(zone == "EUR") { d = (class="type">int)(class="num">31 - MathMod((class="num">4 + MathFloor(class="num">5*y/class="num">4)), class="num">7)); class=class="str">"cmt">// determine the last Sunday in March for the EU class="kw">switch spr = StringToTime(""+(class="type">class="kw">string)y+".class="num">03."+(class="type">class="kw">string)d+" class="num">03:class="num">00"); class=class="str">"cmt">// convert to class="type">class="kw">datetime format if(t < spr) { DST_EUR = class="num">0; class=class="str">"cmt">// no time offset nxtSwitch_EUR = spr; class=class="str">"cmt">// set the next time class="kw">switch if(IS_DEBUG_MODE) Print(zone,"-DST for ",TimeToString(t)," DST: ",StringFormat("% 5i",DST_EUR)," nxtSwitch: ",DoWs(nxtSwitch_EUR)," ",TimeToString(nxtSwitch_EUR)); class="kw">return; } }
「欧区冬夏令切换的日期推算与偏移赋值」
欧洲时区每年两次切换夏令时:三月最后一个周日为春切(回冬令偏移 0),十月最后一个周日为秋切(回夏令偏移 -3600 秒,即伦敦时间相对 GMT+1)。下面这段代码用整数运算直接算出这两个周日,而不依赖任何外部日历表。 d = (int)(31 - MathMod((1 + MathFloor(5*y/4)), 7)); 这行的核心在于:年份 y 乘 5 除 4 向下取整,加 1 后对 7 取模,再用 31 减,得到的就是 10 月最后一个周日的日期号。把 y、'.10.'、d 拼成字符串丢给 StringToTime,就能拿到该日 03:00 的 datetime。 若当前时间 t 早于秋切 aut,说明还在夏令区间内,DST_EUR 赋 -3600,并把 nxtSwitch_EUR 设为 aut 作为下次切换锚点;否则 y 自增一年,重算三月最后一个周日 spr,t 早于 spr 则冬令偏移为 0。两个分支都会 return,落空才走到末尾 Print 报错——这意味着传入的 t 居然不在任何合理区间,大概率是 y 初值或时区 zone 传错。 在 MT5 里把 IS_DEBUG_MODE 打开跑一遍,能看到类似 '-DST for 2023.10.28 02:00 DST: -3600 nxtSwitch: Sun 2023.10.29 03:00' 的日志,对照自己经纪商 EURUSD 的点差跳变时间,能验证时区偏移是否算准。外汇与贵金属受宏观事件冲击,时区错配可能造成止损触发偏差,实盘前务必用历史数据自测。
d = (class="type">int)(class="num">31 - MathMod((class="num">1 + MathFloor(class="num">5*y/class="num">4)), class="num">7)); class=class="str">"cmt">// determine the last Sunday in October for the EU class="kw">switch aut = StringToTime(""+(class="type">class="kw">string)y+".class="num">10."+(class="type">class="kw">string)d+" class="num">03:class="num">00"); class=class="str">"cmt">// convert to class="type">class="kw">datetime format if(t < aut) { DST_EUR =-class="num">3600; class=class="str">"cmt">// = +1h => class="num">09:class="num">00 London time = GMT+05h+DST_EU = GMT+class="num">0+class="num">1 = GMT+class="num">1; nxtSwitch_EUR = aut; class=class="str">"cmt">// set the next time class="kw">switch if(IS_DEBUG_MODE) Print(zone,"-DST for ",TimeToString(t)," DST: ",StringFormat("% 5i",DST_EUR)," nxtSwitch: ",DoWs(nxtSwitch_EUR)," ",TimeToString(nxtSwitch_EUR)); class="kw">return; } y++; class=class="str">"cmt">// re-calc the spring class="kw">switch for the next year d = (class="type">int)(class="num">31 - MathMod((class="num">4 + MathFloor(class="num">5*y/class="num">4)), class="num">7)); class=class="str">"cmt">// determine the last Sunday in March for the EU class="kw">switch spr = StringToTime(""+(class="type">class="kw">string)y+".class="num">03."+(class="type">class="kw">string)d+" class="num">03:class="num">00"); class=class="str">"cmt">// convert to class="type">class="kw">datetime format if(t < spr) { DST_EUR = class="num">0; class=class="str">"cmt">// no time offset nxtSwitch_EUR = spr; class=class="str">"cmt">// set the next time class="kw">switch if(IS_DEBUG_MODE) Print(zone,"-DST for ",TimeToString(t)," DST: ",StringFormat("% 5i",DST_EUR)," nxtSwitch: ",DoWs(nxtSwitch_EUR)," ",TimeToString(nxtSwitch_EUR)); class="kw">return; } Print("ERROR for ",zone," @ ",TimeToString(t)," DST: ",StringFormat("% 5i",DST_EUR)," nxtSwitch: ",DoWs(nxtSwitch_EUR)," ",TimeToString(nxtSwitch_EUR)," winter: ",TimeToString(aut)," spring: ",TimeToString(spr)); class="kw">return; }
用 nextDST 抓主要货币区的夏令时切换点
在 MT5 里做跨时区策略回测时,DST(夏令时)切换会直接改掉蜡烛的 UTC 偏移,不处理就会让欧美、澳美等品种的会话起点算错。MQL5 提供 nextDST 函数,传入币种代码和某个时间点,就能拿到该币种下一次进入或退出夏令时的具体时刻。 下面这段调用把 EUR、USD、AUD 在 2019 年三个典型日期(2月、6月、11月)的 DST 边界都打了一遍。注意 2019.02.05 和 2019.06.05 都落在欧美冬夏令切换窗口附近,而 2019.11.20 已临近美国退出夏令时,返回值会显示距该时间点最近的一次切换。 外汇与贵金属交易受时区错配影响大,DST 算错可能让止损触发逻辑偏移数个小时,属高风险隐患。开 MT5 把这段代码贴进脚本跑一遍,核对返回的时间戳是否和你经纪商的历史会话对齐,是验证时区处理的直接动作。
nextDST("EUR", D&class="macro">#x27;class="num">2019.02.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("EUR", D&class="macro">#x27;class="num">2019.06.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("EUR", D&class="macro">#x27;class="num">2019.11.class="num">20 class="num">20:class="num">00&class="macro">#x27;); nextDST("USD", D&class="macro">#x27;class="num">2019.02.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("USD", D&class="macro">#x27;class="num">2019.06.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("USD", D&class="macro">#x27;class="num">2019.11.class="num">20 class="num">20:class="num">00&class="macro">#x27;); nextDST("AUD", D&class="macro">#x27;class="num">2019.02.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("AUD", D&class="macro">#x27;class="num">2019.06.class="num">05 class="num">20:class="num">00&class="macro">#x27;); nextDST("AUD", D&class="macro">#x27;class="num">2019.11.class="num">20 class="num">20:class="num">00&class="macro">#x27;);
◍ 俄罗斯时区跳变别用固定规则硬套
MQ 体系有俄罗斯背景,俄区用户量也不小,所以 GMT 偏移计算里不能漏掉俄罗斯自身的时钟变更。难点在于:俄罗斯从 1970 到 2014 年之间,冬令时偏移在 -10800 秒(UTC+3)和 -14400 秒(UTC+4)之间反复横跳,且切换日期毫无线性规律,靠公式推算容易出错。 稳妥做法是把历次变更点塞进一个二维数组,第一列是生效时间,第二列是相对 GMT 的秒偏移。下面这段就是直接用查表方式解决,数组实际有 67 行(SzRussiaTimeSwitch = 67),覆盖 1970.01 到 3000.12 的兜底值。 offsetRubGMT() 的逻辑是倒序扫描数组,找到不大于传入时间 t 的最近一条记录,再取它的下一条作为下次切换点和当前偏移。注意第 66 行索引用了 fmin 防止越界——最后一条 3000.12.31 23:59 的 -10800 只是防溢出占位,不是真实政策。 在 MT5 里把这段贴进 EA 或脚本,传一个历史 datetime 进去,就能核对某笔俄区成交的 GMT 还原是否准。外汇和贵金属本身杠杆高、滑点凶,时区算错可能让回测和实盘对不上,务必自己跑一遍验证。
class="type">long RussiaTimeSwitch[][class="num">2] = { D&class="macro">#x27;class="num">1970.01.class="num">00 class="num">00:class="num">00&class="macro">#x27;, -class="num">10800, D&class="macro">#x27;class="num">1980.01.class="num">00 class="num">00:class="num">00&class="macro">#x27;, -class="num">10800, D&class="macro">#x27;class="num">1981.04.class="num">01 class="num">00:class="num">00&class="macro">#x27;, -class="num">14400, ... D&class="macro">#x27;class="num">2012.01.class="num">00 class="num">00:class="num">00&class="macro">#x27;, -class="num">14400, D&class="macro">#x27;class="num">2014.10.class="num">26 class="num">02:class="num">00&class="macro">#x27;, -class="num">10800, D&class="macro">#x27;class="num">3000.12.class="num">31 class="num">23:class="num">59&class="macro">#x27;, -class="num">10800 }; class="type">int SzRussiaTimeSwitch = class="num">67; class=class="str">"cmt">// ArraySize of RussiaTimeSwitch class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Russian Time Switches | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void offsetRubGMT(const class="type">class="kw">datetime t) { class="type">int i = SzRussiaTimeSwitch; class=class="str">"cmt">//ArrayRange(RussiaTimeSwitch,class="num">0); class="num">66 while(i-->class="num">0 && t < RussiaTimeSwitch[i][class="num">0]) class="kw">continue; class=class="str">"cmt">// t >= RussiaTimeSwitch[i][class="num">0] nxtSwitch_RUB = (class="type">class="kw">datetime)RussiaTimeSwitch[fmin(SzRussiaTimeSwitch-class="num">1,i+class="num">1)][class="num">0]; DST_RUS = (class="type">int)RussiaTimeSwitch[fmin(SzRussiaTimeSwitch-class="num">1,i+class="num">1)][class="num">1]; class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+
「时移状态的实时刷新逻辑」
在 EA 或指标运行时,时移不是一次算完就不动的。checkTimeOffset() 就是专门盯着当前时刻 tB,决定要不要重新拉取各币种夏令时状态的守门函数。 函数开头先比对 tB 与 nxtSwitch_USD / EUR / AUD 三个下次切换时间。若 tB 比这三者都小,说明没有任何地区跨过 DST 边界,直接 return,省掉无谓计算。 一旦 tB 超过某个币种的切换点,就调用 nextDST() 重算该币种状态。USD 和 EUR 是必算项,因为经纪商时移由美欧二者相对冬夏状态决定;AUD、RUB 只在用户关心时才保留,不需要就用 // 注释掉对应行。 重算完后按组合赋值:DST_USD+DST_EUR==0 代表欧美都在冬令时,取 USwinEUwin 偏移与 secFxWiWi 市场秒数;二者相等且非 0 即同处夏令时,走 USsumEUsum / secFxSuSu;不等则是美夏欧冬,用 USsumEUwin / secFxSuWi。这样无论实盘还是策略测试器,都能随时推出 GMT 与本地时间。 别把正态当圣经:测试器里手动改系统时间往前拨,若 actOffset 没跟着跳,多半是 nxtSwitch_* 初始值没覆盖到回测起点。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| function to determine broker offset for the time tB given | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void checkTimeOffset(class="type">class="kw">datetime tB) { if(tB < nxtSwitch_USD && tB < nxtSwitch_EUR && tB < nxtSwitch_AUD) class="kw">return; class=class="str">"cmt">// nothing has changed, class="kw">return if(tB>nxtSwitch_USD) nextDST("USD", tB); class=class="str">"cmt">// US has switched if(tB>nxtSwitch_EUR) nextDST("EUR", tB); class=class="str">"cmt">// EU has switched if(tB>nxtSwitch_AUD) nextDST("AUD", tB); class=class="str">"cmt">// AU has switched if(tB>nxtSwitch_RUB) nextDST("RUB", tB); class=class="str">"cmt">// RU has switched if(DST_USD+DST_EUR==class="num">0) class=class="str">"cmt">// both in winter(normal) time { OffsetBroker.actOffset = OffsetBroker.USwinEUwin; OffsetBroker.actSecFX = OffsetBroker.secFxWiWi; } else if(DST_USD == DST_EUR) class=class="str">"cmt">// else both in summer time { OffsetBroker.actOffset = OffsetBroker.USsumEUsum; OffsetBroker.actSecFX = OffsetBroker.secFxSuSu; } else if(DST_USD != DST_EUR) class=class="str">"cmt">// US:summer EU:winter { OffsetBroker.actOffset = OffsetBroker.USsumEUwin; OffsetBroker.actSecFX = OffsetBroker.secFxSuWi; } }
用脚本把时区偏移跑一遍
把时区处理封装进 DealingWithTime.mqh 后,最省事的办法是写个附加脚本 DealingWithTimeScript.mq5,直接在智能系统日志里把经纪商时间、GMT、纽约时间的对应关系打出来。日志里能看到 EUR/USD 在 2020.10.23 23:00 经纪商时间对应 GMT 20:00、纽约 16:00,距外汇收盘还剩 143h;而 2021.04.02 的 hDiff 变成 -3,说明夏令时切换让偏移量跳了 1 小时。 脚本先用 CopyTime("EURUSD",PERIOD_H1,datetime(0),TimeCurrent(),arr1h) 把 EURUSD 的 H1 历史时间戳全拉进数组,再靠 while(++b<=sz) 从最老一根遍历到当前根,频率跟策略测试器回测一致。每根新柱先调 checkTimeOffset(arr1h[b]) 做轻量重算检查,避免重复计算吃资源。 为了不淹没在几千根柱里,脚本按随机值挑大约 1/5 的柱来打印。输出里 t[28194] 对应 2002.05.20 22:00 经纪商时间,DST_EUR 为 -3600,GMT 推算为 19:00、纽约 15:00,距收盘 left:97h——验证一下:周一到周五 22:00 是 96h,加周五最后 1h 正好 97h,逻辑自洽。 一套跑下来,EA 或指标其实只需两个调用:setBokerOffset() 拿到冬夏令偏移,再配合 checkTimeOffset() 实时修正。外汇和贵金属市场受时区跳变影响大,夏令时切换周报价可能错位,上 MT5 挂这个脚本核一遍自己经纪商的 hDiff 再实盘更稳妥。
class="macro">#include <DealingWithTime.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Finding the broker offsets | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- step class="num">1: set the broker time offsets in winter, summer and in between class="type">bool isTimeSet = setBokerOffset(); if(!isTimeSet) { Alert("setBokerOffset failed"); class="kw">return; } class=class="str">"cmt">//--- step class="num">2: get the quotes(here only 1h time stamps) class="type">class="kw">datetime arr1h[], tGMT, tNY, tLeft;