क्वांट्स के लिए Uniswap v3: Concentrated Liquidity और Tick Math फर्स्ट प्रिंसिपल्स से
Uniswap v3 पर एक LP position दरअसल एक छिपा हुआ limit-order-book range order है। दो कीमतों के बीच liquidity डिपॉज़िट करें, और आपने असेट को अपनी range में नीचे जाते समय खरीदने और ऊपर जाते समय बेचने का वादा कर दिया है — बिल्कुल वही जो resting limit orders का एक grid किसी CLOB पर करता है। लेकिन कॉन्ट्रैक्ट कीमतें, मात्राएं, या कोई order book स्टोर नहीं करता। यह सिर्फ तीन नंबर स्टोर करता है: Q64.96 फिक्स्ड पॉइंट में एक square-root price, एक इंटीजर tick index, और एक aggregate liquidity वैल्यू । अगर आप एक v3 position के बारे में उसी तरह सोचना चाहते हैं जैसे आप किसी market-making मॉडल में quotes के बारे में सोचते हैं — inventory, fill prices, adverse selection — तो आपको इन ऑन-चेन प्रिमिटिव्स और उन ट्रेडिंग कॉन्सेप्ट्स के बीच अनुवाद करना आना चाहिए जिन्हें वे एनकोड करते हैं। यह लेख Uniswap v3 whitepaper (Adams, Zinsmeister, Salem, Keefer, Robinson, 2021) और core contracts का अनुसरण करते हुए वह अनुवाद फर्स्ट प्रिंसिपल्स से बनाता है, और वहां खत्म होता है जहां से आजकल हर गंभीर LP चर्चा शुरू होती है: loss-versus-rebalancing।
x·y = k से virtual reserves तक
Uniswap v2 constant-product market maker है: एक pool token0 का reserve और token1 का reserve रखता है और हर swap पर लागू करता है
token1 की यूनिट्स में token0 की मार्जिनल कीमत है, और liquidity पूरे प्राइस एक्सिस पर समान रूप से फैली होती है। यह अत्यधिक capital-अकुशल है: एक stablecoin पेयर जो 0.999 और 1.001 के बीच ट्रेड करता है, अपनी 99% से ज्यादा capital उन कीमतों पर रखता है जो कभी प्रिंट ही नहीं होंगी।
v3 का दांव यह है कि हर LP को अपनी capital एक range तक सीमित करने दिया जाए। range के अंदर, position को बिल्कुल v2 pool की तरह व्यवहार करना चाहिए — वही bonding curve, वही मार्जिनल प्राइसिंग — लेकिन सिर्फ उतने reserves इस्तेमाल करते हुए जितने उस range को कवर करने के लिए जरूरी हैं। whitepaper इसे virtual reserves से औपचारिक रूप देता है: position ऐसे व्यवहार करता है जैसे उसके पास v2-स्टाइल reserves हों जो एक curve पर हों, जबकि real reserves virtual reserves में से वह घटाकर मिलते हैं जो position range की सीमाओं पर रखता:
यह whitepaper का equation 2.2 है, और v3 में यह सबसे महत्वपूर्ण अकेला equation है। translated curve axes को छूता है: पर real reserve शून्य पर पहुंच जाता है (position 100% token1 है), और पर real reserve शून्य पर पहुंच जाता है (100% token0)। range के बाहर position निष्क्रिय हो जाता है — एक टोकन का फिक्स्ड बैग, जो कुछ भी नहीं कमाता।

पैरामीटर , जिसे liquidity कहा जाता है, वह invariant है जो की जगह लेता है। इसे के रूप में परिभाषित किया गया है, और इसकी एक साफ व्याख्या है जिसे whitepaper स्पष्ट रूप से बताता है: liquidity "प्रति यूनिट square-root price के virtual reserves" है। range के अंदर किसी भी कीमत पर, virtual reserves होते हैं
क्यों state variable √P है
v3 का core price को ट्रैक नहीं करता। यह को ट्रैक करता है, जिसे sqrtPriceX96 के रूप में स्टोर किया जाता है, एक unsigned Q64.96 फिक्स्ड-पॉइंट नंबर:
जहां raw price है: token1 base units प्रति token0 base unit, decimals सहित (इसके बारे में नीचे और जानकारी है)। square root का कारण gas golf नहीं है, यह algebra है। virtual reserve identities को डिफरेंशिएट करें और आपको दो मूलभूत swap equations मिलते हैं, जो SqrtPriceMath library में implement किए गए हैं:
दोनों token deltas square-root price (या उसके reciprocal) में लीनियर हैं, जिसमें proportionality का constant है। इसलिए एक tick के अंदर एक swap को किसी curve inversion या Newton iteration की जरूरत नहीं पड़ती — बस को हिलाने के लिए एक गुणा, फिर amounts कैलकुलेट करने के लिए दो गुणा। जब कोई swap इतना बड़ा हो कि price को किसी initialized tick के पार धकेल दे, pool उसे cross करता है, वहां संदर्भित net liquidity (liquidityNet) जोड़ता या घटाता है, और नए aggregate के साथ जारी रहता है। ग्लोबली, pool एक piecewise-constant-product AMM है: initialized ticks के बीच constant , ticks पर jumps।
एक market maker के लिए, यही सही mental model है: pool का aggregate प्रोफाइल order-book depth का DEX equivalent है। जहां CLOB depth price levels की यूनिट्स में quote होती है, वहीं AMM depth प्रति tick है — और यह conversion बिल्कुल ऊपर वाली identity है।
Ticks: एक लॉग-स्पेस्ड प्राइस ग्रिड
Ranges मनमानी कीमतों पर शुरू और खत्म नहीं हो सकतीं; उन्हें ticks पर snap होना पड़ता है। Tick इस कीमत के बराबर होता है
तो हर tick अपने पड़ोसियों से एक basis point दूर होता है — absolute terms में नहीं, बल्कि relative terms में। यह log spacing जानबूझकर की गई है: एक फिक्स्ड additive grid $0.0001 पर ट्रेड होने वाले टोकन के लिए बेतुके रूप से coarse होगा और $100,000 पर बेतुके रूप से fine, जबकि एक geometric grid हर price स्केल पर एकसमान 1 bp resolution देता है। TickMath library दोनों दिशाओं में convert करती है — getSqrtRatioAtTick और getTickAtSqrtRatio — किसी exponential को कॉल करने के बजाय precomputed constants पर bit-twiddling इस्तेमाल करते हुए। Tick indices MIN_TICK = -887272 और MAX_TICK = 887272 से बाउंडेड हैं, जो price range कवर करता है: यह किसी भी token pair के लिए काफी चौड़ा है जो uint256 arithmetic में मौजूद हो सकता है।
हर tick इस्तेमाल करने लायक नहीं होता। हर fee tier एक tick spacing लागू करता है, और positions सिर्फ उन ticks का इस्तेमाल कर सकते हैं जो उससे विभाज्य हों:
| Fee tier | Tick spacing | न्यूनतम range चौड़ाई | विशिष्ट उपयोग |
|---|---|---|---|
| 0.01% | 1 | ~1 bp | stable/stable |
| 0.05% | 10 | ~10 bp | stable pairs, ETH/stables |
| 0.30% | 60 | ~60 bp | majors |
| 1.00% | 200 | ~2% | exotic / volatile |
0.05%/0.30%/1% tiers मई 2021 में लॉन्च के साथ आए; 0.01% tier नवंबर 2021 में governance vote से जोड़ा गया। ज्यादा fee वाले pools पर coarser spacing potentially crossable ticks की संख्या को कम रखती है और swap gas costs को बाउंडेड रखती है।
एक decimals ट्रैप जो हर किसी को एक बार जरूर काटता है: raw ऑन-चेन price token1 प्रति token0 base units में है। mainnet USDC/WETH pool में, token0 USDC है (6 decimals) और token1 WETH है (18 decimals), तो $3,000 प्रति ETH की human price एक raw price WETH-wei प्रति USDC-unit के बराबर होती है, जो इस tick पर रहती है
जिसमें है। अगर आपका monitoring dashboard USDC/WETH के लिए 196k के आसपास कोई tick दिखाता है, तो अब आप जान गए हैं क्यों — और यह भी कि human price है, जिसे जरूरत के हिसाब से invert किया जाता है।
(L, price range) से Token amounts: सटीक फॉर्मूले
इस सेक्शन का मुख्य निष्कर्ष: liquidity वाली एक position को पर और current price पर लें, तो उसके पास क्या है? range में swap equations को integrate करने पर तीन cases मिलते हैं:
Price range से नीचे () — position 100% token0 है:
Price range से ऊपर () — position 100% token1 है:
Price range के अंदर ():
यही बिल्कुल वह है जो SqrtPriceMath.getAmount0Delta और getAmount1Delta कैलकुलेट करते हैं (directed rounding के साथ — कॉन्ट्रैक्ट हमेशा यूजर के खिलाफ राउंड करता है, यह एक डिटेल है जो मायने रखती है अगर आप इसे किसी backtester में replicate करते हैं और wei-level discrepancies पर हैरान होते हैं)।
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, range [2500, 3500]
मान लीजिए ETH USDC पर ट्रेड करता है और आप range में 1 ETH डिपॉज़िट करना चाहते हैं। Square roots: , , ।
ETH leg को तय करता है:
फिर USDC leg इससे मिलता है:
तो इस position को mint करने में 1 ETH + 3,523.69 USDC लगते हैं, कुल वैल्यू $6,523.69, और ध्यान दें कि दोनों legs 50/50 नहीं हैं — split इस पर निर्भर करता है कि range के अंदर कहां बैठता है (एक असममित range वही तरीका है जिससे आप directional view व्यक्त करते हैं, या एक टोकन के साथ शुद्ध "range order" रखते हैं)।
अब price को boundaries तक ले चलें:
- पर position पूरी तरह ETH में बदल चुका है: ETH, जिसकी कीमत $5,716.68 है। आपने नीचे जाते समय 1.2867 ETH को 3523.69 / 1.2867 \approx \2{,}739$ की liquidity-weighted average price पर खरीदा।
- पर यह पूरी तरह USDC में बदल चुका है: USDC। आपने ऊपर जाते समय अपना ETH $3,240 के average पर बेचा।
यही range-order वाली व्याख्या को ठोस रूप देता है: position है ही 3000 से 2500 तक bids की एक सीढ़ी और 3000 से 3500 तक asks की, जिसमें प्रति tick साइज़ के समानुपाती है। और concentration ही वह चीज़ है जिसके लिए आपको भुगतान मिलता है: उसी $6,523.69 capital के साथ एक full-range v2-स्टाइल position का होता — concentrated position प्रति tick 12.4× ज्यादा depth quote करती है, और (जब तक range में है) प्रति डॉलर capital पर 12.4× दर से fees कमाती है।
Fee accounting: feeGrowthGlobal और feeGrowthInside
v3 fees position में compound नहीं होतीं (यह v2 से जानबूझकर किया गया एक विचलन है, जहां fees में reinvest होती थीं)। वे side-by-side, प्रति token, accrue होती हैं, और accounting O(1) bookkeeping का एक छोटा मास्टरपीस है जिसे समझना जरूरी है क्योंकि हर LP analytics pipeline इसे दोबारा implement करती है।
Pool दो global accumulators रखता है, feeGrowthGlobal0X128 और feeGrowthGlobal1X128: pool की शुरुआत से liquidity की प्रति यूनिट cumulative fees, Q128.128 फिक्स्ड पॉइंट में। हर swap पर, fee amount (input token से लिया गया) current in-range से divide होकर accumulator में जोड़ा जाता है। यही वह mechanism है जो v3 economics के सबसे बुनियादी नियम के पीछे है: fees सिर्फ उस liquidity को accrue होती हैं जो swap के समय range में होती है। Out-of-range positions सिर्फ निष्क्रिय inventory नहीं होतीं — निष्क्रिय रहते हुए वे बिल्कुल शून्य कमाती हैं।
global growth का सही हिस्सा एक finite range को attribute करने के लिए, हर initialized tick feeGrowthOutside0/1X128 स्टोर करता है — वह fee growth जो current price के सापेक्ष tick के दूसरी तरफ हुई (यह वैल्यू हर बार tick क्रॉस होने पर अपनी व्याख्या बदल लेती है, जो इस स्कीम को O(1) बनाता है)। फिर पर एक position के लिए current tick के साथ:
हर position एक snapshot feeGrowthInsideLast स्टोर करती है, और uncollected fees बस
होती हैं, जो हर बार position को छूने पर lazily अपडेट होती हैं। इसके दो व्यावहारिक परिणाम हैं। पहला, किसी position की fee income मापने का एकमात्र ईमानदार तरीका feeGrowthInside deltas हैं — pool-level volume सैंपल करना और अपने TVL शेयर के हिसाब से prorate करना गलत हो जाता है जब भी price आपकी range boundaries के इर्द-गिर्द घूमती है। दूसरा, चूंकि fees compound होने के बजाय tokensOwed के रूप में बैठती हैं, realized LP returns में एक cash-drag term होता है जो v2 में नहीं था; auto-compounding vaults ठीक इसी को arbitrage करने के लिए मौजूद हैं (अपनी खुद की fee घटाकर)।

Payoff: एक short straddle जिसे रखने के लिए आपको भुगतान मिलता है (शायद)
range के अंदर, price के फंक्शन के रूप में position की वैल्यू है
में Concave — term ही पूरी कहानी है। range के बाहर यह लीनियर हो जाता है: नीचे slope (आप एक फिक्स्ड ETH बैग में लॉन्ग हैं), ऊपर slope 0 (आप USDC में फ्लैट हैं)। हमारे वर्क्ड उदाहरण को पूरी range में चलाकर सिर्फ deposited tokens को होल्ड करने से तुलना करें:
| (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 से underperform करता है और सिर्फ minting price पर उसके बराबर होता है। कैप्ड upside, बढ़ी हुई downside participation, strike पर maximum relative वैल्यू... यह एक short straddle का payoff है (ज्यादा सटीक कहें तो, एक बार जब लीनियर tails को HODL benchmark के खिलाफ नेट कर दिया जाए, तो पर फैले options की एक strip में एक short position)। fee stream ही premium है। range को concentrate करना tighter strikes चुनना है: range में रहते हुए प्रति यूनिट समय ज्यादा premium, price हिलने पर तेज़ और गहरा divergence। हर v3 LP एक short-volatility trader है, चाहे उसने ऐसा बनना चुना हो या नहीं — यह उस inventory-risk trade-off का ऑन-चेन भाई-बहन है जिसे Avellaneda–Stoikov order-book market makers के लिए औपचारिक रूप देता है, जिसमें range की चौड़ाई quoted spread की भूमिका निभाती है।

उस टेबल में जो गैप है उसका पारंपरिक नाम impermanent (divergence) loss है, लेकिन IL-बनाम-HODL एक त्रुटिपूर्ण benchmark है: यह उस loss को, जो आपने एक liquidity provider के रूप में उठाया, उस market risk के साथ मिला देता है जो आप वैसे भी उठाते। ज्यादा तीखी decomposition Milionis, Moallemi, Roughgarden, और Zhang (2022) से आती है, "Automated Market Making and Loss-Versus-Rebalancing" (arXiv:2208.06046)। LP को HODL के बजाय एक rebalancing portfolio के खिलाफ benchmark करें जो हर पल pool जैसी ही token quantities रखता है — लेकिन arbitrageurs के खिलाफ ट्रेड करने के बजाय frictionless external market price पर ट्रेड करता है। अंतर LVR ("lever") है: LP P&L का वह हिस्सा जो शुद्ध adverse selection है, जो उन arbitrageurs को भुगतान किया जाता है जो हर price move के बाद AMM के stale quotes को उठा लेते हैं। volatility वाले geometric-Brownian price के तहत, LVR इस instantaneous दर पर accrue होता है
— जैसा लेखक कहते हैं, "AMMs के लिए एक Black–Scholes फॉर्मूला", जिसमें current price पर pool की मार्जिनल depth है। full-range constant-product pool के लिए यह प्रसिद्ध प्रति यूनिट समय पूल वैल्यू में सिमट जाता है: 5% daily volatility पर, हर दिन pool का करीब 3.1 bp arbitrageurs को बह जाता है, चाहे fees हों या न हों। Concentration को गुणा कर देता है — हमारा 12.4× depth वाला उदाहरण range में रहते हुए एक ~12.4× LVR मशीन भी है। position के +EV होने के लिए fees को उस bleed से आगे निकलना होगा, और LVR (जैसा "IL" नहीं है) एक hedgeable, predictable चलने वाली लागत है, जो इसे accounting की सही यूनिट बनाती है। पूरा profitability calculus — fee APR बनाम LVR बनाम realized volatility, कब LPing delta-hedged short options से बेहतर होती है — यह आने वाले impermanent loss और LVR डीप डाइव का विषय है, और hedging mechanics को v3 LP strategies और hedging में अपना खुद का treatment मिलता है।
एक और लागत एक लेयर नीचे रहती है: चूंकि AMM quotes सिर्फ तभी अपडेट होते हैं जब कोई ट्रेड करता है, हर LP position उन mempool games से भी एक्सपोज़्ड होती है जो उन ट्रेड्स के इर्द-गिर्द खेले जाते हैं — उन swappers के खिलाफ sandwich attacks जो आपकी fees चुकाते हैं, और arbitrage bundles जो block by block आपके LVR को realize करते हैं। वह पूरा ecosystem MEV और sandwich-attack आर्टिकल में मैप किया गया है।
ऑन-चेन Pool State पढ़ना
ऊपर बताई गई हर चीज़ pool contract के खिलाफ तीन सस्ती calls से observable है।
slot0() hot state को एक storage slot में पैक करता है: sqrtPriceX96, current tick, oracle observation indices, और protocol-fee/lock flags। liquidity() current in-range aggregate लौटाता है — ध्यान दें यह TVL नहीं है; यह depth parameter है जो current tick पर सक्रिय है और जब price किसी initialized tick को क्रॉस करता है तो यह असंततता से (discontinuously) उछलता है। ticks(int24) प्रति-tick state लौटाता है: liquidityGross (कुल जो tick को संदर्भित करता है), liquidityNet (signed जो left-to-right क्रॉस करते समय जुड़ता है), और feeGrowthOutside accumulators। initialized ticks पर liquidityNet को iterate करना (tickBitmap आपको बताता है कि कौन से ticks मौजूद हैं बिना सभी 1.7 मिलियन को scan किए) पूरे depth profile को फिर से बनाता है — आपका 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)
Positions खुद दो जगहों पर रहती हैं। core pool उन्हें (owner, tickLower, tickUpper) से key करता है — प्रति owner-range triple एक aggregate slot। Retail और ज्यादातर funds इसके बजाय periphery NonfungiblePositionManager (mainnet: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88) के जरिए mint करते हैं, जो हर position को एक ERC-721 NFT में लपेटता है और positions(tokenId) एक्सपोज़ करता है जो पूरा tuple लौटाता है: tokens, fee tier, range, , feeGrowthInsideLast, और tokensOwed। portfolio monitoring के लिए, वह एक call और pool का current feeGrowthInside (पिछले सेक्शन की तरह ticks() से दोबारा कैलकुलेट किया गया) आपको बिना किसी indexer को छुए mark-to-market वैल्यू और accrued fees दे देता है — हालांकि किसी भी historical चीज़ के लिए आपको swap-event replay या एक subgraph चाहिए होगा, क्योंकि fee growth path-dependent है और chain सिर्फ current accumulators स्टोर करती है।
Capital deploy करने से पहले समझने लायक दो operational डिटेल्स हैं। पहला, tick spacing आपकी strategy space को quantize करती है: 0.05% tier पर आप 10-bp-चौड़ी ranges रख सकते हैं और लगभग एक सच्चे limit order जैसा कुछ चला सकते हैं (spot के ठीक ऊपर/नीचे mint करें, fee इकट्ठा करें, क्रॉसिंग के बाद burn करें — whitepaper स्पष्ट रूप से इस "range order" उपयोग-मामले को इसी तरह फ्रेम करता है), जबकि 1% tier पर आपकी न्यूनतम range ~2% चौड़ी होती है और LOB analogy मोटी (coarse) हो जाती है। दूसरा, एक range order बिना cancellation priority वाला एक limit order है: अगर price आपकी range को क्रॉस करके वापस आ जाए, तो आप inventory को round-trip कर देते हैं और edge वापस दे देते हैं (fees अपने पास रखते हुए)। Passive ranges ऐसे quotes हैं जिन्हें आप वापस नहीं खींच सकते — यही वजह है कि rebalancing-frequency वाला सवाल, और उसका जवाब देने के लिए LVR लेंस, इतने मायने रखते हैं।
यहां से आगे क्या
v3 stack, संक्षेप में: कीमतें 1.0001-geometric grid पर ticks हैं; depth है, जो square roots के अंतर के अलावा कुछ नहीं से token amounts में convertible है; fees एक प्रति- accumulator हैं जिसे आप अपनी range boundaries पर differentiate करते हैं; और जो resulting position मिलती है वह एक short-vol range order है जिसकी चलने वाली लागत का एक नाम, एक फॉर्मूला, और एक arXiv नंबर है। इन प्रिमिटिव्स के हाथ में होने के बाद, दिलचस्प सवाल क्वांटिटेटिव बन जाते हैं: कितनी चौड़ी, कितनी बार rebalance करें, और क्या किसी दिए गए pool और regime के लिए fees LVR को clear करती हैं — यही ठीक वह जगह है जहां यह सीरीज़ आगे जाती है।
संदर्भ
- 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।
Authors
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.