Files
mev-beta/pkg/execution/alerts.go

292 lines
6.9 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}