package execution import ( "fmt" "math/big" "time" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/types" ) // AlertLevel defines the severity of an alert type AlertLevel int const ( InfoLevel AlertLevel = iota WarningLevel CriticalLevel ) func (al AlertLevel) String() string { switch al { case InfoLevel: return "INFO" case WarningLevel: return "WARNING" case CriticalLevel: return "CRITICAL" default: return "UNKNOWN" } } // Alert represents a system alert type Alert struct { Level AlertLevel Title string Message string Opportunity *types.ArbitrageOpportunity Timestamp time.Time } // AlertConfig holds configuration for the alert system type AlertConfig struct { EnableConsoleAlerts bool EnableFileAlerts bool EnableWebhook bool WebhookURL string MinProfitForAlert *big.Int // Minimum profit to trigger alert (wei) MinROIForAlert float64 // Minimum ROI to trigger alert (0.05 = 5%) AlertCooldown time.Duration // Minimum time between alerts } // AlertSystem handles opportunity alerts and notifications type AlertSystem struct { config *AlertConfig logger *logger.Logger lastAlertTime time.Time alertCount uint64 } // NewAlertSystem creates a new alert system func NewAlertSystem(config *AlertConfig, logger *logger.Logger) *AlertSystem { return &AlertSystem{ config: config, logger: logger, lastAlertTime: time.Time{}, alertCount: 0, } } // SendOpportunityAlert sends an alert for a profitable opportunity func (as *AlertSystem) SendOpportunityAlert(opp *types.ArbitrageOpportunity) { // Check cooldown if time.Since(as.lastAlertTime) < as.config.AlertCooldown { as.logger.Debug("Alert cooldown active, skipping alert") return } // Check minimum thresholds if opp.NetProfit.Cmp(as.config.MinProfitForAlert) < 0 { return } if opp.ROI < as.config.MinROIForAlert { return } // Determine alert level level := as.determineAlertLevel(opp) // Create alert alert := &Alert{ Level: level, Title: fmt.Sprintf("Profitable Arbitrage Opportunity Detected"), Message: as.formatOpportunityMessage(opp), Opportunity: opp, Timestamp: time.Now(), } // Send alert via configured channels as.sendAlert(alert) as.lastAlertTime = time.Now() as.alertCount++ } // SendExecutionAlert sends an alert for execution results func (as *AlertSystem) SendExecutionAlert(result *ExecutionResult) { var level AlertLevel var title string if result.Success { level = InfoLevel title = "Arbitrage Executed Successfully" } else { level = WarningLevel title = "Arbitrage Execution Failed" } alert := &Alert{ Level: level, Title: title, Message: as.formatExecutionMessage(result), Timestamp: time.Now(), } as.sendAlert(alert) } // SendSystemAlert sends a system-level alert func (as *AlertSystem) SendSystemAlert(level AlertLevel, title, message string) { alert := &Alert{ Level: level, Title: title, Message: message, Timestamp: time.Now(), } as.sendAlert(alert) } // determineAlertLevel determines the appropriate alert level func (as *AlertSystem) determineAlertLevel(opp *types.ArbitrageOpportunity) AlertLevel { // Critical if ROI > 10% or profit > 1 ETH oneETH := new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)) if opp.ROI > 0.10 || opp.NetProfit.Cmp(oneETH) > 0 { return CriticalLevel } // Warning if ROI > 5% or profit > 0.1 ETH pointOneETH := new(big.Int).Mul(big.NewInt(1), big.NewInt(1e17)) if opp.ROI > 0.05 || opp.NetProfit.Cmp(pointOneETH) > 0 { return WarningLevel } return InfoLevel } // sendAlert sends an alert via all configured channels func (as *AlertSystem) sendAlert(alert *Alert) { // Console alert if as.config.EnableConsoleAlerts { as.sendConsoleAlert(alert) } // File alert if as.config.EnableFileAlerts { as.sendFileAlert(alert) } // Webhook alert if as.config.EnableWebhook && as.config.WebhookURL != "" { as.sendWebhookAlert(alert) } } // sendConsoleAlert prints alert to console func (as *AlertSystem) sendConsoleAlert(alert *Alert) { emoji := "ā„¹ļø" switch alert.Level { case WarningLevel: emoji = "āš ļø" case CriticalLevel: emoji = "🚨" } as.logger.Info(fmt.Sprintf("%s [%s] %s", emoji, alert.Level, alert.Title)) as.logger.Info(alert.Message) } // sendFileAlert writes alert to file func (as *AlertSystem) sendFileAlert(alert *Alert) { // TODO: Implement file-based alerts // Write to logs/alerts/alert_YYYYMMDD_HHMMSS.json } // sendWebhookAlert sends alert to webhook (Slack, Discord, etc.) func (as *AlertSystem) sendWebhookAlert(alert *Alert) { // TODO: Implement webhook alerts // POST JSON to configured webhook URL as.logger.Debug(fmt.Sprintf("Would send webhook alert to: %s", as.config.WebhookURL)) } // formatOpportunityMessage formats an opportunity alert message func (as *AlertSystem) formatOpportunityMessage(opp *types.ArbitrageOpportunity) string { profitETH := new(big.Float).Quo( new(big.Float).SetInt(opp.NetProfit), big.NewFloat(1e18), ) gasEstimate := "N/A" if opp.GasEstimate != nil { gasEstimate = opp.GasEstimate.String() } return fmt.Sprintf(` šŸŽÆ Arbitrage Opportunity Details: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • ID: %s • Path: %v • Protocol: %s • Amount In: %s wei • Estimated Profit: %.6f ETH • ROI: %.2f%% • Gas Estimate: %s wei • Confidence: %.1f%% • Price Impact: %.2f%% • Expires: %s ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `, opp.ID, opp.Path, opp.Protocol, opp.AmountIn.String(), profitETH, opp.ROI*100, gasEstimate, opp.Confidence*100, opp.PriceImpact*100, opp.ExpiresAt.Format("15:04:05"), ) } // formatExecutionMessage formats an execution result message func (as *AlertSystem) formatExecutionMessage(result *ExecutionResult) string { status := "āœ… SUCCESS" if !result.Success { status = "āŒ FAILED" } profitETH := "N/A" if result.ActualProfit != nil { p := new(big.Float).Quo( new(big.Float).SetInt(result.ActualProfit), big.NewFloat(1e18), ) profitETH = fmt.Sprintf("%.6f ETH", p) } errorMsg := "" if result.Error != nil { errorMsg = fmt.Sprintf("\n• Error: %v", result.Error) } return fmt.Sprintf(` %s Arbitrage Execution ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ • Opportunity ID: %s • Tx Hash: %s • Actual Profit: %s • Gas Used: %d • Slippage: %.2f%% • Execution Time: %v%s ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `, status, result.OpportunityID, result.TxHash.Hex(), profitETH, result.GasUsed, result.SlippagePercent*100, result.ExecutionTime, errorMsg, ) } // GetAlertCount returns the total number of alerts sent func (as *AlertSystem) GetAlertCount() uint64 { return as.alertCount }