Uniswap v3 per quant: liquidità concentrata e matematica dei tick dai principi primi
Una posizione LP su Uniswap v3 e, sotto mentite spoglie, un range order da order book a limite. Depositi liquidità tra due prezzi e ti sei impegnato a comprare l'asset mentre scende attraverso il tuo range e a venderlo mentre sale — esattamente cio che fa una griglia di ordini limite in attesa su un CLOB. Ma il contratto non memorizza prezzi, quantita o un order book. Memorizza tre numeri: un prezzo in radice quadrata in virgola fissa Q64.96, un indice di tick intero, e un valore di liquidità aggregata . Se vuoi ragionare su una posizione v3 nello stesso modo in cui ragioneresti sulle quote in un modello di market making — inventario, prezzi di esecuzione, selezione avversa — devi essere in grado di tradurre tra queste primitive on-chain e i concetti di trading che codificano. Questo articolo costruisce quella traduzione dai principi primi, seguendo il whitepaper di Uniswap v3 (Adams, Zinsmeister, Salem, Keefer, Robinson, 2021) e i contratti core, e termina dove ormai inizia ogni discussione seria sugli LP: loss-versus-rebalancing.
Da x·y = k alle riserve virtuali
Uniswap v2 e il market maker a prodotto costante: un pool detiene riserve di token0 e di token1 e impone
a ogni swap. Il prezzo marginale di token0 in unita di token1 e , e la liquidità e distribuita uniformemente sull'intero asse dei prezzi . Questo e capital-inefficiente all'estremo: una coppia di stablecoin che scambia tra 0.999 e 1.001 mantiene oltre il 99% del proprio capitale posizionato a prezzi che non si verificheranno mai.
La mossa di v3 e permettere a ogni LP di restringere il proprio capitale a un range . All'interno del range, la posizione deve comportarsi esattamente come un pool v2 — stessa curva di bonding, stesso pricing marginale — ma usando solo le riserve necessarie a coprire quel range. Il whitepaper formalizza questo con le riserve virtuali: la posizione agisce come se detenesse riserve in stile v2 che giacciono su una curva , mentre le riserve reali sono quelle virtuali meno cio che la posizione deterrebbe ai confini del range:
Questa e l'equazione 2.2 del whitepaper, ed e l'equazione singola piu importante in v3. La curva traslata tocca gli assi: a la riserva reale tocca zero (posizione al 100% token1), e a la riserva reale tocca zero (100% token0). Fuori dal range la posizione diventa inerte — un sacco fisso di un solo token, che non guadagna nulla.

Il parametro , chiamato liquidità, e l'invariante che sostituisce . E definito come , e ha un'interpretazione pulita che il whitepaper rende esplicita: la liquidità e "riserve virtuali per unita di radice quadrata del prezzo". A qualsiasi prezzo all'interno del range, le riserve virtuali sono
Perche la variabile di stato e √P
Il core di v3 non traccia il prezzo. Traccia , memorizzato come sqrtPriceX96, un numero unsigned in virgola fissa Q64.96:
dove e il prezzo raw: unita base di token1 per unita base di token0, decimali inclusi (approfondiamo piu avanti). La ragione della radice quadrata non e ottimizzazione del gas, e algebra. Differenzia le identita delle riserve virtuali e ottieni le due equazioni fondamentali dello swap, implementate nella libreria SqrtPriceMath:
Entrambi i delta dei token sono lineari nella radice quadrata del prezzo (o nel suo reciproco), con come costante di proporzionalità. Uno swap all'interno di un singolo tick non richiede quindi alcuna inversione di curva, nessuna iterazione di Newton — solo una moltiplicazione per muovere , poi due moltiplicazioni per calcolare gli importi. Quando uno swap e abbastanza grande da spingere il prezzo oltre un tick inizializzato, il pool lo attraversa, aggiunge o rimuove la liquidità netta referenziata li (liquidityNet), e continua con il nuovo aggregato. Globalmente, il pool e un AMM a prodotto costante a tratti: costante tra i tick inizializzati, salti in corrispondenza dei tick.
Per un market maker, questo e il modello mentale corretto: il profilo aggregato del pool e l'equivalente DEX della profondita dell'order book. Dove la profondita CLOB e quotata in unita per livello di prezzo, la profondita AMM e per tick — e la conversione e esattamente l'identità vista sopra.
Tick: una griglia di prezzi a spaziatura logaritmica
I range non possono iniziare e finire a prezzi arbitrari; devono agganciarsi ai tick. Il tick corrisponde al prezzo
quindi ogni tick dista un basis point dai suoi vicini — non in termini assoluti, ma relativi. Questa spaziatura logaritmica e deliberata: una griglia additiva fissa sarebbe assurdamente grossolana per un token che scambia a $0.0001 e assurdamente fine a $100{,}000, mentre una griglia geometrica fornisce una risoluzione uniforme di 1 bp a ogni scala di prezzo. La libreria TickMath converte in entrambe le direzioni — getSqrtRatioAtTick e getTickAtSqrtRatio — usando manipolazioni di bit su costanti precalcolate anziché chiamare una funzione esponenziale. Gli indici di tick sono limitati da MIN_TICK = -887272 e MAX_TICK = 887272, il che copre l'intervallo di prezzo : abbastanza ampio per qualsiasi coppia di token che possa esistere nell'aritmetica uint256.
Non tutti i tick sono utilizzabili. Ogni fascia di commissione impone una tick spacing, e le posizioni possono usare solo tick divisibili per essa:
| Fascia commissione | Tick spacing | Larghezza minima range | Uso tipico |
|---|---|---|---|
| 0.01% | 1 | ~1 bp | stable/stable |
| 0.05% | 10 | ~10 bp | coppie stabili, ETH/stable |
| 0.30% | 60 | ~60 bp | major |
| 1.00% | 200 | ~2% | esotiche / volatili |
Le fasce 0.05%/0.30%/1% sono state lanciate a maggio 2021; la fascia 0.01% e stata aggiunta tramite voto di governance a novembre 2021. Una spaziatura piu grossolana sui pool ad alta commissione mantiene basso il numero di tick potenzialmente attraversabili e limita i costi gas degli swap.
Una trappola sui decimali in cui tutti inciampano prima o poi: il prezzo raw on-chain e token1 per token0 in unita base. Nel pool mainnet USDC/WETH, token0 e USDC (6 decimali) e token1 e WETH (18 decimali), quindi un prezzo umano di $3{,}000 per ETH corrisponde a un prezzo raw di WETH-wei per unita USDC, che si trova al tick
con . Se la tua dashboard di monitoraggio mostra un tick vicino a 196k per USDC/WETH, ora sai il perche — e che il prezzo umano e invertito secondo necessita.
Importi dei token da (L, range di prezzo): le formule esatte
Il risultato centrale di questa sezione: data una posizione con liquidità su e prezzo corrente , cosa detiene? Integrando le equazioni di swap sul range si ottengono tre casi:
Prezzo sotto il range () — la posizione e al 100% token0:
Prezzo sopra il range () — la posizione e al 100% token1:
Prezzo nel range ():
Sono esattamente cio che calcolano SqrtPriceMath.getAmount0Delta e getAmount1Delta (con arrotondamento direzionato — il contratto arrotonda sempre a sfavore dell'utente, un dettaglio che conta se replichi questo in un backtester e ti chiedi il perche di discrepanze a livello di 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)
Esempio svolto: ETH/USDC, range [2500, 3500]
Supponi che ETH scambi a USDC e che tu voglia depositare 1 ETH nel range . Radici quadrate: , , .
La gamba ETH fissa :
La gamba USDC segue di conseguenza:
Quindi mintare questa posizione richiede 1 ETH + 3,523.69 USDC, valore totale $6,523.69, e nota che le due gambe non sono 50/50 — la ripartizione dipende da dove si trova all'interno del range (un range asimmetrico e esattamente il modo in cui esprimi una view direzionale, o piazzi un "range order" puro con un solo token).
Ora percorri il prezzo fino ai confini:
- A la posizione si e convertita interamente in ETH: ETH, del valore di $5,716.68. Hai comprato 1.2867 ETH durante la discesa a un prezzo medio ponderato per liquidità di 3523.69 / 1.2867 \approx \2{,}739$.
- A si e convertita interamente in USDC: USDC. Hai venduto il tuo ETH durante la salita a una media di $3,240.
Questa e la lettura da range order resa concreta: la posizione e una scala di bid da 3000 fino a 2500 e ask da 3000 fino a 3500, con size per tick proporzionale a . E la concentrazione e cio per cui vieni pagato: una posizione full-range in stile v2 con lo stesso capitale di $6,523.69 avrebbe — la posizione concentrata quota 12.4 volte piu profondita per tick, e (mentre e in range) guadagna commissioni a 12.4 volte il tasso per dollaro di capitale.
Contabilita delle commissioni: feeGrowthGlobal e feeGrowthInside
Le commissioni di v3 non si capitalizzano nella posizione (una rottura deliberata rispetto a v2, dove le commissioni venivano reinvestite in ). Maturano separatamente, per token, e la contabilita e un piccolo capolavoro di bookkeeping O(1) che vale la pena capire perche ogni pipeline di analytics per LP la reimplementa.
Il pool mantiene due accumulatori globali, feeGrowthGlobal0X128 e feeGrowthGlobal1X128: commissioni cumulative per unita di liquidità dall'inception del pool, in virgola fissa Q128.128. A ogni swap, l'importo della commissione (prelevato dal token in input) viene diviso per l' attualmente in range e aggiunto all'accumulatore. Questo e il meccanismo dietro la regola cardine dell'economia di v3: le commissioni maturano solo per la liquidità che e in range nel momento dello swap. Le posizioni fuori range non sono semplicemente inventario inerte — guadagnano esattamente zero mentre sono inattive.
Per attribuire la giusta fetta di crescita globale a un range finito, ogni tick inizializzato memorizza feeGrowthOutside0/1X128 — la crescita delle commissioni avvenuta sul lato opposto del tick rispetto al prezzo corrente (il valore inverte interpretazione ogni volta che il tick viene attraversato, il che e cio che rende lo schema O(1)). Quindi per una posizione su con tick corrente :
Ogni posizione memorizza uno snapshot feeGrowthInsideLast, e le commissioni non ancora raccolte sono semplicemente
aggiornate in modo lazy ogni volta che la posizione viene toccata. Due conseguenze pratiche. Primo, i delta di feeGrowthInside sono l'unico modo onesto per misurare il reddito da commissioni di una posizione — campionare il volume a livello di pool e distribuirlo pro-rata in base alla tua quota di TVL da un risultato sbagliato ogni volta che il prezzo vaga attorno ai confini del tuo range. Secondo, poiche le commissioni restano come tokensOwed invece di capitalizzarsi, i rendimenti LP realizzati hanno un termine di cash-drag che v2 non aveva; i vault ad auto-compounding esistono proprio per arbitraggiare via questo effetto (meno la loro stessa commissione).

Il payoff: uno short straddle che vieni pagato per detenere (forse)
All'interno del range, il valore della posizione in funzione del prezzo e
Concava in — il termine e tutta la storia. Fuori dal range diventa lineare: pendenza sotto (sei long su un sacco fisso di ETH), pendenza 0 sopra (sei flat in USDC). Percorri il nostro esempio svolto lungo il range e confrontalo con il semplice HODL dei token depositati:
| (USDC/ETH) | Valore LP | Valore HODL | Divergenza |
|---|---|---|---|
| 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 |
L'LP sottoperforma l'HODL in entrambe le direzioni e lo eguaglia solo al prezzo di minting. Upside limitato, partecipazione al downside amplificata, valore relativo massimo allo strike... questo e il payoff di uno short straddle (piu precisamente, una volta che le code lineari sono nettate contro il benchmark HODL, una posizione short in una strip di opzioni con strike distribuiti su ). Il flusso di commissioni e il premio. Concentrare il range significa scegliere strike piu stretti: piu premio per unita di tempo mentre si e in range, divergenza piu rapida e piu profonda quando il prezzo si muove. Ogni LP di v3 e un trader short-volatility, che lo abbia scelto o no — il cugino on-chain del trade-off tra rischio di inventario che Avellaneda–Stoikov formalizza per i market maker da order book, con la larghezza del range che gioca il ruolo dello spread quotato.

Il nome tradizionale per il gap in quella tabella e impermanent (divergence) loss, ma il confronto IL-versus-HODL e un benchmark difettoso: confonde la perdita subita in quanto liquidity provider con il rischio di mercato che avresti comunque sostenuto. La scomposizione piu affilata viene da Milionis, Moallemi, Roughgarden e Zhang (2022), "Automated Market Making and Loss-Versus-Rebalancing" (arXiv:2208.06046). Confronta l'LP non con l'HODL ma con un portafoglio di ribilanciamento che detiene, in ogni istante, le stesse quantita di token del pool — ma scambia al prezzo di mercato esterno frictionless anziché contro gli arbitraggisti. La differenza e l'LVR ("lever"): la componente del P&L dell'LP che e pura selezione avversa, pagata agli arbitraggisti che approfittano delle quote stantie dell'AMM dopo ogni movimento di prezzo. Sotto un prezzo geometrico-browniano con volatilità , l'LVR matura al tasso istantaneo
— una "formula di Black–Scholes per gli AMM", come la definiscono gli autori, con la profondita marginale del pool al prezzo corrente. Per il pool a prodotto costante full-range questa si riduce al famoso del valore del pool per unita di tempo: al 5% di volatilità giornaliera, circa 3.1 bp del pool sanguinano verso gli arbitraggisti ogni giorno, commissioni o no. La concentrazione moltiplica — il nostro esempio con profondita 12.4× sopra e anche una macchina LVR ~12.4× mentre e in range. Le commissioni devono superare quel sanguinamento perche la posizione sia +EV, e l'LVR (a differenza dell'"IL") e un costo corrente coprubile e prevedibile, il che e cio che lo rende la giusta unita di conto. Il calcolo di profittabilità completo — APR da commissioni contro LVR contro volatilità realizzata, quando fare LP batte le opzioni short delta-hedged — e l'oggetto del prossimo approfondimento su impermanent loss e LVR, e la meccanica di hedging ha una trattazione dedicata in strategie LP e hedging per v3.
Un altro costo vive un livello piu sotto: poiche le quote AMM si aggiornano solo quando qualcuno fa uno swap, ogni posizione LP e anche esposta ai giochi di mempool giocati attorno a quegli swap — attacchi sandwich contro gli swapper che pagano le tue commissioni, e bundle di arbitraggio che realizzano il tuo LVR blocco dopo blocco. Quell'ecosistema e mappato nell'articolo su MEV e attacchi sandwich.
Leggere lo stato del pool on-chain
Tutto cio che precede e osservabile tramite tre chiamate economiche al contratto del pool.
slot0() impacchetta lo stato caldo in un singolo storage slot: sqrtPriceX96, il tick corrente, gli indici di osservazione dell'oracolo, e i flag di protocol-fee/lock. liquidity() restituisce l' aggregato in range corrente — nota che questo non e la TVL; e il parametro di profondita attivo al tick corrente e salta in modo discontinuo quando il prezzo attraversa un tick inizializzato. ticks(int24) restituisce lo stato per-tick: liquidityGross ( totale che referenzia il tick), liquidityNet ( con segno aggiunto attraversando da sinistra a destra), e gli accumulatori feeGrowthOutside. Iterando liquidityNet sui tick inizializzati (il tickBitmap ti dice quali esistono senza scansionare tutti gli 1.7 milioni) ricostruisci l'intero profilo di profondita — il tuo snapshot dell'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)
Le posizioni stesse vivono in due luoghi. Il pool core le indicizza tramite (owner, tickLower, tickUpper) — uno slot aggregato per ogni tripla owner-range. Retail e la maggior parte dei fondi invece mintano tramite la periphery NonfungiblePositionManager (mainnet: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88), che avvolge ogni posizione in un NFT ERC-721 ed espone positions(tokenId) restituendo la tupla completa: token, fascia di commissione, range, , feeGrowthInsideLast, e tokensOwed. Per il monitoraggio del portafoglio, quella singola chiamata piu il feeGrowthInside corrente del pool (ricalcolato da ticks() come nella sezione precedente) ti da valore mark-to-market e commissioni maturate senza toccare un indexer — anche se per qualsiasi cosa storica vorrai il replay degli swap-event o un subgraph, perche la crescita delle commissioni e path-dependent e la chain memorizza solo gli accumulatori correnti.
Due dettagli operativi che vale la pena interiorizzare prima di dispiegare capitale. Primo, la tick spacing quantizza lo spazio della tua strategia: sulla fascia 0.05% puoi piazzare range larghi 10 bp e far girare qualcosa di vicino a un vero ordine limite (minta appena sopra/sotto lo spot, raccogli la commissione, distruggi dopo l'attraversamento — il whitepaper inquadra esplicitamente questo caso d'uso come "range order"), mentre sulla fascia 1% il tuo range minimo e largo ~2% e l'analogia con il LOB diventa grossolana. Secondo, un range order e un ordine limite senza priorita di cancellazione: se il prezzo attraversa il tuo range e torna indietro, fai un round-trip dell'inventario e restituisci l'edge (tenendoti le commissioni). I range passivi sono quote che non puoi ritirare — il che e esattamente il motivo per cui la domanda sulla frequenza di ribilanciamento, e la lente LVR per rispondervi, contano cosi tanto.
Dove questo ti lascia
Lo stack di v3, compresso: i prezzi sono tick su una griglia geometrica 1.0001; la profondita e , convertibile in importi di token con nient'altro che differenze di radici quadrate; le commissioni sono un accumulatore per- che sottrai lungo i confini del tuo range; e la posizione risultante e un range order short-vol il cui costo corrente ha un nome, una formula, e un numero arXiv. Con le primitive in mano, le domande interessanti diventano quantitative: quanto largo, con quale frequenza ribilanciare, e se le commissioni coprono l'LVR per un dato pool e regime — che e esattamente dove questa serie va a parare in seguito.
Riferimenti
- Adams, H., Zinsmeister, N., Salem, M., Keefer, R., Robinson, D. (2021). Uniswap v3 Core (whitepaper).
- Librerie core di Uniswap v3:
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.
Autori
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.