Uniswap v3 voor quants: geconcentreerde liquiditeit en tick-wiskunde vanaf de grondbeginselen
Een LP-positie op Uniswap v3 is in wezen een range order uit een limit-orderboek, vermomd. Je zet liquiditeit in tussen twee prijzen en daarmee heb je je verplicht om het asset te kopen terwijl de prijs door je range naar beneden zakt, en het te verkopen terwijl de prijs naar boven beweegt — precies wat een raster van openstaande limietorders doet op een CLOB. Maar het contract slaat geen prijzen, hoeveelheden of een orderboek op. Het slaat drie getallen op: een vierkantswortelprijs in Q64.96 fixed point, een geheel tick-indexgetal, en een geaggregeerde liquiditeitswaarde . Als je wilt redeneren over een v3-positie zoals je zou redeneren over quotes in een market-making-model — inventaris, fillprijzen, adverse selection — dan moet je in staat zijn om die on-chain primitieven te vertalen naar de handelsconcepten die ze coderen. Dit artikel bouwt die vertaling op vanaf de grondbeginselen, volgens het Uniswap v3-whitepaper (Adams, Zinsmeister, Salem, Keefer, Robinson, 2021) en de kerncontracten, en eindigt waar elke serieuze LP-discussie tegenwoordig begint: loss-versus-rebalancing.
Van x·y = k naar virtuele reserves
Uniswap v2 is de constant-product market maker: een pool houdt reserves van token0 en van token1 aan en handhaaft bij elke swap
De marginale prijs van token0 in eenheden van token1 is , en liquiditeit is uniform verspreid over de hele prijsas . Dat is in extreme mate kapitaalinefficiënt: een stablecoin-paar dat handelt tussen 0,999 en 1,001 houdt meer dan 99% van zijn kapitaal gepositioneerd op prijzen die nooit zullen printen.
De zet van v3 is om elke LP hun kapitaal te laten beperken tot een range . Binnen de range moet de positie zich exact gedragen als een v2-pool — dezelfde bonding curve, dezelfde marginale prijsvorming — maar met gebruik van alleen de reserves die nodig zijn om die range te dekken. Het whitepaper formaliseert dit met virtuele reserves: de positie gedraagt zich alsof ze v2-achtige reserves aanhoudt die op een curve liggen, terwijl de echte reserves de virtuele reserves zijn minus wat de positie zou aanhouden bij de grenzen van de range:
Dit is vergelijking 2.2 uit het whitepaper, en het is de belangrijkste vergelijking in v3. De vertaalde curve raakt de assen: bij bereikt de echte -reserve nul (de positie is 100% token1), en bij bereikt de echte -reserve nul (100% token0). Buiten de range wordt de positie inert — een vaste zak van één token, die niets verdient.

De parameter , genaamd liquiditeit, is de invariant die vervangt. Hij wordt gedefinieerd als , en heeft een heldere interpretatie die het whitepaper expliciet maakt: liquiditeit is "virtuele reserves per eenheid vierkantswortelprijs". Bij elke prijs binnen de range zijn de virtuele reserves
Waarom de statusvariabele √P is
De kern van v3 volgt de prijs niet. Ze volgt , opgeslagen als sqrtPriceX96, een unsigned Q64.96 fixed-point getal:
waarbij de ruwe prijs is: token1-basiseenheden per token0-basiseenheid, inclusief decimalen (hierover later meer). De reden voor de vierkantswortel is geen gas-golfje, het is algebra. Differentieer de identiteiten voor virtuele reserves en je krijgt de twee fundamentele swap-vergelijkingen, geïmplementeerd in de SqrtPriceMath-library:
Beide token-delta's zijn lineair in de vierkantswortelprijs (of de reciproque ervan), met als evenredigheidsconstante. Een swap binnen één tick vereist daarom geen curve-inversie, geen Newton-iteratie — slechts één vermenigvuldiging om te verplaatsen, gevolgd door twee vermenigvuldigingen om de bedragen te berekenen. Wanneer een swap groot genoeg is om de prijs over een geïnitialiseerde tick heen te duwen, steekt de pool die tick over, voegt de daar gerefereerde netto-liquiditeit toe of trekt die af (liquidityNet), en gaat verder met de nieuwe geaggregeerde . Globaal gezien is de pool een stuksgewijs-constant-product-AMM: constante tussen geïnitialiseerde ticks, sprongen bij de ticks.
Voor een market maker is dit het juiste mentale model: het geaggregeerde -profiel van de pool is het DEX-equivalent van orderboekdiepte. Waar CLOB-diepte wordt genoteerd in eenheden per prijsniveau, is AMM-diepte per tick — en de omrekening is precies de -identiteit hierboven.
Ticks: een logaritmisch verdeeld prijsraster
Ranges kunnen niet op willekeurige prijzen beginnen en eindigen; ze moeten aansluiten op ticks. Tick komt overeen met prijs
zodat elke tick één basispunt verwijderd is van zijn buren — niet in absolute termen, maar in relatieve termen. Deze logaritmische verdeling is doelbewust: een vast additief raster zou absurd grof zijn voor een token dat handelt op $0,0001 en absurd fijn bij $100.000, terwijl een geometrisch raster op elke prijsschaal een uniforme resolutie van 1 bp geeft. De TickMath-library rekent beide kanten op — getSqrtRatioAtTick en getTickAtSqrtRatio — met bit-manipulatie over vooraf berekende constanten in plaats van een exponentiële functie aan te roepen. Tick-indexen worden begrensd door MIN_TICK = -887272 en MAX_TICK = 887272, wat het prijsbereik dekt: ruim genoeg voor elk tokenpaar dat kan bestaan binnen uint256-rekenkunde.
Niet elke tick is bruikbaar. Elk fee-tarief legt een tick spacing op, en posities mogen alleen ticks gebruiken die daardoor deelbaar zijn:
| Fee-tarief | Tick spacing | Min. rangebreedte | Typisch gebruik |
|---|---|---|---|
| 0,01% | 1 | ~1 bp | stable/stable |
| 0,05% | 10 | ~10 bp | stable-paren, ETH/stables |
| 0,30% | 60 | ~60 bp | majors |
| 1,00% | 200 | ~2% | exotisch/volatiel |
De tarieven 0,05%/0,30%/1% werden gelanceerd bij de release in mei 2021; het tarief van 0,01% werd toegevoegd via een governance-stemming in november 2021. Een grovere spacing op high-fee pools houdt het aantal potentieel oversteekbare ticks laag en begrenst de gaskosten van swaps.
Eén decimalenval waar iedereen een keer in trapt: de ruwe on-chain prijs is token1 per token0 in basiseenheden. In de mainnet USDC/WETH-pool is token0 USDC (6 decimalen) en token1 WETH (18 decimalen), dus een menselijke prijs van $3.000 per ETH komt overeen met een ruwe prijs van WETH-wei per USDC-eenheid, wat overeenkomt met tick
met . Als je monitoringdashboard een tick rond 196k laat zien voor USDC/WETH, weet je nu waarom — en dat de menselijke prijs is, indien nodig omgekeerd.
Tokenbedragen uit (L, prijsrange): de exacte formules
De kernopbrengst van deze sectie: gegeven een positie met liquiditeit op en huidige prijs , wat houdt ze aan? Door de swap-vergelijkingen over de range te integreren, krijg je drie gevallen:
Prijs onder de range () — de positie is 100% token0:
Prijs boven de range () — de positie is 100% token1:
Prijs binnen de range ():
Dit is precies wat SqrtPriceMath.getAmount0Delta en getAmount1Delta berekenen (met gerichte afronding — het contract rondt altijd af in het nadeel van de gebruiker, een detail dat ertoe doet als je dit repliceert in een backtester en je afvraagt waar wei-niveau-afwijkingen vandaan komen).
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)
Uitgewerkt voorbeeld: ETH/USDC, range [2500, 3500]
Stel ETH handelt op USDC en je wilt 1 ETH storten in de range . Vierkantswortels: , , .
Het ETH-been legt vast:
Het USDC-been volgt vervolgens:
Het minten van deze positie kost dus 1 ETH + 3.523,69 USDC, totale waarde $6.523,69, en merk op dat de benen niet 50/50 zijn — de verdeling hangt af van waar zich binnen de range bevindt (een asymmetrische range is precies hoe je een directionele visie uitdrukt, of een zuivere "range order" plaatst met één token).
Loop nu de prijs naar de grenzen:
- Bij is de positie volledig omgezet naar ETH: ETH, ter waarde van $5.716,68. Je hebt 1,2867 ETH gekocht op weg naar beneden tegen een liquiditeitsgewogen gemiddelde prijs van 3523,69 / 1,2867 \approx \2{.}739$.
- Bij is ze volledig omgezet naar USDC: USDC. Je hebt je ETH verkocht op weg naar boven tegen een gemiddelde van $3.240.
Dat is de range-order-lezing concreet gemaakt: de positie is een ladder van biedingen van 3000 naar beneden tot 2500 en laatprijzen van 3000 naar boven tot 3500, met een omvang per tick evenredig aan . En concentratie is waar je voor betaald wordt: een full-range v2-achtige positie met hetzelfde kapitaal van $6.523,69 zou hebben — de geconcentreerde positie quote't 12,4× meer diepte per tick, en verdient (zolang ze in range is) fees tegen 12,4× het tarief per dollar kapitaal.
Fee-boekhouding: feeGrowthGlobal en feeGrowthInside
v3-fees rollen niet op in de positie (een bewuste breuk met v2, waar fees herbelegd werden in ). Ze accumuleren naast elkaar, per token, en de boekhouding is een klein meesterwerk van O(1)-administratie dat het waard is te begrijpen, omdat elke LP-analysepijplijn dit opnieuw implementeert.
De pool onderhoudt twee globale accumulators, feeGrowthGlobal0X128 en feeGrowthGlobal1X128: cumulatieve fees per eenheid liquiditeit sinds de oprichting van de pool, in Q128.128 fixed point. Bij elke swap wordt het fee-bedrag (genomen uit het inputtoken) gedeeld door de huidige in-range en toegevoegd aan de accumulator. Dit is het mechanisme achter de kardinale regel van v3-economie: fees accumuleren alleen naar liquiditeit die in range is op het moment van de swap. Out-of-range posities zijn niet alleen inerte inventaris — ze verdienen exact nul terwijl ze inactief zijn.
Om de juiste plak van globale groei toe te wijzen aan een eindige range, slaat elke geïnitialiseerde tick feeGrowthOutside0/1X128 op — de fee-groei die plaatsvond aan de andere kant van de tick ten opzichte van de huidige prijs (de waarde wisselt van interpretatie elke keer dat de tick wordt overgestoken, wat het schema O(1) maakt). Voor een positie op met huidige tick geldt dan:
Elke positie slaat een snapshot feeGrowthInsideLast op, en niet-geïnde fees zijn simpelweg
lui bijgewerkt telkens wanneer de positie wordt aangeraakt. Twee praktische gevolgen. Ten eerste zijn feeGrowthInside-delta's de enige eerlijke manier om het fee-inkomen van een positie te meten — het bemonsteren van poolniveau-volume en pro-rata verdelen over jouw aandeel in TVL geeft een fout resultaat wanneer de prijs rond je rangegrenzen dwaalt. Ten tweede, omdat fees blijven staan als tokensOwed in plaats van op te rollen, hebben gerealiseerde LP-rendementen een cash-drag-term die v2 niet had; auto-compounding vaults bestaan precies om dit weg te arbitreren (minus hun eigen fee).

De payoff: een short straddle waarvoor je (misschien) betaald wordt
Binnen de range is de positiewaarde als functie van de prijs
Concaaf in — de term is het hele verhaal. Buiten de range wordt het lineair: helling eronder (je bent long een vaste ETH-zak), helling 0 erboven (je bent flat in USDC). Doorloop ons uitgewerkte voorbeeld over de range en vergelijk het met simpelweg de gestorte tokens aanhouden:
| (USDC/ETH) | LP-waarde | HODL-waarde | Divergentie |
|---|---|---|---|
| 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 |
De LP presteert slechter dan HODL in beide richtingen en evenaart het alleen bij de mint-prijs. Beperkte upside, versterkte deelname aan de downside, maximale relatieve waarde bij de strike... dit is de payoff van een short straddle (preciezer: zodra de lineaire staarten worden verrekend tegen de HODL-benchmark, een short-positie in een reeks opties met strikes verspreid over ). De fee-stroom is de premie. Het concentreren van de range is het kiezen van krappere strikes: meer premie per tijdseenheid terwijl je in range bent, snellere en diepere divergentie wanneer de prijs beweegt. Elke v3-LP is een short-volatiliteitshandelaar, of ze dat nu bewust kozen of niet — de on-chain tegenhanger van de inventarisrisico-afweging die Avellaneda–Stoikov formaliseert voor orderboek-market-makers, waarbij de rangebreedte de rol van de gequote spread speelt.

De traditionele naam voor het gat in die tabel is impermanent (divergentie) verlies, maar IL-versus-HODL is een gebrekkige benchmark: het vermengt het verlies dat je leed als liquiditeitsverschaffer met het marktrisico dat je toch al zou hebben gedragen. De scherpere ontleding komt van Milionis, Moallemi, Roughgarden en Zhang (2022), "Automated Market Making and Loss-Versus-Rebalancing" (arXiv:2208.06046). Benchmark de LP niet tegen HODL maar tegen een rebalancerende portefeuille die op elk moment dezelfde tokenhoeveelheden aanhoudt als de pool — maar handelt tegen de frictieloze externe marktprijs in plaats van tegen arbitrageurs. Het verschil is LVR ("lever"): de component van LP P&L die pure adverse selection is, betaald aan arbitrageurs die de verouderde quotes van de AMM oppikken na elke prijsbeweging. Onder een geometrische Brownse prijsbeweging met volatiliteit accumuleert LVR met de momentane snelheid
— een "Black–Scholes-formule voor AMM's", zoals de auteurs het stellen, met de marginale diepte van de pool bij de huidige prijs. Voor de full-range constant-product-pool valt dit terug op de beroemde van poolwaarde per tijdseenheid: bij 5% dagelijkse volatiliteit lekt ruwweg 3,1 bp van de pool elke dag weg naar arbitrageurs, fees of geen fees. Concentratie vermenigvuldigt — ons voorbeeld met 12,4× diepte hierboven is ook een ~12,4× LVR-machine zolang het in range is. Fees moeten die uitstroom overtreffen wil de positie +EV zijn, en LVR (in tegenstelling tot "IL") is een afdekbare, voorspelbare lopende kost, wat het de juiste rekeneenheid maakt. De volledige winstgevendheidsberekening — fee-APR versus LVR versus gerealiseerde volatiliteit, wanneer LPing wint van delta-gehedgede short opties — is het onderwerp van de aankomende diepgaande analyse van impermanent loss en LVR, en de hedgingmechanica krijgt zijn eigen behandeling in v3 LP-strategieën en hedging.
Nog een kost schuilt één laag dieper: omdat AMM-quotes alleen updaten wanneer iemand handelt, is elke LP-positie ook blootgesteld aan de mempool-spelletjes die rond die trades worden gespeeld — sandwich-aanvallen tegen de swappers die jouw fees betalen, en arbitragebundels die jouw LVR block per block realiseren. Dat ecosysteem wordt in kaart gebracht in het artikel over MEV en sandwich-aanvallen.
Poolstatus on-chain uitlezen
Alles hierboven is observeerbaar via drie goedkope calls op het poolcontract.
slot0() pakt de hot state samen in één storage slot: sqrtPriceX96, de huidige tick, oracle-observatie-indices, en de protocol-fee/lock-vlaggen. liquidity() geeft de huidige geaggregeerde in-range terug — merk op dat dit geen TVL is; het is de diepteparameter die actief is op de huidige tick, en die springt discontinu wanneer de prijs een geïnitialiseerde tick oversteekt. ticks(int24) geeft per-tick status terug: liquidityGross (totale die naar de tick verwijst), liquidityNet (signed toegevoegd bij het oversteken van links naar rechts), en de feeGrowthOutside-accumulators. Door liquidityNet te itereren over geïnitialiseerde ticks (de tickBitmap vertelt je welke ticks bestaan zonder alle 1,7 miljoen te scannen) reconstrueer je het volledige diepteprofiel — jouw orderboeksnapshot.
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)
Posities zelf leven op twee plaatsen. De kernpool sleutelt ze op (owner, tickLower, tickUpper) — één geaggregeerd slot per eigenaar-range-drietal. Particuliere gebruikers en de meeste fondsen minten in plaats daarvan via de periferie NonfungiblePositionManager (mainnet: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88), die elke positie verpakt in een ERC-721-NFT en positions(tokenId) blootlegt, die de volledige tuple teruggeeft: tokens, fee-tarief, range, , feeGrowthInsideLast, en tokensOwed. Voor portefeuillebewaking geeft die ene call plus de huidige feeGrowthInside van de pool (herberekend uit ticks() zoals in de vorige sectie) je mark-to-market-waarde en opgebouwde fees zonder een indexer aan te raken — hoewel je voor alles wat historisch is swap-event-replay of een subgraph nodig hebt, omdat fee-groei padafhankelijk is en de chain alleen de huidige accumulators opslaat.
Twee operationele details die het waard zijn om je eigen te maken voordat je kapitaal inzet. Ten eerste kwantiseert tick spacing je strategieruimte: op de 0,05%-tier kun je ranges van 10 bp breed plaatsen en iets draaien dat dicht bij een echte limietorder komt (mint net boven/onder spot, int de fee, burn na het oversteken — het whitepaper kadert dit "range order"-gebruik expliciet), terwijl je minimale range op de 1%-tier ~2% breed is en de LOB-analogie grof wordt. Ten tweede is een range order een limietorder zonder annuleringsprioriteit: als de prijs je range oversteekt en terugkeert, maak je een rondreis met je inventaris en geef je de edge terug (met behoud van de fees). Passieve ranges zijn quotes die je niet kunt intrekken — precies waarom de vraag naar rebalancingfrequentie, en de LVR-lens om die te beantwoorden, er zo toe doen.
Waar dit je achterlaat
De v3-stack, samengevat: prijzen zijn ticks op een 1,0001-geometrisch raster; diepte is , om te zetten naar tokenbedragen met niets meer dan verschillen van vierkantswortels; fees zijn een per--accumulator die je verschilt over je rangegrenzen; en de resulterende positie is een short-vol range order waarvan de lopende kost een naam, een formule en een arXiv-nummer heeft. Met de primitieven in handen worden de interessante vragen kwantitatief: hoe breed, hoe vaak rebalanceren, en of fees LVR dekken voor een gegeven pool en regime — wat precies is waar deze serie vervolgens naartoe gaat.
Referenties
- Adams, H., Zinsmeister, N., Salem, M., Keefer, R., Robinson, D. (2021). Uniswap v3 Core (whitepaper).
- Uniswap v3-kernlibraries:
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.
Auteurs
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.