时间序列基础:时间戳对齐、重采样、滚动窗口计算

做期权做市,说白了就是跟时间赛跑。你拿到的行情数据,每一笔都有个时间戳。但问题来了——不同交易所、不同数据源的时间戳,往往不是对齐的。我刚开始做这个的时候,就吃过这个亏。

举个例子。你同时收到上证50ETF的行情和它的期权行情。ETF的tick可能每100ms来一次,期权那边可能每50ms就来一次。你直接拿这两个序列做计算?那结果肯定乱套。所以,时间戳对齐是第一步。

时间戳对齐:把不同频率的数据拉到同一张桌子上

对齐的方式其实不复杂。我常用的方法是「向前填充」——用最近的一个有效值,去填补空缺的时间点。你想想看,做市商报价的时候,价格变化没那么频繁,用前一个值顶一下,完全说得通。

核心思路: 选定一个基准时间轴,把其他序列的数据「贴」上去。

具体怎么做?我习惯用pandas的reindex方法。先建一个完整的时间索引,然后让数据对齐到这个索引上。

import pandas as pd
import numpy as np

# 假设这是ETF的行情数据
etf_data = pd.DataFrame({
    'price': [3.200, 3.205, 3.210],
    'volume': [1000, 1500, 1200]
}, index=pd.to_datetime(['2024-01-15 09:30:00.000',
                         '2024-01-15 09:30:00.100',
                         '2024-01-15 09:30:00.200']))

# 这是期权行情数据
option_data = pd.DataFrame({
    'bid': [0.050, 0.052, 0.051, 0.053],
    'ask': [0.055, 0.057, 0.056, 0.058]
}, index=pd.to_datetime(['2024-01-15 09:30:00.000',
                         '2024-01-15 09:30:00.050',
                         '2024-01-15 09:30:00.150',
                         '2024-01-15 09:30:00.200']))

# 建立统一的毫秒级时间轴
base_index = pd.date_range(
    start='2024-01-15 09:30:00.000',
    end='2024-01-15 09:30:00.200',
    freq='50ms'
)

# 对齐ETF数据
etf_aligned = etf_data.reindex(base_index, method='ffill')
# 对齐期权数据
option_aligned = option_data.reindex(base_index, method='ffill')

print(etf_aligned)
print(option_aligned)

我的经验: 对齐的时候,记得检查一下时间戳的精度。有的数据源给的是毫秒,有的是微秒。差一个数量级,结果就完全不一样了。

重采样:把高频数据降下来,或者把低频数据提上去

对齐之后,你可能还需要统一数据的频率。比如,把所有的tick数据都变成1秒一根的K线。这就是重采样。

重采样有两种方向:

  • 降采样: 从高频到低频。比如从tick到1秒K线。常用操作有取均值、取最后值、取OHLC。
  • 升采样: 从低频到高频。比如从1秒到100ms。常用操作是插值或向前填充。

我个人做期权做市的时候,最常用的是降采样到100ms。为什么?因为100ms刚好能捕捉到大部分的价格波动,又不会产生太多噪声。

# 把tick数据重采样到100ms
resampled = option_data.resample('100ms').agg({
    'bid': 'last',
    'ask': 'last'
})

print(resampled)

注意: 重采样的时候,小心「幸存者偏差」。如果你用最后值,但那个时间窗口内没有数据,就会产生NaN。我曾经因为这个原因,在回测里漏掉了一笔大单,差点酿成大错。

滚动窗口计算:滑动的时间切片

滚动窗口,说白了就是在一个固定大小的窗口里做计算,然后窗口慢慢往前滑。做市商最常用的就是滚动均线、滚动标准差这些。

为什么要用滚动窗口?因为市场是动态的。你用全天的数据算一个均值,那反应太慢了。用最近10笔交易算,才能跟上节奏。

# 计算5笔交易的滚动均值
option_data['bid_ma5'] = option_data['bid'].rolling(window=5).mean()
option_data['bid_std5'] = option_data['bid'].rolling(window=5).std()

print(option_data)

这里有个坑——窗口大小怎么选?我个人的经验是:

  • 做高频做市,窗口选5-10笔就够了
  • 做中低频,窗口可以选20-50笔
  • 千万别用固定时间窗口(比如5秒),因为交易频率变化很大,时间窗口里可能一笔都没有

避坑指南: 我曾经用固定时间窗口做滚动计算,结果在交易清淡的时候,窗口里全是旧数据,算出来的指标完全失真。后来改成固定笔数窗口,问题就解决了。

实战中的组合拳

在实际的期权做市系统里,这三个操作往往是组合使用的。我给你画个流程图,你就明白了。

期权做市信号处理流程 原始Tick数据 (不同频率、不同时间戳) 时间戳对齐 (向前填充/向后填充) 重采样 (统一频率) 滚动窗口计算 (均值、标准差、波动率) 信号输出 (报价、对冲、风控)

你看这个流程,数据进来之后,先对齐,再重采样,然后做滚动计算,最后输出信号。每一步都不能少。

一个完整的例子

我把上面三个步骤串起来,给你一个完整的代码示例。这个例子模拟了做市商处理行情数据的典型场景。

import pandas as pd
import numpy as np

# 模拟数据生成
np.random.seed(42)
time_stamps = pd.date_range('2024-01-15 09:30:00', periods=1000, freq='50ms')
prices = 3.200 + np.cumsum(np.random.randn(1000) * 0.001)

data = pd.DataFrame({
    'price': prices,
    'volume': np.random.randint(100, 1000, 1000)
}, index=time_stamps)

# 第一步:时间戳对齐(这里数据已经是齐的,但演示一下)
aligned_data = data.resample('100ms').last()

# 第二步:重采样到100ms
resampled = aligned_data.resample('100ms').agg({
    'price': 'last',
    'volume': 'sum'
})

# 第三步:滚动窗口计算
resampled['ma10'] = resampled['price'].rolling(window=10).mean()
resampled['std10'] = resampled['price'].rolling(window=10).std()
resampled['upper'] = resampled['ma10'] + 2 * resampled['std10']
resampled['lower'] = resampled['ma10'] - 2 * resampled['std10']

# 输出前20行
print(resampled.head(20))

一个小技巧: 滚动窗口计算的时候,记得用min_periods参数。比如rolling(window=10, min_periods=5),这样在数据不够10笔的时候,也能算出结果。我在实盘里就是这么干的。

嗯,时间序列的基础就这些。对齐、重采样、滚动窗口,这三个操作是期权做市系统的地基。地基打牢了,后面的信号处理才能站得住。

专注资料整理