package risk import ( "fmt" "math/big" "sync" "time" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/math" ) // RiskManager manages risk for MEV operations type RiskManager struct { logger *logger.Logger mu sync.RWMutex decimalConverter *math.DecimalConverter // Position limits maxPositionSize *big.Int // Maximum position size in wei dailyLossLimit *big.Int // Maximum daily loss in wei maxConcurrent int // Maximum concurrent positions maxPositionSizeDecimal *math.UniversalDecimal dailyLossLimitDecimal *math.UniversalDecimal minProfitThresholdDecimal *math.UniversalDecimal maxGasPriceDecimal *math.UniversalDecimal // Current state currentPositions int dailyLoss *big.Int lastReset time.Time // Risk metrics totalTrades uint64 successfulTrades uint64 failedTrades uint64 totalProfit *big.Int totalLoss *big.Int totalProfitDecimal *math.UniversalDecimal totalLossDecimal *math.UniversalDecimal // Configuration minProfitThreshold *big.Int // Minimum profit threshold in wei maxSlippage float64 // Maximum acceptable slippage maxGasPrice *big.Int // Maximum gas price willing to pay // Circuit breaker circuitBreaker *CircuitBreaker } func (rm *RiskManager) fromWei(value *big.Int, symbol string) *math.UniversalDecimal { if value == nil { zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, symbol) return zero } decimals := uint8(18) if symbol == "GWEI" { decimals = 9 } return rm.decimalConverter.FromWei(value, decimals, symbol) } // RiskAssessment represents a risk assessment for an MEV opportunity type RiskAssessment struct { OpportunityID string RiskScore float64 // 0-1, higher = riskier Confidence float64 // 0-1, higher = more confident MaxPositionSize *big.Int // Maximum position size for this opportunity RecommendedGas *big.Int // Recommended gas price SlippageLimit float64 // Maximum slippage for this opportunity Profitability float64 // Expected ROI percentage Acceptable bool // Whether the opportunity passes risk checks Reason string // Reason for acceptance/rejection MaxPositionSizeDecimal *math.UniversalDecimal RecommendedGasDecimal *math.UniversalDecimal } // CircuitBreaker manages circuit breaking for risk control type CircuitBreaker struct { failures int lastFailure time.Time resetTimeout time.Duration failureThreshold int open bool mu sync.RWMutex } // NewRiskManager creates a new risk manager func NewRiskManager(logger *logger.Logger) *RiskManager { dc := math.NewDecimalConverter() rm := &RiskManager{ logger: logger, decimalConverter: dc, maxPositionSize: big.NewInt(1000000000000000000), // 1 ETH dailyLossLimit: big.NewInt(100000000000000000), // 0.1 ETH maxConcurrent: 5, currentPositions: 0, dailyLoss: big.NewInt(0), lastReset: time.Now(), totalTrades: 0, successfulTrades: 0, failedTrades: 0, totalProfit: big.NewInt(0), totalLoss: big.NewInt(0), minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH maxSlippage: 0.01, // 1% maxGasPrice: big.NewInt(20000000000), // 20 gwei circuitBreaker: &CircuitBreaker{ resetTimeout: 5 * time.Minute, failureThreshold: 3, }, } rm.maxPositionSizeDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxPositionSize), 18, "ETH") rm.dailyLossLimitDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.dailyLossLimit), 18, "ETH") rm.minProfitThresholdDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.minProfitThreshold), 18, "ETH") rm.maxGasPriceDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxGasPrice), 9, "GWEI") rm.totalProfitDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH") rm.totalLossDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH") // Start daily reset timer go rm.dailyReset() return rm } // AssessOpportunity assesses the risk of an MEV opportunity func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) *RiskAssessment { rm.mu.Lock() defer rm.mu.Unlock() assessment := &RiskAssessment{ OpportunityID: opportunityID, RiskScore: 0.0, Confidence: 0.0, MaxPositionSize: big.NewInt(0), RecommendedGas: big.NewInt(0), SlippageLimit: 0.0, Profitability: 0.0, Acceptable: false, Reason: "", MaxPositionSizeDecimal: rm.maxPositionSizeDecimal, RecommendedGasDecimal: rm.maxGasPriceDecimal, } // Check circuit breaker if rm.circuitBreaker.IsOpen() { assessment.Reason = "Circuit breaker is open" return assessment } expectedProfitDec := rm.fromWei(expectedProfit, "ETH") gasPriceDec := rm.fromWei(gasPrice, "GWEI") dailyLossDec := rm.fromWei(rm.dailyLoss, "ETH") // Check if we've exceeded concurrent position limits if rm.currentPositions >= rm.maxConcurrent { assessment.Reason = fmt.Sprintf("Maximum concurrent positions reached: %d", rm.maxConcurrent) return assessment } // Check if we've exceeded daily loss limits if cmp, err := rm.decimalConverter.Compare(dailyLossDec, rm.dailyLossLimitDecimal); err == nil && cmp > 0 { assessment.Reason = fmt.Sprintf("Daily loss limit exceeded: %s > %s", rm.decimalConverter.ToHumanReadable(dailyLossDec), rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal)) return assessment } // Check minimum profit threshold if cmp, err := rm.decimalConverter.Compare(expectedProfitDec, rm.minProfitThresholdDecimal); err == nil && cmp < 0 { assessment.Reason = fmt.Sprintf("Profit below minimum threshold: %s < %s", rm.decimalConverter.ToHumanReadable(expectedProfitDec), rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal)) return assessment } // Check slippage tolerance if slippage > rm.maxSlippage { assessment.Reason = fmt.Sprintf("Slippage exceeds limit: %.2f%% > %.2f%%", slippage*100, rm.maxSlippage*100) return assessment } // Check gas price limits if cmp, err := rm.decimalConverter.Compare(gasPriceDec, rm.maxGasPriceDecimal); err == nil && cmp > 0 { assessment.Reason = fmt.Sprintf("Gas price exceeds limit: %s > %s", rm.decimalConverter.ToHumanReadable(gasPriceDec), rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal)) return assessment } // Calculate risk score based on multiple factors riskScore := rm.calculateRiskScore(expectedProfit, gasCost, slippage, gasPrice) assessment.RiskScore = riskScore // Calculate confidence based on historical performance confidence := rm.calculateConfidence() assessment.Confidence = confidence // Calculate profitability (ROI percentage) profitability := rm.calculateProfitability(expectedProfit, gasCost) assessment.Profitability = profitability // Determine maximum position size based on risk maxPosition := rm.calculateMaxPositionSize(riskScore, expectedProfit, gasCost) assessment.MaxPositionSize = maxPosition assessment.MaxPositionSizeDecimal = rm.fromWei(maxPosition, "ETH") // Recommend gas price based on network conditions and risk recommendedGas := rm.calculateRecommendedGas(gasPrice, riskScore) assessment.RecommendedGas = recommendedGas assessment.RecommendedGasDecimal = rm.fromWei(recommendedGas, "GWEI") // Set slippage limit based on risk tolerance slippageLimit := rm.calculateSlippageLimit(riskScore) assessment.SlippageLimit = slippageLimit // Determine if opportunity is acceptable acceptable := rm.isAcceptable(riskScore, profitability, confidence) assessment.Acceptable = acceptable if acceptable { assessment.Reason = "Opportunity passes all risk checks" } else { assessment.Reason = fmt.Sprintf("Risk score too high: %.2f", riskScore) } rm.logger.Debug(fmt.Sprintf("Risk assessment for %s: Risk=%.2f, Confidence=%.2f, Profitability=%.2f%%, Acceptable=%t", opportunityID, riskScore, confidence, profitability, acceptable)) return assessment } // RecordTrade records the result of a trade for risk management func (rm *RiskManager) RecordTrade(success bool, profit, gasCost *big.Int) { rm.mu.Lock() defer rm.mu.Unlock() rm.totalTrades++ if success { rm.successfulTrades++ if profit != nil { rm.totalProfit.Add(rm.totalProfit, profit) profitDec := rm.fromWei(profit, "ETH") rm.totalProfitDecimal, _ = rm.decimalConverter.Add(rm.totalProfitDecimal, profitDec) } } else { rm.failedTrades++ if gasCost != nil { rm.totalLoss.Add(rm.totalLoss, gasCost) rm.dailyLoss.Add(rm.dailyLoss, gasCost) lossDec := rm.fromWei(gasCost, "ETH") rm.totalLossDecimal, _ = rm.decimalConverter.Add(rm.totalLossDecimal, lossDec) } } // Update circuit breaker if !success { rm.circuitBreaker.RecordFailure() } else { rm.circuitBreaker.RecordSuccess() } rm.logger.Debug(fmt.Sprintf("Trade recorded: Success=%t, Profit=%s, Gas=%s, DailyLoss=%s", success, rm.decimalConverter.ToHumanReadable(rm.fromWei(profit, "ETH")), rm.decimalConverter.ToHumanReadable(rm.fromWei(gasCost, "ETH")), rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH")))) } // UpdatePositionCount updates the current position count func (rm *RiskManager) UpdatePositionCount(delta int) { rm.mu.Lock() defer rm.mu.Unlock() rm.currentPositions += delta if rm.currentPositions < 0 { rm.currentPositions = 0 } rm.logger.Debug(fmt.Sprintf("Position count updated: %d", rm.currentPositions)) } // GetStatistics returns risk management statistics func (rm *RiskManager) GetStatistics() map[string]interface{} { rm.mu.RLock() defer rm.mu.RUnlock() return map[string]interface{}{ "total_trades": rm.totalTrades, "successful_trades": rm.successfulTrades, "failed_trades": rm.failedTrades, "success_rate": float64(rm.successfulTrades) / float64(max(1, rm.totalTrades)), "total_profit": rm.decimalConverter.ToHumanReadable(rm.totalProfitDecimal), "total_loss": rm.decimalConverter.ToHumanReadable(rm.totalLossDecimal), "daily_loss": rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH")), "daily_loss_limit": rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal), "current_positions": rm.currentPositions, "max_concurrent": rm.maxConcurrent, "circuit_breaker_open": rm.circuitBreaker.IsOpen(), "min_profit_threshold": rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal), "max_slippage": rm.maxSlippage * 100, // Convert to percentage "max_gas_price": rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal), } } // dailyReset resets daily counters func (rm *RiskManager) dailyReset() { ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() for { <-ticker.C rm.mu.Lock() rm.dailyLoss = big.NewInt(0) rm.lastReset = time.Now() rm.mu.Unlock() rm.logger.Info("Daily risk counters reset") } } // calculateRiskScore calculates a risk score based on multiple factors func (rm *RiskManager) calculateRiskScore(expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) float64 { // Base risk (0-0.3) baseRisk := 0.1 // Gas risk (0-0.3) - higher gas = higher risk gasRisk := 0.0 if gasPrice != nil && rm.maxGasPrice != nil && rm.maxGasPrice.Sign() > 0 { gasRatio := new(big.Float).Quo(new(big.Float).SetInt(gasPrice), new(big.Float).SetInt(rm.maxGasPrice)) gasRatioFloat, _ := gasRatio.Float64() gasRisk = gasRatioFloat * 0.3 if gasRisk > 0.3 { gasRisk = 0.3 } } // Slippage risk (0-0.2) - higher slippage = higher risk slippageRisk := slippage * 0.2 if slippageRisk > 0.2 { slippageRisk = 0.2 } // Profit risk (0-0.2) - lower profit = higher relative risk profitRisk := 0.0 if expectedProfit != nil && expectedProfit.Sign() > 0 { // Lower profits are riskier relative to gas costs if gasCost != nil && gasCost.Sign() > 0 { profitRatio := new(big.Float).Quo(new(big.Float).SetInt(gasCost), new(big.Float).SetInt(expectedProfit)) profitRatioFloat, _ := profitRatio.Float64() profitRisk = profitRatioFloat * 0.2 if profitRisk > 0.2 { profitRisk = 0.2 } } } totalRisk := baseRisk + gasRisk + slippageRisk + profitRisk if totalRisk > 1.0 { totalRisk = 1.0 } return totalRisk } // calculateConfidence calculates confidence based on historical performance func (rm *RiskManager) calculateConfidence() float64 { if rm.totalTrades == 0 { return 0.5 // Default confidence for new system } successRate := float64(rm.successfulTrades) / float64(rm.totalTrades) confidence := successRate * 0.8 // Weight success rate at 80% // Add bonus for high volume of trades volumeBonus := float64(min(rm.totalTrades, 1000)) / 1000.0 * 0.2 // Max 20% bonus confidence += volumeBonus if confidence > 1.0 { confidence = 1.0 } return confidence } // calculateProfitability calculates profitability as ROI percentage func (rm *RiskManager) calculateProfitability(expectedProfit, gasCost *big.Int) float64 { if expectedProfit == nil || expectedProfit.Sign() <= 0 { return 0.0 } netProfit := new(big.Int).Sub(expectedProfit, gasCost) if netProfit.Sign() <= 0 { return 0.0 } // For profitability calculation, we need an investment amount // Use a reasonable default investment (e.g., 1 ETH) investment := big.NewInt(1000000000000000000) // 1 ETH roi := new(big.Float).Quo(new(big.Float).SetInt(netProfit), new(big.Float).SetInt(investment)) roi.Mul(roi, big.NewFloat(100)) roiFloat, _ := roi.Float64() return roiFloat } // calculateMaxPositionSize calculates maximum position size based on risk func (rm *RiskManager) calculateMaxPositionSize(riskScore float64, expectedProfit, gasCost *big.Int) *big.Int { // Start with maximum position size maxPosition := new(big.Int).Set(rm.maxPositionSize) // Reduce position size based on risk score riskMultiplier := 1.0 - riskScore maxPositionFloat := new(big.Float).SetInt(maxPosition) maxPositionFloat.Mul(maxPositionFloat, big.NewFloat(riskMultiplier)) result := new(big.Int) maxPositionFloat.Int(result) return result } // calculateRecommendedGas calculates recommended gas price based on risk func (rm *RiskManager) calculateRecommendedGas(gasPrice *big.Int, riskScore float64) *big.Int { if gasPrice == nil { return big.NewInt(0) } // For high-risk opportunities, recommend higher gas price for faster execution // For low-risk opportunities, can use lower gas price gasMultiplier := 1.0 + (riskScore * 0.5) // Up to 50% increase for high risk recommendedGas := new(big.Float).SetInt(gasPrice) recommendedGas.Mul(recommendedGas, big.NewFloat(gasMultiplier)) result := new(big.Int) recommendedGas.Int(result) return result } // calculateSlippageLimit calculates slippage limit based on risk func (rm *RiskManager) calculateSlippageLimit(riskScore float64) float64 { // For high-risk opportunities, allow less slippage // For low-risk opportunities, can tolerate more slippage slippageLimit := rm.maxSlippage * (1.0 - riskScore*0.5) // Reduce by up to 50% for high risk return slippageLimit } // isAcceptable determines if an opportunity is acceptable based on risk criteria func (rm *RiskManager) isAcceptable(riskScore, profitability, confidence float64) bool { // Must pass all individual criteria AND overall risk threshold if riskScore > 0.7 { return false // Too risky regardless of other factors } if profitability < 1.0 { return false // Less than 1% ROI } if confidence < 0.3 { return false // Low confidence } // Overall weighted score weightedScore := (riskScore * 0.4) + ((100 - profitability) * 0.3) + ((1 - confidence) * 0.3) return weightedScore < 0.5 } // max returns the larger of two integers func max(a, b uint64) uint64 { if a > b { return a } return b } // min returns the smaller of two integers func min(a, b uint64) uint64 { if a < b { return a } return b } // formatEther formats a big.Int wei amount as ETH string func formatEther(wei *big.Int) string { if wei == nil { return "0" } ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18)) result, _ := ether.Float64() return fmt.Sprintf("%.6f", result) } // formatGwei formats a big.Int wei amount as Gwei string func formatGwei(wei *big.Int) string { if wei == nil { return "0" } gwei := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e9)) result, _ := gwei.Float64() return fmt.Sprintf("%.2f", result) } // IsOpen checks if the circuit breaker is open func (cb *CircuitBreaker) IsOpen() bool { cb.mu.RLock() defer cb.mu.RUnlock() return cb.open } // RecordFailure records a failure in the circuit breaker func (cb *CircuitBreaker) RecordFailure() { cb.mu.Lock() defer cb.mu.Unlock() cb.failures++ cb.lastFailure = time.Now() if cb.failures >= cb.failureThreshold { cb.open = true } } // RecordSuccess records a success in the circuit breaker and potentially closes it func (cb *CircuitBreaker) RecordSuccess() { cb.mu.Lock() defer cb.mu.Unlock() // Reset failures if enough time has passed if time.Since(cb.lastFailure) > cb.resetTimeout { cb.failures = 0 cb.open = false } }