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>
738 lines
24 KiB
Go
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
|
|
}
|