퀀트를 위한 Uniswap v3: 제1원리로 이해하는 집중 유동성과 틱 수학
Uniswap v3의 LP 포지션은 겉모습만 다를 뿐 사실상 호가창(LOB)의 레인지 주문이다. 두 가격 사이에 유동성을 예치하면, 가격이 레인지를 따라 내려갈 때는 자산을 사고 올라갈 때는 파는 것에 약정한 셈이 된다. 이는 CLOB에서 대기 중인 지정가 주문 격자가 하는 일과 정확히 같다. 하지만 컨트랙트는 가격도, 수량도, 호가창도 저장하지 않는다. 저장하는 것은 세 개의 숫자뿐이다. Q64.96 고정소수점으로 표현된 제곱근 가격, 정수 틱 인덱스, 그리고 집계 유동성 값 이다. v3 포지션을 마켓 메이킹 모델에서 호가를 다루듯이 — 인벤토리, 체결 가격, 역선택의 관점에서 — 추론하고 싶다면, 이 온체인 프리미티브와 그것들이 인코딩하는 트레이딩 개념 사이를 번역할 수 있어야 한다. 이 글은 Uniswap v3 백서(Adams, Zinsmeister, Salem, Keefer, Robinson, 2021)와 코어 컨트랙트를 따라 그 번역을 제1원리에서부터 세워 나가며, 오늘날 진지한 LP 논의가 모두 출발하는 지점, 즉 loss-versus-rebalancing에서 마무리한다.
x·y = k 에서 가상 준비금까지
Uniswap v2는 상수곱 마켓 메이커다. 풀은 token0의 준비금 와 token1의 준비금 를 보유하고, 모든 스왑에서 다음을 강제한다.
token1 단위로 표현한 token0의 한계 가격은 이며, 유동성은 전체 가격 축 에 균일하게 퍼져 있다. 이는 극단적으로 자본 비효율적이다. 0.999에서 1.001 사이에서 거래되는 스테이블코인 쌍은 자본의 99% 이상을 결코 찍히지 않을 가격에 배치해 둔다.
v3의 착안은 각 LP가 자신의 자본을 레인지 로 제한할 수 있게 하는 것이다. 레인지 안에서 포지션은 v2 풀과 정확히 동일하게 동작해야 한다 — 같은 본딩 커브, 같은 한계 가격 — 다만 그 레인지를 커버하는 데 필요한 준비금만 사용한다. 백서는 이를 **가상 준비금(virtual reserves)**으로 형식화한다. 포지션은 마치 커브 위에 놓인 v2식 준비금 를 보유한 것처럼 동작하는 한편, **실제 준비금(real reserves)**은 그 가상 준비금에서 레인지 경계에서 포지션이 보유하게 될 양을 뺀 값이다.
이것이 백서의 식 2.2이며, v3에서 가장 중요한 단 하나의 식이다. 이동된 커브는 축에 접한다. 에서 실제 준비금이 0이 되고(포지션은 100% token1), 에서 실제 준비금이 0이 된다(100% token0). 레인지 밖에서 포지션은 비활성 상태가 된다 — 한 토큰만 담긴 고정된 자루로, 아무것도 벌지 못한다.

**유동성(liquidity)**이라 불리는 파라미터 은 를 대체하는 불변량이다. 이는 로 정의되며, 백서가 명시하는 깔끔한 해석을 갖는다. 유동성은 "제곱근 가격 단위당 가상 준비금"이다. 레인지 안의 임의의 가격 에서 가상 준비금은 다음과 같다.
왜 상태 변수가 √P인가
v3의 코어는 가격을 추적하지 않는다. 부호 없는 Q64.96 고정소수점 숫자인 sqrtPriceX96으로 저장되는 를 추적한다.
여기서 는 원시(raw) 가격, 즉 token0 베이스 단위당 token1 베이스 단위이며 소수 자릿수가 포함된다(이에 관해서는 아래에서 더 다룬다). 제곱근을 쓰는 이유는 가스 최적화 트릭이 아니라 대수(algebra) 때문이다. 가상 준비금 항등식을 미분하면 두 개의 근본적인 스왑 방정식을 얻는데, 이는 SqrtPriceMath 라이브러리에 구현되어 있다.
두 토큰 델타 모두 제곱근 가격(또는 그 역수)에 대해 선형이며, 이 비례 상수 역할을 한다. 따라서 하나의 틱 안에서 일어나는 스왑에는 커브 역산도, 뉴턴 반복도 필요 없다 — 를 이동시키는 곱셈 한 번, 그리고 양을 계산하는 곱셈 두 번이면 된다. 스왑이 가격을 초기화된 틱을 넘어 밀 만큼 클 때, 풀은 그 틱을 크로스하고 거기 참조된 순유동성(liquidityNet)을 더하거나 빼고, 새로운 집계 로 계속 진행한다. 전역적으로 풀은 조각별 상수곱 AMM이다 — 초기화된 틱 사이에서는 이 상수이고, 틱에서는 점프한다.
마켓 메이커에게 이것은 올바른 사고 모델이다. 풀의 집계 프로파일은 호가창 심도의 DEX 등가물이다. CLOB 심도가 가격 레벨당 단위로 호가되는 반면, AMM 심도는 틱당 이며 — 그 변환은 정확히 위의 항등식이다.
틱: 로그 간격 가격 격자
레인지는 임의의 가격에서 시작하고 끝날 수 없다. **틱(tick)**에 맞춰 스냅되어야 한다. 틱 는 다음 가격에 대응한다.
즉 각 틱은 이웃과 1 베이시스 포인트 떨어져 있다 — 절대적 의미가 아니라 상대적 의미에서다. 이 로그 간격은 의도적이다. 고정된 가산 격자는 $0.0001에 거래되는 토큰에는 터무니없이 조밀하지 못하고 $100{,}000에는 터무니없이 세밀할 텐데, 기하 격자는 모든 가격 스케일에서 균일한 1 bp 해상도를 제공한다. TickMath 라이브러리는 양방향 변환을 수행한다 — getSqrtRatioAtTick과 getTickAtSqrtRatio — 지수 함수를 호출하는 대신 미리 계산된 상수에 대한 비트 연산을 사용한다. 틱 인덱스는 MIN_TICK = -887272와 MAX_TICK = 887272로 제한되며, 이는 가격 범위 을 커버한다. uint256 산술로 존재할 수 있는 어떤 토큰 쌍에도 충분히 넓다.
모든 틱을 사용할 수 있는 것은 아니다. 각 수수료 티어는 **틱 간격(tick spacing)**을 부과하며, 포지션은 그것으로 나누어 떨어지는 틱만 사용할 수 있다.
| 수수료 티어 | 틱 간격 | 최소 레인지 폭 | 일반적 용도 |
|---|---|---|---|
| 0.01% | 1 | ~1 bp | 스테이블/스테이블 |
| 0.05% | 10 | ~10 bp | 스테이블 쌍, ETH/스테이블 |
| 0.30% | 60 | ~60 bp | 메이저 |
| 1.00% | 200 | ~2% | 이색적 / 변동성 높음 |
0.05%/0.30%/1% 티어는 2021년 5월 출시 시 함께 나왔고, 0.01% 티어는 2021년 11월 거버넌스 투표로 추가되었다. 고수수료 풀에서 더 성긴 간격은 크로스될 수 있는 틱의 수를 낮게 유지하고 스왑 가스 비용을 제한한다.
누구나 한 번은 걸려드는 소수점 함정이 하나 있다. 원시 온체인 가격은 베이스 단위로 표현한 token0당 token1이다. 메인넷 USDC/WETH 풀에서 token0은 USDC(6 소수 자릿수), token1은 WETH(18 소수 자릿수)이므로, ETH당 $3{,}000이라는 사람이 읽는 가격은 USDC-단위당 WETH-wei라는 원시 가격에 대응하고, 이는 다음 틱에 위치한다.
이때 이다. 모니터링 대시보드가 USDC/WETH에 대해 196k 근처의 틱을 보여준다면 이제 그 이유를 알 것이다 — 그리고 사람이 읽는 가격은 을 필요에 따라 역수로 취한 값이라는 것도.
(L, 가격 레인지)로부터 토큰 양 구하기: 정확한 공식
이 섹션의 핵심 산출물이다. 유동성 을 가진 포지션이 위에 있고 현재 가격이 일 때, 무엇을 보유하는가? 스왑 방정식을 레인지 전체에 걸쳐 적분하면 세 가지 경우가 나온다.
가격이 레인지 아래 () — 포지션은 100% token0이다.
가격이 레인지 위 () — 포지션은 100% token1이다.
가격이 레인지 안 ():
이것들이 바로 SqrtPriceMath.getAmount0Delta와 getAmount1Delta가 계산하는 것이다(방향성 있는 반올림과 함께 — 컨트랙트는 항상 사용자에게 불리한 쪽으로 반올림하는데, 이는 백테스터에서 이를 재현하다가 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를 예치하고 싶다고 하자. 제곱근: , , .
ETH 레그가 을 확정한다.
그러면 USDC 레그가 뒤따른다.
따라서 이 포지션을 민팅하는 데는 1 ETH + 3,523.69 USDC, 총 가치 $6,523.69가 필요하며, 레그가 50/50이 아니라는 점에 주목하라 — 그 배분은 가 레인지 안 어디에 위치하는지에 따라 달라진다(비대칭 레인지는 정확히 방향성 뷰를 표현하거나 한 토큰으로 순수한 "레인지 주문"을 놓는 방법이다).
이제 가격을 경계까지 걸어보자.
- 에서 포지션은 완전히 ETH로 전환되었다. ETH, 가치 $5,716.68. 내려가는 길에 유동성 가중 평균가 3523.69 / 1.2867 \approx \2{,}739$로 1.2867 ETH를 매수했다.
- 에서 완전히 USDC로 전환되었다. USDC. 올라가는 길에 평균 $3,240에 ETH를 매도했다.
이것이 구체화된 레인지 주문의 읽기다. 포지션은 3000에서 2500까지 내려가는 매수 사다리와 3000에서 3500까지 올라가는 매도 사다리 그 자체이며, 틱당 규모는 에 비례한다. 그리고 집중은 당신이 대가로 받는 것이다. 동일한 $6,523.69의 자본을 가진 전체 레인지 v2식 포지션은 를 가질 텐데 — 집중 포지션은 틱당 12.4배 더 많은 심도를 호가하며, (레인지 안에 있는 동안) 자본 1달러당 12.4배의 비율로 수수료를 번다.
수수료 회계: feeGrowthGlobal과 feeGrowthInside
v3 수수료는 포지션에 복리로 재투자되지 않는다(수수료가 로 재투자되던 v2로부터의 의도적 단절이다). 수수료는 토큰별로 나란히 누적되며, 그 회계는 O(1) 부기의 작은 걸작이라 이해할 가치가 있다. 모든 LP 분석 파이프라인이 이를 재구현하기 때문이다.
풀은 두 개의 전역 누산기 feeGrowthGlobal0X128과 feeGrowthGlobal1X128을 유지한다. 이는 풀 개시 이래 유동성 단위당 누적 수수료이며, Q128.128 고정소수점으로 표현된다. 각 스왑에서 수수료 양(입력 토큰에서 취해진다)은 현재 레인지 안 로 나뉘어 누산기에 더해진다. 이것이 v3 경제학의 기본 규칙 뒤에 있는 메커니즘이다. 수수료는 스왑 시점에 레인지 안에 있는 유동성에만 누적된다. 레인지 밖 포지션은 단지 비활성 인벤토리인 것이 아니라 — 비활성인 동안 정확히 0을 번다.
전역 성장의 올바른 조각을 유한한 레인지에 귀속시키기 위해, 각 초기화된 틱 는 feeGrowthOutside0/1X128을 저장한다 — 현재 가격을 기준으로 그 틱의 반대편에서 발생한 수수료 성장이다(이 값은 틱이 크로스될 때마다 해석이 뒤집히며, 바로 이것이 이 방식을 O(1)로 만든다). 그러면 현재 틱 에서 위의 포지션에 대해:
각 포지션은 스냅샷 feeGrowthInsideLast를 저장하며, 미수취 수수료는 단순히 다음과 같다.
이는 포지션이 건드려질 때마다 게으르게(lazily) 갱신된다. 실용적 결과가 두 가지 있다. 첫째, feeGrowthInside 델타는 포지션의 수수료 수입을 측정하는 유일하게 정직한 방법이다 — 풀 수준 거래량을 샘플링하고 TVL 대비 자신의 지분으로 안분하는 방식은 가격이 레인지 경계 주변을 배회할 때마다 틀린 값을 낸다. 둘째, 수수료가 복리로 쌓이는 대신 tokensOwed로 놓여 있기 때문에, 실현된 LP 수익률에는 v2에 없던 현금 드래그 항이 생긴다. 오토 컴파운딩 볼트는 정확히 이것을 (자체 수수료를 뺀 만큼) 차익거래하기 위해 존재한다.

페이오프: 보유하면 대가를 받는(어쩌면) 숏 스트래들
레인지 안에서 가격의 함수로서 포지션 가치는 다음과 같다.
에 대해 오목하다 — 항이 이야기의 전부다. 레인지 밖에서는 선형이 된다. 아래쪽에서는 기울기 (고정된 ETH 자루에 롱), 위쪽에서는 기울기 0(USDC로 플랫). 우리의 구체적 예제를 레인지 전체에 걸쳐 돌려보고, 단순히 예치한 토큰을 보유하는 것과 비교해보자.
| (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을 언더퍼폼하며, 오직 민팅 가격에서만 그것과 일치한다. 상방은 제한되고, 하방 참여는 증폭되며, 행사가에서 상대 가치가 최대다... 이것이 **숏 스트래들(short straddle)**의 페이오프다(더 정확히는, 선형 꼬리를 HODL 벤치마크에 대해 상쇄하고 나면, 에 걸쳐 행사가가 분포한 옵션 스트립의 숏 포지션이다). 수수료 스트림이 프리미엄이다. 레인지를 좁히는 것은 더 타이트한 행사가를 선택하는 것이다 — 레인지 안에 있는 동안 단위 시간당 프리미엄은 더 많지만, 가격이 움직이면 괴리가 더 빠르고 깊어진다. 모든 v3 LP는 스스로 선택했든 아니든 숏 변동성 트레이더다 — 이는 Avellaneda–Stoikov가 호가창 마켓 메이커에 대해 형식화하는 인벤토리 리스크 트레이드오프의 온체인 형제이며, 레인지 폭이 호가된 스프레드의 역할을 한다.

그 표의 간극에 대한 전통적 명칭은 비영구적 손실(impermanent/divergence loss)이지만, IL-대-HODL은 결함 있는 벤치마크다. 이는 유동성 공급자로서 입은 손실을 어차피 짊어졌을 시장 리스크와 뒤섞는다. 더 날카로운 분해는 Milionis, Moallemi, Roughgarden, Zhang(2022)의 "Automated Market Making and Loss-Versus-Rebalancing"(arXiv:2208.06046)에서 온다. LP를 HODL이 아니라 리밸런싱 포트폴리오에 대해 벤치마크하라 — 이 포트폴리오는 매 순간 풀과 동일한 토큰 수량을 보유하되, 차익거래자를 상대하는 대신 마찰 없는 외부 시장 가격으로 거래한다. 그 차이가 LVR("레버")이다. LP 손익 중 순수한 역선택 성분으로, 매 가격 이동 후 AMM의 낡은 호가를 골라내는 차익거래자에게 지불된다. 변동성 를 갖는 기하 브라운 가격 하에서, LVR은 다음의 순간 비율로 누적된다.
— 저자들의 표현대로 "AMM을 위한 Black–Scholes 공식"이며, 는 현재 가격에서 풀의 한계 심도다. 전체 레인지 상수곱 풀의 경우 이는 단위 시간당 풀 가치의 그 유명한 로 붕괴한다. 일일 변동성 5%에서, 수수료가 있든 없든 풀의 대략 3.1 bp가 매일 차익거래자에게 새어 나간다. 집중은 를 곱한다 — 위의 12.4배 심도 예제는 레인지 안에 있는 동안 ~12.4배 LVR 기계이기도 하다. 포지션이 +EV가 되려면 수수료가 그 출혈을 앞서야 하며, LVR은("IL"과 달리) 헤지 가능하고 예측 가능한 상시 비용인데, 바로 이것이 LVR을 올바른 계산 단위로 만든다. 전체 수익성 계산 — 수수료 APR 대 LVR 대 실현 변동성, 언제 LP가 델타 헤지된 숏 옵션을 이기는가 — 는 곧 나올 비영구적 손실과 LVR 심층 분석의 주제이며, 헤지 메커니즘은 v3 LP 전략과 헤징에서 별도로 다룬다.
한 층 아래에 비용이 하나 더 있다. AMM 호가는 누군가 거래할 때만 갱신되기 때문에, 모든 LP 포지션은 그 거래들 주변에서 벌어지는 멤풀 게임에도 노출된다 — 당신의 수수료를 지불하는 스와퍼에 대한 샌드위치 공격, 그리고 당신의 LVR을 블록 단위로 실현하는 차익거래 번들이다. 그 생태계는 MEV와 샌드위치 공격 글에서 지도로 그려진다.
온체인에서 풀 상태 읽기
위의 모든 것은 풀 컨트랙트에 대한 세 번의 값싼 호출로 관측 가능하다.
**slot0()**는 뜨거운 상태를 하나의 스토리지 슬롯에 담는다. sqrtPriceX96, 현재 tick, 오라클 관측 인덱스, 그리고 프로토콜 수수료/락 플래그다. **liquidity()**는 현재 레인지 안 집계 을 반환한다 — 이는 TVL이 아니라는 점에 유의하라. 이는 현재 틱에서 활성화된 심도 파라미터이며, 가격이 초기화된 틱을 크로스할 때 불연속적으로 점프한다. **ticks(int24)**는 틱별 상태를 반환한다. liquidityGross(틱을 참조하는 전체 ), liquidityNet(좌에서 우로 크로스할 때 더해지는 부호 있는 ), 그리고 feeGrowthOutside 누산기다. 초기화된 틱들에 대해 liquidityNet을 반복하면(어떤 틱이 존재하는지는 170만 개를 전부 스캔하지 않고도 tickBitmap이 알려준다) 전체 심도 프로파일 를 재구성한다 — 당신의 호가창 스냅샷이다.
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)
포지션 자체는 두 곳에 존재한다. 코어 풀은 이를 (owner, tickLower, tickUpper)로 키잉한다 — 소유자-레인지 삼중항당 하나의 집계 슬롯이다. 리테일과 대부분의 펀드는 대신 페리페리 NonfungiblePositionManager(메인넷: 0xC36442b4a4522E871399CD717aBDD847Ab11FE88)를 통해 민팅하는데, 이는 각 포지션을 ERC-721 NFT로 감싸고 전체 튜플을 반환하는 positions(tokenId)를 노출한다. 토큰, 수수료 티어, 레인지, , feeGrowthInsideLast, 그리고 tokensOwed다. 포트폴리오 모니터링의 경우, 그 한 번의 호출과 풀의 현재 feeGrowthInside(앞 섹션처럼 ticks()로부터 재계산)를 합치면 인덱서를 건드리지 않고도 시가 평가 가치와 누적 수수료를 얻는다 — 다만 과거 데이터가 필요하다면 스왑 이벤트 리플레이나 서브그래프를 원하게 될 텐데, 수수료 성장이 경로 의존적이고 체인은 현재 누산기만 저장하기 때문이다.
자본을 투입하기 전에 체득해 둘 만한 운영상의 세부 사항이 두 가지 있다. 첫째, 틱 간격이 당신의 전략 공간을 양자화한다. 0.05% 티어에서는 10-bp 폭 레인지를 놓고 진짜 지정가 주문에 가까운 무언가를 운용할 수 있는 반면(현물 바로 위/아래에 민팅하고, 수수료를 걷고, 크로싱 후 소각한다 — 백서는 이 "레인지 주문" 용례를 명시적으로 다룬다), 1% 티어에서는 최소 레인지가 ~2% 폭이라 LOB 유비가 성겨진다. 둘째, 레인지 주문은 취소 우선순위가 없는 지정가 주문이다. 가격이 당신의 레인지를 크로스했다가 되돌아오면, 인벤토리를 왕복하고 엣지를 돌려준다(수수료는 유지). 패시브 레인지는 당신이 거둘 수 없는 호가다 — 바로 이것이 리밸런싱 빈도 문제와 그것에 답하기 위한 LVR 렌즈가 그토록 중요한 이유다.
이제 남은 것
v3 스택을 압축하면 이렇다. 가격은 1.0001-기하 격자 위의 틱이다. 심도는 이며, 제곱근의 차이 이상 아무것도 필요 없이 토큰 양으로 변환 가능하다. 수수료는 레인지 경계에 걸쳐 차분을 취하는 당 누산기다. 그리고 그 결과 포지션은 상시 비용에 이름과 공식과 arXiv 번호가 붙은 숏 변동성 레인지 주문이다. 프리미티브를 손에 쥐면 흥미로운 질문들은 정량적이 된다. 얼마나 넓게, 얼마나 자주 리밸런싱할지, 그리고 주어진 풀과 국면에서 수수료가 LVR을 청산하는지 — 바로 이 시리즈가 다음으로 향하는 지점이다.
참고 문헌
- 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.