第三章 市场数据获取与清洗
做期权做市,数据就是你的弹药。弹药有问题,枪法再准也白搭。
我刚开始做这行的时候,犯过一个低级错误——用了某天下午3点15分的快照数据,没注意交易所的撮合规则。结果波动率曲面画出来全是锯齿,回测曲线倒是漂亮,实盘一跑就亏钱。后来排查了三天,才发现是数据时间戳没对齐。
所以这一章,咱们就聊聊怎么把交易所的原始数据,变成能用的干净数据。
3.1 从交易所获取期权链数据
国内期权市场,目前主要就是上证50ETF期权、沪深300ETF期权、中证500ETF期权,还有中金所的股指期权。每家交易所的数据格式略有不同,但核心字段差不多。
我个人习惯用CTP接口或者万得、聚宽的API来拉数据。但不管用哪种方式,你拿到的原始数据大概长这样:
# 伪代码示例:获取期权链数据
import pandas as pd
from datetime import datetime
# 假设从交易所API获取
raw_data = {
'ticker': ['510050C2309M02700', '510050P2309M02700'],
'type': ['call', 'put'],
'strike': [2.700, 2.700],
'expiry': ['2023-09-27', '2023-09-27'],
'last_price': [0.0450, 0.0320],
'bid': [0.0448, 0.0318],
'ask': [0.0452, 0.0322],
'volume': [12500, 9800],
'open_interest': [45200, 38900],
'timestamp': ['2023-09-15 14:30:00', '2023-09-15 14:30:00']
}
df = pd.DataFrame(raw_data)
这里有个坑——合约代码的解析。你看那个ticker字段,里面包含了标的、到期日、行权价等信息。我建议你写个解析函数,一次性拆开:
def parse_option_ticker(ticker):
"""解析期权合约代码"""
# 示例:510050C2309M02700
# 510050 = 标的代码, C = call, 2309 = 2023年9月, M = 月合约, 02700 = 2.700
symbol = ticker[:6]
option_type = 'call' if ticker[6] == 'C' else 'put'
# 继续解析...
return symbol, option_type, strike, expiry
3.2 处理缺失值
数据缺失是家常便饭。比如某个深度虚值期权,全天都没成交,bid/ask都是空的。或者某只合约因为流动性差,某分钟的快照里只有ask没有bid。
我处理缺失值的策略分三步:
- 识别缺失模式:是随机缺失,还是系统性缺失?
- 填充或删除:根据业务场景决定
- 标记:填充过的数据一定要打标签
举个例子,对于bid/ask缺失的情况:
# 处理缺失的买卖价
def fill_missing_bid_ask(df):
# 如果bid缺失但ask存在,用ask - 最小价差估算
df['bid'] = df['bid'].fillna(df['ask'] - 0.0001)
# 如果ask缺失但bid存在,用bid + 最小价差估算
df['ask'] = df['ask'].fillna(df['bid'] + 0.0001)
# 如果都缺失,直接删除该行
df = df.dropna(subset=['bid', 'ask'], how='all')
return df
还有一种情况——时间序列上的缺失。比如某只合约在14:30到14:35之间没有数据。这时候我一般用前向填充,但会加一个时间阈值。超过5分钟没数据,我就认为这个合约暂时失效了。
3.3 异常值检测
异常值比缺失值更危险。因为它看起来像正常数据,但实际上是错的。
我常用的检测方法有几种:
- 价格突变检测:相邻两个tick的价格变化超过N个标准差
- 买卖价差异常:bid-ask spread突然放大到正常值的10倍以上
- 隐含波动率跳跃:同一行权价、相邻到期日的IV出现不合理差异
这里给个简单的异常检测代码:
def detect_price_spike(df, threshold=5):
"""检测价格突变"""
df['price_change'] = df['last_price'].pct_change()
mean = df['price_change'].mean()
std = df['price_change'].std()
df['is_spike'] = abs(df['price_change'] - mean) > threshold * std
return df[df['is_spike'] == False]
嗯,这里要注意——阈值设多少合适?我个人习惯用5倍标准差。但如果是高频数据,可能要用3倍。因为高频数据噪声更大,阈值设太高反而漏掉异常。
避坑指南: 我曾经遇到过一种情况——某只深度实值期权,因为一笔大单成交,价格瞬间跳了20%。用统计方法检测,它确实是异常值。但实际这笔交易是真实的,只是因为流动性太差。所以,异常值不一定是错误数据。我现在的做法是:先标记,再人工复核,或者用成交量辅助判断。
3.4 调整分红与拆股
分红和拆股,是期权数据清洗里最容易被忽视的环节。
为什么?因为期权合约的条款会随着标的资产的变化而调整。比如50ETF分红,期权合约的行权价、合约单位都会变。如果你不调整,波动率曲面就会出现断层。
调整的逻辑其实不复杂:
| 事件类型 | 调整方式 | 示例 |
|---|---|---|
| 现金分红 | 行权价 = 原行权价 - 分红金额 | 原行权价2.700,分红0.05,调整后2.650 |
| 股票分红 | 合约单位 = 原合约单位 × (1 + 送股比例) | 原合约10000股,10送1,调整后11000股 |
| 拆股 | 行权价按比例调整,合约单位按比例调整 | 1拆2,行权价减半,合约单位翻倍 |
具体实现时,我建议维护一个调整因子表:
# 调整因子示例
adjustment_factors = {
'2023-06-15': {'dividend': 0.05, 'split_ratio': 1.0},
'2023-12-20': {'dividend': 0.08, 'split_ratio': 1.0},
}
def adjust_option_data(df, adjustment_factors):
"""根据分红拆股调整期权数据"""
for date, factors in adjustment_factors.items():
mask = df['expiry'] > date
if factors['dividend'] > 0:
df.loc[mask, 'strike'] -= factors['dividend']
if factors['split_ratio'] != 1.0:
df.loc[mask, 'strike'] /= factors['split_ratio']
df.loc[mask, 'contract_size'] *= factors['split_ratio']
return df
3.5 数据清洗的整体流程
说了这么多,咱们把整个流程串起来。我画了一张图,你看看:
这个流程看起来简单,但实际跑起来,你会发现很多细节。比如数据获取这一步,不同交易所的接口频率限制不一样。上交所的行情推送是500ms一次,中金所是100ms一次。你想想看,如果混在一起用,时间戳对齐就是个麻烦事。
我个人习惯的做法是:先统一时间基准。把所有数据都对齐到同一个时间戳上,比如都用毫秒级时间戳。然后再按上面的流程一步步走。
最后说一句: 数据清洗没有一劳永逸的方案。市场在变,交易所的规则在变,你的清洗逻辑也得跟着变。我每个月都会review一次清洗流程,看看有没有新的异常模式出现。做量化,说白了就是跟数据打交道。数据干净了,后面的模型才能靠谱。
公众号:蓝海数据掘金营,微信deep3321