686 lines
19 KiB
Go
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
|
|
} |