交易所 API 与数据获取基础
做量化你只需要三类数据:K 线(OHLCV)用于策略研究,深度(orderbook)用于估算滑点,成交明细(trades)用于分析成交结构。获取方式上,历史数据用 REST 批量拉取,实盘信号用 WebSocket 订阅;API 密钥一律只开读取权限,禁止提币。
本文要点
- K 线是聚合后的数据,天然丢失了盘中路径信息,只用 K 线回测会低估滑点。
- REST 适合拉历史和低频轮询,WebSocket 适合实盘推送;两者通常要同时用。
- API key 必须遵循最小权限:只开读取,不开提币,绑定 IP 白名单。
- 限频是硬约束,必须实现指数退避重试,否则会被临时封禁。
- 多交易所联合回测前必须做时间对齐,按时间取交集而不是简单拼接。
三类行情数据:分别解决什么问题
所有交易所提供的行情数据本质上是同一批成交事件的三种不同精细度的呈现。理解它们的差别,直接决定你的回测有多可信。
K 线(OHLCV)是把一段时间内的所有成交压缩成五个数字:开盘、最高、最低、收盘、成交量。它体积小、易存储,一年的 1 分钟 BTC 数据大约 52 万行,几十兆就装下了。绝大多数策略研究都从这里开始。代价是路径信息全丢——同一根 K 线,价格可能先冲高再回落,也可能先下探再拉升,两种情况对止损单的影响完全不同,但 K 线看起来一模一样。
深度(orderbook)是某一时刻挂单簿的快照:买卖两侧各若干档的价格与数量。它回答的是「我如果现在市价买 5 个 BTC,会成交在什么均价」。没有深度数据,你的滑点假设就只能靠猜。
成交明细(trades)是逐笔成交流水:时间、价格、数量、主动方向。它体积最大,但能揭示 K 线看不到的东西,比如大单是集中在一瞬间成交还是分散在整根 K 线里。
| 维度 | K线 OHLCV | 深度 orderbook | 成交明细 trades |
|---|---|---|---|
| 粒度 | 按周期聚合 | 某时刻的挂单快照 | 逐笔成交 |
| 主要用途 | 策略研究与回测 | 滑点估算、流动性评估 | 微观结构、成交质量分析 |
| 数据量级 | 小,年级别几十 MB | 大,快照频率决定 | 很大,日级别可达 GB |
| 历史可得性 | 通常提供数年 | 多数交易所不提供历史 | 部分提供,窗口有限 |
| 最大局限 | 丢失盘中价格路径 | 只是瞬时状态,不含撤单意图 | 存储与处理成本高 |
| 适合的策略周期 | 分钟到日线 | 分钟级以下执行优化 | 秒级、做市、套利 |
REST 与 WebSocket:什么时候用哪个
REST 是请求-响应模式:你发一个 HTTP 请求,交易所返回一段数据。它的优势是简单、可重放、适合分页拉取大量历史。缺点是每次请求都有网络往返延迟,而且受限频约束,不适合高频轮询。
WebSocket 是长连接推送:你订阅一次,之后交易所主动把更新推给你。延迟通常在几十毫秒量级,不占用 REST 的请求配额。缺点是连接会断,断线期间的数据会丢,你必须自己实现重连与补数逻辑。
实际工程里几乎总是两者并用:启动时用 REST 拉一段历史把状态填满,然后切到 WebSocket 接增量推送,断线重连后再用 REST 补齐缺口。这个模式是行业标准做法,不要试图只用一种。
- 历史回测数据:REST 分页拉取,按时间落盘存 parquet 或数据库。
- 实盘信号计算:WebSocket 订阅 K 线或 ticker,本地维护滚动窗口。
- 下单与查询持仓:REST,因为需要确定的响应结果。
- 深度维护:WebSocket 增量更新 + 定期 REST 快照校验,防止本地簿偏离。
拉取历史 K 线的通用写法
各交易所的 REST 接口路径和参数名不同,但分页拉取的骨架是一样的:用时间戳或 ID 作为游标,每次拉一批,把游标推进到最后一根的时间,直到追上当前时间。下面是一段通用的 requests 实现,接口地址与参数名需要你按目标交易所的文档替换。
import time
import requests
import pandas as pd
BASE = "https://api.example-exchange.com" # 替换为目标交易所文档给出的地址
PATH = "/v1/klines" # 替换为实际路径
def fetch_batch(symbol, interval, start_ms, limit=1000, max_retry=5):
"""拉一批 K 线,带指数退避重试。返回原始 list。"""
params = {
"symbol": symbol,
"interval": interval,
"startTime": start_ms,
"limit": limit,
}
delay = 1.0
for attempt in range(max_retry):
try:
r = requests.get(BASE + PATH, params=params, timeout=10)
except requests.RequestException:
time.sleep(delay)
delay *= 2
continue
if r.status_code == 200:
return r.json()
if r.status_code == 429: # 触发限频
wait = float(r.headers.get("Retry-After", delay))
time.sleep(wait)
delay *= 2
continue
if 500 <= r.status_code < 600: # 服务端临时故障
time.sleep(delay)
delay *= 2
continue
r.raise_for_status() # 4xx 参数错误,直接暴露
raise RuntimeError("fetch_batch failed after retries")
def fetch_klines(symbol, interval, start_ms, end_ms, step_ms):
"""分页拉取区间内全部 K 线,去重并按时间排序。"""
rows, cursor = [], start_ms
while cursor < end_ms:
batch = fetch_batch(symbol, interval, cursor)
if not batch:
break
rows.extend(batch)
last_open = int(batch[-1][0])
if last_open <= cursor: # 游标没前进,防死循环
break
cursor = last_open + step_ms
time.sleep(0.25) # 主动限速,别把配额打满
df = pd.DataFrame(rows, columns=[
"open_time", "open", "high", "low", "close", "volume"
])
df["open_time"] = pd.to_datetime(df["open_time"].astype("int64"), unit="ms", utc=True)
for c in ["open", "high", "low", "close", "volume"]:
df[c] = df[c].astype(float)
df = (df.drop_duplicates("open_time")
.sort_values("open_time")
.set_index("open_time"))
return df
三个细节值得注意。第一,游标必须校验是否前进,否则接口返回同一批数据时会陷入死循环。第二,主动 sleep 比等着被限频再退避更有效,把速率控制在配额的七成左右最稳。第三,时间戳统一转成 UTC,跨交易所对齐时能省掉大量麻烦。
python
API 密钥权限:只开读取,禁止提币
这一节是本文最重要的部分。行情数据大多是公开接口,根本不需要密钥;只有查询账户和下单才需要。而一旦密钥泄露,损失是不可逆的。
原则是最小权限:研究阶段只用公开接口,一把密钥都不要建。需要查账户时建一把只读密钥。真正要实盘下单时再单独建一把带交易权限的密钥,且永不开启提币权限。提币权限对量化策略没有任何用处,开启它只会把风险放大到全部资产。
| 配置项 | 研究/回测阶段 | 实盘阶段 | 理由 |
|---|---|---|---|
| 读取行情 | 公开接口,无需密钥 | 公开接口,无需密钥 | 行情不涉及账户 |
| 读取账户 | 关闭 | 开启 | 对账与持仓核对需要 |
| 现货/合约交易 | 关闭 | 开启 | 下单必需 |
| 提币权限 | 关闭 | 永久关闭 | 策略不需要,泄露即全损 |
| IP 白名单 | 开启 | 必须开启 | 密钥泄露后异地无法使用 |
| 密钥存储 | 不产生密钥 | 环境变量或密钥管理服务 | 禁止硬编码进代码 |
| 定期轮换 | 不适用 | 每 1-3 个月更换 | 缩短泄露暴露窗口 |
限频与重试:把它当硬约束设计
每个交易所都有请求频率上限,通常按权重计算:查一次深度可能算 5 分,拉一批 K 线算 2 分,每分钟总权重有上限。超限的后果是返回 429,连续超限会被临时封 IP,时长从几分钟到几小时。
正确做法有三层。第一层是主动限速,在客户端维护一个令牌桶,把速率压在配额的 70% 以下,别指望交易所帮你刹车。第二层是指数退避重试,遇到 429 或 5xx 时等待时间逐次加倍,并优先读取响应头里的 Retry-After。第三层是区分错误类型:4xx 里除了 429 都是你的参数写错了,重试一万次也没用,应该立刻抛出让你看到。
还有一个容易忽略的点:幂等性。查询接口重试无害,下单接口重试可能导致重复成交。所有下单请求都应带上客户端自定义订单号,交易所据此去重。
多交易所数据对齐的三个坑
跨交易所做套利或对比研究时,数据对齐的错误会直接产出虚假的价差信号。三个最常见的问题需要专门处理。
第一是时间戳语义不一致。有的交易所 K 线时间戳是开盘时间,有的是收盘时间;有的用毫秒,有的用秒。混用会造成整体错位一个周期,价差图上会出现根本不存在的稳定套利机会。统一约定用 UTC 开盘时间。
第二是缺失周期的处理。交易所维护、新币上线时间不同、流动性枯竭时没有成交,都会造成 K 线缺失。多标的对齐时必须按时间取交集,即所有标的都有数据的时刻才保留。用 outer join 再前向填充会制造出「一边价格在动、另一边停住」的假价差。
第三是合约规格差异。同名合约的面值、结算币种、资金费率结算频率可能都不同。价差要在折算成同一计价口径之后再比较,否则数字没有意义。关于成本与价差如何影响真实收益,可以配合回测成本模型一起看。
- 统一时区与时间戳语义为 UTC 开盘时间。
- 按周期重建完整时间索引,标记缺口位置。
- 多标的按时间取交集,禁止前向填充跨越缺口。
- 核对合约面值与计价单位,折算到同一口径。
- 抽样人工核对若干时点的原始数据,确认对齐正确。
常见问题
做回测一定要自己拉数据吗,有没有现成的?
有公开数据源和第三方数据服务,作为起步足够用。但自建数据管道有两个不可替代的好处:你清楚每个字段的口径和清洗规则,以及实盘时数据源和回测保持一致。建议先用现成数据验证想法,确定方向后再自建。
WebSocket 断线丢的数据怎么补?
标准做法是记录最后收到的一条数据的时间戳,重连成功后用 REST 拉取从该时间戳到当前的区间,去重后合并进本地缓存。同时要监控断线频率,如果频繁断线通常是网络或心跳配置问题,而不是交易所故障。
只做研究需要建 API 密钥吗?
不需要。K 线、深度、成交明细在绝大多数交易所都是公开接口,无需认证即可访问。只有查询账户余额、持仓和下单才需要密钥。研究阶段不建密钥,等于把这一类风险直接降为零。
深度数据没有历史,滑点怎么估?
退一步用可获得的信息近似:用成交明细统计不同时段的单笔成交规模分布,或用 K 线的高低价差与成交量构造流动性代理指标。更保守的做法是在回测里直接把滑点设成一个偏悲观的固定值,先看策略能不能承受。所有回测结果均为历史数据,不构成收益承诺。