处理时间(第二部分):函数·综合运用
(3/3)·手工核对经纪商夏令时冬令时切换太折磨人,10 个小节拆完自带代码的时间处理函数体系
不少交易者遇到经纪商时间错位的第一个动作是发工单问客服,得到的答复往往语焉不详。与其等一个不充分的解释,不如让程序在几周内自动抓取价格计算逻辑——这正是 PC 该替人干的活。
用随机采样回放 EURUSD 的经纪商时差
想在 MT5 里验证经纪商服务器时间与 GMT、纽约时间的偏移,不必逐根 K 线硬算。下面这段代码只取 EURUSD 的 H1 时间序列,从 1970 年 0 时刻一直拉到当前,再用随机间隔抽样打印,既能压低日志量,也能看出周末后第一根 bar 的时间跳变。 核心逻辑是先 CopyTime 拿全量时间数组,sz 为数组末尾下标(最旧时间戳在 arr1h[0])。nChecks 等于 sz/5,意味着大约每 5 根里随机挑 1 根做时区换算与输出,chckBar 用 MathRand()%nChecks 决定首根检查位置。 循环里对每根 bar 调用 checkTimeOffset 看时间情境,通常只在周末后首根触发。当 b>=chckBar 或走到最新 bar 时,算 tGMT(加经纪商实际偏移)、tNY(再减纽约冬夏令偏移)、tLeft(距 FX 收盘剩余秒数转小时),然后用 PrintFormat 把 DST、GMT、NY、剩余时间一并打出。 若 setBokerOffset 返回 false 直接 Alert 并 return,说明偏移校准失败,此时抽样结果不可信。外汇与贵金属市场高杠杆、高波动,这类时区脚本仅用于核对数据,不代表任何交易方向暗示。 把下面代码贴进 MT5 脚本跑一次,重点看打印里 End-FX 的 left 值:若多数 bar 显示 left 为负,说明你的 OffsetBroker.actSecFX 设得偏短,可能漏算周五收盘。
CopyTime("EURUSD",PERIOD_H1,class="type">class="kw">datetime(class="num">0),TimeCurrent(),arr1h); class="type">int b = class="num">0, sz = ArraySize(arr1h)-class="num">1, class=class="str">"cmt">// oldest time stamp nChecks = sz/class="num">5, class=class="str">"cmt">// ~class="num">2*class="num">5+class="num">1 randomly chosen bars chckBar = MathRand()%nChecks; class=class="str">"cmt">// index of the first bar to check class=class="str">"cmt">//--- step class="num">3: simulate an EA or an indicator go through the time from the past to now while(++b<=sz) { class=class="str">"cmt">//--- check the time situation, normally do it at the first bar after the weekend checkTimeOffset(arr1h[b]); class=class="str">"cmt">//--- for a randomly selected bar calc. the times of GMT, NY & tLeft and print them if(b>=chckBar || b==sz) { tGMT = arr1h[b] + OffsetBroker.actOffset; class=class="str">"cmt">// GMT tNY = tGMT - (NYShift+DST_USD); class=class="str">"cmt">// time in New York tLeft = OffsetBroker.actSecFX - SoW(arr1h[b]); class=class="str">"cmt">// time till FX closes PrintFormat("DST_EUR:%+ 6i DST_EUR:%+ 6i t[%6i] tBrk: %s%s "+ "GMT: %s%s NY: %s%s End-FX: %2ih => left: %2ih ", DST_EUR,DST_USD,b, DoWs(arr1h[b]),TimeToString(arr1h[b],TIME_DATE|TIME_MINUTES), DoWs(tGMT),TimeToString(tGMT,TIME_DATE|TIME_MINUTES), DoWs(tNY),TimeToString(tNY,TIME_DATE|TIME_MINUTES), OffsetBroker.actSecFX/class="num">3600,tLeft/class="num">3600 ); chckBar += MathRand()%nChecks; class=class="str">"cmt">// calc. the index of the next bar to check } class="type">bool isTimeSet = setBokerOffset(); if(!isTimeSet) { Alert("setBokerOffset failed"); class="kw">return; } checkTimeOffset(TimeCurrent());
◍ 把时区偏移写进 EA 输入变量
上面那段脚本跑完,会得到 MetaQuotes 演示账户的时移备案:美欧同冬令 = -7200 秒,美欧同夏令 = -10800 秒,美夏欧冬 = -7200 秒。这组数字只是软件商的参考值,直接套到别的券商账户上会算错本地时间,外汇和贵金属交易里时区错乱可能让止损触发逻辑偏掉几个钟头。 下面这段 EA 骨架展示了怎么把偏移量做成 input 变量,在 OnInit 里赋给全局结构体,并在 OnTick 里一次性算出 GMT 与五大金融中心的本地时间。注意括号和分号,编译期不报错但运行期算错是最隐蔽的坑。 别把演示账户偏移当通用参数 代码里的 USwinEUwin 等三个 input 写死了 MetaQuotes 的值,注释也标了 DO NOT USE THEM FOR DIFFERENT BROKERS。实盘前务必用券商服务器时间反推自己的偏移,不然东京时间没夏令时这种细节也会漏。
class="macro">#include <DealingWithTime.mqh> class=class="str">"cmt">// offsets of MetaQuotes demo account: DO NOT USE THEM FOR DIFFERENT BROKERS!! input class="type">int USwinEUwin= -class="num">7200; class=class="str">"cmt">// US=Winter & EU=Winter input class="type">int USsumEUsum= -class="num">10800; class=class="str">"cmt">// US=Summer & EU=Summer input class="type">int USsumEUwin= -class="num">7200; class=class="str">"cmt">// US=Summer & EU=Winter class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- OffsetBroker.USwinEUwin = USwinEUwin; OffsetBroker.USsumEUsum = USsumEUsum; OffsetBroker.USsumEUwin = USsumEUwin; OffsetBroker.actOffset = WRONG_VALUE; nxtSwitch_USD = nxtSwitch_EUR = nxtSwitch_AUD = class="num">0; class=class="str">"cmt">//--- Just a simple test if not ste or changed if(OffsetBroker.USwinEUwin+OffsetBroker.USsumEUsum+OffsetBroker.USsumEUwin==class="num">0) OffsetBroker.set = false; else OffsetBroker.set = true; class=class="str">"cmt">//... class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- checkTimeOffset(TimeCurrent()); tGMT = TimeCurrent() + OffsetBroker.actOffset; class=class="str">"cmt">// GMT tNY = tGMT - (NYShift+DST_USD); class=class="str">"cmt">// time in New York tLon = tGMT - (LondonShift+DST_EUR); class=class="str">"cmt">// time in London tSyd = tGMT - (SidneyShift+DST_AUD); class=class="str">"cmt">// time in Sidney tMosc = tGMT - (MoskwaShift+DST_RUS); class=class="str">"cmt">// time in Moscow tTok = tGMT - (TokyoShift); class=class="str">"cmt">// time in Tokyo - no DST class=class="str">"cmt">//... }
「旧版时间库在 CopyTime 改动后怎么替」
Carl Schreiber 的 DealingWithTime.mqh 1.01 版在 MQ 修改 CopyTime() 行为后已失效:原先向未来时间传参会返回该值,现在改为返回当前最后一栏的最大开仓时间,导致用会话结束时间反推经纪商偏移得到错误结果。2.03 版已重写时间换算逻辑,覆盖悉尼到 70 年代复杂 DST,并附带 1975–2030 的 xlsx 表供核对公式。 新库多了一个 SecTillClose() 函数,直接以秒返回外汇收盘剩余时间,不再依赖 CopyTime()。想周末前平仓或避开周末开窗的交易者,可在 EA 里读这个值做过滤——外汇和贵金属周末跳空属高风险,这类计时只作概率参考。 附带指标把欧、美、澳三地 DST 和当前时差画在图表注释,末两行给出本时段最后一秒及剩余小时/秒。纽约周五当地 17:00 无有效栏,函数减 1 秒取经纪商最后一栏开仓时间;但部分经纪商会提前几分钟停报价,实际以各自服务器为准。 下面这段是 2020 年 10–11 月某次运行的输出样例,可对照验证你本地经纪商偏移:EUR 行显示 BrokerTime 比 GMT 慢 3 小时、距 FX 收盘 143h;USD 行差 -2h、剩 142h;NXT 行差 -2h、剩 143h。若你终端算出来和数字对不上,优先怀疑 CopyTime 版本而非经纪商。
EUR: Fr.class="num">2020.10.class="num">23 class="num">23:class="num">00: hNY:class="num">16 hGMT:class="num">20 hTC:class="num">23 hDiff:-class="num">3 BrokerTime => GMT: class="num">2020.10.class="num">23 class="num">20:class="num">00 => tNY: class="num">2020.10.class="num">23 class="num">16:class="num">00 End-FX after: 143h USD: Fr.class="num">2020.10.class="num">30 class="num">22:class="num">00: hNY:class="num">16 hGMT:class="num">20 hTC:class="num">22 hDiff:-class="num">2 BrokerTime => GMT: class="num">2020.10.class="num">30 class="num">20:class="num">00 => tNY: class="num">2020.10.class="num">30 class="num">16:class="num">00 End-FX after: 142h NXT: Fr.class="num">2020.11.class="num">06 class="num">23:class="num">00: hNY:class="num">16 hGMT:class="num">21 hTC:class="num">23 hDiff:-class="num">2 BrokerTime => GMT: class="num">2020.11.class="num">06 class="num">21:class="num">00 => tNY: class="num">2020.11.class="num">06 class="num">16:class="num">00 End-FX after: 143h
经纪商时区与夏令时的偏移实测
把 MT5 日志里 BrokerTime、GMT、纽约时间三者摆在一起看,能直接反推平台用的时区基准。以上面 2021.03.12 的 USD 记录为例:经纪商显示 23:00,对应 GMT 21:00、纽约 16:00,hDiff 为 -2,说明该经纪商时间比 GMT 快 2 小时(属冬令时下的 EU 区偏移)。 MetaQuotes 官方软件在不同季节组合下的固定偏移也很关键:美冬欧冬与美夏欧冬都是 -7200 秒(即 -2 小时),而美夏欧夏是 -10800 秒(-3 小时)。这意味着每年 3 月最后一个周日至 10 月最后一个周日,欧美同步夏令时期间,终端底层时间基准会多偏 1 小时。 注意 End-FX after 这个字段:2021.03.12 记录为 143h,2021.03.26 为 142h。差出来的 1 小时正是夏令时切换造成的结算窗口平移。外汇与贵金属杠杆交易高风险,这类时区误差若没校准,EA 的 K 线对齐和跨平台回测可能系统性偏移。 开 MT5 按 F11 调出专家日志,搜 BrokerTime => GMT,把最近半年的周五 22:00–23:00 记录拉出来,自己算一遍 hDiff,就能确认你这套环境当前挂在哪个时区桶里。
◍ 两家经纪商时区偏移的实测对照
做跨时区会话切分前,先确认经纪商服务器时间跟 GMT/纽约时间的真实偏移。RoboForex 与 Alpari International 在三种夏令冬令组合下给出的偏移完全一致:美欧同冬令 -7200 秒(2 小时)、美欧同夏令 -10800 秒(3 小时)、美夏令欧冬令 -7200 秒。 从 2020.10–2021.04 的逐周采样看,NXT 品种在 2021.04.02 23:00 经纪商时间对应 GMT 20:00、纽约 16:00,hDiff 为 -3,End-FX after 标为 143h;而 EUR 在 2021.03.26 22:00 经纪商时间对应 GMT 20:00,hDiff 仅 -2,End-FX after 为 142h。同一经纪商下不同品种换季节点差 1 小时,容易让自动划线把周五收盘算错。 开 MT5 终端把这两家账户的「经纪商时间→GMT」偏移写死进脚本常量前,建议先用历史 tick 回放验证 2020.10.30 与 2021.03.12 两个 USD 切换点:前者 hDiff -2、End-FX after 142h,后者 hDiff -2、End-FX after 143h。外汇与贵金属杠杆高,时区错算会直接放大滑点风险,参数务必以实盘账户时间为准。
「经纪商时区偏移的实测切片」
把 Pepperstone 的服务器时间(hTC)和纽约、GMT 做对照,能直接看出夏令时切换带来的时差跳变。2020.10.30 那天 hTC=23 时,hDiff 是 -3,也就是经纪商时间比纽约晚 3 小时;到了 2020.11.06,hDiff 变成 -2,因为美国刚结束夏令时但经纪商还没完全对齐,GMT 侧从 20 点挪到了 21 点。 EUR 和 USD 在 2020.10.23 与 2020.10.30 的切片里,hNY 恒为 16、hGMT 恒为 20、hTC 恒为 23,End-FX after 都是 143h,说明外汇周收盘的计数逻辑在冬令时区内是稳定的。 Pepperstone 给出的三组偏移量更直白:美冬欧冬 = -7200 秒(2 小时),美夏欧夏与美夏欧冬都是 -10800 秒(3 小时)。开 MT5 看自己 broker 的足彩时间,若和这组数字对不上,就说明你的经纪商时区基准跟它不同,回测里按 GMT 切 K 线会偏。 外汇和贵金属杠杆高,时区错配可能让止损挂在错误的日线收盘后,实盘前务必用历史切片验一遍。
Eightcap 经纪商时区偏移的实测记录
把经纪商服务器时间(hTC)对齐到纽约(hNY)和 GMT(hGMT),能直接看出外汇周线切分边界在哪。以 Eightcap Pty Ltd 为例,2021.03.12 美元盘后记录显示 hTC=23、hGMT=21、hNY=16,hDiff=-2,即经纪商时间比 GMT 快 2 小时,换算到纽约时间仍是当天 16:00。 EUR 在 2021.03.26 的快照里 hTC=23、hGMT=20、hDiff=-3,说明欧盟夏令切换后经纪商与 GMT 的偏移拉到 -3 小时;但同日的另一行(22:00 经纪商时间)hDiff 又回到 -2,暴露出切换日当天经纪商并非整点跳变,而是延后到 23:00 才落定新偏移。 该券商公布的固定偏移量为:美冬欧冬 -7200 秒(=-2h)、美夏欧夏 -10800 秒(=-3h)、美夏欧冬 -10800 秒。实战中不能只信公告值,要在 MT5 终端用周五 23:00 前后的报价时间戳自己核对一次。 End-FX after 字段大多落在 143h,少数(如 2020.10.30、2021.03.26 的 22:00 行)为 142h,差出的 1 小时就是时区切换当周的边界摩擦,贵金属与外汇周末停盘的高风险窗口正卡在这里。
◍ 经纪商时区偏移与纽约时间落柱
同一根 UTC 20:00 的 K 线,在 InstaForex 组会被映射成纽约 16:00(hDiff 为 -3 到 -1 小时不等),而在 JFD 组多数时段 hDiff 为 0 或 -1。2021.04.02 23:00 那条记录里,BrokerTime 指向 GMT 20:00、tNY 16:00,End-FX after 标到 143h,说明夏令切换前后 FX 会话结束点会差出 2 小时。 不同券商的 Time Offset 直接决定你看到的纽约时段起点。InstaForex 在 USsumEUsum 组合下偏移 -10800 秒(即 -3h),JFD 同组合只有 -3600 秒;如果你的 EA 按经纪商时间推算美盘开盘,换平台不校正就会错判 2 根小时线。 要把纽约时间写进数据窗口,可用三个缓冲区存原始秒、小时、分钟。下面这段负责按每根 bar 回填纽约时间,核心是先让 time 数组按系列倒序,再借助 MqlDateTime 拆结构。 别把经纪商时间当 GMT 用 接真实账户前,先在策略测试器里打印几家平台的 hDiff,确认偏移表和你代码里的预设一致,外汇和贵金属杠杆品种跳空风险高,时区错配可能让止损挂在错误会话。
class="type">class="kw">double NyRawTimeBuffer[]; class="type">class="kw">double NyHourBuffer[]; class="type">class="kw">double NyMinuteBuffer[]; class="type">void AssignNyTime(const class="type">class="kw">datetime& time[],class="type">int rates_total) { class="type">MqlDateTime dT_struc; class=class="str">"cmt">//--- 为每根蜡烛分配纽约原始时间(秒)、纽约小时、纽约分钟 ArraySetAsSeries(time,true);
「把经纪商时间换算成纽约钟点」
在逐根 K 线回算时,先调用 checkTimeOffset 捕捉夏令时切换点,再把 broker 时间叠上 actOffset 得到 GMT,随后减去 NYShift 与 DST_USD 得到纽约本地时间。这段代码把 tNY 强转成 int 塞进 NyRawTimeBuffer,并用 TimeToStruct 拆出 hour / min 分别落盘,方便后续按美盘钟点过滤信号。 for(int z=0;z<rates_total;z++) { checkTimeOffset(time[z]); // 检查夏令时的变化 datetime tC, tGMT, tNY; tC = time[z]; tGMT = time[z] + OffsetBroker.actOffset; // GMT tNY = tGMT - (NYShift+DST_USD); // 纽约时间 int j = int (tNY); // 将日期时间转换为 int NyRawTimeBuffer[z]=j; TimeToStruct(tNY,dT_struc); NyHourBuffer[z]=dT_struc.hour; NyMinuteBuffer[z]=dT_struc.min; } return; } //--- 查找经纪人偏移量 OffsetBroker.set = false; CheckLoadHistory("EURUSD",PERIOD_H1,TERMINAL_MAXBARS,true); b = CopyTime("EURUSD",PERIOD_H1,BegWk+26*3600,5,arrTme); // 获取欧盟切换前最后 1 小时的时间条 //-----------------------------------------------------如果是指标,则不要求加载其自身数据 if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol) return(true); //+-----------------------------------------------------------------------------------------------------------------------------+ //| 确定给定时间 (tB) 的经纪人偏移量的函数 //+-----------------------------------------------------------------------------------------------------------------------------+ void CDealWithTime::OnTick(void) { string vMethod = __FUNCTION__ + " Line[" + (string)__LINE__ + "] "; // 对 TimeH01[] 进行每小时更新 if(IsNewBarH01()) { // ...BegWk = BoW(tC),原始代码 datetime weekBegin = BoW(TimeCurrent()); // 原始代码为弱开始时间添加了 5 个小节。 // 将 5 天 * 3600 秒(InHour)加到弱开始时间,并不等于 5 个条形图,因为弱开始时间可能没有条形图。 // 周末/节假日。因此,WeekBegin+5*3600 的结果可能是一个时间,而上面没有横杠。 int barShift = iBarShift(mSymbol,mTimeFrame,weekBegin+(5*3600)); 定位 broker 偏移时,代码先用 CheckLoadHistory 强制拉满 EURUSD 的 H1 历史,再用 CopyTime 从 BegWk+26*3600 抓 5 根bar——这对应的是欧盟夏令切回前最后 1 小时窗口。注意若跑在指标里且品种周期自吻合,直接 return true 跳过自身数据加载,避免重复吃资源。 用 iBarShift 找 weekBegin+5*3600 的 bar 位置是个坑:周初可能因周末或假期根本没 bar,5天×3600秒的偏移不等于 5 根实体 K 线。实盘里外汇与贵金属杠杆高、跳空频繁,这类时间映射误差可能让美盘开盘策略错判 1~2 根 bar,建议在 MT5 里打印 barShift 与实际 time[] 比对验证。
for(class="type">int z=class="num">0;z<rates_total;z++) { checkTimeOffset(time[z]); class=class="str">"cmt">// 检查夏令时的变化 class="type">class="kw">datetime tC, tGMT, tNY; tC = time[z]; tGMT = time[z] + OffsetBroker.actOffset; class=class="str">"cmt">// GMT tNY = tGMT - (NYShift+DST_USD); class=class="str">"cmt">// 纽约时间 class="type">int j = class="type">int (tNY); class=class="str">"cmt">// 将日期时间转换为 class="type">int NyRawTimeBuffer[z]=j; TimeToStruct(tNY,dT_struc); NyHourBuffer[z]=dT_struc.hour; NyMinuteBuffer[z]=dT_struc.min; } class="kw">return; } class=class="str">"cmt">//--- 查找经纪人偏移量 OffsetBroker.set = false; CheckLoadHistory("EURUSD",PERIOD_H1,TERMINAL_MAXBARS,true); b = CopyTime("EURUSD",PERIOD_H1,BegWk+class="num">26*class="num">3600,class="num">5,arrTme); class=class="str">"cmt">// 获取欧盟切换前最后 class="num">1 小时的时间条 class=class="str">"cmt">//-----------------------------------------------------如果是指标,则不要求加载其自身数据 if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol) class="kw">return(true); class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------------------------------------+ class=class="str">"cmt">//| 确定给定时间 (tB) 的经纪人偏移量的函数 class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------------------------------------+ class="type">void CDealWithTime::OnTick(class="type">void) { class="type">class="kw">string vMethod = __FUNCTION__ + " Line[" + (class="type">class="kw">string)__LINE__ + "] "; class=class="str">"cmt">// 对 TimeH01[] 进行每小时更新 if(IsNewBarH01()) { class=class="str">"cmt">// ...BegWk = BoW(tC),原始代码 class="type">class="kw">datetime weekBegin = BoW(TimeCurrent()); class=class="str">"cmt">// 原始代码为弱开始时间添加了 class="num">5 个小节。 class=class="str">"cmt">// 将 class="num">5 天 * class="num">3600 秒(InHour)加到弱开始时间,并不等于 class="num">5 个条形图,因为弱开始时间可能没有条形图。 class=class="str">"cmt">// 周末/节假日。因此,WeekBegin+class="num">5*class="num">3600 的结果可能是一个时间,而上面没有横杠。 class="type">int barShift = iBarShift(mSymbol,mTimeFrame,weekBegin+(class="num">5*class="num">3600));
别急着下结论
上面这段收尾逻辑跑在 OnTick() 末尾,核心是解决跨周与 DST 切换时时间轴错位的坑。以 EURUSD H1 实测日志为例:weekBegin 取 2024.01.01 02:00,timeStop 落到 2023.12.31 21:00,time5th 为 2023.12.29 23:00,说明欧美节假与 broker 休市会让条形图时间出现回跳。 CopyTime 如果一次拿不全 bars 根数,就靠 while 循环最多重试 10 次、每次 Sleep(100) 毫秒补数据;attempt>0 才打印诊断,避免刷屏。IsNewBarW01() 触发后还会再跑 setBokerOffset() 最多 10 次,修正券商服务器与本地时间的偏移。 外汇和贵金属杠杆高、跳空频繁,这类时间校正不写稳,周初第一根 K 线算错就可能让策略信号整体偏移。把这段代码直接贴进 EA 的 OnTick 尾段,用 2023 年 12 月那周 EU 切换前的历史数据回放,看 PrintFormat 打出的 weekBegin/timeStop 是否和你 broker 的休市窗对得上。
class="type">class="kw">datetime timeStop = iTime(mSymbol,mTimeFrame,barShift); class="type">int bars = Bars(mSymbol,mTimeFrame,weekBegin,timeStop); ResetLastError(); class="type">int attempt = class="num">0; while(CopyTime(mSymbol,mTimeFrame,weekBegin,timeStop,TimeH01) != bars && attempt <= class="num">10) { Sleep(class="num">100); attempt++; } if(attempt > class="num">0) { PrintFormat("%s: GetLastError[%i] copiedTime %s-%s for [%i] bars weekBegin[%s] to timeStop[%i][%s]",vMethod,GetLastError(),mSymbol,EnumToString(mTimeFrame),bars,TimeToString(weekBegin),barShift,TimeToString(timeStop)); } if(IsNewBarW01()) { checkTimeOffset(TimeCurrent()); class="type">int attempt = class="num">0; class="type">bool isOffset = false; do{ isOffset = setBokerOffset(); attempt++; } while(!isOffset && attempt <= class="num">10); }