Files
mev-beta/orig/pkg/math/arbitrage_calculator.go
Administrator 803de231ba feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor:

**Structure Changes:**
- Moved all V1 code to orig/ folder (preserved with git mv)
- Created docs/planning/ directory
- Added orig/README_V1.md explaining V1 preservation

**Planning Documents:**
- 00_V2_MASTER_PLAN.md: Complete architecture overview
  - Executive summary of critical V1 issues
  - High-level component architecture diagrams
  - 5-phase implementation roadmap
  - Success metrics and risk mitigation

- 07_TASK_BREAKDOWN.md: Atomic task breakdown
  - 99+ hours of detailed tasks
  - Every task < 2 hours (atomic)
  - Clear dependencies and success criteria
  - Organized by implementation phase

**V2 Key Improvements:**
- Per-exchange parsers (factory pattern)
- Multi-layer strict validation
- Multi-index pool cache
- Background validation pipeline
- Comprehensive observability

**Critical Issues Addressed:**
- Zero address tokens (strict validation + cache enrichment)
- Parsing accuracy (protocol-specific parsers)
- No audit trail (background validation channel)
- Inefficient lookups (multi-index cache)
- Stats disconnection (event-driven metrics)

Next Steps:
1. Review planning documents
2. Begin Phase 1: Foundation (P1-001 through P1-010)
3. Implement parsers in Phase 2
4. Build cache system in Phase 3
5. Add validation pipeline in Phase 4
6. Migrate and test in Phase 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:14:26 +01:00

738 lines
24 KiB
Go

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
}