package exchanges import ( "context" "fmt" "math/big" "sort" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/math" "github.com/fraktal/mev-beta/pkg/types" ) // ArbitrageOpportunity represents a cross-exchange arbitrage opportunity type ArbitrageOpportunity struct { TokenIn common.Address TokenOut common.Address BuyExchange math.ExchangeType SellExchange math.ExchangeType BuyAmount *big.Int SellAmount *big.Int BuyPrice *big.Float SellPrice *big.Float Spread *big.Float // Price difference (SellPrice - BuyPrice) SpreadPercent *big.Float // Spread as percentage Profit *big.Int // Estimated profit in wei GasCost *big.Int // Estimated gas cost NetProfit *big.Int // Net profit after gas ROI float64 // Return on investment percentage Confidence float64 // Confidence score (0.0 to 1.0) MaxSlippage float64 // Maximum acceptable slippage ExecutionTime time.Duration // Estimated execution time Path []string // Execution path description Timestamp time.Time // When the opportunity was discovered } // CrossExchangeArbitrageFinder finds arbitrage opportunities between exchanges type CrossExchangeArbitrageFinder struct { client *ethclient.Client logger *logger.Logger registry *ExchangeRegistry engine *math.ExchangePricingEngine minSpread *big.Float // Minimum required spread percentage minProfit *big.Int // Minimum required profit in wei maxSlippage float64 // Maximum acceptable slippage } // NewCrossExchangeArbitrageFinder creates a new cross-exchange arbitrage finder func NewCrossExchangeArbitrageFinder( client *ethclient.Client, logger *logger.Logger, registry *ExchangeRegistry, engine *math.ExchangePricingEngine, ) *CrossExchangeArbitrageFinder { return &CrossExchangeArbitrageFinder{ client: client, logger: logger, registry: registry, engine: engine, minSpread: big.NewFloat(0.003), // 0.3% minimum spread after fees minProfit: big.NewInt(10000000000000000), // 0.01 ETH minimum profit maxSlippage: 0.01, // 1% maximum slippage } } // FindArbitrageOpportunities finds cross-exchange arbitrage opportunities for a token pair func (a *CrossExchangeArbitrageFinder) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*ArbitrageOpportunity, error) { // Get all exchanges that support this token pair exchanges := a.registry.GetExchangesForPair(tokenIn, tokenOut) if len(exchanges) < 2 { return nil, nil // Need at least 2 exchanges to find arbitrage } var opportunities []*ArbitrageOpportunity // Compare all exchange pairs for arbitrage opportunities for i, buyExchangeConfig := range exchanges { for j, sellExchangeConfig := range exchanges { if i == j { continue // Skip same exchange } // Check if we can buy on buyExchange and sell on sellExchange opportunity, err := a.findDirectArbitrage(ctx, tokenIn, tokenOut, buyExchangeConfig.Type, sellExchangeConfig.Type) if err != nil { continue } if opportunity != nil { // Validate the opportunity if a.isValidOpportunity(opportunity) { opportunities = append(opportunities, opportunity) } } } } // Sort opportunities by net profit descending sort.Slice(opportunities, func(i, j int) bool { return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0 }) return opportunities, nil } // findDirectArbitrage finds a direct arbitrage opportunity between two exchanges func (a *CrossExchangeArbitrageFinder) findDirectArbitrage( ctx context.Context, tokenIn, tokenOut common.Address, buyExchangeType, sellExchangeType math.ExchangeType, ) (*ArbitrageOpportunity, error) { // Get swap routers for both exchanges buyRouter := a.registry.GetSwapRouter(buyExchangeType) sellRouter := a.registry.GetSwapRouter(sellExchangeType) if buyRouter == nil || sellRouter == nil { return nil, fmt.Errorf("missing swap router for one of the exchanges") } // Use a standard amount for comparison (1 ETH equivalent) standardAmount := big.NewInt(1000000000000000000) // 1 ETH // Calculate how much we'd get if we buy on buyExchange amountAfterBuy, err := buyRouter.CalculateSwap(tokenIn, tokenOut, standardAmount) if err != nil { return nil, fmt.Errorf("error calculating buy swap: %w", err) } // Calculate how much we'd get if we sell the result on sellExchange amountAfterSell, err := sellRouter.CalculateSwap(tokenOut, tokenIn, amountAfterBuy) if err != nil { return nil, fmt.Errorf("error calculating sell swap: %w", err) } // Calculate the spread and profit spread := new(big.Float).Sub( new(big.Float).SetInt(amountAfterSell), new(big.Float).SetInt(standardAmount), ) // Calculate spread percentage spreadPercent := new(big.Float).Quo( spread, new(big.Float).SetInt(standardAmount), ) // If the spread is positive, we have an opportunity if spread.Sign() > 0 { // Calculate profit in terms of the original token profit := new(big.Int).Sub(amountAfterSell, standardAmount) // Calculate estimated gas cost (this would be more sophisticated in production) gasCost := big.NewInt(500000000000000) // 0.0005 ETH in wei as estimate // Calculate net profit netProfit := new(big.Int).Sub(profit, gasCost) // Calculate ROI roi := new(big.Float).Quo( new(big.Float).SetInt(netProfit), new(big.Float).SetInt(standardAmount), ) roiFloat, _ := roi.Float64() // Get exchange config names for the path buyConfig := a.registry.GetExchangeByType(buyExchangeType) sellConfig := a.registry.GetExchangeByType(sellExchangeType) buyExchangeName := "Unknown" sellExchangeName := "Unknown" if buyConfig != nil { buyExchangeName = buyConfig.Name } if sellConfig != nil { sellExchangeName = sellConfig.Name } // Create opportunity opportunity := &ArbitrageOpportunity{ TokenIn: tokenIn, TokenOut: tokenOut, BuyExchange: buyExchangeType, SellExchange: sellExchangeType, BuyAmount: standardAmount, SellAmount: amountAfterSell, BuyPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterBuy), new(big.Float).SetInt(standardAmount)), SellPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterSell), new(big.Float).SetInt(amountAfterBuy)), Spread: spread, SpreadPercent: spreadPercent, Profit: profit, GasCost: gasCost, NetProfit: netProfit, ROI: roiFloat * 100, // Convert to percentage Confidence: 0.9, // High confidence for basic calculation MaxSlippage: a.maxSlippage, Path: []string{buyExchangeName, sellExchangeName}, Timestamp: time.Now(), } return opportunity, nil } return nil, nil // No arbitrage opportunity } // isValidOpportunity checks if an arbitrage opportunity meets our criteria func (a *CrossExchangeArbitrageFinder) isValidOpportunity(opportunity *ArbitrageOpportunity) bool { // Check if spread is above minimum spreadPercentFloat, _ := opportunity.SpreadPercent.Float64() minSpreadFloat, _ := a.minSpread.Float64() if spreadPercentFloat < minSpreadFloat { return false } // Check if net profit is above minimum if opportunity.NetProfit.Cmp(a.minProfit) < 0 { return false } // Check if ROI is reasonable if opportunity.ROI < 0.1 { // 0.1% minimum ROI return false } // Additional validations could go here return true } // FindTriangleArbitrage finds triangular arbitrage opportunities across exchanges func (a *CrossExchangeArbitrageFinder) FindTriangleArbitrage(ctx context.Context, tokens []common.Address) ([]*ArbitrageOpportunity, error) { if len(tokens) < 3 { return nil, fmt.Errorf("need at least 3 tokens for triangular arbitrage") } var opportunities []*ArbitrageOpportunity // For each combination of 3 tokens for i := 0; i < len(tokens)-2; i++ { for j := i + 1; j < len(tokens)-1; j++ { for k := j + 1; k < len(tokens); k++ { // Try A -> B -> C -> A cycle cycleOpportunities, err := a.findTriangleCycle(ctx, tokens[i], tokens[j], tokens[k]) if err == nil && cycleOpportunities != nil { opportunities = append(opportunities, cycleOpportunities...) } // Try A -> C -> B -> A cycle (reverse) cycleOpportunities, err = a.findTriangleCycle(ctx, tokens[i], tokens[k], tokens[j]) if err == nil && cycleOpportunities != nil { opportunities = append(opportunities, cycleOpportunities...) } } } } // Sort by net profit descending sort.Slice(opportunities, func(i, j int) bool { return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0 }) return opportunities, nil } // findTriangleCycle finds opportunities for a specific 3-token cycle func (a *CrossExchangeArbitrageFinder) findTriangleCycle( ctx context.Context, tokenA, tokenB, tokenC common.Address, ) ([]*ArbitrageOpportunity, error) { // Find if there are exchanges supporting A->B, B->C, C->A // This would be complex to implement completely, so here's a simplified approach var opportunities []*ArbitrageOpportunity // Get exchanges for each pair exchangesAB := a.registry.GetExchangesForPair(tokenA, tokenB) exchangesBC := a.registry.GetExchangesForPair(tokenB, tokenC) exchangesCA := a.registry.GetExchangesForPair(tokenC, tokenA) if len(exchangesAB) == 0 || len(exchangesBC) == 0 || len(exchangesCA) == 0 { return nil, nil // Can't form a cycle without all pairs } // Use first available exchange for each leg of the cycle // In a real implementation, we'd try all combinations if len(exchangesAB) > 0 && len(exchangesBC) > 0 && len(exchangesCA) > 0 { // For this simplified version, we'll just use the first available exchanges // More complex implementations would iterate through all combinations // Start with 1 of tokenA startAmount := big.NewInt(1000000000000000000) // 1 tokenA equivalent // Get the three exchanges we'll use exchangeAB := exchangesAB[0].Type exchangeBC := exchangesBC[0].Type exchangeCA := exchangesCA[0].Type // Get swap routers routerAB := a.registry.GetSwapRouter(exchangeAB) routerBC := a.registry.GetSwapRouter(exchangeBC) routerCA := a.registry.GetSwapRouter(exchangeCA) if routerAB == nil || routerBC == nil || routerCA == nil { return nil, nil } // Execute the cycle: A -> B -> C -> A amountB, err := routerAB.CalculateSwap(tokenA, tokenB, startAmount) if err != nil { return nil, nil } amountC, err := routerBC.CalculateSwap(tokenB, tokenC, amountB) if err != nil { return nil, nil } finalAmount, err := routerCA.CalculateSwap(tokenC, tokenA, amountC) if err != nil { return nil, nil } // Calculate profit profit := new(big.Int).Sub(finalAmount, startAmount) // If we made a profit, we have an opportunity if profit.Sign() > 0 { // Calculate spread percentage spreadPercent := new(big.Float).Quo( new(big.Float).SetInt(profit), new(big.Float).SetInt(startAmount), ) // Calculate ROI roi := new(big.Float).Quo( new(big.Float).SetInt(profit), new(big.Float).SetInt(startAmount), ) roiFloat, _ := roi.Float64() // Calculate estimated gas cost for the complex multi-swap gasCost := big.NewInt(1500000000000000) // 0.0015 ETH in wei for 3 swaps // Calculate net profit netProfit := new(big.Int).Sub(profit, gasCost) // Create opportunity opportunity := &ArbitrageOpportunity{ TokenIn: tokenA, TokenOut: tokenA, // We start and end with the same token BuyExchange: exchangeAB, SellExchange: exchangeCA, // This is somewhat of a convention for this type of arbitrage BuyAmount: startAmount, SellAmount: finalAmount, Spread: new(big.Float).SetInt(profit), SpreadPercent: spreadPercent, Profit: profit, GasCost: gasCost, NetProfit: netProfit, ROI: roiFloat * 100, Confidence: 0.7, // Lower confidence for complex triangle arbitrage MaxSlippage: a.maxSlippage, Path: []string{exchangesAB[0].Name, exchangesBC[0].Name, exchangesCA[0].Name}, Timestamp: time.Now(), } opportunities = append(opportunities, opportunity) } } return opportunities, nil } // ValidateOpportunity checks if an arbitrage opportunity is still valid func (a *CrossExchangeArbitrageFinder) ValidateOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) (bool, error) { // Check if the opportunity is still fresh (not too old) if time.Since(opportunity.Timestamp) > 100*time.Millisecond { // 100ms expiration return false, nil } // Recalculate the opportunity to see if it's still profitable recalculated, err := a.findDirectArbitrage(ctx, opportunity.TokenIn, opportunity.TokenOut, opportunity.BuyExchange, opportunity.SellExchange) if err != nil || recalculated == nil { return false, err } // Check if the recalculated profit is still above our minimum if recalculated.NetProfit.Cmp(a.minProfit) < 0 { return false, nil } return true, nil } // EstimateExecutionGas estimates the gas cost for executing an arbitrage opportunity func (a *CrossExchangeArbitrageFinder) EstimateExecutionGas(opportunity *ArbitrageOpportunity) (*big.Int, error) { // In a real implementation, this would estimate gas based on complexity // For this implementation, we'll use a basic estimation based on the type of arbitrage baseGas := big.NewInt(100000) // Base gas for a single swap // For each exchange in the path, add more gas gasPerSwap := big.NewInt(100000) totalGas := new(big.Int).Add(baseGas, new(big.Int).Mul(gasPerSwap, big.NewInt(int64(len(opportunity.Path))))) return totalGas, nil } // ConvertToCanonicalOpportunity converts our internal opportunity to the canonical type func (a *CrossExchangeArbitrageFinder) ConvertToCanonicalOpportunity(opportunity *ArbitrageOpportunity) *types.ArbitrageOpportunity { // Convert our internal opportunity struct to the canonical ArbitrageOpportunity type roiFloat, _ := opportunity.ROI, opportunity.ROI gasEstimate := big.NewInt(200000) // Default gas estimate return &types.ArbitrageOpportunity{ Path: opportunity.Path, Pools: []string{}, // Would be populated with actual pool addresses AmountIn: opportunity.BuyAmount, Profit: opportunity.Profit, NetProfit: opportunity.NetProfit, GasEstimate: gasEstimate, ROI: roiFloat, Protocol: "cross-exchange", ExecutionTime: int64(opportunity.ExecutionTime.Milliseconds()), Confidence: opportunity.Confidence, PriceImpact: 0.005, // 0.5% estimated price impact MaxSlippage: opportunity.MaxSlippage, TokenIn: opportunity.TokenIn, TokenOut: opportunity.TokenOut, Timestamp: opportunity.Timestamp.Unix(), Risk: 0.1, // Low risk for simple arbitrage } } // FindHighPriorityArbitrage looks for opportunities with high probability and profit func (a *CrossExchangeArbitrageFinder) FindHighPriorityArbitrage(ctx context.Context) ([]*ArbitrageOpportunity, error) { // Get high priority tokens from the registry highPriorityTokens := a.registry.GetHighPriorityTokens(10) // Top 10 tokens var allOpportunities []*ArbitrageOpportunity // Create token address array from high priority tokens var tokenAddresses []common.Address for _, token := range highPriorityTokens { tokenAddresses = append(tokenAddresses, common.HexToAddress(token.Address)) } // Find arbitrage opportunities between high priority tokens for i, tokenA := range tokenAddresses { for j, tokenB := range tokenAddresses { if i < j { // Only check each pair once opportunities, err := a.FindArbitrageOpportunities(ctx, tokenA, tokenB) if err != nil { continue } allOpportunities = append(allOpportunities, opportunities...) } } } // Sort by profitability and confidence sort.Slice(allOpportunities, func(i, j int) bool { // Sort by net profit first, then by confidence if allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) == 0 { return allOpportunities[i].Confidence > allOpportunities[j].Confidence } return allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) > 0 }) // Return only the top opportunities (top 20) if len(allOpportunities) > 20 { allOpportunities = allOpportunities[:20] } return allOpportunities, nil }