Files
mev-beta/pkg/profitcalc/slippage_protection.go

349 lines
11 KiB
Go

package profitcalc
import (
"fmt"
"math/big"
"github.com/fraktal/mev-beta/internal/logger"
)
// SlippageProtector provides slippage calculation and protection for arbitrage trades
type SlippageProtector struct {
logger *logger.Logger
maxSlippageBps int64 // Maximum allowed slippage in basis points
liquidityBuffer float64 // Buffer factor for liquidity calculations
}
// SlippageAnalysis contains detailed slippage analysis for a trade
type SlippageAnalysis struct {
TradeAmount *big.Float // Amount being traded
PoolLiquidity *big.Float // Available liquidity in pool
EstimatedSlippage float64 // Estimated slippage percentage
SlippageBps int64 // Slippage in basis points
PriceImpact float64 // Price impact percentage
EffectivePrice *big.Float // Price after slippage
MinAmountOut *big.Float // Minimum amount out with slippage protection
IsAcceptable bool // Whether slippage is within acceptable limits
RiskLevel string // "Low", "Medium", "High", "Extreme"
Recommendation string // Trading recommendation
}
// NewSlippageProtector creates a new slippage protection manager
func NewSlippageProtector(logger *logger.Logger) *SlippageProtector {
return &SlippageProtector{
logger: logger,
maxSlippageBps: 500, // 5% maximum slippage
liquidityBuffer: 0.8, // Use 80% of available liquidity for calculations
}
}
// AnalyzeSlippage performs comprehensive slippage analysis for a potential trade
func (sp *SlippageProtector) AnalyzeSlippage(
tradeAmount *big.Float,
poolLiquidity *big.Float,
currentPrice *big.Float,
) *SlippageAnalysis {
if tradeAmount == nil || poolLiquidity == nil || currentPrice == nil {
return &SlippageAnalysis{
IsAcceptable: false,
RiskLevel: "Extreme",
Recommendation: "Insufficient data for slippage calculation",
}
}
// Calculate trade size as percentage of pool liquidity
tradeSizeRatio := new(big.Float).Quo(tradeAmount, poolLiquidity)
tradeSizeFloat, _ := tradeSizeRatio.Float64()
// CRITICAL FIX: Use proper constant product AMM formula
// Bug: Old formula "tradeSize / 2.0" is mathematically incorrect
// Correct Uniswap V2 formula: dy = (y * dx * 997) / (x * 1000 + dx * 997)
// Slippage = (marketPrice - executionPrice) / marketPrice
estimatedSlippage := sp.calculateConstantProductSlippage(tradeAmount, poolLiquidity, currentPrice)
slippageBps := int64(estimatedSlippage * 10000)
// Calculate price impact (similar to slippage but different calculation)
priceImpact := sp.calculatePriceImpact(tradeSizeFloat)
// Calculate effective price after slippage
slippageFactor := 1.0 - estimatedSlippage
effectivePrice := new(big.Float).Mul(currentPrice, big.NewFloat(slippageFactor))
// Calculate minimum amount out with slippage protection
minAmountOut := new(big.Float).Quo(tradeAmount, effectivePrice)
// Determine risk level and acceptability
riskLevel, isAcceptable := sp.assessRiskLevel(slippageBps, tradeSizeFloat)
recommendation := sp.generateRecommendation(slippageBps, tradeSizeFloat, riskLevel)
analysis := &SlippageAnalysis{
TradeAmount: tradeAmount,
PoolLiquidity: poolLiquidity,
EstimatedSlippage: estimatedSlippage,
SlippageBps: slippageBps,
PriceImpact: priceImpact,
EffectivePrice: effectivePrice,
MinAmountOut: minAmountOut,
IsAcceptable: isAcceptable,
RiskLevel: riskLevel,
Recommendation: recommendation,
}
sp.logger.Debug(fmt.Sprintf("Slippage analysis: Trade=%s, Liquidity=%s, Slippage=%.2f%%, Risk=%s",
tradeAmount.String(), poolLiquidity.String(), estimatedSlippage*100, riskLevel))
return analysis
}
// calculateConstantProductSlippage calculates slippage using correct Uniswap V2 constant product formula
// Formula: dy = (y * dx * 997) / (x * 1000 + dx * 997)
// Where: x = reserveIn, y = reserveOut, dx = amountIn, dy = amountOut
// Slippage = (marketPrice - executionPrice) / marketPrice
func (sp *SlippageProtector) calculateConstantProductSlippage(
amountIn *big.Float,
reserveIn *big.Float,
marketPrice *big.Float,
) float64 {
// Handle edge cases
if amountIn == nil || reserveIn == nil || marketPrice == nil {
return 1.0 // 100% slippage for invalid inputs
}
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || marketPrice.Sign() <= 0 {
return 1.0
}
// Calculate: dx * 997 (Uniswap V2 fee is 0.3%, so 997/1000 remains)
fee := big.NewFloat(997)
dx997 := new(big.Float).Mul(amountIn, fee)
// Calculate: x * 1000
x1000 := new(big.Float).Mul(reserveIn, big.NewFloat(1000))
// Calculate: x * 1000 + dx * 997
denominator := new(big.Float).Add(x1000, dx997)
// Calculate execution price = dx / (x * 1000 + dx * 997)
// This represents how much the price moved
executionRatio := new(big.Float).Quo(dx997, denominator)
// For small trades, slippage ≈ executionRatio / 2
// For larger trades, use more precise calculation
tradeSizeRatio := new(big.Float).Quo(amountIn, reserveIn)
tradeSizeFloat, _ := tradeSizeRatio.Float64()
var slippage float64
if tradeSizeFloat < 0.01 { // < 1% of pool
// Linear approximation for small trades
ratioFloat, _ := executionRatio.Float64()
slippage = ratioFloat / 2.0
} else {
// Full calculation for larger trades
// Slippage = (marketPrice - executionPrice) / marketPrice
// executionPrice = dy / dx = (y * 997) / (x * 1000 + dx * 997)
// Simplified: slippage ≈ dx / (2 * (x + dx/2))
halfDx := new(big.Float).Quo(amountIn, big.NewFloat(2))
reservePlusHalfDx := new(big.Float).Add(reserveIn, halfDx)
twoTimesReservePlusHalfDx := new(big.Float).Mul(reservePlusHalfDx, big.NewFloat(2))
slippageFloat := new(big.Float).Quo(amountIn, twoTimesReservePlusHalfDx)
slippage, _ = slippageFloat.Float64()
// Apply fee adjustment (0.3% fee adds to effective slippage)
slippage += 0.003
}
// Cap at 100%
if slippage > 1.0 {
slippage = 1.0
} else if slippage < 0 {
slippage = 0
}
return slippage
}
// calculatePriceImpact calculates price impact using AMM mechanics
func (sp *SlippageProtector) calculatePriceImpact(tradeSizeRatio float64) float64 {
// For constant product AMMs (like Uniswap V2):
// Price impact = trade_size / (1 + trade_size)
priceImpact := tradeSizeRatio / (1.0 + tradeSizeRatio)
// Cap at 100%
if priceImpact > 1.0 {
priceImpact = 1.0
}
return priceImpact
}
// assessRiskLevel determines risk level based on slippage and trade size
func (sp *SlippageProtector) assessRiskLevel(slippageBps int64, tradeSizeRatio float64) (string, bool) {
isAcceptable := slippageBps <= sp.maxSlippageBps
var riskLevel string
switch {
case slippageBps <= 50: // <= 0.5%
riskLevel = "Low"
case slippageBps <= 200: // <= 2%
riskLevel = "Medium"
case slippageBps <= 500: // <= 5%
riskLevel = "High"
default:
riskLevel = "Extreme"
isAcceptable = false
}
// Additional checks for trade size
if tradeSizeRatio > 0.5 { // > 50% of pool
riskLevel = "Extreme"
isAcceptable = false
} else if tradeSizeRatio > 0.2 { // > 20% of pool
if riskLevel == "Low" {
riskLevel = "Medium"
} else if riskLevel == "Medium" {
riskLevel = "High"
}
}
return riskLevel, isAcceptable
}
// generateRecommendation provides trading recommendations based on analysis
func (sp *SlippageProtector) generateRecommendation(slippageBps int64, tradeSizeRatio float64, riskLevel string) string {
switch riskLevel {
case "Low":
return "Safe to execute - low slippage expected"
case "Medium":
if tradeSizeRatio > 0.1 {
return "Consider splitting trade into smaller parts"
}
return "Proceed with caution - moderate slippage expected"
case "High":
return "High slippage risk - consider reducing trade size or finding alternative routes"
case "Extreme":
if tradeSizeRatio > 0.5 {
return "Trade too large for pool - split into multiple smaller trades"
}
return "Excessive slippage - avoid this trade"
default:
return "Unable to assess - insufficient data"
}
}
// CalculateOptimalTradeSize calculates optimal trade size to stay within slippage limits
func (sp *SlippageProtector) CalculateOptimalTradeSize(
poolLiquidity *big.Float,
maxSlippageBps int64,
) *big.Float {
if poolLiquidity == nil || poolLiquidity.Sign() <= 0 {
return big.NewFloat(0)
}
// Convert max slippage to ratio
maxSlippageRatio := float64(maxSlippageBps) / 10000.0
// For simplified AMM: optimal_trade_size = 2 * liquidity * max_slippage
optimalRatio := 2.0 * maxSlippageRatio
// Apply safety factor
safetyFactor := 0.8 // Use 80% of optimal to be conservative
optimalRatio *= safetyFactor
optimalSize := new(big.Float).Mul(poolLiquidity, big.NewFloat(optimalRatio))
sp.logger.Debug(fmt.Sprintf("Calculated optimal trade size: %s (%.2f%% of pool) for max slippage %d bps",
optimalSize.String(), optimalRatio*100, maxSlippageBps))
return optimalSize
}
// EstimateGasForSlippage estimates additional gas needed for slippage protection
func (sp *SlippageProtector) EstimateGasForSlippage(analysis *SlippageAnalysis) uint64 {
baseGas := uint64(0)
// Higher slippage might require more complex routing
switch analysis.RiskLevel {
case "Low":
baseGas = 0 // No additional gas
case "Medium":
baseGas = 20000 // Additional gas for price checks
case "High":
baseGas = 50000 // Additional gas for complex routing
case "Extreme":
baseGas = 100000 // Maximum additional gas for emergency handling
}
return baseGas
}
// SetMaxSlippage updates the maximum allowed slippage
func (sp *SlippageProtector) SetMaxSlippage(bps int64) {
sp.maxSlippageBps = bps
sp.logger.Info(fmt.Sprintf("Updated maximum slippage to %d bps (%.2f%%)", bps, float64(bps)/100))
}
// GetMaxSlippage returns the current maximum slippage setting
func (sp *SlippageProtector) GetMaxSlippage() int64 {
return sp.maxSlippageBps
}
// ValidateTradeParameters performs comprehensive validation of trade parameters
func (sp *SlippageProtector) ValidateTradeParameters(
tradeAmount *big.Float,
poolLiquidity *big.Float,
minLiquidity *big.Float,
) error {
if tradeAmount == nil || tradeAmount.Sign() <= 0 {
return fmt.Errorf("invalid trade amount: must be positive")
}
if poolLiquidity == nil || poolLiquidity.Sign() <= 0 {
return fmt.Errorf("invalid pool liquidity: must be positive")
}
if minLiquidity != nil && poolLiquidity.Cmp(minLiquidity) < 0 {
return fmt.Errorf("insufficient pool liquidity: %s < %s required",
poolLiquidity.String(), minLiquidity.String())
}
// Check if trade is reasonable relative to pool size
tradeSizeRatio := new(big.Float).Quo(tradeAmount, poolLiquidity)
tradeSizeFloat, _ := tradeSizeRatio.Float64()
if tradeSizeFloat > 0.9 { // > 90% of pool
return fmt.Errorf("trade size too large: %.1f%% of pool liquidity", tradeSizeFloat*100)
}
return nil
}
// CalculateSlippageAdjustedProfit adjusts profit calculations for slippage
func (sp *SlippageProtector) CalculateSlippageAdjustedProfit(
grossProfit *big.Float,
analysis *SlippageAnalysis,
) *big.Float {
if grossProfit == nil || analysis == nil {
return big.NewFloat(0)
}
// Reduce profit by estimated slippage impact
slippageImpact := big.NewFloat(analysis.EstimatedSlippage)
slippageReduction := new(big.Float).Mul(grossProfit, slippageImpact)
adjustedProfit := new(big.Float).Sub(grossProfit, slippageReduction)
// Ensure profit doesn't go negative due to slippage
if adjustedProfit.Sign() < 0 {
adjustedProfit = big.NewFloat(0)
}
sp.logger.Debug(fmt.Sprintf("Slippage-adjusted profit: %s -> %s (reduction: %s)",
grossProfit.String(), adjustedProfit.String(), slippageReduction.String()))
return adjustedProfit
}