586 lines
22 KiB
Go
586 lines
22 KiB
Go
package swap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/events"
|
|
"github.com/fraktal/mev-beta/pkg/marketdata"
|
|
"github.com/fraktal/mev-beta/pkg/profitcalc"
|
|
"github.com/fraktal/mev-beta/pkg/scanner/market"
|
|
stypes "github.com/fraktal/mev-beta/pkg/types"
|
|
"github.com/fraktal/mev-beta/pkg/uniswap"
|
|
"github.com/holiman/uint256"
|
|
)
|
|
|
|
// SwapAnalyzer handles analysis of swap events for price movement opportunities
|
|
type SwapAnalyzer struct {
|
|
logger *logger.Logger
|
|
marketDataLogger *marketdata.MarketDataLogger
|
|
profitCalculator *profitcalc.ProfitCalculator
|
|
opportunityRanker *profitcalc.OpportunityRanker
|
|
}
|
|
|
|
// NewSwapAnalyzer creates a new swap analyzer
|
|
func NewSwapAnalyzer(
|
|
logger *logger.Logger,
|
|
marketDataLogger *marketdata.MarketDataLogger,
|
|
profitCalculator *profitcalc.ProfitCalculator,
|
|
opportunityRanker *profitcalc.OpportunityRanker,
|
|
) *SwapAnalyzer {
|
|
return &SwapAnalyzer{
|
|
logger: logger,
|
|
marketDataLogger: marketDataLogger,
|
|
profitCalculator: profitCalculator,
|
|
opportunityRanker: opportunityRanker,
|
|
}
|
|
}
|
|
|
|
// AnalyzeSwapEvent analyzes a swap event for arbitrage opportunities
|
|
func (s *SwapAnalyzer) AnalyzeSwapEvent(event events.Event, marketScanner *market.MarketScanner) {
|
|
s.logger.Debug(fmt.Sprintf("Analyzing swap event in pool %s", event.PoolAddress))
|
|
|
|
// Get comprehensive pool data to determine factory and fee
|
|
poolInfo, poolExists := s.marketDataLogger.GetPoolInfo(event.PoolAddress)
|
|
factory := common.Address{}
|
|
fee := uint32(3000) // Default 0.3%
|
|
if poolExists {
|
|
factory = poolInfo.Factory
|
|
fee = poolInfo.Fee
|
|
} else {
|
|
// Determine factory from known DEX protocols
|
|
factory = marketScanner.GetFactoryForProtocol(event.Protocol)
|
|
}
|
|
|
|
// Create comprehensive swap event data for market data logger
|
|
swapData := &marketdata.SwapEventData{
|
|
TxHash: event.TransactionHash,
|
|
BlockNumber: event.BlockNumber,
|
|
LogIndex: uint(0), // Default log index (would need to be extracted from receipt)
|
|
Timestamp: time.Now(),
|
|
PoolAddress: event.PoolAddress,
|
|
Factory: factory,
|
|
Protocol: event.Protocol,
|
|
Token0: event.Token0,
|
|
Token1: event.Token1,
|
|
Sender: common.Address{}, // Default sender (would need to be extracted from transaction)
|
|
Recipient: common.Address{}, // Default recipient (would need to be extracted from transaction)
|
|
SqrtPriceX96: event.SqrtPriceX96,
|
|
Liquidity: event.Liquidity,
|
|
Tick: int32(event.Tick),
|
|
}
|
|
|
|
// Extract swap amounts from event (handle signed amounts correctly)
|
|
if event.Amount0 != nil && event.Amount1 != nil {
|
|
amount0Float := new(big.Float).SetInt(event.Amount0)
|
|
amount1Float := new(big.Float).SetInt(event.Amount1)
|
|
|
|
// Determine input/output based on sign (negative means token was removed from pool = output)
|
|
if amount0Float.Sign() < 0 {
|
|
// Token0 out, Token1 in
|
|
swapData.Amount0Out = new(big.Int).Abs(event.Amount0)
|
|
swapData.Amount1In = event.Amount1
|
|
swapData.Amount0In = big.NewInt(0)
|
|
swapData.Amount1Out = big.NewInt(0)
|
|
} else if amount1Float.Sign() < 0 {
|
|
// Token0 in, Token1 out
|
|
swapData.Amount0In = event.Amount0
|
|
swapData.Amount1Out = new(big.Int).Abs(event.Amount1)
|
|
swapData.Amount0Out = big.NewInt(0)
|
|
swapData.Amount1In = big.NewInt(0)
|
|
} else {
|
|
// Both positive (shouldn't happen in normal swaps, but handle gracefully)
|
|
swapData.Amount0In = event.Amount0
|
|
swapData.Amount1In = event.Amount1
|
|
swapData.Amount0Out = big.NewInt(0)
|
|
swapData.Amount1Out = big.NewInt(0)
|
|
}
|
|
}
|
|
|
|
// Calculate USD values using profit calculator's price oracle
|
|
swapData.AmountInUSD, swapData.AmountOutUSD, swapData.FeeUSD = s.calculateSwapUSDValues(swapData, fee)
|
|
|
|
// Calculate price impact based on pool liquidity and swap amounts
|
|
swapData.PriceImpact = s.calculateSwapPriceImpact(event, swapData)
|
|
|
|
// Log comprehensive swap event to market data logger
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := s.marketDataLogger.LogSwapEvent(ctx, event, swapData); err != nil {
|
|
s.logger.Debug(fmt.Sprintf("Failed to log swap event to market data logger: %v", err))
|
|
}
|
|
|
|
// Log the swap event to database (legacy)
|
|
marketScanner.LogSwapEvent(event)
|
|
|
|
// Get pool data with caching
|
|
poolData, err := marketScanner.GetPoolData(event.PoolAddress.Hex())
|
|
if err != nil {
|
|
s.logger.Error(fmt.Sprintf("Error getting pool data for %s: %v", event.PoolAddress, err))
|
|
return
|
|
}
|
|
|
|
// Calculate price impact
|
|
priceMovement, err := s.calculatePriceMovement(event, poolData)
|
|
if err != nil {
|
|
s.logger.Error(fmt.Sprintf("Error calculating price movement for pool %s: %v", event.PoolAddress, err))
|
|
return
|
|
}
|
|
|
|
// Log opportunity with actual swap amounts from event (legacy)
|
|
s.logSwapOpportunity(event, poolData, priceMovement, marketScanner)
|
|
|
|
// Check if the movement is significant
|
|
if marketScanner.IsSignificantMovement(priceMovement, marketScanner.Config().MinProfitThreshold) {
|
|
s.logger.Info(fmt.Sprintf("Significant price movement detected in pool %s: %+v", event.PoolAddress, priceMovement))
|
|
|
|
// Look for arbitrage opportunities
|
|
opportunities := s.findArbitrageOpportunities(event, priceMovement, marketScanner)
|
|
if len(opportunities) > 0 {
|
|
s.logger.Info(fmt.Sprintf("Found %d arbitrage opportunities for pool %s", len(opportunities), event.PoolAddress))
|
|
for _, opp := range opportunities {
|
|
s.logger.Info(fmt.Sprintf("Arbitrage opportunity: %+v", opp))
|
|
// Execute the arbitrage opportunity
|
|
marketScanner.ExecuteArbitrageOpportunity(opp)
|
|
}
|
|
} else {
|
|
s.logger.Debug(fmt.Sprintf("Price movement in pool %s is not significant: %f", event.PoolAddress, priceMovement.PriceImpact))
|
|
}
|
|
}
|
|
}
|
|
|
|
// logSwapOpportunity logs swap opportunities using actual amounts from events
|
|
func (s *SwapAnalyzer) logSwapOpportunity(event events.Event, poolData *market.CachedData, priceMovement *market.PriceMovement, marketScanner *market.MarketScanner) {
|
|
// Convert amounts from big.Int to big.Float for profit calculation
|
|
amountInFloat := big.NewFloat(0)
|
|
amountOutFloat := big.NewFloat(0)
|
|
amountInDisplay := float64(0)
|
|
amountOutDisplay := float64(0)
|
|
|
|
// For swap events, Amount0 and Amount1 represent the actual swap amounts
|
|
// The sign indicates direction (positive = token added to pool, negative = token removed from pool)
|
|
if event.Amount0 != nil {
|
|
amount0Float := new(big.Float).SetInt(event.Amount0)
|
|
if event.Amount1 != nil {
|
|
amount1Float := new(big.Float).SetInt(event.Amount1)
|
|
|
|
// Determine input/output based on sign (negative means token was removed from pool = output)
|
|
if amount0Float.Sign() < 0 {
|
|
// Token0 out, Token1 in
|
|
amountOutFloat = new(big.Float).Abs(amount0Float)
|
|
amountInFloat = amount1Float
|
|
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
|
|
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
|
|
} else {
|
|
// Token0 in, Token1 out
|
|
amountInFloat = amount0Float
|
|
amountOutFloat = new(big.Float).Abs(amount1Float)
|
|
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
|
|
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Analyze arbitrage opportunity using the profit calculator
|
|
var estimatedProfitUSD float64 = 0.0
|
|
var profitData map[string]interface{}
|
|
|
|
if amountInFloat.Sign() > 0 && amountOutFloat.Sign() > 0 {
|
|
opportunity := s.profitCalculator.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
event.Token0,
|
|
event.Token1,
|
|
new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)), // Convert to ETH units
|
|
new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)), // Convert to ETH units
|
|
event.Protocol,
|
|
)
|
|
|
|
if opportunity != nil {
|
|
// Add opportunity to ranking system
|
|
rankedOpp := s.opportunityRanker.AddOpportunity(opportunity)
|
|
|
|
// Use the calculated profit for logging
|
|
if opportunity.NetProfit != nil {
|
|
estimatedProfitFloat, _ := opportunity.NetProfit.Float64()
|
|
estimatedProfitUSD = estimatedProfitFloat * 2000 // Assume 1 ETH = $2000 for USD conversion
|
|
}
|
|
|
|
// Add detailed profit analysis to additional data
|
|
profitData = map[string]interface{}{
|
|
"arbitrageId": opportunity.ID,
|
|
"isExecutable": opportunity.IsExecutable,
|
|
"rejectReason": opportunity.RejectReason,
|
|
"confidence": opportunity.Confidence,
|
|
"profitMargin": opportunity.ProfitMargin,
|
|
"netProfitETH": s.profitCalculator.FormatEther(opportunity.NetProfit),
|
|
"gasCostETH": s.profitCalculator.FormatEther(opportunity.GasCost),
|
|
"estimatedProfitETH": s.profitCalculator.FormatEther(opportunity.EstimatedProfit),
|
|
}
|
|
|
|
// Add ranking data if available
|
|
if rankedOpp != nil {
|
|
profitData["opportunityScore"] = rankedOpp.Score
|
|
profitData["opportunityRank"] = rankedOpp.Rank
|
|
profitData["competitionRisk"] = rankedOpp.CompetitionRisk
|
|
profitData["updateCount"] = rankedOpp.UpdateCount
|
|
}
|
|
}
|
|
} else if priceMovement != nil {
|
|
// Fallback to simple price impact calculation
|
|
estimatedProfitUSD = priceMovement.PriceImpact * 100
|
|
}
|
|
|
|
// Resolve token symbols
|
|
tokenIn := s.resolveTokenSymbol(event.Token0.Hex())
|
|
tokenOut := s.resolveTokenSymbol(event.Token1.Hex())
|
|
|
|
// Create additional data with profit analysis
|
|
additionalData := map[string]interface{}{
|
|
"poolAddress": event.PoolAddress.Hex(),
|
|
"protocol": event.Protocol,
|
|
"token0": event.Token0.Hex(),
|
|
"token1": event.Token1.Hex(),
|
|
"tokenIn": tokenIn,
|
|
"tokenOut": tokenOut,
|
|
"blockNumber": event.BlockNumber,
|
|
}
|
|
|
|
// Add price impact if available
|
|
if priceMovement != nil {
|
|
additionalData["priceImpact"] = priceMovement.PriceImpact
|
|
}
|
|
|
|
// Merge profit analysis data
|
|
if profitData != nil {
|
|
for k, v := range profitData {
|
|
additionalData[k] = v
|
|
}
|
|
}
|
|
|
|
// Log the opportunity using actual swap amounts and profit analysis
|
|
s.logger.Opportunity(event.TransactionHash.Hex(), "", event.PoolAddress.Hex(), "Swap", event.Protocol,
|
|
amountInDisplay, amountOutDisplay, 0.0, estimatedProfitUSD, additionalData)
|
|
}
|
|
|
|
// resolveTokenSymbol converts token address to human-readable symbol
|
|
func (s *SwapAnalyzer) resolveTokenSymbol(tokenAddress string) string {
|
|
// Convert to lowercase for consistent lookup
|
|
addr := strings.ToLower(tokenAddress)
|
|
|
|
// Known Arbitrum token mappings (same as in L2 parser)
|
|
tokenMap := map[string]string{
|
|
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1": "WETH",
|
|
"0xaf88d065e77c8cc2239327c5edb3a432268e5831": "USDC",
|
|
"0xff970a61a04b1ca14834a43f5de4533ebddb5cc8": "USDC.e",
|
|
"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9": "USDT",
|
|
"0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f": "WBTC",
|
|
"0x912ce59144191c1204e64559fe8253a0e49e6548": "ARB",
|
|
"0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a": "GMX",
|
|
"0xf97f4df75117a78c1a5a0dbb814af92458539fb4": "LINK",
|
|
"0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0": "UNI",
|
|
"0xba5ddd1f9d7f570dc94a51479a000e3bce967196": "AAVE",
|
|
"0x0de59c86c306b9fead9fb67e65551e2b6897c3f6": "KUMA",
|
|
"0x6efa9b8883dfb78fd75cd89d8474c44c3cbda469": "DIA",
|
|
"0x440017a1b021006d556d7fc06a54c32e42eb745b": "G@ARB",
|
|
"0x11cdb42b0eb46d95f990bedd4695a6e3fa034978": "CRV",
|
|
"0x040d1edc9569d4bab2d15287dc5a4f10f56a56b8": "BAL",
|
|
"0x354a6da3fcde098f8389cad84b0182725c6c91de": "COMP",
|
|
"0x2e9a6df78e42c50b0cefcf9000d0c3a4d34e1dd5": "MKR",
|
|
}
|
|
|
|
if symbol, exists := tokenMap[addr]; exists {
|
|
return symbol
|
|
}
|
|
|
|
// Return truncated address if not in mapping
|
|
if len(tokenAddress) > 10 {
|
|
return tokenAddress[:6] + "..." + tokenAddress[len(tokenAddress)-4:]
|
|
}
|
|
return tokenAddress
|
|
}
|
|
|
|
// calculatePriceMovement calculates the price movement from a swap event using cached mathematical functions
|
|
func (s *SwapAnalyzer) calculatePriceMovement(event events.Event, poolData *market.CachedData) (*market.PriceMovement, error) {
|
|
s.logger.Debug(fmt.Sprintf("Calculating price movement for pool %s", event.PoolAddress))
|
|
|
|
// Get current price from pool data using cached function
|
|
currentPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
|
|
if currentPrice == nil {
|
|
return nil, fmt.Errorf("failed to calculate current price from sqrtPriceX96")
|
|
}
|
|
|
|
// Calculate price impact based on swap amounts
|
|
var priceImpact float64
|
|
if event.Amount0.Sign() > 0 && event.Amount1.Sign() > 0 {
|
|
// Both amounts are positive, calculate the impact
|
|
amount0Float := new(big.Float).SetInt(event.Amount0)
|
|
amount1Float := new(big.Float).SetInt(event.Amount1)
|
|
|
|
// Price impact = |amount1 / amount0 - current_price| / current_price
|
|
swapPrice := new(big.Float).Quo(amount1Float, amount0Float)
|
|
priceDiff := new(big.Float).Sub(swapPrice, currentPrice)
|
|
priceDiff.Abs(priceDiff)
|
|
|
|
priceImpactFloat := new(big.Float).Quo(priceDiff, currentPrice)
|
|
priceImpact, _ = priceImpactFloat.Float64()
|
|
}
|
|
|
|
movement := &market.PriceMovement{
|
|
Token0: event.Token0.Hex(),
|
|
Token1: event.Token1.Hex(),
|
|
Pool: event.PoolAddress.Hex(),
|
|
Protocol: event.Protocol,
|
|
AmountIn: event.Amount0,
|
|
AmountOut: event.Amount1,
|
|
PriceBefore: currentPrice,
|
|
PriceAfter: currentPrice, // For now, assume same price (could be calculated based on swap)
|
|
PriceImpact: priceImpact,
|
|
TickBefore: poolData.Tick,
|
|
TickAfter: poolData.Tick, // For now, assume same tick
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
s.logger.Debug(fmt.Sprintf("Price movement calculated: impact=%.6f%%, amount_in=%s", priceImpact*100, event.Amount0.String()))
|
|
return movement, nil
|
|
}
|
|
|
|
// findArbitrageOpportunities looks for arbitrage opportunities based on price movements
|
|
func (s *SwapAnalyzer) findArbitrageOpportunities(event events.Event, movement *market.PriceMovement, marketScanner *market.MarketScanner) []stypes.ArbitrageOpportunity {
|
|
s.logger.Debug(fmt.Sprintf("Searching for arbitrage opportunities for pool %s", event.PoolAddress))
|
|
|
|
opportunities := make([]stypes.ArbitrageOpportunity, 0)
|
|
|
|
// Get related pools for the same token pair
|
|
relatedPools := marketScanner.FindRelatedPools(event.Token0, event.Token1)
|
|
|
|
// If we have related pools, compare prices
|
|
if len(relatedPools) > 0 {
|
|
// Get the current price in this pool
|
|
currentPrice := movement.PriceBefore
|
|
|
|
// Compare with prices in related pools
|
|
for _, pool := range relatedPools {
|
|
// Skip the same pool
|
|
if pool.Address == event.PoolAddress {
|
|
continue
|
|
}
|
|
|
|
// Get pool data
|
|
poolData, err := marketScanner.GetPoolData(pool.Address.Hex())
|
|
if err != nil {
|
|
s.logger.Error(fmt.Sprintf("Error getting pool data for related pool %s: %v", pool.Address.Hex(), err))
|
|
continue
|
|
}
|
|
|
|
// Check if poolData.SqrtPriceX96 is nil to prevent panic
|
|
if poolData.SqrtPriceX96 == nil {
|
|
s.logger.Error(fmt.Sprintf("Pool data for %s has nil SqrtPriceX96", pool.Address.Hex()))
|
|
continue
|
|
}
|
|
|
|
// Calculate price in the related pool using cached function
|
|
relatedPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
|
|
|
|
// Check if currentPrice or relatedPrice is nil to prevent panic
|
|
if currentPrice == nil || relatedPrice == nil {
|
|
s.logger.Error(fmt.Sprintf("Nil price detected for pool comparison"))
|
|
continue
|
|
}
|
|
|
|
// Calculate price difference
|
|
priceDiff := new(big.Float).Sub(currentPrice, relatedPrice)
|
|
priceDiffRatio := new(big.Float).Quo(priceDiff, relatedPrice)
|
|
|
|
// If there's a significant price difference, we might have an arbitrage opportunity
|
|
priceDiffFloat, _ := priceDiffRatio.Float64()
|
|
// Lower threshold for Arbitrum where spreads are smaller
|
|
arbitrageThreshold := 0.001 // 0.1% threshold instead of 0.5%
|
|
if priceDiffFloat > arbitrageThreshold {
|
|
// Estimate potential profit
|
|
estimatedProfit := marketScanner.EstimateProfit(event, pool, priceDiffFloat)
|
|
|
|
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
|
|
opp := stypes.ArbitrageOpportunity{
|
|
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
|
|
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
|
|
Profit: estimatedProfit,
|
|
GasEstimate: big.NewInt(300000), // Estimated gas cost
|
|
ROI: priceDiffFloat * 100, // Convert to percentage
|
|
Protocol: fmt.Sprintf("%s->%s", event.Protocol, pool.Protocol),
|
|
}
|
|
opportunities = append(opportunities, opp)
|
|
s.logger.Info(fmt.Sprintf("Found arbitrage opportunity: %+v", opp))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also look for triangular arbitrage opportunities
|
|
triangularOpps := marketScanner.FindTriangularArbitrageOpportunities(event)
|
|
opportunities = append(opportunities, triangularOpps...)
|
|
|
|
return opportunities
|
|
}
|
|
|
|
// calculateSwapUSDValues calculates USD values for swap amounts using the profit calculator's price oracle
|
|
func (s *SwapAnalyzer) calculateSwapUSDValues(swapData *marketdata.SwapEventData, fee uint32) (amountInUSD, amountOutUSD, feeUSD float64) {
|
|
if s.profitCalculator == nil {
|
|
return 0, 0, 0
|
|
}
|
|
|
|
// Get token prices in USD
|
|
token0Price := s.getTokenPriceUSD(swapData.Token0)
|
|
token1Price := s.getTokenPriceUSD(swapData.Token1)
|
|
|
|
// Calculate decimals for proper conversion
|
|
token0Decimals := s.getTokenDecimals(swapData.Token0)
|
|
token1Decimals := s.getTokenDecimals(swapData.Token1)
|
|
|
|
// Calculate amount in USD
|
|
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
|
|
amount0InFloat := s.bigIntToFloat(swapData.Amount0In, token0Decimals)
|
|
amountInUSD = amount0InFloat * token0Price
|
|
} else if swapData.Amount1In != nil && swapData.Amount1In.Sign() > 0 {
|
|
amount1InFloat := s.bigIntToFloat(swapData.Amount1In, token1Decimals)
|
|
amountInUSD = amount1InFloat * token1Price
|
|
}
|
|
|
|
// Calculate amount out USD
|
|
if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
|
|
amount0OutFloat := s.bigIntToFloat(swapData.Amount0Out, token0Decimals)
|
|
amountOutUSD = amount0OutFloat * token0Price
|
|
} else if swapData.Amount1Out != nil && swapData.Amount1Out.Sign() > 0 {
|
|
amount1OutFloat := s.bigIntToFloat(swapData.Amount1Out, token1Decimals)
|
|
amountOutUSD = amount1OutFloat * token1Price
|
|
}
|
|
|
|
// Calculate fee USD (fee tier as percentage of input amount)
|
|
feePercent := float64(fee) / 1000000.0 // Convert from basis points
|
|
feeUSD = amountInUSD * feePercent
|
|
|
|
return amountInUSD, amountOutUSD, feeUSD
|
|
}
|
|
|
|
// calculateSwapPriceImpact calculates the price impact of a swap based on pool liquidity and amounts
|
|
func (s *SwapAnalyzer) calculateSwapPriceImpact(event events.Event, swapData *marketdata.SwapEventData) float64 {
|
|
if event.SqrtPriceX96 == nil || event.Liquidity == nil {
|
|
return 0.0
|
|
}
|
|
|
|
// Get pre-swap price from sqrtPriceX96
|
|
prePrice := s.sqrtPriceX96ToPrice(event.SqrtPriceX96)
|
|
if prePrice == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// Calculate effective swap size in token0 terms
|
|
var swapSize *big.Int
|
|
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
|
|
swapSize = swapData.Amount0In
|
|
} else if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
|
|
swapSize = swapData.Amount0Out
|
|
} else {
|
|
return 0.0
|
|
}
|
|
|
|
// Calculate price impact as percentage of pool liquidity
|
|
liquidity := event.Liquidity.ToBig()
|
|
if liquidity.Sign() == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// Proper price impact calculation for AMMs: impact = swapSize / (liquidity + swapSize)
|
|
// This is more accurate than the quadratic approximation for real AMMs
|
|
swapSizeFloat := new(big.Float).SetInt(swapSize)
|
|
liquidityFloat := new(big.Float).SetInt(liquidity)
|
|
|
|
// Calculate the price impact ratio
|
|
priceImpactRatio := new(big.Float).Quo(swapSizeFloat, new(big.Float).Add(liquidityFloat, swapSizeFloat))
|
|
|
|
// Convert to percentage
|
|
priceImpactPercent, _ := priceImpactRatio.Float64()
|
|
return priceImpactPercent * 100.0
|
|
}
|
|
|
|
// getTokenPriceUSD gets the USD price of a token using various price sources
|
|
func (s *SwapAnalyzer) getTokenPriceUSD(tokenAddr common.Address) float64 {
|
|
// Known token prices (in a production system, this would query price oracles)
|
|
knownPrices := map[common.Address]float64{
|
|
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 2000.0, // WETH
|
|
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 1.0, // USDC
|
|
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 1.0, // USDC.e
|
|
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 1.0, // USDT
|
|
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 43000.0, // WBTC
|
|
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 0.75, // ARB
|
|
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 45.0, // GMX
|
|
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 12.0, // LINK
|
|
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 8.0, // UNI
|
|
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 85.0, // AAVE
|
|
}
|
|
|
|
if price, exists := knownPrices[tokenAddr]; exists {
|
|
return price
|
|
}
|
|
|
|
// For unknown tokens, return 0 (in production, would query price oracle or DEX)
|
|
return 0.0
|
|
}
|
|
|
|
// getTokenDecimals returns the decimal places for a token
|
|
func (s *SwapAnalyzer) getTokenDecimals(tokenAddr common.Address) uint8 {
|
|
// Known token decimals
|
|
knownDecimals := map[common.Address]uint8{
|
|
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 18, // WETH
|
|
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 6, // USDC
|
|
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 6, // USDC.e
|
|
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 6, // USDT
|
|
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 8, // WBTC
|
|
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 18, // ARB
|
|
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 18, // GMX
|
|
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 18, // LINK
|
|
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 18, // UNI
|
|
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 18, // AAVE
|
|
}
|
|
|
|
if decimals, exists := knownDecimals[tokenAddr]; exists {
|
|
return decimals
|
|
}
|
|
|
|
// Default to 18 for unknown tokens
|
|
return 18
|
|
}
|
|
|
|
// bigIntToFloat converts a big.Int amount to float64 accounting for token decimals
|
|
func (s *SwapAnalyzer) bigIntToFloat(amount *big.Int, decimals uint8) float64 {
|
|
if amount == nil {
|
|
return 0.0
|
|
}
|
|
|
|
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)
|
|
amountFloat := new(big.Float).SetInt(amount)
|
|
divisorFloat := new(big.Float).SetInt(divisor)
|
|
|
|
result := new(big.Float).Quo(amountFloat, divisorFloat)
|
|
resultFloat, _ := result.Float64()
|
|
|
|
return resultFloat
|
|
}
|
|
|
|
// sqrtPriceX96ToPrice converts sqrtPriceX96 to a regular price using cached mathematical functions
|
|
func (s *SwapAnalyzer) sqrtPriceX96ToPrice(sqrtPriceX96 *uint256.Int) float64 {
|
|
if sqrtPriceX96 == nil {
|
|
return 0.0
|
|
}
|
|
|
|
// Use cached function for optimized calculation
|
|
price := uniswap.SqrtPriceX96ToPriceCached(sqrtPriceX96.ToBig())
|
|
|
|
priceFloat, _ := price.Float64()
|
|
return priceFloat
|
|
}
|