Files
mev-beta/pkg/arbitrage/service_old.go.bak
2025-09-16 11:05:47 -05:00

686 lines
19 KiB
Go

package arbitrage
import (
"context"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/security"
)
// ArbitrageService orchestrates arbitrage detection and execution
type ArbitrageService struct {
client *ethclient.Client
logger *logger.Logger
config *config.ArbitrageConfig
// Core components
marketManager *market.MarketManager
multiHopScanner *MultiHopScanner
executor *ArbitrageExecutor
eventMonitor *monitor.Monitor
poolDiscovery *pools.PoolDiscovery
// Channels for communication
eventsChan chan *events.SwapEvent
arbitrageChan chan *ArbitrageOpportunity
resultsChan chan *ExecutionResult
// State management
isRunning bool
runMutex sync.RWMutex
ctx context.Context
cancel context.CancelFunc
// Metrics and monitoring
stats *ArbitrageStats
statsMutex sync.RWMutex
// Risk management
maxConcurrentExecutions int
executionSemaphore chan struct{}
// Database integration
database ArbitrageDatabase
}
// ArbitrageOpportunity represents a detected arbitrage opportunity
type ArbitrageOpportunity struct {
ID string
Path *ArbitragePath
TriggerEvent *events.SwapEvent
DetectedAt time.Time
EstimatedProfit *big.Int
RequiredAmount *big.Int
Urgency int // 1-10 priority level
ExpiresAt time.Time
}
// ArbitrageStats contains service statistics
type ArbitrageStats struct {
TotalOpportunitiesDetected int64
TotalOpportunitiesExecuted int64
TotalSuccessfulExecutions int64
TotalProfitRealized *big.Int
TotalGasSpent *big.Int
AverageExecutionTime time.Duration
LastExecutionTime time.Time
}
// ArbitrageDatabase interface for persistence
type ArbitrageDatabase interface {
SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error
SaveExecution(ctx context.Context, result *ExecutionResult) error
GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error)
SavePoolData(ctx context.Context, poolData *pools.PoolData) error
GetPoolData(ctx context.Context, poolAddress common.Address) (*pools.PoolData, error)
}
// NewArbitrageService creates a new arbitrage service
func NewArbitrageService(
client *ethclient.Client,
logger *logger.Logger,
config *config.ArbitrageConfig,
keyManager *security.KeyManager,
database ArbitrageDatabase,
) (*ArbitrageService, error) {
ctx, cancel := context.WithCancel(context.Background())
// Create market manager
marketManager := market.NewMarketManager(logger, client)
// Create multi-hop scanner
multiHopScanner := NewMultiHopScanner(logger, marketManager)
// Create arbitrage executor
executor, err := NewArbitrageExecutor(
client,
logger,
keyManager,
common.HexToAddress(config.ArbitrageContractAddress),
common.HexToAddress(config.FlashSwapContractAddress),
)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create arbitrage executor: %w", err)
}
// Create event monitor
eventMonitor := monitor.NewMonitor(logger, client)
// Create pool discovery service
poolDiscovery, err := pools.NewPoolDiscovery(client, logger, config.PoolDiscoveryConfig)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create pool discovery: %w", err)
}
// Initialize stats
stats := &ArbitrageStats{
TotalProfitRealized: big.NewInt(0),
TotalGasSpent: big.NewInt(0),
}
service := &ArbitrageService{
client: client,
logger: logger,
config: config,
marketManager: marketManager,
multiHopScanner: multiHopScanner,
executor: executor,
eventMonitor: eventMonitor,
poolDiscovery: poolDiscovery,
eventsChan: make(chan *events.SwapEvent, 1000),
arbitrageChan: make(chan *ArbitrageOpportunity, 100),
resultsChan: make(chan *ExecutionResult, 100),
ctx: ctx,
cancel: cancel,
stats: stats,
maxConcurrentExecutions: config.MaxConcurrentExecutions,
executionSemaphore: make(chan struct{}, config.MaxConcurrentExecutions),
database: database,
}
return service, nil
}
// Start begins the arbitrage service
func (as *ArbitrageService) Start() error {
as.runMutex.Lock()
defer as.runMutex.Unlock()
if as.isRunning {
return fmt.Errorf("arbitrage service is already running")
}
as.logger.Info("Starting arbitrage service...")
// Start pool discovery
if err := as.poolDiscovery.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start pool discovery: %w", err)
}
// Start market manager
if err := as.marketManager.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start market manager: %w", err)
}
// Start event monitor
if err := as.eventMonitor.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start event monitor: %w", err)
}
// Start worker goroutines
go as.eventProcessor()
go as.arbitrageDetector()
go as.arbitrageExecutor()
go as.resultProcessor()
go as.poolDataProcessor()
go as.statsUpdater()
as.isRunning = true
as.logger.Info("Arbitrage service started successfully")
return nil
}
// Stop stops the arbitrage service
func (as *ArbitrageService) Stop() error {
as.runMutex.Lock()
defer as.runMutex.Unlock()
if !as.isRunning {
return nil
}
as.logger.Info("Stopping arbitrage service...")
// Cancel context to stop all workers
as.cancel()
// Stop components
as.eventMonitor.Stop()
as.marketManager.Stop()
as.poolDiscovery.Stop()
// Close channels
close(as.eventsChan)
close(as.arbitrageChan)
close(as.resultsChan)
as.isRunning = false
as.logger.Info("Arbitrage service stopped")
return nil
}
// eventProcessor processes incoming swap events
func (as *ArbitrageService) eventProcessor() {
defer as.logger.Info("Event processor stopped")
// Subscribe to swap events from the event monitor
swapEvents := as.eventMonitor.SubscribeToSwapEvents()
for {
select {
case <-as.ctx.Done():
return
case event := <-swapEvents:
if event != nil {
as.processSwapEvent(event)
}
}
}
}
// processSwapEvent processes a single swap event for arbitrage opportunities
func (as *ArbitrageService) processSwapEvent(event *events.SwapEvent) {
as.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s",
event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String()))
// Check if this swap is large enough to potentially move prices
if !as.isSignificantSwap(event) {
return
}
// Forward to arbitrage detection
select {
case as.eventsChan <- event:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Event channel full, dropping swap event")
}
}
// isSignificantSwap checks if a swap is large enough to create arbitrage opportunities
func (as *ArbitrageService) isSignificantSwap(event *events.SwapEvent) bool {
// Convert amounts to absolute values for comparison
amount0Abs := new(big.Int).Abs(event.Amount0)
amount1Abs := new(big.Int).Abs(event.Amount1)
// Check if either amount is above our threshold
minSwapSize := big.NewInt(as.config.MinSignificantSwapSize)
return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0
}
// arbitrageDetector detects arbitrage opportunities from swap events
func (as *ArbitrageService) arbitrageDetector() {
defer as.logger.Info("Arbitrage detector stopped")
for {
select {
case <-as.ctx.Done():
return
case event := <-as.eventsChan:
if event != nil {
as.detectArbitrageOpportunities(event)
}
}
}
}
// detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event
func (as *ArbitrageService) detectArbitrageOpportunities(event *events.SwapEvent) {
start := time.Now()
// Determine the tokens involved in potential arbitrage
tokens := []common.Address{event.Token0, event.Token1}
var allOpportunities []*ArbitrageOpportunity
// Scan for opportunities starting with each token
for _, token := range tokens {
// Determine appropriate amount to use for scanning
scanAmount := as.calculateScanAmount(event, token)
// Use multi-hop scanner to find arbitrage paths
paths, err := as.multiHopScanner.ScanForArbitrage(as.ctx, token, scanAmount)
if err != nil {
as.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err))
continue
}
// Convert paths to opportunities
for _, path := range paths {
if as.isValidOpportunity(path) {
opportunity := &ArbitrageOpportunity{
ID: as.generateOpportunityID(path, event),
Path: path,
TriggerEvent: event,
DetectedAt: time.Now(),
EstimatedProfit: path.NetProfit,
RequiredAmount: scanAmount,
Urgency: as.calculateUrgency(path),
ExpiresAt: time.Now().Add(as.config.OpportunityTTL),
}
allOpportunities = append(allOpportunities, opportunity)
}
}
}
// Sort opportunities by urgency and profit
as.rankOpportunities(allOpportunities)
// Send top opportunities for execution
maxOpportunities := as.config.MaxOpportunitiesPerEvent
for i, opportunity := range allOpportunities {
if i >= maxOpportunities {
break
}
// Update stats
as.statsMutex.Lock()
as.stats.TotalOpportunitiesDetected++
as.statsMutex.Unlock()
// Save to database
if err := as.database.SaveOpportunity(as.ctx, opportunity); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err))
}
// Send for execution
select {
case as.arbitrageChan <- opportunity:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Arbitrage channel full, dropping opportunity")
}
}
elapsed := time.Since(start)
as.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities",
elapsed, len(allOpportunities)))
}
// isValidOpportunity validates an arbitrage opportunity
func (as *ArbitrageService) isValidOpportunity(path *ArbitragePath) bool {
// Check minimum profit threshold
minProfit := big.NewInt(as.config.MinProfitWei)
if path.NetProfit.Cmp(minProfit) < 0 {
return false
}
// Check minimum ROI
if path.ROI < as.config.MinROIPercent {
return false
}
// Check path freshness
if time.Since(path.LastUpdated) > as.config.MaxPathAge {
return false
}
// Check gas profitability
currentGasPrice, err := as.client.SuggestGasPrice(as.ctx)
if err != nil {
// Assume worst case if we can't get gas price
currentGasPrice = big.NewInt(as.config.MaxGasPriceWei)
}
return as.executor.IsProfitableAfterGas(path, currentGasPrice)
}
// calculateScanAmount determines the optimal amount to use for arbitrage scanning
func (as *ArbitrageService) calculateScanAmount(event *events.SwapEvent, token common.Address) *big.Int {
// Base amount on the swap size, but cap it for risk management
var swapAmount *big.Int
if token == event.Token0 {
swapAmount = new(big.Int).Abs(event.Amount0)
} else {
swapAmount = new(big.Int).Abs(event.Amount1)
}
// Use a fraction of the swap amount (default 10%)
scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10))
// Ensure minimum scan amount
minAmount := big.NewInt(as.config.MinScanAmountWei)
if scanAmount.Cmp(minAmount) < 0 {
scanAmount = minAmount
}
// Cap maximum scan amount
maxAmount := big.NewInt(as.config.MaxScanAmountWei)
if scanAmount.Cmp(maxAmount) > 0 {
scanAmount = maxAmount
}
return scanAmount
}
// calculateUrgency calculates the urgency level of an opportunity
func (as *ArbitrageService) calculateUrgency(path *ArbitragePath) int {
// Base urgency on ROI
urgency := int(path.ROI / 2) // 2% ROI = urgency level 1
// Boost urgency for higher profits
profitETH := new(big.Float).SetInt(path.NetProfit)
profitETH.Quo(profitETH, big.NewFloat(1e18))
profitFloat, _ := profitETH.Float64()
if profitFloat > 1.0 { // > 1 ETH profit
urgency += 5
} else if profitFloat > 0.1 { // > 0.1 ETH profit
urgency += 2
}
// Cap urgency between 1 and 10
if urgency < 1 {
urgency = 1
}
if urgency > 10 {
urgency = 10
}
return urgency
}
// rankOpportunities sorts opportunities by priority
func (as *ArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) {
// Sort by urgency (descending) then by profit (descending)
for i := 0; i < len(opportunities); i++ {
for j := i + 1; j < len(opportunities); j++ {
iOpp := opportunities[i]
jOpp := opportunities[j]
if jOpp.Urgency > iOpp.Urgency {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
} else if jOpp.Urgency == iOpp.Urgency {
if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
}
}
}
}
}
// arbitrageExecutor executes arbitrage opportunities
func (as *ArbitrageService) arbitrageExecutor() {
defer as.logger.Info("Arbitrage executor stopped")
for {
select {
case <-as.ctx.Done():
return
case opportunity := <-as.arbitrageChan:
if opportunity != nil {
// Rate limit concurrent executions
select {
case as.executionSemaphore <- struct{}{}:
go as.executeOpportunity(opportunity)
case <-as.ctx.Done():
return
default:
as.logger.Warn("Too many concurrent executions, dropping opportunity")
}
}
}
}
}
// executeOpportunity executes a single arbitrage opportunity
func (as *ArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) {
defer func() {
<-as.executionSemaphore // Release semaphore
}()
// Check if opportunity is still valid
if time.Now().After(opportunity.ExpiresAt) {
as.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID))
return
}
// Update stats
as.statsMutex.Lock()
as.stats.TotalOpportunitiesExecuted++
as.statsMutex.Unlock()
// Prepare execution parameters
params := &ArbitrageParams{
Path: opportunity.Path,
InputAmount: opportunity.RequiredAmount,
MinOutputAmount: as.calculateMinOutput(opportunity),
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
FlashSwapData: []byte{}, // Additional data if needed
}
as.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
// Execute the arbitrage
result, err := as.executor.ExecuteArbitrage(as.ctx, params)
if err != nil {
as.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v",
opportunity.ID, err))
}
// Send result for processing
select {
case as.resultsChan <- result:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Results channel full, dropping execution result")
}
}
// calculateMinOutput calculates minimum acceptable output for slippage protection
func (as *ArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int {
// Calculate expected output
expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit)
// Apply slippage tolerance
slippageTolerance := as.config.SlippageTolerance
slippageMultiplier := big.NewFloat(1.0 - slippageTolerance)
expectedFloat := new(big.Float).SetInt(expectedOutput)
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier)
minOutput := new(big.Int)
minOutputFloat.Int(minOutput)
return minOutput
}
// resultProcessor processes execution results
func (as *ArbitrageService) resultProcessor() {
defer as.logger.Info("Result processor stopped")
for {
select {
case <-as.ctx.Done():
return
case result := <-as.resultsChan:
if result != nil {
as.processExecutionResult(result)
}
}
}
}
// processExecutionResult processes a single execution result
func (as *ArbitrageService) processExecutionResult(result *ExecutionResult) {
// Update statistics
as.statsMutex.Lock()
if result.Success {
as.stats.TotalSuccessfulExecutions++
as.stats.TotalProfitRealized.Add(as.stats.TotalProfitRealized, result.ProfitRealized)
}
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
as.stats.TotalGasSpent.Add(as.stats.TotalGasSpent, gasCost)
as.stats.LastExecutionTime = time.Now()
as.statsMutex.Unlock()
// Save to database
if err := as.database.SaveExecution(as.ctx, result); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err))
}
// Log results
if result.Success {
as.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
} else {
as.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
result.TransactionHash.Hex(), result.Error))
}
}
// poolDataProcessor handles pool data updates and saves them to database
func (as *ArbitrageService) poolDataProcessor() {
defer as.logger.Info("Pool data processor stopped")
// Subscribe to pool discovery events
poolEvents := as.poolDiscovery.SubscribeToPoolEvents()
for {
select {
case <-as.ctx.Done():
return
case poolData := <-poolEvents:
if poolData != nil {
// Save pool data to database
if err := as.database.SavePoolData(as.ctx, poolData); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save pool data: %v", err))
}
// Update market manager with new pool
as.marketManager.AddPool(poolData)
as.logger.Debug(fmt.Sprintf("Added new pool: %s (%s/%s)",
poolData.Address.Hex(), poolData.Token0.Hex(), poolData.Token1.Hex()))
}
}
}
}
// statsUpdater periodically logs service statistics
func (as *ArbitrageService) statsUpdater() {
defer as.logger.Info("Stats updater stopped")
ticker := time.NewTicker(as.config.StatsUpdateInterval)
defer ticker.Stop()
for {
select {
case <-as.ctx.Done():
return
case <-ticker.C:
as.logStats()
}
}
}
// logStats logs current service statistics
func (as *ArbitrageService) logStats() {
as.statsMutex.RLock()
stats := *as.stats
as.statsMutex.RUnlock()
successRate := 0.0
if stats.TotalOpportunitiesExecuted > 0 {
successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100
}
as.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+
"Total Profit: %s ETH, Total Gas: %s ETH",
stats.TotalOpportunitiesDetected,
stats.TotalOpportunitiesExecuted,
successRate,
formatEther(stats.TotalProfitRealized),
formatEther(stats.TotalGasSpent)))
}
// generateOpportunityID generates a unique ID for an arbitrage opportunity
func (as *ArbitrageService) generateOpportunityID(path *ArbitragePath, event *events.SwapEvent) string {
return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano())
}
// GetStats returns current service statistics
func (as *ArbitrageService) GetStats() *ArbitrageStats {
as.statsMutex.RLock()
defer as.statsMutex.RUnlock()
// Return a copy to avoid race conditions
statsCopy := *as.stats
return &statsCopy
}
// IsRunning returns whether the service is currently running
func (as *ArbitrageService) IsRunning() bool {
as.runMutex.RLock()
defer as.runMutex.RUnlock()
return as.isRunning
}