package pricing import ( "math" "math/big" ) // Uniswap V3 Pricing Library // Based on: https://github.com/t4sk/notes/tree/main/python/uniswap-v3 // // Key concepts: // - Prices are stored as sqrtPriceX96 (Q64.96 fixed-point format) // - Ticks represent discrete 0.01% price movements // - Each tick corresponds to price = 1.0001^tick const ( // Q96 is the scaling factor used in Uniswap V3 (2^96) Q96 = 96 // TickBase is the constant representing each tick's 0.01% price change TickBase = 1.0001 // MinTick is the minimum tick value in Uniswap V3 MinTick = -887272 // MaxTick is the maximum tick value in Uniswap V3 MaxTick = 887272 ) var ( // q96Big is 2^96 as a big.Int for calculations q96Big = new(big.Int).Lsh(big.NewInt(1), Q96) // q96Float is 2^96 as a big.Float for calculations q96Float = new(big.Float).SetInt(q96Big) ) // SqrtPriceX96ToPrice converts Uniswap V3's sqrtPriceX96 to a human-readable price // // Formula: price = (sqrtPriceX96 / 2^96)^2 * (10^decimals0 / 10^decimals1) // // Example from Python notebook: // sqrt_price_x_96 = 3443439269043970780644209 // price = (sqrt_price_x_96 / 2^96)^2 * (1e18 / 1e6) ≈ 1888.97 USDC per ETH // // Parameters: // - sqrtPriceX96: The sqrt price in Q64.96 format // - token0Decimals: Number of decimals for token0 // - token1Decimals: Number of decimals for token1 // // Returns: Price of token0 in terms of token1 func SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) *big.Float { if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 { return big.NewFloat(0) } // Convert to float sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96) // Divide by 2^96 to get the actual sqrt(price) sqrtPrice := new(big.Float).Quo(sqrtPriceFloat, q96Float) // Square to get price price := new(big.Float).Mul(sqrtPrice, sqrtPrice) // Adjust for decimal differences // price = price * (10^token0Decimals / 10^token1Decimals) if token0Decimals != token1Decimals { decimalAdjustment := new(big.Float).SetInt( new(big.Int).Exp( big.NewInt(10), big.NewInt(int64(token0Decimals)-int64(token1Decimals)), nil, ), ) price = new(big.Float).Mul(price, decimalAdjustment) } return price } // TickToPrice converts a Uniswap V3 tick to a price // // Formula: price = 1.0001^tick * (10^decimals0 / 10^decimals1) // // Example from Python notebook: // tick = -200963 // price = 1.0001^(-200963) * (1e18 / 1e6) ≈ 1873.80 USDC per ETH // // Parameters: // - tick: The tick value (-887272 to 887272) // - token0Decimals: Number of decimals for token0 // - token1Decimals: Number of decimals for token1 // // Returns: Price of token0 in terms of token1 func TickToPrice(tick int32, token0Decimals, token1Decimals uint8) *big.Float { // Calculate price = 1.0001^tick price := math.Pow(TickBase, float64(tick)) // Adjust for decimal differences decimalAdjustment := math.Pow10(int(token0Decimals) - int(token1Decimals)) adjustedPrice := price * decimalAdjustment return big.NewFloat(adjustedPrice) } // SqrtPriceX96ToTick converts sqrtPriceX96 to a tick value // // Formula: tick = 2 * ln(sqrtPrice / 2^96) / ln(1.0001) // // Example from Python notebook: // sqrt_price = 3436899527919986964832931 // tick = 2 * ln(sqrt_price / 2^96) / ln(1.0001) ≈ -200920.39 // // Parameters: // - sqrtPriceX96: The sqrt price in Q64.96 format // // Returns: Tick value (rounded to nearest integer) func SqrtPriceX96ToTick(sqrtPriceX96 *big.Int) int32 { if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 { return 0 } // Convert to float64 for logarithm calculation sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96) q96Float := new(big.Float).SetInt(q96Big) // Calculate ratio: sqrtPrice / 2^96 ratio := new(big.Float).Quo(sqrtPriceFloat, q96Float) // Convert to float64 ratioFloat64, _ := ratio.Float64() // Calculate tick = 2 * ln(ratio) / ln(1.0001) tick := 2.0 * math.Log(ratioFloat64) / math.Log(TickBase) // Round to nearest integer tickRounded := int32(math.Round(tick)) // Clamp to valid range if tickRounded < MinTick { return MinTick } if tickRounded > MaxTick { return MaxTick } return tickRounded } // PriceToTick converts a price to a tick value // // Formula: tick = ln(price) / ln(1.0001) // // Note: Price should already be adjusted for decimal differences // // Parameters: // - price: The price ratio (token0/token1), adjusted for decimals // // Returns: Tick value (rounded to nearest integer) func PriceToTick(price float64) int32 { if price <= 0 { return MinTick } // Calculate tick = ln(price) / ln(1.0001) tick := math.Log(price) / math.Log(TickBase) // Round to nearest integer tickRounded := int32(math.Round(tick)) // Clamp to valid range if tickRounded < MinTick { return MinTick } if tickRounded > MaxTick { return MaxTick } return tickRounded } // TickToSqrtPriceX96 converts a tick to sqrtPriceX96 // // Formula: sqrtPriceX96 = sqrt(1.0001^tick) * 2^96 // // Parameters: // - tick: The tick value // // Returns: SqrtPriceX96 value func TickToSqrtPriceX96(tick int32) *big.Int { // Calculate price = 1.0001^tick price := math.Pow(TickBase, float64(tick)) // Calculate sqrtPrice = sqrt(price) sqrtPrice := math.Sqrt(price) // Calculate sqrtPriceX96 = sqrtPrice * 2^96 sqrtPriceFloat := big.NewFloat(sqrtPrice) sqrtPriceX96Float := new(big.Float).Mul(sqrtPriceFloat, q96Float) // Convert to big.Int sqrtPriceX96Int, _ := sqrtPriceX96Float.Int(nil) return sqrtPriceX96Int } // GetPriceImpact calculates the price impact of a swap in basis points (BPS) // // Parameters: // - oldSqrtPriceX96: The sqrt price before the swap // - newSqrtPriceX96: The sqrt price after the swap // - token0Decimals: Number of decimals for token0 // - token1Decimals: Number of decimals for token1 // // Returns: Price impact in basis points (10000 BPS = 100%) func GetPriceImpact(oldSqrtPriceX96, newSqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) float64 { if oldSqrtPriceX96 == nil || newSqrtPriceX96 == nil || oldSqrtPriceX96.Sign() == 0 || newSqrtPriceX96.Sign() == 0 { return 0 } oldPrice := SqrtPriceX96ToPrice(oldSqrtPriceX96, token0Decimals, token1Decimals) newPrice := SqrtPriceX96ToPrice(newSqrtPriceX96, token0Decimals, token1Decimals) // Calculate percentage change priceDiff := new(big.Float).Sub(newPrice, oldPrice) percentChange := new(big.Float).Quo(priceDiff, oldPrice) // Convert to BPS (multiply by 10000) bps, _ := new(big.Float).Mul(percentChange, big.NewFloat(10000)).Float64() // Return absolute value if bps < 0 { return -bps } return bps } // GetTickSpacing returns the tick spacing for a given fee tier // // Uniswap V3 uses different tick spacings for different fee tiers: // - 0.01% fee (100): tick spacing = 1 // - 0.05% fee (500): tick spacing = 10 // - 0.30% fee (3000): tick spacing = 60 // - 1.00% fee (10000): tick spacing = 200 // // Parameters: // - feeBPS: The fee in basis points // // Returns: Tick spacing for the fee tier func GetTickSpacing(feeBPS uint32) int32 { switch feeBPS { case 100: // 0.01% return 1 case 500: // 0.05% return 10 case 3000: // 0.30% return 60 case 10000: // 1.00% return 200 default: // Default to 60 (most common tier) return 60 } } // GetNearestUsableTick returns the nearest usable tick for a given fee tier // // Ticks must be aligned to the tick spacing of the fee tier. // // Parameters: // - tick: The desired tick value // - tickSpacing: The tick spacing for the fee tier // // Returns: Nearest usable tick aligned to tick spacing func GetNearestUsableTick(tick int32, tickSpacing int32) int32 { // Round to nearest multiple of tickSpacing rounded := (tick / tickSpacing) * tickSpacing // Clamp to valid range if rounded < MinTick { return MinTick } if rounded > MaxTick { return MaxTick } return rounded }