package discovery import ( "context" "fmt" "math" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/internal/logger" exchangeMath "github.com/fraktal/mev-beta/pkg/math" ) // ArbitrageCalculator handles arbitrage opportunity calculations type ArbitrageCalculator struct { logger *logger.Logger config *ArbitrageConfig mathCalc *exchangeMath.MathCalculator } // NewArbitrageCalculator creates a new arbitrage calculator func NewArbitrageCalculator(logger *logger.Logger, config *ArbitrageConfig, mathCalc *exchangeMath.MathCalculator) *ArbitrageCalculator { return &ArbitrageCalculator{ logger: logger, config: config, mathCalc: mathCalc, } } // findArbitrageOpportunities finds arbitrage opportunities across all pools func (ac *ArbitrageCalculator) findArbitrageOpportunities(ctx context.Context, gasPrice *big.Int, pools map[common.Address]*PoolInfoDetailed, logger *logger.Logger, config *ArbitrageConfig, mathCalc *exchangeMath.MathCalculator) []*ArbitrageOpportunityDetailed { opportunities := make([]*ArbitrageOpportunityDetailed, 0) // Group pools by token pairs tokenPairPools := ac.groupPoolsByTokenPairs(pools) // Check each token pair for arbitrage for tokenPair, pools := range tokenPairPools { if len(pools) < 2 { continue // Need at least 2 pools for arbitrage } // Check all pool combinations for i := 0; i < len(pools); i++ { for j := i + 1; j < len(pools); j++ { poolA := pools[i] poolB := pools[j] // Skip if same factory type (no arbitrage opportunity) if poolA.FactoryType == poolB.FactoryType { continue } // Calculate arbitrage arb := ac.calculateArbitrage(poolA, poolB, gasPrice, tokenPair, mathCalc) if arb != nil && arb.NetProfit.Sign() > 0 { opportunities = append(opportunities, arb) } } } } // Sort by net profit (highest first) for i := 0; i < len(opportunities)-1; i++ { for j := i + 1; j < len(opportunities); j++ { if opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) < 0 { opportunities[i], opportunities[j] = opportunities[j], opportunities[i] } } } return opportunities } // calculateArbitrage calculates arbitrage between two pools func (ac *ArbitrageCalculator) calculateArbitrage(poolA, poolB *PoolInfoDetailed, gasPrice *big.Int, tokenPair string, mathCalc *exchangeMath.MathCalculator) *ArbitrageOpportunityDetailed { // Skip pools with zero or nil reserves (uninitialized pools) if poolA.Reserve0 == nil || poolA.Reserve1 == nil || poolB.Reserve0 == nil || poolB.Reserve1 == nil || poolA.Reserve0.Sign() <= 0 || poolA.Reserve1.Sign() <= 0 || poolB.Reserve0.Sign() <= 0 || poolB.Reserve1.Sign() <= 0 { return nil } // Get math calculators for each pool type mathA := mathCalc.GetMathForExchange(poolA.FactoryType) mathB := mathCalc.GetMathForExchange(poolB.FactoryType) // Get spot prices priceA, err := mathA.GetSpotPrice(poolA.Reserve0, poolA.Reserve1) if err != nil { return nil } // Check if priceA is valid (not zero, infinity, or NaN) priceAFloat, _ := priceA.Float64() if priceA.Cmp(big.NewFloat(0)) == 0 || math.IsInf(priceAFloat, 0) || math.IsNaN(priceAFloat) { return nil // Invalid priceA value } priceB, err := mathB.GetSpotPrice(poolB.Reserve0, poolB.Reserve1) if err != nil { return nil } // Check if priceB is valid (not zero, infinity, or NaN) priceBFloat, _ := priceB.Float64() if priceB.Cmp(big.NewFloat(0)) == 0 || math.IsInf(priceBFloat, 0) || math.IsNaN(priceBFloat) { return nil // Invalid priceB value } // Calculate price difference priceDiff := new(big.Float).Sub(priceA, priceB) // Additional check if priceA is infinity, NaN, or zero before division priceAFloatCheck, _ := priceA.Float64() priceBFloatCheck, _ := priceB.Float64() if math.IsNaN(priceAFloatCheck) || math.IsNaN(priceBFloatCheck) || math.IsInf(priceAFloatCheck, 0) || math.IsInf(priceBFloatCheck, 0) || priceA.Cmp(big.NewFloat(0)) == 0 { return nil // Invalid price values } // Perform the division priceDiff.Quo(priceDiff, priceA) // Check if the result of the division is valid (not NaN or Infinity) priceDiffFloat, accuracy := priceDiff.Float64() if math.IsNaN(priceDiffFloat) || math.IsInf(priceDiffFloat, 0) || accuracy != big.Exact { return nil // Invalid price difference value } // Check if price difference exceeds minimum threshold minThreshold, exists := ac.config.ProfitMargins["arbitrage"] if !exists { minThreshold = 0.001 // Default to 0.1% if not specified } if abs(priceDiffFloat) < minThreshold { return nil } // Calculate optimal arbitrage amount (simplified) amountIn := big.NewInt(100000000000000000) // 0.1 ETH test amount // Calculate amounts amountOutA, _ := mathA.CalculateAmountOut(amountIn, poolA.Reserve0, poolA.Reserve1, poolA.Fee) if amountOutA == nil { return nil } amountOutB, _ := mathB.CalculateAmountIn(amountOutA, poolB.Reserve1, poolB.Reserve0, poolB.Fee) if amountOutB == nil { return nil } // Calculate profit profit := new(big.Int).Sub(amountOutB, amountIn) if profit.Sign() <= 0 { return nil } // Calculate gas cost gasCost := new(big.Int).Mul(gasPrice, big.NewInt(300000)) // ~300k gas // Net profit netProfit := new(big.Int).Sub(profit, gasCost) if netProfit.Sign() <= 0 { return nil } // Convert to USD (simplified - assume ETH price) profitUSD := float64(netProfit.Uint64()) / 1e18 * 2000 // Assume $2000 ETH if profitUSD < ac.config.MinProfitUSD { return nil } // Calculate price impacts with validation priceImpactA, errA := mathA.CalculatePriceImpact(amountIn, poolA.Reserve0, poolA.Reserve1) priceImpactB, errB := mathB.CalculatePriceImpact(amountOutA, poolB.Reserve1, poolB.Reserve0) // Validate price impacts to prevent NaN or Infinity if errA != nil || errB != nil { return nil } // Check if price impacts are valid numbers if math.IsNaN(priceImpactA) || math.IsInf(priceImpactA, 0) || math.IsNaN(priceImpactB) || math.IsInf(priceImpactB, 0) { return nil } return &ArbitrageOpportunityDetailed{ ID: fmt.Sprintf("arb_%d_%s", time.Now().Unix(), tokenPair), Type: "arbitrage", TokenIn: poolA.Token0, TokenOut: poolA.Token1, AmountIn: amountIn, ExpectedAmountOut: amountOutA, ActualAmountOut: amountOutB, Profit: profit, ProfitUSD: profitUSD, ProfitMargin: priceDiffFloat, GasCost: gasCost, NetProfit: netProfit, ExchangeA: poolA.FactoryType, ExchangeB: poolB.FactoryType, PoolA: poolA.Address, PoolB: poolB.Address, PriceImpactA: priceImpactA, PriceImpactB: priceImpactB, Confidence: 0.8, RiskScore: 0.3, ExecutionTime: time.Duration(15) * time.Second, Timestamp: time.Now(), } } // Helper methods func abs(x float64) float64 { if x < 0 { return -x } return x } // groupPoolsByTokenPairs groups pools by token pairs func (ac *ArbitrageCalculator) groupPoolsByTokenPairs(pools map[common.Address]*PoolInfoDetailed) map[string][]*PoolInfoDetailed { groups := make(map[string][]*PoolInfoDetailed) for _, pool := range pools { if !pool.Active { continue } // Create token pair key (sorted) var pairKey string if pool.Token0.Big().Cmp(pool.Token1.Big()) < 0 { pairKey = fmt.Sprintf("%s-%s", pool.Token0.Hex(), pool.Token1.Hex()) } else { pairKey = fmt.Sprintf("%s-%s", pool.Token1.Hex(), pool.Token0.Hex()) } groups[pairKey] = append(groups[pairKey], pool) } return groups }