349 lines
11 KiB
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
|
|
}
|