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>
This commit is contained in:
737
orig/pkg/math/arbitrage_calculator.go
Normal file
737
orig/pkg/math/arbitrage_calculator.go
Normal file
@@ -0,0 +1,737 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user