Files
mev-beta/pkg/arbitrage/calculator.go
Administrator 688311f1e0 fix(compilation): resolve type system and interface errors
- Add GetPoolsByToken method to cache interface and implementation
- Fix interface pointer types (use interface not *interface)
- Fix SwapEvent.TokenIn/TokenOut usage to use GetInputToken/GetOutputToken methods
- Fix ethereum.CallMsg import and usage
- Fix parser factory and validator initialization in main.go
- Remove unused variables and imports

WIP: Still fixing main.go config struct field mismatches

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 19:46:06 +01:00

481 lines
14 KiB
Go

package arbitrage
import (
"context"
"fmt"
"log/slog"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/your-org/mev-bot/pkg/parsers"
"github.com/your-org/mev-bot/pkg/types"
)
// CalculatorConfig contains configuration for profitability calculations
type CalculatorConfig struct {
MinProfitWei *big.Int // Minimum net profit in wei
MinROI float64 // Minimum ROI percentage (e.g., 0.05 = 5%)
MaxPriceImpact float64 // Maximum acceptable price impact (e.g., 0.10 = 10%)
MaxGasPriceGwei uint64 // Maximum gas price in gwei
SlippageTolerance float64 // Slippage tolerance (e.g., 0.005 = 0.5%)
}
// DefaultCalculatorConfig returns default configuration
func DefaultCalculatorConfig() *CalculatorConfig {
minProfit := new(big.Int).Mul(big.NewInt(5), new(big.Int).Exp(big.NewInt(10), big.NewInt(16), nil)) // 0.05 ETH
return &CalculatorConfig{
MinProfitWei: minProfit,
MinROI: 0.05, // 5%
MaxPriceImpact: 0.10, // 10%
MaxGasPriceGwei: 100, // 100 gwei
SlippageTolerance: 0.005, // 0.5%
}
}
// Calculator calculates profitability of arbitrage opportunities
type Calculator struct {
config *CalculatorConfig
logger *slog.Logger
gasEstimator *GasEstimator
}
// NewCalculator creates a new calculator
func NewCalculator(config *CalculatorConfig, gasEstimator *GasEstimator, logger *slog.Logger) *Calculator {
if config == nil {
config = DefaultCalculatorConfig()
}
return &Calculator{
config: config,
gasEstimator: gasEstimator,
logger: logger.With("component", "calculator"),
}
}
// CalculateProfitability calculates the profitability of a path
func (c *Calculator) CalculateProfitability(ctx context.Context, path *Path, inputAmount *big.Int, gasPrice *big.Int) (*Opportunity, error) {
if len(path.Pools) == 0 {
return nil, fmt.Errorf("path has no pools")
}
if inputAmount == nil || inputAmount.Sign() <= 0 {
return nil, fmt.Errorf("invalid input amount")
}
startTime := time.Now()
// Simulate the swap through each pool in the path
currentAmount := new(big.Int).Set(inputAmount)
pathSteps := make([]*PathStep, 0, len(path.Pools))
totalPriceImpact := 0.0
for i, pool := range path.Pools {
tokenIn := path.Tokens[i]
tokenOut := path.Tokens[i+1]
// Calculate swap output
amountOut, priceImpact, err := c.calculateSwapOutput(pool, tokenIn, tokenOut, currentAmount)
if err != nil {
c.logger.Warn("failed to calculate swap output",
"pool", pool.Address.Hex(),
"error", err,
)
return nil, fmt.Errorf("failed to calculate swap at pool %s: %w", pool.Address.Hex(), err)
}
// Create path step
step := &PathStep{
PoolAddress: pool.Address,
Protocol: pool.Protocol,
TokenIn: tokenIn,
TokenOut: tokenOut,
AmountIn: currentAmount,
AmountOut: amountOut,
Fee: pool.Fee,
}
// Calculate fee amount
step.FeeAmount = c.calculateFeeAmount(currentAmount, pool.Fee, pool.Protocol)
// Store V3-specific state if applicable
if pool.Protocol == types.ProtocolUniswapV3 && pool.SqrtPriceX96 != nil {
step.SqrtPriceX96Before = new(big.Int).Set(pool.SqrtPriceX96)
// Calculate new price after swap
zeroForOne := tokenIn == pool.Token0
newPrice, err := c.calculateNewPriceV3(pool, currentAmount, zeroForOne)
if err == nil {
step.SqrtPriceX96After = newPrice
}
}
pathSteps = append(pathSteps, step)
totalPriceImpact += priceImpact
// Update current amount for next hop
currentAmount = amountOut
}
// Calculate profits
outputAmount := currentAmount
grossProfit := new(big.Int).Sub(outputAmount, inputAmount)
// Estimate gas cost
gasCost, err := c.gasEstimator.EstimateGasCost(ctx, path, gasPrice)
if err != nil {
c.logger.Warn("failed to estimate gas cost", "error", err)
gasCost = big.NewInt(0)
}
// Calculate net profit
netProfit := new(big.Int).Sub(grossProfit, gasCost)
// Calculate ROI
roi := 0.0
if inputAmount.Sign() > 0 {
inputFloat, _ := new(big.Float).SetInt(inputAmount).Float64()
profitFloat, _ := new(big.Float).SetInt(netProfit).Float64()
roi = profitFloat / inputFloat
}
// Average price impact across all hops
avgPriceImpact := totalPriceImpact / float64(len(pathSteps))
// Create opportunity
opportunity := &Opportunity{
ID: fmt.Sprintf("%s-%d", path.Pools[0].Address.Hex(), time.Now().UnixNano()),
Type: path.Type,
DetectedAt: startTime,
BlockNumber: path.Pools[0].BlockNumber,
Path: pathSteps,
InputToken: path.Tokens[0],
OutputToken: path.Tokens[len(path.Tokens)-1],
InputAmount: inputAmount,
OutputAmount: outputAmount,
GrossProfit: grossProfit,
GasCost: gasCost,
NetProfit: netProfit,
ROI: roi,
PriceImpact: avgPriceImpact,
Priority: c.calculatePriority(netProfit, roi),
ExecuteAfter: time.Now(),
ExpiresAt: time.Now().Add(30 * time.Second), // 30 second expiration
Executable: c.isExecutable(netProfit, roi, avgPriceImpact),
}
c.logger.Debug("calculated profitability",
"opportunityID", opportunity.ID,
"inputAmount", inputAmount.String(),
"outputAmount", outputAmount.String(),
"grossProfit", grossProfit.String(),
"netProfit", netProfit.String(),
"roi", fmt.Sprintf("%.2f%%", roi*100),
"priceImpact", fmt.Sprintf("%.2f%%", avgPriceImpact*100),
"gasPrice", gasCost.String(),
"executable", opportunity.Executable,
"duration", time.Since(startTime),
)
return opportunity, nil
}
// calculateSwapOutput calculates the output amount for a swap
func (c *Calculator) calculateSwapOutput(pool *types.PoolInfo, tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, float64, error) {
switch pool.Protocol {
case types.ProtocolUniswapV2, types.ProtocolSushiSwap:
return c.calculateSwapOutputV2(pool, tokenIn, tokenOut, amountIn)
case types.ProtocolUniswapV3:
return c.calculateSwapOutputV3(pool, tokenIn, tokenOut, amountIn)
case types.ProtocolCurve:
return c.calculateSwapOutputCurve(pool, tokenIn, tokenOut, amountIn)
default:
return nil, 0, fmt.Errorf("unsupported protocol: %s", pool.Protocol)
}
}
// calculateSwapOutputV2 calculates output for UniswapV2-style pools
func (c *Calculator) calculateSwapOutputV2(pool *types.PoolInfo, tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, float64, error) {
if pool.Reserve0 == nil || pool.Reserve1 == nil {
return nil, 0, fmt.Errorf("pool has nil reserves")
}
// Determine direction
var reserveIn, reserveOut *big.Int
if tokenIn == pool.Token0 {
reserveIn = pool.Reserve0
reserveOut = pool.Reserve1
} else if tokenIn == pool.Token1 {
reserveIn = pool.Reserve1
reserveOut = pool.Reserve0
} else {
return nil, 0, fmt.Errorf("token not in pool")
}
// Apply fee (0.3% = 9970/10000)
fee := pool.Fee
if fee == 0 {
fee = 30 // Default 0.3%
}
// amountInWithFee = amountIn * (10000 - fee) / 10000
amountInWithFee := new(big.Int).Mul(amountIn, big.NewInt(int64(10000-fee)))
amountInWithFee.Div(amountInWithFee, big.NewInt(10000))
// amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee)
numerator := new(big.Int).Mul(reserveOut, amountInWithFee)
denominator := new(big.Int).Add(reserveIn, amountInWithFee)
amountOut := new(big.Int).Div(numerator, denominator)
// Calculate price impact
priceImpact := c.calculatePriceImpactV2(reserveIn, reserveOut, amountIn, amountOut)
return amountOut, priceImpact, nil
}
// calculateSwapOutputV3 calculates output for UniswapV3 pools
func (c *Calculator) calculateSwapOutputV3(pool *types.PoolInfo, tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, float64, error) {
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
return nil, 0, fmt.Errorf("pool missing V3 state")
}
zeroForOne := tokenIn == pool.Token0
// Use V3 math utilities
amountOut, priceAfter, err := parsers.CalculateSwapAmounts(
pool.SqrtPriceX96,
pool.Liquidity,
amountIn,
zeroForOne,
pool.Fee,
)
if err != nil {
return nil, 0, fmt.Errorf("V3 swap calculation failed: %w", err)
}
// Calculate price impact
priceImpact := c.calculatePriceImpactV3(pool.SqrtPriceX96, priceAfter)
return amountOut, priceImpact, nil
}
// calculateSwapOutputCurve calculates output for Curve pools
func (c *Calculator) calculateSwapOutputCurve(pool *types.PoolInfo, tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, float64, error) {
// Simplified Curve calculation
// In production, this should use the actual Curve StableSwap formula
if pool.Reserve0 == nil || pool.Reserve1 == nil {
return nil, 0, fmt.Errorf("pool has nil reserves")
}
// Determine direction (validate token is in pool)
if tokenIn != pool.Token0 && tokenIn != pool.Token1 {
return nil, 0, fmt.Errorf("token not in pool")
}
// Simplified: assume 1:1 swap with low slippage for stablecoins
// TODO: Implement proper Curve StableSwap math using reserves and amp coefficient
// This is a rough approximation - actual Curve math is more complex
fee := pool.Fee
if fee == 0 {
fee = 4 // Default 0.04% for Curve
}
// Scale amounts to same decimals
amountInScaled := amountIn
if tokenIn == pool.Token0 {
amountInScaled = types.ScaleToDecimals(amountIn, pool.Token0Decimals, 18)
} else {
amountInScaled = types.ScaleToDecimals(amountIn, pool.Token1Decimals, 18)
}
// Apply fee
amountOutScaled := new(big.Int).Mul(amountInScaled, big.NewInt(int64(10000-fee)))
amountOutScaled.Div(amountOutScaled, big.NewInt(10000))
// Scale back to output token decimals
var amountOut *big.Int
if tokenOut == pool.Token0 {
amountOut = types.ScaleToDecimals(amountOutScaled, 18, pool.Token0Decimals)
} else {
amountOut = types.ScaleToDecimals(amountOutScaled, 18, pool.Token1Decimals)
}
// Curve has very low price impact for stablecoins
priceImpact := 0.001 // 0.1%
return amountOut, priceImpact, nil
}
// calculateNewPriceV3 calculates the new sqrtPriceX96 after a swap
func (c *Calculator) calculateNewPriceV3(pool *types.PoolInfo, amountIn *big.Int, zeroForOne bool) (*big.Int, error) {
_, priceAfter, err := parsers.CalculateSwapAmounts(
pool.SqrtPriceX96,
pool.Liquidity,
amountIn,
zeroForOne,
pool.Fee,
)
return priceAfter, err
}
// calculatePriceImpactV2 calculates price impact for V2 swaps
func (c *Calculator) calculatePriceImpactV2(reserveIn, reserveOut, amountIn, amountOut *big.Int) float64 {
// Price before swap
priceBefore := new(big.Float).Quo(
new(big.Float).SetInt(reserveOut),
new(big.Float).SetInt(reserveIn),
)
// Price after swap
newReserveIn := new(big.Int).Add(reserveIn, amountIn)
newReserveOut := new(big.Int).Sub(reserveOut, amountOut)
if newReserveIn.Sign() == 0 {
return 1.0 // 100% impact
}
priceAfter := new(big.Float).Quo(
new(big.Float).SetInt(newReserveOut),
new(big.Float).SetInt(newReserveIn),
)
// Impact = |priceAfter - priceBefore| / priceBefore
diff := new(big.Float).Sub(priceAfter, priceBefore)
diff.Abs(diff)
impact := new(big.Float).Quo(diff, priceBefore)
impactFloat, _ := impact.Float64()
return impactFloat
}
// calculatePriceImpactV3 calculates price impact for V3 swaps
func (c *Calculator) calculatePriceImpactV3(priceBefore, priceAfter *big.Int) float64 {
if priceBefore.Sign() == 0 {
return 1.0
}
priceBeforeFloat := new(big.Float).SetInt(priceBefore)
priceAfterFloat := new(big.Float).SetInt(priceAfter)
diff := new(big.Float).Sub(priceAfterFloat, priceBeforeFloat)
diff.Abs(diff)
impact := new(big.Float).Quo(diff, priceBeforeFloat)
impactFloat, _ := impact.Float64()
return impactFloat
}
// calculateFeeAmount calculates the fee paid in a swap
func (c *Calculator) calculateFeeAmount(amountIn *big.Int, feeBasisPoints uint32, protocol types.ProtocolType) *big.Int {
if feeBasisPoints == 0 {
return big.NewInt(0)
}
// Fee amount = amountIn * feeBasisPoints / 10000
feeAmount := new(big.Int).Mul(amountIn, big.NewInt(int64(feeBasisPoints)))
feeAmount.Div(feeAmount, big.NewInt(10000))
return feeAmount
}
// calculatePriority calculates priority score for an opportunity
func (c *Calculator) calculatePriority(netProfit *big.Int, roi float64) int {
// Priority based on both absolute profit and ROI
// Higher profit and ROI = higher priority
profitScore := 0
if netProfit.Sign() > 0 {
// Convert to ETH for scoring
profitEth := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt64(1e18),
)
profitEthFloat, _ := profitEth.Float64()
profitScore = int(profitEthFloat * 100) // Scale to integer
}
roiScore := int(roi * 1000) // Scale to integer
priority := profitScore + roiScore
return priority
}
// isExecutable checks if an opportunity meets execution criteria
func (c *Calculator) isExecutable(netProfit *big.Int, roi, priceImpact float64) bool {
// Check minimum profit
if netProfit.Cmp(c.config.MinProfitWei) < 0 {
return false
}
// Check minimum ROI
if roi < c.config.MinROI {
return false
}
// Check maximum price impact
if priceImpact > c.config.MaxPriceImpact {
return false
}
return true
}
// OptimizeInputAmount finds the optimal input amount for maximum profit
func (c *Calculator) OptimizeInputAmount(ctx context.Context, path *Path, gasPrice *big.Int, maxInput *big.Int) (*Opportunity, error) {
c.logger.Debug("optimizing input amount",
"path", fmt.Sprintf("%d pools", len(path.Pools)),
"maxInput", maxInput.String(),
)
// Binary search for optimal input
low := new(big.Int).Div(maxInput, big.NewInt(100)) // Start at 1% of max
high := new(big.Int).Set(maxInput)
bestOpp := (*Opportunity)(nil)
iterations := 0
maxIterations := 20
for low.Cmp(high) < 0 && iterations < maxIterations {
iterations++
// Try mid point
mid := new(big.Int).Add(low, high)
mid.Div(mid, big.NewInt(2))
opp, err := c.CalculateProfitability(ctx, path, mid, gasPrice)
if err != nil {
c.logger.Warn("optimization iteration failed", "error", err)
break
}
if bestOpp == nil || opp.NetProfit.Cmp(bestOpp.NetProfit) > 0 {
bestOpp = opp
}
// If profit is increasing, try larger amount
// If profit is decreasing, try smaller amount
if opp.NetProfit.Sign() > 0 && opp.PriceImpact < c.config.MaxPriceImpact {
low = new(big.Int).Add(mid, big.NewInt(1))
} else {
high = new(big.Int).Sub(mid, big.NewInt(1))
}
}
if bestOpp == nil {
return nil, fmt.Errorf("failed to find profitable input amount")
}
c.logger.Info("optimized input amount",
"iterations", iterations,
"optimalInput", bestOpp.InputAmount.String(),
"netProfit", bestOpp.NetProfit.String(),
"roi", fmt.Sprintf("%.2f%%", bestOpp.ROI*100),
)
return bestOpp, nil
}