Uniswap v3 สำหรับ Quant: Concentrated Liquidity และคณิตศาสตร์ของ Tick จากหลักการพื้นฐาน
ตำแหน่ง LP บน Uniswap v3 แท้จริงแล้วคือ range order แบบ limit-order-book ที่ปลอมตัวมา ฝากสภาพคล่องระหว่างราคาสองระดับ แล้วคุณก็ผูกมัดตัวเองให้ต้องซื้อสินทรัพย์ตอนราคาลงผ่านช่วงของคุณ และขายตอนราคาขึ้น — เหมือนกับที่กริดของ limit order ที่วางค้างไว้ทำบน CLOB ทุกประการ แต่ contract ไม่ได้เก็บราคา ปริมาณ หรือ order book มันเก็บตัวเลขสามค่าเท่านั้น คือ square-root price ในรูปแบบ Q64.96 fixed point, ดัชนี tick แบบจำนวนเต็ม และค่า liquidity รวม ถ้าคุณอยากทำความเข้าใจตำแหน่ง v3 ในแบบเดียวกับที่คุณคิดเรื่องราคาเสนอในโมเดลการทำตลาด — inventory, ราคาที่ถูกจับคู่, adverse selection — คุณต้องแปลงกลับไปกลับมาระหว่าง primitive บนเชนเหล่านี้กับแนวคิดการเทรดที่มันเข้ารหัสไว้ให้ได้ บทความนี้จะสร้างการแปลนั้นจากหลักการพื้นฐาน โดยอิงตาม whitepaper ของ Uniswap v3 (Adams, Zinsmeister, Salem, Keefer, Robinson, 2021) และ core contracts และจบลงตรงจุดที่การพูดคุยเรื่อง LP อย่างจริงจังทุกวันนี้เริ่มต้น นั่นคือ loss-versus-rebalancing
จาก x·y = k สู่ virtual reserves
Uniswap v2 คือ constant-product market maker กล่าวคือ pool ถือ reserve ของ token0 และ ของ token1 และบังคับใช้เงื่อนไข
ในทุกการสวอป ราคามาร์จินัลของ token0 ในหน่วยของ token1 คือ และสภาพคล่องถูกกระจายอย่างสม่ำเสมอไปทั่วแกนราคาทั้งหมด นั่นทำให้ใช้เงินทุนอย่างไม่มีประสิทธิภาพอย่างมาก คู่ stablecoin ที่เทรดกันระหว่าง 0.999 ถึง 1.001 จะเก็บเงินทุนกว่า 99% ไว้ที่ราคาที่จะไม่มีวันเกิดขึ้นจริง
การเคลื่อนไหวของ v3 คือการให้ LP แต่ละรายจำกัดเงินทุนของตัวเองไว้ในช่วง ภายในช่วงนั้น ตำแหน่งจะต้องมีพฤติกรรมเหมือนกับ pool ของ v2 เป๊ะ ๆ — bonding curve เดียวกัน การตั้งราคามาร์จินัลแบบเดียวกัน — แต่ใช้เฉพาะ reserve ที่จำเป็นเพื่อครอบคลุมช่วงนั้น whitepaper ได้ทำให้เป็นรูปแบบทางการด้วย virtual reserves: ตำแหน่งทำตัวราวกับว่ามันถือ reserve สไตล์ v2 ที่อยู่บนเส้นโค้ง ในขณะที่ real reserves คือ virtual reserves ลบด้วยสิ่งที่ตำแหน่งจะถืออยู่ที่ขอบเขตของช่วง:
นี่คือสมการที่ 2.2 ของ whitepaper และเป็นสมการที่สำคัญที่สุดเพียงหนึ่งเดียวใน v3 เส้นโค้งที่ถูกแปลนี้แตะแกน: ที่ real reserve ของ แตะศูนย์ (ตำแหน่งเป็น token1 ทั้งหมด 100%) และที่ real reserve ของ แตะศูนย์ (เป็น token0 ทั้งหมด 100%) นอกช่วงนี้ตำแหน่งจะไม่ทำงาน — กลายเป็นถุงของ token เดียวที่นิ่งอยู่เฉย ๆ ไม่ได้อะไรเลย

พารามิเตอร์ ที่เรียกว่า liquidity คือค่าคงที่ที่มาแทน มันถูกนิยามเป็น และมีการตีความที่ชัดเจนตามที่ whitepaper ระบุไว้ตรง ๆ นั่นคือ liquidity คือ "virtual reserves ต่อหน่วยของ square-root price" ที่ราคาใด ๆ ภายในช่วง virtual reserves คือ
เหตุผลที่ state variable คือ √P
แกนหลักของ v3 ไม่ได้ติดตามราคา แต่ติดตาม ซึ่งเก็บไว้เป็น sqrtPriceX96 ตัวเลข unsigned Q64.96 fixed-point:
โดยที่ คือราคา raw: token1 base unit ต่อ token0 base unit รวมทศนิยมด้วย (รายละเอียดเพิ่มเติมด้านล่าง) เหตุผลของ square root ไม่ใช่เรื่องประหยัด gas แต่เป็นเรื่องพีชคณิต หากคุณ differentiate อัตลักษณ์ของ virtual reserve คุณจะได้สมการพื้นฐานสองสมการของการสวอป ซึ่งถูก implement ไว้ใน library SqrtPriceMath:
ทั้งสอง token delta เป็น เชิงเส้น เทียบกับ square-root price (หรือส่วนกลับของมัน) โดยมี เป็นค่าคงที่ของสัดส่วน ดังนั้นการสวอปภายใน tick เดียวจึงไม่ต้องอาศัยการกลับด้านเส้นโค้ง ไม่ต้องใช้ Newton iteration — แค่การคูณครั้งเดียวเพื่อขยับ แล้วคูณอีกสองครั้งเพื่อคำนวณปริมาณ เมื่อการสวอปมีขนาดใหญ่พอที่จะผลักราคาข้าม tick ที่ถูก initialize แล้ว pool จะข้าม tick นั้น เพิ่มหรือลบ net liquidity ที่อ้างอิงอยู่ตรงนั้น (liquidityNet) และดำเนินต่อด้วยค่า รวมใหม่ ในภาพรวม pool คือ AMM แบบ piecewise-constant-product: คงที่ระหว่าง tick ที่ initialize แล้ว และกระโดดที่ tick
สำหรับ market maker นี่คือกรอบความคิดที่ถูกต้อง โปรไฟล์ รวมของ pool คือสิ่งที่เทียบเท่ากับความลึกของ order book บน DEX ในขณะที่ความลึกของ CLOB ถูกเสนอเป็นหน่วยต่อระดับราคา ความลึกของ AMM คือ ต่อ tick — และการแปลงคือสมการ ข้างต้นนั่นเอง
Tick: กริดราคาแบบ log-spaced
ช่วงต่าง ๆ ไม่สามารถเริ่มต้นและสิ้นสุดที่ราคาใด ๆ ตามอำเภอใจได้ พวกมันต้องยึดติดกับ tick Tick สอดคล้องกับราคา
ดังนั้นแต่ละ tick จึงห่างจากเพื่อนบ้านหนึ่ง basis point — ไม่ใช่ในเชิงสัมบูรณ์ แต่ใน เชิงสัมพัทธ์ การกำหนดระยะห่างแบบ log นี้เป็นการออกแบบโดยเจตนา กริดแบบบวกคงที่จะหยาบเกินไปอย่างไร้เหตุผลสำหรับ token ที่เทรดที่ $0.0001 และละเอียดเกินไปอย่างไร้เหตุผลที่ $100{,}000 ในขณะที่กริดแบบเรขาคณิตให้ความละเอียด 1 bp สม่ำเสมอในทุกระดับราคา library TickMath แปลงได้ทั้งสองทิศทาง — getSqrtRatioAtTick และ getTickAtSqrtRatio — โดยใช้การจัดการบิตกับค่าคงที่ที่คำนวณล่วงหน้า แทนที่จะเรียกฟังก์ชันเอ็กซ์โพเนนเชียล ดัชนี tick ถูกจำกัดด้วย MIN_TICK = -887272 และ MAX_TICK = 887272 ซึ่งครอบคลุมช่วงราคา กว้างพอสำหรับคู่ token ใด ๆ ที่สามารถมีอยู่ได้ใน arithmetic แบบ uint256
ไม่ใช่ทุก tick ที่ใช้งานได้ แต่ละ fee tier กำหนด tick spacing และตำแหน่งสามารถใช้ได้เฉพาะ tick ที่หารด้วยค่านี้ลงตัวเท่านั้น:
| Fee tier | Tick spacing | ความกว้างขั้นต่ำของช่วง | การใช้งานทั่วไป |
|---|---|---|---|
| 0.01% | 1 | ~1 bp | stable/stable |
| 0.05% | 10 | ~10 bp | คู่ stable, ETH/stables |
| 0.30% | 60 | ~60 bp | คู่หลัก |
| 1.00% | 200 | ~2% | สินทรัพย์แปลกใหม่ / ผันผวนสูง |
fee tier 0.05%/0.30%/1% เปิดตัวมาตั้งแต่แรกในเดือนพฤษภาคม 2021 ส่วน tier 0.01% ถูกเพิ่มเข้ามาโดยการโหวตของ governance ในเดือนพฤศจิกายน 2021 tick spacing ที่หยาบกว่าบน pool ที่มี fee สูงช่วยควบคุมจำนวน tick ที่อาจถูกข้ามให้อยู่ในระดับต่ำ และจำกัดต้นทุน gas ของการสวอปให้อยู่ในขอบเขต
กับดักเรื่องทศนิยมที่ทุกคนต้องเจอสักครั้ง คือ ราคา raw บนเชนคือ token1 ต่อ token0 ในหน่วย base unit ใน mainnet pool ของ USDC/WETH, token0 คือ USDC (6 ทศนิยม) และ token1 คือ WETH (18 ทศนิยม) ดังนั้นราคาที่มนุษย์อ่านว่า $3{,}000 ต่อ ETH จะสอดคล้องกับราคา raw ที่ WETH-wei ต่อ USDC-unit ซึ่งอยู่ที่ tick
โดยมี ถ้าแดชบอร์ดที่คุณติดตามแสดง tick ใกล้ 196k สำหรับ USDC/WETH ตอนนี้คุณก็รู้แล้วว่าทำไม — และรู้ว่าราคาที่ มนุษย์ อ่านได้คือ ที่ถูกกลับด้านตามความจำเป็น
จำนวน token จาก (L, ช่วงราคา): สูตรที่แน่นอน
สิ่งที่ต้องส่งมอบหลักของหัวข้อนี้: กำหนดตำแหน่งที่มี liquidity บนช่วง และราคาปัจจุบัน มันถืออะไรอยู่บ้าง การอินทิเกรตสมการการสวอปตลอดช่วงจะได้สามกรณี:
ราคาต่ำกว่าช่วง () — ตำแหน่งเป็น token0 ทั้งหมด 100%:
ราคาสูงกว่าช่วง () — ตำแหน่งเป็น token1 ทั้งหมด 100%:
ราคาอยู่ในช่วง ():
นี่คือสิ่งที่ SqrtPriceMath.getAmount0Delta และ getAmount1Delta คำนวณได้พอดี (พร้อมการปัดเศษที่มีทิศทาง — contract จะปัดเศษในทางที่ไม่เป็นผลดีต่อผู้ใช้เสมอ ซึ่งเป็นรายละเอียดที่สำคัญหากคุณจำลองสิ่งนี้ใน backtester และสงสัยว่าทำไมมีความคลาดเคลื่อนระดับ 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 เทรดที่ USDC และคุณต้องการฝาก 1 ETH เข้าไปในช่วง Square root: , ,
ขา ETH เป็นตัวกำหนด :
ขา USDC ตามมา:
ดังนั้นการ mint ตำแหน่งนี้ต้องใช้ 1 ETH + 3,523.69 USDC มูลค่ารวม $6,523.69 และสังเกตว่าขาทั้งสองข้าง ไม่ใช่ 50/50 — สัดส่วนขึ้นอยู่กับตำแหน่งของ ในช่วงนั้น (ช่วงที่ไม่สมมาตรคือวิธีที่แน่ชัดในการแสดงมุมมองเชิงทิศทาง หรือวางเป็น "range order" บริสุทธิ์ด้วย token เดียว)
ทีนี้ลองเดินราคาไปยังขอบเขต:
- ที่ ตำแหน่งแปลงเป็น ETH ทั้งหมดแล้ว: ETH มูลค่า $5,716.68 คุณซื้อ ETH เพิ่ม 1.2867 ETH ระหว่างที่ราคาลงมา ที่ราคาเฉลี่ยถ่วงน้ำหนักด้วยสภาพคล่องที่ประมาณ 3523.69 / 1.2867 \approx \2{,}739$
- ที่ มันแปลงเป็น USDC ทั้งหมดแล้ว: USDC คุณขาย ETH ของคุณระหว่างที่ราคาขึ้น ที่ราคาเฉลี่ย $3,240
นั่นคือการอ่านความหมายแบบ range order ที่จับต้องได้: ตำแหน่งนี้ คือ บันไดของ bid ตั้งแต่ 3000 ลงไปถึง 2500 และ ask ตั้งแต่ 3000 ขึ้นไปถึง 3500 โดยขนาดต่อ tick เป็นสัดส่วนกับ และการรวมศูนย์คือสิ่งที่คุณได้รับผลตอบแทนเป็นการแลกเปลี่ยน ตำแหน่งสไตล์ v2 แบบ full-range ที่ใช้เงินทุน $6,523.69 เท่ากันจะมี — ตำแหน่งที่รวมศูนย์เสนอความลึก มากกว่า 12.4 เท่า ต่อ tick และ (ขณะที่ยังอยู่ในช่วง) รับ fee ในอัตราที่มากกว่า 12.4 เท่า ต่อเงินทุนหนึ่งดอลลาร์
การบันทึก fee: feeGrowthGlobal และ feeGrowthInside
fee ของ v3 ไม่ได้ทบต้นเข้าไปในตำแหน่ง (ซึ่งเป็นการเปลี่ยนแปลงโดยเจตนาจาก v2 ที่ fee ถูกนำกลับไปลงทุนใน ) แต่มันสะสมแยกต่างหาก ต่อ token แต่ละตัว และระบบบัญชีนี้คือผลงานชิ้นเล็ก ๆ ที่ยอดเยี่ยมของการทำบัญชีแบบ O(1) ที่คุ้มค่าแก่การทำความเข้าใจ เพราะทุก pipeline วิเคราะห์ LP ต้อง implement ระบบนี้ซ้ำ
Pool เก็บตัวสะสมรวมสองตัว คือ feeGrowthGlobal0X128 และ feeGrowthGlobal1X128: fee สะสมต่อหน่วยของ liquidity นับตั้งแต่ pool ถูกสร้างขึ้น ในรูปแบบ Q128.128 fixed point ในทุกการสวอป จำนวน fee (ที่หักจาก token ขาเข้า) จะถูกหารด้วย ที่อยู่ในช่วง ในขณะนั้น และบวกเข้าไปในตัวสะสม นี่คือกลไกเบื้องหลังกฎหลักของเศรษฐศาสตร์ v3: fee จะสะสมเฉพาะกับ liquidity ที่อยู่ในช่วงในขณะที่เกิดการสวอปเท่านั้น ตำแหน่งที่อยู่นอกช่วงไม่ได้เป็นเพียงสินค้าคงคลังที่นิ่งเฉย ๆ เท่านั้น แต่ยังได้รับผลตอบแทนเป็นศูนย์เป๊ะในขณะที่ไม่ทำงาน
เพื่อระบุส่วนที่ถูกต้องของการเติบโตแบบรวมให้กับช่วงที่จำกัด แต่ละ tick ที่ initialize แล้ว จะเก็บ feeGrowthOutside0/1X128 — การเติบโตของ fee ที่เกิดขึ้นบนอีกด้าน ตรงข้าม ของ tick เมื่อเทียบกับราคาปัจจุบัน (ค่านี้จะพลิกความหมายทุกครั้งที่ tick ถูกข้าม ซึ่งเป็นสิ่งที่ทำให้ scheme นี้เป็น O(1)) จากนั้นสำหรับตำแหน่งบนช่วง ที่มี tick ปัจจุบันคือ :
แต่ละตำแหน่งเก็บสแนปช็อต feeGrowthInsideLast และ fee ที่ยังไม่ได้เก็บก็คือ
ซึ่งอัปเดตแบบ lazy ทุกครั้งที่ตำแหน่งถูกแตะ มีผลในทางปฏิบัติสองประการ ประการแรก delta ของ feeGrowthInside เป็นวิธี เดียว ที่ตรงไปตรงมาในการวัดรายได้จาก fee ของตำแหน่ง การสุ่มตัวอย่างปริมาณการเทรดระดับ pool แล้วเฉลี่ยตามสัดส่วนของ TVL ที่คุณถืออยู่จะให้ผลลัพธ์ผิดพลาดทุกครั้งที่ราคาเคลื่อนที่ไปมาใกล้ขอบเขตช่วงของคุณ ประการที่สอง เนื่องจาก fee อยู่ในรูป tokensOwed แทนที่จะทบต้น ผลตอบแทน LP ที่เกิดขึ้นจริงจึงมีเทอมของ cash-drag ที่ v2 ไม่มี vault แบบ auto-compounding มีอยู่ก็เพื่อทำ arbitrage ส่วนต่างนี้ออกไป (ลบด้วย fee ของตัวมันเอง)

Payoff: short straddle ที่คุณได้รับค่าตอบแทนให้ถือ (อาจจะ)
ภายในช่วง มูลค่าตำแหน่งในฐานะฟังก์ชันของราคาคือ
เว้าลงเมื่อเทียบกับ — เทอม คือหัวใจทั้งหมดของเรื่องนี้ นอกช่วง มันจะกลายเป็นเชิงเส้น: slope ด้านล่าง (คุณ long ถุง ETH คงที่) และ slope 0 ด้านบน (คุณ flat ใน USDC) ลองรันตัวอย่างเชิงตัวเลขของเราตลอดทั้งช่วง และเทียบกับการแค่ถือ token ที่ฝากไว้เฉย ๆ:
| (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 ใน ทั้งสอง ทิศทาง และเท่ากันเฉพาะที่ราคาตอน mint เท่านั้น ผลตอบแทนขาขึ้นถูกจำกัด การมีส่วนร่วมกับขาลงถูกขยาย และมูลค่าสัมพัทธ์สูงสุดอยู่ที่ strike... นี่คือ payoff ของ short straddle (พูดให้แม่นยำกว่านั้น เมื่อหักส่วนหางเชิงเส้นออกจากเกณฑ์เปรียบเทียบ HODL แล้ว มันคือตำแหน่ง short ในชุดของ option ที่ถูก strike ไว้ตลอดช่วง ) กระแส fee คือ premium การรวมศูนย์ช่วงคือการเลือก strike ที่แคบกว่า — premium ต่อหน่วยเวลามากกว่าขณะที่อยู่ในช่วง แต่ก็มีการเบี่ยงเบนที่เร็วและลึกกว่าเมื่อราคาเคลื่อนที่ LP ทุกคนบน v3 คือเทรดเดอร์ short-volatility ไม่ว่าจะเลือกเป็นแบบนั้นหรือไม่ก็ตาม — เป็นพี่น้องบนเชนของการแลกเปลี่ยนความเสี่ยง inventory ที่Avellaneda–Stoikovได้ทำให้เป็นรูปแบบทางการสำหรับ market maker บน order book โดยความกว้างของช่วงมีบทบาทเหมือนกับ spread ที่เสนอ

ชื่อดั้งเดิมของช่องว่างในตารางนั้นคือ impermanent (divergence) loss แต่ IL-เทียบกับ-HODL เป็นเกณฑ์เปรียบเทียบที่มีข้อบกพร่อง เพราะมันปนกันระหว่างการขาดทุนที่คุณได้รับ ในฐานะผู้ให้สภาพคล่อง กับความเสี่ยงตลาดที่คุณจะแบกรับอยู่แล้วไม่ว่าอย่างไร การแยกส่วนที่คมชัดกว่ามาจาก Milionis, Moallemi, Roughgarden และ Zhang (2022), "Automated Market Making and Loss-Versus-Rebalancing" (arXiv:2208.06046) ให้เปรียบเทียบ LP ไม่ใช่กับ HODL แต่กับ rebalancing portfolio ที่ถือ token ในปริมาณเดียวกับ pool ในทุกขณะเวลา — แต่เทรดที่ราคาตลาดภายนอกแบบไร้แรงเสียดทาน แทนที่จะเทรดกับ arbitrageur ส่วนต่างนี้คือ LVR ("lever") ซึ่งเป็นส่วนของ P&L ของ LP ที่เป็น adverse selection แท้ ๆ จ่ายให้กับ arbitrageur ที่คอยเก็บเกี่ยวราคาเสนอที่เก่าของ AMM หลังการเคลื่อนไหวของราคาทุกครั้ง ภายใต้ราคาที่เคลื่อนที่แบบ geometric-Brownian ด้วยความผันผวน LVR สะสมในอัตรา instantaneous
— "สูตร Black–Scholes สำหรับ AMM" ตามคำของผู้เขียน โดยที่ คือความลึกมาร์จินัลของ pool ที่ราคาปัจจุบัน สำหรับ pool constant-product แบบ full-range สิ่งนี้ลดรูปลงมาเหลือค่าที่รู้จักกันดีคือ ของมูลค่า pool ต่อหน่วยเวลา ที่ความผันผวนรายวัน 5% ประมาณ 3.1 bp ของ pool ไหลออกไปให้ arbitrageur ทุกวัน ไม่ว่าจะมี fee หรือไม่ก็ตาม การรวมศูนย์ทวีคูณ ตัวอย่างความลึก 12.4 เท่าของเราด้านบนก็เป็นเครื่องจักร LVR ที่มากกว่าประมาณ 12.4 เท่าเช่นกันขณะที่อยู่ในช่วง fee ต้องวิ่งแซงหน้าการไหลออกนั้นเพื่อให้ตำแหน่งเป็น +EV และ LVR (ต่างจาก "IL") เป็นต้นทุนที่ดำเนินอยู่ต่อเนื่อง คาดการณ์ได้ และป้องกันความเสี่ยงได้ ซึ่งเป็นเหตุผลที่ทำให้มันเป็นหน่วยบัญชีที่ถูกต้อง การคำนวณความสามารถในการทำกำไรแบบเต็มรูปแบบ — fee APR เทียบกับ LVR เทียบกับความผันผวนที่เกิดขึ้นจริง เมื่อไหร่ที่การทำ LP ชนะ delta-hedged short option — เป็นหัวข้อของบทวิเคราะห์เชิงลึกเรื่อง impermanent loss และ LVR ที่กำลังจะมาถึง และกลไกการ hedge จะได้รับการกล่าวถึงในบทความของตัวเองในกลยุทธ์ LP ของ v3 และการ hedge
ยังมีต้นทุนอีกชั้นหนึ่งที่ซ่อนอยู่ลึกลงไป เพราะราคาเสนอของ AMM อัปเดตก็ต่อเมื่อมีคนเทรดเท่านั้น ทุกตำแหน่ง LP จึงถูกเปิดเผยต่อเกมใน mempool ที่เล่นรอบ ๆ การเทรดเหล่านั้นด้วย — sandwich attack ต่อผู้สวอปที่จ่าย fee ให้คุณ และ arbitrage bundle ที่ทำให้ LVR ของคุณเกิดขึ้นจริงทีละบล็อก ระบบนิเวศนั้นถูกอธิบายไว้ในบทความเรื่อง MEV และ sandwich attack
การอ่าน state ของ pool บนเชน
ทุกสิ่งข้างต้นสามารถสังเกตได้จากการเรียกที่ราคาถูกสามครั้งไปยัง pool contract
slot0() บรรจุ state ที่ใช้บ่อยไว้ใน storage slot เดียว: sqrtPriceX96, tick ปัจจุบัน, ดัชนีการสังเกตของ oracle และ flag ของ protocol-fee/lock liquidity() คืนค่า รวมที่อยู่ในช่วงปัจจุบัน — โปรดสังเกตว่านี่ ไม่ใช่ TVL แต่เป็นพารามิเตอร์ความลึกที่ทำงานอยู่ที่ tick ปัจจุบัน และมันจะกระโดดแบบไม่ต่อเนื่องเมื่อราคาข้าม tick ที่ initialize แล้ว ticks(int24) คืนค่า state ต่อ tick: liquidityGross ( รวมที่อ้างอิงถึง tick นั้น) liquidityNet ( ที่มีเครื่องหมาย ซึ่งถูกเพิ่มเมื่อข้ามจากซ้ายไปขวา) และตัวสะสม feeGrowthOutside การวนซ้ำผ่าน liquidityNet บน tick ที่ initialize แล้ว (tickBitmap บอกคุณว่า tick ไหนมีอยู่จริงโดยไม่ต้องสแกนทั้ง 1.7 ล้าน tick) จะสร้างโปรไฟล์ความลึก เต็มรูปแบบขึ้นมาใหม่ — สแนปช็อต order book ของคุณเอง
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)
ตำแหน่งเองอยู่ในสองที่ core pool ระบุตำแหน่งด้วย key (owner, tickLower, tickUpper) — หนึ่ง slot รวมต่อคู่ owner-range หนึ่งคู่ นักลงทุนรายย่อยและกองทุนส่วนใหญ่กลับ mint ผ่าน periphery NonfungiblePositionManager (mainnet: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88) ซึ่งห่อแต่ละตำแหน่งไว้ใน NFT แบบ ERC-721 และเปิด positions(tokenId) ที่คืนค่า tuple แบบเต็ม: token, fee tier, ช่วง, , feeGrowthInsideLast และ tokensOwed สำหรับการติดตามพอร์ตโฟลิโอ การเรียกครั้งเดียวนั้นบวกกับ feeGrowthInside ปัจจุบันของ pool (คำนวณใหม่จาก ticks() ตามหัวข้อก่อนหน้า) ให้มูลค่า mark-to-market และ fee ที่สะสมแล้วโดยไม่ต้องแตะ indexer เลย — แม้ว่าสำหรับข้อมูลย้อนหลังใด ๆ คุณจะต้องใช้การ replay swap-event หรือ subgraph เพราะการเติบโตของ fee ขึ้นอยู่กับเส้นทาง (path-dependent) และเชนเก็บเฉพาะตัวสะสมปัจจุบันเท่านั้น
รายละเอียดเชิงปฏิบัติการสองข้อที่ควรทำความเข้าใจให้ลึกซึ้งก่อนวางเงินทุน ข้อแรก tick spacing กำหนดปริมาณ (quantize) พื้นที่กลยุทธ์ของคุณ บน tier 0.05% คุณสามารถวางช่วงที่กว้างเพียง 10 bp และรันบางสิ่งที่ใกล้เคียงกับ limit order จริง (mint ไว้เหนือ/ใต้ spot เล็กน้อย เก็บ fee แล้ว burn หลังจากถูกข้าม — whitepaper กล่าวถึงกรณีการใช้งาน "range order" นี้ไว้อย่างชัดเจน) ในขณะที่บน tier 1% ช่วงขั้นต่ำของคุณกว้างประมาณ 2% และการเปรียบเทียบกับ LOB จะหยาบขึ้น ข้อสอง range order คือ limit order ที่ไม่มีสิทธิ์ยกเลิกก่อน (cancellation priority) หากราคาข้ามช่วงของคุณแล้วย้อนกลับมา คุณจะ round-trip inventory ของคุณและคืนขอบ (edge) กลับไป (โดยยังคง fee ไว้) ช่วงแบบ passive คือราคาเสนอที่คุณดึงกลับไม่ได้ — ซึ่งเป็นเหตุผลที่คำถามเรื่องความถี่ในการ rebalance และมุมมองแบบ LVR สำหรับตอบคำถามนั้น มีความสำคัญมาก
สิ่งที่เหลืออยู่กับคุณ
สรุป stack ของ v3 ให้กระชับ: ราคาคือ tick บนกริดเรขาคณิต 1.0001; ความลึกคือ ซึ่งแปลงเป็นจำนวน token ได้ด้วยเพียงผลต่างของ square root; fee คือตัวสะสมต่อ- ที่คุณหาผลต่างข้ามขอบเขตช่วงของคุณ; และตำแหน่งที่ได้ก็คือ range order แบบ short-vol ที่มีต้นทุนที่ดำเนินต่อเนื่องซึ่งมีชื่อเรียก มีสูตร และมีหมายเลข arXiv เมื่อมี primitive เหล่านี้อยู่ในมือแล้ว คำถามที่น่าสนใจก็กลายเป็นเชิงปริมาณ: กว้างแค่ไหน rebalance บ่อยแค่ไหน และ fee จะชดเชย LVR ได้หรือไม่สำหรับ pool และสภาวะตลาดที่กำหนด — ซึ่งเป็นจุดที่ซีรีส์นี้จะไปต่อในลำดับถัดไป
เอกสารอ้างอิง
- Adams, H., Zinsmeister, N., Salem, M., Keefer, R., Robinson, D. (2021). Uniswap v3 Core (whitepaper).
- Uniswap v3 core libraries:
TickMath.sol,SqrtPriceMath.sol,Position.sol,Tick.sol. - Milionis, J., Moallemi, C., Roughgarden, T., Zhang, A. L. (2022). Automated Market Making and Loss-Versus-Rebalancing. arXiv:2208.06046.
ผู้เขียน
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.