feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
126
orig/pkg/uniswap/advanced_cached.go
Normal file
126
orig/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
orig/pkg/uniswap/advanced_cached_bench_test.go
Normal file
47
orig/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
orig/pkg/uniswap/advanced_cached_test.go
Normal file
82
orig/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")
|
||||
}
|
||||
44
orig/pkg/uniswap/cached.go
Normal file
44
orig/pkg/uniswap/cached.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import "math/big"
|
||||
|
||||
// SqrtPriceX96ToPriceCached converts sqrtPriceX96 to a price using cached constants.
|
||||
func SqrtPriceX96ToPriceCached(sqrtPriceX96 *big.Int) *big.Float {
|
||||
// Initialize cached constants
|
||||
initConstants()
|
||||
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
// price = sqrtPriceX96^2 / 2^192
|
||||
|
||||
// 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())
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96Cached converts a price to sqrtPriceX96 using cached constants.
|
||||
func PriceToSqrtPriceX96Cached(price *big.Float) *big.Int {
|
||||
// Initialize 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())
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPrice.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
27
orig/pkg/uniswap/cached_bench_test.go
Normal file
27
orig/pkg/uniswap/cached_bench_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPriceCached(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++ {
|
||||
_ = SqrtPriceX96ToPriceCached(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96Cached(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96Cached(price)
|
||||
}
|
||||
}
|
||||
33
orig/pkg/uniswap/cached_test.go
Normal file
33
orig/pkg/uniswap/cached_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCachedFunctionAccuracy(t *testing.T) {
|
||||
// Test SqrtPriceX96ToPrice vs SqrtPriceX96ToPriceCached
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96 (price = 1.0)
|
||||
|
||||
originalResult := SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
cachedResult := SqrtPriceX96ToPriceCached(sqrtPriceX96)
|
||||
|
||||
// Compare the results
|
||||
originalFloat, _ := originalResult.Float64()
|
||||
cachedFloat, _ := cachedResult.Float64()
|
||||
|
||||
assert.InDelta(t, originalFloat, cachedFloat, 0.0001, "SqrtPriceX96ToPrice and SqrtPriceX96ToPriceCached should produce similar results")
|
||||
|
||||
// Test PriceToSqrtPriceX96 vs PriceToSqrtPriceX96Cached
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
originalResult2 := PriceToSqrtPriceX96(price)
|
||||
cachedResult2 := PriceToSqrtPriceX96Cached(price)
|
||||
|
||||
// Compare the results
|
||||
diff := new(big.Int).Sub(originalResult2, cachedResult2)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "PriceToSqrtPriceX96 and PriceToSqrtPriceX96Cached should produce similar results")
|
||||
}
|
||||
72
orig/pkg/uniswap/constants.go
Normal file
72
orig/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).SetPrec(256).SetInt(q96)
|
||||
q192Float = new(big.Float).SetPrec(256).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
orig/pkg/uniswap/constants_test.go
Normal file
37
orig/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")
|
||||
}
|
||||
554
orig/pkg/uniswap/contracts.go
Normal file
554
orig/pkg/uniswap/contracts.go
Normal file
@@ -0,0 +1,554 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// UniswapV3Pool represents a Uniswap V3 pool contract interface
|
||||
type UniswapV3Pool struct {
|
||||
address common.Address
|
||||
client *ethclient.Client
|
||||
abi abi.ABI
|
||||
}
|
||||
|
||||
// PoolState represents the current state of a Uniswap V3 pool
|
||||
type PoolState struct {
|
||||
SqrtPriceX96 *uint256.Int
|
||||
Tick int
|
||||
Liquidity *uint256.Int
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Fee int64
|
||||
}
|
||||
|
||||
// Uniswap V3 Pool ABI (only the functions we need)
|
||||
const UniswapV3PoolABI = `[
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "slot0",
|
||||
"outputs": [
|
||||
{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
|
||||
{"internalType": "int24", "name": "tick", "type": "int24"},
|
||||
{"internalType": "uint16", "name": "observationIndex", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinality", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"},
|
||||
{"internalType": "uint8", "name": "feeProtocol", "type": "uint8"},
|
||||
{"internalType": "bool", "name": "unlocked", "type": "bool"}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "liquidity",
|
||||
"outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "token0",
|
||||
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "token1",
|
||||
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "fee",
|
||||
"outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
// NewUniswapV3Pool creates a new Uniswap V3 pool interface
|
||||
func NewUniswapV3Pool(address common.Address, client *ethclient.Client) *UniswapV3Pool {
|
||||
// Parse the ABI
|
||||
parsedABI, err := abi.JSON(strings.NewReader(UniswapV3PoolABI))
|
||||
if err != nil {
|
||||
// If ABI parsing fails, continue with empty ABI (fallback mode)
|
||||
parsedABI = abi.ABI{}
|
||||
}
|
||||
|
||||
return &UniswapV3Pool{
|
||||
address: address,
|
||||
client: client,
|
||||
abi: parsedABI,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPoolState fetches the current state of a Uniswap V3 pool
|
||||
func (p *UniswapV3Pool) GetPoolState(ctx context.Context) (*PoolState, error) {
|
||||
// ENHANCED: Use pool detector to verify this is actually a V3 pool before attempting slot0()
|
||||
detector := NewPoolDetector(p.client)
|
||||
poolVersion, err := detector.DetectPoolVersion(ctx, p.address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect pool version for %s: %w", p.address.Hex(), err)
|
||||
}
|
||||
|
||||
// If not a V3 pool, return a descriptive error
|
||||
if poolVersion != PoolVersionV3 {
|
||||
return nil, fmt.Errorf("pool %s is %s, not Uniswap V3 (cannot call slot0)", p.address.Hex(), poolVersion.String())
|
||||
}
|
||||
|
||||
// Call slot0() to get sqrtPriceX96, tick, and other slot0 data
|
||||
slot0Data, err := p.callSlot0(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call slot0: %w", err)
|
||||
}
|
||||
|
||||
// Call liquidity() to get current liquidity
|
||||
liquidity, err := p.callLiquidity(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Call token0() and token1() to get token addresses
|
||||
token0, err := p.callToken0(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call token0: %w", err)
|
||||
}
|
||||
|
||||
token1, err := p.callToken1(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call token1: %w", err)
|
||||
}
|
||||
|
||||
// Call fee() to get fee tier
|
||||
fee, err := p.callFee(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call fee: %w", err)
|
||||
}
|
||||
|
||||
return &PoolState{
|
||||
SqrtPriceX96: slot0Data.SqrtPriceX96,
|
||||
Tick: slot0Data.Tick,
|
||||
Liquidity: liquidity,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Fee: fee,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Slot0Data represents the data returned by slot0()
|
||||
type Slot0Data struct {
|
||||
SqrtPriceX96 *uint256.Int
|
||||
Tick int
|
||||
ObservationIndex int
|
||||
ObservationCardinality int
|
||||
ObservationCardinalityNext int
|
||||
FeeProtocol int
|
||||
Unlocked bool
|
||||
}
|
||||
|
||||
// callSlot0 calls the slot0() function on the pool contract
|
||||
func (p *UniswapV3Pool) callSlot0(ctx context.Context) (*Slot0Data, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("slot0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack slot0 call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call slot0: %w", err)
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check for empty response (indicates V2 pool or invalid contract)
|
||||
if len(result) == 0 {
|
||||
return nil, fmt.Errorf("empty response from slot0 call - pool %s may be V2 (no slot0 function) or invalid contract", p.address.Hex())
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Use Unpack() method which returns values directly, not UnpackIntoInterface
|
||||
unpacked, err := p.abi.Unpack("slot0", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack slot0 result (got %d bytes): %w", len(result), err)
|
||||
}
|
||||
|
||||
// Ensure we have the expected number of return values
|
||||
if len(unpacked) < 7 {
|
||||
return nil, fmt.Errorf("unexpected number of return values from slot0: got %d, expected 7 (pool may not be UniswapV3)", len(unpacked))
|
||||
}
|
||||
|
||||
// Convert the unpacked values
|
||||
sqrtPriceX96, ok := unpacked[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert sqrtPriceX96 to *big.Int")
|
||||
}
|
||||
|
||||
tick, ok := unpacked[1].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert tick to *big.Int")
|
||||
}
|
||||
|
||||
observationIndex, ok := unpacked[2].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationIndex to uint16")
|
||||
}
|
||||
|
||||
observationCardinality, ok := unpacked[3].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationCardinality to uint16")
|
||||
}
|
||||
|
||||
observationCardinalityNext, ok := unpacked[4].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationCardinalityNext to uint16")
|
||||
}
|
||||
|
||||
feeProtocol, ok := unpacked[5].(uint8)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert feeProtocol to uint8")
|
||||
}
|
||||
|
||||
unlocked, ok := unpacked[6].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert unlocked to bool")
|
||||
}
|
||||
|
||||
return &Slot0Data{
|
||||
SqrtPriceX96: uint256.MustFromBig(sqrtPriceX96),
|
||||
Tick: int(tick.Int64()),
|
||||
ObservationIndex: int(observationIndex),
|
||||
ObservationCardinality: int(observationCardinality),
|
||||
ObservationCardinalityNext: int(observationCardinalityNext),
|
||||
FeeProtocol: int(feeProtocol),
|
||||
Unlocked: unlocked,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// callLiquidity calls the liquidity() function on the pool contract
|
||||
func (p *UniswapV3Pool) callLiquidity(ctx context.Context) (*uint256.Int, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("liquidity")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack liquidity call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var liquidity *big.Int
|
||||
err = p.abi.UnpackIntoInterface(&liquidity, "liquidity", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack liquidity result: %w", err)
|
||||
}
|
||||
|
||||
return uint256.MustFromBig(liquidity), nil
|
||||
}
|
||||
|
||||
// callToken0 calls the token0() function on the pool contract
|
||||
func (p *UniswapV3Pool) callToken0(ctx context.Context) (common.Address, error) {
|
||||
return p.callToken(ctx, "token0")
|
||||
}
|
||||
|
||||
// callToken1 calls the token1() function on the pool contract
|
||||
func (p *UniswapV3Pool) callToken1(ctx context.Context) (common.Address, error) {
|
||||
return p.callToken(ctx, "token1")
|
||||
}
|
||||
|
||||
// callToken is a generic function to call token0() or token1() functions on the pool contract
|
||||
func (p *UniswapV3Pool) callToken(ctx context.Context, tokenFunc string) (common.Address, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack(tokenFunc)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to pack %s call: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to call %s: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var token common.Address
|
||||
err = p.abi.UnpackIntoInterface(&token, tokenFunc, result)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to unpack %s result: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// callFee calls the fee() function on the pool contract
|
||||
func (p *UniswapV3Pool) callFee(ctx context.Context) (int64, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("fee")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to pack fee call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to call fee: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var fee *big.Int
|
||||
err = p.abi.UnpackIntoInterface(&fee, "fee", result)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to unpack fee result: %w", err)
|
||||
}
|
||||
|
||||
return fee.Int64(), nil
|
||||
}
|
||||
|
||||
// CalculatePoolAddress calculates the deterministic address of a Uniswap V3 pool
|
||||
func CalculatePoolAddress(factory common.Address, token0, token1 common.Address, fee int64) common.Address {
|
||||
// This implements the CREATE2 address calculation for Uniswap V3 pools
|
||||
// Using the correct salt and init code hash for Uniswap V3
|
||||
|
||||
// Correct Uniswap V3 pool init code hash
|
||||
initCodeHash := common.HexToHash("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54")
|
||||
|
||||
// Encode the pool parameters for the salt
|
||||
encoded := make([]byte, 0, 64)
|
||||
encoded = append(encoded, token0.Bytes()...)
|
||||
encoded = append(encoded, token1.Bytes()...)
|
||||
encoded = append(encoded, common.BigToHash(big.NewInt(fee)).Bytes()...)
|
||||
|
||||
// Calculate the salt
|
||||
salt := crypto.Keccak256Hash(encoded)
|
||||
|
||||
// Calculate CREATE2 address
|
||||
addr := crypto.CreateAddress2(factory, salt, initCodeHash.Bytes())
|
||||
return addr
|
||||
}
|
||||
|
||||
// IsValidPool checks if an address is a valid pool by checking for code existence
|
||||
// This is a simplified version to break import cycle - use PoolValidator for comprehensive validation
|
||||
func IsValidPool(ctx context.Context, client *ethclient.Client, address common.Address) bool {
|
||||
// Check if address has code (basic contract existence check)
|
||||
code, err := client.CodeAt(ctx, address, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must have contract code
|
||||
if len(code) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional basic check: ensure it's not zero address
|
||||
return address != common.HexToAddress("0x0000000000000000000000000000000000000000")
|
||||
}
|
||||
|
||||
// ParseABI parses an ABI JSON string and returns the parsed ABI
|
||||
func ParseABI(abiJSON string) (abi.ABI, error) {
|
||||
return abi.JSON(strings.NewReader(abiJSON))
|
||||
}
|
||||
|
||||
// UniswapV3Pricing provides Uniswap V3 pricing calculations
|
||||
type UniswapV3Pricing struct {
|
||||
client *ethclient.Client
|
||||
}
|
||||
|
||||
// NewUniswapV3Pricing creates a new Uniswap V3 pricing calculator
|
||||
func NewUniswapV3Pricing(client *ethclient.Client) *UniswapV3Pricing {
|
||||
return &UniswapV3Pricing{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPrice calculates the price for a token pair by querying Uniswap V3 pools
|
||||
func (p *UniswapV3Pricing) GetPrice(ctx context.Context, token0, token1 common.Address) (*big.Int, error) {
|
||||
// This is a simplified implementation that queries a common WETH/USDC pool
|
||||
// In production, you would:
|
||||
// 1. Discover pools for the token pair
|
||||
// 2. Query multiple pools to get the best price
|
||||
// 3. Handle different fee tiers
|
||||
|
||||
// For demonstration, we'll use a common pool (WETH/USDC 0.05% fee)
|
||||
// In practice, you would dynamically discover pools for the token pair
|
||||
poolAddress := common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0") // WETH/USDC 0.05% pool on Arbitrum
|
||||
|
||||
// Create pool interface
|
||||
pool := NewUniswapV3Pool(poolAddress, p.client)
|
||||
|
||||
// Get pool state
|
||||
poolState, err := pool.GetPoolState(ctx)
|
||||
if err != nil {
|
||||
// Fallback to realistic mock data with per-pool variation
|
||||
// This simulates what you'd get from a real pool but with deterministic variation
|
||||
|
||||
// Create variation based on token addresses to make different token pairs have different prices
|
||||
token0Bytes := token0.Bytes()
|
||||
token1Bytes := token1.Bytes()
|
||||
|
||||
// Simple hash-based variation
|
||||
variation := int64(token0Bytes[19]) - int64(token1Bytes[19])
|
||||
|
||||
// Base price (in wei, representing price with 18 decimals)
|
||||
basePriceStr := "2000000000000000000000" // 2000 USDC per WETH (2000 * 10^18)
|
||||
basePrice, ok := new(big.Int).SetString(basePriceStr, 10)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse base price")
|
||||
}
|
||||
|
||||
// Apply variation (-50% to +50%)
|
||||
variationBig := big.NewInt(variation)
|
||||
hundred := big.NewInt(100)
|
||||
priceVariation := new(big.Int).Mul(basePrice, variationBig)
|
||||
priceVariation.Div(priceVariation, hundred)
|
||||
finalPrice := new(big.Int).Add(basePrice, priceVariation)
|
||||
|
||||
// Ensure price is positive
|
||||
if finalPrice.Sign() <= 0 {
|
||||
finalPrice = basePrice
|
||||
}
|
||||
|
||||
return finalPrice, nil
|
||||
}
|
||||
|
||||
// Convert sqrtPriceX96 to actual price
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
sqrtPriceX96 := poolState.SqrtPriceX96.ToBig()
|
||||
|
||||
// Calculate sqrtPriceX96^2
|
||||
sqrtPriceSquared := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
q192 := new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
price := new(big.Int).Div(sqrtPriceSquared, q192)
|
||||
|
||||
return price, nil
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToPrice converts sqrtPriceX96 to price
|
||||
func (p *UniswapV3Pricing) SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int) *big.Int {
|
||||
// Convert sqrtPriceX96 to actual price
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
|
||||
if sqrtPriceX96 == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// Calculate sqrtPriceX96^2
|
||||
sqrtPriceSquared := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
q192 := new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
price := new(big.Int).Div(sqrtPriceSquared, q192)
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// CalculateAmountOut calculates output amount using proper Uniswap V3 concentrated liquidity math
|
||||
func (p *UniswapV3Pricing) CalculateAmountOut(amountIn, sqrtPriceX96, liquidity *big.Int) (*big.Int, error) {
|
||||
if amountIn == nil || sqrtPriceX96 == nil || liquidity == nil {
|
||||
return nil, fmt.Errorf("input parameters cannot be nil")
|
||||
}
|
||||
|
||||
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
||||
return nil, fmt.Errorf("input parameters must be positive")
|
||||
}
|
||||
|
||||
// Implement proper Uniswap V3 concentrated liquidity calculation
|
||||
// Based on the formula: Δy = L * (√P₁ - √P₀) where L is liquidity
|
||||
// And the price movement: √P₁ = √P₀ + Δx / L
|
||||
|
||||
// For token0 -> token1 swap:
|
||||
// 1. Calculate new sqrt price after swap
|
||||
// 2. Calculate output amount based on liquidity and price change
|
||||
|
||||
// Calculate Δ(sqrt(P)) based on input amount and liquidity
|
||||
// For exact input: Δ(1/√P) = Δx / L
|
||||
// So: 1/√P₁ = 1/√P₀ + Δx / L
|
||||
// Therefore: √P₁ = √P₀ / (1 + Δx * √P₀ / L)
|
||||
|
||||
// Calculate the new sqrt price after the swap
|
||||
numerator := new(big.Int).Mul(amountIn, sqrtPriceX96)
|
||||
denominator := new(big.Int).Add(liquidity, numerator)
|
||||
|
||||
// Check for overflow/underflow
|
||||
if denominator.Sign() <= 0 {
|
||||
return nil, fmt.Errorf("invalid calculation: denominator non-positive")
|
||||
}
|
||||
|
||||
sqrtPriceNext := new(big.Int).Div(new(big.Int).Mul(liquidity, sqrtPriceX96), denominator)
|
||||
|
||||
// Calculate the output amount: Δy = L * (√P₀ - √P₁)
|
||||
priceDiff := new(big.Int).Sub(sqrtPriceX96, sqrtPriceNext)
|
||||
amountOut := new(big.Int).Mul(liquidity, priceDiff)
|
||||
|
||||
// Adjust for Q96 scaling: divide by 2^96
|
||||
q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
||||
amountOut.Div(amountOut, q96)
|
||||
|
||||
// Apply trading fee (typically 0.3% = 3000 basis points for most pools)
|
||||
// Fee is taken from input, so output is calculated on (amountIn - fee)
|
||||
fee := big.NewInt(3000) // 0.3% in basis points
|
||||
feeAmount := new(big.Int).Mul(amountOut, fee)
|
||||
feeAmount.Div(feeAmount, big.NewInt(1000000)) // Divide by 1M to get basis points
|
||||
amountOut.Sub(amountOut, feeAmount)
|
||||
|
||||
// Additional slippage protection for large trades
|
||||
// If trade is > 1% of liquidity, apply additional slippage
|
||||
tradeSize := new(big.Int).Mul(amountIn, big.NewInt(100))
|
||||
if tradeSize.Cmp(liquidity) > 0 {
|
||||
// Large trade - apply additional slippage of 0.1% per 1% of liquidity
|
||||
liquidityRatio := new(big.Int).Div(tradeSize, liquidity)
|
||||
additionalSlippage := new(big.Int).Mul(amountOut, liquidityRatio)
|
||||
additionalSlippage.Div(additionalSlippage, big.NewInt(10000)) // 0.01% base slippage
|
||||
amountOut.Sub(amountOut, additionalSlippage)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
36
orig/pkg/uniswap/lookup/lookup_bench_test.go
Normal file
36
orig/pkg/uniswap/lookup/lookup_bench_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPriceWithLookup(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++ {
|
||||
_ = SqrtPriceX96ToPriceWithLookup(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96WithLookup(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96WithLookup(price)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTickToSqrtPriceX96WithLookup(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TickToSqrtPriceX96WithLookup(tick)
|
||||
}
|
||||
}
|
||||
69
orig/pkg/uniswap/lookup/optimized.go
Normal file
69
orig/pkg/uniswap/lookup/optimized.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// SqrtPriceX96ToPriceWithLookup converts sqrtPriceX96 to a price using lookup tables
|
||||
func SqrtPriceX96ToPriceWithLookup(sqrtPriceX96 *big.Int) *big.Float {
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
// price = sqrtPriceX96^2 / 2^192
|
||||
|
||||
// Calculate sqrtPriceX96^2
|
||||
sqrtPriceSquared := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Convert to big.Float for division
|
||||
price := new(big.Float).SetInt(sqrtPriceSquared)
|
||||
|
||||
// Divide by 2^192 using lookup table
|
||||
q192 := GetQ192()
|
||||
q192Float := new(big.Float).SetInt(q192)
|
||||
price.Quo(price, q192Float)
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96WithLookup converts a price to sqrtPriceX96 using lookup tables
|
||||
func PriceToSqrtPriceX96WithLookup(price *big.Float) *big.Int {
|
||||
// sqrtPriceX96 = sqrt(price) * 2^96
|
||||
|
||||
// Calculate sqrt(price)
|
||||
sqrtPrice := new(big.Float).Sqrt(price)
|
||||
|
||||
// Multiply by 2^96 using lookup table
|
||||
q96Int := GetQ96()
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
sqrtPrice.Mul(sqrtPrice, q96)
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPrice.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// TickToSqrtPriceX96WithLookup converts a tick to sqrtPriceX96 using lookup tables
|
||||
func TickToSqrtPriceX96WithLookup(tick int) *big.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
|
||||
// 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)
|
||||
price.Mul(price, q96)
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
price.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
120
orig/pkg/uniswap/lookup/tables.go
Normal file
120
orig/pkg/uniswap/lookup/tables.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Package lookup provides lookup tables for frequently used Uniswap V3 calculations.
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// Lookup tables for frequently used values
|
||||
sqrt10001Table map[int]*big.Float
|
||||
q96Table *big.Int
|
||||
q192Table *big.Int
|
||||
|
||||
// Once variables for initializing lookup tables
|
||||
sqrt10001Once sync.Once
|
||||
q96Once sync.Once
|
||||
)
|
||||
|
||||
// initSqrt10001Table initializes the lookup table for sqrt(1.0001^n)
|
||||
func initSqrt10001Table() {
|
||||
sqrt10001Once.Do(func() {
|
||||
sqrt10001Table = make(map[int]*big.Float)
|
||||
|
||||
// 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)
|
||||
|
||||
// Store in lookup table
|
||||
sqrt10001Table[i] = new(big.Float).SetFloat64(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// initQTables initializes the lookup tables for Q96 and Q192
|
||||
func initQTables() {
|
||||
q96Once.Do(func() {
|
||||
// Q96 = 2^96
|
||||
q96Table = new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil)
|
||||
|
||||
// Q192 = 2^192 = (2^96)^2
|
||||
q192Table = new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
})
|
||||
}
|
||||
|
||||
// GetSqrt10001 retrieves the precomputed sqrt(1.0001^n) value
|
||||
func GetSqrt10001(n int) *big.Float {
|
||||
initSqrt10001Table()
|
||||
|
||||
// Check if value is in lookup table
|
||||
if val, ok := sqrt10001Table[n]; ok {
|
||||
return val
|
||||
}
|
||||
|
||||
// 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 if it's within a reasonable range
|
||||
// to prevent memory overflow
|
||||
if n >= -500000 && n <= 500000 {
|
||||
sqrt10001Table[n] = new(big.Float).SetFloat64(result)
|
||||
}
|
||||
|
||||
return new(big.Float).SetFloat64(result)
|
||||
}
|
||||
|
||||
// GetQ96 retrieves the precomputed Q96 value (2^96)
|
||||
func GetQ96() *big.Int {
|
||||
initQTables()
|
||||
return q96Table
|
||||
}
|
||||
|
||||
// GetQ192 retrieves the precomputed Q192 value (2^192)
|
||||
func GetQ192() *big.Int {
|
||||
initQTables()
|
||||
return q192Table
|
||||
}
|
||||
|
||||
// Helper function for computing powers efficiently
|
||||
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 exponentiation by squaring
|
||||
return powInt(base, int(exp))
|
||||
}
|
||||
|
||||
// Integer power function using exponentiation by squaring
|
||||
func powInt(base float64, exp int) float64 {
|
||||
if exp < 0 {
|
||||
return 1.0 / powInt(base, -exp)
|
||||
}
|
||||
|
||||
result := 1.0
|
||||
for exp > 0 {
|
||||
if exp&1 == 1 {
|
||||
result *= base
|
||||
}
|
||||
base *= base
|
||||
exp >>= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
248
orig/pkg/uniswap/multicall.go
Normal file
248
orig/pkg/uniswap/multicall.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// Multicall3 address on Arbitrum
|
||||
var Multicall3Address = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")
|
||||
|
||||
// Multicall3 ABI (simplified - only aggregate3 function)
|
||||
const Multicall3ABI = `[{
|
||||
"inputs": [{
|
||||
"components": [{
|
||||
"internalType": "address",
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
}, {
|
||||
"internalType": "bool",
|
||||
"name": "allowFailure",
|
||||
"type": "bool"
|
||||
}, {
|
||||
"internalType": "bytes",
|
||||
"name": "callData",
|
||||
"type": "bytes"
|
||||
}],
|
||||
"internalType": "struct Multicall3.Call3[]",
|
||||
"name": "calls",
|
||||
"type": "tuple[]"
|
||||
}],
|
||||
"name": "aggregate3",
|
||||
"outputs": [{
|
||||
"components": [{
|
||||
"internalType": "bool",
|
||||
"name": "success",
|
||||
"type": "bool"
|
||||
}, {
|
||||
"internalType": "bytes",
|
||||
"name": "returnData",
|
||||
"type": "bytes"
|
||||
}],
|
||||
"internalType": "struct Multicall3.Result[]",
|
||||
"name": "returnData",
|
||||
"type": "tuple[]"
|
||||
}],
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
}]`
|
||||
|
||||
// Call3 represents a single call in Multicall3
|
||||
type Call3 struct {
|
||||
Target common.Address
|
||||
AllowFailure bool
|
||||
CallData []byte
|
||||
}
|
||||
|
||||
// Result3 represents the result of a Multicall3 call
|
||||
type Result3 struct {
|
||||
Success bool
|
||||
ReturnData []byte
|
||||
}
|
||||
|
||||
// MulticallBatcher batches multiple RPC calls into a single Multicall3 transaction
|
||||
type MulticallBatcher struct {
|
||||
client *ethclient.Client
|
||||
multicallABI abi.ABI
|
||||
}
|
||||
|
||||
// NewMulticallBatcher creates a new multicall batcher
|
||||
func NewMulticallBatcher(client *ethclient.Client) (*MulticallBatcher, error) {
|
||||
parsedABI, err := abi.JSON(strings.NewReader(Multicall3ABI))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Multicall3 ABI: %w", err)
|
||||
}
|
||||
|
||||
return &MulticallBatcher{
|
||||
client: client,
|
||||
multicallABI: parsedABI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExecuteMulticall executes multiple calls in a single transaction
|
||||
func (m *MulticallBatcher) ExecuteMulticall(ctx context.Context, calls []Call3) ([]Result3, error) {
|
||||
if len(calls) == 0 {
|
||||
return []Result3{}, nil
|
||||
}
|
||||
|
||||
// Pack the aggregate3 call
|
||||
data, err := m.multicallABI.Pack("aggregate3", calls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack multicall data: %w", err)
|
||||
}
|
||||
|
||||
// Create the call message
|
||||
msg := ethereum.CallMsg{
|
||||
To: &Multicall3Address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
// Execute the call
|
||||
result, err := m.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute multicall: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the results
|
||||
var results []Result3
|
||||
err = m.multicallABI.UnpackIntoInterface(&results, "aggregate3", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack multicall results: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// BatchPoolDataCalls batches slot0, liquidity, token0, token1, and fee calls for a pool
|
||||
func (m *MulticallBatcher) BatchPoolDataCalls(ctx context.Context, poolAddress common.Address, poolABI abi.ABI) (
|
||||
slot0Data []byte,
|
||||
liquidityData []byte,
|
||||
token0Data []byte,
|
||||
token1Data []byte,
|
||||
feeData []byte,
|
||||
err error,
|
||||
) {
|
||||
// Prepare call data for each function
|
||||
slot0CallData, err := poolABI.Pack("slot0")
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack slot0: %w", err)
|
||||
}
|
||||
|
||||
liquidityCallData, err := poolABI.Pack("liquidity")
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack liquidity: %w", err)
|
||||
}
|
||||
|
||||
token0CallData, err := poolABI.Pack("token0")
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack token0: %w", err)
|
||||
}
|
||||
|
||||
token1CallData, err := poolABI.Pack("token1")
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack token1: %w", err)
|
||||
}
|
||||
|
||||
feeCallData, err := poolABI.Pack("fee")
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack fee: %w", err)
|
||||
}
|
||||
|
||||
// Create multicall calls
|
||||
calls := []Call3{
|
||||
{Target: poolAddress, AllowFailure: true, CallData: slot0CallData},
|
||||
{Target: poolAddress, AllowFailure: true, CallData: liquidityCallData},
|
||||
{Target: poolAddress, AllowFailure: true, CallData: token0CallData},
|
||||
{Target: poolAddress, AllowFailure: true, CallData: token1CallData},
|
||||
{Target: poolAddress, AllowFailure: true, CallData: feeCallData},
|
||||
}
|
||||
|
||||
// Execute multicall
|
||||
results, err := m.ExecuteMulticall(ctx, calls)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Extract results
|
||||
if len(results) != 5 {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("expected 5 results, got %d", len(results))
|
||||
}
|
||||
|
||||
// Return raw data (caller will unpack based on success flags)
|
||||
return results[0].ReturnData,
|
||||
results[1].ReturnData,
|
||||
results[2].ReturnData,
|
||||
results[3].ReturnData,
|
||||
results[4].ReturnData,
|
||||
nil
|
||||
}
|
||||
|
||||
// BatchMultiplePoolData batches pool data calls for multiple pools
|
||||
func (m *MulticallBatcher) BatchMultiplePoolData(ctx context.Context, pools []common.Address, poolABI abi.ABI) (
|
||||
results map[common.Address]map[string][]byte,
|
||||
err error,
|
||||
) {
|
||||
results = make(map[common.Address]map[string][]byte)
|
||||
|
||||
if len(pools) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Prepare all calls
|
||||
var allCalls []Call3
|
||||
callMap := make(map[int]struct {
|
||||
poolAddr common.Address
|
||||
funcName string
|
||||
})
|
||||
|
||||
functions := []string{"slot0", "liquidity", "token0", "token1", "fee"}
|
||||
|
||||
callIndex := 0
|
||||
for _, poolAddr := range pools {
|
||||
for _, funcName := range functions {
|
||||
callData, err := poolABI.Pack(funcName)
|
||||
if err != nil {
|
||||
// Skip this call if packing fails
|
||||
continue
|
||||
}
|
||||
|
||||
allCalls = append(allCalls, Call3{
|
||||
Target: poolAddr,
|
||||
AllowFailure: true,
|
||||
CallData: callData,
|
||||
})
|
||||
|
||||
callMap[callIndex] = struct {
|
||||
poolAddr common.Address
|
||||
funcName string
|
||||
}{poolAddr, funcName}
|
||||
callIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// Execute multicall
|
||||
multicallResults, err := m.ExecuteMulticall(ctx, allCalls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute multicall: %w", err)
|
||||
}
|
||||
|
||||
// Parse results
|
||||
for i, result := range multicallResults {
|
||||
if info, ok := callMap[i]; ok {
|
||||
if _, exists := results[info.poolAddr]; !exists {
|
||||
results[info.poolAddr] = make(map[string][]byte)
|
||||
}
|
||||
if result.Success {
|
||||
results[info.poolAddr][info.funcName] = result.ReturnData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
79
orig/pkg/uniswap/optimized.go
Normal file
79
orig/pkg/uniswap/optimized.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// SqrtPriceX96ToPriceOptimized converts sqrtPriceX96 to a price using uint256 operations
|
||||
// This is a more optimized version that avoids big.Float operations where possible
|
||||
func SqrtPriceX96ToPriceOptimized(sqrtPriceX96 *uint256.Int) *big.Float {
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
// price = sqrtPriceX96^2 / 2^192
|
||||
|
||||
// Calculate sqrtPriceX96^2 using uint256
|
||||
sqrtPriceSquared := new(uint256.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Convert to big.Float for division
|
||||
price := new(big.Float).SetInt(sqrtPriceSquared.ToBig())
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
// We can use a precomputed value for 2^192
|
||||
q192 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil))
|
||||
price.Quo(price, q192)
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96Optimized converts a price to sqrtPriceX96 using optimized operations
|
||||
func PriceToSqrtPriceX96Optimized(price *big.Float) *uint256.Int {
|
||||
// sqrtPriceX96 = sqrt(price) * 2^96
|
||||
|
||||
// Calculate sqrt(price)
|
||||
sqrtPrice := new(big.Float).Sqrt(price)
|
||||
|
||||
// Multiply by 2^96
|
||||
q96Int := new(big.Int)
|
||||
q96Int.SetString(Q96, 10)
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
sqrtPrice.Mul(sqrtPrice, q96)
|
||||
|
||||
// Convert to uint256
|
||||
sqrtPriceX96Int := sqrtPriceX96Big(sqrtPrice)
|
||||
sqrtPriceX96 := uint256.MustFromBig(sqrtPriceX96Int)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// Helper function to convert big.Float to big.Int
|
||||
func sqrtPriceX96Big(f *big.Float) *big.Int {
|
||||
i, _ := f.Int(nil)
|
||||
return i
|
||||
}
|
||||
|
||||
// TickToSqrtPriceX96Optimized converts a tick to sqrtPriceX96 using optimized operations
|
||||
func TickToSqrtPriceX96Optimized(tick int) *uint256.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
// 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)
|
||||
|
||||
// Multiply by 2^96
|
||||
q96Int := new(big.Int)
|
||||
q96Int.SetString(Q96, 10)
|
||||
q96 := new(big.Float).SetInt(q96Int)
|
||||
price.Mul(price, q96)
|
||||
|
||||
// Convert to uint256
|
||||
sqrtPriceX96Int := sqrtPriceX96Big(price)
|
||||
sqrtPriceX96 := uint256.MustFromBig(sqrtPriceX96Int)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
39
orig/pkg/uniswap/optimized_bench_test.go
Normal file
39
orig/pkg/uniswap/optimized_bench_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPriceOptimized(b *testing.B) {
|
||||
// Create a test sqrtPriceX96 value using uint256
|
||||
bigInt := new(big.Int)
|
||||
bigInt.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
sqrtPriceX96, _ := uint256.FromBig(bigInt)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = SqrtPriceX96ToPriceOptimized(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96Optimized(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96Optimized(price)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTickToSqrtPriceX96Optimized(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TickToSqrtPriceX96Optimized(tick)
|
||||
}
|
||||
}
|
||||
28
orig/pkg/uniswap/optimized_cached.go
Normal file
28
orig/pkg/uniswap/optimized_cached.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Package uniswap provides mathematical functions for Uniswap V3 calculations.
|
||||
package uniswap
|
||||
|
||||
import "math/big"
|
||||
|
||||
// SqrtPriceX96ToPriceOptimizedCached provides the same functionality as SqrtPriceX96ToPriceAdvanced
|
||||
// This alias is maintained for API compatibility with existing code that might reference this function
|
||||
func SqrtPriceX96ToPriceOptimizedCached(sqrtPriceX96 *big.Int) *big.Float {
|
||||
return SqrtPriceX96ToPriceAdvanced(sqrtPriceX96)
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96OptimizedCached provides the same functionality as PriceToSqrtPriceX96Advanced
|
||||
// This alias is maintained for API compatibility with existing code that might reference this function
|
||||
func PriceToSqrtPriceX96OptimizedCached(price *big.Float) *big.Int {
|
||||
return PriceToSqrtPriceX96Advanced(price)
|
||||
}
|
||||
|
||||
// TickToSqrtPriceX96OptimizedCached provides the same functionality as TickToSqrtPriceX96Advanced
|
||||
// This alias is maintained for API compatibility with existing code that might reference this function
|
||||
func TickToSqrtPriceX96OptimizedCached(tick int) *big.Int {
|
||||
return TickToSqrtPriceX96Advanced(tick)
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToTickOptimizedCached provides the same functionality as SqrtPriceX96ToTickAdvanced
|
||||
// This alias is maintained for API compatibility with existing code that might reference this function
|
||||
func SqrtPriceX96ToTickOptimizedCached(sqrtPriceX96 *big.Int) int {
|
||||
return SqrtPriceX96ToTickAdvanced(sqrtPriceX96)
|
||||
}
|
||||
47
orig/pkg/uniswap/optimized_cached_bench_test.go
Normal file
47
orig/pkg/uniswap/optimized_cached_bench_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPriceOptimizedCached(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++ {
|
||||
_ = SqrtPriceX96ToPriceOptimizedCached(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96OptimizedCached(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96OptimizedCached(price)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTickToSqrtPriceX96OptimizedCached(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TickToSqrtPriceX96OptimizedCached(tick)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSqrtPriceX96ToTickOptimizedCached(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++ {
|
||||
_ = SqrtPriceX96ToTickOptimizedCached(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
79
orig/pkg/uniswap/optimized_cached_test.go
Normal file
79
orig/pkg/uniswap/optimized_cached_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqrtPriceX96ToPriceOptimizedCached(t *testing.T) {
|
||||
// Test with sqrtPriceX96 = 2^96 (should give price = 1.0)
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
result := SqrtPriceX96ToPriceOptimizedCached(sqrtPriceX96)
|
||||
expected := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
resultFloat, _ := result.Float64()
|
||||
expectedFloat, _ := expected.Float64()
|
||||
|
||||
assert.InDelta(t, expectedFloat, resultFloat, 0.0001, "Result should be close to 1.0")
|
||||
}
|
||||
|
||||
func TestPriceToSqrtPriceX96OptimizedCached(t *testing.T) {
|
||||
// Test with price = 1.0 (should give sqrtPriceX96 = 2^96)
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
result := PriceToSqrtPriceX96OptimizedCached(price)
|
||||
expected, _ := new(big.Int).SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(expected, result)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "Result should be close to 2^96")
|
||||
}
|
||||
|
||||
func TestTickToSqrtPriceX96OptimizedCached(t *testing.T) {
|
||||
// Test with tick = 0 (should give sqrtPriceX96 = 2^96)
|
||||
tick := 0
|
||||
|
||||
result := TickToSqrtPriceX96OptimizedCached(tick)
|
||||
expected, _ := new(big.Int).SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(expected, result)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "Result should be close to 2^96 for tick 0")
|
||||
}
|
||||
|
||||
func TestSqrtPriceX96ToTickOptimizedCached(t *testing.T) {
|
||||
// Test with sqrtPriceX96 = 2^96 (should give tick = 0)
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
result := SqrtPriceX96ToTickOptimizedCached(sqrtPriceX96)
|
||||
expected := 0
|
||||
|
||||
assert.Equal(t, expected, result, "Result should be 0 for sqrtPriceX96 = 2^96")
|
||||
}
|
||||
|
||||
func TestOptimizedCachedRoundTripConversions(t *testing.T) {
|
||||
// Test sqrtPriceX96 -> price -> sqrtPriceX96 round trip with optimized cached functions
|
||||
// Since these functions are aliases to the Advanced versions, this tests the same functionality
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96 (price = 1.0)
|
||||
|
||||
price := SqrtPriceX96ToPriceOptimizedCached(sqrtPriceX96)
|
||||
resultSqrtPriceX96 := PriceToSqrtPriceX96OptimizedCached(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(sqrtPriceX96, resultSqrtPriceX96)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "Round trip conversion should be accurate")
|
||||
|
||||
// Test tick -> sqrtPriceX96 -> tick round trip with optimized cached functions
|
||||
tick := 100000
|
||||
sqrtPrice := TickToSqrtPriceX96OptimizedCached(tick)
|
||||
resultTick := SqrtPriceX96ToTickOptimizedCached(sqrtPrice)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
assert.InDelta(t, tick, resultTick, 1, "Round trip tick conversion should be accurate")
|
||||
}
|
||||
50
orig/pkg/uniswap/optimized_test.go
Normal file
50
orig/pkg/uniswap/optimized_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptimizedFunctionAccuracy(t *testing.T) {
|
||||
// Test SqrtPriceX96ToPrice vs SqrtPriceX96ToPriceOptimized
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96 (price = 1.0)
|
||||
|
||||
originalResult := SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
|
||||
sqrtPriceX96Uint256 := uint256.MustFromBig(sqrtPriceX96)
|
||||
optimizedResult := SqrtPriceX96ToPriceOptimized(sqrtPriceX96Uint256)
|
||||
|
||||
// Compare the results
|
||||
originalFloat, _ := originalResult.Float64()
|
||||
optimizedFloat, _ := optimizedResult.Float64()
|
||||
|
||||
assert.InDelta(t, originalFloat, optimizedFloat, 0.0001, "SqrtPriceX96ToPrice and SqrtPriceX96ToPriceOptimized should produce similar results")
|
||||
}
|
||||
|
||||
func TestPriceToSqrtPriceX96Accuracy(t *testing.T) {
|
||||
// Test PriceToSqrtPriceX96 vs PriceToSqrtPriceX96Optimized
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
originalResult := PriceToSqrtPriceX96(price)
|
||||
optimizedResult := PriceToSqrtPriceX96Optimized(price)
|
||||
|
||||
// Compare the results
|
||||
diff := new(big.Int).Sub(originalResult, optimizedResult.ToBig())
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "PriceToSqrtPriceX96 and PriceToSqrtPriceX96Optimized should produce similar results")
|
||||
}
|
||||
|
||||
func TestTickToSqrtPriceX96Accuracy(t *testing.T) {
|
||||
// Test TickToSqrtPriceX96 vs TickToSqrtPriceX96Optimized
|
||||
tick := 100000
|
||||
|
||||
originalResult := TickToSqrtPriceX96(tick)
|
||||
optimizedResult := TickToSqrtPriceX96Optimized(tick)
|
||||
|
||||
// Compare the results
|
||||
diff := new(big.Int).Sub(originalResult, optimizedResult.ToBig())
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "TickToSqrtPriceX96 and TickToSqrtPriceX96Optimized should produce similar results")
|
||||
}
|
||||
273
orig/pkg/uniswap/pool_detector.go
Normal file
273
orig/pkg/uniswap/pool_detector.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// PoolVersion represents the version of a DEX pool
|
||||
type PoolVersion int
|
||||
|
||||
const (
|
||||
PoolVersionUnknown PoolVersion = iota
|
||||
PoolVersionV2 // Uniswap V2 style (uses getReserves)
|
||||
PoolVersionV3 // Uniswap V3 style (uses slot0)
|
||||
PoolVersionBalancer
|
||||
PoolVersionCurve
|
||||
)
|
||||
|
||||
// String returns the string representation of the pool version
|
||||
func (pv PoolVersion) String() string {
|
||||
switch pv {
|
||||
case PoolVersionV2:
|
||||
return "UniswapV2"
|
||||
case PoolVersionV3:
|
||||
return "UniswapV3"
|
||||
case PoolVersionBalancer:
|
||||
return "Balancer"
|
||||
case PoolVersionCurve:
|
||||
return "Curve"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// PoolDetector detects the version of a DEX pool
|
||||
type PoolDetector struct {
|
||||
client *ethclient.Client
|
||||
|
||||
// Cache of detected pool versions
|
||||
versionCache map[common.Address]PoolVersion
|
||||
}
|
||||
|
||||
// NewPoolDetector creates a new pool detector
|
||||
func NewPoolDetector(client *ethclient.Client) *PoolDetector {
|
||||
return &PoolDetector{
|
||||
client: client,
|
||||
versionCache: make(map[common.Address]PoolVersion),
|
||||
}
|
||||
}
|
||||
|
||||
// DetectPoolVersion detects the version of a pool by checking which functions it supports
|
||||
func (pd *PoolDetector) DetectPoolVersion(ctx context.Context, poolAddress common.Address) (PoolVersion, error) {
|
||||
// Check cache first
|
||||
if version, exists := pd.versionCache[poolAddress]; exists {
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// Try V3 first (slot0 function)
|
||||
if pd.hasSlot0(ctx, poolAddress) {
|
||||
pd.versionCache[poolAddress] = PoolVersionV3
|
||||
return PoolVersionV3, nil
|
||||
}
|
||||
|
||||
// Try V2 (getReserves function)
|
||||
if pd.hasGetReserves(ctx, poolAddress) {
|
||||
pd.versionCache[poolAddress] = PoolVersionV2
|
||||
return PoolVersionV2, nil
|
||||
}
|
||||
|
||||
// Try Balancer (getPoolId function)
|
||||
if pd.hasGetPoolId(ctx, poolAddress) {
|
||||
pd.versionCache[poolAddress] = PoolVersionBalancer
|
||||
return PoolVersionBalancer, nil
|
||||
}
|
||||
|
||||
// Unknown pool type
|
||||
pd.versionCache[poolAddress] = PoolVersionUnknown
|
||||
return PoolVersionUnknown, errors.New("unable to detect pool version")
|
||||
}
|
||||
|
||||
// hasSlot0 checks if a pool has the slot0() function (Uniswap V3)
|
||||
func (pd *PoolDetector) hasSlot0(ctx context.Context, poolAddress common.Address) bool {
|
||||
// Create minimal ABI for slot0 function
|
||||
slot0ABI := `[{
|
||||
"inputs": [],
|
||||
"name": "slot0",
|
||||
"outputs": [
|
||||
{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
|
||||
{"internalType": "int24", "name": "tick", "type": "int24"},
|
||||
{"internalType": "uint16", "name": "observationIndex", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinality", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"},
|
||||
{"internalType": "uint8", "name": "feeProtocol", "type": "uint8"},
|
||||
{"internalType": "bool", "name": "unlocked", "type": "bool"}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}]`
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(slot0ABI))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
data, err := parsedABI.Pack("slot0")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := pd.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if result has the expected length for slot0 return values
|
||||
// slot0 returns 7 values, should be at least 7*32 = 224 bytes
|
||||
return len(result) >= 224
|
||||
}
|
||||
|
||||
// hasGetReserves checks if a pool has the getReserves() function (Uniswap V2)
|
||||
func (pd *PoolDetector) hasGetReserves(ctx context.Context, poolAddress common.Address) bool {
|
||||
// Create minimal ABI for getReserves function
|
||||
getReservesABI := `[{
|
||||
"inputs": [],
|
||||
"name": "getReserves",
|
||||
"outputs": [
|
||||
{"internalType": "uint112", "name": "_reserve0", "type": "uint112"},
|
||||
{"internalType": "uint112", "name": "_reserve1", "type": "uint112"},
|
||||
{"internalType": "uint32", "name": "_blockTimestampLast", "type": "uint32"}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}]`
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(getReservesABI))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
data, err := parsedABI.Pack("getReserves")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := pd.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if result has the expected length for getReserves return values
|
||||
// getReserves returns 3 values (uint112, uint112, uint32) = 96 bytes
|
||||
return len(result) >= 96
|
||||
}
|
||||
|
||||
// hasGetPoolId checks if a pool has the getPoolId() function (Balancer)
|
||||
func (pd *PoolDetector) hasGetPoolId(ctx context.Context, poolAddress common.Address) bool {
|
||||
// Create minimal ABI for getPoolId function
|
||||
getPoolIdABI := `[{
|
||||
"inputs": [],
|
||||
"name": "getPoolId",
|
||||
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}]`
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(getPoolIdABI))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
data, err := parsedABI.Pack("getPoolId")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := pd.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if result is a bytes32 (32 bytes)
|
||||
return len(result) == 32
|
||||
}
|
||||
|
||||
// GetReservesV2 fetches reserves from a Uniswap V2 style pool
|
||||
func (pd *PoolDetector) GetReservesV2(ctx context.Context, poolAddress common.Address) (*big.Int, *big.Int, error) {
|
||||
getReservesABI := `[{
|
||||
"inputs": [],
|
||||
"name": "getReserves",
|
||||
"outputs": [
|
||||
{"internalType": "uint112", "name": "_reserve0", "type": "uint112"},
|
||||
{"internalType": "uint112", "name": "_reserve1", "type": "uint112"},
|
||||
{"internalType": "uint32", "name": "_blockTimestampLast", "type": "uint32"}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}]`
|
||||
|
||||
parsedABI, err := abi.JSON(strings.NewReader(getReservesABI))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse getReserves ABI: %w", err)
|
||||
}
|
||||
|
||||
data, err := parsedABI.Pack("getReserves")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to pack getReserves call: %w", err)
|
||||
}
|
||||
|
||||
msg := ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := pd.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to call getReserves: %w", err)
|
||||
}
|
||||
|
||||
unpacked, err := parsedABI.Unpack("getReserves", result)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unpack getReserves result: %w", err)
|
||||
}
|
||||
|
||||
if len(unpacked) < 2 {
|
||||
return nil, nil, fmt.Errorf("unexpected number of return values from getReserves: got %d, expected 3", len(unpacked))
|
||||
}
|
||||
|
||||
reserve0, ok := unpacked[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("failed to convert reserve0 to *big.Int")
|
||||
}
|
||||
|
||||
reserve1, ok := unpacked[1].(*big.Int)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("failed to convert reserve1 to *big.Int")
|
||||
}
|
||||
|
||||
return reserve0, reserve1, nil
|
||||
}
|
||||
|
||||
// ClearCache clears the version cache
|
||||
func (pd *PoolDetector) ClearCache() {
|
||||
pd.versionCache = make(map[common.Address]PoolVersion)
|
||||
}
|
||||
|
||||
// GetCachedVersion returns the cached version for a pool, if available
|
||||
func (pd *PoolDetector) GetCachedVersion(poolAddress common.Address) (PoolVersion, bool) {
|
||||
version, exists := pd.versionCache[poolAddress]
|
||||
return version, exists
|
||||
}
|
||||
180
orig/pkg/uniswap/precision_test.go
Normal file
180
orig/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)
|
||||
}
|
||||
}
|
||||
150
orig/pkg/uniswap/pricing.go
Normal file
150
orig/pkg/uniswap/pricing.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
const (
|
||||
// Q96 represents 2^96 used in Uniswap V3 sqrtPriceX96 calculations
|
||||
Q96 = "79228162514264337593543950336" // 2^96 as string to avoid overflow
|
||||
|
||||
// Tick spacing for different fee tiers
|
||||
LowTickSpacing = 10
|
||||
MediumTickSpacing = 60
|
||||
HighTickSpacing = 200
|
||||
)
|
||||
|
||||
// SqrtPriceX96ToPrice converts sqrtPriceX96 to a price
|
||||
// Price is represented as token1/token0
|
||||
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).SetPrec(256).SetInt(sqrtPriceX96)
|
||||
|
||||
// Calculate sqrtPrice^2
|
||||
price := new(big.Float).SetPrec(256)
|
||||
price.Mul(sqrtPrice, sqrtPrice)
|
||||
|
||||
// Divide by 2^192 using global cached constant
|
||||
denominator := new(big.Float).SetPrec(256).SetInt(GetQ192())
|
||||
price.Quo(price, denominator)
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// PriceToSqrtPriceX96 converts a price to sqrtPriceX96
|
||||
func PriceToSqrtPriceX96(price *big.Float) *big.Int {
|
||||
// sqrtPriceX96 = sqrt(price) * 2^96
|
||||
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// Calculate sqrt(price)
|
||||
input := new(big.Float).SetPrec(256).Copy(price)
|
||||
sqrtPrice := new(big.Float).SetPrec(256).Sqrt(input)
|
||||
|
||||
// Multiply by 2^96 using global cached constant
|
||||
multiplier := new(big.Float).SetPrec(256).SetInt(GetQ96())
|
||||
sqrtPrice.Mul(sqrtPrice, multiplier)
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPrice.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// TickToSqrtPriceX96 converts a tick to sqrtPriceX96
|
||||
func TickToSqrtPriceX96(tick int) *big.Int {
|
||||
// sqrtPriceX96 = 1.0001^(tick/2) * 2^96
|
||||
|
||||
// Initialize global cached constants
|
||||
initConstants()
|
||||
|
||||
// Calculate 1.0001^(tick/2)
|
||||
// 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 using global cached constant
|
||||
price.Mul(price, GetQ96Float())
|
||||
|
||||
// Convert to big.Int
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
price.Int(sqrtPriceX96)
|
||||
|
||||
return sqrtPriceX96
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToTick converts sqrtPriceX96 to a tick
|
||||
func SqrtPriceX96ToTick(sqrtPriceX96 *big.Int) int {
|
||||
// tick = log_1.0001(sqrtPriceX96 / 2^96)^2
|
||||
// tick = log_1.0001(price)
|
||||
// tick = 2 * log_1.0001(sqrtPriceX96 / 2^96)
|
||||
|
||||
if sqrtPriceX96.Cmp(big.NewInt(0)) <= 0 {
|
||||
return 0 // Invalid input
|
||||
}
|
||||
|
||||
// Initialize cached constants
|
||||
initConstants()
|
||||
|
||||
// Convert to big.Float
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
q96Float := GetQ96Float()
|
||||
|
||||
// Calculate sqrtPriceX96 / 2^96
|
||||
ratio := new(big.Float).Quo(sqrtPrice, q96Float)
|
||||
|
||||
// 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 tick
|
||||
tick := int(2.0 * lnValue * GetInvLnBase())
|
||||
|
||||
return tick
|
||||
}
|
||||
|
||||
// GetTickAtSqrtPrice calculates the tick for a given sqrtPriceX96 using uint256
|
||||
func GetTickAtSqrtPrice(sqrtPriceX96 *uint256.Int) int {
|
||||
// This is a simplified implementation
|
||||
// In practice, you would use a more precise logarithmic calculation
|
||||
|
||||
// Convert to big.Int for calculation
|
||||
sqrtPriceBig := sqrtPriceX96.ToBig()
|
||||
return SqrtPriceX96ToTick(sqrtPriceBig)
|
||||
}
|
||||
|
||||
// GetNextTick calculates the next initialized tick
|
||||
func GetNextTick(currentTick int, tickSpacing int) int {
|
||||
// Round down to nearest tick spacing
|
||||
tick := ((currentTick / tickSpacing) + 1) * tickSpacing
|
||||
return tick
|
||||
}
|
||||
|
||||
// GetPreviousTick calculates the previous initialized tick
|
||||
func GetPreviousTick(currentTick int, tickSpacing int) int {
|
||||
// Round down to nearest tick spacing
|
||||
tick := (currentTick / tickSpacing) * tickSpacing
|
||||
return tick
|
||||
}
|
||||
81
orig/pkg/uniswap/pricing_bench_test.go
Normal file
81
orig/pkg/uniswap/pricing_bench_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
func BenchmarkSqrtPriceX96ToPrice(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++ {
|
||||
_ = SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPriceToSqrtPriceX96(b *testing.B) {
|
||||
// Create a test price value
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = PriceToSqrtPriceX96(price)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTickToSqrtPriceX96(b *testing.B) {
|
||||
tick := 100000
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TickToSqrtPriceX96(tick)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSqrtPriceX96ToTick(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++ {
|
||||
_ = SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetTickAtSqrtPrice(b *testing.B) {
|
||||
// Create a test sqrtPriceX96 value using uint256
|
||||
bigInt := new(big.Int)
|
||||
bigInt.SetString("79228162514264337593543950336", 10)
|
||||
sqrtPriceX96, _ := uint256.FromBig(bigInt)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetTickAtSqrtPrice(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetNextTick(b *testing.B) {
|
||||
currentTick := 100000
|
||||
tickSpacing := MediumTickSpacing
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetNextTick(currentTick, tickSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetPreviousTick(b *testing.B) {
|
||||
currentTick := 100000
|
||||
tickSpacing := MediumTickSpacing
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetPreviousTick(currentTick, tickSpacing)
|
||||
}
|
||||
}
|
||||
78
orig/pkg/uniswap/pricing_test.go
Normal file
78
orig/pkg/uniswap/pricing_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSqrtPriceX96ToPrice(t *testing.T) {
|
||||
// Test case 1: Basic conversion
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
expected := 1.0
|
||||
actual := SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
actualFloat, _ := actual.Float64()
|
||||
|
||||
assert.InDelta(t, expected, actualFloat, 0.0001, "SqrtPriceX96ToPrice should convert correctly")
|
||||
|
||||
// Test case 2: Another value - we'll check the relative error instead
|
||||
sqrtPriceX96 = new(big.Int)
|
||||
sqrtPriceX96.SetString("158556325028528675187087900672", 10) // 2 * 2^96
|
||||
expected = 4.0 // (2)^2
|
||||
actual = SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
actualFloat, _ = actual.Float64()
|
||||
|
||||
// Check that it's close to 4.0 (allowing for floating point precision issues)
|
||||
assert.InDelta(t, expected, actualFloat, 0.01, "SqrtPriceX96ToPrice should convert correctly for 2*2^96")
|
||||
}
|
||||
|
||||
func TestPriceToSqrtPriceX96(t *testing.T) {
|
||||
// Test case 1: Basic conversion
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
actual := PriceToSqrtPriceX96(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(sqrtPriceX96, actual)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "PriceToSqrtPriceX96 should convert correctly")
|
||||
|
||||
// Test case 2: Another value
|
||||
price = new(big.Float).SetFloat64(4.0)
|
||||
sqrtPriceX96 = new(big.Int)
|
||||
sqrtPriceX96.SetString("158556325028528675187087900672", 10) // 2 * 2^96
|
||||
actual = PriceToSqrtPriceX96(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff = new(big.Int).Sub(sqrtPriceX96, actual)
|
||||
// Print actual and expected for debugging
|
||||
t.Logf("Expected: %s, Actual: %s, Diff: %s", sqrtPriceX96.String(), actual.String(), diff.String())
|
||||
// Create a large tolerance value
|
||||
tolerance := new(big.Int)
|
||||
tolerance.SetString("200000000000000000000000000", 10)
|
||||
// Increase the tolerance for the test to account for the large difference
|
||||
assert.True(t, diff.Cmp(tolerance) < 0, "PriceToSqrtPriceX96 should convert correctly for price=4.0")
|
||||
}
|
||||
|
||||
func TestTickToSqrtPriceX96(t *testing.T) {
|
||||
// Test case 1: Tick 0 should result in price 1.0
|
||||
expected := new(big.Int)
|
||||
expected.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
actual := TickToSqrtPriceX96(0)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(expected, actual)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "TickToSqrtPriceX96 should convert tick 0 correctly")
|
||||
}
|
||||
|
||||
func TestSqrtPriceX96ToTick(t *testing.T) {
|
||||
// Test case 1: sqrtPriceX96 for price 1.0 should result in tick 0
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
expected := 0
|
||||
actual := SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
|
||||
assert.Equal(t, expected, actual, "SqrtPriceX96ToTick should convert sqrtPriceX96 for price 1.0 correctly")
|
||||
}
|
||||
60
orig/pkg/uniswap/roundtrip_test.go
Normal file
60
orig/pkg/uniswap/roundtrip_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRoundTripConversions(t *testing.T) {
|
||||
// Test sqrtPriceX96 -> price -> sqrtPriceX96 round trip
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96 (price = 1.0)
|
||||
|
||||
price := SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
resultSqrtPriceX96 := PriceToSqrtPriceX96(price)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
diff := new(big.Int).Sub(sqrtPriceX96, resultSqrtPriceX96)
|
||||
assert.True(t, diff.Cmp(big.NewInt(1000000000000)) < 0, "Round trip conversion should be accurate")
|
||||
|
||||
// Test tick -> sqrtPriceX96 -> tick round trip
|
||||
tick := 100000
|
||||
sqrtPrice := TickToSqrtPriceX96(tick)
|
||||
resultTick := SqrtPriceX96ToTick(sqrtPrice)
|
||||
|
||||
// Allow for small differences due to floating point precision
|
||||
assert.InDelta(t, tick, resultTick, 1, "Round trip tick conversion should be accurate")
|
||||
}
|
||||
|
||||
func TestGetTickAtSqrtPriceWithUint256(t *testing.T) {
|
||||
// Test with a known value
|
||||
bigInt := new(big.Int)
|
||||
bigInt.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
sqrtPriceX96, _ := uint256.FromBig(bigInt)
|
||||
|
||||
tick := GetTickAtSqrtPrice(sqrtPriceX96)
|
||||
expectedTick := 0 // sqrtPriceX96 = 2^96 corresponds to price = 1.0, which is tick 0
|
||||
|
||||
assert.Equal(t, expectedTick, tick, "GetTickAtSqrtPrice should return correct tick")
|
||||
}
|
||||
|
||||
func TestTickSpacingCalculations(t *testing.T) {
|
||||
currentTick := 100000
|
||||
|
||||
// Test with medium tick spacing (60)
|
||||
nextTick := GetNextTick(currentTick, MediumTickSpacing)
|
||||
previousTick := GetPreviousTick(currentTick, MediumTickSpacing)
|
||||
|
||||
assert.Equal(t, 100020, nextTick, "GetNextTick should return correct next tick")
|
||||
assert.Equal(t, 99960, previousTick, "GetPreviousTick should return correct previous tick")
|
||||
|
||||
// Test with low tick spacing (10)
|
||||
nextTick = GetNextTick(currentTick, LowTickSpacing)
|
||||
previousTick = GetPreviousTick(currentTick, LowTickSpacing)
|
||||
|
||||
assert.Equal(t, 100010, nextTick, "GetNextTick should return correct next tick")
|
||||
assert.Equal(t, 100000, previousTick, "GetPreviousTick should return correct previous tick")
|
||||
}
|
||||
Reference in New Issue
Block a user