第四节:时间序列索引处理

做期权高频做市,时间就是一切。

我刚开始接触期权数据时,最头疼的就是时间列。交易所给的数据,时间格式五花八门。有的带时区,有的不带。有的精确到毫秒,有的只到秒。你想想看,高频交易里,一毫秒的偏差可能就让你错过一笔好单子。

这一节,我们就来搞定时间序列索引。说白了,就是把时间列变成真正的「时间」,让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(芝加哥期权交易所),时区问题就来了。

我曾经踩过一个坑。有一次,我把美国的数据和国内的数据直接合并,没处理时区。结果回测时发现,策略在半夜「神奇地」开仓了。排查了半天,原来是时区没对齐。

处理时区,我推荐两步走:

  1. 先本地化:告诉Pandas这个时间属于哪个时区
  2. 再转换:转到目标时区
# 假设数据是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'。它保证两个数据源的时间点都保留。如果某个时间点只有期权数据,标的物价格就用前一个值填充。

核心原则:高频数据向低频数据对齐时,用前向填充(ffill)。低频数据向高频数据对齐时,用插值(interpolate)或填充。做市场景下,永远不要用未来数据填充过去。

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'调整。

嗯,时间序列处理就讲到这里。这些技巧,我在实盘系统里天天用。你把它练熟了,处理高频数据就会顺手很多。


专注资料整理