- name
- lp-analyst
- description
- Use when analyzing LP positions, calculating impermanent loss, optimizing position ranges, estimating fee revenue, rebalancing strategies, or building automated liquidity management. Covers both Uniswap V3 NonfungiblePositionManager and V4 PositionManager positions.
# LP Position Analysis
## Impermanent Loss Mathematics
### V2 Full-Range IL
For a 50/50 constant-product pool where `r = current_price / entry_price`:
```
IL(r) = 2√r / (1 + r) - 1
```
| Price Change | r | IL |
|-------------|-----|---------|
| -50% | 0.50 | -5.72% |
| -25% | 0.75 | -1.03% |
| -10% | 0.90 | -0.14% |
| 0% | 1.00 | 0.00% |
| +10% | 1.10 | -0.14% |
| +25% | 1.25 | -0.62% |
| +50% | 1.50 | -2.02% |
| +100% | 2.00 | -5.72% |
| +300% | 4.00 | -20.00% |
IL is always non-positive. The loss is symmetric on a log scale: a 2x and a 0.5x move produce the same IL.
### V3 Concentrated Liquidity IL
For a position with tick range `[tickLower, tickUpper]` mapping to price range `[Pa, Pb]` where `Pa < Pb`, with liquidity `L` and current price `P`:
**Position token amounts (the core V3 math):**
```
If Pa ≤ P ≤ Pb (in range):
amount0 = L × (1/√P - 1/√Pb)
amount1 = L × (√P - √Pa)
If P < Pa (below range — 100% token0):
amount0 = L × (1/√Pa - 1/√Pb)
amount1 = 0
If P > Pb (above range — 100% token1):
amount0 = 0
amount1 = L × (√Pb - √Pa)
```
**Position value in token1 terms:**
```
value(P) = amount0 × P + amount1
```
Expanding for the in-range case:
```
value(P) = L × (P/√P - P/√Pb + √P - √Pa)
= L × (√P - √Pa + √P - P/√Pb)
= L × (2√P - √Pa - P/√Pb)
```
**HODL value** (holding the initial token amounts without providing liquidity):
At entry price `P₀` (in range), the initial amounts are:
```
a0 = L × (1/√P₀ - 1/√Pb)
a1 = L × (√P₀ - √Pa)
```
HODL value at current price `P`:
```
value_HODL(P) = a0 × P + a1
= L × (P/√P₀ - P/√Pb + √P₀ - √Pa)
```
**Impermanent loss:**
```
IL = value_LP(P) / value_HODL(P) - 1
```
Concentrated positions amplify IL relative to V2. The amplification factor equals the capital efficiency multiplier.
### Worked Example: ETH/USDC
Setup:
- Entry price: P₀ = 3000 USDC/ETH
- Range: Pa = 2500, Pb = 3500
- Deposit: 1 ETH + 3000 USDC worth of value
Compute initial amounts (P₀ = 3000, in range):
```
√3000 ≈ 54.772
√2500 = 50.000
√3500 ≈ 59.161
Liquidity L from 1 ETH + equivalent USDC:
From token0: L = amount0 / (1/√P - 1/√Pb)
From token1: L = amount1 / (√P - √Pa)
Using the ratio to find L for a balanced deposit of value V at price P₀:
V = L × (2√P₀ - √Pa - P₀/√Pb)
V = L × (2 × 54.772 - 50.000 - 3000/59.161)
V = L × (109.544 - 50.000 - 50.709)
V = L × 8.835
For V = 6000 USDC (1 ETH at $3000 + 3000 USDC):
L = 6000 / 8.835 ≈ 679.0
```
Now ETH moves to $3500 (upper bound):
```
P = 3500 = Pb → position is 100% USDC (token1)
amount0 = 0
amount1 = L × (√Pb - √Pa) = 679.0 × (59.161 - 50.000) = 679.0 × 9.161 ≈ 6220
value_LP = 6220 USDC
```
HODL value at $3500:
```
Initial amounts at P₀ = 3000:
a0 = 679.0 × (1/54.772 - 1/59.161) = 679.0 × (0.01826 - 0.01690) = 679.0 × 0.001359 ≈ 0.923 ETH
a1 = 679.0 × (54.772 - 50.000) = 679.0 × 4.772 ≈ 3240 USDC
value_HODL = 0.923 × 3500 + 3240 = 3230 + 3240 = 6470 USDC
```
Impermanent loss:
```
IL = 6220 / 6470 - 1 ≈ -3.86%
```
Compare with V2 full-range IL at the same price move (r = 3500/3000 ≈ 1.167):
```
IL_v2 = 2√1.167 / (1 + 1.167) - 1 = 2 × 1.0801 / 2.167 - 1 ≈ -0.28%
```
The concentrated position suffers ~14x more IL — consistent with the capital efficiency multiplier for this range.
## Fee Revenue Estimation
### Fee Accrual Model
```
fee_revenue = volume_in_range × fee_tier × (position_liquidity / total_liquidity_in_range)
```
Annualized:
```
annual_fees = daily_fee_revenue × 365
fee_APR = annual_fees / position_value
```
The net return of an LP position is:
```
net_return = fee_APR + IL
```
A position is profitable when fee revenue exceeds IL.
### Fee Tiers
| Tier | Fee | Tick Spacing | Typical Pairs |
|------|-----|-------------|---------------|
| 0.01% | 100 | 1 | Stablecoin/stablecoin (USDC/USDT) |
| 0.05% | 500 | 10 | Correlated assets (wstETH/ETH) |
| 0.30% | 3000 | 60 | Standard pairs (ETH/USDC) |
| 1.00% | 10000 | 200 | Exotic / long-tail pairs |
### Active Time Ratio
Concentrated positions only earn fees while the current price is within range. The active time ratio `α` represents the fraction of time the position is in range:
```
effective_fee_APR = fee_APR × α
```
For a ±10% range on ETH/USDC, historical α is typically 60-80% over a month. Narrower ranges have lower α.
### Fee Growth Tracking (V3)
Uniswap V3 tracks cumulative fees per unit of liquidity using Q128.128 fixed-point accumulators:
```
feeGrowthGlobal0X128 — cumulative token0 fees per unit liquidity (pool-wide)
feeGrowthGlobal1X128 — cumulative token1 fees per unit liquidity (pool-wide)
```
Per-position uncollected fees:
```
uncollected0 = (feeGrowthInside0CurrentX128 - feeGrowthInside0LastX128) × liquidity / 2^128
uncollected1 = (feeGrowthInside1CurrentX128 - feeGrowthInside1LastX128) × liquidity / 2^128
```
Where `feeGrowthInsideX128` is computed from the tick-level `feeGrowthOutside` values:
```solidity
// Pseudocode for feeGrowthInside
if currentTick >= tickUpper:
feeGrowthInside = feeGrowthOutside[tickUpper] - feeGrowthOutside[tickLower]
elif currentTick < tickLower:
feeGrowthInside = feeGrowthOutside[tickLower] - feeGrowthOutside[tickUpper]
else:
feeGrowthInside = feeGrowthGlobal - feeGrowthOutside[tickLower] - feeGrowthOutside[tickUpper]
```
## Position Range Optimization
### Capital Efficiency Multiplier
For a range `[Pa, Pb]`, capital efficiency relative to full range is:
```
efficiency = 1 / (1 - √(Pa / Pb))
```
| Range | Pa/Pb | Efficiency |
|-------|-------|-----------|
| ±0.1% (stables) | 0.998 | ~1000x |
| ±1% | 0.980 | ~100x |
| ±5% | 0.905 | ~19x |
| ±10% | 0.818 | ~5.2x |
| ±25% | 0.600 | ~3.8x |
| ±50% | 0.333 | ~2.2x |
| Full range | 0→∞ | 1x |
Higher efficiency means more fees earned per dollar of capital, but also more IL per dollar and more frequent out-of-range events.
### Range Selection Heuristics
**Stablecoin pairs (USDC/USDT):**
- Range: ±0.05% to ±0.5% around peg
- Fee tier: 0.01%
- Rebalance: rarely needed if peg holds
- Capital efficiency: 200x–2000x
**Correlated pairs (wstETH/ETH):**
- Range: ±1% to ±5%
- Fee tier: 0.05%
- Rebalance: weekly or when staking rate changes materially
- Capital efficiency: 20x–100x
**Major pairs (ETH/USDC):**
- Range: ±10% to ±30% based on volatility regime
- Fee tier: 0.30%
- Rebalance: when price approaches range boundary
- Capital efficiency: 3x–10x
**Volatile pairs (memecoins, new tokens):**
- Range: ±50% or wider
- Fee tier: 1.00%
- Rebalance: avoid — gas often exceeds benefit
- Capital efficiency: 1.5x–3x
### Tick Math
Prices map to ticks via:
```
tick = floor(log(price) / log(1.0001))
price = 1.0001^tick
```
Tick spacing constrains which ticks can be used. A position's range must align to the pool's tick spacing:
```
tickLower = floor(desired_tick / tickSpacing) × tickSpacing
tickUpper = ceil(desired_tick / tickSpacing) × tickSpacing
```
## Reading Positions Onchain
### V3 NonfungiblePositionManager
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {INonfungiblePositionManager} from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
interface IPositionReader {
function analyzePosition(uint256 tokenId) external view returns (
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity
);
}
contract V3PositionReader {
INonfungiblePositionManager public immutable NPM;
constructor(address npm_) {
NPM = INonfungiblePositionManager(npm_);
}
function getPosition(uint256 tokenId) external view returns (
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
) {
(
, // nonce
, // operator
token0,
token1,
fee,
tickLower,
tickUpper,
liquidity,
feeGrowthInside0LastX128,
feeGrowthInside1LastX128,
tokensOwed0,
tokensOwed1
) = NPM.positions(tokenId);
}
}
```
### V4 PositionManager
```solidity
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PositionInfo} from "v4-periphery/src/libraries/PositionInfoLibrary.sol";
contract V4PositionReader {
IPositionManager public immutable PM;
constructor(address pm_) {
PM = IPositionManager(pm_);
}
/// @notice Reads a V4 LP position's pool key, tick range, and liquidity
function getPosition(uint256 tokenId) external view returns (
PoolKey memory poolKey,
int24 tickLower,
int24 tickUpper,
uint128 liquidity
) {
PositionInfo info;
(poolKey, info) = PM.getPoolAndPositionInfo(tokenId);
tickLower = info.tickLower();
tickUpper = info.tickUpper();
liquidity = PM.getPositionLiquidity(tokenId);
}
}
```
### Converting Ticks to Prices
```solidity
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
/// @notice Convert a tick to a human-readable price
/// @dev price = 1.0001^tick, adjusted for token decimals
function tickToPrice(int24 tick, uint8 decimals0, uint8 decimals1) pure returns (uint256) {
uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(tick);
// price = (sqrtPriceX96 / 2^96)^2 × 10^(decimals0 - decimals1)
uint256 priceX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
return priceX192 * (10 ** decimals0) / (10 ** decimals1) >> 192;
}
```
## Position Value Calculation
### Token Amounts from Liquidity
```solidity
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
/// @notice Calculate the token amounts for a position
function getAmounts(
int24 tickCurrent,
int24 tickLower,
int24 tickUpper,
uint128 liquidity
) pure returns (uint256 amount0, uint256 amount1) {
uint160 sqrtPriceCurrent = TickMath.getSqrtPriceAtTick(tickCurrent);
uint160 sqrtPriceLower = TickMath.getSqrtPriceAtTick(tickLower);
uint160 sqrtPriceUpper = TickMath.getSqrtPriceAtTick(tickUpper);
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceCurrent,
sqrtPriceLower,
sqrtPriceUpper,
liquidity
);
}
```
### Value in USD Terms
```
value_usd = amount0 × price0_usd + amount1 × price1_usd
```
For ETH/USDC where token0 = USDC, token1 = WETH:
```
value_usd = amount0 × 1.0 + amount1 × eth_price_usd
```
Always check token ordering — V3/V4 enforce `token0 < token1` by address sort.
## Fee Collection
### V3 Fee Collection
The NonfungiblePositionManager accumulates fees internally. To collect, first poke the position to update fee accounting, then call `collect`:
```solidity
/// @notice Collect all accrued fees from a V3 position
/// @dev A zero-liquidity decrease pokes the position to update fee snapshots
function collectFees(
INonfungiblePositionManager npm,
uint256 tokenId
) external returns (uint256 collected0, uint256 collected1) {
npm.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: 0,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
}));
(collected0, collected1) = npm.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
}
```
### V4 Fee Collection
V4 positions collect fees through the `PositionManager.collect` action inside a multicall/modifyLiquidities batch. Fees settle through the PoolManager's transient accounting.
## Rebalancing Strategies
### Time-Based
Rebalance at fixed intervals (e.g., every 24 hours, every 7 days). Simple to implement with Chainlink Automation or Gelato tasks.
**Pros:** predictable gas spend, simple logic
**Cons:** rebalances even when unnecessary, misses urgent rebalances when price moves fast
GitHub에서 보기