第四节:时间序列索引处理
做期权高频做市,时间就是一切。
我刚开始接触期权数据时,最头疼的就是时间列。交易所给的数据,时间格式五花八门。有的带时区,有的不带。有的精确到毫秒,有的只到秒。你想想看,高频交易里,一毫秒的偏差可能就让你错过一笔好单子。
这一节,我们就来搞定时间序列索引。说白了,就是把时间列变成真正的「时间」,让Python能听懂它。
4.1 把时间列转为DatetimeIndex
先看一个最常见的场景。你从数据商那里拿到一份期权行情数据,长这样:
import pandas as pd
# 模拟原始数据
data = {
'time': ['2024-01-15 09:30:00.123', '2024-01-15 09:30:00.456', '2024-01-15 09:30:01.789'],
'symbol': ['510050.SH', '510050.SH', '510050.SH'],
'bid_price': [2.850, 2.851, 2.852],
'ask_price': [2.852, 2.853, 2.854]
}
df = pd.DataFrame(data)
print(df.dtypes)
# time列是object类型,不是时间类型
嗯,这里要注意。Pandas默认把时间列读成字符串(object类型)。你不能直接用它做时间运算,也不能按时间切片。
解决办法很简单:
# 转为DatetimeIndex
df['time'] = pd.to_datetime(df['time'])
df = df.set_index('time')
print(df.index)
# DatetimeIndex(['2024-01-15 09:30:00.123', ...], dtype='datetime64[ns]', ...)
我个人习惯用pd.to_datetime(),它很智能。大部分常见格式都能自动识别。但如果你遇到奇葩格式,比如「2024/01/15 09:30:00.123」,记得加个参数:
df['time'] = pd.to_datetime(df['time'], format='%Y/%m/%d %H:%M:%S.%f')
指定格式能提升解析速度。我在处理百万级数据时,这个优化能省下好几秒。
4.2 处理时区问题
做期权做市,你可能会同时看多个市场。上交所、深交所、中金所,它们都在北京时间。但如果你接入的是国际数据源,比如CBOE(芝加哥期权交易所),时区问题就来了。
我曾经踩过一个坑。有一次,我把美国的数据和国内的数据直接合并,没处理时区。结果回测时发现,策略在半夜「神奇地」开仓了。排查了半天,原来是时区没对齐。
处理时区,我推荐两步走:
- 先本地化:告诉Pandas这个时间属于哪个时区
- 再转换:转到目标时区
# 假设数据是UTC时间
df_utc = df.copy()
df_utc.index = df_utc.index.tz_localize('UTC')
print(df_utc.index)
# DatetimeIndex(['2024-01-15 09:30:00.123+00:00', ...], dtype='datetime64[ns, UTC]', ...)
# 转到北京时间(UTC+8)
df_beijing = df_utc.copy()
df_beijing.index = df_beijing.index.tz_convert('Asia/Shanghai')
print(df_beijing.index)
# DatetimeIndex(['2024-01-15 17:30:00.123+08:00', ...], dtype='datetime64[ns, Asia/Shanghai]', ...)
tz_localize,会报错。先用df.index.tz检查一下是否已有时区。
常用的时区字符串:
'Asia/Shanghai'— 北京时间'UTC'— 协调世界时'America/Chicago'— 芝加哥时间(CBOE所在地)'America/New_York'— 纽约时间
4.3 重采样与频率对齐
高频数据通常是毫秒级甚至微秒级的。但做策略时,你可能需要按秒、按分钟来聚合。这就是重采样(resample)的用武之地。
举个例子。你有一笔逐笔成交数据,想算每秒的平均买卖价差:
# 模拟高频数据
import numpy as np
np.random.seed(42)
time_index = pd.date_range('2024-01-15 09:30:00', periods=10000, freq='5ms')
df_highfreq = pd.DataFrame({
'bid': 2.850 + np.random.normal(0, 0.001, 10000),
'ask': 2.852 + np.random.normal(0, 0.001, 10000)
}, index=time_index)
# 重采样到1秒,计算平均价差
df_1s = df_highfreq.resample('1S').agg({
'bid': 'mean',
'ask': 'mean'
})
df_1s['spread'] = df_1s['ask'] - df_1s['bid']
print(df_1s.head())
重采样的核心参数是频率字符串:
| 频率 | 字符串 | 说明 |
|---|---|---|
| 毫秒 | '1ms', '100ms' |
1毫秒、100毫秒 |
| 秒 | '1S', '30S' |
1秒、30秒 |
| 分钟 | '1T' 或 '1min' |
1分钟 |
| 小时 | '1H' |
1小时 |
| 交易日 | '1B' |
1个交易日(跳过周末) |
重采样时,你还可以用不同的聚合函数:
# 每秒的OHLC(开盘、最高、最低、收盘)
df_ohlc = df_highfreq['bid'].resample('1S').ohlc()
print(df_ohlc.head())
fillna()或interpolate()填充。我个人倾向于用前向填充(ffill),因为做市时,最新的报价就是最有效的。
4.4 频率对齐:让不同数据源「同步」
做市策略往往需要多个数据源。比如,期权行情和标的物行情。它们的更新频率可能不一样。期权可能每10毫秒更新一次,标的物可能每100毫秒更新一次。
这时候,你需要做频率对齐。说白了,就是把两个不同频率的时间序列,对齐到同一个时间轴上。
# 模拟两个不同频率的数据
time_opt = pd.date_range('2024-01-15 09:30:00', periods=1000, freq='10ms')
time_underlying = pd.date_range('2024-01-15 09:30:00', periods=100, freq='100ms')
df_opt = pd.DataFrame({'opt_price': 2.850 + np.random.normal(0, 0.01, 1000)}, index=time_opt)
df_underlying = pd.DataFrame({'underlying_price': 2.800 + np.random.normal(0, 0.005, 100)}, index=time_underlying)
# 对齐到100ms频率,用前向填充
df_aligned = df_opt.resample('100ms').ffill().join(df_underlying, how='outer')
print(df_aligned.head())
这里的关键是how='outer'。它保证两个数据源的时间点都保留。如果某个时间点只有期权数据,标的物价格就用前一个值填充。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 时间戳精度丢失:Pandas默认用纳秒(ns)存储。如果你的数据是微秒(us)或毫秒(ms),转成DatetimeIndex后精度不会丢。但如果你用
to_csv再读回来,记得指定date_format,否则可能被截断。 - 时区转换后排序:转换时区后,时间顺序可能变。比如UTC的23:00转成北京时间就是第二天的07:00。记得重新
sort_index()。 - 重采样边界:默认情况下,重采样以「左闭右开」的方式分组。比如
resample('1S'),09:30:00.000到09:30:00.999的数据归到09:30:00这一组。如果你想要不同的边界,用label='right'或closed='left'调整。
嗯,时间序列处理就讲到这里。这些技巧,我在实盘系统里天天用。你把它练熟了,处理高频数据就会顺手很多。