给 Deribit 做一个实时行情终端: 六个产品, 一套代码
❝不开浏览器,在终端里盯 Deribit 的现货、永续、交割、期权——六类产品共用一套级联选择器和深度图。本文讲清:每个产品的合约信息从哪个接口来,以及那套选择器是怎么从「永续一级」一步步长成「期权四级」的。
一、3 个接口, 6 个产品
Deribit 所有合约信息来自同一个接口:
GET /api/v2/public/get_instruments?kind={kind}&active=true&expired=false
kind 只有三种:spot / future / option。但业务上要区分六个产品,关键在于 future 这一种要按两个字段二次切分:
settlement_period:perpetual(永续)还是其它(交割)settlement_currency:USDC(线性 / U 本位 = UM)还是 BTC/ETH(反向 / 币本位 = CM)
于是六个产品这样落到接口上:
- Spot —
kind=spot,如 BTC_USDC - UM-Perpetual —
kind=future + 永续 + USDC,如 BTC_USDC-PERPETUAL - UM-Futures —
kind=future + 交割 + USDC,如 BTC_USDC-16JUN26 - CM-Perpetual —
kind=future + 永续 + BTC/ETH,如 BTC-PERPETUAL - CM-Futures —
kind=future + 交割 + BTC/ETH,如 BTC-16JUN26 - Option —
kind=option,如 BTC-16JUN26-100000-C
一句话:3 个接口拉数据,future 靠结算周期 + 结算币种过滤出 4 个,加上 spot/option 共 6 个。
实测规模(主网):Spot 18、UM-Perp 18、UM-Futures 39、CM-Perp 2(只有 BTC/ETH)、CM-Futures 22、Option 约 4200 个。
二、WebSocket 频道:合约名直接拼
接口返回的合约原生名(raw_symbol)就是 websocket 的频道名,book / 成交直接拼:
book.{raw_symbol}.100ms # 增量订单簿(snapshot + change)trades.{raw_symbol}.100ms # 逐笔成交
对六个产品都通用——永续、交割、期权、现货都是这一套,公有频道免鉴权。订单簿的 snapshot/change 维护与断流重连是另一篇的内容,这里复用同一个状态机。
三、级联选择器:从永续一步步长到期权
终端里的核心是一个「产品 → … → 合约」的级联选择器。它是逐步演进的,每一步先用真实数据验证可行,再加一层。
第一步:永续(1 级)
最简单:一个标的直接对应一个合约。
- CM-Perpetual:
BTC/USD → BTC-PERPETUAL - UM-Perpetual:
ADA_USDC → ADA_USDC-PERPETUAL
界面两行:Product + Underlying,Tab 切换聚焦行,←/→ 选。
【配图:cm-perpetual.png — CM 永续(币本位,只有 BTC/ETH,tick 0.5)】
【配图:um-perpetual.png — UM 永续(USDC 线性,18 个标的)】
【配图:spot.png — 现货】
❝踩坑:Deribit 的 CM 永续只有 BTC/ETH。一开始硬编码了 SOL-PERPETUAL,永远没数据——SOL 在 Deribit 是 USDC 线性永续(属 UM)。所以标的列表必须从接口动态拉,不能写死。
第二步:交割(加一层到期)
一个标的有多个到期(BTC 交割有十几个),所以标的之下要再选一个到期才能定一个合约:
产品 → 标的 → 到期 → 合约BTC → 16JUN26 → BTC-16JUN26
界面多一行 Expiry,且只在交割品种时出现。
【配图:um-futures.png — UM 交割(USDC 线性)】
【配图:cm-futures.png — CM 交割(BTC 十几个到期,到期行自动窗口化)】
第三步:期权(加行权价 + 方向),触发重构
期权要 标的 → 到期 → 行权价 → 方向(Call/Put) 四级,约 4200 个。再往上一步的嵌套结构里堆行权价/方向会很丑。于是换成维度过滤模型——一把统一了所有六个产品:
# 每个产品声明它的级联维度PRODUCT_DIMS = {"Spot": ["underlying"],"UM-Perpetual": ["underlying"],"UM-Futures": ["underlying", "expiry"],"CM-Perpetual": ["underlying"],"CM-Futures": ["underlying", "expiry"],"Option": ["underlying", "expiry", "strike", "type"],}
核心是一个逐级过滤函数:第 N 级的候选 = 在「上层已选值」过滤后的合约集合里,该维度的去重值;选满所有维度就锁定唯一合约:
defbuild_levels(metas, dims, sel): levels, flt = [], metasfor d in dims: key = DIM_KEY[d] # 怎么从合约读这一维的值 vals = sorted({key(m) for m in flt}) # 当前候选if sel.get(d) notin vals: # 上层一变,本层非法选择回落默认 sel[d] = vals[0] if vals elseNone cur = sel.get(d) levels.append((d, vals, vals.index(cur) if cur in vals else0)) flt = [m for m in flt if key(m) == cur]return levels, (flt[0] if flt elseNone) # 最后剩的就是目标合约
这就是「消除特殊情况」:永续 1 维、交割 2 维、期权 4 维,但代码只有一条 build_levels,没有针对产品的分支。选择器行数 = 产品 + 维度,动态生成。
【配图:option-inverse.png — 期权四级级联,行权价行支持键入数字跳转】
四、几个让它好用的细节
键入跳转(type-to-jump):行权价有几十个,←/→ 翻太慢。聚焦某行直接敲数字就增量跳到首个匹配项——输 65 跳 65,000、输 100 跳 100,000,行首显示 /查询。
长列表窗口化:标的 18 个、行权价几十个,一行放不下。以选中项为中心开一个窗口,两侧用 ‹N / N› 标隐藏数量,选中项永远可见。
价格精度自适应:Deribit 币本位期权价格极小(以 BTC 计价),固定一位小数全是 0.0。改成按合约的最小变动价位(tick)算该显示几位:
def_price_decimals(tick):# 0.5→1, 2.5→1, 0.0001→4, 10→0 s = f"{tick:.10f}".rstrip("0")return len(s.split(".")[1]) if"."in s else0
同样是 BTC 期权,两种计价方式精度天差地别,同一套代码都对:
【配图:option-inverse.png — 币本位 BTC 期权,mid 0.0090,自动 4 位小数】

【配图:option-linear.png — USDC 计价 BTC 期权,mid 192,自动 0 位小数】
切换即重订阅:任意一级变化导致目标合约改变时,取消订阅旧的、订阅新的、重置盘口与成交状态;残留的旧消息按当前合约的频道名自动过滤掉。
单循环无竞态:用一个事件循环同时等「行情推送」和「键盘按键」,谁先到处理谁,单线程顺序执行,零共享状态竞态。键盘走终端 cbreak 原始模式读单键。
五、一条主线
整套东西其实是五层,每层都被六个产品共用:
- 数据层:3 个接口(
get_instruments?kind=…)→ 6 个元数据 provider → 统一的合约模型 - 频道层:合约名直接拼
book.* / trades.*,六产品通用 - 状态层:同一个订单簿状态机(snapshot + 增量),教学、实盘、可视化三处复用
- 选择层:一张
PRODUCT_DIMS 表 + 一条 build_levels 过滤管线,覆盖 1~4 级级联
❝先搞对数据结构,代码自然简洁:维度模型一上,永续 / 交割 / 期权的差异就退化成一张表,剩下全是同一条过滤管线。