- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing - Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives - Added LRU caching system for address validation with 10-minute TTL - Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures - Fixed duplicate function declarations and import conflicts across multiple files - Added error recovery mechanisms with multiple fallback strategies - Updated tests to handle new validation behavior for suspicious addresses - Fixed parser test expectations for improved validation system - Applied gofmt formatting fixes to ensure code style compliance - Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot - Resolved critical security vulnerabilities in heuristic address extraction - Progress: Updated TODO audit from 10% to 35% complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1006 lines
32 KiB
Go
1006 lines
32 KiB
Go
package arbitrage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
stdmath "math"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/arbitrum"
|
|
"github.com/fraktal/mev-beta/pkg/exchanges"
|
|
"github.com/fraktal/mev-beta/pkg/math"
|
|
"github.com/fraktal/mev-beta/pkg/mev"
|
|
"github.com/fraktal/mev-beta/pkg/security"
|
|
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
|
)
|
|
|
|
// safeConvertUint64ToInt64 safely converts a uint64 to int64, capping at MaxInt64 if overflow would occur
|
|
func safeConvertUint64ToInt64(v uint64) int64 {
|
|
if v > stdmath.MaxInt64 {
|
|
return stdmath.MaxInt64
|
|
}
|
|
return int64(v)
|
|
}
|
|
|
|
// LiveExecutionFramework orchestrates the complete MEV bot pipeline
|
|
type LiveExecutionFramework struct {
|
|
// Core components
|
|
client *ethclient.Client
|
|
logger *logger.Logger
|
|
keyManager *security.KeyManager
|
|
gasEstimator *arbitrum.L2GasEstimator
|
|
decimalConverter *math.DecimalConverter
|
|
|
|
// Exchange and market components
|
|
exchangeRegistry *exchanges.ExchangeRegistry
|
|
pricingEngine *math.ExchangePricingEngine
|
|
calculator *math.ArbitrageCalculator
|
|
|
|
// Detection and execution
|
|
detectionEngine *ArbitrageDetectionEngine
|
|
flashExecutor *FlashSwapExecutor
|
|
competitionAnalyzer *mev.CompetitionAnalyzer
|
|
|
|
// Configuration
|
|
config FrameworkConfig
|
|
|
|
// State management
|
|
isRunning bool
|
|
runningMutex sync.RWMutex
|
|
stopChan chan struct{}
|
|
|
|
// Performance tracking
|
|
stats *FrameworkStats
|
|
statsMutex sync.RWMutex
|
|
|
|
// Opportunity processing
|
|
opportunityQueue chan *pkgtypes.ArbitrageOpportunity
|
|
executionQueue chan *ExecutionTask
|
|
workerPool *ExecutionWorkerPool
|
|
}
|
|
|
|
// FrameworkConfig configures the live execution framework
|
|
|
|
type FrameworkConfig struct {
|
|
// Detection settings
|
|
DetectionConfig DetectionConfig
|
|
|
|
// Execution settings
|
|
ExecutionConfig ExecutionConfig
|
|
|
|
// Risk management
|
|
MaxConcurrentExecutions int
|
|
DailyProfitTarget *math.UniversalDecimal
|
|
DailyLossLimit *math.UniversalDecimal
|
|
MaxPositionSize *math.UniversalDecimal
|
|
|
|
// Performance settings
|
|
WorkerPoolSize int
|
|
OpportunityQueueSize int
|
|
ExecutionQueueSize int
|
|
|
|
// Emergency controls
|
|
EmergencyStopEnabled bool
|
|
CircuitBreakerEnabled bool
|
|
MaxFailureRate float64 // Stop if failure rate exceeds this
|
|
HealthCheckInterval time.Duration
|
|
}
|
|
|
|
// ExecutionTask is defined in executor.go to avoid duplication
|
|
|
|
// LiveExecutionMetrics contains metrics from the live execution framework
|
|
type LiveExecutionMetrics struct {
|
|
OpportunitiesDetected int64
|
|
OpportunitiesExecuted int64
|
|
SuccessfulExecutions int64
|
|
FailedExecutions int64
|
|
TotalProfit *math.UniversalDecimal
|
|
TotalGasCost *math.UniversalDecimal
|
|
AverageExecutionTime time.Duration
|
|
AverageGasUsed uint64
|
|
CurrentWorkers int
|
|
QueueLength int
|
|
HealthStatus string
|
|
LastExecutionTime time.Time
|
|
LastOpportunityTime time.Time
|
|
}
|
|
|
|
// TaskPriority represents execution priority
|
|
type TaskPriority int
|
|
|
|
const (
|
|
PriorityLow TaskPriority = 1
|
|
PriorityMedium TaskPriority = 2
|
|
PriorityHigh TaskPriority = 3
|
|
PriorityCritical TaskPriority = 4
|
|
)
|
|
|
|
// FrameworkStats tracks framework performance
|
|
type FrameworkStats struct {
|
|
StartTime time.Time
|
|
TotalOpportunitiesDetected uint64
|
|
TotalOpportunitiesQueued uint64
|
|
TotalExecutionsAttempted uint64
|
|
TotalExecutionsSuccessful uint64
|
|
TotalProfitRealized *math.UniversalDecimal
|
|
TotalGasCostPaid *math.UniversalDecimal
|
|
AverageExecutionTime time.Duration
|
|
CurrentSuccessRate float64
|
|
DailyStats map[string]*DailyStats
|
|
}
|
|
|
|
// DailyStats tracks daily performance
|
|
type DailyStats struct {
|
|
Date string
|
|
OpportunitiesDetected uint64
|
|
ExecutionsAttempted uint64
|
|
ExecutionsSuccessful uint64
|
|
ProfitRealized *math.UniversalDecimal
|
|
GasCostPaid *math.UniversalDecimal
|
|
NetProfit *math.UniversalDecimal
|
|
}
|
|
|
|
// ExecutionWorkerPool manages concurrent execution workers
|
|
type ExecutionWorkerPool struct {
|
|
workers int
|
|
taskChan chan *ExecutionTask
|
|
wg sync.WaitGroup
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
framework *LiveExecutionFramework
|
|
}
|
|
|
|
// NewLiveExecutionFramework creates a new live execution framework
|
|
func NewLiveExecutionFramework(
|
|
client *ethclient.Client,
|
|
logger *logger.Logger,
|
|
keyManager *security.KeyManager,
|
|
gasEstimator *arbitrum.L2GasEstimator,
|
|
flashSwapContract,
|
|
arbitrageContract common.Address,
|
|
config FrameworkConfig,
|
|
) (*LiveExecutionFramework, error) {
|
|
|
|
// Initialize exchange registry
|
|
exchangeRegistry := exchanges.NewExchangeRegistry(client, logger)
|
|
|
|
// Initialize pricing engine
|
|
pricingEngine := math.NewExchangePricingEngine()
|
|
|
|
// Create gas estimator wrapper for calculator
|
|
gasEstWrapper := &GasEstimatorWrapper{gasEstimator: gasEstimator}
|
|
|
|
// Initialize arbitrage calculator
|
|
calculator := math.NewArbitrageCalculator(gasEstWrapper)
|
|
|
|
// Initialize competition analyzer
|
|
competitionAnalyzer := mev.NewCompetitionAnalyzer(client, logger)
|
|
|
|
// Initialize detection engine
|
|
detectionEngine := NewArbitrageDetectionEngine(
|
|
exchangeRegistry,
|
|
gasEstWrapper,
|
|
logger,
|
|
config.DetectionConfig,
|
|
)
|
|
|
|
// Initialize flash executor
|
|
flashExecutor := NewFlashSwapExecutor(
|
|
client,
|
|
logger,
|
|
keyManager,
|
|
gasEstimator,
|
|
flashSwapContract,
|
|
arbitrageContract,
|
|
config.ExecutionConfig,
|
|
)
|
|
|
|
// Initialize statistics
|
|
stats := &FrameworkStats{
|
|
StartTime: time.Now(),
|
|
DailyStats: make(map[string]*DailyStats),
|
|
}
|
|
|
|
dc := math.NewDecimalConverter()
|
|
stats.TotalProfitRealized, _ = dc.FromString("0", 18, "ETH")
|
|
stats.TotalGasCostPaid, _ = dc.FromString("0", 18, "ETH")
|
|
|
|
framework := &LiveExecutionFramework{
|
|
client: client,
|
|
logger: logger,
|
|
keyManager: keyManager,
|
|
gasEstimator: gasEstimator,
|
|
decimalConverter: dc,
|
|
exchangeRegistry: exchangeRegistry,
|
|
pricingEngine: pricingEngine,
|
|
calculator: calculator,
|
|
detectionEngine: detectionEngine,
|
|
flashExecutor: flashExecutor,
|
|
competitionAnalyzer: competitionAnalyzer,
|
|
config: config,
|
|
stats: stats,
|
|
stopChan: make(chan struct{}),
|
|
opportunityQueue: make(chan *pkgtypes.ArbitrageOpportunity, config.OpportunityQueueSize),
|
|
executionQueue: make(chan *ExecutionTask, config.ExecutionQueueSize),
|
|
}
|
|
|
|
// Set default configuration
|
|
framework.setDefaultConfig()
|
|
|
|
return framework, nil
|
|
}
|
|
|
|
// setDefaultConfig sets default configuration values
|
|
func (framework *LiveExecutionFramework) setDefaultConfig() {
|
|
if framework.config.MaxConcurrentExecutions == 0 {
|
|
framework.config.MaxConcurrentExecutions = 5
|
|
}
|
|
|
|
if framework.config.WorkerPoolSize == 0 {
|
|
framework.config.WorkerPoolSize = 10
|
|
}
|
|
|
|
if framework.config.OpportunityQueueSize == 0 {
|
|
framework.config.OpportunityQueueSize = 1000
|
|
}
|
|
|
|
if framework.config.ExecutionQueueSize == 0 {
|
|
framework.config.ExecutionQueueSize = 100
|
|
}
|
|
|
|
if framework.config.MaxFailureRate == 0 {
|
|
framework.config.MaxFailureRate = 0.5 // 50% failure rate threshold
|
|
}
|
|
|
|
if framework.config.HealthCheckInterval == 0 {
|
|
framework.config.HealthCheckInterval = 30 * time.Second
|
|
}
|
|
|
|
if framework.config.DailyProfitTarget == nil {
|
|
framework.config.DailyProfitTarget, _ = framework.decimalConverter.FromString("1", 18, "ETH")
|
|
}
|
|
|
|
if framework.config.DailyLossLimit == nil {
|
|
framework.config.DailyLossLimit, _ = framework.decimalConverter.FromString("0.1", 18, "ETH")
|
|
}
|
|
|
|
if framework.config.MaxPositionSize == nil {
|
|
framework.config.MaxPositionSize, _ = framework.decimalConverter.FromString("10", 18, "ETH")
|
|
}
|
|
}
|
|
|
|
// Start begins the live execution framework
|
|
func (framework *LiveExecutionFramework) Start(ctx context.Context) error {
|
|
framework.runningMutex.Lock()
|
|
defer framework.runningMutex.Unlock()
|
|
|
|
if framework.isRunning {
|
|
return fmt.Errorf("framework is already running")
|
|
}
|
|
|
|
framework.logger.Info("🚀 Starting Live MEV Execution Framework...")
|
|
framework.logger.Info("================================================")
|
|
framework.logger.Info(fmt.Sprintf("⚙️ Max Concurrent Executions: %d", framework.config.MaxConcurrentExecutions))
|
|
framework.logger.Info(fmt.Sprintf("💰 Daily Profit Target: %s ETH", framework.decimalConverter.ToHumanReadable(framework.config.DailyProfitTarget)))
|
|
framework.logger.Info(fmt.Sprintf("🛡️ Daily Loss Limit: %s ETH", framework.decimalConverter.ToHumanReadable(framework.config.DailyLossLimit)))
|
|
framework.logger.Info(fmt.Sprintf("📊 Worker Pool Size: %d", framework.config.WorkerPoolSize))
|
|
|
|
// Initialize worker pool
|
|
framework.initializeWorkerPool(ctx)
|
|
|
|
// Start detection engine
|
|
if err := framework.detectionEngine.Start(ctx); err != nil {
|
|
return fmt.Errorf("failed to start detection engine: %w", err)
|
|
}
|
|
|
|
framework.isRunning = true
|
|
|
|
// Start main processing loops
|
|
go framework.opportunityProcessor(ctx)
|
|
go framework.executionCoordinator(ctx)
|
|
go framework.healthMonitor(ctx)
|
|
go framework.performanceTracker(ctx)
|
|
|
|
framework.logger.Info("✅ Live Execution Framework started successfully!")
|
|
framework.logger.Info("🔍 Monitoring for arbitrage opportunities...")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop halts the live execution framework
|
|
func (framework *LiveExecutionFramework) Stop() error {
|
|
framework.runningMutex.Lock()
|
|
defer framework.runningMutex.Unlock()
|
|
|
|
if !framework.isRunning {
|
|
return fmt.Errorf("framework is not running")
|
|
}
|
|
|
|
framework.logger.Info("🛑 Stopping Live Execution Framework...")
|
|
|
|
// Signal stop
|
|
close(framework.stopChan)
|
|
|
|
// Stop detection engine
|
|
if err := framework.detectionEngine.Stop(); err != nil {
|
|
framework.logger.Warn(fmt.Sprintf("Error stopping detection engine: %v", err))
|
|
}
|
|
|
|
// Stop worker pool
|
|
if framework.workerPool != nil {
|
|
framework.workerPool.Stop()
|
|
}
|
|
|
|
framework.isRunning = false
|
|
|
|
// Print final statistics
|
|
framework.printFinalStats()
|
|
|
|
framework.logger.Info("✅ Live Execution Framework stopped successfully")
|
|
|
|
return nil
|
|
}
|
|
|
|
// initializeWorkerPool sets up the execution worker pool
|
|
func (framework *LiveExecutionFramework) initializeWorkerPool(ctx context.Context) {
|
|
framework.workerPool = &ExecutionWorkerPool{
|
|
workers: framework.config.WorkerPoolSize,
|
|
taskChan: framework.executionQueue,
|
|
framework: framework,
|
|
}
|
|
|
|
workerCtx, cancel := context.WithCancel(ctx)
|
|
framework.workerPool.ctx = workerCtx
|
|
framework.workerPool.cancel = cancel
|
|
|
|
framework.workerPool.Start()
|
|
}
|
|
|
|
// opportunityProcessor processes detected opportunities
|
|
func (framework *LiveExecutionFramework) opportunityProcessor(ctx context.Context) {
|
|
framework.logger.Debug("Starting opportunity processor...")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-framework.stopChan:
|
|
return
|
|
case opportunity := <-framework.detectionEngine.GetOpportunityChannel():
|
|
framework.processOpportunity(ctx, opportunity)
|
|
}
|
|
}
|
|
}
|
|
|
|
// convertArbitrageOpportunityToMEVOpportunity converts types.ArbitrageOpportunity to mev.MEVOpportunity
|
|
func (framework *LiveExecutionFramework) convertArbitrageOpportunityToMEVOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) *mev.MEVOpportunity {
|
|
// Convert the arbitrage opportunity to MEV opportunity format for competition analysis
|
|
estimatedProfit := big.NewInt(0)
|
|
if opportunity.NetProfit != nil {
|
|
estimatedProfit = new(big.Int).Set(opportunity.NetProfit)
|
|
}
|
|
|
|
// Calculate required gas estimate (placeholder - would be more precise in production)
|
|
requiredGas := uint64(800000) // typical for flash swaps
|
|
if len(opportunity.Path) > 0 {
|
|
requiredGas += uint64(len(opportunity.Path)-1) * 100000 // additional for each hop
|
|
}
|
|
|
|
return &mev.MEVOpportunity{
|
|
TxHash: "", // Will be populated later
|
|
Block: 0, // Will be determined at execution time
|
|
OpportunityType: "arbitrage", // or "sandwich", "liquidation", etc.
|
|
EstimatedProfit: estimatedProfit,
|
|
RequiredGas: requiredGas,
|
|
Competition: 0, // This will be determined by the analyzer
|
|
Confidence: opportunity.Confidence,
|
|
}
|
|
}
|
|
|
|
// processOpportunity processes a single arbitrage opportunity
|
|
func (framework *LiveExecutionFramework) processOpportunity(ctx context.Context, opportunity *pkgtypes.ArbitrageOpportunity) {
|
|
framework.statsMutex.Lock()
|
|
framework.stats.TotalOpportunitiesDetected++
|
|
framework.statsMutex.Unlock()
|
|
|
|
framework.logger.Debug(fmt.Sprintf("Processing opportunity: %s profit",
|
|
framework.decimalConverter.ToHumanReadable(universalFromWei(framework.decimalConverter, opportunity.NetProfit, "ETH"))))
|
|
|
|
// Perform risk checks
|
|
if !framework.performRiskChecks(opportunity) {
|
|
framework.logger.Debug("Opportunity failed risk checks, skipping")
|
|
return
|
|
}
|
|
|
|
// Convert opportunity for competition analysis
|
|
mevOpportunity := framework.convertArbitrageOpportunityToMEVOpportunity(opportunity)
|
|
|
|
// Analyze competition
|
|
competitionAnalysis, err := framework.competitionAnalyzer.AnalyzeCompetition(ctx, mevOpportunity)
|
|
if err != nil {
|
|
framework.logger.Warn(fmt.Sprintf("Competition analysis failed: %v", err))
|
|
return
|
|
}
|
|
|
|
// Check if we should proceed based on competition
|
|
if !framework.shouldExecuteBasedOnCompetition(competitionAnalysis) {
|
|
framework.logger.Debug("Skipping opportunity due to competition analysis")
|
|
return
|
|
}
|
|
|
|
// Determine priority
|
|
priority := framework.calculatePriority(opportunity, competitionAnalysis)
|
|
|
|
// Create execution task
|
|
task := &ExecutionTask{
|
|
Opportunity: opportunity,
|
|
CompetitionAnalysis: competitionAnalysis,
|
|
Priority: int(priority), // Convert TaskPriority to int
|
|
SubmissionTime: time.Now(),
|
|
ResultChan: make(chan *ExecutionResult, 1),
|
|
}
|
|
|
|
// Queue for execution
|
|
select {
|
|
case framework.executionQueue <- task:
|
|
framework.statsMutex.Lock()
|
|
framework.stats.TotalOpportunitiesQueued++
|
|
framework.statsMutex.Unlock()
|
|
|
|
framework.logger.Info(fmt.Sprintf("🎯 Queued opportunity for execution: %s profit, Priority: %d",
|
|
framework.decimalConverter.ToHumanReadable(universalFromWei(framework.decimalConverter, opportunity.NetProfit, "ETH")), priority))
|
|
default:
|
|
framework.logger.Warn("Execution queue full, dropping opportunity")
|
|
}
|
|
}
|
|
|
|
// executionCoordinator coordinates the execution of queued opportunities
|
|
func (framework *LiveExecutionFramework) executionCoordinator(ctx context.Context) {
|
|
framework.logger.Debug("Starting execution coordinator...")
|
|
|
|
activExecutions := 0
|
|
maxConcurrent := framework.config.MaxConcurrentExecutions
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-framework.stopChan:
|
|
return
|
|
case task := <-framework.executionQueue:
|
|
// Check if we can start another execution
|
|
if activExecutions >= maxConcurrent {
|
|
framework.logger.Debug("Max concurrent executions reached, queuing task")
|
|
// Put the task back in queue (simplified - production would use priority queue)
|
|
go func() {
|
|
time.Sleep(100 * time.Millisecond)
|
|
select {
|
|
case framework.executionQueue <- task:
|
|
default:
|
|
framework.logger.Warn("Failed to requeue task")
|
|
}
|
|
}()
|
|
continue
|
|
}
|
|
|
|
activExecutions++
|
|
|
|
// Execute asynchronously
|
|
go func(t *ExecutionTask) {
|
|
defer func() { activExecutions-- }()
|
|
framework.executeOpportunity(ctx, t)
|
|
}(task)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExecuteOpportunity executes a single arbitrage opportunity
|
|
func (framework *LiveExecutionFramework) ExecuteOpportunity(ctx context.Context, task *ExecutionTask) (*ExecutionResult, error) {
|
|
// Submit the task
|
|
framework.SubmitExecutionTask(ctx, task)
|
|
|
|
// Wait for completion with timeout
|
|
select {
|
|
case result := <-task.ResultChan:
|
|
if result == nil {
|
|
return nil, fmt.Errorf("execution returned nil result")
|
|
}
|
|
return result, nil
|
|
case <-time.After(45 * time.Second): // 45s timeout
|
|
return nil, fmt.Errorf("execution timeout")
|
|
}
|
|
}
|
|
|
|
// executeOpportunity executes a single arbitrage opportunity (internal worker method)
|
|
func (framework *LiveExecutionFramework) executeOpportunity(ctx context.Context, task *ExecutionTask) {
|
|
framework.statsMutex.Lock()
|
|
framework.stats.TotalExecutionsAttempted++
|
|
framework.statsMutex.Unlock()
|
|
|
|
framework.logger.Info(fmt.Sprintf("🚀 Executing arbitrage: %s expected profit",
|
|
opportunityAmountString(framework.decimalConverter, task.Opportunity)))
|
|
|
|
startTime := time.Now()
|
|
|
|
// Execute the arbitrage
|
|
result, err := framework.flashExecutor.ExecuteArbitrage(ctx, task.Opportunity)
|
|
if err != nil {
|
|
framework.logger.Error(fmt.Sprintf("Execution failed: %v", err))
|
|
result = &ExecutionResult{
|
|
Success: false,
|
|
Error: err,
|
|
ErrorMessage: err.Error(),
|
|
}
|
|
}
|
|
|
|
executionTime := time.Since(startTime)
|
|
|
|
// Update statistics
|
|
framework.updateExecutionStats(result, executionTime)
|
|
|
|
// Log result
|
|
if result.Success {
|
|
realized := executionProfitToString(framework.decimalConverter, result)
|
|
framework.logger.Info(fmt.Sprintf("✅ Execution successful: %s profit realized in %v",
|
|
realized,
|
|
executionTime))
|
|
} else {
|
|
framework.logger.Warn(fmt.Sprintf("❌ Execution failed: %s", result.ErrorMessage))
|
|
}
|
|
|
|
// Send result back if someone is waiting
|
|
select {
|
|
case task.ResultChan <- result:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// Helper methods for risk management and decision making
|
|
|
|
func (framework *LiveExecutionFramework) performRiskChecks(opportunity *pkgtypes.ArbitrageOpportunity) bool {
|
|
// Check position size
|
|
if !framework.checkPositionSize(opportunity) {
|
|
return false
|
|
}
|
|
|
|
// Check daily loss limit
|
|
if !framework.checkDailyLossLimit() {
|
|
return false
|
|
}
|
|
|
|
// Check daily profit target
|
|
if !framework.checkDailyProfitTarget(opportunity) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// checkPositionSize validates that the position size is within limits
|
|
func (framework *LiveExecutionFramework) checkPositionSize(opportunity *pkgtypes.ArbitrageOpportunity) bool {
|
|
amountWei := opportunity.AmountIn
|
|
if amountWei == nil {
|
|
amountWei = opportunity.RequiredAmount
|
|
}
|
|
if framework.config.MaxPositionSize != nil {
|
|
positionSize := universalFromWei(framework.decimalConverter, amountWei, "ETH")
|
|
if comp, _ := framework.decimalConverter.Compare(positionSize, framework.config.MaxPositionSize); comp > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkDailyLossLimit validates that we haven't exceeded daily loss limits
|
|
func (framework *LiveExecutionFramework) checkDailyLossLimit() bool {
|
|
today := time.Now().Format("2006-01-02")
|
|
if framework.config.DailyLossLimit != nil {
|
|
if dailyStats, exists := framework.stats.DailyStats[today]; exists && dailyStats.NetProfit != nil {
|
|
if dailyStats.NetProfit.IsNegative() {
|
|
loss := dailyStats.NetProfit.Copy()
|
|
loss.Value.Abs(loss.Value)
|
|
if comp, err := framework.decimalConverter.Compare(loss, framework.config.DailyLossLimit); err == nil {
|
|
if comp > 0 {
|
|
framework.logger.Warn("Daily loss limit reached, skipping opportunity")
|
|
return false
|
|
}
|
|
} else {
|
|
framework.logger.Warn(fmt.Sprintf("Failed to compare daily loss to limit: %v", err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkDailyProfitTarget validates that we haven't exceeded daily profit target
|
|
func (framework *LiveExecutionFramework) checkDailyProfitTarget(opportunity *pkgtypes.ArbitrageOpportunity) bool {
|
|
today := time.Now().Format("2006-01-02")
|
|
if dailyStats, exists := framework.stats.DailyStats[today]; exists {
|
|
if comp, _ := framework.decimalConverter.Compare(dailyStats.ProfitRealized, framework.config.DailyProfitTarget); comp >= 0 {
|
|
framework.logger.Info("Daily profit target reached, being conservative")
|
|
// Could still execute high-confidence opportunities
|
|
if opportunity.Confidence < 0.9 {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) shouldExecuteBasedOnCompetition(analysis *mev.CompetitionData) bool {
|
|
// Skip if competition is too intense
|
|
// CompetitionLevel for CompetitionData is a float64 representing intensity (0.0-1.0)
|
|
if analysis.CompetitionLevel > 0.8 { // Considered extreme competition
|
|
return false
|
|
}
|
|
|
|
// Skip if profit after gas is negative
|
|
if analysis.NetProfit.Sign() < 0 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) calculatePriority(
|
|
opportunity *pkgtypes.ArbitrageOpportunity,
|
|
competition *mev.CompetitionData,
|
|
) TaskPriority {
|
|
|
|
// Base priority on profit size
|
|
largeProfit, _ := framework.decimalConverter.FromString("0.1", 18, "ETH")
|
|
mediumProfit, _ := framework.decimalConverter.FromString("0.05", 18, "ETH")
|
|
|
|
var basePriority TaskPriority
|
|
|
|
netProfitDecimal := opportunityNetProfitDecimal(framework.decimalConverter, opportunity)
|
|
|
|
if comp, _ := framework.decimalConverter.Compare(netProfitDecimal, largeProfit); comp > 0 {
|
|
basePriority = PriorityHigh
|
|
} else if comp, _ := framework.decimalConverter.Compare(netProfitDecimal, mediumProfit); comp > 0 {
|
|
basePriority = PriorityMedium
|
|
} else {
|
|
basePriority = PriorityLow
|
|
}
|
|
|
|
// Adjust for confidence
|
|
if opportunity.Confidence > 0.9 && basePriority == PriorityHigh {
|
|
basePriority = PriorityCritical
|
|
}
|
|
|
|
// Adjust for competition - CompetitionData uses CompetitionLevel (0.0-1.0) instead of CompetitionLevel string
|
|
if competition.CompetitionLevel > 0.6 { // high competition
|
|
if basePriority > PriorityLow {
|
|
basePriority--
|
|
}
|
|
}
|
|
|
|
return basePriority
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) updateExecutionStats(result *ExecutionResult, executionTime time.Duration) {
|
|
framework.statsMutex.Lock()
|
|
defer framework.statsMutex.Unlock()
|
|
|
|
if result.Success {
|
|
framework.stats.TotalExecutionsSuccessful++
|
|
|
|
if result.ProfitRealized != nil {
|
|
profitDecimal, _ := math.NewUniversalDecimal(result.ProfitRealized, 18, "ETH")
|
|
framework.stats.TotalProfitRealized, _ = framework.decimalConverter.Add(
|
|
framework.stats.TotalProfitRealized,
|
|
profitDecimal,
|
|
)
|
|
}
|
|
}
|
|
|
|
if result.GasCost != nil {
|
|
framework.stats.TotalGasCostPaid, _ = framework.decimalConverter.Add(
|
|
framework.stats.TotalGasCostPaid,
|
|
result.GasCost,
|
|
)
|
|
}
|
|
|
|
// Update success rate
|
|
if framework.stats.TotalExecutionsAttempted > 0 {
|
|
framework.stats.CurrentSuccessRate = float64(framework.stats.TotalExecutionsSuccessful) / float64(framework.stats.TotalExecutionsAttempted)
|
|
}
|
|
|
|
// Update average execution time
|
|
framework.stats.AverageExecutionTime = (framework.stats.AverageExecutionTime + executionTime) / 2
|
|
|
|
// Update daily stats
|
|
framework.updateDailyStats(result)
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) updateDailyStats(result *ExecutionResult) {
|
|
today := time.Now().Format("2006-01-02")
|
|
|
|
if _, exists := framework.stats.DailyStats[today]; !exists {
|
|
framework.stats.DailyStats[today] = &DailyStats{
|
|
Date: today,
|
|
}
|
|
framework.stats.DailyStats[today].ProfitRealized, _ = framework.decimalConverter.FromString("0", 18, "ETH")
|
|
framework.stats.DailyStats[today].GasCostPaid, _ = framework.decimalConverter.FromString("0", 18, "ETH")
|
|
framework.stats.DailyStats[today].NetProfit, _ = framework.decimalConverter.FromString("0", 18, "ETH")
|
|
}
|
|
|
|
dailyStats := framework.stats.DailyStats[today]
|
|
dailyStats.ExecutionsAttempted++
|
|
|
|
if result.Success {
|
|
dailyStats.ExecutionsSuccessful++
|
|
|
|
if result.ProfitRealized != nil {
|
|
profitDecimal, _ := math.NewUniversalDecimal(result.ProfitRealized, 18, "ETH")
|
|
dailyStats.ProfitRealized, _ = framework.decimalConverter.Add(dailyStats.ProfitRealized, profitDecimal)
|
|
}
|
|
}
|
|
|
|
if result.GasCost != nil {
|
|
dailyStats.GasCostPaid, _ = framework.decimalConverter.Add(dailyStats.GasCostPaid, result.GasCost)
|
|
}
|
|
|
|
// Calculate net profit
|
|
dailyStats.NetProfit, _ = framework.decimalConverter.Subtract(dailyStats.ProfitRealized, dailyStats.GasCostPaid)
|
|
}
|
|
|
|
// healthMonitor monitors the health of the framework
|
|
func (framework *LiveExecutionFramework) healthMonitor(ctx context.Context) {
|
|
ticker := time.NewTicker(framework.config.HealthCheckInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-framework.stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
framework.performHealthCheck()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) performHealthCheck() {
|
|
framework.statsMutex.RLock()
|
|
successRate := framework.stats.CurrentSuccessRate
|
|
framework.statsMutex.RUnlock()
|
|
|
|
// Check if failure rate exceeds threshold
|
|
if successRate < (1.0 - framework.config.MaxFailureRate) {
|
|
framework.logger.Warn(fmt.Sprintf("⚠️ Success rate below threshold: %.1f%%", successRate*100))
|
|
|
|
if framework.config.CircuitBreakerEnabled {
|
|
framework.logger.Warn("🔥 Circuit breaker triggered - stopping framework")
|
|
framework.Stop()
|
|
}
|
|
}
|
|
|
|
// Log health status
|
|
framework.logger.Debug(fmt.Sprintf("Health check - Success rate: %.1f%%, Active: %t",
|
|
successRate*100, framework.isRunning))
|
|
}
|
|
|
|
// performanceTracker tracks and logs performance metrics
|
|
func (framework *LiveExecutionFramework) performanceTracker(ctx context.Context) {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-framework.stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
framework.logPerformanceMetrics()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) logPerformanceMetrics() {
|
|
framework.statsMutex.RLock()
|
|
stats := framework.stats
|
|
framework.statsMutex.RUnlock()
|
|
|
|
framework.logger.Info("📊 Performance Metrics:")
|
|
framework.logger.Info(fmt.Sprintf(" Opportunities Detected: %d", stats.TotalOpportunitiesDetected))
|
|
framework.logger.Info(fmt.Sprintf(" Executions Attempted: %d", stats.TotalExecutionsAttempted))
|
|
framework.logger.Info(fmt.Sprintf(" Success Rate: %.1f%%", stats.CurrentSuccessRate*100))
|
|
framework.logger.Info(fmt.Sprintf(" Total Profit: %s ETH", framework.decimalConverter.ToHumanReadable(stats.TotalProfitRealized)))
|
|
framework.logger.Info(fmt.Sprintf(" Total Gas Cost: %s ETH", framework.decimalConverter.ToHumanReadable(stats.TotalGasCostPaid)))
|
|
|
|
// Calculate net profit
|
|
netProfit, _ := framework.decimalConverter.Subtract(stats.TotalProfitRealized, stats.TotalGasCostPaid)
|
|
framework.logger.Info(fmt.Sprintf(" Net Profit: %s ETH", framework.decimalConverter.ToHumanReadable(netProfit)))
|
|
framework.logger.Info(fmt.Sprintf(" Average Execution Time: %v", stats.AverageExecutionTime))
|
|
}
|
|
|
|
func (framework *LiveExecutionFramework) printFinalStats() {
|
|
framework.statsMutex.RLock()
|
|
stats := framework.stats
|
|
framework.statsMutex.RUnlock()
|
|
|
|
framework.logger.Info("📈 Final Statistics:")
|
|
framework.logger.Info("==================")
|
|
framework.logger.Info(fmt.Sprintf("Runtime: %v", time.Since(stats.StartTime)))
|
|
framework.logger.Info(fmt.Sprintf("Opportunities Detected: %d", stats.TotalOpportunitiesDetected))
|
|
framework.logger.Info(fmt.Sprintf("Opportunities Queued: %d", stats.TotalOpportunitiesQueued))
|
|
framework.logger.Info(fmt.Sprintf("Executions Attempted: %d", stats.TotalExecutionsAttempted))
|
|
framework.logger.Info(fmt.Sprintf("Executions Successful: %d", stats.TotalExecutionsSuccessful))
|
|
framework.logger.Info(fmt.Sprintf("Final Success Rate: %.1f%%", stats.CurrentSuccessRate*100))
|
|
framework.logger.Info(fmt.Sprintf("Total Profit Realized: %s ETH", framework.decimalConverter.ToHumanReadable(stats.TotalProfitRealized)))
|
|
framework.logger.Info(fmt.Sprintf("Total Gas Cost Paid: %s ETH", framework.decimalConverter.ToHumanReadable(stats.TotalGasCostPaid)))
|
|
|
|
netProfit, _ := framework.decimalConverter.Subtract(stats.TotalProfitRealized, stats.TotalGasCostPaid)
|
|
framework.logger.Info(fmt.Sprintf("Final Net Profit: %s ETH", framework.decimalConverter.ToHumanReadable(netProfit)))
|
|
}
|
|
|
|
// GetStats returns current framework statistics
|
|
func (framework *LiveExecutionFramework) GetStats() *FrameworkStats {
|
|
framework.statsMutex.RLock()
|
|
defer framework.statsMutex.RUnlock()
|
|
|
|
// Return a copy to avoid race conditions
|
|
statsCopy := *framework.stats
|
|
return &statsCopy
|
|
}
|
|
|
|
// GetMetrics returns live execution metrics
|
|
func (framework *LiveExecutionFramework) GetMetrics() *LiveExecutionMetrics {
|
|
stats := framework.GetStats()
|
|
|
|
return &LiveExecutionMetrics{
|
|
OpportunitiesDetected: safeConvertUint64ToInt64(stats.TotalOpportunitiesDetected),
|
|
SuccessfulExecutions: safeConvertUint64ToInt64(stats.TotalExecutionsSuccessful),
|
|
FailedExecutions: safeConvertUint64ToInt64(stats.TotalExecutionsAttempted - stats.TotalExecutionsSuccessful), // Failed = Attempted - Successful
|
|
TotalProfit: stats.TotalProfitRealized,
|
|
AverageExecutionTime: stats.AverageExecutionTime,
|
|
CurrentWorkers: int(framework.config.WorkerPoolSize), // Using configured worker pool size as a proxy for active workers
|
|
QueueLength: int(len(framework.executionQueue)), // Current queue length
|
|
}
|
|
}
|
|
|
|
// SubmitExecutionTask submits an execution task to the framework queue
|
|
func (framework *LiveExecutionFramework) SubmitExecutionTask(ctx context.Context, task *ExecutionTask) {
|
|
// Check if the framework is running
|
|
framework.runningMutex.RLock()
|
|
isRunning := framework.isRunning
|
|
framework.runningMutex.RUnlock()
|
|
|
|
if !isRunning {
|
|
framework.logger.Error("Cannot submit task: framework is not running")
|
|
return
|
|
}
|
|
|
|
// Add the task to the execution queue
|
|
select {
|
|
case framework.executionQueue <- task:
|
|
framework.logger.Info(fmt.Sprintf("🎯 Queued execution task for opportunity with priority: %d", task.Priority))
|
|
framework.statsMutex.Lock()
|
|
framework.stats.TotalOpportunitiesQueued++
|
|
framework.statsMutex.Unlock()
|
|
case <-ctx.Done():
|
|
framework.logger.Warn("Context cancelled while trying to submit execution task")
|
|
case <-time.After(5 * time.Second): // Timeout to avoid blocking indefinitely
|
|
framework.logger.Error("Failed to submit execution task: queue is full")
|
|
}
|
|
}
|
|
|
|
// SetMonitoringMode sets the framework to monitoring-only mode
|
|
func (framework *LiveExecutionFramework) SetMonitoringMode(enabled bool) {
|
|
framework.runningMutex.Lock()
|
|
defer framework.runningMutex.Unlock()
|
|
// In a real implementation, this would control whether execution is enabled
|
|
// For now, we'll just log the change
|
|
if enabled {
|
|
framework.logger.Info("✅ Live execution framework set to monitoring mode")
|
|
} else {
|
|
framework.logger.Info("✅ Live execution framework set to active mode")
|
|
}
|
|
}
|
|
|
|
// Worker pool implementation
|
|
|
|
func (pool *ExecutionWorkerPool) Start() {
|
|
for i := 0; i < pool.workers; i++ {
|
|
pool.wg.Add(1)
|
|
go pool.worker()
|
|
}
|
|
}
|
|
|
|
func (pool *ExecutionWorkerPool) Stop() {
|
|
pool.cancel()
|
|
pool.wg.Wait()
|
|
}
|
|
|
|
func (pool *ExecutionWorkerPool) worker() {
|
|
defer pool.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-pool.ctx.Done():
|
|
return
|
|
case task := <-pool.taskChan:
|
|
pool.framework.executeOpportunity(pool.ctx, task)
|
|
}
|
|
}
|
|
}
|
|
|
|
func opportunityNetProfitDecimal(dc *math.DecimalConverter, opportunity *pkgtypes.ArbitrageOpportunity) *math.UniversalDecimal {
|
|
if opportunity == nil {
|
|
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
|
|
return zero
|
|
}
|
|
|
|
if opportunity.Quantities != nil {
|
|
if val, ok := new(big.Int).SetString(opportunity.Quantities.NetProfit.Value, 10); ok {
|
|
if ud, err := math.NewUniversalDecimal(val, opportunity.Quantities.NetProfit.Decimals, opportunity.Quantities.NetProfit.Symbol); err == nil {
|
|
return ud
|
|
}
|
|
}
|
|
}
|
|
|
|
return universalFromWei(dc, opportunity.NetProfit, "ETH")
|
|
}
|
|
|
|
func opportunityAmountString(dc *math.DecimalConverter, opportunity *pkgtypes.ArbitrageOpportunity) string {
|
|
if opportunity == nil {
|
|
return "n/a"
|
|
}
|
|
|
|
ud := opportunityNetProfitDecimal(dc, opportunity)
|
|
return floatStringFromDecimal(ud, 6)
|
|
}
|
|
|
|
func executionProfitToString(dc *math.DecimalConverter, result *ExecutionResult) string {
|
|
if result != nil && result.ProfitRealized != nil {
|
|
ud := universalOrFromWei(dc, nil, result.ProfitRealized, 18, "ETH")
|
|
return floatStringFromDecimal(ud, 6)
|
|
}
|
|
return "0.000000"
|
|
}
|
|
|
|
// GasEstimatorWrapper wraps the Arbitrum gas estimator to implement the math.GasEstimator interface
|
|
type GasEstimatorWrapper struct {
|
|
gasEstimator *arbitrum.L2GasEstimator
|
|
}
|
|
|
|
func (w *GasEstimatorWrapper) EstimateSwapGas(exchangeType math.ExchangeType, poolData *math.PoolData) (uint64, error) {
|
|
// Return estimates based on exchange type
|
|
switch exchangeType {
|
|
case math.ExchangeUniswapV3, math.ExchangeCamelot:
|
|
return 200000, nil // Concentrated liquidity swaps
|
|
case math.ExchangeUniswapV2, math.ExchangeSushiSwap:
|
|
return 150000, nil // Simple AMM swaps
|
|
case math.ExchangeBalancer:
|
|
return 250000, nil // Weighted pool swaps
|
|
case math.ExchangeCurve:
|
|
return 180000, nil // Stable swaps
|
|
default:
|
|
return 200000, nil // Default estimate
|
|
}
|
|
}
|
|
|
|
func (w *GasEstimatorWrapper) EstimateFlashSwapGas(route []*math.PoolData) (uint64, error) {
|
|
baseGas := uint64(300000) // Base flash swap overhead
|
|
gasPerHop := uint64(150000) // Additional gas per hop
|
|
|
|
return baseGas + gasPerHop*uint64(len(route)), nil
|
|
}
|
|
|
|
func (w *GasEstimatorWrapper) GetCurrentGasPrice() (*math.UniversalDecimal, error) {
|
|
// Return a mock gas price - production would get from the gas estimator
|
|
dc := math.NewDecimalConverter()
|
|
gasPrice, _ := dc.FromString("0.1", 9, "GWEI") // 0.1 gwei
|
|
return gasPrice, nil
|
|
}
|