Files
mev-beta/pkg/arbitrum/profitability_tracker.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- 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>
2025-10-17 00:12:55 -05:00

588 lines
19 KiB
Go

package arbitrum
import (
"encoding/json"
"fmt"
"math"
"math/big"
"os"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/security"
)
// ProfitabilityTracker tracks detailed profitability metrics and analytics
type ProfitabilityTracker struct {
logger *logger.Logger
mu sync.RWMutex
// Performance metrics
totalTrades uint64
successfulTrades uint64
failedTrades uint64
totalProfit *big.Int
totalGasCost *big.Int
netProfit *big.Int
// Time-based metrics
hourlyStats map[string]*HourlyStats
dailyStats map[string]*DailyStats
weeklyStats map[string]*WeeklyStats
// Token pair analytics
tokenPairStats map[string]*TokenPairStats
// Exchange analytics
exchangeStats map[string]*ExchangeStats
// Opportunity analytics
opportunityStats *OpportunityStats
// File handles for logging
profitLogFile *os.File
opportunityLogFile *os.File
performanceLogFile *os.File
startTime time.Time
}
// HourlyStats represents hourly performance statistics
type HourlyStats struct {
Hour string `json:"hour"`
Trades uint64 `json:"trades"`
SuccessfulTrades uint64 `json:"successful_trades"`
TotalProfit string `json:"total_profit_wei"`
TotalGasCost string `json:"total_gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
AverageROI float64 `json:"average_roi"`
BestTrade string `json:"best_trade_profit_wei"`
WorstTrade string `json:"worst_trade_loss_wei"`
Timestamp time.Time `json:"timestamp"`
}
// DailyStats represents daily performance statistics
type DailyStats struct {
Date string `json:"date"`
Trades uint64 `json:"trades"`
SuccessfulTrades uint64 `json:"successful_trades"`
TotalProfit string `json:"total_profit_wei"`
TotalGasCost string `json:"total_gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
ProfitUSD float64 `json:"profit_usd"`
ROI float64 `json:"roi"`
SuccessRate float64 `json:"success_rate"`
CapitalEfficiency float64 `json:"capital_efficiency"`
Timestamp time.Time `json:"timestamp"`
}
// WeeklyStats represents weekly performance statistics
type WeeklyStats struct {
Week string `json:"week"`
Trades uint64 `json:"trades"`
SuccessfulTrades uint64 `json:"successful_trades"`
TotalProfit string `json:"total_profit_wei"`
TotalGasCost string `json:"total_gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
ProfitUSD float64 `json:"profit_usd"`
WeeklyROI float64 `json:"weekly_roi"`
BestDay string `json:"best_day"`
WorstDay string `json:"worst_day"`
Timestamp time.Time `json:"timestamp"`
}
// TokenPairStats represents statistics for specific token pairs
type TokenPairStats struct {
TokenIn string `json:"token_in"`
TokenOut string `json:"token_out"`
Trades uint64 `json:"trades"`
SuccessfulTrades uint64 `json:"successful_trades"`
TotalProfit string `json:"total_profit_wei"`
TotalGasCost string `json:"total_gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
AverageProfit string `json:"average_profit_wei"`
BestTrade string `json:"best_trade_profit_wei"`
SuccessRate float64 `json:"success_rate"`
AverageExecutionTime time.Duration `json:"average_execution_time"`
LastTrade time.Time `json:"last_trade"`
}
// ExchangeStats represents statistics for specific exchanges
type ExchangeStats struct {
Exchange string `json:"exchange"`
Trades uint64 `json:"trades"`
SuccessfulTrades uint64 `json:"successful_trades"`
TotalProfit string `json:"total_profit_wei"`
TotalGasCost string `json:"total_gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
SuccessRate float64 `json:"success_rate"`
AverageProfit string `json:"average_profit_wei"`
LastTrade time.Time `json:"last_trade"`
}
// OpportunityStats represents overall opportunity statistics
type OpportunityStats struct {
TotalOpportunities uint64 `json:"total_opportunities"`
ExecutedOpportunities uint64 `json:"executed_opportunities"`
SkippedDueToCapital uint64 `json:"skipped_due_to_capital"`
SkippedDueToGas uint64 `json:"skipped_due_to_gas"`
SkippedDueToRisk uint64 `json:"skipped_due_to_risk"`
ExecutionRate float64 `json:"execution_rate"`
LastOpportunity time.Time `json:"last_opportunity"`
}
// TradeEvent represents a trade event for logging
type TradeEvent struct {
Timestamp time.Time `json:"timestamp"`
TradeID string `json:"trade_id"`
Type string `json:"type"` // "arbitrage", "sandwich", "liquidation"
TokenIn string `json:"token_in"`
TokenOut string `json:"token_out"`
AmountIn string `json:"amount_in_wei"`
ExpectedProfit string `json:"expected_profit_wei"`
ActualProfit string `json:"actual_profit_wei"`
GasCost string `json:"gas_cost_wei"`
NetProfit string `json:"net_profit_wei"`
ROI float64 `json:"roi"`
ExecutionTime string `json:"execution_time"`
Success bool `json:"success"`
Exchange string `json:"exchange"`
Pool string `json:"pool"`
TxHash string `json:"tx_hash"`
FailureReason string `json:"failure_reason,omitempty"`
}
// NewProfitabilityTracker creates a new profitability tracker
func NewProfitabilityTracker(logger *logger.Logger) (*ProfitabilityTracker, error) {
// Create log files
profitFile, err := os.OpenFile("logs/profitability.jsonl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to create profit log file: %w", err)
}
opportunityFile, err := os.OpenFile("logs/opportunities.jsonl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to create opportunity log file: %w", err)
}
performanceFile, err := os.OpenFile("logs/performance.jsonl", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to create performance log file: %w", err)
}
return &ProfitabilityTracker{
logger: logger,
totalProfit: big.NewInt(0),
totalGasCost: big.NewInt(0),
netProfit: big.NewInt(0),
hourlyStats: make(map[string]*HourlyStats),
dailyStats: make(map[string]*DailyStats),
weeklyStats: make(map[string]*WeeklyStats),
tokenPairStats: make(map[string]*TokenPairStats),
exchangeStats: make(map[string]*ExchangeStats),
opportunityStats: &OpportunityStats{},
profitLogFile: profitFile,
opportunityLogFile: opportunityFile,
performanceLogFile: performanceFile,
startTime: time.Now(),
}, nil
}
// LogTrade logs a completed trade and updates all relevant statistics
func (pt *ProfitabilityTracker) LogTrade(trade *TradeEvent) {
pt.mu.Lock()
defer pt.mu.Unlock()
// Update overall metrics
pt.totalTrades++
if trade.Success {
pt.successfulTrades++
if actualProfit, ok := new(big.Int).SetString(trade.ActualProfit, 10); ok {
pt.totalProfit.Add(pt.totalProfit, actualProfit)
}
} else {
pt.failedTrades++
}
if gasCost, ok := new(big.Int).SetString(trade.GasCost, 10); ok {
pt.totalGasCost.Add(pt.totalGasCost, gasCost)
}
pt.netProfit = new(big.Int).Sub(pt.totalProfit, pt.totalGasCost)
// Update time-based statistics
pt.updateHourlyStats(trade)
pt.updateDailyStats(trade)
pt.updateWeeklyStats(trade)
// Update token pair statistics
pt.updateTokenPairStats(trade)
// Update exchange statistics
pt.updateExchangeStats(trade)
// Log to file
pt.logTradeToFile(trade)
// Log performance metrics every 10 trades
if pt.totalTrades%10 == 0 {
pt.logPerformanceMetrics()
}
}
// LogOpportunity logs an arbitrage opportunity (executed or skipped)
func (pt *ProfitabilityTracker) LogOpportunity(tokenIn, tokenOut common.Address, profit *big.Int, executed bool, skipReason string) {
pt.mu.Lock()
defer pt.mu.Unlock()
pt.opportunityStats.TotalOpportunities++
pt.opportunityStats.LastOpportunity = time.Now()
if executed {
pt.opportunityStats.ExecutedOpportunities++
} else {
switch skipReason {
case "capital":
pt.opportunityStats.SkippedDueToCapital++
case "gas":
pt.opportunityStats.SkippedDueToGas++
case "risk":
pt.opportunityStats.SkippedDueToRisk++
}
}
// Update execution rate
if pt.opportunityStats.TotalOpportunities > 0 {
pt.opportunityStats.ExecutionRate = float64(pt.opportunityStats.ExecutedOpportunities) / float64(pt.opportunityStats.TotalOpportunities)
}
// Log to opportunity file
opportunityEvent := map[string]interface{}{
"timestamp": time.Now(),
"token_in": tokenIn.Hex(),
"token_out": tokenOut.Hex(),
"profit_wei": profit.String(),
"executed": executed,
"skip_reason": skipReason,
}
if data, err := json.Marshal(opportunityEvent); err == nil {
if _, writeErr := pt.opportunityLogFile.Write(append(data, '\n')); writeErr != nil {
pt.logger.Error("Failed to write opportunity log", "error", writeErr)
}
if syncErr := pt.opportunityLogFile.Sync(); syncErr != nil {
pt.logger.Error("Failed to sync opportunity log", "error", syncErr)
}
}
}
// GetRealTimeStats returns current real-time statistics
func (pt *ProfitabilityTracker) GetRealTimeStats() map[string]interface{} {
pt.mu.RLock()
defer pt.mu.RUnlock()
runtime := time.Since(pt.startTime)
successRate := 0.0
if pt.totalTrades > 0 {
successRate = float64(pt.successfulTrades) / float64(pt.totalTrades)
}
avgTradesPerHour := 0.0
if runtime.Hours() > 0 {
avgTradesPerHour = float64(pt.totalTrades) / runtime.Hours()
}
profitUSD := pt.weiToUSD(pt.netProfit)
dailyProfitProjection := profitUSD * (24.0 / runtime.Hours())
return map[string]interface{}{
"runtime_hours": runtime.Hours(),
"total_trades": pt.totalTrades,
"successful_trades": pt.successfulTrades,
"failed_trades": pt.failedTrades,
"success_rate": successRate,
"total_profit_wei": pt.totalProfit.String(),
"total_gas_cost_wei": pt.totalGasCost.String(),
"net_profit_wei": pt.netProfit.String(),
"net_profit_usd": profitUSD,
"daily_profit_projection": dailyProfitProjection,
"avg_trades_per_hour": avgTradesPerHour,
"execution_rate": pt.opportunityStats.ExecutionRate,
"total_opportunities": pt.opportunityStats.TotalOpportunities,
"executed_opportunities": pt.opportunityStats.ExecutedOpportunities,
}
}
// GetTopTokenPairs returns the most profitable token pairs
func (pt *ProfitabilityTracker) GetTopTokenPairs(limit int) []*TokenPairStats {
pt.mu.RLock()
defer pt.mu.RUnlock()
var pairs []*TokenPairStats
for _, stats := range pt.tokenPairStats {
pairs = append(pairs, stats)
}
// Sort by net profit
for i := 0; i < len(pairs)-1; i++ {
for j := i + 1; j < len(pairs); j++ {
profitI, _ := new(big.Int).SetString(pairs[i].NetProfit, 10)
profitJ, _ := new(big.Int).SetString(pairs[j].NetProfit, 10)
if profitI.Cmp(profitJ) < 0 {
pairs[i], pairs[j] = pairs[j], pairs[i]
}
}
}
if len(pairs) > limit {
pairs = pairs[:limit]
}
return pairs
}
// Private helper methods
func (pt *ProfitabilityTracker) updateHourlyStats(trade *TradeEvent) {
hour := trade.Timestamp.Format("2006-01-02-15")
stats, exists := pt.hourlyStats[hour]
if !exists {
stats = &HourlyStats{
Hour: hour,
Timestamp: trade.Timestamp,
}
pt.hourlyStats[hour] = stats
}
stats.Trades++
if trade.Success {
stats.SuccessfulTrades++
if profit, ok := new(big.Int).SetString(trade.ActualProfit, 10); ok && profit.Sign() > 0 {
currentProfit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
newProfit := new(big.Int).Add(currentProfit, profit)
stats.TotalProfit = newProfit.String()
}
}
if gasCost, ok := new(big.Int).SetString(trade.GasCost, 10); ok {
currentGas, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
newGas := new(big.Int).Add(currentGas, gasCost)
stats.TotalGasCost = newGas.String()
}
// Calculate net profit
profit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
gasCost, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
netProfit := new(big.Int).Sub(profit, gasCost)
stats.NetProfit = netProfit.String()
}
func (pt *ProfitabilityTracker) updateDailyStats(trade *TradeEvent) {
date := trade.Timestamp.Format("2006-01-02")
stats, exists := pt.dailyStats[date]
if !exists {
stats = &DailyStats{
Date: date,
Timestamp: trade.Timestamp,
}
pt.dailyStats[date] = stats
}
stats.Trades++
if trade.Success {
stats.SuccessfulTrades++
if profit, ok := new(big.Int).SetString(trade.ActualProfit, 10); ok && profit.Sign() > 0 {
currentProfit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
newProfit := new(big.Int).Add(currentProfit, profit)
stats.TotalProfit = newProfit.String()
}
}
if gasCost, ok := new(big.Int).SetString(trade.GasCost, 10); ok {
currentGas, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
newGas := new(big.Int).Add(currentGas, gasCost)
stats.TotalGasCost = newGas.String()
}
// Calculate derived metrics
profit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
gasCost, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
netProfit := new(big.Int).Sub(profit, gasCost)
stats.NetProfit = netProfit.String()
stats.ProfitUSD = pt.weiToUSD(netProfit)
if stats.Trades > 0 {
stats.SuccessRate = float64(stats.SuccessfulTrades) / float64(stats.Trades)
}
}
func (pt *ProfitabilityTracker) updateWeeklyStats(trade *TradeEvent) {
// Implementation similar to daily stats but for weekly periods
year, week := trade.Timestamp.ISOWeek()
weekKey := fmt.Sprintf("%d-W%02d", year, week)
stats, exists := pt.weeklyStats[weekKey]
if !exists {
stats = &WeeklyStats{
Week: weekKey,
Timestamp: trade.Timestamp,
}
pt.weeklyStats[weekKey] = stats
}
stats.Trades++
if trade.Success {
stats.SuccessfulTrades++
}
}
func (pt *ProfitabilityTracker) updateTokenPairStats(trade *TradeEvent) {
pairKey := fmt.Sprintf("%s-%s", trade.TokenIn, trade.TokenOut)
stats, exists := pt.tokenPairStats[pairKey]
if !exists {
stats = &TokenPairStats{
TokenIn: trade.TokenIn,
TokenOut: trade.TokenOut,
}
pt.tokenPairStats[pairKey] = stats
}
stats.Trades++
stats.LastTrade = trade.Timestamp
if trade.Success {
stats.SuccessfulTrades++
if profit, ok := new(big.Int).SetString(trade.ActualProfit, 10); ok && profit.Sign() > 0 {
currentProfit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
newProfit := new(big.Int).Add(currentProfit, profit)
stats.TotalProfit = newProfit.String()
}
}
if gasCost, ok := new(big.Int).SetString(trade.GasCost, 10); ok {
currentGas, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
newGas := new(big.Int).Add(currentGas, gasCost)
stats.TotalGasCost = newGas.String()
}
// Calculate derived metrics
profit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
gasCost, _ := new(big.Int).SetString(stats.TotalGasCost, 10)
netProfit := new(big.Int).Sub(profit, gasCost)
stats.NetProfit = netProfit.String()
if stats.Trades > 0 {
stats.SuccessRate = float64(stats.SuccessfulTrades) / float64(stats.Trades)
}
// Calculate average profit per successful trade
if stats.SuccessfulTrades > 0 {
successfulTradesInt64, err := security.SafeUint64ToInt64(stats.SuccessfulTrades)
if err != nil {
pt.logger.Error("Successful trades count exceeds int64 maximum", "successfulTrades", stats.SuccessfulTrades, "error", err)
// Use maximum safe value as fallback to avoid division by zero
successfulTradesInt64 = math.MaxInt64
}
avgProfit := new(big.Int).Div(profit, big.NewInt(successfulTradesInt64))
stats.AverageProfit = avgProfit.String()
} else {
// If no successful trades, average profit is 0
stats.AverageProfit = "0"
}
}
func (pt *ProfitabilityTracker) updateExchangeStats(trade *TradeEvent) {
stats, exists := pt.exchangeStats[trade.Exchange]
if !exists {
stats = &ExchangeStats{
Exchange: trade.Exchange,
}
pt.exchangeStats[trade.Exchange] = stats
}
stats.Trades++
stats.LastTrade = trade.Timestamp
if trade.Success {
stats.SuccessfulTrades++
if profit, ok := new(big.Int).SetString(trade.ActualProfit, 10); ok && profit.Sign() > 0 {
currentProfit, _ := new(big.Int).SetString(stats.TotalProfit, 10)
newProfit := new(big.Int).Add(currentProfit, profit)
stats.TotalProfit = newProfit.String()
}
}
if stats.Trades > 0 {
stats.SuccessRate = float64(stats.SuccessfulTrades) / float64(stats.Trades)
}
}
func (pt *ProfitabilityTracker) logTradeToFile(trade *TradeEvent) {
if data, err := json.Marshal(trade); err == nil {
if _, writeErr := pt.profitLogFile.Write(append(data, '\n')); writeErr != nil {
pt.logger.Error("Failed to write trade data to profit log", "error", writeErr)
}
if syncErr := pt.profitLogFile.Sync(); syncErr != nil {
pt.logger.Error("Failed to sync profit log file", "error", syncErr)
}
} else {
pt.logger.Error("Failed to marshal trade event for logging", "error", err)
}
}
func (pt *ProfitabilityTracker) logPerformanceMetrics() {
metrics := pt.GetRealTimeStats()
metrics["timestamp"] = time.Now()
metrics["type"] = "performance_snapshot"
if data, err := json.Marshal(metrics); err == nil {
if _, writeErr := pt.performanceLogFile.Write(append(data, '\n')); writeErr != nil {
pt.logger.Error("Failed to write performance metrics", "error", writeErr)
}
if syncErr := pt.performanceLogFile.Sync(); syncErr != nil {
pt.logger.Error("Failed to sync performance log file", "error", syncErr)
}
} else {
pt.logger.Error("Failed to marshal performance metrics", "error", err)
}
}
func (pt *ProfitabilityTracker) weiToUSD(wei *big.Int) float64 {
// Assume ETH = $2000 for calculations
ethPrice := 2000.0
ethAmount := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18))
ethFloat, _ := ethAmount.Float64()
return ethFloat * ethPrice
}
// Close closes all log files
func (pt *ProfitabilityTracker) Close() error {
var errors []error
if pt.profitLogFile != nil {
if err := pt.profitLogFile.Close(); err != nil {
errors = append(errors, err)
}
}
if pt.opportunityLogFile != nil {
if err := pt.opportunityLogFile.Close(); err != nil {
errors = append(errors, err)
}
}
if pt.performanceLogFile != nil {
if err := pt.performanceLogFile.Close(); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return fmt.Errorf("errors closing profitability tracker: %v", errors)
}
return nil
}