3、数据获取与存储:从交易所API获取订单簿快照,使用Parquet格式存储

好,咱们进入第三章。这一章要解决一个很实际的问题:数据从哪来,又该存成什么样

我个人习惯把数据获取和存储放在一起讲。为什么?因为这两件事是绑定的。你从交易所拿到的原始数据,如果不做任何处理直接存,后面回放的时候会非常痛苦。我踩过这个坑,所以想让你一开始就走对路。

3.1 订单簿快照:你到底需要什么?

先搞清楚我们要拿什么。

订单簿快照,说白了就是交易所某一时刻的「挂单全景图」。它包含两个核心部分:买单队列卖单队列。每个队列里,价格从高到低(买单)或从低到高(卖单)排列,每个价格后面跟着对应的数量。

举个例子,一个典型的订单簿快照长这样:

{
  "bids": [
    [100.50, 1.2],  // 价格100.50,数量1.2个BTC
    [100.40, 3.5],
    [100.30, 0.8]
  ],
  "asks": [
    [100.60, 2.1],
    [100.70, 1.5],
    [100.80, 4.0]
  ],
  "timestamp": 1700000000000,
  "symbol": "BTCUSDT"
}

你想想看,这个结构其实很简单。但真正做回放的时候,你会发现一个问题:快照是瞬时的,而市场是连续的。如果你只存快照,中间的价格变动就全丢了。

核心原则:快照用于重建市场状态,增量更新(tick级数据)用于还原过程。两者结合,才能做真正的回放。

3.2 从交易所API获取数据

现在主流交易所都提供REST API和WebSocket两种方式。我的建议是:用REST API拿快照,用WebSocket收增量。为什么?

  • REST API:请求一次,拿到完整的订单簿。适合定时抓取快照。
  • WebSocket:实时推送,但只推送变化的部分。适合做增量更新。

嗯,这里要注意:不同交易所的API格式差别很大。我以币安为例,写一个简单的快照获取函数:

import requests
import json

def fetch_orderbook_snapshot(symbol="btcusdt", limit=100):
    """
    从币安获取订单簿快照
    :param symbol: 交易对,如 btcusdt
    :param limit: 返回的深度,最大5000
    :return: 字典格式的快照数据
    """
    url = f"https://api.binance.com/api/v3/depth"
    params = {
        "symbol": symbol.upper(),
        "limit": limit
    }
    
    try:
        response = requests.get(url, params=params, timeout=5)
        response.raise_for_status()
        data = response.json()
        
        # 加上我们自己的时间戳,精确到毫秒
        data["fetch_timestamp"] = int(time.time() * 1000)
        return data
    except Exception as e:
        print(f"获取快照失败: {e}")
        return None

小技巧:我建议在返回数据里加上fetch_timestamp字段。因为交易所返回的时间戳是服务器时间,但网络延迟会导致你实际拿到数据的时间更晚。两个时间戳一对比,你就能知道数据的新鲜度。

3.3 为什么选Parquet?

好,数据拿到了。存成什么格式?

你可能会想:CSV不行吗?JSON不行吗?

行,但不好。我在项目中遇到过这样的场景:一天抓了上亿条tick数据,用CSV存,一个文件几十GB,读取慢得要命。后来换成Parquet,文件大小直接缩到原来的1/5,读取速度快了10倍不止。

Parquet的优势

特性 说明
列式存储 只读取你需要的列,而不是整行数据
高压缩比 支持Snappy、Zstd等压缩算法,节省磁盘空间
Schema自描述 文件里自带字段类型信息,不用额外定义
与大数据生态兼容 Spark、Pandas、DuckDB都能直接读

说白了,Parquet就是为「分析型工作负载」设计的。你后面做回放、做统计,经常要按时间范围过滤数据,或者只取某几个字段。Parquet在这些场景下表现极好。

3.4 把快照存成Parquet

接下来是实操。我们用Python的pandaspyarrow库来实现。

先安装依赖:

pip install pandas pyarrow requests

然后写一个存储函数:

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

def save_snapshot_to_parquet(snapshot, symbol, output_dir="data/orderbook"):
    """
    将订单簿快照保存为Parquet文件
    :param snapshot: 从API获取的快照字典
    :param symbol: 交易对
    :param output_dir: 输出目录
    """
    # 创建目录
    os.makedirs(output_dir, exist_ok=True)
    
    # 解析快照数据
    bids = snapshot.get("bids", [])
    asks = snapshot.get("asks", [])
    timestamp = snapshot.get("fetch_timestamp", 0)
    
    # 将买单和卖单分别转为DataFrame
    df_bids = pd.DataFrame(bids, columns=["price", "quantity"])
    df_bids["side"] = "bid"
    df_bids["timestamp"] = timestamp
    df_bids["symbol"] = symbol
    
    df_asks = pd.DataFrame(asks, columns=["price", "quantity"])
    df_asks["side"] = "ask"
    df_asks["timestamp"] = timestamp
    df_asks["symbol"] = symbol
    
    # 合并成一个DataFrame
    df = pd.concat([df_bids, df_asks], ignore_index=True)
    
    # 生成文件名:按日期和交易对组织
    date_str = datetime.fromtimestamp(timestamp / 1000).strftime("%Y%m%d")
    filename = f"{symbol}_{date_str}_{timestamp}.parquet"
    filepath = os.path.join(output_dir, filename)
    
    # 保存为Parquet
    df.to_parquet(filepath, index=False, compression="snappy")
    print(f"快照已保存: {filepath}")
    return filepath

避坑指南:我曾经犯过一个错误——把所有快照都塞进一个Parquet文件里。结果文件越来越大,读取时内存直接爆了。正确的做法是按时间分片,比如每小时或每天一个文件。这样既方便管理,也方便按时间范围回放。

3.5 数据存储的组织结构

文件多了以后,怎么组织目录结构?我推荐这种分层方式:

data/
  orderbook/
    BTCUSDT/
      2024/
        01/
          01/
            BTCUSDT_20240101_1700000000000.parquet
            BTCUSDT_20240101_1700005000000.parquet
            ...
        02/
          ...
    ETHUSDT/
      ...

这种结构的好处是:

  • 按交易对分:不同品种的数据互不干扰
  • 按年月日分:回溯某一天的数据,直接定位到对应目录
  • 文件名带时间戳:一眼就能看出数据的时间范围

3.6 完整的数据获取流程

最后,我把整个流程串起来。下面这张图展示了从API到Parquet的完整链路:

订单簿数据获取与存储流程 交易所API REST / WebSocket 请求 数据解析 JSON → DataFrame 转换 Parquet存储 Snappy压缩 磁盘 每条记录包含的字段: • symbol: 交易对(如 BTCUSDT) • side: 买卖方向(bid / ask) • price: 价格(浮点数) • quantity: 数量(浮点数) • timestamp: 获取时间戳(毫秒) • exchange: 交易所来源(可选) ⚠️ 注意事项 1. 频率限制:交易所API有请求频率限制,建议每秒不超过1次快照请求 2. 数据校验:拿到数据后先检查bids和asks是否为空,价格是否合理 3. 异常处理:网络超时、API返回错误码时,要有重试机制

3.7 读取与验证

存完之后,怎么确认数据是对的?写一个简单的读取函数:

def read_parquet_snapshot(filepath):
    """
    读取Parquet文件并验证数据完整性
    """
    df = pd.read_parquet(filepath)
    
    print(f"数据条数: {len(df)}")
    print(f"字段列表: {df.columns.tolist()}")
    print(f"时间范围: {df['timestamp'].min()} ~ {df['timestamp'].max()}")
    print(f"买卖分布: \n{df['side'].value_counts()}")
    
    # 检查是否有空值
    if df.isnull().sum().sum() > 0:
        print("⚠️ 数据中存在空值,请检查")
    else:
        print("✅ 数据完整性检查通过")
    
    return df

个人经验:我每次存完数据,都会立刻读回来验证一遍。别嫌麻烦,我曾经因为网络波动导致一批数据全是空值,如果没及时检查,后面回放时整个策略都会跑偏。

好了,这一章的内容就这些。从API获取快照,用Parquet存储,每一步都有坑,但走通了之后,你会发现后面的回放工作会顺畅很多。


蓝海数据掘金营,专注资料整理