4. 数据获取与存储

高频回测的起点是可靠、低延迟的行情数据。本章聚焦两个核心环节:从交易所API实时获取Tick数据与使用列式存储格式高效持久化。错误的获取策略会导致数据缺失或延迟失真;低效的存储格式则会让回测I/O成为瓶颈。

4.1 交易所API实时行情获取架构

高频行情获取需要同时处理多品种、高吞吐的增量数据。典型的架构采用WebSocket长连接接收推送,配合REST API做补录与校验。

实时行情获取架构 交易所行情网关 WebSocket 全量/增量推送 异步数据接收器 (asyncio/websockets) 协议解析 + 时间戳校验 数据分流 实时信号计算 列式存储引擎 关键:本地时钟同步(NTP)与交易所时间戳对齐,避免延迟偏差导致回测失真

4.2 WebSocket实时Tick数据获取实战

以币安(Binance)现货行情为例,使用websockets库订阅深度快照与增量数据。核心步骤包括:

  1. 建立连接:通过wss://stream.binance.com:9443/ws建立WebSocket连接。
  2. 订阅主题:发送JSON订阅消息,如{"method":"SUBSCRIBE","params":["btcusdt@depth20@100ms"],"id":1}。
  3. 异步接收:使用asyncio事件循环持续接收消息,避免阻塞。
  4. 解析与校验:提取bids/asks数组、更新时间戳,校验数据连续性。
  5. 数据分发:将解析后的Tick对象送入内存队列,供回测引擎或存储模块消费。
避坑提示:高频场景下,WebSocket断线重连是常态。务必实现指数退避重连逻辑,并在重连后通过REST API获取缺失区间的补录数据,保证数据完整性。

4.3 Tick数据存储格式对比

传统CSV或JSON格式在高频场景下存在严重的I/O瓶颈。Parquet与Feather作为列式存储格式,专为分析型工作负载优化。下表对比了三种格式在Tick数据场景下的关键差异:

特性 CSV Parquet Feather
存储类型 行式文本 列式二进制 列式二进制
压缩率 低(文本冗余) 高(列式编码+压缩) 中(轻量压缩)
读取速度 慢(全量扫描) 快(谓词下推+列裁剪) 极快(内存映射)
写入速度 中等 较慢(编码开销) 极快(零拷贝)
Schema支持 无(需外部定义) 强(嵌套类型+元数据) 强(Arrow Schema)
适用场景 小规模、临时分析 长期归档、跨平台分析 高频写入、快速回测加载

4.4 使用Parquet/Feather存储Tick数据

推荐策略:实时写入使用Feather(低延迟、零拷贝),长期归档使用Parquet(高压缩、跨平台兼容)。以下代码演示了两种格式的写入与读取:

import pandas as pd
import pyarrow.feather as feather
import pyarrow.parquet as pq
import pyarrow as pa
from datetime import datetime

# 模拟Tick数据
tick_data = pd.DataFrame({
    'symbol': ['BTCUSDT'] * 1000,
    'timestamp': pd.date_range('2025-01-01', periods=1000, freq='100ms'),
    'bid_price': [50000.0 + i * 0.1 for i in range(1000)],
    'ask_price': [50001.0 + i * 0.1 for i in range(1000)],
    'bid_volume': [0.5 + i * 0.01 for i in range(1000)],
    'ask_volume': [0.6 + i * 0.01 for i in range(1000)]
})

# 写入Feather(适合实时追加)
feather.write_feather(tick_data, 'tick_20250101.feather')
print(f"Feather写入完成,文件大小: {os.path.getsize('tick_20250101.feather') / 1024:.2f} KB")

# 写入Parquet(适合归档,启用Snappy压缩)
table = pa.Table.from_pandas(tick_data)
pq.write_table(table, 'tick_20250101.parquet', compression='snappy')
print(f"Parquet写入完成,文件大小: {os.path.getsize('tick_20250101.parquet') / 1024:.2f} KB")

# 读取Feather(极速加载)
df_feather = feather.read_feather('tick_20250101.feather')
print(f"Feather读取耗时: {timeit.timeit(lambda: feather.read_feather('tick_20250101.feather'), number=10)/10:.4f}秒")

# 读取Parquet(支持列裁剪)
df_parquet = pq.read_table('tick_20250101.parquet', columns=['timestamp', 'bid_price']).to_pandas()
print(f"Parquet列裁剪读取耗时: {timeit.timeit(lambda: pq.read_table('tick_20250101.parquet', columns=['timestamp', 'bid_price']), number=10)/10:.4f}秒")

4.5 数据存储与回测流水线集成

将数据获取、存储与回测引擎串联,形成完整的离线回测流水线。下图展示了数据从交易所到回测结果的全链路:

离线回测数据流水线 WebSocket实时获取 Feather实时写入 Parquet归档压缩 回测引擎加载 每日收盘后执行Parquet归档,删除原始Feather文件以释放空间 T+0 实时 T+0 盘后 T+1 回测
最佳实践:使用pyarrow.dataset API可以统一读取Feather和Parquet文件,无需切换代码。对于跨多日回测,建议按日期分区存储,如data/2025/01/01/tick.parquet,利用分区裁剪加速查询。

4.6 数据质量校验与异常处理

高频数据易受网络抖动、交易所限流、时钟偏差等因素影响。在存储前必须执行以下校验:

  • 时间戳单调性检查:确保每个品种的Tick时间戳严格递增,出现回退时标记并丢弃。
  • 价格合理性校验:过滤超出合理价格区间的异常Tick(如价格突变超过阈值)。
  • 数据连续性检测:检查相邻Tick时间间隔是否超过预设阈值(如1秒),触发补录流程。
  • 重复数据去重:基于交易所消息ID或本地哈希去重,避免重复写入。
注意:不要在生产环境中直接使用pd.DataFrame.append逐行追加数据,这会导致内存碎片和性能下降。应使用list收集批量Tick后,一次性转换为DataFrame并写入。