4. 时间索引处理:将时间列转为DatetimeIndex、设置时区、排序、处理非交易时间数据

做Tick数据清洗,时间处理是绕不开的坎。我刚开始接触高频数据时,觉得时间列不就是个字符串嘛,转成时间格式不就完了?结果被坑得够呛——时区不对导致数据错位、非交易时间混进来把统计搞崩、排序乱掉让回测结果完全失真。今天咱们就把这块硬骨头啃下来。

4.1 为什么时间索引这么重要?

Tick数据是按毫秒甚至微秒级别记录的。每一笔报价、每一笔成交,都带着精确的时间戳。如果你不把这个时间戳变成索引,后续的切片、重采样、对齐操作会非常痛苦。

说白了,时间索引就是Tick数据的「身份证」。有了它,你才能快速定位某几秒内的行情,才能把不同交易所的数据对齐,才能做各种时间窗口的计算。

核心要点: 永远把时间列设为DataFrame的索引,而不是留在普通列里。这是Tick数据处理的黄金法则。

4.2 将时间列转为DatetimeIndex

假设我们拿到的原始数据长这样:

import pandas as pd

# 模拟原始Tick数据
df = pd.DataFrame({
    'time': ['2024-01-15 09:30:00.123', '2024-01-15 09:30:00.456', '2024-01-15 09:30:01.002'],
    'price': [100.5, 100.52, 100.51],
    'volume': [100, 200, 150]
})

print(df.dtypes)
# time      object
# price    float64
# volume     int64

你看,time列是object类型,也就是字符串。直接用它做时间操作?不行的。

转换方法很简单:

# 转为datetime类型
df['time'] = pd.to_datetime(df['time'])

# 设为索引
df = df.set_index('time')

print(df.index)
# DatetimeIndex(['2024-01-15 09:30:00.123', '2024-01-15 09:30:00.456',
#                '2024-01-15 09:30:01.002'],
#               dtype='datetime64[ns]', name='time', freq=None)

嗯,这里要注意:pd.to_datetime()默认能识别大部分常见格式。但如果你遇到奇葩格式,比如'20240115 09:30:00.123'这种,就需要指定format参数了。

df['time'] = pd.to_datetime(df['time'], format='%Y%m%d %H:%M:%S.%f')

我的习惯: 只要数据量不大,我一般先不指定format,让pandas自动推断。但如果数据量超过几百万行,一定要手动指定format,速度能快10倍以上。

4.3 设置时区

Tick数据最头疼的问题之一就是时区。国内A股用北京时间(Asia/Shanghai),港股用香港时间,美股用美东时间。如果你把不同市场的数据混在一起分析,时区不对就是灾难。

我曾经接过一个项目,客户说他的数据是UTC时间,结果分析时发现跟行情软件对不上。查了半天,原来是数据商给的是「本地时间」但没标注。从那以后,我拿到任何数据第一件事就是确认时区。

# 假设数据是UTC时间,需要转为北京时间
df.index = df.index.tz_localize('UTC').tz_convert('Asia/Shanghai')

print(df.index)
# DatetimeIndex(['2024-01-15 17:30:00.123+08:00', 
#                '2024-01-15 17:30:00.456+08:00',
#                '2024-01-15 17:30:01.002+08:00'],
#               dtype='datetime64[ns, Asia/Shanghai]', name='time')

注意顺序:先tz_localize声明当前时区,再tz_convert转换到目标时区。如果你直接tz_convert,pandas会报错,因为它不知道你当前是什么时区。

避坑指南: 我曾经犯过一个错——数据明明是北京时间,我直接tz_localize('UTC'),然后tz_convert('Asia/Shanghai')。结果所有时间都偏移了8小时。正确的做法是:先确认数据原始时区,再tz_localize那个时区,最后tz_convert

4.4 排序

Tick数据是按时间顺序产生的,但实际拿到的数据不一定有序。比如从多个数据源合并、或者数据存储时出了点问题,都可能导致时间乱序。

排序很简单:

# 按时间升序排列
df = df.sort_index()

# 检查是否还有乱序
print(df.index.is_monotonic_increasing)
# True

为什么一定要排序?因为很多时间序列操作(比如asfreqresample)都要求索引是单调递增的。不排序的话,这些操作要么报错,要么给出错误结果。

小技巧: 排序后建议检查一下是否有重复的时间戳。如果有,说明同一毫秒内有多笔数据,需要根据业务逻辑决定是保留全部、取平均还是去重。

# 检查重复时间戳
duplicated = df.index.duplicated(keep='first')
print(f'重复时间戳数量: {duplicated.sum()}')

# 如果有重复,可以这样处理
df = df[~duplicated]  # 保留第一条
# 或者
df = df.groupby(df.index).agg({'price': 'mean', 'volume': 'sum'})  # 聚合

4.5 处理非交易时间数据

这是Tick数据清洗中最容易被忽视的一环。你以为拿到的是交易时段的数据?不一定。很多数据商会在盘前、盘中、盘后都推送数据,甚至周末也有测试数据。

以A股为例,交易时间是上午9:30-11:30,下午13:00-15:00。其他时间的数据要么是集合竞价,要么是测试数据,要么是垃圾数据。

# 定义交易时间范围
trading_start = pd.Timestamp('09:30:00').time()
trading_mid_end = pd.Timestamp('11:30:00').time()
trading_after_start = pd.Timestamp('13:00:00').time()
trading_end = pd.Timestamp('15:00:00').time()

# 筛选交易时间数据
def is_trading_time(t):
    time_part = t.time()
    if trading_start <= time_part <= trading_mid_end:
        return True
    if trading_after_start <= time_part <= trading_end:
        return True
    return False

df_trading = df[df.index.map(is_trading_time)]

你想想看,如果不做这个过滤,你计算日内波动率时会把盘前的零碎数据也算进去,结果完全失真。

我的经验: 对于期货市场,还要考虑夜盘。比如螺纹钢有21:00-23:00的夜盘,第二天9:00-15:00的日盘。这时候需要把夜盘数据算作下一个交易日的开始。我一般会写一个自定义的交易日历函数来处理。

4.6 完整流程与知识体系

下面这张图总结了时间索引处理的完整流程,我建议你保存下来,每次处理Tick数据时对照着走一遍:

Tick数据时间索引处理流程 原始Tick数据 pd.to_datetime() + set_index() 转为DatetimeIndex tz_localize() + tz_convert() 设置/转换时区 sort_index() 按时间排序 + 去重 自定义时间过滤函数 剔除盘前/盘中/盘后/周末数据 常见坑点 • 时区声明顺序错误 • 未处理重复时间戳 • 忽略集合竞价数据 • 夜盘日期归属错误 • 未检查单调递增性 • 大文件未指定format 输出结果 干净的 DatetimeIndex 可直接用于分析

4.7 实战中的注意事项

最后,我总结几个实战中容易踩的坑:

  • 时区缩写别乱用: 别写'CST',它既可以是美国中部时间,也可以是中国标准时间,还可以是古巴标准时间。老老实实用'Asia/Shanghai'、'US/Eastern'这种完整名称。
  • 夏令时问题: 美股有夏令时,每年3月和11月切换。如果你用pytz库,它会自动处理。但如果你手动加减小时数,一定会出错。
  • 非交易时间的定义: 不同品种不一样。股指期货有早盘15:15-15:30的集合竞价,商品期货有夜盘,债券交易时间也不同。一定要确认清楚。
  • 性能问题: 如果数据量上千万行,df.index.map(is_trading_time)这种逐行操作会很慢。我建议用between_time方法,或者先筛选日期再筛选时间。

我曾经踩过的坑: 有一次处理美股Tick数据,我直接用between_time('09:30', '16:00')来过滤交易时间。结果发现数据少了整整一个小时。后来才意识到,美股下午4点收盘,但最后几秒的订单可能在4:00:00之后才被记录。正确的做法是留一点缓冲,比如between_time('09:30', '16:00:05')

时间索引处理看起来简单,但细节决定成败。把这一步做扎实了,后面的特征工程才能建立在可靠的基础上。记住:脏数据进去,垃圾结果出来。时间索引就是你的第一道防线。