package internal import ( "context" "encoding/json" "fmt" "log" "math/big" "os" "path/filepath" "sort" "strings" "time" "github.com/fraktal/mev-beta/pkg/arbitrage" "github.com/fraktal/mev-beta/pkg/math" ) type ValidatorConfig struct { Exchanges string MinProfitBP float64 MaxSlippage float64 OutputDir string Verbose bool DryRun bool TestMode bool } type OpportunityValidator struct { config *ValidatorConfig supportedExchanges []string detectionEngine *arbitrage.DetectionEngine calculator *math.ArbitrageCalculator results *ValidationResults } type ValidationResults struct { TotalOpportunities int `json:"total_opportunities"` ValidOpportunities int `json:"valid_opportunities"` InvalidOpportunities int `json:"invalid_opportunities"` AverageProfitBP float64 `json:"average_profit_bp"` MaxProfitBP float64 `json:"max_profit_bp"` OpportunityDetails []OpportunityResult `json:"opportunity_details"` ExchangeBreakdown map[string]int `json:"exchange_breakdown"` ValidationErrors []ValidationError `json:"validation_errors"` Timestamp time.Time `json:"timestamp"` DurationMs int64 `json:"duration_ms"` } type OpportunityResult struct { ID string `json:"id"` Exchange1 string `json:"exchange1"` Exchange2 string `json:"exchange2"` TokenA string `json:"token_a"` TokenB string `json:"token_b"` ProfitBP float64 `json:"profit_bp"` SlippageBP float64 `json:"slippage_bp"` GasCostETH float64 `json:"gas_cost_eth"` NetProfitBP float64 `json:"net_profit_bp"` ExecutionTime time.Time `json:"execution_time"` Valid bool `json:"valid"` ValidationNotes []string `json:"validation_notes"` } type ValidationError struct { Type string `json:"type"` Message string `json:"message"` Exchange string `json:"exchange,omitempty"` TokenPair string `json:"token_pair,omitempty"` Timestamp time.Time `json:"timestamp"` } func NewOpportunityValidator(config *ValidatorConfig) (*OpportunityValidator, error) { // Parse supported exchanges exchanges := strings.Split(config.Exchanges, ",") for i, exchange := range exchanges { exchanges[i] = strings.TrimSpace(exchange) } // Initialize detection engine detectionEngine, err := arbitrage.NewDetectionEngine(&arbitrage.DetectionEngineConfig{ MinProfitBasisPoints: config.MinProfitBP, MaxSlippageBasisPoints: config.MaxSlippage, EnabledExchanges: exchanges, }) if err != nil { return nil, fmt.Errorf("failed to initialize detection engine: %w", err) } // Initialize arbitrage calculator calculator := math.NewArbitrageCalculator() return &OpportunityValidator{ config: config, supportedExchanges: exchanges, detectionEngine: detectionEngine, calculator: calculator, results: &ValidationResults{ OpportunityDetails: make([]OpportunityResult, 0), ExchangeBreakdown: make(map[string]int), ValidationErrors: make([]ValidationError, 0), Timestamp: time.Now(), }, }, nil } func (ov *OpportunityValidator) ValidateOpportunities(ctx context.Context) error { startTime := time.Now() defer func() { ov.results.DurationMs = time.Since(startTime).Milliseconds() }() if ov.config.TestMode { return ov.validateTestOpportunities(ctx) } return ov.validateRealOpportunities(ctx) } func (ov *OpportunityValidator) validateTestOpportunities(ctx context.Context) error { // Create simulated test opportunities testOpportunities := ov.generateTestOpportunities() for _, opportunity := range testOpportunities { select { case <-ctx.Done(): return ctx.Err() default: result := ov.validateSingleOpportunity(opportunity) ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result) ov.updateStatistics(result) if ov.config.Verbose { fmt.Printf("Validated opportunity %s: Valid=%t, Profit=%.2f bp\n", result.ID, result.Valid, result.ProfitBP) } } } return nil } func (ov *OpportunityValidator) validateRealOpportunities(ctx context.Context) error { // In real mode, we would connect to live data sources // For now, implement basic validation framework if ov.config.Verbose { fmt.Println("Scanning for real-time arbitrage opportunities...") } // This would integrate with the actual market scanner // For demonstration, create a few real-world scenarios realOpportunities := ov.generateRealWorldScenarios() for _, opportunity := range realOpportunities { select { case <-ctx.Done(): return ctx.Err() default: result := ov.validateSingleOpportunity(opportunity) ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result) ov.updateStatistics(result) if ov.config.Verbose { fmt.Printf("Validated real opportunity %s: Valid=%t, Profit=%.2f bp\n", result.ID, result.Valid, result.ProfitBP) } } } return nil } func (ov *OpportunityValidator) validateSingleOpportunity(opportunity *TestOpportunity) OpportunityResult { result := OpportunityResult{ ID: opportunity.ID, Exchange1: opportunity.Exchange1, Exchange2: opportunity.Exchange2, TokenA: opportunity.TokenA, TokenB: opportunity.TokenB, ExecutionTime: time.Now(), ValidationNotes: make([]string, 0), } // Calculate profit using arbitrage calculator profitCalculation, err := ov.calculateProfit(opportunity) if err != nil { ov.addValidationError("profit_calculation", err.Error(), opportunity.Exchange1, fmt.Sprintf("%s/%s", opportunity.TokenA, opportunity.TokenB)) result.Valid = false result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Profit calculation failed: %v", err)) return result } result.ProfitBP = profitCalculation.ProfitBP result.SlippageBP = profitCalculation.SlippageBP result.GasCostETH = profitCalculation.GasCostETH result.NetProfitBP = profitCalculation.NetProfitBP // Validate profit threshold if result.ProfitBP < ov.config.MinProfitBP { result.Valid = false result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Profit %.2f bp below minimum threshold %.2f bp", result.ProfitBP, ov.config.MinProfitBP)) } // Validate slippage threshold if result.SlippageBP > ov.config.MaxSlippage { result.Valid = false result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Slippage %.2f bp exceeds maximum %.2f bp", result.SlippageBP, ov.config.MaxSlippage)) } // Validate net profit after gas costs if result.NetProfitBP <= 0 { result.Valid = false result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Net profit %.2f bp not profitable after gas costs", result.NetProfitBP)) } // Additional exchange-specific validations if err := ov.validateExchangeSpecific(opportunity); err != nil { result.Valid = false result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Exchange validation failed: %v", err)) } // If no validation errors, mark as valid if len(result.ValidationNotes) == 0 { result.Valid = true result.ValidationNotes = append(result.ValidationNotes, "All validations passed") } return result } type TestOpportunity struct { ID string Exchange1 string Exchange2 string TokenA string TokenB string Price1 *big.Float Price2 *big.Float Liquidity *big.Float GasLimit uint64 } type ProfitCalculation struct { ProfitBP float64 SlippageBP float64 GasCostETH float64 NetProfitBP float64 } func (ov *OpportunityValidator) calculateProfit(opportunity *TestOpportunity) (*ProfitCalculation, error) { // Convert prices to UniversalDecimal for precise calculations price1UD, err := math.NewUniversalDecimal(opportunity.Price1, 18) if err != nil { return nil, fmt.Errorf("failed to convert price1: %w", err) } price2UD, err := math.NewUniversalDecimal(opportunity.Price2, 18) if err != nil { return nil, fmt.Errorf("failed to convert price2: %w", err) } // Calculate profit basis points priceDiff := price2UD.Sub(price1UD) profitRatio := priceDiff.Div(price1UD) profitBP := profitRatio.Mul(math.NewUniversalDecimalFromFloat(10000, 18)).Float64() // Estimate slippage based on liquidity liquidityUD, err := math.NewUniversalDecimal(opportunity.Liquidity, 18) if err != nil { return nil, fmt.Errorf("failed to convert liquidity: %w", err) } // Simple slippage model: inversely proportional to liquidity baseSlippage := 10.0 // 10 bp base slippage liquidityFactor := liquidityUD.Float64() / 1000000.0 // Normalize to millions slippageBP := baseSlippage / (1.0 + liquidityFactor) // Estimate gas cost (simplified) gasPriceGwei := 0.5 // 0.5 gwei on Arbitrum gasCostETH := float64(opportunity.GasLimit) * gasPriceGwei * 1e-9 // Convert gas cost to basis points (assuming 1 ETH trade size) gasCostBP := gasCostETH * 10000.0 // Calculate net profit netProfitBP := profitBP - slippageBP - gasCostBP return &ProfitCalculation{ ProfitBP: profitBP, SlippageBP: slippageBP, GasCostETH: gasCostETH, NetProfitBP: netProfitBP, }, nil } func (ov *OpportunityValidator) validateExchangeSpecific(opportunity *TestOpportunity) error { // Validate exchange-specific constraints switch opportunity.Exchange1 { case "uniswap_v2": return ov.validateUniswapV2(opportunity) case "uniswap_v3": return ov.validateUniswapV3(opportunity) case "curve": return ov.validateCurve(opportunity) case "balancer": return ov.validateBalancer(opportunity) default: return fmt.Errorf("unsupported exchange: %s", opportunity.Exchange1) } } func (ov *OpportunityValidator) validateUniswapV2(opportunity *TestOpportunity) error { // Uniswap V2 specific validations if opportunity.Price1.Cmp(big.NewFloat(0)) <= 0 { return fmt.Errorf("invalid price for Uniswap V2") } return nil } func (ov *OpportunityValidator) validateUniswapV3(opportunity *TestOpportunity) error { // Uniswap V3 specific validations if opportunity.Liquidity.Cmp(big.NewFloat(0)) <= 0 { return fmt.Errorf("insufficient liquidity for Uniswap V3") } return nil } func (ov *OpportunityValidator) validateCurve(opportunity *TestOpportunity) error { // Curve specific validations return nil } func (ov *OpportunityValidator) validateBalancer(opportunity *TestOpportunity) error { // Balancer specific validations return nil } func (ov *OpportunityValidator) generateTestOpportunities() []*TestOpportunity { opportunities := make([]*TestOpportunity, 0) // ETH/USDC opportunities across different exchanges opportunities = append(opportunities, &TestOpportunity{ ID: "test_eth_usdc_uniswap_v2_v3", Exchange1: "uniswap_v2", Exchange2: "uniswap_v3", TokenA: "ETH", TokenB: "USDC", Price1: big.NewFloat(2000.0), Price2: big.NewFloat(2002.5), Liquidity: big.NewFloat(5000000.0), GasLimit: 150000, }) opportunities = append(opportunities, &TestOpportunity{ ID: "test_eth_usdc_curve_balancer", Exchange1: "curve", Exchange2: "balancer", TokenA: "ETH", TokenB: "USDC", Price1: big.NewFloat(2001.0), Price2: big.NewFloat(2004.0), Liquidity: big.NewFloat(10000000.0), GasLimit: 180000, }) // WBTC/ETH opportunities opportunities = append(opportunities, &TestOpportunity{ ID: "test_wbtc_eth_uniswap", Exchange1: "uniswap_v2", Exchange2: "uniswap_v3", TokenA: "WBTC", TokenB: "ETH", Price1: big.NewFloat(15.5), Price2: big.NewFloat(15.52), Liquidity: big.NewFloat(2000000.0), GasLimit: 200000, }) // Low profit opportunity (should fail validation) opportunities = append(opportunities, &TestOpportunity{ ID: "test_low_profit", Exchange1: "uniswap_v2", Exchange2: "curve", TokenA: "USDC", TokenB: "USDT", Price1: big.NewFloat(1.0), Price2: big.NewFloat(1.0005), Liquidity: big.NewFloat(1000000.0), GasLimit: 120000, }) // High slippage opportunity (should fail validation) opportunities = append(opportunities, &TestOpportunity{ ID: "test_high_slippage", Exchange1: "uniswap_v3", Exchange2: "balancer", TokenA: "ETH", TokenB: "RARE_TOKEN", Price1: big.NewFloat(100.0), Price2: big.NewFloat(105.0), Liquidity: big.NewFloat(10000.0), // Low liquidity GasLimit: 250000, }) return opportunities } func (ov *OpportunityValidator) generateRealWorldScenarios() []*TestOpportunity { // Generate more realistic scenarios based on actual market conditions return []*TestOpportunity{ { ID: "real_eth_usdc_arbitrum", Exchange1: "uniswap_v3", Exchange2: "curve", TokenA: "ETH", TokenB: "USDC", Price1: big.NewFloat(2456.78), Price2: big.NewFloat(2459.12), Liquidity: big.NewFloat(8500000.0), GasLimit: 165000, }, { ID: "real_wbtc_eth_arbitrum", Exchange1: "balancer", Exchange2: "uniswap_v2", TokenA: "WBTC", TokenB: "ETH", Price1: big.NewFloat(16.234), Price2: big.NewFloat(16.251), Liquidity: big.NewFloat(3200000.0), GasLimit: 195000, }, } } func (ov *OpportunityValidator) updateStatistics(result OpportunityResult) { ov.results.TotalOpportunities++ if result.Valid { ov.results.ValidOpportunities++ } else { ov.results.InvalidOpportunities++ } // Update exchange breakdown ov.results.ExchangeBreakdown[result.Exchange1]++ ov.results.ExchangeBreakdown[result.Exchange2]++ // Update profit statistics if result.Valid && result.ProfitBP > 0 { if ov.results.MaxProfitBP < result.ProfitBP { ov.results.MaxProfitBP = result.ProfitBP } // Calculate running average totalProfit := ov.results.AverageProfitBP * float64(ov.results.ValidOpportunities-1) ov.results.AverageProfitBP = (totalProfit + result.ProfitBP) / float64(ov.results.ValidOpportunities) } } func (ov *OpportunityValidator) addValidationError(errorType, message, exchange, tokenPair string) { ov.results.ValidationErrors = append(ov.results.ValidationErrors, ValidationError{ Type: errorType, Message: message, Exchange: exchange, TokenPair: tokenPair, Timestamp: time.Now(), }) } func (ov *OpportunityValidator) MonitorOpportunities(ctx context.Context) error { if ov.config.Verbose { fmt.Println("Starting real-time opportunity monitoring...") } ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: // Reset results for new monitoring cycle ov.results = &ValidationResults{ OpportunityDetails: make([]OpportunityResult, 0), ExchangeBreakdown: make(map[string]int), ValidationErrors: make([]ValidationError, 0), Timestamp: time.Now(), } if err := ov.validateRealOpportunities(ctx); err != nil { log.Printf("Error during opportunity validation: %v", err) continue } if err := ov.GenerateReport(); err != nil { log.Printf("Error generating report: %v", err) continue } if ov.config.Verbose { fmt.Printf("Monitoring cycle complete. Found %d valid opportunities out of %d total\n", ov.results.ValidOpportunities, ov.results.TotalOpportunities) } } } } func (ov *OpportunityValidator) GenerateReport() error { // Sort opportunities by profit (descending) sort.Slice(ov.results.OpportunityDetails, func(i, j int) bool { return ov.results.OpportunityDetails[i].ProfitBP > ov.results.OpportunityDetails[j].ProfitBP }) // Generate JSON report jsonReport, err := json.MarshalIndent(ov.results, "", " ") if err != nil { return fmt.Errorf("failed to marshal results: %w", err) } // Save JSON report timestamp := time.Now().Format("2006-01-02_15-04-05") jsonPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_validation_%s.json", timestamp)) if err := os.WriteFile(jsonPath, jsonReport, 0644); err != nil { return fmt.Errorf("failed to write JSON report: %w", err) } // Generate summary report summaryPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_summary_%s.txt", timestamp)) if err := ov.generateSummaryReport(summaryPath); err != nil { return fmt.Errorf("failed to generate summary report: %w", err) } if ov.config.Verbose { fmt.Printf("Reports generated:\n") fmt.Printf(" JSON: %s\n", jsonPath) fmt.Printf(" Summary: %s\n", summaryPath) } return nil } func (ov *OpportunityValidator) generateSummaryReport(filePath string) error { summary := fmt.Sprintf(`Arbitrage Opportunity Validation Report Generated: %s Duration: %d ms SUMMARY ======= Total Opportunities Analyzed: %d Valid Opportunities: %d Invalid Opportunities: %d Success Rate: %.1f%% PROFIT STATISTICS ================ Average Profit: %.2f bp Maximum Profit: %.2f bp EXCHANGE BREAKDOWN ================== `, ov.results.Timestamp.Format("2006-01-02 15:04:05"), ov.results.DurationMs, ov.results.TotalOpportunities, ov.results.ValidOpportunities, ov.results.InvalidOpportunities, float64(ov.results.ValidOpportunities)/float64(ov.results.TotalOpportunities)*100, ov.results.AverageProfitBP, ov.results.MaxProfitBP) for exchange, count := range ov.results.ExchangeBreakdown { summary += fmt.Sprintf("%s: %d opportunities\n", exchange, count) } summary += "\nTOP OPPORTUNITIES\n=================\n" for i, opp := range ov.results.OpportunityDetails { if i >= 5 { // Show top 5 break } status := "INVALID" if opp.Valid { status = "VALID" } summary += fmt.Sprintf("%d. %s (%s/%s) - %.2f bp - %s\n", i+1, opp.ID, opp.Exchange1, opp.Exchange2, opp.ProfitBP, status) } if len(ov.results.ValidationErrors) > 0 { summary += "\nVALIDATION ERRORS\n================\n" for _, err := range ov.results.ValidationErrors { summary += fmt.Sprintf("- %s: %s\n", err.Type, err.Message) } } return os.WriteFile(filePath, []byte(summary), 0644) }