时间序列基础:交易数据的时序特性
交易数据天然具有时间顺序,每一笔成交、每一次报价、每一根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
时区处理铁律:
- 先本地化,再转换:无时区时间必须先
tz_localize指定原始时区,才能tz_convert到目标时区 - 统一存储为UTC:数据库和中间文件建议统一使用UTC时间,展示时再转换为本地时间
- 注意夏令时:美国、欧洲等市场有夏令时切换,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 时序特性与训练集切分的关系
下图展示了交易数据从原始时间序列到训练集/测试集切分的完整流程,重点体现时间顺序保留、频率对齐、时区统一三个关键步骤。
2.7 本章小结
- 时序特性是交易数据区别于静态数据的根本特征,训练集切分必须严格保留时间顺序
- 时间戳处理是第一步,使用
pd.to_datetime()将原始时间转换为DatetimeIndex - 频率管理通过
resample()实现,确保所有数据在相同时间粒度上对齐 - 时区统一使用
tz_localize()和tz_convert(),跨市场数据必须统一时区 - 基础操作包括时间索引筛选、偏移、滑动窗口,是后续切分算法的基石