第一章 数据获取与清洗:交易所API对接与数据对齐

做期现套利,说白了就是吃价差。但价差这东西,转瞬即逝。你想想看,如果数据都拿不准,策略再牛也是白搭。我个人习惯,把数据获取和清洗看作整个套利系统的地基——地基不稳,楼盖得再高也得塌。

这一章,咱们就聊聊怎么从交易所拿到干净、对齐的数据。嗯,这里要注意,我说的“干净”不是指数据本身有没有病毒,而是指它能不能直接用。

1.1 交易所API对接:Binance和OKX

目前主流交易所,Binance和OKX的API文档写得还算清楚。我刚开始对接时,踩过不少坑。比如,Binance的REST接口有频率限制,你请求太快会被封IP。OKX的WebSocket订阅格式又跟Binance不太一样。

先看一个最简单的REST请求示例,获取现货最新价格:

import requests
import json

# Binance 现货价格
def get_binance_spot_price(symbol='BTCUSDT'):
    url = f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}'
    resp = requests.get(url)
    data = resp.json()
    return float(data['price'])

# OKX 现货价格
def get_okx_spot_price(symbol='BTC-USDT'):
    url = f'https://www.okx.com/api/v5/market/ticker?instId={symbol}'
    resp = requests.get(url)
    data = resp.json()
    return float(data['data'][0]['last'])

print(f'Binance BTC: {get_binance_spot_price()}')
print(f'OKX BTC: {get_okx_spot_price()}')
个人经验: 我建议把API key和secret放在环境变量里,别硬编码在代码中。曾经有个同事把key直接提交到GitHub,几分钟后账户就被盗了。血的教训。

期货数据稍微复杂点。因为期货有不同交割月份,比如当季、次季。你需要指定合约代码。Binance的永续合约代码是 BTCUSDT 后面加个 PERP,OKX则是 BTC-USDT-SWAP

# Binance 永续合约价格
def get_binance_futures_price(symbol='BTCUSDT'):
    url = f'https://fapi.binance.com/fapi/v1/ticker/price?symbol={symbol}'
    resp = requests.get(url)
    data = resp.json()
    return float(data['price'])

# OKX 永续合约价格
def get_okx_futures_price(symbol='BTC-USDT-SWAP'):
    url = f'https://www.okx.com/api/v5/market/ticker?instId={symbol}'
    resp = requests.get(url)
    data = resp.json()
    return float(data['data'][0]['last'])

1.2 期货与现货数据对齐

数据拿回来了,但有个问题:现货和期货的报价时间戳可能不一致。比如,现货数据是100ms更新一次,期货可能是200ms。你直接拿两个时间点的价格算价差,结果肯定不准。

怎么做对齐?我个人习惯用“最近邻匹配”。说白了,就是找到时间上最接近的那一对数据。

import pandas as pd

# 假设有两个DataFrame:spot_df 和 futures_df
# 都包含 'timestamp' 和 'price' 两列

def align_data(spot_df, futures_df):
    # 将时间戳设为索引
    spot_df = spot_df.set_index('timestamp')
    futures_df = futures_df.set_index('timestamp')
    
    # 合并,使用最近邻方法
    merged = pd.merge_asof(
        spot_df.sort_index(),
        futures_df.sort_index(),
        left_index=True,
        right_index=True,
        direction='nearest',
        tolerance=pd.Timedelta('100ms')  # 最多容忍100ms偏差
    )
    return merged

aligned = align_data(spot_df, futures_df)
print(aligned.head())
核心要点: 对齐的容忍度不能太大。我曾经试过用1秒的容忍度,结果价差序列里出现了明显的“锯齿”,后来发现是数据错位导致的。建议控制在100-200ms以内。

1.3 缺失值处理

交易所偶尔会抽风,比如网络抖动导致某条数据没返回。这时候数据里就会出现NaN。你不能直接跳过,因为策略需要连续的数据流。

处理缺失值,我常用的方法有三种:

  • 前向填充(ffill): 用上一个有效值填充。适合价格这种变化不大的数据。
  • 线性插值: 用前后两个值的平均值。适合成交量这类数据。
  • 直接删除: 如果缺失太多,干脆扔掉这一行。但要注意,别把连续数据搞断了。
# 前向填充
df['price'].fillna(method='ffill', inplace=True)

# 线性插值
df['volume'].interpolate(method='linear', inplace=True)

# 删除缺失超过50%的行
df.dropna(thresh=len(df.columns) * 0.5, inplace=True)
避坑指南: 我曾经在回测时用了前向填充,结果实盘时发现,缺失值导致策略在那一瞬间用了旧价格,开仓方向完全反了。所以,缺失值处理一定要结合你的策略逻辑。如果价差突然跳变,前向填充可能会掩盖问题。

1.4 时间戳统一

不同交易所的时间戳格式可能不同。Binance用的是毫秒级Unix时间戳,OKX用的是毫秒级,但有些接口返回的是字符串格式。你必须统一成一种格式,否则对齐时会出问题。

我建议统一用 datetime 对象,或者毫秒级整数。看个人习惯。我自己喜欢用毫秒级整数,因为做数学运算快。

import pandas as pd

# 假设从OKX拿到的是字符串时间戳 '2025-01-15T10:30:00.000Z'
def parse_okx_timestamp(ts_str):
    # 去掉末尾的Z,然后转成datetime
    dt = pd.to_datetime(ts_str.replace('Z', ''))
    # 转成毫秒级时间戳
    return int(dt.timestamp() * 1000)

# 假设从Binance拿到的是毫秒级整数 1736922600000
# 直接使用即可

# 统一成毫秒级整数后,再对齐
spot_df['timestamp_ms'] = spot_df['timestamp'].apply(parse_okx_timestamp)
futures_df['timestamp_ms'] = futures_df['timestamp']  # 假设已经是毫秒级
小技巧: 如果你发现时间戳总是差8小时,别慌。那是时区问题。交易所通常返回的是UTC时间,你本地可能是东八区。统一用UTC,别转本地时间,否则跨交易所对比时会乱套。

知识体系总览

下面这张图,是我自己总结的数据获取与清洗流程。你可以把它当作一个检查清单:

期现套利数据获取与清洗流程 1. API对接 Binance / OKX 2. 数据对齐 最近邻匹配 3. 缺失值处理 ffill / 插值 / 删除 4. 时间戳统一 详细说明: • API对接:REST获取最新价,WebSocket订阅实时流,注意频率限制 • 数据对齐:使用pd.merge_asof,容忍度控制在100-200ms • 缺失值处理:价格用ffill,成交量用插值,缺失过多直接删除 • 时间戳统一:全部转成毫秒级整数,使用UTC时间避免时区混乱 最终输出:对齐后的DataFrame,包含现货价格、期货价格、时间戳

嗯,到这里,数据获取与清洗的核心内容就讲完了。你可能会问,为什么花这么大篇幅讲这些基础工作?因为我在实盘中吃过太多亏了。数据不对,策略再漂亮也是纸上谈兵。记住一句话:垃圾进,垃圾出

下一章,咱们会聊价差计算与统计特征。但先把今天的内容消化掉,把代码跑通。有问题随时翻回来看这张流程图。


专注资料整理