Skip to main content

lp-analyst

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.

Ir a la instalación

Datos de origen

Repositorio
ccashwell/evm-cortex
Última actividad en el origen
10 de abril de 2026 a las 16:31
Idioma detectado de SKILL.md
inglés
Estrellas
131
Forks
18

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub