292 lines
6.9 KiB
Go
292 lines
6.9 KiB
Go
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
|
||
}
|