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:
539
orig/pkg/risk/manager.go
Normal file
539
orig/pkg/risk/manager.go
Normal file
@@ -0,0 +1,539 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
83
orig/pkg/risk/manager_test.go
Normal file
83
orig/pkg/risk/manager_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package risk
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
)
|
||||
|
||||
func TestAssessOpportunityProvidesDecimalSnapshots(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
expectedProfit := big.NewInt(20000000000000000) // 0.02 ETH
|
||||
gasCost := big.NewInt(500000000000000) // 0.0005 ETH
|
||||
gasPrice := big.NewInt(3000000000) // 3 gwei
|
||||
|
||||
assessment := rm.AssessOpportunity("op-1", expectedProfit, gasCost, 0.005, gasPrice)
|
||||
if assessment.MaxPositionSizeDecimal == nil {
|
||||
t.Fatalf("expected max position size decimal snapshot to be populated")
|
||||
}
|
||||
if assessment.RecommendedGasDecimal == nil {
|
||||
t.Fatalf("expected gas decimal snapshot to be populated")
|
||||
}
|
||||
|
||||
expectedMax := rm.fromWei(assessment.MaxPositionSize, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.MaxPositionSizeDecimal, expectedMax); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected position decimal %s to match %s", rm.decimalConverter.ToHumanReadable(assessment.MaxPositionSizeDecimal), rm.decimalConverter.ToHumanReadable(expectedMax))
|
||||
}
|
||||
|
||||
expectedGas := rm.fromWei(assessment.RecommendedGas, "GWEI")
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.RecommendedGasDecimal, expectedGas); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected gas decimal %s to match %s", rm.decimalConverter.ToHumanReadable(assessment.RecommendedGasDecimal), rm.decimalConverter.ToHumanReadable(expectedGas))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssessOpportunityRejectsBelowMinimumProfit(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
belowThreshold := big.NewInt(5000000000000000) // 0.005 ETH < 0.01 ETH
|
||||
gasCost := big.NewInt(100000000000000) // 0.0001 ETH
|
||||
gasPrice := big.NewInt(1500000000) // 1.5 gwei
|
||||
|
||||
assessment := rm.AssessOpportunity("op-2", belowThreshold, gasCost, 0.001, gasPrice)
|
||||
if assessment.Acceptable {
|
||||
t.Fatalf("expected opportunity below profit threshold to be rejected")
|
||||
}
|
||||
if !strings.Contains(assessment.Reason, "Profit below minimum threshold") {
|
||||
t.Fatalf("unexpected rejection reason: %s", assessment.Reason)
|
||||
}
|
||||
|
||||
thresholdDecimal := rm.minProfitThresholdDecimal
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.MaxPositionSizeDecimal, rm.maxPositionSizeDecimal); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected max position decimal to remain default when rejected")
|
||||
}
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.minProfitThresholdDecimal, thresholdDecimal); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected threshold decimal to remain unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordTradeTracksDecimalTotals(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
profit := big.NewInt(20000000000000000) // 0.02 ETH
|
||||
gasCost := big.NewInt(5000000000000000) // 0.005 ETH
|
||||
|
||||
rm.RecordTrade(true, profit, gasCost)
|
||||
|
||||
expectedProfit, _ := math.NewUniversalDecimal(profit, 18, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.totalProfitDecimal, expectedProfit); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected total profit decimal %s to equal %s", rm.decimalConverter.ToHumanReadable(rm.totalProfitDecimal), rm.decimalConverter.ToHumanReadable(expectedProfit))
|
||||
}
|
||||
|
||||
rm.RecordTrade(false, nil, gasCost)
|
||||
expectedLoss, _ := math.NewUniversalDecimal(gasCost, 18, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.totalLossDecimal, expectedLoss); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected total loss decimal %s to equal %s", rm.decimalConverter.ToHumanReadable(rm.totalLossDecimal), rm.decimalConverter.ToHumanReadable(expectedLoss))
|
||||
}
|
||||
}
|
||||
232
orig/pkg/risk/profit_tiers.go
Normal file
232
orig/pkg/risk/profit_tiers.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package risk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// ProfitTier represents a profit threshold tier with specific requirements
|
||||
type ProfitTier struct {
|
||||
Name string
|
||||
MinProfitMarginBps int64 // Minimum profit margin in basis points
|
||||
MaxProfitMarginBps int64 // Maximum profit margin in basis points (exclusive)
|
||||
MinExecutionSizeETH float64 // Minimum execution size in ETH
|
||||
MaxGasCostRatio float64 // Maximum gas cost as ratio of profit
|
||||
MaxSlippageBps int64 // Maximum slippage in basis points
|
||||
RequireHighLiquidity bool // Require high liquidity pools
|
||||
Description string
|
||||
}
|
||||
|
||||
// ProfitTierSystem manages profit validation across different tiers
|
||||
type ProfitTierSystem struct {
|
||||
logger *logger.Logger
|
||||
tiers []ProfitTier
|
||||
}
|
||||
|
||||
// NewProfitTierSystem creates a new profit tier system
|
||||
func NewProfitTierSystem(logger *logger.Logger) *ProfitTierSystem {
|
||||
return &ProfitTierSystem{
|
||||
logger: logger,
|
||||
tiers: []ProfitTier{
|
||||
{
|
||||
Name: "Ultra High Margin",
|
||||
MinProfitMarginBps: 1000, // 10%+
|
||||
MaxProfitMarginBps: 100000,
|
||||
MinExecutionSizeETH: 0.05,
|
||||
MaxGasCostRatio: 0.3,
|
||||
MaxSlippageBps: 200, // 2%
|
||||
RequireHighLiquidity: false,
|
||||
Description: "Rare high-margin opportunities (10%+) - Minimum 0.05 ETH execution",
|
||||
},
|
||||
{
|
||||
Name: "High Margin",
|
||||
MinProfitMarginBps: 500, // 5-10%
|
||||
MaxProfitMarginBps: 1000,
|
||||
MinExecutionSizeETH: 0.1,
|
||||
MaxGasCostRatio: 0.4,
|
||||
MaxSlippageBps: 150, // 1.5%
|
||||
RequireHighLiquidity: false,
|
||||
Description: "High-margin opportunities (5-10%) - Minimum 0.1 ETH execution",
|
||||
},
|
||||
{
|
||||
Name: "Medium Margin",
|
||||
MinProfitMarginBps: 200, // 2-5%
|
||||
MaxProfitMarginBps: 500,
|
||||
MinExecutionSizeETH: 0.5,
|
||||
MaxGasCostRatio: 0.35,
|
||||
MaxSlippageBps: 100, // 1%
|
||||
RequireHighLiquidity: true,
|
||||
Description: "Medium-margin opportunities (2-5%) - Minimum 0.5 ETH execution, high liquidity required",
|
||||
},
|
||||
{
|
||||
Name: "Standard Margin",
|
||||
MinProfitMarginBps: 100, // 1-2%
|
||||
MaxProfitMarginBps: 200,
|
||||
MinExecutionSizeETH: 1.0,
|
||||
MaxGasCostRatio: 0.25,
|
||||
MaxSlippageBps: 75, // 0.75%
|
||||
RequireHighLiquidity: true,
|
||||
Description: "Standard-margin opportunities (1-2%) - Minimum 1 ETH execution, high liquidity required",
|
||||
},
|
||||
{
|
||||
Name: "Low Margin",
|
||||
MinProfitMarginBps: 50, // 0.5-1%
|
||||
MaxProfitMarginBps: 100,
|
||||
MinExecutionSizeETH: 2.0,
|
||||
MaxGasCostRatio: 0.15,
|
||||
MaxSlippageBps: 50, // 0.5%
|
||||
RequireHighLiquidity: true,
|
||||
Description: "Low-margin opportunities (0.5-1%) - Minimum 2 ETH execution, high liquidity required, strict gas limits",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOpportunity validates an arbitrage opportunity against tier requirements
|
||||
func (pts *ProfitTierSystem) ValidateOpportunity(
|
||||
profitMarginBps int64,
|
||||
executionSizeETH float64,
|
||||
gasCostRatio float64,
|
||||
slippageBps int64,
|
||||
hasHighLiquidity bool,
|
||||
) (*ValidationResult, error) {
|
||||
|
||||
// Find applicable tier
|
||||
tier := pts.findTier(profitMarginBps)
|
||||
if tier == nil {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Tier: nil,
|
||||
FailureReason: fmt.Sprintf("Profit margin %d bps is below minimum threshold (50 bps / 0.5%%)", profitMarginBps),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate execution size
|
||||
if executionSizeETH < tier.MinExecutionSizeETH {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Tier: tier,
|
||||
FailureReason: fmt.Sprintf("Execution size %.4f ETH is below tier minimum %.2f ETH for %s tier", executionSizeETH, tier.MinExecutionSizeETH, tier.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate gas cost ratio
|
||||
if gasCostRatio > tier.MaxGasCostRatio {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Tier: tier,
|
||||
FailureReason: fmt.Sprintf("Gas cost ratio %.2f%% exceeds tier maximum %.2f%% for %s tier", gasCostRatio*100, tier.MaxGasCostRatio*100, tier.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate slippage
|
||||
if slippageBps > tier.MaxSlippageBps {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Tier: tier,
|
||||
FailureReason: fmt.Sprintf("Slippage %d bps exceeds tier maximum %d bps for %s tier", slippageBps, tier.MaxSlippageBps, tier.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate liquidity requirement
|
||||
if tier.RequireHighLiquidity && !hasHighLiquidity {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Tier: tier,
|
||||
FailureReason: fmt.Sprintf("High liquidity required for %s tier but not available", tier.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// All checks passed
|
||||
return &ValidationResult{
|
||||
IsValid: true,
|
||||
Tier: tier,
|
||||
FailureReason: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// findTier finds the appropriate tier for a given profit margin
|
||||
func (pts *ProfitTierSystem) findTier(profitMarginBps int64) *ProfitTier {
|
||||
for i := range pts.tiers {
|
||||
tier := &pts.tiers[i]
|
||||
if profitMarginBps >= tier.MinProfitMarginBps && profitMarginBps < tier.MaxProfitMarginBps {
|
||||
return tier
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTierForMargin returns the tier for a specific profit margin
|
||||
func (pts *ProfitTierSystem) GetTierForMargin(profitMarginBps int64) *ProfitTier {
|
||||
return pts.findTier(profitMarginBps)
|
||||
}
|
||||
|
||||
// GetAllTiers returns all defined tiers
|
||||
func (pts *ProfitTierSystem) GetAllTiers() []ProfitTier {
|
||||
return pts.tiers
|
||||
}
|
||||
|
||||
// CalculateProfitMarginBps calculates profit margin in basis points
|
||||
func CalculateProfitMarginBps(profit, revenue *big.Float) int64 {
|
||||
if revenue.Cmp(big.NewFloat(0)) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// margin = (profit / revenue) * 10000
|
||||
margin := new(big.Float).Quo(profit, revenue)
|
||||
margin.Mul(margin, big.NewFloat(10000))
|
||||
|
||||
marginInt, _ := margin.Int64()
|
||||
return marginInt
|
||||
}
|
||||
|
||||
// CalculateGasCostRatio calculates gas cost as a ratio of profit
|
||||
func CalculateGasCostRatio(gasCost, profit *big.Float) float64 {
|
||||
if profit.Cmp(big.NewFloat(0)) == 0 {
|
||||
return 1.0 // 100% if no profit
|
||||
}
|
||||
|
||||
ratio := new(big.Float).Quo(gasCost, profit)
|
||||
ratioFloat, _ := ratio.Float64()
|
||||
|
||||
return ratioFloat
|
||||
}
|
||||
|
||||
// ValidationResult contains the result of tier validation
|
||||
type ValidationResult struct {
|
||||
IsValid bool
|
||||
Tier *ProfitTier
|
||||
FailureReason string
|
||||
}
|
||||
|
||||
// EstimateMinExecutionSize estimates minimum execution size for a profit margin
|
||||
func (pts *ProfitTierSystem) EstimateMinExecutionSize(profitMarginBps int64) float64 {
|
||||
tier := pts.findTier(profitMarginBps)
|
||||
if tier == nil {
|
||||
// Default to highest requirement if below minimum
|
||||
return 2.0
|
||||
}
|
||||
return tier.MinExecutionSizeETH
|
||||
}
|
||||
|
||||
// GetTierSummary returns a summary of all tiers for logging
|
||||
func (pts *ProfitTierSystem) GetTierSummary() string {
|
||||
summary := "Profit Tier System Configuration:\n"
|
||||
for i, tier := range pts.tiers {
|
||||
summary += fmt.Sprintf(" Tier %d: %s\n", i+1, tier.Name)
|
||||
summary += fmt.Sprintf(" Margin: %.2f%% - %.2f%%\n", float64(tier.MinProfitMarginBps)/100, float64(tier.MaxProfitMarginBps)/100)
|
||||
summary += fmt.Sprintf(" Min Size: %.2f ETH\n", tier.MinExecutionSizeETH)
|
||||
summary += fmt.Sprintf(" Max Gas Ratio: %.1f%%\n", tier.MaxGasCostRatio*100)
|
||||
summary += fmt.Sprintf(" Max Slippage: %.2f%%\n", float64(tier.MaxSlippageBps)/100)
|
||||
summary += fmt.Sprintf(" High Liquidity Required: %v\n", tier.RequireHighLiquidity)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// IsHighLiquidity determines if a pool has high liquidity
|
||||
func IsHighLiquidity(liquidityETH float64) bool {
|
||||
// Threshold: 50 ETH+ liquidity is considered high
|
||||
return liquidityETH >= 50.0
|
||||
}
|
||||
481
orig/pkg/risk/profit_validator.go
Normal file
481
orig/pkg/risk/profit_validator.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package risk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// ProfitValidator validates profitability of MEV opportunities
|
||||
type ProfitValidator struct {
|
||||
logger *logger.Logger
|
||||
mu sync.RWMutex
|
||||
|
||||
// Profit thresholds
|
||||
minProfitUSD float64 // Minimum profit in USD
|
||||
minProfitETH *big.Int // Minimum profit in ETH
|
||||
minProfitMargin float64 // Minimum profit margin percentage
|
||||
maxSlippage float64 // Maximum acceptable slippage
|
||||
maxGasPrice *big.Int // Maximum gas price willing to pay
|
||||
|
||||
// Historical performance tracking
|
||||
totalOpportunities uint64
|
||||
profitableOps uint64
|
||||
unprofitableOps uint64
|
||||
totalProfitETH *big.Int
|
||||
totalGasCostETH *big.Int
|
||||
averageGasCost *big.Int
|
||||
|
||||
// Performance metrics
|
||||
validationSuccessRate float64
|
||||
averageProfitMargin float64
|
||||
averageSlippage float64
|
||||
|
||||
// Token price tracking
|
||||
tokenPrices map[common.Address]*TokenPrice
|
||||
priceMu sync.RWMutex
|
||||
|
||||
// Validation configuration
|
||||
strictValidation bool // Whether to use strict validation rules
|
||||
}
|
||||
|
||||
// TokenPrice represents real-time token pricing data
|
||||
type TokenPrice struct {
|
||||
Address common.Address
|
||||
PriceUSD float64
|
||||
LastUpdated time.Time
|
||||
Confidence float64 // Price confidence 0-1
|
||||
Volume24h float64
|
||||
Volatility float64
|
||||
}
|
||||
|
||||
// ProfitValidationResult represents the result of a profit validation
|
||||
type ProfitValidationResult struct {
|
||||
OpportunityID string
|
||||
Valid bool
|
||||
Reason string
|
||||
ExpectedProfitETH *big.Int
|
||||
ExpectedProfitUSD float64
|
||||
GasCostETH *big.Int
|
||||
GasCostUSD float64
|
||||
NetProfitETH *big.Int
|
||||
NetProfitUSD float64
|
||||
ProfitMargin float64
|
||||
Slippage float64
|
||||
GasPrice *big.Int
|
||||
Acceptable bool
|
||||
Recommendation string
|
||||
Confidence float64
|
||||
RiskScore float64
|
||||
ValidationTime time.Duration
|
||||
}
|
||||
|
||||
// NewProfitValidator creates a new profit validator
|
||||
func NewProfitValidator(logger *logger.Logger) *ProfitValidator {
|
||||
return &ProfitValidator{
|
||||
logger: logger,
|
||||
minProfitUSD: 5.0, // $5 minimum profit
|
||||
minProfitETH: big.NewInt(10000000000000000), // 0.01 ETH minimum profit
|
||||
minProfitMargin: 0.005, // 0.5% minimum margin
|
||||
maxSlippage: 0.01, // 1% maximum slippage
|
||||
maxGasPrice: big.NewInt(20000000000), // 20 gwei max gas price
|
||||
totalOpportunities: 0,
|
||||
profitableOps: 0,
|
||||
unprofitableOps: 0,
|
||||
totalProfitETH: big.NewInt(0),
|
||||
totalGasCostETH: big.NewInt(0),
|
||||
averageGasCost: big.NewInt(0),
|
||||
validationSuccessRate: 0.0,
|
||||
averageProfitMargin: 0.0,
|
||||
averageSlippage: 0.0,
|
||||
tokenPrices: make(map[common.Address]*TokenPrice),
|
||||
strictValidation: true, // Default to strict validation
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateProfit validates the profitability of an MEV opportunity
|
||||
func (pv *ProfitValidator) ValidateProfit(opportunityID string, expectedProfitETH *big.Int, gasCostETH *big.Int, slippage float64, gasPrice *big.Int) *ProfitValidationResult {
|
||||
startTime := time.Now()
|
||||
|
||||
pv.mu.Lock()
|
||||
pv.totalOpportunities++
|
||||
pv.mu.Unlock()
|
||||
|
||||
result := &ProfitValidationResult{
|
||||
OpportunityID: opportunityID,
|
||||
Valid: false,
|
||||
Reason: "",
|
||||
ExpectedProfitETH: expectedProfitETH,
|
||||
ExpectedProfitUSD: 0.0,
|
||||
GasCostETH: gasCostETH,
|
||||
GasCostUSD: 0.0,
|
||||
NetProfitETH: big.NewInt(0),
|
||||
NetProfitUSD: 0.0,
|
||||
ProfitMargin: 0.0,
|
||||
Slippage: slippage,
|
||||
GasPrice: gasPrice,
|
||||
Acceptable: false,
|
||||
Recommendation: "",
|
||||
Confidence: 0.0,
|
||||
RiskScore: 0.0,
|
||||
ValidationTime: 0,
|
||||
}
|
||||
|
||||
// Calculate USD values using token prices
|
||||
expectedProfitUSD := pv.convertETHToUSD(expectedProfitETH)
|
||||
gasCostUSD := pv.convertETHToUSD(gasCostETH)
|
||||
|
||||
result.ExpectedProfitUSD = expectedProfitUSD
|
||||
result.GasCostUSD = gasCostUSD
|
||||
|
||||
// Calculate net profit
|
||||
netProfitETH := new(big.Int).Sub(expectedProfitETH, gasCostETH)
|
||||
if netProfitETH.Sign() < 0 {
|
||||
netProfitETH = big.NewInt(0)
|
||||
}
|
||||
|
||||
result.NetProfitETH = netProfitETH
|
||||
result.NetProfitUSD = pv.convertETHToUSD(netProfitETH)
|
||||
|
||||
// Calculate profit margin
|
||||
if expectedProfitETH.Sign() > 0 {
|
||||
margin := new(big.Float).Quo(new(big.Float).SetInt(netProfitETH), new(big.Float).SetInt(expectedProfitETH))
|
||||
marginFloat, _ := margin.Float64()
|
||||
result.ProfitMargin = marginFloat * 100 // Convert to percentage
|
||||
}
|
||||
|
||||
// Perform validation checks
|
||||
valid, reason := pv.performValidationChecks(expectedProfitETH, netProfitETH, gasCostETH, slippage, gasPrice)
|
||||
result.Valid = valid
|
||||
result.Reason = reason
|
||||
|
||||
// Determine if opportunity is acceptable
|
||||
acceptable, recommendation := pv.isAcceptable(expectedProfitETH, netProfitETH, gasCostETH, slippage, gasPrice)
|
||||
result.Acceptable = acceptable
|
||||
result.Recommendation = recommendation
|
||||
|
||||
// Calculate confidence based on validation results
|
||||
result.Confidence = pv.calculateConfidence(valid, acceptable, result.ProfitMargin, slippage)
|
||||
result.RiskScore = pv.calculateRiskScore(slippage, gasPrice)
|
||||
|
||||
// Update statistics
|
||||
pv.mu.Lock()
|
||||
if valid && acceptable {
|
||||
pv.profitableOps++
|
||||
pv.totalProfitETH.Add(pv.totalProfitETH, netProfitETH)
|
||||
pv.totalGasCostETH.Add(pv.totalGasCostETH, gasCostETH)
|
||||
} else {
|
||||
pv.unprofitableOps++
|
||||
}
|
||||
pv.updateValidationMetrics()
|
||||
pv.mu.Unlock()
|
||||
|
||||
result.ValidationTime = time.Since(startTime)
|
||||
|
||||
// Log validation result
|
||||
if result.Valid && result.Acceptable {
|
||||
pv.logger.Info(fmt.Sprintf("✅ Profit validation PASSED for %s: Net profit %s ETH ($%.2f), Margin %.2f%%",
|
||||
opportunityID, formatEther(result.NetProfitETH), result.NetProfitUSD, result.ProfitMargin))
|
||||
} else {
|
||||
pv.logger.Debug(fmt.Sprintf("❌ Profit validation FAILED for %s: %s", opportunityID, result.Reason))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// performValidationChecks performs all validation checks
|
||||
func (pv *ProfitValidator) performValidationChecks(expectedProfitETH, netProfitETH, gasCostETH *big.Int, slippage float64, gasPrice *big.Int) (bool, string) {
|
||||
// Check minimum profit in ETH
|
||||
if expectedProfitETH.Cmp(pv.minProfitETH) < 0 {
|
||||
return false, fmt.Sprintf("Expected profit %s ETH below minimum %s ETH",
|
||||
formatEther(expectedProfitETH), formatEther(pv.minProfitETH))
|
||||
}
|
||||
|
||||
// Check minimum profit in USD
|
||||
expectedProfitUSD := pv.convertETHToUSD(expectedProfitETH)
|
||||
if expectedProfitUSD < pv.minProfitUSD {
|
||||
return false, fmt.Sprintf("Expected profit $%.2f below minimum $%.2f",
|
||||
expectedProfitUSD, pv.minProfitUSD)
|
||||
}
|
||||
|
||||
// Check net profit is positive
|
||||
if netProfitETH.Sign() <= 0 {
|
||||
return false, fmt.Sprintf("Net profit %s ETH is not positive",
|
||||
formatEther(netProfitETH))
|
||||
}
|
||||
|
||||
// Check slippage tolerance
|
||||
if slippage > pv.maxSlippage {
|
||||
return false, fmt.Sprintf("Slippage %.2f%% exceeds maximum %.2f%%",
|
||||
slippage*100, pv.maxSlippage*100)
|
||||
}
|
||||
|
||||
// Check gas price limits
|
||||
if gasPrice.Cmp(pv.maxGasPrice) > 0 {
|
||||
return false, fmt.Sprintf("Gas price %s gwei exceeds maximum %s gwei",
|
||||
formatGwei(gasPrice), formatGwei(pv.maxGasPrice))
|
||||
}
|
||||
|
||||
// Check gas cost vs profit ratio
|
||||
if gasCostETH.Sign() > 0 && expectedProfitETH.Sign() > 0 {
|
||||
gasRatio := new(big.Float).Quo(new(big.Float).SetInt(gasCostETH), new(big.Float).SetInt(expectedProfitETH))
|
||||
gasRatioFloat, _ := gasRatio.Float64()
|
||||
if gasRatioFloat > 0.5 { // Gas cost > 50% of expected profit
|
||||
return false, fmt.Sprintf("Gas cost %s ETH (%.1f%%) too high relative to profit %s ETH",
|
||||
formatEther(gasCostETH), gasRatioFloat*100, formatEther(expectedProfitETH))
|
||||
}
|
||||
}
|
||||
|
||||
return true, "All validation checks passed"
|
||||
}
|
||||
|
||||
// isAcceptable determines if an opportunity is acceptable
|
||||
func (pv *ProfitValidator) isAcceptable(expectedProfitETH, netProfitETH, gasCostETH *big.Int, slippage float64, gasPrice *big.Int) (bool, string) {
|
||||
// Calculate profit margin
|
||||
var profitMargin float64
|
||||
if expectedProfitETH.Sign() > 0 {
|
||||
margin := new(big.Float).Quo(new(big.Float).SetInt(netProfitETH), new(big.Float).SetInt(expectedProfitETH))
|
||||
marginFloat, _ := margin.Float64()
|
||||
profitMargin = marginFloat * 100 // Convert to percentage
|
||||
}
|
||||
|
||||
// Check minimum profit margin
|
||||
if profitMargin < pv.minProfitMargin*100 {
|
||||
return false, fmt.Sprintf("Profit margin %.2f%% below minimum %.2f%%",
|
||||
profitMargin, pv.minProfitMargin*100)
|
||||
}
|
||||
|
||||
// For strict validation, apply additional checks
|
||||
if pv.strictValidation {
|
||||
// Check if net profit is at least 2x gas cost
|
||||
doubleGasCost := new(big.Int).Mul(gasCostETH, big.NewInt(2))
|
||||
if netProfitETH.Cmp(doubleGasCost) < 0 {
|
||||
return false, fmt.Sprintf("Net profit %s ETH not at least 2x gas cost %s ETH",
|
||||
formatEther(netProfitETH), formatEther(gasCostETH))
|
||||
}
|
||||
|
||||
// Check if profit margin is at least 2x minimum
|
||||
if profitMargin < pv.minProfitMargin*200 {
|
||||
return false, fmt.Sprintf("Profit margin %.2f%% not at least 2x minimum %.2f%%",
|
||||
profitMargin, pv.minProfitMargin*200)
|
||||
}
|
||||
}
|
||||
|
||||
return true, "Opportunity is acceptable"
|
||||
}
|
||||
|
||||
// calculateConfidence calculates confidence in the validation result
|
||||
func (pv *ProfitValidator) calculateConfidence(valid, acceptable bool, profitMargin, slippage float64) float64 {
|
||||
if !valid || !acceptable {
|
||||
return 0.1 // Low confidence for invalid opportunities
|
||||
}
|
||||
|
||||
// Start with base confidence
|
||||
confidence := 0.5
|
||||
|
||||
// Increase confidence based on profit margin
|
||||
if profitMargin > pv.minProfitMargin*300 { // 3x minimum margin
|
||||
confidence += 0.3
|
||||
} else if profitMargin > pv.minProfitMargin*200 { // 2x minimum margin
|
||||
confidence += 0.2
|
||||
} else if profitMargin > pv.minProfitMargin*150 { // 1.5x minimum margin
|
||||
confidence += 0.1
|
||||
}
|
||||
|
||||
// Increase confidence for low slippage
|
||||
if slippage < pv.maxSlippage*0.3 { // 30% of max slippage
|
||||
confidence += 0.1
|
||||
} else if slippage < pv.maxSlippage*0.5 { // 50% of max slippage
|
||||
confidence += 0.05
|
||||
}
|
||||
|
||||
// Cap at maximum confidence
|
||||
if confidence > 0.95 {
|
||||
confidence = 0.95
|
||||
}
|
||||
|
||||
return confidence
|
||||
}
|
||||
|
||||
// calculateRiskScore calculates a risk score for the opportunity
|
||||
func (pv *ProfitValidator) calculateRiskScore(slippage float64, gasPrice *big.Int) float64 {
|
||||
// Base risk (0-0.2)
|
||||
baseRisk := 0.1
|
||||
|
||||
// Slippage risk (0-0.3)
|
||||
slippageRisk := slippage / pv.maxSlippage * 0.3
|
||||
if slippageRisk > 0.3 {
|
||||
slippageRisk = 0.3
|
||||
}
|
||||
|
||||
// Gas price risk (0-0.3)
|
||||
gasRisk := 0.0
|
||||
if gasPrice != nil && pv.maxGasPrice != nil && pv.maxGasPrice.Sign() > 0 {
|
||||
gasRatio := new(big.Float).Quo(new(big.Float).SetInt(gasPrice), new(big.Float).SetInt(pv.maxGasPrice))
|
||||
gasRatioFloat, _ := gasRatio.Float64()
|
||||
gasRisk = gasRatioFloat * 0.3
|
||||
if gasRisk > 0.3 {
|
||||
gasRisk = 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total risk score
|
||||
totalRisk := baseRisk + slippageRisk + gasRisk
|
||||
if totalRisk > 1.0 {
|
||||
totalRisk = 1.0
|
||||
}
|
||||
|
||||
return totalRisk
|
||||
}
|
||||
|
||||
// updateValidationMetrics updates validation metrics
|
||||
func (pv *ProfitValidator) updateValidationMetrics() {
|
||||
if pv.totalOpportunities > 0 {
|
||||
pv.validationSuccessRate = float64(pv.profitableOps) / float64(pv.totalOpportunities)
|
||||
}
|
||||
|
||||
// Update average metrics (simplified)
|
||||
if pv.profitableOps > 0 {
|
||||
avgProfitMargin := new(big.Float).Quo(new(big.Float).SetInt(pv.totalProfitETH), big.NewFloat(float64(pv.profitableOps)))
|
||||
avgMarginFloat, _ := avgProfitMargin.Float64()
|
||||
pv.averageProfitMargin = avgMarginFloat * 100 // Convert to percentage
|
||||
}
|
||||
}
|
||||
|
||||
// convertETHToUSD converts ETH amount to USD using current token prices
|
||||
func (pv *ProfitValidator) convertETHToUSD(ethAmount *big.Int) float64 {
|
||||
if ethAmount == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Get current ETH price in USD
|
||||
ethPrice := pv.getTokenPriceUSD(common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")) // WETH
|
||||
if ethPrice == 0.0 {
|
||||
ethPrice = 2000.0 // Default to $2000 ETH
|
||||
}
|
||||
|
||||
// Convert ETH to USD
|
||||
ethFloat := new(big.Float).SetInt(ethAmount)
|
||||
ethDivisor := new(big.Float).SetFloat64(1e18) // Convert wei to ETH
|
||||
ethETH := new(big.Float).Quo(ethFloat, ethDivisor)
|
||||
|
||||
usdFloat := new(big.Float).Mul(ethETH, big.NewFloat(ethPrice))
|
||||
usdValue, _ := usdFloat.Float64()
|
||||
|
||||
return usdValue
|
||||
}
|
||||
|
||||
// getTokenPriceUSD gets the USD price of a token
|
||||
func (pv *ProfitValidator) getTokenPriceUSD(tokenAddr common.Address) float64 {
|
||||
pv.priceMu.RLock()
|
||||
defer pv.priceMu.RUnlock()
|
||||
|
||||
if price, exists := pv.tokenPrices[tokenAddr]; exists {
|
||||
// Check if price is recent (within 5 minutes)
|
||||
if time.Since(price.LastUpdated) < 5*time.Minute {
|
||||
return price.PriceUSD
|
||||
}
|
||||
}
|
||||
|
||||
// Return known prices for common tokens
|
||||
knownPrices := map[common.Address]float64{
|
||||
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 2000.0, // WETH
|
||||
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 1.0, // USDC
|
||||
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 1.0, // USDC.e
|
||||
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 1.0, // USDT
|
||||
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 43000.0, // WBTC
|
||||
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 0.75, // ARB
|
||||
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 45.0, // GMX
|
||||
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 12.0, // LINK
|
||||
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 8.0, // UNI
|
||||
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 85.0, // AAVE
|
||||
}
|
||||
|
||||
if price, exists := knownPrices[tokenAddr]; exists {
|
||||
return price
|
||||
}
|
||||
|
||||
// Default to $0 for unknown tokens
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// UpdateTokenPrice updates the price of a token
|
||||
func (pv *ProfitValidator) UpdateTokenPrice(tokenAddr common.Address, priceUSD float64, confidence float64) {
|
||||
pv.priceMu.Lock()
|
||||
defer pv.priceMu.Unlock()
|
||||
|
||||
pv.tokenPrices[tokenAddr] = &TokenPrice{
|
||||
Address: tokenAddr,
|
||||
PriceUSD: priceUSD,
|
||||
LastUpdated: time.Now(),
|
||||
Confidence: confidence,
|
||||
Volume24h: 0.0, // Would be populated from real data
|
||||
Volatility: 0.0, // Would be populated from real data
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatistics returns validation statistics
|
||||
func (pv *ProfitValidator) GetStatistics() map[string]interface{} {
|
||||
pv.mu.RLock()
|
||||
defer pv.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_opportunities": pv.totalOpportunities,
|
||||
"profitable_opportunities": pv.profitableOps,
|
||||
"unprofitable_opportunities": pv.unprofitableOps,
|
||||
"validation_success_rate": pv.validationSuccessRate * 100, // Convert to percentage
|
||||
"average_profit_margin": pv.averageProfitMargin,
|
||||
"average_slippage": pv.averageSlippage * 100, // Convert to percentage
|
||||
"total_profit_eth": formatEther(pv.totalProfitETH),
|
||||
"total_gas_cost_eth": formatEther(pv.totalGasCostETH),
|
||||
"tracked_tokens": len(pv.tokenPrices),
|
||||
"strict_validation": pv.strictValidation,
|
||||
}
|
||||
}
|
||||
|
||||
// SetStrictValidation enables or disables strict validation
|
||||
func (pv *ProfitValidator) SetStrictValidation(strict bool) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.strictValidation = strict
|
||||
}
|
||||
|
||||
// SetMinProfitUSD sets the minimum profit threshold in USD
|
||||
func (pv *ProfitValidator) SetMinProfitUSD(minProfit float64) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.minProfitUSD = minProfit
|
||||
}
|
||||
|
||||
// SetMinProfitETH sets the minimum profit threshold in ETH
|
||||
func (pv *ProfitValidator) SetMinProfitETH(minProfit *big.Int) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.minProfitETH = minProfit
|
||||
}
|
||||
|
||||
// SetMinProfitMargin sets the minimum profit margin percentage
|
||||
func (pv *ProfitValidator) SetMinProfitMargin(minMargin float64) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.minProfitMargin = minMargin
|
||||
}
|
||||
|
||||
// SetMaxSlippage sets the maximum acceptable slippage
|
||||
func (pv *ProfitValidator) SetMaxSlippage(maxSlippage float64) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.maxSlippage = maxSlippage
|
||||
}
|
||||
|
||||
// SetMaxGasPrice sets the maximum gas price willing to pay
|
||||
func (pv *ProfitValidator) SetMaxGasPrice(maxGasPrice *big.Int) {
|
||||
pv.mu.Lock()
|
||||
defer pv.mu.Unlock()
|
||||
pv.maxGasPrice = maxGasPrice
|
||||
}
|
||||
Reference in New Issue
Block a user