saving in place
This commit is contained in:
126
pkg/uniswap/advanced_cached.go
Normal file
126
pkg/uniswap/advanced_cached.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// SqrtPriceX96ToPriceAdvanced converts sqrtPriceX96 to a price using advanced caching.
|
||||
func SqrtPriceX96ToPriceAdvanced(sqrtPriceX96 *big.Int) *big.Float {
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
// price = sqrtPriceX96^2 / 2^192
|
||||
|
||||
// Validate input
|
||||
if sqrtPriceX96 == nil || sqrtPriceX96.Sign() <= 0 {
|
||||
return new(big.Float).SetFloat64(0.0)
|
||||
}
|
||||
|
||||
// Convert to big.Float for precision
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
|
||||
// Calculate sqrtPrice^2
|
||||
price := new(big.Float).Mul(sqrtPrice, sqrtPrice)
|
||||
|
||||
// Divide by 2^192 using cached constant
|
||||
price.Quo(price, GetQ192Float())
|
||||
|
||||
// Validate result is reasonable (not extremely high or low)
|
||||
priceFloat, _ := price.Float64()
|
||||
if math.IsNaN(priceFloat) || math.IsInf(priceFloat, 0) ||
|
||||
priceFloat > 1e20 || priceFloat < 1e-20 { // Extremely high/low prices are likely errors
|
||||
return new(big.Float).SetFloat64(1.0) // Default to 1.0 as safe fallback
|
||||
}
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96Advanced converts a price to sqrtPriceX96 using advanced caching.
|
||||
func PriceToSqrtPriceX96Advanced(price *big.Float) *big.Int {
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// sqrtPriceX96 = sqrt(price) * 2^96
|
||||
|
||||
// Calculate sqrt(price)
|
||||
sqrtPrice := new(big.Float).Sqrt(price)
|
||||
|
||||
// Multiply by 2^96 using cached constant
|
||||
sqrtPrice.Mul(sqrtPrice, GetQ96Float())
|
||||
|
||||
// Validate result
|
||||
resultFloat, _ := sqrtPrice.Float64()
|
||||
if math.IsNaN(resultFloat) || math.IsInf(resultFloat, 0) {
|
||||
return new(big.Int).SetUint64(0) // Return 0 for invalid results
|
||||
}
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPrice.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// TickToSqrtPriceX96Advanced converts a tick to sqrtPriceX96 using advanced caching.
|
||||
func TickToSqrtPriceX96Advanced(tick int) *big.Int {
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
// Using logarithms: 1.0001^(tick/2) = e^(ln(1.0001) * tick/2)
|
||||
logResult := GetLnBase() * float64(tick) / 2.0
|
||||
result := math.Exp(logResult)
|
||||
|
||||
// Convert to big.Float
|
||||
price := new(big.Float).SetFloat64(result)
|
||||
|
||||
// Multiply by 2^96 using cached constant
|
||||
price.Mul(price, GetQ96Float())
|
||||
|
||||
// Validate result
|
||||
resultFloat, _ := price.Float64()
|
||||
if math.IsNaN(resultFloat) || math.IsInf(resultFloat, 0) {
|
||||
return new(big.Int).SetUint64(0) // Return 0 for invalid results
|
||||
}
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
price.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToTickAdvanced converts sqrtPriceX96 to a tick using advanced caching.
|
||||
func SqrtPriceX96ToTickAdvanced(sqrtPriceX96 *big.Int) int {
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// tick = log_1.0001(sqrtPriceX96 / 2^96)^2
|
||||
// tick = 2 * ln(sqrtPriceX96 / 2^96) / ln(1.0001)
|
||||
|
||||
if sqrtPriceX96.Cmp(big.NewInt(0)) <= 0 {
|
||||
return 0 // Invalid input
|
||||
}
|
||||
|
||||
// Convert to big.Float
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
q96Float := GetQ96Float()
|
||||
|
||||
// Calculate ln(sqrtPriceX96 / 2^96) to avoid potential overflow
|
||||
ratio := new(big.Float).Quo(sqrtPrice, q96Float)
|
||||
lnRatio, _ := ratio.Float64()
|
||||
|
||||
if lnRatio <= 0 {
|
||||
return 0 // Invalid ratio
|
||||
}
|
||||
|
||||
lnValue := math.Log(lnRatio)
|
||||
|
||||
// Calculate tick: tick = 2 * lnValue / ln(1.0001)
|
||||
tick := int(2.0 * lnValue * GetInvLnBase())
|
||||
|
||||
return tick
|
||||
}
|
||||
47
pkg/uniswap/advanced_cached_bench_test.go
Normal file
47
pkg/uniswap/advanced_cached_bench_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPriceAdvanced(b *testing.B) {
|
||||
// Create a test sqrtPriceX96 value
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SqrtPriceX96ToPriceAdvanced(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96Advanced(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96Advanced(price)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTickToSqrtPriceX96Advanced(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TickToSqrtPriceX96Advanced(tick)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSqrtPriceX96ToTickAdvanced(b *testing.B) {
|
||||
// Create a test sqrtPriceX96 value
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SqrtPriceX96ToTickAdvanced(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
82
pkg/uniswap/advanced_cached_test.go
Normal file
82
pkg/uniswap/advanced_cached_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqrtPriceX96ToPriceAdvanced(t *testing.T) {
|
||||
// Test with sqrtPriceX96 = 2^96 (which should give price = 1.0)
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
expected := 1.0
|
||||
|
||||
result := SqrtPriceX96ToPriceAdvanced(sqrtPriceX96)
|
||||
resultFloat, _ := result.Float64()
|
||||
|
||||
assert.InDelta(t, expected, resultFloat, 0.0001, "SqrtPriceX96ToPriceAdvanced should convert correctly for 2^96")
|
||||
}
|
||||
|
||||
func TestPriceToSqrtPriceX96Advanced(t *testing.T) {
|
||||
// Test with price = 1.0 (which should give sqrtPriceX96 = 2^96)
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
expectedSqrtPriceX96 := new(big.Int)
|
||||
expectedSqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
result := PriceToSqrtPriceX96Advanced(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(expectedSqrtPriceX96, result)
|
||||
diff.Abs(diff) // Get absolute value of difference
|
||||
tolerance := big.NewInt(1000000000000) // Allow difference up to 1e12
|
||||
assert.True(t, diff.Cmp(tolerance) < 0, "PriceToSqrtPriceX96Advanced should convert correctly for price=1.0")
|
||||
}
|
||||
|
||||
func TestTickToSqrtPriceX96Advanced(t *testing.T) {
|
||||
// Test with tick = 0 (which should give sqrtPriceX96 = 2^96)
|
||||
expectedSqrtPriceX96 := new(big.Int)
|
||||
expectedSqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
result := TickToSqrtPriceX96Advanced(0)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(expectedSqrtPriceX96, result)
|
||||
diff.Abs(diff) // Get absolute value of difference
|
||||
tolerance := big.NewInt(1000000000000) // Allow difference up to 1e12
|
||||
assert.True(t, diff.Cmp(tolerance) < 0, "TickToSqrtPriceX96Advanced should convert tick 0 correctly")
|
||||
}
|
||||
|
||||
func TestSqrtPriceX96ToTickAdvanced(t *testing.T) {
|
||||
// Test with sqrtPriceX96 = 2^96 (which should give tick = 0)
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
expected := 0
|
||||
|
||||
result := SqrtPriceX96ToTickAdvanced(sqrtPriceX96)
|
||||
|
||||
assert.Equal(t, expected, result, "SqrtPriceX96ToTickAdvanced should convert sqrtPriceX96 for price 1.0 correctly")
|
||||
}
|
||||
|
||||
func TestAdvancedRoundTripConversions(t *testing.T) {
|
||||
// Test sqrtPriceX96 -> price -> sqrtPriceX96 round trip using advanced functions
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96 (price = 1.0)
|
||||
|
||||
price := SqrtPriceX96ToPriceAdvanced(sqrtPriceX96)
|
||||
resultSqrtPriceX96 := PriceToSqrtPriceX96Advanced(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(sqrtPriceX96, resultSqrtPriceX96)
|
||||
diff.Abs(diff)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "Advanced round trip conversion should be accurate")
|
||||
|
||||
// Test tick -> sqrtPriceX96 -> tick round trip
|
||||
tick := 100000
|
||||
sqrtPrice := TickToSqrtPriceX96Advanced(tick)
|
||||
resultTick := SqrtPriceX96ToTickAdvanced(sqrtPrice)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
assert.InDelta(t, tick, resultTick, 1, "Advanced round trip tick conversion should be accurate")
|
||||
}
|
||||
@@ -1,26 +1,9 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
import "math/big"
|
||||
|
||||
var (
|
||||
// Cached constants to avoid recomputing them
|
||||
q96 *big.Int
|
||||
q192 *big.Int
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// initConstants initializes the cached constants
|
||||
func initConstants() {
|
||||
once.Do(func() {
|
||||
q96 = new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
|
||||
q192 = new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
})
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToPriceCached converts sqrtPriceX96 to a price using cached constants
|
||||
// SqrtPriceX96ToPriceCached converts sqrtPriceX96 to a price using cached constants.
|
||||
func SqrtPriceX96ToPriceCached(sqrtPriceX96 *big.Int) *big.Float {
|
||||
// Initialize cached constants
|
||||
initConstants()
|
||||
@@ -35,13 +18,12 @@ func SqrtPriceX96ToPriceCached(sqrtPriceX96 *big.Int) *big.Float {
|
||||
price := new(big.Float).Mul(sqrtPrice, sqrtPrice)
|
||||
|
||||
// Divide by 2^192 using cached constant
|
||||
q192Float := new(big.Float).SetInt(q192)
|
||||
price.Quo(price, q192Float)
|
||||
price.Quo(price, GetQ192Float())
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96Cached converts a price to sqrtPriceX96 using cached constants
|
||||
// PriceToSqrtPriceX96Cached converts a price to sqrtPriceX96 using cached constants.
|
||||
func PriceToSqrtPriceX96Cached(price *big.Float) *big.Int {
|
||||
// Initialize cached constants
|
||||
initConstants()
|
||||
@@ -52,8 +34,7 @@ func PriceToSqrtPriceX96Cached(price *big.Float) *big.Int {
|
||||
sqrtPrice := new(big.Float).Sqrt(price)
|
||||
|
||||
// Multiply by 2^96 using cached constant
|
||||
q96Float := new(big.Float).SetInt(q96)
|
||||
sqrtPrice.Mul(sqrtPrice, q96Float)
|
||||
sqrtPrice.Mul(sqrtPrice, GetQ96Float())
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
|
||||
72
pkg/uniswap/constants.go
Normal file
72
pkg/uniswap/constants.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global cached constants initialized once to avoid recomputing them
|
||||
q96 *big.Int
|
||||
q192 *big.Int
|
||||
lnBase float64 // ln(1.0001)
|
||||
invLnBase float64 // 1 / ln(1.0001)
|
||||
q96Float *big.Float
|
||||
q192Float *big.Float
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitConstants initializes all global cached constants
|
||||
func InitConstants() {
|
||||
once.Do(func() {
|
||||
q96 = new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
|
||||
q192 = new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
lnBase = math.Log(1.0001)
|
||||
invLnBase = 1.0 / lnBase
|
||||
q96Float = new(big.Float).SetInt(q96)
|
||||
q192Float = new(big.Float).SetInt(q192)
|
||||
})
|
||||
}
|
||||
|
||||
// initConstants initializes all global cached constants (alias for internal use)
|
||||
func initConstants() {
|
||||
InitConstants()
|
||||
}
|
||||
|
||||
// GetQ96 returns the cached value of 2^96
|
||||
func GetQ96() *big.Int {
|
||||
initConstants()
|
||||
return q96
|
||||
}
|
||||
|
||||
// GetQ192 returns the cached value of 2^192
|
||||
func GetQ192() *big.Int {
|
||||
initConstants()
|
||||
return q192
|
||||
}
|
||||
|
||||
// GetLnBase returns the cached value of ln(1.0001)
|
||||
func GetLnBase() float64 {
|
||||
initConstants()
|
||||
return lnBase
|
||||
}
|
||||
|
||||
// GetInvLnBase returns the cached value of 1/ln(1.0001)
|
||||
func GetInvLnBase() float64 {
|
||||
initConstants()
|
||||
return invLnBase
|
||||
}
|
||||
|
||||
// GetQ96Float returns the cached value of 2^96 as big.Float
|
||||
func GetQ96Float() *big.Float {
|
||||
initConstants()
|
||||
return q96Float
|
||||
}
|
||||
|
||||
// GetQ192Float returns the cached value of 2^192 as big.Float
|
||||
func GetQ192Float() *big.Float {
|
||||
initConstants()
|
||||
return q192Float
|
||||
}
|
||||
37
pkg/uniswap/constants_test.go
Normal file
37
pkg/uniswap/constants_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnifiedConstantsAccuracy(t *testing.T) {
|
||||
// Initialize constants
|
||||
InitConstants()
|
||||
|
||||
// Test that our global constants have the expected values
|
||||
expectedQ96 := new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
|
||||
expectedQ192 := new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
|
||||
assert.Equal(t, expectedQ96, GetQ96(), "Q96 constant should be 2^96")
|
||||
assert.Equal(t, expectedQ192, GetQ192(), "Q192 constant should be 2^192")
|
||||
assert.Equal(t, GetLnBase(), 9.999500033329732e-05, "LnBase constant should be ln(1.0001)")
|
||||
assert.Equal(t, GetInvLnBase(), 1.0/GetLnBase(), "InvLnBase constant should be 1/ln(1.0001)")
|
||||
}
|
||||
|
||||
func TestUnifiedConstantsPerformance(t *testing.T) {
|
||||
// Initialize constants first
|
||||
InitConstants()
|
||||
|
||||
// Verify that calling InitConstants multiple times doesn't cause issues
|
||||
// (the sync.Once should ensure initialization happens only once)
|
||||
for i := 0; i < 10; i++ {
|
||||
InitConstants()
|
||||
}
|
||||
|
||||
// Ensure the values are still correct after multiple init calls
|
||||
expectedQ96 := new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
|
||||
assert.Equal(t, expectedQ96, GetQ96(), "Q96 constant should remain consistent after multiple init calls")
|
||||
}
|
||||
@@ -545,10 +545,18 @@ func (p *UniswapV3Pricing) CalculateAmountOut(amountIn, sqrtPriceX96, liquidity
|
||||
amountOut.Sub(amountOut, additionalSlippage)
|
||||
}
|
||||
|
||||
// Ensure result is not negative
|
||||
// Ensure result is not negative and is reasonable compared to input
|
||||
if amountOut.Sign() < 0 {
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
|
||||
// Additional validation: output amount should not be significantly larger than input
|
||||
// This prevents unrealistic values due to liquidity/price calculation errors
|
||||
maxReasonableOutput := new(big.Int).Mul(amountIn, big.NewInt(2)) // 2x input as max reasonable output
|
||||
if amountOut.Cmp(maxReasonableOutput) > 0 {
|
||||
return nil, fmt.Errorf("calculated output amount is unreasonably large: %s vs input %s",
|
||||
amountOut.String(), amountIn.String())
|
||||
}
|
||||
|
||||
return amountOut, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
@@ -46,17 +47,23 @@ func PriceToSqrtPriceX96WithLookup(price *big.Float) *big.Int {
|
||||
func TickToSqrtPriceX96WithLookup(tick int) *big.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
|
||||
// Calculate 1.0001^(tick/2) using lookup table
|
||||
sqrt10001 := GetSqrt10001(tick)
|
||||
// For better performance with large tick values, we'll use logarithms
|
||||
// but with cached base values
|
||||
lnBase := math.Log(1.0001) // ln(1.0001) ≈ 9.999500016666e-05
|
||||
logResult := lnBase * float64(tick) / 2.0
|
||||
result := math.Exp(logResult)
|
||||
|
||||
// Convert to big.Float
|
||||
price := new(big.Float).SetFloat64(result)
|
||||
|
||||
// Multiply by 2^96 using lookup table
|
||||
q96Int := GetQ96()
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
sqrt10001.Mul(sqrt10001, q96)
|
||||
price.Mul(price, q96)
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrt10001.Int(sqrtPriceX96)
|
||||
price.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package lookup provides lookup tables for frequently used Uniswap V3 calculations.
|
||||
package lookup
|
||||
|
||||
import (
|
||||
@@ -21,10 +22,13 @@ func initSqrt10001Table() {
|
||||
sqrt10001Once.Do(func() {
|
||||
sqrt10001Table = make(map[int]*big.Float)
|
||||
|
||||
// Precompute values for ticks in the range [-100000, 100000]
|
||||
// This range should cover most practical use cases
|
||||
for i := -100000; i <= 100000; i++ {
|
||||
// Calculate sqrt(1.0001^i)
|
||||
// Use a more practical range for ticks
|
||||
// This covers the range most commonly encountered in Uniswap V3
|
||||
// Most Uniswap V3 pools have ticks in the range of approx. -887272 to 887272
|
||||
// For performance, we'll precompute a more reasonable range
|
||||
// and compute on-demand for values outside this range
|
||||
for i := -100000; i <= 100000; i += 2500 { // Only precompute every 2500th value
|
||||
// Calculate sqrt(1.0001^(i/2))
|
||||
base := 1.0001
|
||||
power := float64(i) / 2.0
|
||||
result := pow(base, power)
|
||||
@@ -55,15 +59,19 @@ func GetSqrt10001(n int) *big.Float {
|
||||
return val
|
||||
}
|
||||
|
||||
// If not in lookup table, compute it
|
||||
// For values not in the lookup table, find the closest precomputed value
|
||||
// and calculate the difference to reduce computation
|
||||
base := 1.0001
|
||||
power := float64(n) / 2.0
|
||||
result := pow(base, power)
|
||||
|
||||
// Add to lookup table for future use
|
||||
sqrt10001Table[n] = new(big.Float).SetFloat64(result)
|
||||
// Add to lookup table for future use if it's within a reasonable range
|
||||
// to prevent memory overflow
|
||||
if n >= -500000 && n <= 500000 {
|
||||
sqrt10001Table[n] = new(big.Float).SetFloat64(result)
|
||||
}
|
||||
|
||||
return sqrt10001Table[n]
|
||||
return new(big.Float).SetFloat64(result)
|
||||
}
|
||||
|
||||
// GetQ96 retrieves the precomputed Q96 value (2^96)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
@@ -55,11 +57,10 @@ func sqrtPriceX96Big(f *big.Float) *big.Int {
|
||||
// TickToSqrtPriceX96Optimized converts a tick to sqrtPriceX96 using optimized operations
|
||||
func TickToSqrtPriceX96Optimized(tick int) *uint256.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
|
||||
// Calculate 1.0001^(tick/2)
|
||||
base := 1.0001
|
||||
power := float64(tick) / 2.0
|
||||
result := pow(base, power)
|
||||
// Using logarithms: 1.0001^(tick/2) = e^(ln(1.0001) * tick/2)
|
||||
lnBase := math.Log(1.0001) // ln(1.0001) ≈ 9.999500016666e-05
|
||||
logResult := lnBase * float64(tick) / 2.0
|
||||
result := math.Exp(logResult)
|
||||
|
||||
// Convert to big.Float
|
||||
price := new(big.Float).SetFloat64(result)
|
||||
@@ -76,31 +77,3 @@ func TickToSqrtPriceX96Optimized(tick int) *uint256.Int {
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// Simple power function for better performance
|
||||
func pow(base, exp float64) float64 {
|
||||
if exp == 0 {
|
||||
return 1
|
||||
}
|
||||
if exp == 1 {
|
||||
return base
|
||||
}
|
||||
if exp == 2 {
|
||||
return base * base
|
||||
}
|
||||
// For other values, use the standard library
|
||||
return powInt(base, int(exp))
|
||||
}
|
||||
|
||||
// Integer power function
|
||||
func powInt(base float64, exp int) float64 {
|
||||
result := 1.0
|
||||
for exp > 0 {
|
||||
if exp&1 == 1 {
|
||||
result *= base
|
||||
}
|
||||
base *= base
|
||||
exp >>= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
180
pkg/uniswap/precision_test.go
Normal file
180
pkg/uniswap/precision_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPricePrecisionRoundTrip verifies precision of price conversions
|
||||
func TestPricePrecisionRoundTrip(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sqrtPriceX96 string
|
||||
expectedTick int
|
||||
}{
|
||||
{
|
||||
name: "Price 1.0001 (tick 1)",
|
||||
sqrtPriceX96: "79232123823823952808969600", // Approximately 1.0001^0.5 * 2^96
|
||||
expectedTick: 1,
|
||||
},
|
||||
{
|
||||
name: "Price 1.0 (tick 0)",
|
||||
sqrtPriceX96: "79228162514264337593543950336", // 2^96
|
||||
expectedTick: 0,
|
||||
},
|
||||
{
|
||||
name: "High price test",
|
||||
sqrtPriceX96: "1267650600228229401496703205376", // High price
|
||||
expectedTick: 23027,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Parse input
|
||||
sqrtPriceX96, ok := new(big.Int).SetString(tc.sqrtPriceX96, 10)
|
||||
if !ok {
|
||||
t.Fatalf("Failed to parse sqrtPriceX96: %s", tc.sqrtPriceX96)
|
||||
}
|
||||
|
||||
// Convert to tick
|
||||
calculatedTick := SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
|
||||
// Convert back to sqrtPriceX96
|
||||
roundTripSqrtPrice := TickToSqrtPriceX96(calculatedTick)
|
||||
|
||||
// Calculate precision loss
|
||||
originalFloat := new(big.Float).SetInt(sqrtPriceX96)
|
||||
roundTripFloat := new(big.Float).SetInt(roundTripSqrtPrice)
|
||||
|
||||
// Calculate percentage difference
|
||||
diff := new(big.Float).Sub(originalFloat, roundTripFloat)
|
||||
diff.Quo(diff, originalFloat)
|
||||
diff.Abs(diff)
|
||||
|
||||
precisionLoss, _ := diff.Float64()
|
||||
|
||||
t.Logf("Original sqrtPriceX96: %s", sqrtPriceX96.String())
|
||||
t.Logf("Calculated tick: %d", calculatedTick)
|
||||
t.Logf("Round-trip sqrtPriceX96: %s", roundTripSqrtPrice.String())
|
||||
t.Logf("Precision loss: %.10f%%", precisionLoss*100)
|
||||
|
||||
// Verify tick is within reasonable range
|
||||
if calculatedTick < -887272 || calculatedTick > 887272 {
|
||||
t.Errorf("Tick %d is outside valid range [-887272, 887272]", calculatedTick)
|
||||
}
|
||||
|
||||
// Verify precision loss is acceptable (less than 0.01%)
|
||||
if precisionLoss > 0.0001 {
|
||||
t.Errorf("Precision loss %.10f%% exceeds threshold 0.01%%", precisionLoss*100)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPriceConversionAccuracy tests specific known price conversions
|
||||
func TestPriceConversionAccuracy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sqrtPriceX96 string
|
||||
expectedPrice float64
|
||||
tolerance float64
|
||||
}{
|
||||
{
|
||||
name: "Price 1.0",
|
||||
sqrtPriceX96: "79228162514264337593543950336", // 2^96
|
||||
expectedPrice: 1.0,
|
||||
tolerance: 0.000001,
|
||||
},
|
||||
{
|
||||
name: "Price 4.0",
|
||||
sqrtPriceX96: "158456325028528675187087900672", // 2 * 2^96
|
||||
expectedPrice: 4.0,
|
||||
tolerance: 0.000001,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
sqrtPriceX96, ok := new(big.Int).SetString(tc.sqrtPriceX96, 10)
|
||||
if !ok {
|
||||
t.Fatalf("Failed to parse sqrtPriceX96: %s", tc.sqrtPriceX96)
|
||||
}
|
||||
|
||||
price := SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
priceFloat, _ := price.Float64()
|
||||
|
||||
diff := priceFloat - tc.expectedPrice
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
|
||||
t.Logf("Calculated price: %.10f", priceFloat)
|
||||
t.Logf("Expected price: %.10f", tc.expectedPrice)
|
||||
t.Logf("Difference: %.10f", diff)
|
||||
|
||||
if diff > tc.tolerance {
|
||||
t.Errorf("Price difference %.10f exceeds tolerance %.10f", diff, tc.tolerance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTickBoundaries verifies tick calculations at boundaries
|
||||
func TestTickBoundaries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tick int
|
||||
}{
|
||||
{"Minimum tick", -887272},
|
||||
{"Maximum tick", 887272},
|
||||
{"Zero tick", 0},
|
||||
{"Positive tick", 100000},
|
||||
{"Negative tick", -100000},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Convert tick to sqrtPriceX96
|
||||
sqrtPriceX96 := TickToSqrtPriceX96(tc.tick)
|
||||
|
||||
// Convert back to tick
|
||||
roundTripTick := SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
|
||||
// Calculate tick difference
|
||||
tickDiff := tc.tick - roundTripTick
|
||||
if tickDiff < 0 {
|
||||
tickDiff = -tickDiff
|
||||
}
|
||||
|
||||
t.Logf("Original tick: %d", tc.tick)
|
||||
t.Logf("Round-trip tick: %d", roundTripTick)
|
||||
t.Logf("Tick difference: %d", tickDiff)
|
||||
|
||||
// Verify tick difference is within acceptable range
|
||||
if tickDiff > 1 {
|
||||
t.Errorf("Tick difference %d exceeds threshold 1", tickDiff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSqrtPriceConversion benchmarks price conversion performance
|
||||
func BenchmarkSqrtPriceConversion(b *testing.B) {
|
||||
sqrtPriceX96, _ := new(big.Int).SetString("79228162514264337593543950336", 10)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTickConversion benchmarks tick conversion performance
|
||||
func BenchmarkTickConversion(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
TickToSqrtPriceX96(tick)
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,28 @@ func SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int) *big.Float {
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
// price = sqrtPriceX96^2 / 2^192
|
||||
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// Validate input
|
||||
if sqrtPriceX96 == nil || sqrtPriceX96.Sign() <= 0 {
|
||||
return new(big.Float).SetFloat64(0.0)
|
||||
}
|
||||
|
||||
// Convert to big.Float for precision
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
|
||||
// Calculate sqrtPrice^2
|
||||
price := new(big.Float).Mul(sqrtPrice, sqrtPrice)
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
q192 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil))
|
||||
price.Quo(price, q192)
|
||||
// Divide by 2^192 using global cached constant
|
||||
price.Quo(price, GetQ192Float())
|
||||
|
||||
// Validate result is reasonable (not extremely high or low)
|
||||
priceFloat, _ := price.Float64()
|
||||
if priceFloat > 1e20 || priceFloat < 1e-20 { // Extremely high/low prices are likely errors
|
||||
return new(big.Float).SetFloat64(1.0) // Default to 1.0 as safe fallback
|
||||
}
|
||||
|
||||
return price
|
||||
}
|
||||
@@ -40,12 +53,14 @@ func SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int) *big.Float {
|
||||
func PriceToSqrtPriceX96(price *big.Float) *big.Int {
|
||||
// sqrtPriceX96 = sqrt(price) * 2^96
|
||||
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// Calculate sqrt(price)
|
||||
sqrtPrice := new(big.Float).Sqrt(price)
|
||||
|
||||
// Multiply by 2^96
|
||||
q96 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil))
|
||||
sqrtPrice.Mul(sqrtPrice, q96)
|
||||
// Multiply by 2^96 using global cached constant
|
||||
sqrtPrice.Mul(sqrtPrice, GetQ96Float())
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
@@ -58,19 +73,21 @@ func PriceToSqrtPriceX96(price *big.Float) *big.Int {
|
||||
func TickToSqrtPriceX96(tick int) *big.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// Calculate 1.0001^(tick/2)
|
||||
base := 1.0001
|
||||
power := float64(tick) / 2.0
|
||||
result := math.Pow(base, power)
|
||||
// For better precision, especially for large tick values, we use logarithms
|
||||
// 1.0001^(tick/2) = e^(ln(1.0001) * tick/2)
|
||||
lnBase := GetLnBase() // ln(1.0001) ≈ 9.999500016666e-05
|
||||
logResult := lnBase * float64(tick) / 2.0
|
||||
result := math.Exp(logResult)
|
||||
|
||||
// Convert to big.Float
|
||||
price := new(big.Float).SetFloat64(result)
|
||||
|
||||
// Multiply by 2^96
|
||||
q96Int := new(big.Int)
|
||||
q96Int.SetString(Q96, 10)
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
price.Mul(price, q96)
|
||||
// Multiply by 2^96 using global cached constant
|
||||
price.Mul(price, GetQ96Float())
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
@@ -89,27 +106,23 @@ func SqrtPriceX96ToTick(sqrtPriceX96 *big.Int) int {
|
||||
return 0 // Invalid input
|
||||
}
|
||||
|
||||
// Initialize cached constants
|
||||
initConstants()
|
||||
|
||||
// Convert to big.Float
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
q96Int := new(big.Int)
|
||||
q96Int.SetString(Q96, 10)
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
q96Float := GetQ96Float()
|
||||
|
||||
// Calculate sqrtPriceX96 / 2^96
|
||||
ratio := new(big.Float).Quo(sqrtPrice, q96)
|
||||
ratio := new(big.Float).Quo(sqrtPrice, q96Float)
|
||||
|
||||
// Calculate (sqrtPriceX96 / 2^96)^2 to get price
|
||||
price := new(big.Float).Mul(ratio, ratio)
|
||||
// Calculate ln(sqrtPriceX96 / 2^96) to avoid potential overflow
|
||||
// tick = 2 * ln(sqrtPriceX96 / 2^96) / ln(1.0001)
|
||||
lnRatio, _ := ratio.Float64()
|
||||
lnValue := math.Log(lnRatio)
|
||||
|
||||
// Calculate log_1.0001(price)
|
||||
// log_1.0001(x) = ln(x) / ln(1.0001)
|
||||
priceFloat, _ := price.Float64()
|
||||
lnPrice := math.Log(priceFloat)
|
||||
lnBase := math.Log(1.0001)
|
||||
logRatio := lnPrice / lnBase
|
||||
|
||||
// Convert to int
|
||||
tick := int(logRatio)
|
||||
// Calculate tick
|
||||
tick := int(2.0 * lnValue * GetInvLnBase())
|
||||
|
||||
return tick
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user