时间序列基础:交易数据的时序特性

交易数据天然具有时间顺序,每一笔成交、每一次报价、每一根K线都严格按时间轴排列。这种时序特性决定了我们不能像处理静态表格那样随机切分训练集,而必须保留时间顺序,避免未来信息泄露。本章从时间戳、频率、时区三个核心维度入手,结合pandas基础操作,为后续训练集切分打下坚实的数据基础。

2.1 交易数据的时序特性

交易数据与普通结构化数据最本质的区别在于:时间顺序不可逆。一旦打乱时间顺序,价格序列中的自相关性、趋势、周期、波动聚集等特征将全部失效。

特性 说明 对训练集切分的影响
有序性 数据点按时间先后排列,后一个价格依赖前一个 切分必须保持时间顺序,禁止随机打乱
等/不等间隔 Tick数据不等间隔,分钟K线等间隔 切分窗口需对齐实际时间,而非固定行数
自相关性 当前价格与历史价格存在统计依赖 训练集与测试集之间需留出时间间隔,避免信息泄露
非平稳性 统计特性(均值、方差)随时间变化 切分需考虑市场状态切换,避免跨状态训练
时区依赖 不同交易所的开盘/收盘时间不同 跨市场数据需统一时区后再切分
核心原则:交易风控训练集切分的第一条铁律——绝不使用未来数据。任何切分方案都必须保证训练集的时间窗口完全在测试集之前。

2.2 时间戳处理

交易数据中的时间戳通常以字符串或整数形式存储,需要先转换为pandas的DatetimeIndex才能进行高效操作。

2.2.1 常见时间戳格式

格式类型 示例 说明
ISO 8601 字符串 2025-01-15 09:30:00 标准可读格式,含日期和时间
Unix 时间戳(秒) 1736915400 从1970-01-01开始的秒数
Unix 时间戳(毫秒) 1736915400000 高频交易常用,精度到毫秒
自定义格式 20250115_093000 某些数据源的非标准格式

2.2.2 时间戳转换实战

import pandas as pd

# 示例:原始交易数据
data = {
    'timestamp': ['2025-01-15 09:30:00', '2025-01-15 09:30:01', '2025-01-15 09:30:02'],
    'price': [100.5, 100.52, 100.48],
    'volume': [100, 200, 150]
}
df = pd.DataFrame(data)

# 将字符串列转换为DatetimeIndex
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.set_index('timestamp')

print(df)
#                      price  volume
# timestamp                         
# 2025-01-15 09:30:00  100.50     100
# 2025-01-15 09:30:01  100.52     200
# 2025-01-15 09:30:02  100.48     150

# 处理Unix毫秒时间戳
unix_ms = [1736915400000, 1736915401000, 1736915402000]
df_unix = pd.DataFrame({'price': [100.5, 100.52, 100.48]},
                       index=pd.to_datetime(unix_ms, unit='ms'))
print(df_unix)
#                          price
# 2025-01-15 09:30:00  100.50
# 2025-01-15 09:30:01  100.52
# 2025-01-15 09:30:02  100.48
常见陷阱:Unix时间戳的单位(秒/毫秒/微秒)容易混淆。务必确认数据源文档,使用正确的unit参数。单位错误会导致时间偏移数十年。

2.3 频率与重采样

交易数据频率决定了时间序列的粒度。Tick数据(逐笔成交)频率最高,分钟K线次之,日线最低。在训练集切分时,需要根据策略类型选择合适的频率。

2.3.1 常见交易数据频率

频率 pandas别名 典型用途
Tick(逐笔) 无固定别名 高频做市、订单簿分析
1分钟 1T 或 1min 日内趋势跟踪
5分钟 5T 或 5min 日内波段交易
1小时 1H 短线策略
1天 1D 中长线策略、风控回测
1周 1W 组合风控、宏观对冲

2.3.2 重采样:从高频到低频

重采样是将高频数据聚合为低频数据的关键操作,在训练集切分前经常需要统一数据频率。

# 生成1秒频率的模拟Tick数据
tick_index = pd.date_range('2025-01-15 09:30:00', periods=1000, freq='1s')
tick_data = pd.DataFrame({
    'price': 100 + pd.np.random.randn(1000).cumsum() * 0.01,
    'volume': pd.np.random.randint(1, 100, 1000)
}, index=tick_index)

# 重采样为1分钟K线
minute_data = tick_data.resample('1T').agg({
    'price': 'ohlc',      # Open, High, Low, Close
    'volume': 'sum'
})

# 展开多级列索引
minute_data.columns = ['open', 'high', 'low', 'close', 'volume']
print(minute_data.head())
#                      open    high     low   close  volume
# 2025-01-15 09:30:00  100.00  100.03  99.97  100.01    4567
# 2025-01-15 09:31:00  100.01  100.05  99.98  100.03    5123
# 2025-01-15 09:32:00  100.03  100.06  99.99  100.04    4890
重采样注意事项:
  • OHLC(开盘价、最高价、最低价、收盘价)是K线数据的标准聚合方式
  • 成交量使用sum聚合,而非均值
  • 重采样会引入前向填充或后向填充问题,需根据策略选择
  • 训练集切分前,确保所有数据频率一致,避免时间轴错位

2.4 时区处理

全球交易市场分布在不同的时区,跨市场策略必须统一时区后才能进行训练集切分。pandas的时区处理功能强大但容易出错。

2.4.1 时区设置与转换

# 创建无时区的时间序列
naive_idx = pd.date_range('2025-01-15 09:30:00', periods=5, freq='1min')

# 本地化:为无时区时间指定时区
ny_idx = naive_idx.tz_localize('America/New_York')
print('纽约时间:', ny_idx)
# DatetimeIndex(['2025-01-15 09:30:00-05:00', ...], freq='1T')

# 转换时区:从纽约时间转为北京时间
bj_idx = ny_idx.tz_convert('Asia/Shanghai')
print('北京时间:', bj_idx)
# DatetimeIndex(['2025-01-15 22:30:00+08:00', ...], freq='1T')

# 实际应用:合并纽交所和上交所数据时统一时区
ny_data = pd.DataFrame({'price': [100, 101, 102]}, index=ny_idx)
sh_data = pd.DataFrame({'price': [98, 99, 100]}, index=bj_idx)

# 统一为UTC时间后再合并
ny_utc = ny_data.tz_convert('UTC')
sh_utc = sh_data.tz_convert('UTC')
combined = pd.concat([ny_utc, sh_utc]).sort_index()
print(combined)
#                            price
# 2025-01-15 14:30:00+00:00    100
# 2025-01-15 14:30:00+00:00     98
# 2025-01-15 14:31:00+00:00    101
# 2025-01-15 14:31:00+00:00     99
# 2025-01-15 14:32:00+00:00    102
# 2025-01-15 14:32:00+00:00    100
时区处理铁律:
  1. 先本地化,再转换:无时区时间必须先tz_localize指定原始时区,才能tz_convert到目标时区
  2. 统一存储为UTC:数据库和中间文件建议统一使用UTC时间,展示时再转换为本地时间
  3. 注意夏令时:美国、欧洲等市场有夏令时切换,pandas会自动处理,但需确认数据源是否已调整

2.5 pandas时间序列基础操作

掌握以下基础操作,是进行训练集切分的前提。这些操作覆盖了时间索引的筛选、偏移、滑动窗口等核心功能。

2.5.1 时间索引筛选

# 创建日频数据
dates = pd.date_range('2025-01-01', '2025-03-31', freq='D')
df_daily = pd.DataFrame({'price': 100 + pd.np.random.randn(len(dates)).cumsum() * 0.5},
                        index=dates)

# 按年份筛选
df_2025 = df_daily['2025']

# 按月份筛选
df_jan = df_daily['2025-01']

# 按日期范围筛选
df_range = df_daily['2025-01-15':'2025-02-15']

# 按时间点精确筛选
df_point = df_daily.loc['2025-02-01']

# 按时间属性筛选(使用indexer)
df_monday = df_daily[df_daily.index.dayofweek == 0]  # 周一
df_month_end = df_daily[df_daily.index.is_month_end]  # 月末

2.5.2 时间偏移与滑动窗口

# 时间偏移:用于构建训练集和测试集的时间边界
train_end = pd.Timestamp('2025-02-28')
test_start = train_end + pd.DateOffset(days=1)  # 留出1天间隔
test_end = test_start + pd.DateOffset(months=1)

print(f'训练集截止: {train_end.date()}')
print(f'测试集起始: {test_start.date()}')
print(f'测试集截止: {test_end.date()}')

# 滑动窗口:用于滚动训练
# 固定窗口大小(60天),每次滑动30天
window = 60
step = 30
for i in range(0, len(df_daily) - window, step):
    train = df_daily.iloc[i:i+window]
    test_start_idx = i + window
    test_end_idx = min(test_start_idx + step, len(df_daily))
    test = df_daily.iloc[test_start_idx:test_end_idx]
    print(f'训练: {train.index[0].date()} ~ {train.index[-1].date()}, '
          f'测试: {test.index[0].date()} ~ {test.index[-1].date()}')

2.5.3 时间序列基础操作总结

操作类别 方法/属性 说明
创建 pd.date_range() 生成等间隔时间序列
转换 pd.to_datetime() 将字符串/整数转为时间戳
索引 df['2025-01'] 按年月日字符串切片
属性 .year, .month, .day, .dayofweek 提取时间成分
偏移 pd.DateOffset(), pd.Timedelta() 时间加减运算
重采样 .resample() 改变时间频率
时区 .tz_localize(), .tz_convert() 时区设置与转换
滑动 .rolling(), .expanding() 滑动窗口计算
移位 .shift() 时间序列滞后/超前

2.6 时序特性与训练集切分的关系

下图展示了交易数据从原始时间序列到训练集/测试集切分的完整流程,重点体现时间顺序保留、频率对齐、时区统一三个关键步骤。

交易数据时序处理与训练集切分流程 原始交易数据 Tick / 分钟K / 日线 时间戳标准化 → DatetimeIndex 频率统一 重采样 / 对齐 时区统一 → UTC / 目标时区 训练集切分 时间窗口 / 滚动 切分前必须完成的三步预处理 ① 时间戳标准化:所有数据统一为 pandas DatetimeIndex,支持高效切片和偏移 ② 频率统一:不同来源的数据重采样到相同频率,避免时间轴错位 ③ 时区统一:跨市场数据统一为 UTC 或同一目标时区,消除时差干扰 可用的训练集 / 测试集 时间顺序保留 · 无未来信息泄露

2.7 本章小结

  • 时序特性是交易数据区别于静态数据的根本特征,训练集切分必须严格保留时间顺序
  • 时间戳处理是第一步,使用pd.to_datetime()将原始时间转换为DatetimeIndex
  • 频率管理通过resample()实现,确保所有数据在相同时间粒度上对齐
  • 时区统一使用tz_localize()和tz_convert(),跨市场数据必须统一时区
  • 基础操作包括时间索引筛选、偏移、滑动窗口,是后续切分算法的基石