← 返回文章列表
July 18, 2026
5 分鐘閱讀

面向量化的 Uniswap v3:從第一性原理理解集中流動性與 tick 數學

面向量化的 Uniswap v3:從第一性原理理解集中流動性與 tick 數學
#Uniswap
#DeFi
#AMM
#集中流動性
#做市
#LVR
#量化
#以太坊

Uniswap v3 上的一個 LP 頭寸,本質上是偽裝成區間訂單的限價訂單簿掛單。你在兩個價格之間存入流動性,就等於承諾:價格向下穿越你的區間時買入資產,價格向上穿越時賣出資產——這正是 CLOB 上一組靜止的限價掛單所做的事情。但合約並不儲存價格、數量或訂單簿。它只儲存三個數字:一個 Q64.96 定點格式的平方根價格、一個整數 tick 索引,以及一個聚合流動性值 LL。如果你想像分析做市模型中的報價那樣去分析一個 v3 頭寸——庫存、成交價、逆向選擇——你就必須能夠在這些鏈上原語與它們所編碼的交易概念之間來回轉換。本文將從第一性原理構建這套轉換,遵循 Uniswap v3 白皮書(Adams、Zinsmeister、Salem、Keefer、Robinson,2021)與核心合約,並落腳於如今每一場嚴肅的 LP 討論都會繞到的起點:損失對再平衡(loss-versus-rebalancing)。

從 x·y = k 到虛擬儲備

Uniswap v2 是恆定乘積做市商:一個資金池持有 token0 的儲備 xx 和 token1 的儲備 yy,並在每一次交換時強制滿足

xy=kx \cdot y = k

以 token1 計價的 token0 邊際價格為 P=y/xP = y/x,而流動性被均勻鋪在整條價格軸 (0,)(0, \infty) 上。這在資本效率上是極度浪費的:一個只在 0.999 到 1.001 之間交易的穩定幣交易對,會把超過 99% 的資本擺在永遠不會成交的價格上。

v3 的做法是讓每個 LP 把自己的資本限制在一個區間 [pa,pb][p_a, p_b] 內。在區間之內,該頭寸的行為必須與一個 v2 資金池完全一致——相同的繫結曲線、相同的邊際定價——但只使用覆蓋該區間所需的那部分儲備。白皮書用虛擬儲備將這一點形式化:頭寸表現得彷彿持有 v2 風格的儲備 (xv,yv)(x_v, y_v),落在曲線 xvyv=L2x_v \cdot y_v = L^2 上,而真實儲備則是虛擬儲備減去該頭寸在區間邊界處會持有的那部分:

(x+Lpb)(y+Lpa)=L2\left(x + \frac{L}{\sqrt{p_b}}\right)\left(y + L\sqrt{p_a}\right) = L^2

這就是白皮書的 2.2 式,也是 v3 中最重要的一個方程。經過平移的曲線與座標軸相切:在 P=pbP = p_b 處真實 xx 儲備歸零(頭寸 100% 為 token1),在 P=paP = p_a 處真實 yy 儲備歸零(100% 為 token0)。在區間之外頭寸進入惰性狀態——變成某一種代幣的固定持倉,不再賺取任何收益。

Uniswap v2 恆定乘積曲線與展示虛擬儲備和真實儲備的 v3 平移曲線的對比

參數 LL 被稱為流動性,是取代 kk 的不變量。它被定義為 L=kL = \sqrt{k},並且白皮書給出了一個清晰明確的解釋:流動性即"每單位平方根價格所對應的虛擬儲備"。在區間內的任意價格 PP 處,虛擬儲備為

xv=LP,yv=LPx_v = \frac{L}{\sqrt{P}}, \qquad y_v = L\sqrt{P}

為什麼狀態變數是 √P

v3 的核心並不追蹤價格,而是追蹤 P\sqrt{P},以 sqrtPriceX96 儲存,這是一個無符號的 Q64.96 定點數:

sqrtPriceX96=P296\texttt{sqrtPriceX96} = \sqrt{P} \cdot 2^{96}

這裡的 PP原始價格:每單位 token0 基礎單位對應多少 token1 基礎單位,包含小數位在內(下文詳述)。之所以取平方根,並不是為了省 gas,而是出於代數上的必然。對虛擬儲備恆等式求微分,就得到兩條基本的交換方程,它們實現在 SqrtPriceMath 庫中:

Δy=LΔP,Δx=LΔ ⁣(1P)\Delta y = L \cdot \Delta\sqrt{P}, \qquad \Delta x = L \cdot \Delta\!\left(\frac{1}{\sqrt{P}}\right)

兩種代幣的增量都是平方根價格(或其倒數)的線性函式,LL 作為比例常數。因此,一次落在單個 tick 內的交換既不需要對曲線求逆,也不需要牛頓迭代——只需一次乘法來移動 P\sqrt{P},再兩次乘法來計算數量。當一次交換足夠大,把價格推過某個已初始化的 tick 時,資金池會跨越它,加上或減去該處引用的淨流動性(liquidityNet),然後以新的聚合 LL 繼續。從全域看,資金池是一個分段恆定乘積的 AMM:在相鄰的已初始化 tick 之間 LL 為常數,而在 tick 處發生跳變。

對做市商而言,這才是正確的心智模型:資金池的聚合 L(P)L(P) 曲線就是訂單簿深度在 DEX 上的等價物。CLOB 深度以每個價位上的單位數量報出,而 AMM 深度是每個 tick 上的 LL——兩者之間的換算正是上面那個 Δy=LΔP\Delta y = L\Delta\sqrt{P} 恆等式。

Tick:一張對數間隔的價格網格

區間不能從任意價格開始或結束;它們必須對齊到 tick。tick ii 對應價格

p(i)=1.0001ip(i)=1.0001i/2p(i) = 1.0001^i \quad\Longrightarrow\quad \sqrt{p(i)} = 1.0001^{i/2}

因此每個 tick 與其相鄰 tick 相差一個基點——不是絕對意義上的,而是相對意義上的。這種對數間隔是刻意設計的:一張固定加法網格對於一個交易價格為 $0.0001 的代幣而言會粗糙得離譜,對於價格為 $100{,}000 的代幣又會精細得離譜;而幾何網格在任何價格量級上都給出統一的 1 bp 解析度。TickMath 庫支援雙向轉換——getSqrtRatioAtTickgetTickAtSqrtRatio——它在預計算常量上做位運算,而不是去呼叫指數函式。tick 索引受限於 MIN_TICK = -887272MAX_TICK = 887272,覆蓋了價格區間 [2128,2128][2^{-128}, 2^{128}]:對任何能存在於 uint256 運算中的代幣對而言都足夠寬。

並非每個 tick 都可用。每個費率檔都規定了一個 tick 間隔,頭寸只能使用能被它整除的 tick:

費率檔 tick 間隔 最小區間寬度 典型用途
0.01% 1 ~1 bp 穩定幣/穩定幣
0.05% 10 ~10 bp 穩定幣對、ETH/穩定幣
0.30% 60 ~60 bp 主流幣
1.00% 200 ~2% 小眾幣/高波動幣

0.05%/0.30%/1% 三檔在 2021 年 5 月上線時就已存在;0.01% 檔由治理投票於 2021 年 11 月新增。高費率池採用更粗的間隔,能壓低潛在可跨越 tick 的數量,並把交換 gas 成本控制在有限範圍內。

有一個小數位陷阱,每個人都會被坑一次:鏈上的原始價格是以基礎單位計的每 token0 對應多少 token1。在主網的 USDC/WETH 池中,token0 是 USDC(6 位小數),token1 是 WETH(18 位小數),因此人類可讀的每 ETH $3{,}000 對應的原始價格是 1012/30003.333×10810^{12}/3000 \approx 3.333 \times 10^{8} WETH-wei 每 USDC-單位,落在 tick

i=log1.0001 ⁣(3.333×108)=196,256i = \left\lfloor \log_{1.0001}\!\left(3.333\times10^{8}\right) \right\rfloor = 196{,}256

對應 sqrtPriceX961.4465×1033\texttt{sqrtPriceX96} \approx 1.4465 \times 10^{33}。如果你的監控面板上 USDC/WETH 的 tick 顯示在 196k 附近,你現在就知道原因了——而且人類可讀價格是 (sqrtPriceX96/296)210dec0dec1\left(\texttt{sqrtPriceX96}/2^{96}\right)^2 \cdot 10^{\,\text{dec}_0 - \text{dec}_1},視需要取倒數。

從 (L, 價格區間) 求代幣數量:精確公式

本節的核心交付物:給定一個在 [pa,pb][p_a, p_b] 上、流動性為 LL 的頭寸和當前價格 PP,它持有什麼?把交換方程在區間上積分,得到三種情形:

價格在區間之下PpaP \le p_a)——頭寸 100% 為 token0:

x=L(1pa1pb),y=0x = L\left(\frac{1}{\sqrt{p_a}} - \frac{1}{\sqrt{p_b}}\right), \qquad y = 0

價格在區間之上PpbP \ge p_b)——頭寸 100% 為 token1:

x=0,y=L(pbpa)x = 0, \qquad y = L\left(\sqrt{p_b} - \sqrt{p_a}\right)

價格在區間之內pa<P<pbp_a < P < p_b):

x=L(1P1pb),y=L(Ppa)x = L\left(\frac{1}{\sqrt{P}} - \frac{1}{\sqrt{p_b}}\right), \qquad y = L\left(\sqrt{P} - \sqrt{p_a}\right)

這正是 SqrtPriceMath.getAmount0DeltagetAmount1Delta 所計算的內容(帶有定向取整——合約始終朝不利於使用者的方向取整,如果你在回測器裡復現這一邏輯並對 wei 級別的差異感到困惑,這個細節就很重要)。

from math import sqrt

def position_amounts(L: float, pa: float, pb: float, P: float):
    """Token amounts held by a v3 position (float model; core uses Q96 ints)."""
    sa, sb, sp = sqrt(pa), sqrt(pb), sqrt(P)
    if P <= pa:
        return L * (1/sa - 1/sb), 0.0
    if P >= pb:
        return 0.0, L * (sb - sa)
    return L * (1/sp - 1/sb), L * (sp - sa)

完整算例:ETH/USDC,區間 [2500, 3500]

假設 ETH 的交易價格為 P=3000P = 3000 USDC,你想把 1 ETH 存入區間 [2500,3500][2500, 3500]。平方根:2500=50\sqrt{2500} = 503000=54.7723\sqrt{3000} = 54.77233500=59.1608\sqrt{3500} = 59.1608

ETH 這一腿確定了 LL

L=xPpbpbP=1×54.7723×59.160859.160854.7723=738.37L = \frac{x \cdot \sqrt{P}\,\sqrt{p_b}}{\sqrt{p_b} - \sqrt{P}} = \frac{1 \times 54.7723 \times 59.1608}{59.1608 - 54.7723} = 738.37

USDC 這一腿隨之得出:

y=L(Ppa)=738.37×(54.772350)=3,523.69 USDCy = L\left(\sqrt{P} - \sqrt{p_a}\right) = 738.37 \times (54.7723 - 50) = 3{,}523.69 \text{ USDC}

於是鑄造這個頭寸需要 1 ETH + 3,523.69 USDC,總價值 $6,523.69,注意兩條腿並非 50/50——其分配取決於 PP 落在區間內的位置(一個不對稱區間恰恰是你表達方向性觀點、或用單一代幣下一個純"區間訂單"的方式)。

現在把價格推向兩端邊界:

  • P=2500P = 2500頭寸已完全轉換為 ETH:x=738.37×(1/501/59.1608)=2.2867x = 738.37 \times (1/50 - 1/59.1608) = 2.2867 ETH,價值 $5,716.68。你在下跌途中以流動性加權平均價 3523.69 / 1.2867 \approx \2{,}739$ 買入了 1.2867 ETH。
  • P=3500P = 3500它已完全轉換為 USDC:y=738.37×(59.160850)=6,764.06y = 738.37 \times (59.1608 - 50) = 6{,}764.06 USDC。你在上漲途中以平均 $3,240 的價格賣出了你的 ETH。

這就把區間訂單的解讀落到了實處:該頭寸就是一梯從 3000 向下到 2500 的買單、以及從 3000 向上到 3500 的賣單,每個 tick 上的規模與 LL 成正比。而你所獲得報酬的來源正是"集中"本身:一個用同樣 $6,523.69 資本鋪滿全區間的 v2 風格頭寸,其 Lv2=V/(2P)=59.55L_{v2} = V/(2\sqrt{P}) = 59.55——集中頭寸每個 tick 報出的深度多出 12.4 倍,並且(在區間內時)單位資本賺取費用的速率也是 12.4 倍。

費用記帳:feeGrowthGlobal 與 feeGrowthInside

v3 的費用不會複利進頭寸內(這是相對 v2 的刻意改變,v2 中費用會再投資進 k\sqrt{k})。它們按代幣分別、並行地累積,而這套記帳是 O(1) 簿記的一件小小的傑作,值得理解,因為每一條 LP 分析流水線都會重新實現它。

資金池維護兩個全域累加器,feeGrowthGlobal0X128feeGrowthGlobal1X128:自資金池建立以來每單位流動性累計的費用,以 Q128.128 定點格式儲存。每次交換時,費用金額(從輸入代幣中收取)會除以當前在區間內LL,然後加進累加器。這正是 v3 經濟學那條鐵律背後的機制:費用只累積給交換發生那一刻處於區間內的流動性。區間外的頭寸不只是惰性庫存——它們在未啟用期間賺取的費用恰好為零。

為了把全域增長中正確的那一片歸屬給一個有限區間,每個已初始化的 tick ii 儲存 feeGrowthOutside0/1X128——相對於當前價格發生在該 tick 另一側的費用增長(每次跨越該 tick,這個值的含義都會翻轉,這正是這套方案能做到 O(1) 的原因)。那麼對於一個在 [i,iu][i_\ell, i_u] 上、當前 tick 為 ici_c 的頭寸:

fbelow(i)={fo(i)icifgfo(i)ic<ifabove(iu)={fo(iu)ic<iufgfo(iu)iciuf_{\text{below}}(i_\ell) = \begin{cases} f_o(i_\ell) & i_c \ge i_\ell \\ f_g - f_o(i_\ell) & i_c < i_\ell \end{cases} \qquad f_{\text{above}}(i_u) = \begin{cases} f_o(i_u) & i_c < i_u \\ f_g - f_o(i_u) & i_c \ge i_u \end{cases} finside=fgfbelow(i)fabove(iu)f_{\text{inside}} = f_g - f_{\text{below}}(i_\ell) - f_{\text{above}}(i_u)

每個頭寸都儲存一份快照 feeGrowthInsideLast,而未領取的費用就是

fees owed=Lfinsidenowfinsidelast2128\text{fees owed} = L \cdot \frac{f_{\text{inside}}^{\text{now}} - f_{\text{inside}}^{\text{last}}}{2^{128}}

在頭寸每次被觸碰時惰性更新。這帶來兩個實際後果。第一,feeGrowthInside 的增量是衡量一個頭寸費用收入唯一誠實的辦法——每當價格在你的區間邊界附近遊走時,對資金池層面的成交量取樣、再按你佔 TVL 的份額去按比例分攤,都會算錯。第二,由於費用是以 tokensOwed 的形式擱置而非複利,已實現的 LP 收益會帶有一個 v2 所沒有的現金拖累項;自動複利金庫存在的意義恰恰就是把這一點套利掉(減去它們自己收取的費用)。

feeGrowthGlobal、區間邊界 tick 上的 feeGrowthOutside,以及 feeGrowthInside 相減過程的示意圖

損益結構:一份你(也許)能收費持有的賣出跨式

在區間內,頭寸價值作為價格的函式為

V(P)=x(P)P+y(P)=L(2PpaPpb)V(P) = x(P) \cdot P + y(P) = L\left(2\sqrt{P} - \sqrt{p_a} - \frac{P}{\sqrt{p_b}}\right)

關於 PP 是凹的——2LP2L\sqrt{P} 這一項就是全部要害。在區間之外它變為線性:下方斜率為 x(pa)x(p_a)(你是一袋固定 ETH 的多頭),上方斜率為 0(你在 USDC 中處於平倉狀態)。把我們的算例在整個區間上跑一遍,並與單純持有所存入的代幣做對比:

PP(USDC/ETH) LP 價值 HODL 價值 偏離
2500 5,716.68 6,023.69 −307.02
2750 6,200.4 6,273.69 −73.3
3000 6,523.69 6,523.69 0
3250 6,706.3 6,773.69 −67.4
3500 6,764.06 7,023.69 −259.63

LP 在兩個方向上都跑輸 HODL,只有在鑄造價格處才與之持平。上行封頂、下行放大參與、在執行價處相對價值最大……這正是一份賣出跨式的損益結構(更精確地說,一旦把線性尾部相對 HODL 基準做淨化處理,就是對一條跨越 [pa,pb][p_a, p_b] 執行價的期權帶的空頭)。費用流就是權利金。收窄區間就是選擇更緊的執行價:在區間內時單位時間的權利金更多,但價格一旦移動,偏離來得更快也更深。每個 v3 LP 都是一名賣出波動率的交易者,無論他們是否主動選擇如此——這正是 Avellaneda–Stoikov 為訂單簿做市商形式化的庫存風險權衡的鏈上孿生兄弟,其中區間寬度扮演著報價價差的角色。

LP 頭寸價值與 HODL 在整個價格區間上的對比,展示出類似賣出跨式的凹形損益結構

那張表裡那道缺口的傳統名稱是無常(偏離)損失,但 IL-對-HODL 是一個有缺陷的基準:它把你作為流動性提供者承擔的損失,和你本來無論如何都要揹負的市場風險混為一談。更鋒利的分解來自 Milionis、Moallemi、Roughgarden 與 Zhang(2022)的"自動做市與損失對再平衡"(arXiv:2208.06046)。不要拿 LP 去對標 HODL,而要對標一個再平衡組合:它在每一瞬間都持有與資金池相同的代幣數量——但按無摩擦的外部市場價格成交,而不是與套利者對手成交。二者之差就是 LVR(讀作"lever"):LP 損益中純屬逆向選擇的那一部分,是在每次價格移動後付給那些狙擊 AMM 陳舊報價的套利者的。在波動率為 σ\sigma 的幾何布朗價格下,LVR 以如下瞬時速率累積

(σ,P)=σ2P22x(P)\ell(\sigma, P) = \frac{\sigma^2 P^2}{2}\,|x'(P)|

——用作者們的話說,這是一條"AMM 版的 Black–Scholes 公式",其中 x(P)|x'(P)| 是資金池在當前價格處的邊際深度。對於全區間恆定乘積池,它坍縮為那個著名的每單位時間池價值的 σ28\frac{\sigma^2}{8}:在 5% 的日波動率下,資金池每天大約有 3.1 bp 流失給套利者,無論有沒有費用都是如此。集中會放大 x(P)|x'(P)|——我們上面那個 12.4 倍深度的例子,在區間內時同樣是一臺約 12.4 倍的 LVR 機器。費用必須跑贏這道流血,頭寸才是正期望值;而 LVR(不同於"IL")是一項可對沖、可預測的執行成本,這正是它成為正確記帳單位的原因。完整的盈利性演算——費用 APR 對 LVR 對已實現波動率、以及在什麼條件下做 LP 優於 delta 對沖的空頭期權——是即將釋出的無常損失與 LVR 深度剖析的主題,而對沖機制本身則在 v3 LP 策略與對沖中單獨展開。

還有一層成本潛伏在再下一層:因為 AMM 的報價只有在有人交易時才更新,每個 LP 頭寸也都暴露在圍繞這些交易展開的記憶體池博弈之下——針對那些支付你費用的交換者的三明治攻擊,以及逐塊兌現你 LVR 的套利捆綁。那片生態被梳理在 MEV 與三明治攻擊文章中。

在鏈上讀取資金池狀態

以上一切,都可以通過對資金池合約發起的三次廉價呼叫觀測到。

slot0() 把熱點狀態打包進一個儲存槽:sqrtPriceX96、當前 tick、預言機觀測索引,以及協議費/鎖定標誌位。liquidity() 返回當前在區間內的聚合 LL——注意這不是 TVL;它是在當前 tick 處生效的深度參數,當價格跨越某個已初始化的 tick 時會不連續地跳變。ticks(int24) 返回逐 tick 的狀態:liquidityGross(引用該 tick 的總 LL)、liquidityNet(從左向右跨越時加上的帶符號 LL),以及 feeGrowthOutside 累加器。在已初始化的 tick 上迭代 liquidityNettickBitmap 會告訴你哪些 tick 存在,無需掃描全部 170 萬個)就能重建出完整的深度曲線 L(i)L(i)——也就是你的訂單簿快照。

from web3 import Web3

w3 = Web3(Web3.HTTPProvider(RPC_URL))
pool = w3.eth.contract(address=POOL, abi=POOL_ABI)  # USDC/WETH 0.05%

sqrt_price_x96, tick, *_ = pool.functions.slot0().call()
L = pool.functions.liquidity().call()

raw_price = (sqrt_price_x96 / 2**96) ** 2          # token1/token0, base units
eth_usdc  = 1 / (raw_price * 10**(18 - 6))          # human USDC per ETH
depth_1tick = L * (1.0001**0.5 - 1) * (sqrt_price_x96 / 2**96)

頭寸本身存在兩個地方。核心資金池以 (owner, tickLower, tickUpper) 為鍵索引它們——每個"所有者-區間"三元組對應一個聚合槽。散戶和大多數基金則改為通過外圍合約 NonfungiblePositionManager(主網:0xC36442b4a4522E871399CD717aBDD847Ab11FE88)鑄造,它把每個頭寸包裹進一個 ERC-721 NFT,並暴露 positions(tokenId),返回完整的元組:代幣、費率檔、區間、LLfeeGrowthInsideLasttokensOwed。對於組合監控,這一次呼叫加上資金池當前的 feeGrowthInside(如前一節所述,從 ticks() 重新計算)就能在不碰任何索引器的情況下給出按市值計價的價值和累計費用——不過對於任何歷史數據,你都會想要交換事件回放或一個 subgraph,因為費用增長是路徑依賴的,而鏈上只儲存當前的累加器。

在投入資金之前,有兩個運營細節值得內化。第一,tick 間隔會把你的策略空間量化:在 0.05% 檔你可以放置 10-bp 寬的區間,跑接近真正限價單的東西(在現價上/下方少量鑄造,收取費用,跨越後銷燬——白皮書明確地把這種"區間訂單"用例作為框架),而在 1% 檔你的最小區間約為 2% 寬,LOB 類比就變粗糙了。第二,區間訂單是一份沒有撤單優先權的限價單:如果價格穿越你的區間又折返回來,你就把庫存來回換了一趟,把邊際收益吐了回去(但保住了費用)。被動區間是你無法撤回的報價——這恰恰就是為什麼再平衡頻率這個問題、以及回答它所用的 LVR 視角,如此重要。

這把你帶到了哪裡

v3 技術棧壓縮成一句話:價格是一張 1.0001 幾何網格上的 tick;深度是 LL,僅憑平方根之差就能換算成代幣數量;費用是一個按 LL 計的累加器,你在自己的區間邊界處對它做差;而由此得到的頭寸是一個賣出波動率的區間訂單,其執行成本有一個名字、一條公式,還有一個 arXiv 編號。手握這些原語之後,有趣的問題就變成了量化問題:區間放多寬、多久再平衡一次,以及對於給定的資金池和市場狀態,費用能否覆蓋 LVR——而這正是本系列接下來要去的地方。

參考文獻

  • Adams, H., Zinsmeister, N., Salem, M., Keefer, R., Robinson, D. (2021). Uniswap v3 Core(白皮書)。
  • Uniswap v3 核心庫:TickMath.solSqrtPriceMath.solPosition.solTick.sol
  • Milionis, J., Moallemi, C., Roughgarden, T., Zhang, A. L. (2022). Automated Market Making and Loss-Versus-Rebalancing. arXiv:2208.06046。
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。