时间序列基础:时间戳对齐、重采样、滚动窗口计算
做期权做市,说白了就是跟时间赛跑。你拿到的行情数据,每一笔都有个时间戳。但问题来了——不同交易所、不同数据源的时间戳,往往不是对齐的。我刚开始做这个的时候,就吃过这个亏。
举个例子。你同时收到上证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秒),因为交易频率变化很大,时间窗口里可能一笔都没有
避坑指南: 我曾经用固定时间窗口做滚动计算,结果在交易清淡的时候,窗口里全是旧数据,算出来的指标完全失真。后来改成固定笔数窗口,问题就解决了。
实战中的组合拳
在实际的期权做市系统里,这三个操作往往是组合使用的。我给你画个流程图,你就明白了。
你看这个流程,数据进来之后,先对齐,再重采样,然后做滚动计算,最后输出信号。每一步都不能少。
一个完整的例子
我把上面三个步骤串起来,给你一个完整的代码示例。这个例子模拟了做市商处理行情数据的典型场景。
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笔的时候,也能算出结果。我在实盘里就是这么干的。
嗯,时间序列的基础就这些。对齐、重采样、滚动窗口,这三个操作是期权做市系统的地基。地基打牢了,后面的信号处理才能站得住。