Uniswap v3 für Quants: Concentrated Liquidity und Tick-Mathematik von Grund auf
Eine LP-Position auf Uniswap v3 ist eine getarnte Range-Order aus dem Limit-Orderbuch. Hinterlegt man Liquidität zwischen zwei Preisen, verpflichtet man sich, den Asset auf dem Weg nach unten innerhalb dieser Range zu kaufen und auf dem Weg nach oben zu verkaufen — genau das, was ein Gitter ruhender Limit-Orders auf einem CLOB tut. Der Smart Contract speichert jedoch keine Preise, Mengen oder ein Orderbuch. Er speichert drei Zahlen: einen Quadratwurzel-Preis im Q64.96-Festkommaformat, einen ganzzahligen Tick-Index und einen aggregierten Liquiditätswert . Wer eine v3-Position so durchdenken will wie Quotes in einem Market-Making-Modell — Inventar, Fill-Preise, Adverse Selection — muss zwischen diesen On-Chain-Primitiven und den Trading-Konzepten übersetzen können, die sie kodieren. Dieser Artikel baut diese Übersetzung von Grund auf, entlang des Uniswap-v3-Whitepapers (Adams, Zinsmeister, Salem, Keefer, Robinson, 2021) und der Kern-Contracts, und endet dort, wo jede ernsthafte LP-Diskussion heute beginnt: bei Loss-versus-Rebalancing.
Von x·y = k zu virtuellen Reserven
Uniswap v2 ist der Constant-Product-Market-Maker: ein Pool hält Reserven von Token0 und von Token1 und erzwingt bei jedem Swap
Der Marginalpreis von Token0 in Einheiten von Token1 ist , und die Liquidität ist gleichmäßig über die gesamte Preisachse verteilt. Das ist extrem kapitalineffizient: Ein Stablecoin-Paar, das zwischen 0,999 und 1,001 handelt, hält mehr als 99% seines Kapitals bei Preisen, die niemals gedruckt werden.
Der Clou von v3 besteht darin, jedem LP zu erlauben, sein Kapital auf eine Range zu beschränken. Innerhalb der Range muss sich die Position exakt wie ein v2-Pool verhalten — gleiche Bonding-Curve, gleiche Marginalpreisbildung — jedoch nur mit den Reserven, die zur Abdeckung dieser Range nötig sind. Das Whitepaper formalisiert dies mit virtuellen Reserven: Die Position verhält sich, als hielte sie v2-artige Reserven auf einer Kurve , während die realen Reserven die virtuellen abzüglich dessen sind, was die Position an den Range-Grenzen halten würde:
Dies ist Gleichung 2.2 des Whitepapers und die mit Abstand wichtigste Gleichung in v3. Die transformierte Kurve berührt die Achsen: bei fällt die reale -Reserve auf null (Position ist zu 100% Token1), und bei fällt die reale -Reserve auf null (100% Token0). Außerhalb der Range wird die Position inaktiv — ein fester Bestand eines einzigen Tokens, der nichts mehr verdient.

Der Parameter , genannt Liquidität, ist die Invariante, die ersetzt. Er ist definiert als und hat eine klare Interpretation, die das Whitepaper explizit macht: Liquidität ist "virtuelle Reserven pro Einheit Quadratwurzel-Preis". Bei jedem Preis innerhalb der Range gilt für die virtuellen Reserven
Warum die Zustandsvariable √P ist
Der Kern von v3 verfolgt nicht den Preis. Er verfolgt , gespeichert als sqrtPriceX96, eine vorzeichenlose Q64.96-Festkommazahl:
wobei der rohe Preis ist: Token1-Basiseinheiten pro Token0-Basiseinheit, Dezimalstellen inklusive (dazu gleich mehr). Der Grund für die Quadratwurzel ist keine Gas-Optimierung, sondern Algebra. Differenziert man die virtuellen-Reserven-Identitäten, erhält man die beiden fundamentalen Swap-Gleichungen, implementiert in der SqrtPriceMath-Bibliothek:
Beide Token-Deltas sind linear im Quadratwurzel-Preis (bzw. seinem Kehrwert), mit als Proportionalitätskonstante. Ein Swap innerhalb eines einzelnen Ticks benötigt daher keine Kurveninversion, keine Newton-Iteration — nur eine Multiplikation, um zu verschieben, dann zwei Multiplikationen zur Berechnung der Beträge. Ist ein Swap groß genug, um den Preis über einen initialisierten Tick zu schieben, überquert der Pool ihn, addiert oder entfernt die dort referenzierte Netto-Liquidität (liquidityNet) und setzt mit dem neuen aggregierten fort. Global betrachtet ist der Pool ein stückweise konstanter Constant-Product-AMM: konstantes zwischen initialisierten Ticks, Sprünge an den Ticks.
Für einen Market Maker ist das das richtige mentale Modell: das aggregierte -Profil des Pools ist das DEX-Äquivalent zur Orderbuch-Tiefe. Wo CLOB-Tiefe in Einheiten pro Preisstufe notiert wird, ist AMM-Tiefe pro Tick — und die Umrechnung ist genau die oben genannte Identität .
Ticks: ein logarithmisch gestaffeltes Preisraster
Ranges können nicht bei beliebigen Preisen beginnen und enden; sie müssen an Ticks einrasten. Tick entspricht dem Preis
sodass jeder Tick einen Basispunkt von seinen Nachbarn entfernt liegt — nicht in absoluten, sondern in relativen Größen. Diese logarithmische Staffelung ist bewusst gewählt: ein festes additives Raster wäre bei einem Token, das zu $0,0001 handelt, absurd grob und bei $100.000 absurd fein, während ein geometrisches Raster auf jeder Preisskala eine gleichmäßige 1-bp-Auflösung liefert. Die TickMath-Bibliothek rechnet in beide Richtungen um — getSqrtRatioAtTick und getTickAtSqrtRatio — mittels Bit-Manipulation über vorberechnete Konstanten statt eines Aufrufs einer Exponentialfunktion. Tick-Indizes sind begrenzt durch MIN_TICK = -887272 und MAX_TICK = 887272, was den Preisbereich abdeckt: weit genug für jedes Token-Paar, das in uint256-Arithmetik existieren kann.
Nicht jeder Tick ist nutzbar. Jede Fee-Stufe erzwingt einen Tick-Spacing, und Positionen dürfen nur Ticks verwenden, die dadurch teilbar sind:
| Fee-Stufe | Tick-Spacing | Min. Range-Breite | Typische Nutzung |
|---|---|---|---|
| 0,01% | 1 | ~1 bp | Stable/Stable |
| 0,05% | 10 | ~10 bp | Stable-Paare, ETH/Stables |
| 0,30% | 60 | ~60 bp | Major-Paare |
| 1,00% | 200 | ~2% | Exotisch/volatil |
Die Stufen 0,05%/0,30%/1% gingen im Mai 2021 live; die 0,01%-Stufe wurde im November 2021 per Governance-Abstimmung hinzugefügt. Ein gröberes Spacing bei Pools mit höherer Fee hält die Zahl potenziell überquerbarer Ticks gering und begrenzt die Gaskosten von Swaps.
Eine Dezimalstellen-Falle, in die jeder einmal tappt: Der rohe On-Chain-Preis ist Token1 pro Token0 in Basiseinheiten. Im Mainnet-USDC/WETH-Pool ist Token0 USDC (6 Dezimalstellen) und Token1 WETH (18 Dezimalstellen), sodass ein menschlicher Preis von $3.000 pro ETH einem rohen Preis von WETH-Wei pro USDC-Einheit entspricht, was bei Tick
liegt, mit . Wenn Ihr Monitoring-Dashboard für USDC/WETH einen Tick nahe 196k anzeigt, wissen Sie jetzt, warum — und dass der menschliche Preis ist, bei Bedarf invertiert.
Token-Beträge aus (L, Preisrange): die exakten Formeln
Das Kernergebnis dieses Abschnitts: Gegeben eine Position mit Liquidität auf und aktuellem Preis — was hält sie? Integriert man die Swap-Gleichungen über die Range, ergeben sich drei Fälle:
Preis unterhalb der Range () — die Position ist zu 100% Token0:
Preis oberhalb der Range () — die Position ist zu 100% Token1:
Preis in der Range ():
Das ist genau das, was SqrtPriceMath.getAmount0Delta und getAmount1Delta berechnen (mit gerichtetem Runden — der Contract rundet immer zulasten des Nutzers, ein Detail, das relevant wird, wenn man dies in einem Backtester nachbildet und sich über Abweichungen auf Wei-Ebene wundert).
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)
Durchgerechnetes Beispiel: ETH/USDC, Range [2500, 3500]
Angenommen, ETH handelt bei USDC, und Sie möchten 1 ETH in die Range einzahlen. Quadratwurzeln: , , .
Das ETH-Bein legt fest:
Das USDC-Bein folgt dann:
Das Minten dieser Position erfordert also 1 ETH + 3.523,69 USDC, Gesamtwert $6.523,69, und beachten Sie: die beiden Beine sind nicht 50/50 gesplittet — der Split hängt davon ab, wo innerhalb der Range liegt (eine asymmetrische Range ist genau die Art, eine gerichtete Meinung auszudrücken, oder eine reine "Range-Order" mit nur einem Token zu platzieren).
Nun bewegen wir den Preis zu den Grenzen:
- Bei hat sich die Position vollständig in ETH umgewandelt: ETH, im Wert von $5.716,68. Sie haben 1,2867 ETH auf dem Weg nach unten zu einem liquiditätsgewichteten Durchschnittspreis von 3523.69 / 1.2867 \approx \2{,}739$ gekauft.
- Bei hat sie sich vollständig in USDC umgewandelt: USDC. Sie haben Ihr ETH auf dem Weg nach oben zu einem Durchschnitt von $3.240 verkauft.
Das ist die Range-Order-Lesart ganz konkret: Die Position ist eine Leiter aus Geboten (Bids) von 3000 abwärts bis 2500 und Angeboten (Asks) von 3000 aufwärts bis 3500, mit einer Größe pro Tick proportional zu . Und Konzentration ist das, wofür man bezahlt wird: eine Full-Range-v2-artige Position mit demselben Kapital von $6.523,69 hätte — die konzentrierte Position quotiert 12,4-mal mehr Tiefe pro Tick und verdient (solange sie in der Range ist) Fees mit der 12,4-fachen Rate pro Dollar Kapital.
Fee-Buchhaltung: feeGrowthGlobal und feeGrowthInside
Fees in v3 werden nicht in die Position hineinkomponiert (ein bewusster Bruch mit v2, wo Fees in reinvestiert wurden). Sie akkumulieren separat, pro Token, und die Buchhaltung ist ein kleines Meisterwerk O(1)-fähiger Buchführung, das zu verstehen sich lohnt, weil jede LP-Analytics-Pipeline sie neu implementiert.
Der Pool führt zwei globale Akkumulatoren, feeGrowthGlobal0X128 und feeGrowthGlobal1X128: kumulative Fees pro Einheit Liquidität seit Pool-Erstellung, im Q128.128-Festkommaformat. Bei jedem Swap wird der Fee-Betrag (aus dem eingezahlten Token entnommen) durch das aktuell in der Range aktive geteilt und dem Akkumulator hinzugefügt. Dies ist der Mechanismus hinter der Kardinalregel der v3-Ökonomie: Fees fließen nur an Liquidität, die zum Zeitpunkt des Swaps in der Range aktiv ist. Positionen außerhalb der Range sind nicht bloß inaktives Inventar — sie verdienen exakt null, solange sie inaktiv sind.
Um den korrekten Anteil des globalen Wachstums einer endlichen Range zuzuordnen, speichert jeder initialisierte Tick feeGrowthOutside0/1X128 — das Fee-Wachstum, das auf der anderen Seite des Ticks relativ zum aktuellen Preis stattfand (der Wert kehrt seine Interpretation bei jeder Tick-Überquerung um, was das Schema O(1) macht). Für eine Position auf mit aktuellem Tick gilt dann:
Jede Position speichert einen Snapshot feeGrowthInsideLast, und nicht abgeholte Fees sind schlicht
der träge aktualisiert wird, sobald die Position berührt wird. Zwei praktische Konsequenzen: Erstens sind feeGrowthInside-Deltas der einzige ehrliche Weg, das Fee-Einkommen einer Position zu messen — Pool-Level-Volumen zu erfassen und nach eigenem Anteil am TVL zu prorata-teilen, geht immer dann schief, wenn der Preis um die Range-Grenzen herum wandert. Zweitens: Weil Fees als tokensOwed liegen bleiben statt zu komponieren, haben realisierte LP-Renditen einen Cash-Drag-Term, den v2 nicht kannte; Auto-Compounding-Vaults existieren genau deshalb, um dies wegzuarbitrieren (abzüglich ihrer eigenen Fee).

Der Payoff: ein Short Straddle, für dessen Halten man bezahlt wird (vielleicht)
Innerhalb der Range ist der Positionswert als Funktion des Preises
Konkav in — der Term ist die ganze Geschichte. Außerhalb der Range wird er linear: Steigung unterhalb (man ist long einen festen ETH-Bestand), Steigung 0 oberhalb (man ist flach in USDC). Rechnen wir unser Beispiel über die Range durch und vergleichen es mit dem simplen Halten der eingezahlten Tokens:
| (USDC/ETH) | LP-Wert | HODL-Wert | Divergenz |
|---|---|---|---|
| 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 |
Der LP unterperformt HODL in beide Richtungen und erreicht Gleichstand nur beim Minting-Preis. Gedeckelte Aufwärtsseite, verstärkte Partizipation an der Abwärtsseite, maximaler relativer Wert am Strike... das ist der Payoff eines Short Straddle (genauer, sobald die linearen Enden gegen die HODL-Benchmark verrechnet werden: eine Short-Position in einem Streifen von Optionen mit Strikes über ). Der Fee-Strom ist die Prämie. Die Range zu konzentrieren bedeutet, engere Strikes zu wählen: mehr Prämie pro Zeiteinheit, solange man in der Range ist, dafür schnellere und tiefere Divergenz, wenn sich der Preis bewegt. Jeder v3-LP ist ein Short-Volatility-Trader, ob er es so gewählt hat oder nicht — das On-Chain-Pendant zum Inventory-Risk-Trade-off, den Avellaneda–Stoikov für Orderbuch-Market-Maker formalisieren, wobei die Range-Breite die Rolle des quotierten Spreads übernimmt.

Der traditionelle Name für die Lücke in dieser Tabelle ist Impermanent (Divergence) Loss, aber IL-versus-HODL ist eine fehlerhafte Benchmark: Sie vermengt den Verlust, den man als Liquidity Provider erlitten hat, mit dem Marktrisiko, das man ohnehin getragen hätte. Die schärfere Zerlegung stammt von Milionis, Moallemi, Roughgarden und Zhang (2022), "Automated Market Making and Loss-Versus-Rebalancing" (arXiv:2208.06046). Man vergleicht den LP nicht mit HODL, sondern mit einem Rebalancing-Portfolio, das zu jedem Zeitpunkt dieselben Token-Mengen wie der Pool hält — aber zum reibungsfreien externen Marktpreis handelt statt gegen Arbitrageure. Die Differenz ist LVR ("Lever"): die Komponente des LP-P&L, die reine Adverse Selection ist, gezahlt an Arbitrageure, die die veralteten Quotes des AMM nach jeder Preisbewegung abgreifen. Unter einer geometrischen Brown'schen Preisbewegung mit Volatilität akkumuliert LVR mit der instantanen Rate
— eine "Black-Scholes-Formel für AMMs", wie die Autoren es formulieren, mit als marginaler Tiefe des Pools beim aktuellen Preis. Für den Full-Range-Constant-Product-Pool kollabiert dies zum berühmten des Poolwerts pro Zeiteinheit: bei 5% täglicher Volatilität bluten grob 3,1 bp des Pools jeden Tag an Arbitrageure ab, Fees hin oder her. Konzentration multipliziert — unser 12,4×-Tiefenbeispiel von oben ist gleichzeitig eine ~12,4×-LVR-Maschine, solange man in der Range ist. Fees müssen dieses Ausbluten überholen, damit die Position +EV ist, und LVR ist (anders als "IL") eine hedgebare, vorhersehbare laufende Kostengröße — das macht sie zur richtigen Rechnungseinheit. Die vollständige Profitabilitätsrechnung — Fee-APR versus LVR versus realisierte Volatilität, wann LPing eine delta-gehedgte Short-Options-Position schlägt — ist Thema des kommenden Deep Dive zu Impermanent Loss und LVR, und die Hedging-Mechanik erhält ihre eigene Behandlung in v3-LP-Strategien und Hedging.
Eine weitere Kostenebene liegt eine Schicht tiefer: Weil AMM-Quotes nur aktualisiert werden, wenn jemand handelt, ist jede LP-Position auch den Mempool-Spielen ausgesetzt, die um diese Trades herum gespielt werden — Sandwich-Angriffe gegen die Swapper, die Ihre Fees zahlen, und Arbitrage-Bundles, die Ihren LVR Block für Block realisieren. Dieses Ökosystem wird in dem Artikel zu MEV und Sandwich-Attacken kartiert.
Pool-Zustand On-Chain auslesen
Alles oben Beschriebene lässt sich anhand von drei günstigen Aufrufen des Pool-Contracts beobachten.
slot0() packt den heißen Zustand in einen einzigen Storage-Slot: sqrtPriceX96, den aktuellen tick, Oracle-Observation-Indizes und die Protocol-Fee-/Lock-Flags. liquidity() liefert das aktuelle in der Range aktive aggregierte — man beachte, dass dies nicht TVL ist; es ist der beim aktuellen Tick aktive Tiefenparameter, der diskontinuierlich springt, sobald der Preis einen initialisierten Tick überquert. ticks(int24) liefert Zustand pro Tick: liquidityGross (gesamtes , das den Tick referenziert), liquidityNet (vorzeichenbehaftetes , das beim Überqueren von links nach rechts hinzugefügt wird) und die feeGrowthOutside-Akkumulatoren. Iteriert man über liquidityNet bei initialisierten Ticks (die tickBitmap verrät, welche existieren, ohne alle 1,7 Millionen zu scannen), lässt sich das vollständige Tiefenprofil rekonstruieren — Ihr Orderbuch-Snapshot.
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)
Positionen selbst leben an zwei Orten. Der Kern-Pool indiziert sie über (owner, tickLower, tickUpper) — ein aggregierter Slot pro Owner-Range-Tripel. Retail-Nutzer und die meisten Fonds minten stattdessen über den Periphery-Contract NonfungiblePositionManager (Mainnet: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88), der jede Position in ein ERC-721-NFT verpackt und positions(tokenId) bereitstellt, das das vollständige Tupel liefert: Tokens, Fee-Stufe, Range, , feeGrowthInsideLast und tokensOwed. Für das Portfolio-Monitoring liefert dieser eine Aufruf plus das aktuelle feeGrowthInside des Pools (neu berechnet aus ticks() wie im vorherigen Abschnitt) Mark-to-Market-Wert und aufgelaufene Fees, ohne einen Indexer anzufassen — für alles Historische braucht man jedoch Swap-Event-Replay oder einen Subgraph, weil Fee-Wachstum pfadabhängig ist und die Chain nur die aktuellen Akkumulatoren speichert.
Zwei operative Details, die man vor dem Kapitaleinsatz verinnerlichen sollte. Erstens quantisiert das Tick-Spacing den Strategieraum: auf der 0,05%-Stufe kann man 10-bp-breite Ranges platzieren und etwas laufen lassen, das einer echten Limit-Order sehr nahekommt (knapp über/unter Spot minten, die Fee kassieren, nach der Überquerung burnen — das Whitepaper rahmt diesen "Range-Order"-Anwendungsfall explizit), während auf der 1%-Stufe die Mindest-Range ~2% breit ist und die LOB-Analogie grob wird. Zweitens ist eine Range-Order eine Limit-Order ohne Storno-Priorität: Überquert der Preis Ihre Range und kehrt zurück, machen Sie einen Roundtrip mit dem Inventar und geben den Vorteil wieder her (behalten aber die Fees). Passive Ranges sind Quotes, die man nicht zurückziehen kann — genau deshalb ist die Frage nach der Rebalancing-Frequenz, und die LVR-Linse zu ihrer Beantwortung, so wichtig.
Wo das hinführt
Der v3-Stack, komprimiert: Preise sind Ticks auf einem 1,0001-geometrischen Raster; Tiefe ist , umrechenbar in Token-Beträge durch nichts weiter als Differenzen von Quadratwurzeln; Fees sind ein Akkumulator pro , den man über die Grenzen der eigenen Range hinweg differenziert; und die resultierende Position ist eine Short-Vol-Range-Order, deren laufende Kosten einen Namen, eine Formel und eine arXiv-Nummer haben. Mit diesen Primitiven in der Hand werden die interessanten Fragen quantitativ: wie breit, wie oft rebalancieren, und ob Fees den LVR für einen gegebenen Pool und ein gegebenes Regime ausgleichen — genau dorthin führt diese Serie als Nächstes.
Referenzen
- Adams, H., Zinsmeister, N., Salem, M., Keefer, R., Robinson, D. (2021). Uniswap v3 Core (Whitepaper).
- Uniswap-v3-Kernbibliotheken:
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.