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 }