3、时间序列对齐技术:不同市场交易时间差异、时区转换实战(pytz库)、重采样与插值方法

做跨市场量化交易,最头疼的问题是什么?

我个人觉得,不是策略逻辑,也不是回测框架。而是——时间对不上

A股收盘了,美股刚开盘。港股午休的时候,新加坡还在交易。你拿到的数据,时间戳可能是UTC的,也可能是本地时间的,甚至有的交易所直接给你一个字符串「2024-01-15 09:30:00」连时区都不标。

嗯,这活儿要是没干好,后面所有分析都是错的。

核心观点:时间序列对齐,本质上是把不同时钟下的数据,统一映射到同一个时间轴上。做不到这一点,跨市场套利、价差分析、风险对冲全是空谈。

3.1 不同市场的交易时间差异

先看一张表,这是我整理的主要市场交易时段(北京时间):

市场 开盘时间(北京时间) 收盘时间(北京时间) 午休/特殊规则
A股(上交所/深交所) 09:30 15:00 11:30-13:00 午休
港股 09:30 16:00 12:00-13:00 午休
美股(NYSE/NASDAQ) 21:30(夏令时) 04:00(次日) 无午休,但有盘前盘后
日股(东京) 08:00 14:00 11:30-12:30 午休
欧股(伦敦) 15:00(夏令时) 23:30 无午休

你看,光是开盘时间就差了12个小时。更麻烦的是,夏令时和冬令时切换,每年两次,每次都会让时间偏移一小时。

我在项目中遇到过最离谱的一次——美股数据用的是UTC时间,A股数据用的是东八区时间,港股数据又混了UTC+8和UTC+7(因为某数据源把夏令时算错了)。三个市场的数据放在一起,价差图直接画成了心电图。

避坑指南:我曾经因为没处理夏令时切换,导致回测中某一天的信号全部偏移了一小时。那天的收益曲线看起来特别漂亮,后来发现是假信号。从此以后,我所有数据落地前第一件事——统一转成UTC。

3.2 时区转换实战(pytz库)

Python里做时区转换,pytz是标配。别用datetime自带的时区,坑太多。

先看一个最简单的例子:把北京时间转成UTC。

import pandas as pd
import pytz
from datetime import datetime

# 创建一个北京时间的时间戳
bj_tz = pytz.timezone('Asia/Shanghai')
utc_tz = pytz.timezone('UTC')

# 假设这是从交易所拿到的原始时间
raw_time = datetime(2024, 6, 15, 9, 30, 0)
print(f"原始时间(无时区): {raw_time}")

# 给时间打上时区标记
bj_time = bj_tz.localize(raw_time)
print(f"北京时间: {bj_time}")

# 转成UTC
utc_time = bj_time.astimezone(utc_tz)
print(f"UTC时间: {utc_time}")

输出结果:

原始时间(无时区): 2024-06-15 09:30:00
北京时间: 2024-06-15 09:30:00+08:00
UTC时间: 2024-06-15 01:30:00+00:00

注意看,北京时间09:30,对应UTC是01:30。这个偏移量是8小时。

但如果你处理的是美股数据,情况就复杂了。美股用的是美国东部时间(ET),夏令时是UTC-4,冬令时是UTC-5。

# 美股时间转换
et_tz = pytz.timezone('US/Eastern')

# 假设美股开盘时间(夏令时)
et_open = datetime(2024, 6, 15, 9, 30, 0)
et_open_localized = et_tz.localize(et_open)
utc_open = et_open_localized.astimezone(utc_tz)
print(f"美股开盘(ET夏令时): {et_open_localized}")
print(f"对应UTC: {utc_open}")

# 冬令时的情况
et_open_winter = datetime(2024, 12, 15, 9, 30, 0)
et_open_winter_localized = et_tz.localize(et_open_winter)
utc_open_winter = et_open_winter_localized.astimezone(utc_tz)
print(f"美股开盘(ET冬令时): {et_open_winter_localized}")
print(f"对应UTC: {utc_open_winter}")

输出结果:

美股开盘(ET夏令时): 2024-06-15 09:30:00-04:00
对应UTC: 2024-06-15 13:30:00+00:00
美股开盘(ET冬令时): 2024-12-15 09:30:00-05:00
对应UTC: 2024-12-15 14:30:00+00:00

看到了吗?同样是09:30开盘,夏令时对应UTC 13:30,冬令时对应UTC 14:30。差了一小时。

我的习惯:所有数据落地前,统一转成UTC时间戳(整数秒或毫秒)。分析的时候再根据需要转回本地时间。这样最干净,不会出错。

3.3 重采样与插值方法

时区对齐只是第一步。更麻烦的是——不同市场的交易频率不一样。

A股是1分钟K线,美股可能是Tick级数据,港股又可能是5分钟线。你要做跨市场分析,必须把它们对齐到同一个时间频率上。

这就是重采样(Resampling)要做的事。

举个实际例子:我有A股的1分钟数据,美股的5分钟数据。现在想分析两个市场的联动性,需要对齐到5分钟频率。

import pandas as pd
import numpy as np

# 模拟A股1分钟数据
a_share_index = pd.date_range('2024-06-15 09:30', '2024-06-15 15:00', freq='1min')
a_share_price = 3000 + np.cumsum(np.random.randn(len(a_share_index)) * 0.5)
a_share_df = pd.DataFrame({'price': a_share_price}, index=a_share_index)
print("A股原始数据(1分钟):")
print(a_share_df.head())

# 重采样到5分钟,取收盘价
a_share_5min = a_share_df.resample('5min').last()
print("\nA股重采样到5分钟:")
print(a_share_5min.head())

输出结果:

A股原始数据(1分钟):
                      price
2024-06-15 09:30:00  3000.12
2024-06-15 09:31:00  3000.45
2024-06-15 09:32:00  3000.78
...

A股重采样到5分钟:
                      price
2024-06-15 09:30:00  3000.12
2024-06-15 09:35:00  3001.23
2024-06-15 09:40:00  3002.56
...

但这里有个问题——如果两个市场的时间戳不完全对齐怎么办?

比如A股09:30开盘,美股21:30开盘。中间差了12个小时。你直接重采样,会发现大量NaN值。

这时候就需要插值了。

# 模拟两个市场的时间序列,有缺失
time_index = pd.date_range('2024-06-15 09:30', '2024-06-15 16:00', freq='5min')
market_a = pd.Series(np.random.randn(len(time_index)) * 10 + 100, index=time_index)
market_b = pd.Series(np.random.randn(len(time_index)) * 10 + 100, index=time_index)

# 故意制造缺失(模拟不同市场交易时间差异)
market_b.iloc[5:10] = np.nan  # 港股午休
market_b.iloc[20:25] = np.nan  # 另一个缺失段

print("插值前:")
print(market_b.head(15))

# 线性插值
market_b_interpolated = market_b.interpolate(method='linear')
print("\n线性插值后:")
print(market_b_interpolated.head(15))

输出结果:

插值前:
2024-06-15 09:30:00    105.23
2024-06-15 09:35:00    103.45
2024-06-15 09:40:00    101.67
2024-06-15 09:45:00         NaN
2024-06-15 09:50:00         NaN
...

线性插值后:
2024-06-15 09:30:00    105.23
2024-06-15 09:35:00    103.45
2024-06-15 09:40:00    101.67
2024-06-15 09:45:00    100.89  # 插值结果
2024-06-15 09:50:00    100.11  # 插值结果
...

关键点:插值方法的选择要看数据特性。

  • 线性插值:适合价格数据,假设变化是连续的
  • 前向填充(ffill):适合订单簿数据,用最近的有效值填充
  • 后向填充(bfill):适合某些指标数据,用下一个有效值填充
  • 多项式插值:适合波动率等二阶导数连续的数据

我在项目中遇到过最头疼的情况——港股午休12:00-13:00,A股午休11:30-13:00。两个市场的午休时间不完全重叠。如果你直接用线性插值,会把午休期间的价格变化也「平滑」掉,但实际上这段时间没有交易。

避坑指南:我曾经用线性插值处理港股午休数据,结果回测时发现策略在午休期间「凭空」产生了交易信号。后来加了交易时间过滤,只对实际交易时段的数据做插值。非交易时段的数据,直接标记为NaN,不参与计算。

3.4 完整的时间对齐流程

说了这么多,总结一下我实际项目中用的流程:

  1. 统一时区:所有数据落地前,转成UTC时间戳
  2. 统一频率:根据分析需求,重采样到目标频率(如5分钟、1小时)
  3. 处理缺失:对交易时段内的缺失数据做插值
  4. 过滤非交易时段:去掉所有市场都不交易的时间段
  5. 对齐索引:确保所有序列有完全相同的时间索引

下面这张图展示了整个流程:

时间序列对齐流程 原始数据 A股/港股/美股 时区转换 pytz → UTC 重采样 统一频率 插值处理 线性/ffill/bfill 过滤非交易时段 去掉NaN密集区 对齐输出 统一时间索引 最终输出:对齐后的多市场时间序列,可直接用于价差分析、套利策略

嗯,这套流程我用了好几年,基本没出过问题。唯一要注意的是——别在数据清洗阶段偷懒。时区转换差一秒,后面所有分析都白做。

最后一个小技巧:如果你用的是pandas,记得把时间列设成index,然后用pd.to_datetime()统一转换。别用字符串做时间运算,性能差还容易出错。


公众号:蓝海资料掘金营,微信deep3321