Crypto OS
Technical Crypto OS第四阶段 · DeFi Engineering

T23 · Perp DEX

保证金、资金费和风险引擎怎样组成一个系统?

练习的能力
BuilderFinancial Literacy
动手
实现仓位与保证金计算,模拟一次连环清算,记录保险基金的消耗。
AI Lab
让 AI 设计资金费率公式,自己用历史数据回测它能不能把价格拉回现货。

一个现实问题

你的永续合约场所上线三周了。你最花力气的是撮合:订单簿、价格优先时间优先、部分成交、撤单、事件推送。它跑得很干净,压测下延迟稳定,成交记录一笔不差。

然后某个凌晨,标的在十二分钟里跌了 18%。

第二天早上你去对账,所有子系统的日志都是正常的:

  • 撮合没有卡过,成交全部落库。
  • 清算引擎触发了 214 次,全部成功。
  • 价格源没有停更,T21 的四个检查一次都没报警。

账对不上

永续合约是一个零和账本:多头赚的每一块钱,必须是空头亏的那一块钱。你把当天所有仓位的盈亏加起来,本应等于零,实际却是负的——系统欠着用户一笔钱。

你顺着差额找过去,原因很简单:有 31 个多头仓位在被平掉的时候,亏损已经超过了它押上的保证金。它们的对手方赚的钱是真的,但付钱的人已经没钱了。你写了三周的撮合引擎,在这件事里一行代码都没错,也一点忙都帮不上。

这个洞是谁的?谁来填?

思想实验

把交易所全部拆掉,只留一张纸和一个人。纸上记着两行:A 做多 10 个单位,B 做空 10 个单位,标的现在 100,价格每动 1 就有一方给另一方 10。

这个人的工作不是撮合——A 和 B 已经配上了。他的工作只有一件:保证这张纸上的每一笔账随时都能兑付。 现在给他加一个条件:A 和 B 都只押了 50 保证金,而不是 1000。

价格开始动。他立刻发现自己必须每时每刻回答四个问题,而这四个问题就是这一章的全部内容。

第一,此刻每个人还剩多少? 价格到 103,A 浮盈 30,B 浮亏 30,B 的净值只剩 20 了。这个数字必须随时可算,而不是等到平仓那天才知道。

第二,什么时候必须把人请出去? 如果等 B 的净值归零才动手,那一刻价格正好在 105,他平仓能精确成交在 105 吗?不能——他需要时间,而价格不等他。所以他必须提前,在 B 还剩一点钱的时候就动手。剩多少才动手,是一个必须写下来的数字。

第三,请晚了的缺口怎么办? 价格从 104 跳到 108,B 的净值变成负 30。A 应该拿到 80,但 B 只交得出 50。这三十块钱必须从某个人身上拿走,因为这张纸必须平。挂着不算数——挂着就等于 A 的余额是假的。

第四,如果所有人都想做多呢? 纸上必须一多一空才成立。看多的人有一百个,看空的只有三个,剩下九十七个人和谁配对?

第四个问题的答案就是第 18 章讲过的资金费率:给「站在拥挤那一侧」标一个价格,让付钱这件事把人从拥挤侧赶到稀缺侧。那一章问的是「它为什么能把价格按住」,这一章问的是「它在代码里怎么写」。

四个问题合在一起,就是一台机器:它持续读价格、持续重算每个人的净值、在净值见底之前把人请出去、请晚了就从别人身上拿钱补上。

撮合决定谁和谁成交。这台机器决定这个系统能不能兑付。

你来决定

回到开头那 31 个穿仓仓位。缺口是一个确定的数字,它必须在今天之内落到某个人头上。

你选谁?

观察结果

四个选项摆在一起,共同结构很清楚:它们都在回答同一个问题——零和账本上那个洞,今天之内落在谁头上。

承担方用户可预期吗触发时机主要代价
挂成坏账永不结清变成先到先得的挤兑
专项资金池第一优先规模有限,计价资产可能同步缩水
按比例扣盈利资金池耗尽后损失来源无法提前定价
强制平掉盈利仓位部分可预期最后一道做对方向也拿不到全部盈利

第一行是唯一一个真正错误的选项,其余三个是同一条防线的不同层级。把它们排成序列,就是这一章的骨架:

  1. 清算按维持保证金线平仓
  2. 仓位自身的剩余保证金
  3. 专项资金池
  4. 强制平掉盈利方
四道防线依次启用。越往右,承担方离「这件事和我有关」越远,因此越需要提前公开规则。

现在把穿仓的发生条件写成一个不等式。它和 T22 那个不等式是同一个形状

穿仓发生的条件:

    标记价格在「触发清算」到「仓位平完」之间的移动幅度
                >
    维持保证金率 + 预留给清算的那部分

其中:
  左边 = 市场的速度 × 你的清算延迟     ← 你控制不了的部分
  右边 = 你设的参数                    ← 你能设计的部分

结构和 T22 完全一致,但有两处差别,这两处差别就是这一章存在的理由右边小一个数量级(借贷的缓冲是二十几个百分点,永续的维持保证金率常见在百分之几甚至千分之几,因为杠杆高),以及缺口不能挂账(借贷的坏账可以记在协议头上慢慢消化,永续的缺口今天就要从某个用户身上拿走)。

于是这一章的那句话:永续合约的本质是一台持续运行的风险引擎,撮合只是它的外壳。 撮合决定成交质量,风险引擎决定这个系统会不会在某个凌晨欠用户的钱。

建立模型

五个部件。按它们在一次价格变化里被触发的顺序来看。

一、标记价格:所有判断的地基

一个永续市场里同时有三个价格,混用它们是这一章最贵的一个错误:

名字它是什么用来做什么
指数价格多个现货场所价格的加权资金费率的基准、标记价格的原料
标记价格指数价格加上一段受限的偏离算盈亏、算清算
最新成交价这个合约自己最近一笔成交显示、撮合

清算必须用标记价格,不能用最新成交价。 最新成交价由你自己这个市场的盘口决定,而盘口可以被一笔交易推动——用它做清算依据,等于把「谁该被清算」的决定权交给任何一个愿意在薄盘口上砸一笔的人。

最朴素的取法是把外部指数价当基准,只允许成交价在它周围一个固定带宽内浮动

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

uint256 constant BPS = 10_000;

/// 标记价格:以外部指数价为基准,只允许围绕它小幅偏离
function markPrice() public view returns (uint256) {
    uint256 index = priceGuard.readPrice();   // T21 的四个检查在这个函数里面
    uint256 last = lastTradePrice;

    uint256 upper = index * (BPS + markBandBps) / BPS;   // markBandBps 可配置,典型量级是几十到几百
    uint256 lower = index * (BPS - markBandBps) / BPS;

    if (last > upper) return upper;
    if (last < lower) return lower;
    return last;
}

这段代码的全部价值在于它把攻击成本从「推动自己的盘口」抬高到了「推动外部现货市场」,而外部现货市场有多厚,就是 T21 那个安全边际的分子。T21 的整整一章都在这个 readPrice() 里面:它返回一个被操纵的值,下面四个部件全部失效,而且每一个都会正常执行、不报任何错。

二、仓位与保证金:随时可算的净值

int256 constant WAD = 1e18;

struct Position {
    int256 size;          // 正数是多头,负数是空头,单位是标的数量
    uint256 margin;       // 这个仓位押上的保证金
    uint256 entryPrice;   // 开仓时的标记价格
    int256 entryFunding;  // 开仓时的累计资金费指数,见下一节
}

/// 未实现盈亏:size 带符号,所以多空两种情况用同一行算完
function unrealizedPnl(Position memory p, uint256 mark) pure returns (int256) {
    return p.size * (int256(mark) - int256(p.entryPrice)) / WAD;
}

/// 名义价值 = 仓位绝对值 × 标记价格
function notional(Position memory p, uint256 mark) pure returns (uint256) {
    int256 s = p.size >= 0 ? p.size : -p.size;
    return uint256(s) * mark / uint256(WAD);
}

size 用带符号整数是这里唯一值得强调的设计选择:多空的盈亏、资金费、清算判断可以共用同一套公式,不必在每个函数里分叉——分叉写法在多空不对称的边界上几乎必然出错。

三、资金费率:不能遍历所有仓位

第 18 章讲清楚了资金费率为什么能把价格按住。工程上它有一个绕不开的约束:你不能每 8 小时遍历所有仓位收一次钱,仓位可能有几万个,链上没有这个预算。

解法和 T22 的利息完全同构:维护一个全局累计指数,仓位只记开仓时的指数值。 任意时刻某个仓位欠多少,是一次减法:

int256 public cumulativeFunding;  // 每一单位多头仓位累计应付的金额,带符号
uint256 public lastFundingAt;

/// 把上次到现在的资金费累进指数里。O(1),和仓位数量无关
function accrueFunding(uint256 mark, uint256 index) internal {
    uint256 elapsed = block.timestamp - lastFundingAt;
    if (elapsed == 0) return;

    // 溢价项:标记价相对指数价偏离多少,放大 WAD 倍
    int256 premium = (int256(mark) - int256(index)) * WAD / int256(index);

    // 费率 = 溢价项摊到每秒 + 利率项,然后必须夹在上下限之内
    int256 rate = premium / int256(fundingPeriod) + interestPerSecond;
    if (rate > fundingCapPerSecond) rate = fundingCapPerSecond;
    if (rate < -fundingCapPerSecond) rate = -fundingCapPerSecond;

    cumulativeFunding += rate * int256(elapsed) * int256(index) / WAD;
    lastFundingAt = block.timestamp;
}

/// 欠的资金费:正数表示这个仓位要付出去,负数表示它在收钱
function fundingOwed(Position memory p, int256 cumulative) pure returns (int256) {
    return p.size * (cumulative - p.entryFunding) / WAD;
}

/// 账户净值 = 保证金 + 未实现盈亏 − 欠的资金费
function equity(Position memory p, uint256 mark, int256 cumulative) pure returns (int256) {
    return int256(p.margin) + unrealizedPnl(p, mark) - fundingOwed(p, cumulative);
}

三个必须自己决定的参数,全都是可配置的,没有普遍正确的值:

参数它决定什么设错了会怎样
fundingPeriod偏离被摊成多长时间收完太长则锚太松,太短则平静期也在反复收费
fundingCapPerSecond这个锚的强度上限设窄了,极端行情下机制顶到头、再也施加不了压力
计费基准按名义价值还是按保证金两者差着一个杠杆倍数,写错就是量级错误

第二行最容易被跳过。上下限不是安全措施,它是锚的最大拉力:偏离一旦超过它能提供的压力,费率就变成一个恒定值、不再随偏离增大,这时候把价格拉回来的只剩套利者,而套利者在极端行情里最容易退场。

还有一件必须写进代码的事:资金费要在清算判断之前累计。 一个长期不动的仓位可能仅仅因为资金费累积就跌破维持线,和价格毫无关系。漏掉这一行,你会在某一天发现一批「看起来很健康」的仓位其实早就该被清算了。

四、清算:账必须在这一步平掉

error NotLiquidatable();

/// 可清算的条件:净值跌到维持要求以下
function isLiquidatable(
    Position memory p,
    uint256 mark,
    int256 cumulative,
    uint256 maintenanceBps    // 维持保证金率,典型量级是几十到几百 bps,通常随仓位规模分档,必须可配置
) pure returns (bool) {
    int256 e = equity(p, mark, cumulative);
    if (e <= 0) return true;
    return uint256(e) <= notional(p, mark) * maintenanceBps / BPS;
}

function liquidate(address trader) external {
    Position storage p = positions[trader];
    uint256 mark = markPrice();
    accrueFunding(mark, priceGuard.readPrice());          // 先结资金费,再做判断

    if (!isLiquidatable(p, mark, cumulativeFunding, maintenanceBps)) revert NotLiquidatable();

    int256 e = equity(p, mark, cumulativeFunding);
    uint256 fee = notional(p, mark) * liquidationFeeBps / BPS;   // 清算人的全部动机

    if (e > int256(fee)) {
        _payout(msg.sender, fee);                  // 清算费给清算人
        _payout(trader, uint256(e) - fee);         // 剩下的退还用户
    } else if (e > 0) {
        _payout(msg.sender, uint256(e));           // 只够付一部分清算费
    } else {
        // 净值已经是负数:穿仓。缺口必须在这一笔交易里找到承担方
        _coverShortfall(uint256(-e));
    }

    _closePosition(trader);
}

function _coverShortfall(uint256 gap) internal {
    emit ShortfallCovered(gap);
    if (insuranceFund >= gap) {
        insuranceFund -= gap;
        return;
    }
    uint256 remaining = gap - insuranceFund;
    insuranceFund = 0;
    _autoDeleverage(remaining);      // 按盈利与杠杆排序,强制平掉盈利方的一部分
}

_coverShortfall 是整个合约里唯一一个不允许 revert 的分支。它可以耗尽资金池、可以动用最后一道防线,但它必须返回,因为账必须在这一笔交易里平掉。

对照 T22 看这一行:借贷的 _recordBadDebt 是「记下来、以后再说」,永续的 _coverShortfall 是「现在就找到钱」。同一个位置,两种完全不同的义务。

五、清算人为什么来

和 T22 一模一样:清算人不领工资,没有义务,只在「名义价值乘以清算费率」大于「Gas 加上平掉这个仓位的滑点」时才出现。 于是两者共享同一组失效模式——小仓位清算不动、拥堵时算不过账、十个人抢一个机会九个白付手续费。区别在后果:借贷是坏账慢慢累积,永续是今天就有人提不出钱

它叫什么

Position仓位

一个用户在这个市场上未平掉的敞口:方向、规模、开仓价、押上的保证金。

工程上把方向写成带符号的数量而不是一个布尔字段,可以让盈亏、资金费和清算判断共用一套公式。多空分叉的写法在边界上几乎必然出错。

Margin保证金

你为一个杠杆仓位押上的本金。

要分清两个数:初始保证金决定能开多大,维持保证金决定什么时候被清算。还要分清模式:逐仓下清算价是一个可以提前算出的常数,全仓下它会随账户里其他仓位的盈亏持续移动,开仓时算出的那个数字不能当常数用。

Mark Price标记价格

用来计算盈亏和触发清算的那个价格,以外部指数价为基准,只允许在受限的带宽内偏离。

它存在的全部理由是:把清算的判断依据从一个薄市场搬到一个厚市场上。 用最新成交价代替它,等于允许任何人用一笔交易决定谁该被清算——这是永续合约历史上最经典的一类事故。

Funding资金费率

按周期在多空两方之间结算的一笔付款,方向和大小由标记价相对指数价的偏离决定。

它不是手续费,钱在用户之间转移。工程实现上不能遍历仓位收取,必须做成累计指数,仓位只记开仓时的指数值。三个必查参数:周期、上下限、计费基准——第二个是这个锚的强度上限

Insurance Fund保险基金

专门用来填穿仓缺口的一笔钱,来源通常是清算过程中的剩余与手续费留存。

判断它的两个问题:它用什么计价(装着这个场所自己发行的资产,它会在最需要它的时刻同步缩水),以及它的规模相对未平仓量是多少。单看基金有多少没有意义——未平仓量是这个市场此刻挂着的存量杠杆,它才是分母。

Auto-deleveraging自动减仓

保险基金不足以覆盖缺口时,按排序强制平掉盈利方一部分仓位来接住敞口。

它是最后一道防线,也是永续合约独有的一类风险:方向判断正确、仓位在盈利,仍然可能被规则平掉。 排序规则必须提前公开,让用户能知道自己排在第几位。

动手

动手实现仓位与保证金计算,模拟一次连环清算,记录保险基金的消耗Solidity + 任意合约测试框架0 元,全程本地网络

实现核心结构与四个纯函数。 Position 结构体,加上 unrealizedPnlnotionalfundingOwedequitysize 用带符号整数。

先只写纯函数,每一个都要能手算验证

  • 多头 10 个单位、开仓价 100、标记价 103 → 浮盈 30;同样规模的空头(size 为负)→ 浮亏 30。
  • 累计资金费指数从 0 涨到 2:多头 10 个单位欠 20,同样规模的空头收 20。
  • 两边的资金费加起来必须是零。这是零和性质的第一个测试,它在后面所有测试里都应该成立。

实现标记价格与资金费累计。 标记价格用「指数价加带宽」的做法,价格源接 T21 的 PriceGuard,四个检查一个都不要去掉。写一个可控的桩价格源,验证三件事:最新成交价在带宽内时返回成交价;被推到带宽之外时返回被夹住的边界值而不是成交价;价格源过期时,整个清算路径的行为符合你在 T21 里显式选过的那个策略。

实现清算与两道兜底。 liquidate 里那三个分支全部写出来,尤其是 e 为负的那一支。_coverShortfall 必须能在保险基金不足时继续执行,不允许 revert。写一个测试专门验证:把保险基金设成 0,构造一个穿仓仓位,调用清算——它必须成功返回并触发自动减仓。

如果它 revert 了,你的合约里就有了一个永远清算不掉的仓位。 这是 T22 那条教训在永续上的版本,后果还要更严重:它会一直占着对手方的头寸,让别人的浮盈永远兑现不了。

构造一次连环清算,记录保险基金的消耗。这是 Lab 的主体。 建三批多头仓位,杠杆递增,驱动一个下跌序列。关键在于:每一步都必须重新读标记价格、重新累计资金费、重新判断,而不是一次算完。

初始标记价 100,维持保证金率按你自己设的值

三批多头: A 批 5 倍杠杆 1 亿、B 批 10 倍杠杆 3 亿、C 批 20 倍杠杆 2 亿
深度假设: 每平掉 5000 万名义价值,标记价格再跌 1%(全局最关键的假设)
起始扰动: 标记价格跌到 96

一轮一轮跑,每轮记一行:

轮次标记价本轮被清算的名义价值本轮穿仓缺口保险基金余额是否触发自动减仓
1
2
3

找出保险基金归零的那一轮。 那一轮就是你这套参数下「规则开始伤害做对方向的人」的起点。

然后只改深度假设,从「每 5000 万跌 1%」改成「每 2 亿跌 1%」,其余全部不变,重跑一遍。两张表放在一起,你会看到这一章最重要的一个结论:同样的杠杆结构、同样的参数、同样的代码,换一个深度假设,一次只是回调,一次是保险基金被打穿。 这也解释了为什么同一套合约配一个流动性很差的标的,风险要高一个数量级——问题不在合约里。

算一次攻击成本。这是整章的量化练习。

前面算的是「行情会不会打穿我」。现在换一个问法:有人主动来打,要花多少钱?

场景:你的市场上有 N 的名义价值堆在某个杠杆水平,
     它们的清算触发价距离当前价 d(百分比)

第 1 步:把标记价格推到 d 需要多少钱
        标记价以外部指数价为基准,所以这一步要推的是外部现货市场
        用 T21 的公式:约等于 现货深度 × (sqrt(1+d) − 1)

第 2 步:被触发的清算规模,以及这些强平单会把价格再推低多少
        这是连环效应,用你上一步的深度假设

第 3 步:攻击者能拿到什么
        a) 他事先建立的反向仓位在这段下跌里的盈利
        b) 他作为清算人收到的清算费
        c) 他在被强平单里以折价接到的货

第 4 步:净利 = 第 3 步 − 第 1 步 − 手续费 − Gas

代三组数:现货深度取 500 万、5000 万、5 亿,d 取 5%。第一组的净利多半是正的——它说明在一个浅标的上,清算不是「价格下跌的后果」,而是一笔可以被主动制造的生意

然后做两次反算。一,在你这套参数下外部现货深度至少要多少,这笔生意才不划算? 这个数字就是这个标的能被安全上架的最低流动性要求,和 T21 里那个抵押品准入门槛是同一个东西。二,把标记价格换成最新成交价,第 1 步要花多少钱? 推动自己盘口通常比推动外部现货市场低一到两个数量级,这个倍数就是标记价格这个设计的全部价值。

全程本地网络,不接任何真实资金。 真实资金只出现在毕业项目,并且必须先通过第五阶段的安全验收。

AI Lab

AI Lab让 AI 设计资金费率公式,自己用历史数据回测它能不能把价格拉回现货Level 2 · AI Copilot

分三步问,第三步是真正的产出:

第一步:
为一个永续合约市场设计资金费率公式。给出完整表达式、每一项的含义、
上下限怎么选,以及一个 O(1) 的链上实现(不允许遍历仓位)。
说明周期长度和上下限这两个参数各自在保护什么。

第二步:
给你一段价格数据:标的现货价格,以及同期永续合约的成交价。
用你上面那个公式逐期算出资金费率,输出每一期的:
偏离幅度、费率、多头的持有成本年化、空头的收益年化。
指出哪些时段费率顶到了上下限。

第三步:
在第二步顶到上限的那些时段,回答:
偏离是继续扩大还是收敛?如果继续扩大,说明什么?
你会怎么改参数,改完之后新的代价是什么?

第一步大多数模型能给出形状正确的公式,错误集中在实现上:很多回答会写一个遍历所有仓位的循环,或者干脆把它推给链下。追问它:几万个仓位、每 8 小时一次,这段代码的 Gas 是多少?

第二步要用真实的历史数据,不要让它自己编一段。去公开数据源拉一段现货与永续的价格序列(剧烈波动的时段比平静时段有价值得多)喂给它,然后自己抽查三期,用纸笔复算它给的费率。

第三步是这个 Lab 的全部价值。顶到上限还在扩大,意味着机制已经失效,而账面上一切正常。 让它回答这种情况下还有什么在把价格往回拉——答案是套利者,而套利者恰恰在这时最容易退场。最后把它的公式接进你 Lab 里的合约跑一遍:不要只看公式漂不漂亮,看它在你的连环清算模拟里表现如何。

AI 说完之后,你必须自己验证

  • 它的公式里有没有上下限——没有上下限的费率公式在极端行情下会给出荒谬的数值
  • 把上下限代进去,算一算在偏离 10% 时这个锚能施加的最大压力是多少,够不够
  • 它算的是按名义价值还是按保证金收费,两者差着一个杠杆倍数,它有没有说清楚
  • 它的实现是不是遍历所有仓位收费——链上没有这个预算,必须是累计指数
  • 它有没有说明费率为负时钱从空头流向多头,以及这时资金池的会计怎么处理
  • 回测里它用的价格是指数价还是标记价,混用这两个会让回测结果完全不可信
  • 它提到的任何具体参数(周期、上下限、维持保证金率),你是否当成可配置的起点而不是事实

真实案例

用最新成交价判断强平的那个时代永续合约早期

早期一些场所直接用合约自己的最新成交价来判断强平。攻击路径完全可以被算出来:估算一批仓位的强平价,估算把自己盘口砸到那里需要多少钱,如果触发的强平规模大于成本,这就是一笔生意。强平单落回同一个薄盘口,把针拉得更长,触发更多强平;针拔掉之后价格回到原位,但被扫掉的仓位回不来了。

行业后来普遍改成外部现货加权的指数价格加上受限的标记价格。这个改动的本质就是本章那段 markPrice() 代码:把清算的判断依据从一个薄市场搬到一个厚市场上。它和 T21 是同一个问题的两个出口。

用自己的仓位把保险基金抽干2022 年某个链上永续场所

攻击者选了一个现货流动性很小、却被这个场所上了永续的标的。他先在永续上建立一个很大的多头,然后去现货市场把价格买上去。指数价随之上涨,他的多头浮盈变成一个巨大的数字——而这个浮盈在这个场所的规则里可以立刻用来提走资产。

没有任何一行代码出错。 清算没触发,因为他在赚钱;价格源没撒谎,因为现货真的涨了;保证金计算完全正确。被绕过的是一个假设:「浮盈是真实的」。

两个教训:上架一个标的之前,先算它的现货深度撑不撑得住这个市场的敞口上限;以及未实现盈亏能不能立刻提走,是一个需要单独做决定的参数,很多实现对它做了限制。

保险基金被一次行情打穿多次剧烈行情中多个场所

价格跳空,大批仓位在维持线以下才被平掉,缺口远超当期清算能覆盖的部分,保险基金迅速见底,自动减仓大规模触发。结果是:大量方向判断正确、正在盈利的用户被强制平掉了部分仓位。 他们没有做错任何事,也没有收到任何风险提示——规则里写着,但他们没读。

这类事件之后的常见改动有三个:公开保险基金余额与未平仓量的比值、公开自动减仓的排序队列让用户看到自己的位置、以及按仓位规模分档提高维持保证金率,让大仓位承担更高的缓冲要求。第三个最有效,也最不受大户欢迎。

资金费率长期顶在上限单边行情持续时多个永续市场

在一段持续的单边行情里,资金费率可以连续多期停在上下限上,而偏离还在扩大。

这时候一个很容易被误读的事实是:费率是最大值,不代表压力是最大值。 费率已经不再随偏离增长,机制的拉力被封顶了,把价格拉回去的只剩套利者。而套利者需要在现货一侧建立反向仓位,这在行情最紧张时恰恰最难做。

上下限是这个锚的强度上限。 它是一个必查参数,而绝大多数用户从来没查过——看任何一个永续市场,这个数字应该排在你清单的前三行。

改一个变量

如果标记价格改回用最新成交价

盘口厚的时候看不出任何区别,代码跑得一样快,测试一样绿。区别全部出现在盘口变薄的时候,而且它改变的不是风险大小,是风险的性质:原本「价格波动导致清算」,现在变成「清算可以被主动制造」。

你 Lab 最后一步算出的那个倍数就是这个改动的代价。这是整章最不该改的一行——它是一个把攻击成本从外部现货市场降到自家盘口的开关。

如果最大杠杆从 20 倍提到 100 倍

单个仓位的爆仓距离从 4.5% 缩到 0.5%,但真正的变化不在单个仓位:同样一笔保证金现在能撑起五倍的名义价值,未平仓量会上升,而这些新增敞口全部堆在极薄的缓冲上,保险基金的规模却没有跟着变。原本需要 5% 的下跌才能点燃第一排骨牌,现在 0.5% 就够了。

把 Lab 那张表按新杠杆重跑一遍,保险基金归零的轮次会明显提前。提高杠杆上限不是多给用户一个选项,它改变了这个市场从平静到失控所需要的扰动大小。

如果资金费从每 8 小时结算一次改成每个区块连续累计

锚变紧了:偏离一出现就开始计费,不用等到结算时刻。用累计指数实现的话这个改动几乎是免费的——把 accrueFunding 挂在每次状态变更前调用即可。

三个新问题随之而来。抢在结算时刻前后开平仓的套利消失了,这是好事。每笔交易要多做一次价格读取和指数更新,Gas 上升。精度:每个区块的费率是一个很小的数,整数运算下反复截断会系统性地少收或多收,累计几十万个区块之后是一个可观的数字——取整方向要让资金池不亏,这和 T19 里「每一次取整都必须让池子赢」是同一条。

如果保险基金用这个场所自己发行的资产计价

平时账面很好看:基金规模可以做得很大,成本很低。问题在于它引入了一个反馈回路——极端行情里缺口变大的同时,这个资产的价格通常也在下跌,基金的实际覆盖能力在最需要它的时刻缩水。你以为有三道防线,实际上第二道在第一道失效的同一时刻跟着失效了。

更彻底的问法是第 21 章那个结构:你的保险基金和你要保的风险,是不是同一个东西驱动的? 如果是,它就不是保险,它是杠杆。

带走的问题

5
谁在支付?

三笔独立的钱,必须分开看:资金费在多空两方之间转移(不经过协议),清算费从被清算的用户流向清算人穿仓缺口从保险基金或盈利方流向被穿仓的对手方。三笔钱的付款人都是用户,收款人也大多是用户。

协议在这套机制里主要不是收钱的一方,而是记账和兜底的一方——这决定了它的失败模式是「兑付不了」,而不是「亏钱」。

8
谁提供流动性?

永续市场的流动性有三层,缺一层就不完整:合约盘口的深度决定开平仓滑点,外部现货市场的深度决定标记价格能不能被操纵,保险基金是缺口出现时唯一不需要找对手方就能动用的资金。很多分析只看第一层——一个盘口看起来很厚、但外部现货很薄的永续市场,是这一章最想让你警惕的东西。

9
谁承担风险?

分四层回答,顺序就是本章那条防线链:平时风险在杠杆使用者自己身上;清算时一部分转移给清算人(他承担滑点和竞争失败的成本);穿仓时转移到保险基金;基金不够时转移到盈利的那一方。最后一层是永续独有的——你可以在做对的情况下被规则平掉,所以保险基金规模、它的计价资产、自动减仓的排序规则都是必查项。

11
如果 Token 价格归零,产品还能运行吗?

这一问在这里有一个很具体的形态:如果这个场所自己发行的资产归零,保险基金还剩多少? 扩展一下更有用——这套机制里有哪些环节依赖一个价格会波动的东西:保险基金的计价资产、清算人的对冲工具、给做市方的激励。每一个这样的依赖,都是一个在极端行情里会和风险同步恶化的部件。

本章自测

一句话带走

永续合约的本质是一台持续运行的风险引擎,撮合只是它的外壳。

做完这一章的动手环节了?勾上它查看全部进度

本页目录