永续合约 10 - 清算引擎架构与实现

本文介绍清算引擎的架构和实现. 覆盖三种执行模式 (本地, 远程 gRPC, 链上 Keeper), 通过 Executor 接口把清算逻辑和执行层解耦, 同时讨论部分清算策略, ADL 自动减仓和保险基金管理, 附 Go 和 Solidity 实现.


一、术语表

1.1 清算核心

术语 英文 含义
清算引擎 Liquidation Engine 监控所有仓位, 在保证金不足时触发强制平仓的组件
清算价格 Liquidation Price 仓位保证金降至维持保证金时对应的标记价格, 预先计算
破产价格 Bankruptcy Price 仓位保证金恰好归零时的价格, 清算订单以此价格挂单
维持保证金率 Maintenance Margin Rate 仓位必须维持的最低保证金比例, 低于此触发清算
保证金率 Margin Ratio 当前保证金 / 仓位名义价值, 衡量仓位健康度
逐仓保证金 Isolated Margin 每个仓位独立保证金, 清算不影响其他仓位
全仓保证金 Cross Margin 账户所有仓位共享保证金池, 盈利仓位可以 “撑住” 亏损仓位
保险基金 Insurance Fund 清算盈余积累的资金池, 用于覆盖清算亏损
部分清算 Partial Liquidation 只平掉一部分仓位 (如 25%), 使保证金恢复健康, 避免全平
自动减仓 ADL (Auto-Deleveraging) 保险基金不足时, 强制减少盈利方仓位以覆盖亏损

1.2 执行模式

术语 英文 含义
进程内执行 Local Execution 清算引擎和撮合引擎在同一进程, 直接函数调用
远程执行 Remote Execution 清算引擎作为独立服务, 通过 gRPC/MQ 提交清算订单
链上执行 On-Chain Execution 清算逻辑在智能合约中, 由 Keeper 调用触发
Keeper Keeper 监控链上仓位并调用清算合约的链下机器人, 赚取清算奖励

二、清算引擎 vs 撮合引擎

维度 撮合引擎 (Matching Engine) 清算引擎 (Liquidation Engine)
触发方式 被动: 用户提交订单 主动: 价格变动触发扫描
输入 买卖订单 所有持仓 + 实时标记价格
输出 成交记录 (Trade) 清算订单 (提交给撮合引擎或链上合约)
频率 有单就撮 每次价格更新都要扫描
现货需要吗 需要 不需要 (现货无杠杆, 不存在爆仓)
故障影响 无法交易 坏账累积, 可能导致系统性风险

关键关系: 清算引擎是撮合引擎的上游. 清算引擎判断 “谁该被清算”, 然后生成一个清算订单, 交给撮合引擎执行. 撮合引擎不关心这个订单是用户主动下的还是清算引擎生成的.

2.1 清算的本质: 谁来接盘?

清算 = 强制关闭仓位 = 做一笔反向交易. 核心问题: 谁来接这笔反向交易?

1
2
3
4
5
6
7
8
被清算的 1 ETH Long 仓位
↓ 强制平仓 = 卖出 1 ETH

谁来买这 1 ETH?
├─ 订单簿上的买单 → 撮合引擎配对
├─ AMM 池子 (Uniswap) → 直接 swap
├─ LP 池 (GMX GLP) → Oracle 定价, 池子接盘
└─ 保险基金 → 自己吃下 (最后手段)

撮合引擎不是清算的必要条件, 它只是 “找对手方” 的一种方式:

对手方模式 代表 需要撮合引擎 特点
订单簿撮合 Binance, dYdX, Hyperliquid 需要 清算订单和普通订单竞争同一个流动性池, 深度越好滑点越小
AMM Swap 链下清算 + Uniswap 结算 不需要 清算订单路由到链上池子 swap, 受池子深度和滑点影响
LP 池接盘 GMX (GLP/GM) 不需要 Oracle 定价, 零滑点, 但 LP 承担全部对手方风险
保险基金直接吃 紧急兜底 不需要 保险基金承担全部风险, 仅作为最后手段

本文的 Executor 接口设计正是为了适配这些不同模式:
订单簿 → LocalExecutor / GRPCExecutor, AMM → UniswapExecutor (swap 路由), LP 池 → 直接调用池合约.
清算引擎的核心逻辑 (扫描, 判断, 生成清算指令) 完全不变, 只替换 Executor 实现.


三、整体架构

清算引擎架构 (Liquidation Engine Architecture) Price Feed Oracle / 撮合引擎 推送 mark price 清算引擎 (Liquidation Engine) 优先队列 按清算价排序 保证金计算 margin <= MM 保险基金 盈余/亏损处理 ADL 引擎 保险基金不足时 Position Store 所有未平仓仓位 Executor 接口 (清算执行抽象) 进程内 LocalExecutor 同进程撮合引擎 engine.Submit(order) 延迟: < 1μs 适用: 单体架构, 测试 远程服务 (API/RPC) GRPCExecutor 自建撮合服务 BinanceExecutor Binance API 延迟: ~1ms 适用: 微服务, L2 清算, CEX 路由 链上合约 OnChainExecutor 自建清算合约 UniswapExecutor AMM swap GMXExecutor GLP 池接盘 适用: DeFi, Keeper 网络 清算结果处理 保险基金结算 ADL (若保险基金不足) 仓位更新 + 事件推送 核心逻辑 (扫描 → 判断 → 生成清算订单) 完全复用, 只有 Executor 实现不同

四、核心类型与接口

4.1 基础类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package liquidation

import (
"context"
"time"

"github.com/shopspring/decimal" // go get github.com/shopspring/decimal
)

// Side 表示仓位方向.
type Side int

const (
Long Side = iota // 多头
Short // 空头
)

func (s Side) String() string {
if s == Long {
return "Long"
}
return "Short"
}

// MarginMode 保证金模式.
type MarginMode int

const (
Isolated MarginMode = iota // 逐仓: 每个仓位独立保证金, 亏损不影响其他仓位
Cross // 全仓: 账户所有仓位共享保证金池, 盈利仓位可以 "救" 亏损仓位
)

// Account 表示一个交易账户.
// 全仓模式下, 清算判断在账户级别进行: 账户总保证金率 <= MMR 时触发清算.
type Account struct {
ID string // 账户地址或 ID
Balance decimal.Decimal // 可用余额 (未分配给逐仓仓位的部分)
Positions map[string]*Position // positionID → Position (该账户下所有仓位)
}

// TotalEquity 计算账户总权益 = 余额 + 所有全仓仓位的未实现盈亏.
// 逐仓仓位不参与, 因为它们的保证金已经从 Balance 中扣除.
func (a *Account) TotalEquity(markPrices map[string]decimal.Decimal) decimal.Decimal {
equity := a.Balance
for _, pos := range a.Positions {
if pos.MarginMode != Cross {
continue
}
mp, ok := markPrices[pos.Symbol]
if !ok {
continue
}
equity = equity.Add(UnrealizedPnL(pos, mp))
}
return equity
}

// TotalMaintenanceMargin 计算账户下所有全仓仓位的维持保证金之和.
func (a *Account) TotalMaintenanceMargin(markPrices map[string]decimal.Decimal) decimal.Decimal {
total := decimal.Zero
for _, pos := range a.Positions {
if pos.MarginMode != Cross {
continue
}
mp, ok := markPrices[pos.Symbol]
if !ok {
continue
}
// MM = markPrice × size × maintenanceRate
total = total.Add(mp.Mul(pos.Size).Mul(pos.MaintenanceRate))
}
return total
}

// Position 表示一个未平仓仓位.
type Position struct {
ID string // 仓位唯一标识
Account string // 账户地址或 ID
Symbol string // 交易对, 如 "ETH-USD"
Side Side // Long 或 Short
Size decimal.Decimal // 仓位数量, 如 1.5 (ETH)
EntryPrice decimal.Decimal // 开仓均价, 如 3000 (USD)
Margin decimal.Decimal // 当前保证金 (逐仓: 独立保证金, 全仓: 初始保证金仅做记录)
MaintenanceRate decimal.Decimal // 维持保证金率, 如 0.005 (0.5%)
MarginMode MarginMode // 逐仓 or 全仓
LiquidationPrice decimal.Decimal // 预计算的清算价格 (仅逐仓有效)
UpdatedAt time.Time
}

// UnrealizedPnL 计算仓位的未实现盈亏.
func UnrealizedPnL(pos *Position, markPrice decimal.Decimal) decimal.Decimal {
if pos.Side == Long {
return markPrice.Sub(pos.EntryPrice).Mul(pos.Size)
}
return pos.EntryPrice.Sub(markPrice).Mul(pos.Size)
}

// LiquidationOrder 清算引擎生成的清算指令.
type LiquidationOrder struct {
PositionID string // 被清算的仓位 ID
Account string // 被清算账户
Symbol string // 交易对
Side Side // 清算方向 (与仓位方向相反)
Size decimal.Decimal // 清算数量
BankruptcyPrice decimal.Decimal // 破产价格 (保证金归零的价格)
MarkPrice decimal.Decimal // 触发清算时的标记价格
Timestamp time.Time
}

// LiquidationResult 清算执行结果.
type LiquidationResult struct {
PositionID string
Executed bool // 是否成功执行
FillPrice decimal.Decimal // 实际成交价格
FillSize decimal.Decimal // 实际成交数量
Surplus decimal.Decimal // 盈余 (> 0 归保险基金) 或亏损 (< 0 保险基金垫付)
}

为什么用 decimal.Decimal 而不是 float64?
永续合约涉及大量资金计算, 浮点数的精度丢失在累积后会导致严重问题.
shopspring/decimal 底层基于 big.Int, 但提供了人类可读的 API:
decimal.NewFromFloat(3000) 而不是 big.NewInt(3000_000_000).
到链上边界 (OnChainExecutor) 时再转为 big.Int 即可.

4.2 Executor 接口

清算引擎的核心抽象: 判断逻辑和执行逻辑分离.

1
2
3
4
5
6
// Executor 抽象清算指令的执行方式.
// 清算引擎只负责 "谁该被清算", Executor 负责 "怎么清算".
type Executor interface {
// Liquidate 执行单个清算指令, 返回执行结果.
Liquidate(ctx context.Context, order LiquidationOrder) (*LiquidationResult, error)
}

三种实现对比:

Executor 适用场景 延迟 特点
LocalExecutor 单体架构, 测试 < 1μs 直接调用撮合引擎函数
GRPCExecutor 微服务, L2 ~1ms 独立部署, 可扩缩容
OnChainExecutor DeFi 协议 1~12s 发送链上交易, Keeper 竞争

五、保证金计算

5.1 清算价格公式

以下公式适用于逐仓 (Isolated) 模式, 每个仓位有独立保证金.
全仓 (Cross) 模式没有单仓位清算价 — 清算判断在账户级别进行, 见 §4.4.

清算价格是 “保证金刚好降到维持保证金” 时的标记价格. 提前算好, 避免每次价格更新都遍历全量仓位.

多头 (Long) 清算条件: 价格下跌导致亏损, 保证金不足:

1
2
3
4
margin + (markPrice - entryPrice) × size = maintenanceRate × markPrice × size

解出 markPrice:
liquidationPrice = (entryPrice × size - margin) / (size × (1 - maintenanceRate))

空头 (Short) 清算条件: 价格上涨导致亏损:

1
2
3
4
margin + (entryPrice - markPrice) × size = maintenanceRate × markPrice × size

解出 markPrice:
liquidationPrice = (entryPrice × size + margin) / (size × (1 + maintenanceRate))

5.2 破产价格公式

破产价格是 “保证金恰好归零” 的价格. 清算订单以此价格挂单, 成交价与破产价之间的差额归保险基金:

1
2
Long:  bankruptcyPrice = entryPrice - margin / size
Short: bankruptcyPrice = entryPrice + margin / size

5.3 全仓 (Cross) 模式清算判断

全仓模式没有单仓位的清算价格, 清算判断在账户级别:

1
2
3
4
5
6
totalEquity = balance + Σ unrealizedPnL(position_i)   // 账户所有全仓仓位的 PnL 汇总
totalMM = Σ (markPrice_i × size_i × MMR_i) // 账户所有全仓仓位的维持保证金之和

accountMarginRatio = totalEquity / totalMM

清算条件: accountMarginRatio <= 1 (即总权益不足以覆盖总维持保证金)

逐仓 vs 全仓对比:

1
2
3
4
5
6
7
8
9
10
11
12
场景: Alice 账户余额 $10,000, 同时持有:
ETH-USD Long 10 ETH @ $3,000 (浮亏 $2,000) MMR=0.5%
BTC-USD Short 0.5 BTC @ $60,000 (浮盈 $1,500) MMR=0.5%

逐仓模式: 两个仓位独立判断
ETH 仓位保证金率 = (margin - 2000) / notional ← 可能已触发清算
BTC 仓位保证金率 = (margin + 1500) / notional ← 非常健康

全仓模式: 账户级别判断
totalEquity = 10000 + (-2000) + 1500 = $9,500
totalMM = (markPrice_ETH × 10 × 0.005) + (markPrice_BTC × 0.5 × 0.005)
BTC 的盈利 "撑住了" ETH 的亏损 → 账户整体安全

5.4 Go 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// CalcLiquidationPrice 计算清算价格.
//
// Long: (entryPrice * size - margin) / (size * (1 - maintenanceRate))
// Short: (entryPrice * size + margin) / (size * (1 + maintenanceRate))
func CalcLiquidationPrice(pos *Position) decimal.Decimal {
one := decimal.NewFromInt(1)
entryTimesSize := pos.EntryPrice.Mul(pos.Size)

if pos.Side == Long {
numerator := entryTimesSize.Sub(pos.Margin)
denominator := pos.Size.Mul(one.Sub(pos.MaintenanceRate))
return numerator.Div(denominator)
}

// Short
numerator := entryTimesSize.Add(pos.Margin)
denominator := pos.Size.Mul(one.Add(pos.MaintenanceRate))
return numerator.Div(denominator)
}

// CalcBankruptcyPrice 计算破产价格 (保证金归零).
//
// Long: entryPrice - margin / size
// Short: entryPrice + margin / size
func CalcBankruptcyPrice(pos *Position) decimal.Decimal {
marginPerUnit := pos.Margin.Div(pos.Size)
if pos.Side == Long {
return pos.EntryPrice.Sub(marginPerUnit)
}
return pos.EntryPrice.Add(marginPerUnit)
}

// IsPositionLiquidatable 判断逐仓仓位是否应被清算.
// 用 margin <= MM (乘法) 而非 marginRatio <= MMR (除法), 避免精度损失.
func IsPositionLiquidatable(pos *Position, markPrice decimal.Decimal) bool {
currentMargin := pos.Margin.Add(UnrealizedPnL(pos, markPrice))
mm := markPrice.Mul(pos.Size).Mul(pos.MaintenanceRate) // MM = notional × MMR
return !currentMargin.IsPositive() || currentMargin.LessThanOrEqual(mm)
}

// IsAccountLiquidatable 判断全仓账户是否应被清算.
// 用 equity <= totalMM (乘法) 而非 ratio <= 1 (除法).
//
// 全仓模式下, 所有仓位的盈亏共享同一个保证金池:
// - 一个仓位亏 $500, 另一个赚 $300 → 净亏 $200, 从账户余额扣
// - 只要账户总权益 > 总维持保证金, 就不会被清算
// - 触发清算时, 引擎选择该账户下风险最高的仓位优先清算
func IsAccountLiquidatable(account *Account, markPrices map[string]decimal.Decimal) bool {
totalMM := account.TotalMaintenanceMargin(markPrices)
if totalMM.IsZero() {
return false // 无仓位
}
equity := account.TotalEquity(markPrices)
return !equity.IsPositive() || equity.LessThanOrEqual(totalMM)
}

// CalcMarginRatio 计算单仓位保证金率 (用于展示/日志, 非清算判断).
// 清算判断请用 IsPositionLiquidatable (纯乘法, 无精度损失).
func CalcMarginRatio(pos *Position, markPrice decimal.Decimal) decimal.Decimal {
effectiveMargin := pos.Margin.Add(UnrealizedPnL(pos, markPrice))
if !effectiveMargin.IsPositive() {
return decimal.Zero
}
notional := markPrice.Mul(pos.Size)
return effectiveMargin.Div(notional)
}

六、仓位优先队列

全量遍历在仓位多了之后太慢. 更好的做法: 按清算价格排序, 价格更新时只从队列头部开始检查.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ETH 当前价格: $3,000, 正在下跌

Long 优先队列 (按清算价从高到低):
┌──────────────────────────────────────────────────┐
│ Alice liqPrice=$2,980 ← 最先被清算 (离当前价最近) │
│ Bob liqPrice=$2,850 │
│ Carol liqPrice=$2,500 │
... │
└──────────────────────────────────────────────────┘
价格跌到 $2,980 → 扫 Alice → 清算
价格继续跌到 $2,850 → 扫 Bob → 清算
价格反弹到 $2,900 → 扫到 Carol ($2,500) 发现安全 → 停止

Short 优先队列 (按清算价从低到高):
价格上涨时从头部开始扫, 逻辑对称

6.1 Go 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import "github.com/tidwall/btree"

// positionItem 是优先队列中的元素, 按清算价排序.
type positionItem struct {
LiquidationPrice decimal.Decimal
PositionID string
}

// PositionQueue 维护按清算价排序的仓位优先队列.
// 使用 btree.BTreeG 泛型版本, 自定义 less 函数 (decimal.Decimal 不是 cmp.Ordered).
type PositionQueue struct {
tree *btree.BTreeG[positionItem]
}

func NewPositionQueue() *PositionQueue {
return &PositionQueue{
tree: btree.NewBTreeG[positionItem](func(a, b positionItem) bool {
return a.LiquidationPrice.LessThan(b.LiquidationPrice)
}),
}
}

// Insert 插入仓位到优先队列.
func (pq *PositionQueue) Insert(posID string, liqPrice decimal.Decimal) {
pq.tree.Set(positionItem{
LiquidationPrice: liqPrice,
PositionID: posID,
})
}

// Remove 从优先队列移除仓位 (平仓或保证金变更后需要重新插入).
func (pq *PositionQueue) Remove(posID string, liqPrice decimal.Decimal) {
pq.tree.Delete(positionItem{
LiquidationPrice: liqPrice,
PositionID: posID,
})
}

// ScanLongsAt 从最高清算价开始, 返回所有清算价 >= markPrice 的多头仓位 ID.
// 即: 这些仓位在当前 markPrice 下应该被清算.
// Reverse: 从树的最大值开始降序遍历, 无需构造 fake pivot.
func (pq *PositionQueue) ScanLongsAt(markPrice decimal.Decimal) []string {
var result []string
pq.tree.Reverse(func(item positionItem) bool {
if item.LiquidationPrice.GreaterThanOrEqual(markPrice) {
result = append(result, item.PositionID)
return true // 继续
}
return false // 安全了, 停止
})
return result
}

// ScanShortsAt 从最低清算价开始, 返回所有清算价 <= markPrice 的空头仓位 ID.
// Scan: 从树的最小值开始升序遍历.
func (pq *PositionQueue) ScanShortsAt(markPrice decimal.Decimal) []string {
var result []string
pq.tree.Scan(func(item positionItem) bool {
if item.LiquidationPrice.LessThanOrEqual(markPrice) {
result = append(result, item.PositionID)
return true
}
return false
})
return result
}

七、清算引擎主循环

清算触发流程 (Liquidation Trigger Flow) Mark Price 更新 Oracle / 撮合引擎推送 OnPriceUpdate() 清算引擎核心入口 逐仓模式 (Isolated) Long 队列: 从最高清算价扫描 Short 队列: 从最低清算价扫描 liqPrice >= markPrice → 清算 Long liqPrice <= markPrice → 清算 Short 全仓模式 (Cross) 遍历持有该 symbol 的账户 equity <= totalMM → 触发清算 选亏损最大的仓位优先清算 清算后重新检查, 直到恢复健康 部分清算 or 全部清算? 名义价值 < MinSize → 全平 | 否则按 PartialRatio 部分平 Executor.Liquidate(order) Local / gRPC / OnChain → 生成清算订单提交执行 结果处理 盈余 → 保险基金 不足 → 触发 ADL 每次 mark price 更新都会触发此流程, 逐仓 O(k) 只扫描危险仓位, 全仓 O(n) 遍历有风险账户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// LiquidationConfig 清算引擎配置.
type LiquidationConfig struct {
// PartialRatio 部分清算比例, 范围 (0, 1].
// 每次清算平掉仓位的这个比例. 例如 0.25 表示每次清算 25%.
// 部分清算后仓位仍不健康会在下一轮继续清算.
// 设为 1.0 则退化为全部清算.
PartialRatio decimal.Decimal // 建议 0.25 ~ 0.50

// MinPositionSize 最小仓位名义价值 (USD).
// 仓位名义价值低于此阈值时直接全部清算, 不做部分清算.
// 原因: 小仓位拆分清算不划算 (gas 成本 / 撮合开销 > 收益).
MinPositionSize decimal.Decimal // 建议 $500 ~ $1,000
}

// Engine 是清算引擎核心.
// 同时支持逐仓和全仓模式:
// - 逐仓: 通过优先队列按单仓位清算价扫描 (O(log n))
// - 全仓: 遍历有风险的账户, 计算账户级别保证金率
type Engine struct {
config LiquidationConfig
positions map[string]*Position // positionID → Position
accounts map[string]*Account // accountID → Account
longQueue *PositionQueue // 逐仓多头优先队列 (按清算价 DESC)
shortQueue *PositionQueue // 逐仓空头优先队列 (按清算价 ASC)
executor Executor // 清算执行器 (可替换)
insurance *InsuranceFund // 保险基金
adl *ADLEngine // 自动减仓引擎
}

// NewEngine 创建清算引擎.
func NewEngine(config LiquidationConfig, executor Executor, insuranceFund *InsuranceFund) *Engine {
return &Engine{
config: config,
positions: make(map[string]*Position),
accounts: make(map[string]*Account),
longQueue: NewPositionQueue(),
shortQueue: NewPositionQueue(),
executor: executor,
insurance: insuranceFund,
adl: NewADLEngine(),
}
}

// GetOrCreateAccount 获取或创建账户.
func (e *Engine) GetOrCreateAccount(accountID string) *Account {
if acc, ok := e.accounts[accountID]; ok {
return acc
}
acc := &Account{
ID: accountID,
Balance: decimal.Zero,
Positions: make(map[string]*Position),
}
e.accounts[accountID] = acc
return acc
}

// RegisterPosition 注册新仓位到清算引擎.
// 逐仓仓位进入优先队列, 全仓仓位只注册到账户.
func (e *Engine) RegisterPosition(pos *Position) {
e.positions[pos.ID] = pos

// 注册到账户
acc := e.GetOrCreateAccount(pos.Account)
acc.Positions[pos.ID] = pos

if pos.MarginMode == Isolated {
// 逐仓: 预计算清算价, 插入优先队列
pos.LiquidationPrice = CalcLiquidationPrice(pos)
if pos.Side == Long {
e.longQueue.Insert(pos.ID, pos.LiquidationPrice)
} else {
e.shortQueue.Insert(pos.ID, pos.LiquidationPrice)
}
}
// 全仓: 不进队列, 在 OnPriceUpdate 时按账户扫描
}

// RemovePosition 从清算引擎移除仓位 (平仓时调用).
func (e *Engine) RemovePosition(posID string) {
pos, ok := e.positions[posID]
if !ok {
return
}

if pos.MarginMode == Isolated {
if pos.Side == Long {
e.longQueue.Remove(posID, pos.LiquidationPrice)
} else {
e.shortQueue.Remove(posID, pos.LiquidationPrice)
}
}

// 从账户中移除
if acc, ok := e.accounts[pos.Account]; ok {
delete(acc.Positions, posID)
}

delete(e.positions, posID)
}

// OnPriceUpdate 是清算引擎的核心入口.
// 每次 mark price 更新时调用, 分别扫描逐仓和全仓仓位.
func (e *Engine) OnPriceUpdate(ctx context.Context, symbol string, markPrice decimal.Decimal) error {
markPrices := map[string]decimal.Decimal{symbol: markPrice}

// ── 逐仓模式: 优先队列扫描 ──

// 1. 扫描需要清算的多头 (价格下跌, 清算价 >= 当前价)
for _, posID := range e.longQueue.ScanLongsAt(markPrice) {
if err := e.liquidatePosition(ctx, posID, markPrice); err != nil {
return fmt.Errorf("liquidate long %s: %w", posID, err)
}
}

// 2. 扫描需要清算的空头 (价格上涨, 清算价 <= 当前价)
for _, posID := range e.shortQueue.ScanShortsAt(markPrice) {
if err := e.liquidatePosition(ctx, posID, markPrice); err != nil {
return fmt.Errorf("liquidate short %s: %w", posID, err)
}
}

// ── 全仓模式: 账户级别扫描 ──

for _, acc := range e.accounts {
if !e.accountHasCrossPositions(acc, symbol) {
continue
}
if IsAccountLiquidatable(acc, markPrices) {
if err := e.liquidateCrossAccount(ctx, acc, markPrices); err != nil {
return fmt.Errorf("liquidate cross account %s: %w", acc.ID, err)
}
}
}

return nil
}

// accountHasCrossPositions 检查账户是否有指定 symbol 的全仓仓位.
func (e *Engine) accountHasCrossPositions(acc *Account, symbol string) bool {
for _, pos := range acc.Positions {
if pos.MarginMode == Cross && pos.Symbol == symbol {
return true
}
}
return false
}

// liquidateCrossAccount 清算全仓账户: 选择亏损最大的仓位优先清算.
// 每次清算一个仓位后重新检查账户保证金率, 直到恢复健康或无仓位可清算.
func (e *Engine) liquidateCrossAccount(ctx context.Context, acc *Account, markPrices map[string]decimal.Decimal) error {
for {
// 账户恢复健康则停止
if !IsAccountLiquidatable(acc, markPrices) {
return nil
}

// 找亏损最大的全仓仓位
var worst *Position
worstPnL := decimal.Zero
for _, pos := range acc.Positions {
if pos.MarginMode != Cross {
continue
}
mp, ok := markPrices[pos.Symbol]
if !ok {
continue
}
pnl := UnrealizedPnL(pos, mp)
if worst == nil || pnl.LessThan(worstPnL) {
worst = pos
worstPnL = pnl
}
}

if worst == nil {
return nil // 无全仓仓位
}

mp := markPrices[worst.Symbol]
if err := e.liquidatePosition(ctx, worst.ID, mp); err != nil {
return err
}
}
}

// liquidatePosition 执行单个仓位的清算 (逐仓和全仓共用).
// 根据 config 决定部分清算还是全部清算:
// - 仓位名义价值 < MinPositionSize → 全部清算 (小仓位不值得拆分)
// - 否则 → 部分清算 (按 PartialRatio 比例平仓)
// - 部分清算后仍不健康 → 下一轮 OnPriceUpdate 继续清算
func (e *Engine) liquidatePosition(ctx context.Context, posID string, markPrice decimal.Decimal) error {
pos, ok := e.positions[posID]
if !ok {
return nil // 已被清算或平仓
}

// 决定清算数量: 部分 or 全部
liqSize := e.calcLiquidationSize(pos, markPrice)

// 构建清算订单
order := LiquidationOrder{
PositionID: pos.ID,
Account: pos.Account,
Symbol: pos.Symbol,
Side: 1 - pos.Side, // 反向: Long 仓位 → 卖出清算
Size: liqSize,
BankruptcyPrice: CalcBankruptcyPrice(pos),
MarkPrice: markPrice,
Timestamp: time.Now(),
}

// 通过 Executor 执行清算
result, err := e.executor.Liquidate(ctx, order)
if err != nil {
return fmt.Errorf("executor.Liquidate: %w", err)
}

if !result.Executed {
return e.adl.Execute(ctx, pos, e.positions, e.executor)
}

// 处理清算盈余/亏损
e.insurance.Settle(result.Surplus)

// 保险基金为负 → 触发 ADL
if e.insurance.Balance().IsNegative() {
deficit := e.insurance.Balance().Neg()
if err := e.adl.CoverDeficit(ctx, pos.Symbol, pos.Side, deficit, e.positions, e.executor); err != nil {
return fmt.Errorf("ADL cover deficit: %w", err)
}
e.insurance.Reset()
}

isFullLiquidation := liqSize.Equal(pos.Size)

if isFullLiquidation {
e.RemovePosition(posID)
} else {
// 部分清算: 按比例缩减仓位和保证金
e.applyPartialLiquidation(pos, result, markPrice)
}

return nil
}

// calcLiquidationSize 计算本次清算的数量.
func (e *Engine) calcLiquidationSize(pos *Position, markPrice decimal.Decimal) decimal.Decimal {
notional := markPrice.Mul(pos.Size)

// 小仓位 → 全部清算
if notional.LessThanOrEqual(e.config.MinPositionSize) {
return pos.Size
}

// 部分清算: size × partialRatio
liqSize := pos.Size.Mul(e.config.PartialRatio)

// 如果剩余仓位名义价值 < MinPositionSize, 也全部清算
remaining := pos.Size.Sub(liqSize)
if markPrice.Mul(remaining).LessThan(e.config.MinPositionSize) {
return pos.Size
}

return liqSize
}

// applyPartialLiquidation 部分清算后更新仓位状态.
// 按清算比例缩减 size 和 margin, 重新计算清算价格.
func (e *Engine) applyPartialLiquidation(pos *Position, result *LiquidationResult, markPrice decimal.Decimal) {
liqRatio := result.FillSize.Div(pos.Size) // 实际清算比例
remainRatio := decimal.NewFromInt(1).Sub(liqRatio)

// 缩减保证金: 按比例扣除, 并扣除清算亏损 (如有)
// 清算盈余归保险基金 (已在 insurance.Settle 处理), 这里只扣 margin
pos.Margin = pos.Margin.Mul(remainRatio)
pos.Size = pos.Size.Sub(result.FillSize)
pos.UpdatedAt = time.Now()

// 逐仓模式: 重新计算清算价格并更新优先队列
if pos.MarginMode == Isolated {
oldLiqPrice := pos.LiquidationPrice
pos.LiquidationPrice = CalcLiquidationPrice(pos)

if pos.Side == Long {
e.longQueue.Remove(pos.ID, oldLiqPrice)
e.longQueue.Insert(pos.ID, pos.LiquidationPrice)
} else {
e.shortQueue.Remove(pos.ID, oldLiqPrice)
e.shortQueue.Insert(pos.ID, pos.LiquidationPrice)
}
}
}

八、三种 Executor 实现

8.1 LocalExecutor (进程内调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// MatchingEngine 是撮合引擎的接口.
// 如果已有撮合引擎原理与 Go 实现的实现, 直接对接即可.
type MatchingEngine interface {
SubmitOrder(symbol string, side Side, price, size decimal.Decimal) ([]Trade, error)
}

type Trade struct {
Price decimal.Decimal
Size decimal.Decimal
}

// LocalExecutor 在同一进程内直接调用撮合引擎.
type LocalExecutor struct {
engine MatchingEngine
}

func NewLocalExecutor(engine MatchingEngine) *LocalExecutor {
return &LocalExecutor{engine: engine}
}

func (e *LocalExecutor) Liquidate(ctx context.Context, order LiquidationOrder) (*LiquidationResult, error) {
trades, err := e.engine.SubmitOrder(order.Symbol, order.Side, order.BankruptcyPrice, order.Size)
if err != nil {
return nil, err
}

if len(trades) == 0 {
return &LiquidationResult{PositionID: order.PositionID, Executed: false}, nil
}

// 计算加权平均成交价
totalSize := decimal.Zero
totalValue := decimal.Zero
for _, t := range trades {
totalSize = totalSize.Add(t.Size)
totalValue = totalValue.Add(t.Price.Mul(t.Size))
}
avgPrice := totalValue.Div(totalSize)

// 计算盈余/亏损
// Long 清算 (卖出): surplus = (fillPrice - bankruptcyPrice) * size
// Short 清算 (买入): surplus = (bankruptcyPrice - fillPrice) * size
var surplus decimal.Decimal
if order.Side == Short { // 原仓位是 Long, 清算卖出
surplus = avgPrice.Sub(order.BankruptcyPrice).Mul(totalSize)
} else {
surplus = order.BankruptcyPrice.Sub(avgPrice).Mul(totalSize)
}

return &LiquidationResult{
PositionID: order.PositionID,
Executed: true,
FillPrice: avgPrice,
FillSize: totalSize,
Surplus: surplus,
}, nil
}

8.2 GRPCExecutor (远程调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// GRPCExecutor 通过 gRPC 调用远程撮合服务.
type GRPCExecutor struct {
client MatchingServiceClient // protobuf 生成的 client
}

func NewGRPCExecutor(addr string) (*GRPCExecutor, error) {
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("dial matching service: %w", err)
}
return &GRPCExecutor{client: NewMatchingServiceClient(conn)}, nil
}

func (e *GRPCExecutor) Liquidate(ctx context.Context, order LiquidationOrder) (*LiquidationResult, error) {
resp, err := e.client.SubmitLiquidation(ctx, &LiquidationRequest{
PositionId: order.PositionID,
Symbol: order.Symbol,
Side: int32(order.Side),
Size: order.Size.String(),
BankruptcyPrice: order.BankruptcyPrice.String(),
})
if err != nil {
return nil, fmt.Errorf("grpc SubmitLiquidation: %w", err)
}

fillPrice, _ := decimal.NewFromString(resp.FillPrice)
fillSize, _ := decimal.NewFromString(resp.FillSize)
surplus, _ := decimal.NewFromString(resp.Surplus)

return &LiquidationResult{
PositionID: order.PositionID,
Executed: resp.Executed,
FillPrice: fillPrice,
FillSize: fillSize,
Surplus: surplus,
}, nil
}

对应的 protobuf 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";
package matching;

service MatchingService {
rpc SubmitLiquidation(LiquidationRequest) returns (LiquidationResponse);
}

message LiquidationRequest {
string position_id = 1;
string symbol = 2;
int32 side = 3;
string size = 4; // decimal 序列化为字符串
string bankruptcy_price = 5;
}

message LiquidationResponse {
bool executed = 1;
string fill_price = 2;
string fill_size = 3;
string surplus = 4;
}

8.3 OnChainExecutor (链上合约调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// OnChainExecutor 通过发送链上交易调用清算合约.
// 适用于 DeFi 协议, Keeper 模式.
//
// 注意: 链上交互需要 big.Int (EVM uint256), 在边界处转换:
// decimal → big.Int: order.Size.Shift(18).BigInt() (乘以 1e18 转定点数)
// big.Int → decimal: decimal.NewFromBigInt(val, -18)
type OnChainExecutor struct {
client *ethclient.Client
contract *LiquidationContract // abigen 生成的合约绑定
privateKey *ecdsa.PrivateKey
chainID *big.Int
}

func NewOnChainExecutor(rpcURL string, contractAddr common.Address, key *ecdsa.PrivateKey) (*OnChainExecutor, error) {
client, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, fmt.Errorf("dial rpc: %w", err)
}
contract, err := NewLiquidationContract(contractAddr, client)
if err != nil {
return nil, fmt.Errorf("bind contract: %w", err)
}
chainID, err := client.ChainID(context.Background())
if err != nil {
return nil, fmt.Errorf("get chain id: %w", err)
}
return &OnChainExecutor{
client: client, contract: contract,
privateKey: key, chainID: chainID,
}, nil
}

func (e *OnChainExecutor) Liquidate(ctx context.Context, order LiquidationOrder) (*LiquidationResult, error) {
auth, err := bind.NewKeyedTransactorWithChainID(e.privateKey, e.chainID)
if err != nil {
return nil, fmt.Errorf("create transactor: %w", err)
}

tx, err := e.contract.Liquidate(auth, order.Account, order.Symbol)
if err != nil {
return nil, fmt.Errorf("send liquidation tx: %w", err)
}

receipt, err := bind.WaitMined(ctx, e.client, tx)
if err != nil {
return nil, fmt.Errorf("wait tx mined: %w", err)
}

if receipt.Status == 0 {
return &LiquidationResult{PositionID: order.PositionID, Executed: false}, nil
}

result, err := e.parseLiquidationEvent(receipt)
if err != nil {
return nil, fmt.Errorf("parse event: %w", err)
}
return result, nil
}

九、链上清算合约 (Solidity)

链上清算与链下清算的关键区别: 任何人都能调用清算, 不需要信任单一的清算引擎.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title Liquidation - 链上清算合约
/// @notice 任何人都能调用 liquidate() 触发清算, 赚取清算奖励
contract Liquidation {
struct Position {
address account;
bool isLong;
uint256 size; // 仓位数量 (1e18 精度)
uint256 entryPrice; // 开仓均价 (1e6 精度)
uint256 margin; // 当前保证金 (1e6 精度)
}

uint256 public constant MAINTENANCE_RATE = 5e15; // 0.5%, 精度 1e18
uint256 public constant LIQUIDATION_REWARD = 25e14; // 0.25%, 给 Keeper 的奖励
uint256 public constant PRECISION = 1e18;

mapping(bytes32 => Position) public positions;
uint256 public insuranceFund;

IPriceOracle public oracle;

event PositionLiquidated(
bytes32 indexed positionId,
address indexed account,
address indexed liquidator,
uint256 markPrice,
uint256 reward
);

/// @notice 清算指定仓位
/// @param positionId 仓位标识
/// @dev 任何人 (Keeper) 都可以调用, 成功后获得清算奖励
function liquidate(bytes32 positionId) external {
Position storage pos = positions[positionId];
require(pos.size > 0, "position not found");

uint256 markPrice = oracle.getPrice(pos.isLong ? "ETH-USD" : "ETH-USD");

// 检查是否满足清算条件: margin <= MM (纯乘法, 避免除法精度损失)
require(_isLiquidatable(pos, markPrice), "position is healthy");

// 计算清算奖励 (从仓位保证金中扣除, 给 Keeper)
uint256 notional = pos.size * markPrice / PRECISION;
uint256 reward = notional * LIQUIDATION_REWARD / PRECISION;

// 计算破产价格和盈余/亏损
uint256 bankruptcyPrice = _calcBankruptcyPrice(pos);
int256 surplus = _calcSurplus(pos, markPrice, bankruptcyPrice);

// 处理保险基金
if (surplus > 0) {
insuranceFund += uint256(surplus);
} else if (surplus < 0) {
uint256 deficit = uint256(-surplus);
if (insuranceFund >= deficit) {
insuranceFund -= deficit;
} else {
// 保险基金不足, 触发 ADL (此处简化)
insuranceFund = 0;
}
}

// 支付 Keeper 奖励 (实际实现中通过 USDC transfer)
payable(msg.sender).transfer(reward);

delete positions[positionId];
emit PositionLiquidated(positionId, pos.account, msg.sender, markPrice, reward);
}

// 用 margin <= MM (乘法) 判断, 而非 marginRatio <= MMR (除法)
function _isLiquidatable(Position storage pos, uint256 markPrice)
internal view returns (bool)
{
int256 pnl;
if (pos.isLong) {
pnl = int256(markPrice) - int256(pos.entryPrice);
} else {
pnl = int256(pos.entryPrice) - int256(markPrice);
}
pnl = pnl * int256(pos.size) / int256(PRECISION);

int256 currentMargin = int256(pos.margin) + pnl;
if (currentMargin <= 0) return true; // 已破产

// MM = notional × MMR = size × markPrice × MAINTENANCE_RATE / PRECISION^2
uint256 mm = pos.size * markPrice / PRECISION * MAINTENANCE_RATE / PRECISION;
return uint256(currentMargin) <= mm;
}

function _calcBankruptcyPrice(Position storage pos)
internal view returns (uint256)
{
uint256 marginPerUnit = pos.margin * PRECISION / pos.size;
if (pos.isLong) {
return pos.entryPrice > marginPerUnit ? pos.entryPrice - marginPerUnit : 0;
} else {
return pos.entryPrice + marginPerUnit;
}
}

function _calcSurplus(Position storage pos, uint256 markPrice, uint256 bankruptcyPrice)
internal view returns (int256)
{
int256 diff;
if (pos.isLong) {
diff = int256(markPrice) - int256(bankruptcyPrice);
} else {
diff = int256(bankruptcyPrice) - int256(markPrice);
}
return diff * int256(pos.size) / int256(PRECISION);
}
}

interface IPriceOracle {
function getPrice(string calldata symbol) external view returns (uint256);
}

9.1 链上 vs 链下清算对比

维度 链下清算 (Go 服务) 链上清算 (Solidity)
谁能触发 仅清算引擎服务 任何人 (Keeper)
信任模型 信任运营方 信任代码 (无需许可)
Keeper 激励 不需要 需要 (否则没人调用)
延迟 μs ~ ms 1~12s (出块)
Gas 成本 每次清算消耗 gas
适用场景 CEX, L2 永续 DeFi 协议 (GMX, Aave)

十、保险基金与 ADL

10.1 保险基金

保险基金流转 (Insurance Fund Flow) 资金来源 (Inflow) 清算盈余 (fillPrice > bankruptcyPrice) 清算罚金 (liquidation fee) 平台注入 (初始资金 / 手续费分成) 多数清算有盈余 → 基金持续增长 保险基金 Insurance Fund Settle(surplus) surplus > 0: 存入 | surplus < 0: 扣除 资金去向 (Outflow) 清算亏损 (fillPrice < bankruptcyPrice) 对手方不足时兜底接盘 基金耗尽 → 触发 ADL 极端行情下可能被耗尽 保险基金生命周期示例 初始: $100K 清算 A: +$500 清算 B: +$200 清算 C: -$800 = $99.9K 正常市况下清算盈余 > 亏损, 基金缓慢增长; 极端行情 (如 LUNA 崩盘) 可能快速耗尽 Binance 保险基金 ~$1B | dYdX ~$13M | Hyperliquid ~$60M (2024 数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// InsuranceFund 管理清算盈余和亏损.
type InsuranceFund struct {
balance decimal.Decimal
mu sync.RWMutex
}

func NewInsuranceFund(initial decimal.Decimal) *InsuranceFund {
return &InsuranceFund{balance: initial}
}

// Settle 结算清算盈余或亏损.
// surplus > 0: 清算盈余, 归入保险基金
// surplus < 0: 清算亏损, 从保险基金扣除
func (f *InsuranceFund) Settle(surplus decimal.Decimal) {
f.mu.Lock()
defer f.mu.Unlock()
f.balance = f.balance.Add(surplus)
}

func (f *InsuranceFund) Balance() decimal.Decimal {
f.mu.RLock()
defer f.mu.RUnlock()
return f.balance
}

func (f *InsuranceFund) Reset() {
f.mu.Lock()
defer f.mu.Unlock()
f.balance = decimal.Zero
}

10.2 ADL (自动减仓)

当保险基金不足以覆盖清算亏损时, 系统需要从盈利方的仓位中 “借” 来覆盖. ADL 的核心是排序: 选谁来被减仓?

ADL 自动减仓触发链路 (Auto-Deleveraging Flow) 清算执行 Executor.Liquidate() 结果检查 surplus < 0 ? 保险基金余额 < 0 ? 基金充足 → 正常结算 保险基金不足! deficit = |balance| 需要 ADL 覆盖亏损 ADL 引擎 CoverDeficit() 强制减仓盈利方 ADL 排序: 选谁被减仓? 优先级 = 盈利百分比 x 有效杠杆 (uPnL / margin) x (notional / (margin + uPnL)) 排序示例 (对手方向盈利仓位) Dave: +150% PnL, 20x 杠杆 → 优先级 30 Eve: +80% PnL, 10x 杠杆 → 优先级 8 Frank: +20% PnL, 3x 杠杆 → 优先级 0.6 ADL 执行过程 1. 从优先级最高的开始减仓 2. 按破产价格强制平仓 (盈利方损失部分未实现利润) 3. 逐个减仓直到 deficit 被覆盖 ADL 是最后手段: 它牺牲了盈利用户的利益来维护系统偿付能力, 因此各平台都尽力避免触发 ADL

排序公式 (与 Binance, Hyperliquid 类似):

1
2
3
4
5
ADL 优先级 = 盈利百分比 × 有效杠杆

其中:
盈利百分比 = unrealizedPnL / margin
有效杠杆 = notionalValue / (margin + unrealizedPnL)

直觉: 赚得越多 + 杠杆越高的仓位, 被 ADL 的优先级越高. 因为这些仓位从市场失衡中获益最多, 且高杠杆本身就是风险来源.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// ADLEngine 自动减仓引擎.
type ADLEngine struct{}

func NewADLEngine() *ADLEngine { return &ADLEngine{} }

// adlCandidate 是 ADL 候选仓位.
type adlCandidate struct {
Position *Position
Priority decimal.Decimal // 越大越优先被减仓
}

// CoverDeficit 通过 ADL 覆盖保险基金亏损.
// 选择对手方向的盈利仓位, 按优先级从高到低减仓, 直到覆盖 deficit.
func (a *ADLEngine) CoverDeficit(
ctx context.Context,
symbol string,
liquidatedSide Side,
deficit decimal.Decimal,
positions map[string]*Position,
executor Executor,
) error {
targetSide := 1 - liquidatedSide

var candidates []adlCandidate
for _, pos := range positions {
if pos.Symbol != symbol || pos.Side != targetSide {
continue
}
priority := a.calcPriority(pos)
if priority.IsPositive() {
candidates = append(candidates, adlCandidate{Position: pos, Priority: priority})
}
}

sort.Slice(candidates, func(i, j int) bool {
return candidates[i].Priority.GreaterThan(candidates[j].Priority)
})

remaining := deficit
for _, c := range candidates {
if !remaining.IsPositive() {
break
}
order := LiquidationOrder{
PositionID: c.Position.ID,
Account: c.Position.Account,
Symbol: symbol,
Side: 1 - c.Position.Side,
Size: c.Position.Size,
MarkPrice: c.Position.EntryPrice,
Timestamp: time.Now(),
}
if _, err := executor.Liquidate(ctx, order); err != nil {
return fmt.Errorf("ADL position %s: %w", c.Position.ID, err)
}
remaining = remaining.Sub(c.Position.Margin)
}
return nil
}

// calcPriority 计算 ADL 优先级.
// priority = profitPercent * effectiveLeverage
func (a *ADLEngine) calcPriority(pos *Position) decimal.Decimal {
// 实际生产中需要传入 markPrice 计算 unrealizedPnL
// 此处为 placeholder
return decimal.Zero
}

// Execute 在清算订单无法成交时触发 ADL.
func (a *ADLEngine) Execute(ctx context.Context, pos *Position, positions map[string]*Position, executor Executor) error {
return a.CoverDeficit(ctx, pos.Symbol, pos.Side, pos.Margin, positions, executor)
}

十一、完整 Demo

用 LocalExecutor 模拟一个完整的清算流程: 创建仓位, 价格波动, 触发清算.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package main

import (
"context"
"fmt"

"github.com/shopspring/decimal"
)

func main() {
// === 初始化 ===
insuranceFund := NewInsuranceFund(decimal.NewFromInt(100_000)) // $100,000
mockEngine := &MockMatchingEngine{}
executor := NewLocalExecutor(mockEngine)

config := LiquidationConfig{
PartialRatio: decimal.NewFromFloat(0.25), // 每次清算 25%
MinPositionSize: decimal.NewFromInt(500), // 名义价值 < $500 时全部清算
}
engine := NewEngine(config, executor, insuranceFund)

// === 注册仓位 ===
// Alice: $300 保证金, 10x 杠杆做多 ETH @ $3,000
// 名义价值 = $300 × 10 = $3,000, 数量 = 1 ETH
// ── 逐仓仓位 ──

alice := &Position{
ID: "pos-alice",
Account: "alice",
Symbol: "ETH-USD",
Side: Long,
Size: decimal.NewFromInt(1), // 1 ETH
EntryPrice: decimal.NewFromInt(3000), // $3,000
Margin: decimal.NewFromInt(300), // $300
MaintenanceRate: decimal.NewFromFloat(0.005), // 0.5%
MarginMode: Isolated,
}
engine.RegisterPosition(alice)
fmt.Printf("Alice (逐仓) 清算价格: $%s\n", alice.LiquidationPrice.StringFixed(2))

bob := &Position{
ID: "pos-bob",
Account: "bob",
Symbol: "ETH-USD",
Side: Short,
Size: decimal.NewFromInt(1),
EntryPrice: decimal.NewFromInt(3000),
Margin: decimal.NewFromInt(150),
MaintenanceRate: decimal.NewFromFloat(0.005),
MarginMode: Isolated,
}
engine.RegisterPosition(bob)
fmt.Printf("Bob (逐仓) 清算价格: $%s\n", bob.LiquidationPrice.StringFixed(2))

// ── 全仓仓位: Carol 同时做多 ETH 和做空 BTC ──

// 先给 Carol 账户充值
carolAcc := engine.GetOrCreateAccount("carol")
carolAcc.Balance = decimal.NewFromInt(10000) // $10,000 可用余额

carolETH := &Position{
ID: "pos-carol-eth",
Account: "carol",
Symbol: "ETH-USD",
Side: Long,
Size: decimal.NewFromInt(10), // 10 ETH
EntryPrice: decimal.NewFromInt(3000), // $3,000
Margin: decimal.NewFromInt(3000), // $3,000 (10x)
MaintenanceRate: decimal.NewFromFloat(0.005),
MarginMode: Cross,
}
engine.RegisterPosition(carolETH)
fmt.Printf("Carol (全仓) ETH Long 10 @ $3,000\n")

carolBTC := &Position{
ID: "pos-carol-btc",
Account: "carol",
Symbol: "BTC-USD",
Side: Short,
Size: decimal.RequireFromString("0.5"), // 0.5 BTC
EntryPrice: decimal.NewFromInt(60000), // $60,000
Margin: decimal.NewFromInt(3000), // $3,000 (10x)
MaintenanceRate: decimal.NewFromFloat(0.005),
MarginMode: Cross,
}
engine.RegisterPosition(carolBTC)
fmt.Printf("Carol (全仓) BTC Short 0.5 @ $60,000\n")

// === 模拟价格波动 ===
ctx := context.Background()
prices := []int64{3000, 2900, 2800, 2750, 2710, 2700}

for _, p := range prices {
markPrice := decimal.NewFromInt(p)
fmt.Printf("\n--- Mark Price: $%d ---\n", p)

if err := engine.OnPriceUpdate(ctx, "ETH-USD", markPrice); err != nil {
fmt.Printf("Error: %v\n", err)
}

fmt.Printf("保险基金余额: $%s\n", insuranceFund.Balance().StringFixed(2))
fmt.Printf("活跃仓位数: %d\n", len(engine.positions))

// 打印 Carol 全仓状态
if len(carolAcc.Positions) > 0 {
// 假设 BTC 价格同步小幅波动 (简化: 固定 $59,000)
carolPrices := map[string]decimal.Decimal{
"ETH-USD": markPrice,
"BTC-USD": decimal.NewFromInt(59000), // BTC 微跌, Carol 空头盈利
}
ratio := CalcAccountMarginRatio(carolAcc, carolPrices)
equity := carolAcc.TotalEquity(carolPrices)
fmt.Printf("Carol 全仓权益: $%s, 保证金率: %s\n",
equity.StringFixed(2), ratio.StringFixed(4))
}
}
}

// MockMatchingEngine 模拟撮合引擎.
type MockMatchingEngine struct{}

func (m *MockMatchingEngine) SubmitOrder(symbol string, side Side, price, size decimal.Decimal) ([]Trade, error) {
fmt.Printf(" [撮合] 清算订单: %s %s %s @ $%s\n",
symbol,
map[Side]string{Long: "BUY", Short: "SELL"}[side],
size.String(),
price.StringFixed(2),
)
return []Trade{{Price: price, Size: size}}, nil
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Alice 清算价格: $2714.57
Bob 清算价格: $3149.25

--- Mark Price: $3000 ---
保险基金余额: $100000.00
活跃仓位数: 2

--- Mark Price: $2900 ---
保险基金余额: $100000.00
活跃仓位数: 2

--- Mark Price: $2800 ---
保险基金余额: $100000.00
活跃仓位数: 2

--- Mark Price: $2750 ---
保险基金余额: $100000.00
活跃仓位数: 2

--- Mark Price: $2710 ---
[撮合] 清算订单: ETH-USD SELL 1 @ $2700.00
保险基金余额: $100010.00
活跃仓位数: 1

--- Mark Price: $2700 ---
保险基金余额: $100010.00
活跃仓位数: 1

十二、小结

12.1 级联清算与死亡螺旋

级联清算 / 死亡螺旋 (Cascading Liquidation / Death Spiral) 1. 市场突发利空 → 价格暴跌 如: LUNA 崩盘, FTX 暴雷, 黑天鹅事件 2. 大量多头仓位触发清算 优先队列扫描 → 批量生成清算订单 3. 清算订单 = 大量卖单涌入 订单簿买方深度不足, 滑点飙升 4. 卖压推动价格进一步下跌! 清算导致的卖出 → 价格下跌 → 更多清算 恶性 循环 5. 终局: 保险基金耗尽 → 触发 ADL → 盈利方被强制减仓 系统偿付能力受损, 用户信心崩溃, 进一步加剧抛售 防御措施 部分清算 (减缓卖压) 充足保险基金 (兜底) 动态维持保证金率 价格熔断 / 限仓 历史案例 2020.03 BTC $8K→$3.8K (BitMEX) 2022.05 LUNA→$0 (级联清算) 2024.08 ETH -20% (Aave/dYdX ADL)

12.2 核心要点

要点 说明
清算引擎是主动组件 每次价格更新触发扫描, 不等用户操作
Executor 接口分离判断和执行 同一套扫描逻辑适配进程内/gRPC/链上三种模式
优先队列避免全量扫描 按清算价排序, 从危险端开始扫, 遇到安全仓位就停
部分清算优于全部清算 可配置比例 (25%~50%), 小仓位直接全平, 大仓位分批减仓
逐仓/全仓双模式 逐仓按仓位判断, 全仓按账户判断 (盈利仓可救亏损仓)
保险基金 + ADL 是最后防线 清算盈余喂保险基金, 不足时 ADL 减仓盈利方
链上清算靠 Keeper 激励 清算奖励 (仓位名义值的 0.25%) 驱动链下机器人竞争清算

12.3 延伸阅读


永续合约 10 - 清算引擎架构与实现
https://mritd.com/2025/11/25/perp-liquidation-engine/
作者
Kovacs
发布于
2025年11月25日
许可协议