package math import ( "context" "fmt" "math" "math/big" "sort" "time" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/pkg/security" "github.com/fraktal/mev-beta/pkg/types" ) // Use the canonical ArbitrageOpportunity from types package // Extended fields for advanced calculations can be added as needed // ExchangeStep represents one step in the arbitrage execution type ExchangeStep struct { Exchange ExchangeType Pool *PoolData TokenIn TokenInfo TokenOut TokenInfo AmountIn *UniversalDecimal AmountOut *UniversalDecimal PriceImpact *UniversalDecimal EstimatedGas uint64 } // RiskAssessment evaluates the risk level of an arbitrage opportunity type RiskAssessment struct { Overall RiskLevel Liquidity RiskLevel PriceImpact RiskLevel Competition RiskLevel Slippage RiskLevel GasPrice RiskLevel Warnings []string OverallRisk float64 // Numeric representation of overall risk (0.0 to 1.0) } // RiskLevel represents different risk categories type RiskLevel string const ( RiskLow RiskLevel = "low" RiskMedium RiskLevel = "medium" RiskHigh RiskLevel = "high" RiskCritical RiskLevel = "critical" ) // ArbitrageCalculator performs precise arbitrage calculations type ArbitrageCalculator struct { pricingEngine *ExchangePricingEngine decimalConverter *DecimalConverter gasEstimator GasEstimator // Configuration minProfitThreshold *UniversalDecimal maxPriceImpact *UniversalDecimal maxSlippage *UniversalDecimal maxGasPriceGwei *UniversalDecimal } // GasEstimator interface for gas cost calculations type GasEstimator interface { EstimateSwapGas(exchange ExchangeType, poolData *PoolData) (uint64, error) EstimateFlashSwapGas(route []*PoolData) (uint64, error) GetCurrentGasPrice() (*UniversalDecimal, error) } // NewArbitrageCalculator creates a new arbitrage calculator func NewArbitrageCalculator(gasEstimator GasEstimator) *ArbitrageCalculator { dc := NewDecimalConverter() // Default configuration minProfit, _ := dc.FromString("0.01", 18, "ETH") // 0.01 ETH minimum maxImpact, _ := dc.FromString("0.02", 4, "PERCENT") // 2% max price impact maxSlip, _ := dc.FromString("0.01", 4, "PERCENT") // 1% max slippage maxGas, _ := dc.FromString("50", 9, "GWEI") // 50 gwei max gas return &ArbitrageCalculator{ pricingEngine: NewExchangePricingEngine(), decimalConverter: dc, gasEstimator: gasEstimator, minProfitThreshold: minProfit, maxPriceImpact: maxImpact, maxSlippage: maxSlip, maxGasPriceGwei: maxGas, } } func toDecimalAmount(ud *UniversalDecimal) types.DecimalAmount { if ud == nil { return types.DecimalAmount{} } return types.DecimalAmount{ Value: ud.Value.String(), Decimals: ud.Decimals, Symbol: ud.Symbol, } } // CalculateArbitrageOpportunity performs comprehensive arbitrage analysis func (calc *ArbitrageCalculator) CalculateArbitrageOpportunity( path []*PoolData, inputAmount *UniversalDecimal, inputToken TokenInfo, outputToken TokenInfo, ) (*types.ArbitrageOpportunity, error) { if len(path) == 0 { return nil, fmt.Errorf("empty arbitrage path") } // Step 1: Calculate execution route with amounts route, err := calc.calculateExecutionRoute(path, inputAmount, inputToken) if err != nil { return nil, fmt.Errorf("error calculating execution route: %w", err) } // Step 2: Get final output amount finalOutput := route[len(route)-1].AmountOut // Step 3: Calculate gas costs totalGasCost, err := calc.calculateTotalGasCost(route) if err != nil { return nil, fmt.Errorf("error calculating gas cost: %w", err) } // Step 4: Calculate profits (convert to common denomination - ETH) grossProfit, netProfit, profitPercentage, err := calc.calculateProfits( inputAmount, finalOutput, totalGasCost, inputToken, outputToken) if err != nil { return nil, fmt.Errorf("error calculating profits: %w", err) } // Step 5: Calculate total price impact totalPriceImpact, err := calc.calculateTotalPriceImpact(route) if err != nil { return nil, fmt.Errorf("error calculating price impact: %w", err) } // Step 6: Calculate minimum output with slippage (we don't use this in the final result) _, err = calc.calculateMinimumOutput(finalOutput) if err != nil { return nil, fmt.Errorf("error calculating minimum output: %w", err) } // Step 7: Assess risks riskAssessment := calc.assessRisks(route, totalPriceImpact, netProfit) // Step 8: Calculate confidence and execution time confidence := calc.calculateConfidence(riskAssessment, netProfit, totalPriceImpact) executionTime := calc.estimateExecutionTime(route) // Convert path to string array pathStrings := make([]string, len(path)) for i, pool := range path { pathStrings[i] = pool.Address // Address is already a string } // Convert pools to string array poolStrings := make([]string, len(path)) for i, pool := range path { poolStrings[i] = pool.Address // Address is already a string } opportunity := &types.ArbitrageOpportunity{ Path: pathStrings, Pools: poolStrings, AmountIn: inputAmount.Value, RequiredAmount: inputAmount.Value, Profit: grossProfit.Value, NetProfit: netProfit.Value, EstimatedProfit: grossProfit.Value, GasEstimate: totalGasCost.Value, ROI: func() float64 { // Convert percentage from 4-decimal format to actual percentage f, _ := profitPercentage.Value.Float64() return f / 10000.0 // Convert from 4-decimal format to actual percentage }(), Protocol: "multi", // Default protocol for multi-step arbitrage ExecutionTime: executionTime, Confidence: confidence, PriceImpact: func() float64 { // Convert percentage from 4-decimal format to actual percentage f, _ := totalPriceImpact.Value.Float64() return f / 10000.0 // Convert from 4-decimal format to actual percentage }(), MaxSlippage: 0.01, // Default 1% max slippage TokenIn: common.HexToAddress(inputToken.Address), TokenOut: common.HexToAddress(outputToken.Address), Timestamp: time.Now().Unix(), DetectedAt: time.Now(), ExpiresAt: time.Now().Add(5 * time.Minute), Risk: riskAssessment.OverallRisk, } opportunity.Quantities = &types.OpportunityQuantities{ AmountIn: toDecimalAmount(inputAmount), AmountOut: toDecimalAmount(finalOutput), GrossProfit: toDecimalAmount(grossProfit), NetProfit: toDecimalAmount(netProfit), GasCost: toDecimalAmount(totalGasCost), ProfitPercent: toDecimalAmount(profitPercentage), PriceImpact: toDecimalAmount(totalPriceImpact), } return opportunity, nil } // calculateExecutionRoute calculates amounts through each step of the arbitrage func (calc *ArbitrageCalculator) calculateExecutionRoute( path []*PoolData, inputAmount *UniversalDecimal, inputToken TokenInfo, ) ([]ExchangeStep, error) { route := make([]ExchangeStep, len(path)) currentAmount := inputAmount currentToken := inputToken for i, pool := range path { // Determine output token for this step var outputToken TokenInfo if currentToken.Address == pool.Token0.Address { outputToken = TokenInfo{ Address: pool.Token1.Address, Symbol: "TOKEN1", // In a real implementation, you'd fetch the actual symbol Decimals: 18, } } else if currentToken.Address == pool.Token1.Address { outputToken = TokenInfo{ Address: pool.Token0.Address, Symbol: "TOKEN0", // In a real implementation, you'd fetch the actual symbol Decimals: 18, } } else { return nil, fmt.Errorf("token %s not found in pool %s", currentToken.Symbol, pool.Address) } // For this simplified implementation, we'll calculate a mock amount out // In a real implementation, you'd use the pricer's CalculateAmountOut method amountOut := currentAmount // Simple 1:1 for this example priceImpact := &UniversalDecimal{Value: big.NewInt(0), Decimals: 4, Symbol: "PERCENT"} // No impact in mock // Estimate gas for this step estimatedGas, err := calc.gasEstimator.EstimateSwapGas(ExchangeUniswapV3, pool) // Using a mock exchange type if err != nil { return nil, fmt.Errorf("error estimating gas for pool %s: %w", pool.Address, err) } // Create execution step route[i] = ExchangeStep{ Exchange: ExchangeUniswapV3, // Using a mock exchange type Pool: pool, TokenIn: currentToken, TokenOut: outputToken, AmountIn: currentAmount, AmountOut: amountOut, PriceImpact: priceImpact, EstimatedGas: estimatedGas, } // Update for next iteration currentAmount = amountOut currentToken = outputToken } return route, nil } // calculateTotalGasCost calculates the total gas cost for the entire route func (calc *ArbitrageCalculator) calculateTotalGasCost(route []ExchangeStep) (*UniversalDecimal, error) { // Get current gas price gasPrice, err := calc.gasEstimator.GetCurrentGasPrice() if err != nil { return nil, fmt.Errorf("error getting gas price: %w", err) } // Sum up all gas estimates totalGas := uint64(0) for _, step := range route { totalGas += step.EstimatedGas } // Add flash swap overhead if multi-step if len(route) > 1 { flashSwapGas, err := calc.gasEstimator.EstimateFlashSwapGas([]*PoolData{}) if err == nil { totalGas += flashSwapGas } } // Convert to gas cost in ETH totalGasInt64, err := security.SafeUint64ToInt64(totalGas) if err != nil { // This is very unlikely for gas calculations, but handle safely // Use maximum safe value as fallback totalGasInt64 = math.MaxInt64 } totalGasBig := big.NewInt(totalGasInt64) totalGasDecimal, err := NewUniversalDecimal(totalGasBig, 0, "GAS") if err != nil { return nil, err } return calc.decimalConverter.Multiply(totalGasDecimal, gasPrice, 18, "ETH") } // calculateProfits calculates gross profit, net profit, and profit percentage func (calc *ArbitrageCalculator) calculateProfits( inputAmount, outputAmount, gasCost *UniversalDecimal, inputToken, outputToken TokenInfo, ) (*UniversalDecimal, *UniversalDecimal, *UniversalDecimal, error) { // Convert amounts to common denomination (ETH) for comparison inputETH := calc.convertToETH(inputAmount, inputToken) outputETH := calc.convertToETH(outputAmount, outputToken) // Gross profit = output - input (in ETH terms) grossProfit, err := calc.decimalConverter.Subtract(outputETH, inputETH) if err != nil { return nil, nil, nil, fmt.Errorf("error calculating gross profit: %w", err) } // Net profit = gross profit - gas cost netProfit, err := calc.decimalConverter.Subtract(grossProfit, gasCost) if err != nil { return nil, nil, nil, fmt.Errorf("error calculating net profit: %w", err) } // Profit percentage = (net profit / input) * 100 profitPercentage, err := calc.decimalConverter.CalculatePercentage(netProfit, inputETH) if err != nil { return nil, nil, nil, fmt.Errorf("error calculating profit percentage: %w", err) } return grossProfit, netProfit, profitPercentage, nil } // calculateTotalPriceImpact calculates cumulative price impact across all steps func (calc *ArbitrageCalculator) calculateTotalPriceImpact(route []ExchangeStep) (*UniversalDecimal, error) { if len(route) == 0 { return NewUniversalDecimal(big.NewInt(0), 4, "PERCENT") } // Compound price impacts: (1 + impact1) * (1 + impact2) - 1 compoundedImpact, err := calc.decimalConverter.FromString("1", 4, "COMPOUND") if err != nil { return nil, err } for _, step := range route { // Convert price impact to factor (1 + impact) one, _ := calc.decimalConverter.FromString("1", 4, "ONE") impactFactor, err := calc.decimalConverter.Add(one, step.PriceImpact) if err != nil { return nil, fmt.Errorf("error calculating impact factor: %w", err) } // Multiply with cumulative impact compoundedImpact, err = calc.decimalConverter.Multiply(compoundedImpact, impactFactor, 4, "COMPOUND") if err != nil { return nil, fmt.Errorf("error compounding impact: %w", err) } } // Subtract 1 to get final impact percentage one, _ := calc.decimalConverter.FromString("1", 4, "ONE") totalImpact, err := calc.decimalConverter.Subtract(compoundedImpact, one) if err != nil { return nil, fmt.Errorf("error calculating total impact: %w", err) } return totalImpact, nil } // calculateMinimumOutput calculates minimum output accounting for slippage func (calc *ArbitrageCalculator) calculateMinimumOutput(expectedOutput *UniversalDecimal) (*UniversalDecimal, error) { // Apply slippage tolerance slippageFactor, err := calc.decimalConverter.Subtract( &UniversalDecimal{Value: big.NewInt(10000), Decimals: 4, Symbol: "ONE"}, calc.maxSlippage, ) if err != nil { return nil, err } return calc.decimalConverter.Multiply(expectedOutput, slippageFactor, 18, "TOKEN") } // assessRisks performs comprehensive risk assessment func (calc *ArbitrageCalculator) assessRisks(route []ExchangeStep, priceImpact, netProfit *UniversalDecimal) RiskAssessment { assessment := RiskAssessment{ Warnings: make([]string, 0), } // Assess liquidity risk assessment.Liquidity = calc.assessLiquidityRisk(route) // Assess price impact risk assessment.PriceImpact = calc.assessPriceImpactRisk(priceImpact) // Assess profitability risk profitRisk := calc.assessProfitabilityRisk(netProfit) // Assess gas price risk assessment.GasPrice = calc.assessGasPriceRisk() // Calculate overall risk (worst of all categories) risks := []RiskLevel{assessment.Liquidity, assessment.PriceImpact, profitRisk, assessment.GasPrice} assessment.Overall = calc.calculateOverallRisk(risks) // Calculate OverallRisk as a numeric value (0.0 to 1.0) based on the overall risk level switch assessment.Overall { case RiskLow: assessment.OverallRisk = 0.1 case RiskMedium: assessment.OverallRisk = 0.4 case RiskHigh: assessment.OverallRisk = 0.7 case RiskCritical: assessment.OverallRisk = 0.95 default: assessment.OverallRisk = 0.5 // Default to medium risk } return assessment } // Helper risk assessment methods func (calc *ArbitrageCalculator) assessLiquidityRisk(route []ExchangeStep) RiskLevel { for _, step := range route { // For this simplified implementation, assume a mock liquidity value // In a real implementation, you'd get this from the pricing engine mockLiquidity, _ := calc.decimalConverter.FromString("1000", 18, "TOKEN") // 1000 tokens if mockLiquidity.IsZero() { return RiskHigh } // Check if trade size is significant portion of liquidity (>10%) tenPercent, _ := calc.decimalConverter.FromString("10", 4, "PERCENT") tradeSizePercent, _ := calc.decimalConverter.CalculatePercentage(step.AmountIn, mockLiquidity) if comp, _ := calc.decimalConverter.Compare(tradeSizePercent, tenPercent); comp > 0 { return RiskMedium } } return RiskLow } func (calc *ArbitrageCalculator) assessPriceImpactRisk(priceImpact *UniversalDecimal) RiskLevel { fivePercent, _ := calc.decimalConverter.FromString("5", 4, "PERCENT") twoPercent, _ := calc.decimalConverter.FromString("2", 4, "PERCENT") if comp, _ := calc.decimalConverter.Compare(priceImpact, fivePercent); comp > 0 { return RiskHigh } if comp, _ := calc.decimalConverter.Compare(priceImpact, twoPercent); comp > 0 { return RiskMedium } return RiskLow } func (calc *ArbitrageCalculator) assessProfitabilityRisk(netProfit *UniversalDecimal) RiskLevel { if netProfit.IsNegative() { return RiskCritical } smallProfit, _ := calc.decimalConverter.FromString("0.001", 18, "ETH") // $1 at $1000/ETH mediumProfit, _ := calc.decimalConverter.FromString("0.01", 18, "ETH") // $10 at $1000/ETH if comp, _ := calc.decimalConverter.Compare(netProfit, smallProfit); comp < 0 { return RiskHigh } if comp, _ := calc.decimalConverter.Compare(netProfit, mediumProfit); comp < 0 { return RiskMedium } return RiskLow } func (calc *ArbitrageCalculator) assessGasPriceRisk() RiskLevel { currentGas, _ := calc.gasEstimator.GetCurrentGasPrice() if comp, _ := calc.decimalConverter.Compare(currentGas, calc.maxGasPriceGwei); comp > 0 { return RiskHigh } twentyGwei, _ := calc.decimalConverter.FromString("20", 9, "GWEI") if comp, _ := calc.decimalConverter.Compare(currentGas, twentyGwei); comp > 0 { return RiskMedium } return RiskLow } func (calc *ArbitrageCalculator) calculateOverallRisk(risks []RiskLevel) RiskLevel { riskScores := map[RiskLevel]int{ RiskLow: 1, RiskMedium: 2, RiskHigh: 3, RiskCritical: 4, } maxScore := 0 for _, risk := range risks { if score := riskScores[risk]; score > maxScore { maxScore = score } } for risk, score := range riskScores { if score == maxScore { return risk } } return RiskLow } // calculateConfidence calculates confidence score based on risk and profit func (calc *ArbitrageCalculator) calculateConfidence(risk RiskAssessment, netProfit, priceImpact *UniversalDecimal) float64 { baseConfidence := 0.5 // Adjust for risk level switch risk.Overall { case RiskLow: baseConfidence += 0.3 case RiskMedium: baseConfidence += 0.1 case RiskHigh: baseConfidence -= 0.2 case RiskCritical: baseConfidence -= 0.4 } // Adjust for profit magnitude if netProfit.IsPositive() { largeProfit, _ := calc.decimalConverter.FromString("0.1", 18, "ETH") if comp, _ := calc.decimalConverter.Compare(netProfit, largeProfit); comp > 0 { baseConfidence += 0.2 } } // Adjust for price impact lowImpact, _ := calc.decimalConverter.FromString("1", 4, "PERCENT") if comp, _ := calc.decimalConverter.Compare(priceImpact, lowImpact); comp < 0 { baseConfidence += 0.1 } if baseConfidence < 0 { baseConfidence = 0 } if baseConfidence > 1 { baseConfidence = 1 } return baseConfidence } // estimateExecutionTime estimates execution time in milliseconds func (calc *ArbitrageCalculator) estimateExecutionTime(route []ExchangeStep) int64 { baseTime := int64(500) // 500ms base // Add time per hop hopTime := int64(len(route)) * 200 // Add time for complex exchanges complexTime := int64(0) for _, step := range route { switch ExchangeType(step.Exchange) { case ExchangeUniswapV3, ExchangeCamelot: complexTime += 300 // Concentrated liquidity is more complex case ExchangeBalancer, ExchangeCurve: complexTime += 400 // Weighted/stable pools are complex default: complexTime += 100 // Simple AMM } } return baseTime + hopTime + complexTime } // convertToETH converts any token amount to ETH for comparison (placeholder) func (calc *ArbitrageCalculator) convertToETH(amount *UniversalDecimal, token TokenInfo) *UniversalDecimal { // This is a placeholder - in production, this would query price oracles // For now, assume 1:1 conversion for demonstration ethAmount, _ := calc.decimalConverter.ConvertTo(amount, 18, "ETH") return ethAmount } // IsOpportunityProfitable checks if opportunity meets minimum criteria // IsOpportunityProfitable checks if an opportunity meets profitability criteria func (calc *ArbitrageCalculator) IsOpportunityProfitable(opportunity *types.ArbitrageOpportunity) bool { if opportunity == nil { return false } // Check minimum profit threshold if !calc.checkProfitThreshold(opportunity) { return false } // Check maximum price impact if !calc.checkPriceImpactThreshold(opportunity) { return false } // Check risk level if !calc.checkRiskLevel(opportunity) { return false } // Check confidence threshold if !calc.checkConfidenceThreshold(opportunity) { return false } return true } // checkProfitThreshold checks if the opportunity meets minimum profit requirements func (calc *ArbitrageCalculator) checkProfitThreshold(opportunity *types.ArbitrageOpportunity) bool { if opportunity.Quantities != nil { if netProfitUD, err := calc.decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil { if cmp, err := calc.decimalConverter.Compare(netProfitUD, calc.minProfitThreshold); err == nil && cmp < 0 { return false } } } else if opportunity.NetProfit != nil { if opportunity.NetProfit.Cmp(calc.minProfitThreshold.Value) < 0 { return false } } else { return false } return true } // checkPriceImpactThreshold checks if the opportunity is below maximum price impact func (calc *ArbitrageCalculator) checkPriceImpactThreshold(opportunity *types.ArbitrageOpportunity) bool { if opportunity.Quantities != nil { if impactUD, err := calc.decimalAmountToUniversal(opportunity.Quantities.PriceImpact); err == nil { if cmp, err := calc.decimalConverter.Compare(impactUD, calc.maxPriceImpact); err == nil && cmp > 0 { return false } } } else { maxImpactFloat := float64(calc.maxPriceImpact.Value.Int64()) / math.Pow10(int(calc.maxPriceImpact.Decimals)) if opportunity.PriceImpact > maxImpactFloat { return false } } return true } // checkRiskLevel checks if the opportunity's risk is acceptable func (calc *ArbitrageCalculator) checkRiskLevel(opportunity *types.ArbitrageOpportunity) bool { return opportunity.Risk < 0.8 // High risk threshold } // checkConfidenceThreshold checks if the opportunity has sufficient confidence func (calc *ArbitrageCalculator) checkConfidenceThreshold(opportunity *types.ArbitrageOpportunity) bool { return opportunity.Confidence >= 0.3 } // SortOpportunitiesByProfitability sorts opportunities by net profit descending func (calc *ArbitrageCalculator) SortOpportunitiesByProfitability(opportunities []*types.ArbitrageOpportunity) { sort.Slice(opportunities, func(i, j int) bool { left, errL := calc.decimalAmountToUniversal(opportunities[i].Quantities.NetProfit) right, errR := calc.decimalAmountToUniversal(opportunities[j].Quantities.NetProfit) if errL == nil && errR == nil { cmp, err := calc.decimalConverter.Compare(left, right) if err == nil { return cmp > 0 } } // Fallback to canonical big.Int comparison return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0 // Descending order }) } func (calc *ArbitrageCalculator) decimalAmountToUniversal(dec types.DecimalAmount) (*UniversalDecimal, error) { if dec.Value == "" { return nil, fmt.Errorf("decimal amount empty") } val, ok := new(big.Int).SetString(dec.Value, 10) if !ok { return nil, fmt.Errorf("invalid decimal amount %s", dec.Value) } return NewUniversalDecimal(val, dec.Decimals, dec.Symbol) } // CalculateArbitrage calculates arbitrage opportunity for a given path and input amount func (calc *ArbitrageCalculator) CalculateArbitrage(ctx context.Context, inputAmount *UniversalDecimal, path []*PoolData) (*types.ArbitrageOpportunity, error) { if len(path) == 0 { return nil, fmt.Errorf("empty path provided") } // Get the input and output tokens for the path inputToken := path[0].Token0 outputToken := path[len(path)-1].Token1 if path[len(path)-1].Token0.Address == inputToken.Address { outputToken = path[len(path)-1].Token0 } // Calculate the arbitrage opportunity for this path opportunity, err := calc.CalculateArbitrageOpportunity(path, inputAmount, inputToken, outputToken) if err != nil { return nil, fmt.Errorf("failed to calculate arbitrage opportunity: %w", err) } return opportunity, nil } // FindOptimalPath finds the most profitable arbitrage path between two tokens func (calc *ArbitrageCalculator) FindOptimalPath(ctx context.Context, tokenA, tokenB common.Address, amount *UniversalDecimal) (*types.ArbitrageOpportunity, error) { // In a real implementation, this would query for available paths between tokens // and calculate the most profitable path. For this implementation, we'll return an error // indicating no path is available since we don't have direct path-finding ability in the calculator return nil, fmt.Errorf("FindOptimalPath not implemented in calculator - use executor.CalculateOptimalPath instead") } // FilterProfitableOpportunities returns only profitable opportunities func (calc *ArbitrageCalculator) FilterProfitableOpportunities(opportunities []*types.ArbitrageOpportunity) []*types.ArbitrageOpportunity { profitable := make([]*types.ArbitrageOpportunity, 0) for _, opp := range opportunities { if calc.IsOpportunityProfitable(opp) { profitable = append(profitable, opp) } } return profitable }