数据源与数据清洗:Tick级数据与Level 2数据
做高频交易,说白了就是跟数据打交道。我入行那会儿,第一个项目就是处理纳秒级的行情数据。当时觉得不就是个时间戳嘛,结果踩的坑能写一本《十万个为什么》。今天咱们就聊聊数据源和数据清洗,尤其是那些让我半夜爬起来改代码的教训。
Tick级数据与Level 2数据
先说说数据源。高频交易里,最基础的是Tick级数据。每一笔成交、每一次报价变动,都算一个Tick。我习惯把Tick数据想象成市场的「心跳」——每跳一下,价格就动一下。
Level 2数据呢?它比Tick数据更细。你想想看,Tick数据只告诉你「成交价是多少」,但Level 2数据会告诉你「买一价、卖一价、买一量、卖一量」,甚至能看到前10档的挂单情况。说白了,Level 2就是市场的「内脏」,你能看到里面在怎么蠕动。
我在项目中遇到过一个问题:某交易所的Level 2数据,买一价和卖一价之间居然有0.01元的价差,但Tick数据里同一时刻的成交价却落在中间。嗯,这里要注意——数据源不一致,是高频交易里最常见的坑。
数据对齐与时间戳处理
数据对齐,听起来简单,做起来要命。尤其是纳秒级数据,时间戳稍微差一点,策略就全错了。
我刚开始做的时候,从两个交易所拿数据,一个用UTC时间,一个用本地时间。结果对齐后,发现买卖信号总是慢半拍。后来一查,原来是时区没统一。你说气不气人?
时间戳处理,我建议统一用Unix纳秒时间戳。为什么?因为不同交易所的时间格式五花八门——有的用字符串"2024-01-15 09:30:00.123456789",有的用整数。你想想看,字符串解析一次就要几十纳秒,高频交易里这几十纳秒可能就是几百万的盈亏。
# 我常用的时间戳标准化函数
def normalize_timestamp(ts, source_format='exchange_a'):
if source_format == 'exchange_a':
# 字符串转纳秒
dt = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S.%f')
return int(dt.timestamp() * 1e9)
elif source_format == 'exchange_b':
# 微秒转纳秒
return ts * 1000
else:
raise ValueError(f"未知格式: {source_format}")
异常值检测与缺失值填充
异常值检测,说白了就是找「不正常的点」。高频数据里,异常值可能是交易所的bug,也可能是网络延迟导致的。我见过最离谱的:某只股票的价格突然从100元跳到10000元,然后又跳回来。嗯,这明显是数据错误。
我的做法是:用滚动窗口的Z-score检测。窗口大小选100个Tick,Z-score阈值设3。超过3倍标准差,就标记为异常。
def detect_outliers_zscore(price_series, window=100, threshold=3):
rolling_mean = price_series.rolling(window=window).mean()
rolling_std = price_series.rolling(window=window).std()
z_scores = (price_series - rolling_mean) / rolling_std
return abs(z_scores) > threshold
缺失值填充呢?高频数据里,缺失值很少是「空值」,更多是「时间戳跳变」。比如,正常每秒有100个Tick,突然某秒只有10个。这时候,我建议用前向填充(forward fill),但要注意:如果连续缺失超过5个Tick,就别填了,直接丢弃。为什么?因为市场状态可能已经变了,填进去的数据反而会误导策略。
我在处理纳秒级数据时踩过的坑
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 坑一:时间戳溢出。 32位整数存纳秒时间戳,会溢出。我有个同事用int存,结果数据跑到2038年就炸了。现在我都用64位整数。
- 坑二:数据排序。 纳秒级数据,有时候时间戳是乱序的。因为网络延迟,后发的数据可能先到。我建议每次读取数据后,先按时间戳排序,再处理。
- 坑三:内存爆炸。 一天的高频Tick数据,可能有几亿条。如果全部加载到内存,32GB都不够。我习惯用分块读取(chunk),或者用数据库存储。
- 坑四:浮点数精度。 价格用浮点数存,加减几次后精度就丢了。我建议用整数存,比如把价格乘以10000,变成整数再计算。
好了,数据源和数据清洗就聊到这儿。记住,高频交易里,数据质量决定了策略的生死。别嫌麻烦,把数据搞干净了,后面的工作才能顺利。