加密货币交易所 三:核心业务概念和实现

指数价格怎么加权、某家现货交易所掉线了权重怎么补、标记价格为什么取三个数的中位数、资金费率的溢价为什么不能拿标记价格去算。外加持仓量的四种加减规则。

位置
第 03 篇 / 共 12 篇
预计
12 分钟

第一篇说过,系统里同时跑着三个价格:最新成交价、指数价格、标记价格,而决定谁被强平的是最后那个。这一篇把它们各自怎么算出来写清楚 —— 这几个式子是整个风控体系的地基,写错一处,下游所有的盈亏和强平判定跟着错。

指数价格:从多家现货加权,掉一家就重新归一

指数价格是标的资产在现货市场上的公允价,它不来自这个合约自己的成交,而是从多家现货交易所采回来加权平均。

指数价格
指数价格 = Σ(交易所权重 × 交易所价格) ÷ Σ交易所权重

代一组数:三家交易所,权重 50%、30%、20%,此刻报价 50000、50100、49900。

算一遍
50000 × 0.5 = 25000
50100 × 0.3 = 15030
49900 × 0.2 = 9980
─────
50010 ÷ (0.5 + 0.3 + 0.2) = 50010

指数价格 50010。注意它比权重最大的那家(50000)略高一点 —— 因为报价最高的那家拿了 30% 的权重,而报价最低的那家只有 20%。

分母那个 Σ交易所权重 平时等于 1,看起来是废的。它存在的理由是某一家掉线的时候。假设第二家(权重 30%)的接口超时了,正确的做法不是拿它上一次的报价接着用(陈旧数据比没有数据更危险),而是把它整个剔除,用剩下两家重新归一:

剔掉权重 30% 那家之后
指数价格 = (50000 × 0.5 + 49900 × 0.2) ÷ (0.5 + 0.2)
= (25000 + 9980) ÷ 0.7
= 34980 ÷ 0.7
= 49971.43

如果不重新归一,直接把分子的三项减成两项,会算出 34980 —— 一个荒谬的价格,而且它会立刻把全市场的多头打到强平线以下。分母那一项就是防这件事的。

同样的道理适用于异常值:某家现货交易所的价格突然偏离其它几家 10%,多半是它自己出了问题(深度被打穿、接口返回了错的字段),而不是市场真的动了。采集环节要在加权之前把这种数据剔掉。

指数价格的采集链路:三家现货交易所按权重把报价送进数据采集服务,先过异常值检测剔掉偏离过大的源,指数计算器按剩余权重重新归一,算出的指数价格写进消息队列,再由撮合引擎、风控系统、清算系统、行情推送四个服务读同一份值 指数价格的采集链路:三家现货交易所按权重把报价送进数据采集服务,先过异常值检测剔掉偏离过大的源,指数计算器按剩余权重重新归一,算出的指数价格写进消息队列,再由撮合引擎、风控系统、清算系统、行情推送四个服务读同一份值

链路本身没什么花样,值得说的是扇出那一端:算出来的指数价格要同时送到撮合、风控、清算和行情推送四个地方去。走消息队列而不是让四个服务各自去调接口,是为了保证它们在同一时刻看到的是同一个价格 —— 风控按 50010 判定强平、清算按 50008 算盈亏,这种偏差在账上没法解释。

标记价格取三个数的中位数

标记价格是合约的公允价,它以指数价格为基准,但不等于指数价格 —— 合约相对现货有基差,直接拿现货价当合约的公允价会系统性地算错所有人的盈亏。

Binance 的 U 本位合约取三个值的中位数:

怎么来的 它代表什么
价格一 指数价格 × (1 + 上期资金费率 × 距下次结算的时间 ÷ 资金费率周期) 按资金费率线性摊销推出的合约价
价格二 指数价格 + 最近 2.5 分钟基差的移动平均 按实际观测到的基差推出的合约价
价格三 合约自己的最新成交价 市场此刻真实在成交的价格

其中价格二里的基差是 (买一 + 卖一) ÷ 2 − 指数价格,每 5 秒采一个点、取最近 30 个点求平均,也就是 2.5 分钟的窗口。这个窗口比多数人想的短得多 —— 它要跟得上基差的真实变化,太长就成了一个滞后的数。

取中位数是这个设计里最要紧的一步。 三个输入各有各的失效方式:价格一在资金费率突刺时会跑偏,价格二在基差短时张开时会跑偏,价格三被一笔砸盘就能拽走。中位数的性质是任何单个输入怎么跑都影响不了结果 —— 它总是落在剩下两个之间。换成算术平均,一个极端值就能按权重把结果拖过去一截。

价格一里那个「距下次结算的时间 ÷ 资金费率周期」是个线性衰减:离结算越近,资金费率对合约公允价的影响越小,因为这笔钱马上就要付掉了,它对持仓成本的剩余影响在归零。分母是这个品种自己的结算周期,8 小时的品种除以 8,4 小时的除以 4 —— 写死 8 只对最常见的那一类成立。

资金费率的溢价不能拿标记价格去算

一个看起来自然、实际上是循环的写法:

错的写法
资金费率 = (标记价格 − 指数价格) ÷ 指数价格

问题在于标记价格本身就是从资金费率推出来的(上一节的价格一)。拿它反过来算资金费率,等于让这个数自己定义自己,两边互相追着跑。

真实的溢价用的是订单簿,不是标记价格。Binance 的做法是:

溢价指数(Premium Index)
P = [ max(0, 冲击买价 − 指数价格) − max(0, 指数价格 − 冲击卖价) ] ÷ 指数价格

「冲击买价 / 冲击卖价」不是买一卖一,而是用一笔规定金额的市价单吃进去,能拿到的平均成交价。用它而不是盘口第一档,是因为第一档挂一手就能改,而要把冲击价推动,得真的在簿上压足够的量。这一条把「在盘口挂一笔小单来操纵资金费率」这条路堵死了。

两个 max(0, ...) 让 P 在合约价夹在冲击买卖价之间时等于 0 —— 也就是合约价和现货价没有明显偏离的时候,溢价为零,不该有人付钱。

完整的资金费率还要再加一层:

资金费率
F = 平均溢价指数 P + clamp(利率 − P, −0.05%, +0.05%)

clamp(x, min, max) 就是把 x 限制在区间内:小于 min 取 min,大于 max 取 max,中间就是它自己。利率项是一个固定值(Binance 默认每 8 小时 0.01%,即每天 0.03%),代表持有两种资产的机会成本差。这个 clamp 的作用是:溢价很小的时候,资金费率被利率项拉向 0.01% 这个基准;溢价一旦拉开,利率项的影响被夹在 ±0.05% 之内,主导项变回溢价本身。

在这之上还有一个总的上下限,而它是算出来的,不是拍一个数。Binance 的口径是 上限 = min(0.75 × (初始保证金率 − 维持保证金率), 维持保证金率),下限取负。BTCUSDT 最高杠杆档的初始保证金率 0.8%、维持保证金率 0.4%,代进去就是 min(0.75 × 0.4%, 0.4%) = ±0.3%

流传很广的「±0.75%」是把公式里的系数 0.75 读成了百分数。别的品种也不是这个值:被单独调整过的多数在 ±2%,个别到 ±3%。

有上限这件事本身比具体数字重要。极端行情里公式会算出荒谬的数值 —— 没有封顶的话,这个本来用来稳定价格的机制会反过来变成风险源:一次超高的资金费率结算,能把一批本来健康的仓位直接扣到强平线以下。

结算周期最常见的是每 8 小时一次,但不是只有这一种 —— Binance 现在 1 小时、4 小时、8 小时三种都在跑,波动大的品种周期更短。周期是哪个会同时影响标记价格里那个衰减项的分母。只有持仓跨过结算点才付费,在结算前平掉就完全绕开这笔钱。

杠杆上限跟着仓位走,不跟着用户走

原始的产品文档里常见一种说法:交易所根据市场波动和用户的风险承受能力,动态调整每个用户的最大可用杠杆。这不是主流交易所的实际做法,至少不是主要的那条规则。

真正在起作用的机制叫风险限额(也叫阶梯保证金):档位挂在仓位的名义价值上,不挂在用户身上。仓位越大,维持保证金率越高,允许的最大杠杆越低。同一个账户,开一个小仓位能用最高杠杆,开一个大仓位就只能用低杠杆 —— 和他是不是 VIP、历史交易记录如何都没关系。

区别不是文字游戏。按用户评分给杠杆意味着两个用户在同一时刻对同一个合约有不同的规则,风控要为每个账户维护一份状态;按仓位大小分档则是一张所有人共用的静态表,撮合前查一次就行。后者可以公开发布、可以被用户预先算进策略里,前者不能。

这套分档的目的是控制清算时的市场冲击,不是控制用户的行为。一个 5000 万名义价值的仓位被强平,这笔单子自己就能砸穿好几个价格档位,交易所得为这个冲击预留更厚的垫子。第六篇讲强平价怎么算的时候会看到,跨档抬高的是维持保证金的边际费率,而最大杠杆是直接跳变的。

至于市场波动性这个因素,它确实会影响保证金要求,但走的是另一条路:交易所整体上调某个品种的保证金要求,通常提前公告,对所有人一起生效。它不是一个按用户实时计算的评分。

有效杠杆则是个事后的观测量,随时在变:

有效杠杆
有效杠杆 = 持仓名义价值 ÷ 账户净值
例:账户净值 1000 USDT,持有 1 BTC 多头、标记价格 10000
有效杠杆 = 10000 ÷ 1000 = 10 倍

价格涨,账户净值涨,有效杠杆自己就降下来了;价格跌,有效杠杆自己往上爬。这就是为什么亏损中的仓位风险是加速恶化的 —— 不是你加了杠杆,是杠杆自己长上去了。

全仓和逐仓在数据模型上是两张不同的表

前面两篇讲过这两种模式的取舍,这里只补实现上的差别:

全仓 逐仓
保证金存在哪 账户层面一个值,所有仓位共用 每个仓位一个值
风险怎么算 要把这个账户所有仓位的浮盈浮亏凑齐 单个仓位自己算完就够
清算触发的粒度 账户 单个仓位
跨分片 :仓位散在不同的撮合分片上 不用

最后一行是工程上真正的分界。逐仓的风险判定是本地的,一个仓位的数据齐了就能算;全仓要求把这个账户在所有交易对上的仓位凑到一起,而它们分布在不同的撮合分片上(第五篇讲分片)。风控服务因此必须额外维护一份跨分片的账户视图,它的更新延迟直接就是全仓风险判定的延迟。

换句话说:全仓给用户省下的资金效率,是用风控系统这一侧的复杂度和延迟换来的。

持仓量的加减规则有四条,不是两条

持仓量(Open Interest)是所有未平仓合约的总和,成交量是一段时间内成交的总和。两者经常被一起提,但它们回答的是不同的问题:成交量说的是「换手有多活跃」,持仓量说的是「有多少钱还压在场内」。同样的成交量,持仓量在涨还是在跌,含义完全相反。

「开仓时增加、平仓时减少」这个说法不够。一笔成交总是同时涉及一个买方和一个卖方,两边各自可能在开仓也可能在平仓,四种组合对持仓量的影响是不一样的:

买方 卖方 持仓量
开多 开空 + 成交量
开多 平多(卖出平仓) 不变
平空(买入平仓) 开空 不变
平空 平多 − 成交量

只有两边都在建仓时持仓量才增加,两边都在了结时才减少,一边开一边平则完全不动。所以维护这个计数器需要知道每一笔成交的双方各自是在开还是在平 —— 这个信息撮合引擎本身不关心,得由下游的仓位服务在更新仓位时算出来。把它当成「撮合引擎顺手加一下」的东西,数就会对不上。

成交量简单得多,滑动窗口累加就行。要注意的只有一件事:一笔成交同时是一个人的买和另一个人的卖,只能算一次,不能两边都记。这是行业里「成交量到底是不是重复计算」这个争论的来源。

数据这边的分层是通行做法:最近 24 小时的放内存数据库支持高频读写,最近若干天的进时序数据库,更早的归档。之上再做预聚合 —— 分钟线、小时线实时算好存下来,日线周线定期跑。这么做的理由很直接:SELECT 一遍原始成交明细来算一根日线,在数据量上是不可行的,而这类查询在 K 线图上每次拖动都会发生。

这篇是 加密货币交易所的第 3 篇。前一篇是 加密货币交易所 二:整体架构,后一篇是 加密货币交易所 四:用户接入系统