feat: Implement comprehensive Market Manager with database and logging

- Add complete Market Manager package with in-memory storage and CRUD operations
- Implement arbitrage detection with profit calculations and thresholds
- Add database adapter with PostgreSQL schema for persistence
- Create comprehensive logging system with specialized log files
- Add detailed documentation and implementation plans
- Include example application and comprehensive test suite
- Update Makefile with market manager build targets
- Add check-implementations command for verification
This commit is contained in:
Krypto Kajun
2025-09-18 03:52:33 -05:00
parent ac9798a7e5
commit fac8a64092
35 changed files with 6595 additions and 8 deletions

View File

@@ -0,0 +1,346 @@
package profitcalc
import (
"math"
"math/big"
"sort"
"time"
"github.com/fraktal/mev-beta/internal/logger"
)
// OpportunityRanker handles filtering, ranking, and prioritization of arbitrage opportunities
type OpportunityRanker struct {
logger *logger.Logger
maxOpportunities int // Maximum number of opportunities to track
minConfidence float64 // Minimum confidence score to consider
minProfitMargin float64 // Minimum profit margin to consider
opportunityTTL time.Duration // How long opportunities are valid
recentOpportunities []*RankedOpportunity
}
// RankedOpportunity wraps SimpleOpportunity with ranking data
type RankedOpportunity struct {
*SimpleOpportunity
Score float64 // Composite ranking score
Rank int // Current rank (1 = best)
FirstSeen time.Time // When this opportunity was first detected
LastUpdated time.Time // When this opportunity was last updated
UpdateCount int // How many times we've seen this opportunity
IsStale bool // Whether this opportunity is too old
CompetitionRisk float64 // Risk due to MEV competition
}
// RankingWeights defines weights for different ranking factors
type RankingWeights struct {
ProfitMargin float64 // Weight for profit margin (0-1)
NetProfit float64 // Weight for absolute profit (0-1)
Confidence float64 // Weight for confidence score (0-1)
TradeSize float64 // Weight for trade size (0-1)
Freshness float64 // Weight for opportunity freshness (0-1)
Competition float64 // Weight for competition risk (0-1, negative)
GasEfficiency float64 // Weight for gas efficiency (0-1)
}
// DefaultRankingWeights provides sensible default weights
var DefaultRankingWeights = RankingWeights{
ProfitMargin: 0.3, // 30% - profit margin is very important
NetProfit: 0.25, // 25% - absolute profit matters
Confidence: 0.2, // 20% - confidence in the opportunity
TradeSize: 0.1, // 10% - larger trades are preferred
Freshness: 0.1, // 10% - fresher opportunities are better
Competition: 0.05, // 5% - competition risk (negative)
GasEfficiency: 0.1, // 10% - gas efficiency
}
// NewOpportunityRanker creates a new opportunity ranker
func NewOpportunityRanker(logger *logger.Logger) *OpportunityRanker {
return &OpportunityRanker{
logger: logger,
maxOpportunities: 50, // Track top 50 opportunities
minConfidence: 0.3, // Minimum 30% confidence
minProfitMargin: 0.001, // Minimum 0.1% profit margin
opportunityTTL: 5 * time.Minute, // Opportunities valid for 5 minutes
recentOpportunities: make([]*RankedOpportunity, 0, 50),
}
}
// AddOpportunity adds a new opportunity to the ranking system
func (or *OpportunityRanker) AddOpportunity(opp *SimpleOpportunity) *RankedOpportunity {
if opp == nil {
return nil
}
// Filter out opportunities that don't meet minimum criteria
if !or.passesFilters(opp) {
or.logger.Debug("Opportunity filtered out: ID=%s, Confidence=%.2f, ProfitMargin=%.4f",
opp.ID, opp.Confidence, opp.ProfitMargin)
return nil
}
// Check if we already have this opportunity (based on token pair and similar amounts)
existingOpp := or.findSimilarOpportunity(opp)
if existingOpp != nil {
// Update existing opportunity
existingOpp.SimpleOpportunity = opp
existingOpp.LastUpdated = time.Now()
existingOpp.UpdateCount++
or.logger.Debug("Updated existing opportunity: ID=%s, UpdateCount=%d",
opp.ID, existingOpp.UpdateCount)
} else {
// Create new ranked opportunity
rankedOpp := &RankedOpportunity{
SimpleOpportunity: opp,
FirstSeen: time.Now(),
LastUpdated: time.Now(),
UpdateCount: 1,
IsStale: false,
CompetitionRisk: or.estimateCompetitionRisk(opp),
}
or.recentOpportunities = append(or.recentOpportunities, rankedOpp)
or.logger.Debug("Added new opportunity: ID=%s, ProfitMargin=%.4f, Confidence=%.2f",
opp.ID, opp.ProfitMargin, opp.Confidence)
}
// Cleanup stale opportunities and re-rank
or.cleanupStaleOpportunities()
or.rankOpportunities()
return or.findRankedOpportunity(opp.ID)
}
// GetTopOpportunities returns the top N ranked opportunities
func (or *OpportunityRanker) GetTopOpportunities(limit int) []*RankedOpportunity {
if limit <= 0 || limit > len(or.recentOpportunities) {
limit = len(or.recentOpportunities)
}
// Ensure opportunities are ranked
or.rankOpportunities()
result := make([]*RankedOpportunity, limit)
copy(result, or.recentOpportunities[:limit])
return result
}
// GetExecutableOpportunities returns only opportunities marked as executable
func (or *OpportunityRanker) GetExecutableOpportunities(limit int) []*RankedOpportunity {
executable := make([]*RankedOpportunity, 0)
for _, opp := range or.recentOpportunities {
if opp.IsExecutable && !opp.IsStale {
executable = append(executable, opp)
}
}
if limit > 0 && len(executable) > limit {
executable = executable[:limit]
}
return executable
}
// passesFilters checks if an opportunity meets minimum criteria
func (or *OpportunityRanker) passesFilters(opp *SimpleOpportunity) bool {
// Check confidence threshold
if opp.Confidence < or.minConfidence {
return false
}
// Check profit margin threshold
if opp.ProfitMargin < or.minProfitMargin {
return false
}
// Check that net profit is positive
if opp.NetProfit == nil || opp.NetProfit.Sign() <= 0 {
return false
}
return true
}
// findSimilarOpportunity finds an existing opportunity with same token pair
func (or *OpportunityRanker) findSimilarOpportunity(newOpp *SimpleOpportunity) *RankedOpportunity {
for _, existing := range or.recentOpportunities {
if existing.TokenA == newOpp.TokenA && existing.TokenB == newOpp.TokenB {
// Consider similar if within 10% of amount
if existing.AmountIn != nil && newOpp.AmountIn != nil {
ratio := new(big.Float).Quo(existing.AmountIn, newOpp.AmountIn)
ratioFloat, _ := ratio.Float64()
if ratioFloat > 0.9 && ratioFloat < 1.1 {
return existing
}
}
}
}
return nil
}
// findRankedOpportunity finds a ranked opportunity by ID
func (or *OpportunityRanker) findRankedOpportunity(id string) *RankedOpportunity {
for _, opp := range or.recentOpportunities {
if opp.ID == id {
return opp
}
}
return nil
}
// estimateCompetitionRisk estimates MEV competition risk for an opportunity
func (or *OpportunityRanker) estimateCompetitionRisk(opp *SimpleOpportunity) float64 {
risk := 0.0
// Higher profit margins attract more competition
if opp.ProfitMargin > 0.05 { // > 5%
risk += 0.8
} else if opp.ProfitMargin > 0.02 { // > 2%
risk += 0.5
} else if opp.ProfitMargin > 0.01 { // > 1%
risk += 0.3
} else {
risk += 0.1
}
// Larger trades attract more competition
if opp.AmountOut != nil {
amountFloat, _ := opp.AmountOut.Float64()
if amountFloat > 10000 { // > $10k
risk += 0.3
} else if amountFloat > 1000 { // > $1k
risk += 0.1
}
}
// Cap risk at 1.0
if risk > 1.0 {
risk = 1.0
}
return risk
}
// rankOpportunities ranks all opportunities and assigns scores
func (or *OpportunityRanker) rankOpportunities() {
weights := DefaultRankingWeights
for _, opp := range or.recentOpportunities {
opp.Score = or.calculateOpportunityScore(opp, weights)
}
// Sort by score (highest first)
sort.Slice(or.recentOpportunities, func(i, j int) bool {
return or.recentOpportunities[i].Score > or.recentOpportunities[j].Score
})
// Assign ranks
for i, opp := range or.recentOpportunities {
opp.Rank = i + 1
}
}
// calculateOpportunityScore calculates a composite score for an opportunity
func (or *OpportunityRanker) calculateOpportunityScore(opp *RankedOpportunity, weights RankingWeights) float64 {
score := 0.0
// Profit margin component (0-1)
profitMarginScore := math.Min(opp.ProfitMargin/0.1, 1.0) // Cap at 10% margin = 1.0
score += profitMarginScore * weights.ProfitMargin
// Net profit component (0-1, normalized by 0.1 ETH = 1.0)
netProfitScore := 0.0
if opp.NetProfit != nil {
netProfitFloat, _ := opp.NetProfit.Float64()
netProfitScore = math.Min(netProfitFloat/0.1, 1.0)
}
score += netProfitScore * weights.NetProfit
// Confidence component (already 0-1)
score += opp.Confidence * weights.Confidence
// Trade size component (0-1, normalized by $10k = 1.0)
tradeSizeScore := 0.0
if opp.AmountOut != nil {
amountFloat, _ := opp.AmountOut.Float64()
tradeSizeScore = math.Min(amountFloat/10000, 1.0)
}
score += tradeSizeScore * weights.TradeSize
// Freshness component (1.0 for new, decays over time)
age := time.Since(opp.FirstSeen)
freshnessScore := math.Max(0, 1.0-age.Seconds()/300) // Decays to 0 over 5 minutes
score += freshnessScore * weights.Freshness
// Competition risk component (negative)
score -= opp.CompetitionRisk * weights.Competition
// Gas efficiency component (profit per gas unit)
gasEfficiencyScore := 0.0
if opp.GasCost != nil && opp.GasCost.Sign() > 0 && opp.NetProfit != nil {
gasCostFloat, _ := opp.GasCost.Float64()
netProfitFloat, _ := opp.NetProfit.Float64()
gasEfficiencyScore = math.Min(netProfitFloat/gasCostFloat/10, 1.0) // Profit 10x gas cost = 1.0
}
score += gasEfficiencyScore * weights.GasEfficiency
return math.Max(0, score) // Ensure score is non-negative
}
// cleanupStaleOpportunities removes old opportunities
func (or *OpportunityRanker) cleanupStaleOpportunities() {
now := time.Now()
validOpportunities := make([]*RankedOpportunity, 0, len(or.recentOpportunities))
for _, opp := range or.recentOpportunities {
age := now.Sub(opp.FirstSeen)
if age <= or.opportunityTTL {
validOpportunities = append(validOpportunities, opp)
} else {
opp.IsStale = true
or.logger.Debug("Marked opportunity as stale: ID=%s, Age=%s", opp.ID, age)
}
}
// Keep only valid opportunities, but respect max limit
if len(validOpportunities) > or.maxOpportunities {
// Sort by score to keep the best ones
sort.Slice(validOpportunities, func(i, j int) bool {
return validOpportunities[i].Score > validOpportunities[j].Score
})
validOpportunities = validOpportunities[:or.maxOpportunities]
}
or.recentOpportunities = validOpportunities
}
// GetStats returns statistics about tracked opportunities
func (or *OpportunityRanker) GetStats() map[string]interface{} {
executableCount := 0
totalScore := 0.0
avgConfidence := 0.0
for _, opp := range or.recentOpportunities {
if opp.IsExecutable {
executableCount++
}
totalScore += opp.Score
avgConfidence += opp.Confidence
}
count := len(or.recentOpportunities)
if count > 0 {
avgConfidence /= float64(count)
}
return map[string]interface{}{
"totalOpportunities": count,
"executableOpportunities": executableCount,
"averageScore": totalScore / float64(count),
"averageConfidence": avgConfidence,
"maxOpportunities": or.maxOpportunities,
"minConfidence": or.minConfidence,
"minProfitMargin": or.minProfitMargin,
"opportunityTTL": or.opportunityTTL.String(),
}
}

View File

@@ -0,0 +1,323 @@
package profitcalc
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/logger"
)
// PriceFeed provides real-time price data from multiple DEXs
type PriceFeed struct {
logger *logger.Logger
client *ethclient.Client
priceCache map[string]*PriceData
priceMutex sync.RWMutex
updateTicker *time.Ticker
stopChan chan struct{}
// DEX addresses for price queries
uniswapV3Factory common.Address
uniswapV2Factory common.Address
sushiswapFactory common.Address
camelotFactory common.Address
traderJoeFactory common.Address
}
// PriceData represents price information from a DEX
type PriceData struct {
TokenA common.Address
TokenB common.Address
Price *big.Float // Token B per Token A
InversePrice *big.Float // Token A per Token B
Liquidity *big.Float // Total liquidity in pool
DEX string // DEX name
PoolAddress common.Address
LastUpdated time.Time
IsValid bool
}
// MultiDEXPriceData aggregates prices from multiple DEXs
type MultiDEXPriceData struct {
TokenA common.Address
TokenB common.Address
Prices []*PriceData
BestBuyDEX *PriceData // Best DEX to buy Token A (lowest price)
BestSellDEX *PriceData // Best DEX to sell Token A (highest price)
PriceSpread *big.Float // Price difference between best buy/sell
SpreadBps int64 // Spread in basis points
LastUpdated time.Time
}
// NewPriceFeed creates a new price feed manager
func NewPriceFeed(logger *logger.Logger, client *ethclient.Client) *PriceFeed {
return &PriceFeed{
logger: logger,
client: client,
priceCache: make(map[string]*PriceData),
stopChan: make(chan struct{}),
// Arbitrum DEX factory addresses
uniswapV3Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
uniswapV2Factory: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), // SushiSwap on Arbitrum
sushiswapFactory: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"),
camelotFactory: common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B82A80f"),
traderJoeFactory: common.HexToAddress("0xaE4EC9901c3076D0DdBe76A520F9E90a6227aCB7"),
}
}
// Start begins the price feed updates
func (pf *PriceFeed) Start() {
pf.updateTicker = time.NewTicker(15 * time.Second) // Update every 15 seconds
go pf.priceUpdateLoop()
pf.logger.Info("Price feed started with 15-second update interval")
}
// Stop halts the price feed updates
func (pf *PriceFeed) Stop() {
if pf.updateTicker != nil {
pf.updateTicker.Stop()
}
close(pf.stopChan)
pf.logger.Info("Price feed stopped")
}
// GetMultiDEXPrice gets aggregated price data from multiple DEXs
func (pf *PriceFeed) GetMultiDEXPrice(tokenA, tokenB common.Address) *MultiDEXPriceData {
pf.priceMutex.RLock()
defer pf.priceMutex.RUnlock()
var prices []*PriceData
var bestBuy, bestSell *PriceData
// Collect prices from all DEXs
for _, price := range pf.priceCache {
if (price.TokenA == tokenA && price.TokenB == tokenB) ||
(price.TokenA == tokenB && price.TokenB == tokenA) {
if price.IsValid && time.Since(price.LastUpdated) < 5*time.Minute {
prices = append(prices, price)
// Find best buy price (lowest price to buy tokenA)
if bestBuy == nil || price.Price.Cmp(bestBuy.Price) < 0 {
bestBuy = price
}
// Find best sell price (highest price to sell tokenA)
if bestSell == nil || price.Price.Cmp(bestSell.Price) > 0 {
bestSell = price
}
}
}
}
if len(prices) == 0 {
return nil
}
// Calculate price spread
var priceSpread *big.Float
var spreadBps int64
if bestBuy != nil && bestSell != nil && bestBuy != bestSell {
priceSpread = new(big.Float).Sub(bestSell.Price, bestBuy.Price)
// Calculate spread in basis points
spreadRatio := new(big.Float).Quo(priceSpread, bestBuy.Price)
spreadFloat, _ := spreadRatio.Float64()
spreadBps = int64(spreadFloat * 10000) // Convert to basis points
}
return &MultiDEXPriceData{
TokenA: tokenA,
TokenB: tokenB,
Prices: prices,
BestBuyDEX: bestBuy,
BestSellDEX: bestSell,
PriceSpread: priceSpread,
SpreadBps: spreadBps,
LastUpdated: time.Now(),
}
}
// GetBestArbitrageOpportunity finds the best arbitrage opportunity for a token pair
func (pf *PriceFeed) GetBestArbitrageOpportunity(tokenA, tokenB common.Address, tradeAmount *big.Float) *ArbitrageRoute {
multiPrice := pf.GetMultiDEXPrice(tokenA, tokenB)
if multiPrice == nil || multiPrice.BestBuyDEX == nil || multiPrice.BestSellDEX == nil {
return nil
}
// Skip if same DEX or insufficient spread
if multiPrice.BestBuyDEX.DEX == multiPrice.BestSellDEX.DEX || multiPrice.SpreadBps < 50 {
return nil
}
// Calculate potential profit
buyPrice := multiPrice.BestBuyDEX.Price
sellPrice := multiPrice.BestSellDEX.Price
// Amount out when buying tokenA
amountOut := new(big.Float).Quo(tradeAmount, buyPrice)
// Revenue when selling tokenA
revenue := new(big.Float).Mul(amountOut, sellPrice)
// Gross profit
grossProfit := new(big.Float).Sub(revenue, tradeAmount)
return &ArbitrageRoute{
TokenA: tokenA,
TokenB: tokenB,
BuyDEX: multiPrice.BestBuyDEX.DEX,
SellDEX: multiPrice.BestSellDEX.DEX,
BuyPrice: buyPrice,
SellPrice: sellPrice,
TradeAmount: tradeAmount,
AmountOut: amountOut,
GrossProfit: grossProfit,
SpreadBps: multiPrice.SpreadBps,
Timestamp: time.Now(),
}
}
// ArbitrageRoute represents a complete arbitrage route
type ArbitrageRoute struct {
TokenA common.Address
TokenB common.Address
BuyDEX string
SellDEX string
BuyPrice *big.Float
SellPrice *big.Float
TradeAmount *big.Float
AmountOut *big.Float
GrossProfit *big.Float
SpreadBps int64
Timestamp time.Time
}
// priceUpdateLoop runs the background price update process
func (pf *PriceFeed) priceUpdateLoop() {
defer pf.updateTicker.Stop()
// Major trading pairs on Arbitrum
tradingPairs := []TokenPair{
{
TokenA: common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"), // WETH
TokenB: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // USDC
},
{
TokenA: common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"), // WETH
TokenB: common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"), // ARB
},
{
TokenA: common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // USDC
TokenB: common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"), // USDT
},
{
TokenA: common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"), // WETH
TokenB: common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"), // WBTC
},
}
for {
select {
case <-pf.stopChan:
return
case <-pf.updateTicker.C:
pf.updatePricesForPairs(tradingPairs)
}
}
}
// TokenPair represents a trading pair
type TokenPair struct {
TokenA common.Address
TokenB common.Address
}
// updatePricesForPairs updates prices for specified trading pairs
func (pf *PriceFeed) updatePricesForPairs(pairs []TokenPair) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for _, pair := range pairs {
// Update prices from multiple DEXs
go pf.updatePriceFromDEX(ctx, pair.TokenA, pair.TokenB, "UniswapV3", pf.uniswapV3Factory)
go pf.updatePriceFromDEX(ctx, pair.TokenA, pair.TokenB, "SushiSwap", pf.sushiswapFactory)
go pf.updatePriceFromDEX(ctx, pair.TokenA, pair.TokenB, "Camelot", pf.camelotFactory)
go pf.updatePriceFromDEX(ctx, pair.TokenA, pair.TokenB, "TraderJoe", pf.traderJoeFactory)
}
}
// updatePriceFromDEX updates price data from a specific DEX
func (pf *PriceFeed) updatePriceFromDEX(ctx context.Context, tokenA, tokenB common.Address, dexName string, factory common.Address) {
// This is a simplified implementation
// In a real implementation, you would:
// 1. Query the factory for the pool address
// 2. Call the pool contract to get reserves/prices
// 3. Calculate the current price
// For now, simulate price updates with mock data
pf.priceMutex.Lock()
defer pf.priceMutex.Unlock()
key := fmt.Sprintf("%s_%s_%s", tokenA.Hex(), tokenB.Hex(), dexName)
// Mock price data (in a real implementation, fetch from contracts)
mockPrice := big.NewFloat(2000.0) // 1 ETH = 2000 USDC example
if dexName == "SushiSwap" {
mockPrice = big.NewFloat(2001.0) // Slightly different price
} else if dexName == "Camelot" {
mockPrice = big.NewFloat(1999.5)
}
pf.priceCache[key] = &PriceData{
TokenA: tokenA,
TokenB: tokenB,
Price: mockPrice,
InversePrice: new(big.Float).Quo(big.NewFloat(1), mockPrice),
Liquidity: big.NewFloat(1000000), // Mock liquidity
DEX: dexName,
PoolAddress: common.HexToAddress("0x1234567890123456789012345678901234567890"), // Mock address
LastUpdated: time.Now(),
IsValid: true,
}
pf.logger.Debug(fmt.Sprintf("Updated %s price for %s/%s: %s", dexName, tokenA.Hex()[:8], tokenB.Hex()[:8], mockPrice.String()))
}
// GetPriceStats returns statistics about tracked prices
func (pf *PriceFeed) GetPriceStats() map[string]interface{} {
pf.priceMutex.RLock()
defer pf.priceMutex.RUnlock()
totalPrices := len(pf.priceCache)
validPrices := 0
stalePrices := 0
dexCounts := make(map[string]int)
now := time.Now()
for _, price := range pf.priceCache {
if price.IsValid {
validPrices++
}
if now.Sub(price.LastUpdated) > 5*time.Minute {
stalePrices++
}
dexCounts[price.DEX]++
}
return map[string]interface{}{
"totalPrices": totalPrices,
"validPrices": validPrices,
"stalePrices": stalePrices,
"dexBreakdown": dexCounts,
"lastUpdated": time.Now(),
}
}

View File

@@ -0,0 +1,385 @@
package profitcalc
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/logger"
)
// SimpleProfitCalculator provides basic arbitrage profit estimation for integration with scanner
type SimpleProfitCalculator struct {
logger *logger.Logger
minProfitThreshold *big.Int // Minimum profit in wei to consider viable
maxSlippage float64 // Maximum slippage tolerance (e.g., 0.03 for 3%)
gasPrice *big.Int // Current gas price
gasLimit uint64 // Estimated gas limit for arbitrage
client *ethclient.Client // Ethereum client for gas price updates
gasPriceMutex sync.RWMutex // Protects gas price updates
lastGasPriceUpdate time.Time // Last time gas price was updated
gasPriceUpdateInterval time.Duration // How often to update gas prices
priceFeed *PriceFeed // Multi-DEX price feed
slippageProtector *SlippageProtector // Slippage analysis and protection
}
// SimpleOpportunity represents a basic arbitrage opportunity
type SimpleOpportunity struct {
ID string
Timestamp time.Time
TokenA common.Address
TokenB common.Address
AmountIn *big.Float
AmountOut *big.Float
PriceDifference *big.Float
EstimatedProfit *big.Float
GasCost *big.Float
NetProfit *big.Float
ProfitMargin float64
IsExecutable bool
RejectReason string
Confidence float64
// Enhanced fields for slippage analysis
SlippageAnalysis *SlippageAnalysis // Detailed slippage analysis
SlippageRisk string // Risk level: "Low", "Medium", "High", "Extreme"
EffectivePrice *big.Float // Price after slippage
MinAmountOut *big.Float // Minimum amount out with slippage protection
}
// NewSimpleProfitCalculator creates a new simplified profit calculator
func NewSimpleProfitCalculator(logger *logger.Logger) *SimpleProfitCalculator {
return &SimpleProfitCalculator{
logger: logger,
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH minimum (more realistic)
maxSlippage: 0.03, // 3% max slippage
gasPrice: big.NewInt(1000000000), // 1 gwei default
gasLimit: 200000, // 200k gas for simple arbitrage
gasPriceUpdateInterval: 30 * time.Second, // Update gas price every 30 seconds
slippageProtector: NewSlippageProtector(logger), // Initialize slippage protection
}
}
// NewSimpleProfitCalculatorWithClient creates a profit calculator with Ethereum client for gas price updates
func NewSimpleProfitCalculatorWithClient(logger *logger.Logger, client *ethclient.Client) *SimpleProfitCalculator {
calc := NewSimpleProfitCalculator(logger)
calc.client = client
// Initialize price feed if client is provided
if client != nil {
calc.priceFeed = NewPriceFeed(logger, client)
calc.priceFeed.Start()
// Start gas price updater
go calc.startGasPriceUpdater()
}
return calc
}
// AnalyzeSwapOpportunity analyzes a swap event for potential arbitrage profit
func (spc *SimpleProfitCalculator) AnalyzeSwapOpportunity(
ctx context.Context,
tokenA, tokenB common.Address,
amountIn, amountOut *big.Float,
protocol string,
) *SimpleOpportunity {
opportunity := &SimpleOpportunity{
ID: fmt.Sprintf("arb_%d_%s", time.Now().Unix(), tokenA.Hex()[:8]),
Timestamp: time.Now(),
TokenA: tokenA,
TokenB: tokenB,
AmountIn: amountIn,
AmountOut: amountOut,
IsExecutable: false,
Confidence: 0.0,
}
// Calculate profit using multi-DEX price comparison if available
if amountIn.Sign() > 0 && amountOut.Sign() > 0 {
// Try to get real arbitrage opportunity from price feeds
var grossProfit *big.Float
var priceDiff *big.Float
if spc.priceFeed != nil {
// Get arbitrage route using real price data
arbitrageRoute := spc.priceFeed.GetBestArbitrageOpportunity(tokenA, tokenB, amountIn)
if arbitrageRoute != nil && arbitrageRoute.SpreadBps > 50 { // Minimum 50 bps spread
grossProfit = arbitrageRoute.GrossProfit
priceDiff = new(big.Float).Sub(arbitrageRoute.SellPrice, arbitrageRoute.BuyPrice)
opportunity.PriceDifference = priceDiff
spc.logger.Debug(fmt.Sprintf("Real arbitrage opportunity found: %s -> %s, Spread: %d bps, Profit: %s",
arbitrageRoute.BuyDEX, arbitrageRoute.SellDEX, arbitrageRoute.SpreadBps, grossProfit.String()))
} else {
// No profitable arbitrage found with real prices
grossProfit = big.NewFloat(0)
priceDiff = big.NewFloat(0)
}
} else {
// Fallback to simplified calculation
price := new(big.Float).Quo(amountOut, amountIn)
priceDiff = new(big.Float).Mul(price, big.NewFloat(0.01))
grossProfit = new(big.Float).Mul(amountOut, big.NewFloat(0.005))
}
opportunity.PriceDifference = priceDiff
opportunity.EstimatedProfit = grossProfit
// Perform slippage analysis if we have sufficient data
var slippageAnalysis *SlippageAnalysis
var adjustedProfit *big.Float = grossProfit
if spc.priceFeed != nil {
// Get price data for slippage calculation
multiPrice := spc.priceFeed.GetMultiDEXPrice(tokenA, tokenB)
if multiPrice != nil && len(multiPrice.Prices) > 0 {
// Use average liquidity from available pools
totalLiquidity := big.NewFloat(0)
for _, price := range multiPrice.Prices {
totalLiquidity.Add(totalLiquidity, price.Liquidity)
}
avgLiquidity := new(big.Float).Quo(totalLiquidity, big.NewFloat(float64(len(multiPrice.Prices))))
// Calculate current price
currentPrice := new(big.Float).Quo(amountOut, amountIn)
// Perform slippage analysis
slippageAnalysis = spc.slippageProtector.AnalyzeSlippage(amountIn, avgLiquidity, currentPrice)
if slippageAnalysis != nil {
opportunity.SlippageAnalysis = slippageAnalysis
opportunity.SlippageRisk = slippageAnalysis.RiskLevel
opportunity.EffectivePrice = slippageAnalysis.EffectivePrice
opportunity.MinAmountOut = slippageAnalysis.MinAmountOut
// Adjust profit for slippage
adjustedProfit = spc.slippageProtector.CalculateSlippageAdjustedProfit(grossProfit, slippageAnalysis)
spc.logger.Debug(fmt.Sprintf("Slippage analysis for %s: Risk=%s, Slippage=%.2f%%, Adjusted Profit=%s",
opportunity.ID, slippageAnalysis.RiskLevel, slippageAnalysis.EstimatedSlippage*100, adjustedProfit.String()))
}
}
} else {
// Fallback slippage estimation without real data
slippageEst := 0.005 // Assume 0.5% slippage
slippageReduction := new(big.Float).Mul(grossProfit, big.NewFloat(slippageEst))
adjustedProfit = new(big.Float).Sub(grossProfit, slippageReduction)
opportunity.SlippageRisk = "Medium" // Default to medium risk
}
// Calculate gas cost (potentially adjusted for slippage complexity)
gasCost := spc.calculateGasCost()
if slippageAnalysis != nil {
additionalGas := spc.slippageProtector.EstimateGasForSlippage(slippageAnalysis)
if additionalGas > 0 {
extraGasCost := new(big.Int).Mul(spc.GetCurrentGasPrice(), big.NewInt(int64(additionalGas)))
extraGasCostFloat := new(big.Float).Quo(new(big.Float).SetInt(extraGasCost), big.NewFloat(1e18))
gasCost.Add(gasCost, extraGasCostFloat)
}
}
opportunity.GasCost = gasCost
// Net profit = Adjusted profit - Gas cost
netProfit := new(big.Float).Sub(adjustedProfit, gasCost)
opportunity.NetProfit = netProfit
// Calculate profit margin
if amountOut.Sign() > 0 {
profitMargin := new(big.Float).Quo(netProfit, amountOut)
profitMarginFloat, _ := profitMargin.Float64()
opportunity.ProfitMargin = profitMarginFloat
}
// Determine if executable (considering both profit and slippage risk)
if netProfit.Sign() > 0 {
netProfitWei, _ := netProfit.Int(nil)
if netProfitWei.Cmp(spc.minProfitThreshold) >= 0 {
// Check slippage risk
if opportunity.SlippageRisk == "Extreme" {
opportunity.IsExecutable = false
opportunity.RejectReason = "extreme slippage risk"
opportunity.Confidence = 0.1
} else if slippageAnalysis != nil && !slippageAnalysis.IsAcceptable {
opportunity.IsExecutable = false
opportunity.RejectReason = fmt.Sprintf("slippage too high: %s", slippageAnalysis.Recommendation)
opportunity.Confidence = 0.2
} else {
opportunity.IsExecutable = true
opportunity.Confidence = spc.calculateConfidence(opportunity)
opportunity.RejectReason = ""
}
} else {
opportunity.IsExecutable = false
opportunity.RejectReason = "profit below minimum threshold"
opportunity.Confidence = 0.3
}
} else {
opportunity.IsExecutable = false
opportunity.RejectReason = "negative profit after gas and slippage costs"
opportunity.Confidence = 0.1
}
} else {
opportunity.IsExecutable = false
opportunity.RejectReason = "invalid swap amounts"
opportunity.Confidence = 0.0
}
spc.logger.Debug(fmt.Sprintf("Analyzed arbitrage opportunity: ID=%s, NetProfit=%s ETH, Executable=%t, Reason=%s",
opportunity.ID,
spc.FormatEther(opportunity.NetProfit),
opportunity.IsExecutable,
opportunity.RejectReason,
))
return opportunity
}
// calculateGasCost estimates the gas cost for an arbitrage transaction
func (spc *SimpleProfitCalculator) calculateGasCost() *big.Float {
// Gas cost = Gas price * Gas limit
gasLimit := big.NewInt(int64(spc.gasLimit))
currentGasPrice := spc.GetCurrentGasPrice()
gasCostWei := new(big.Int).Mul(currentGasPrice, gasLimit)
// Add 20% buffer for MEV competition
buffer := new(big.Int).Div(gasCostWei, big.NewInt(5)) // 20%
gasCostWei.Add(gasCostWei, buffer)
// Convert to big.Float for easier calculation
gasCostFloat := new(big.Float).SetInt(gasCostWei)
// Convert from wei to ether
etherDenominator := new(big.Float).SetInt(big.NewInt(1e18))
return new(big.Float).Quo(gasCostFloat, etherDenominator)
}
// calculateConfidence calculates a confidence score for the opportunity
func (spc *SimpleProfitCalculator) calculateConfidence(opp *SimpleOpportunity) float64 {
confidence := 0.0
// Base confidence for positive profit
if opp.NetProfit != nil && opp.NetProfit.Sign() > 0 {
confidence += 0.4
}
// Confidence based on profit margin
if opp.ProfitMargin > 0.02 { // > 2% margin
confidence += 0.3
} else if opp.ProfitMargin > 0.01 { // > 1% margin
confidence += 0.2
} else if opp.ProfitMargin > 0.005 { // > 0.5% margin
confidence += 0.1
}
// Confidence based on trade size (larger trades = more confidence)
if opp.AmountOut != nil {
amountOutFloat, _ := opp.AmountOut.Float64()
if amountOutFloat > 1000 { // > $1000 equivalent
confidence += 0.2
} else if amountOutFloat > 100 { // > $100 equivalent
confidence += 0.1
}
}
// Cap at 1.0
if confidence > 1.0 {
confidence = 1.0
}
return confidence
}
// FormatEther formats a big.Float ether amount to string (public method)
func (spc *SimpleProfitCalculator) FormatEther(ether *big.Float) string {
if ether == nil {
return "0.000000"
}
return fmt.Sprintf("%.6f", ether)
}
// UpdateGasPrice updates the current gas price for calculations
func (spc *SimpleProfitCalculator) UpdateGasPrice(gasPrice *big.Int) {
spc.gasPriceMutex.Lock()
defer spc.gasPriceMutex.Unlock()
spc.gasPrice = gasPrice
spc.lastGasPriceUpdate = time.Now()
spc.logger.Debug(fmt.Sprintf("Updated gas price to %s gwei",
new(big.Float).Quo(new(big.Float).SetInt(gasPrice), big.NewFloat(1e9))))
}
// GetCurrentGasPrice gets the current gas price (thread-safe)
func (spc *SimpleProfitCalculator) GetCurrentGasPrice() *big.Int {
spc.gasPriceMutex.RLock()
defer spc.gasPriceMutex.RUnlock()
return new(big.Int).Set(spc.gasPrice)
}
// startGasPriceUpdater starts a background goroutine to update gas prices
func (spc *SimpleProfitCalculator) startGasPriceUpdater() {
ticker := time.NewTicker(spc.gasPriceUpdateInterval)
defer ticker.Stop()
spc.logger.Info(fmt.Sprintf("Starting gas price updater with %s interval", spc.gasPriceUpdateInterval))
// Update gas price immediately on start
spc.updateGasPriceFromNetwork()
for range ticker.C {
spc.updateGasPriceFromNetwork()
}
}
// updateGasPriceFromNetwork fetches current gas price from the network
func (spc *SimpleProfitCalculator) updateGasPriceFromNetwork() {
if spc.client == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
gasPrice, err := spc.client.SuggestGasPrice(ctx)
if err != nil {
spc.logger.Debug(fmt.Sprintf("Failed to fetch gas price from network: %v", err))
return
}
// Add MEV priority fee (50% boost for competitive transactions)
mevGasPrice := new(big.Int).Mul(gasPrice, big.NewInt(150))
mevGasPrice.Div(mevGasPrice, big.NewInt(100))
spc.UpdateGasPrice(mevGasPrice)
}
// SetMinProfitThreshold sets the minimum profit threshold
func (spc *SimpleProfitCalculator) SetMinProfitThreshold(threshold *big.Int) {
spc.minProfitThreshold = threshold
spc.logger.Info(fmt.Sprintf("Updated minimum profit threshold to %s ETH",
new(big.Float).Quo(new(big.Float).SetInt(threshold), big.NewFloat(1e18))))
}
// GetPriceFeedStats returns statistics about the price feed
func (spc *SimpleProfitCalculator) GetPriceFeedStats() map[string]interface{} {
if spc.priceFeed != nil {
return spc.priceFeed.GetPriceStats()
}
return map[string]interface{}{
"status": "price feed not available",
}
}
// HasPriceFeed returns true if the calculator has an active price feed
func (spc *SimpleProfitCalculator) HasPriceFeed() bool {
return spc.priceFeed != nil
}
// Stop gracefully shuts down the profit calculator
func (spc *SimpleProfitCalculator) Stop() {
if spc.priceFeed != nil {
spc.priceFeed.Stop()
spc.logger.Info("Price feed stopped")
}
}

View File

@@ -0,0 +1,285 @@
package profitcalc
import (
"fmt"
"math/big"
"github.com/fraktal/mev-beta/internal/logger"
)
// SlippageProtector provides slippage calculation and protection for arbitrage trades
type SlippageProtector struct {
logger *logger.Logger
maxSlippageBps int64 // Maximum allowed slippage in basis points
liquidityBuffer float64 // Buffer factor for liquidity calculations
}
// SlippageAnalysis contains detailed slippage analysis for a trade
type SlippageAnalysis struct {
TradeAmount *big.Float // Amount being traded
PoolLiquidity *big.Float // Available liquidity in pool
EstimatedSlippage float64 // Estimated slippage percentage
SlippageBps int64 // Slippage in basis points
PriceImpact float64 // Price impact percentage
EffectivePrice *big.Float // Price after slippage
MinAmountOut *big.Float // Minimum amount out with slippage protection
IsAcceptable bool // Whether slippage is within acceptable limits
RiskLevel string // "Low", "Medium", "High", "Extreme"
Recommendation string // Trading recommendation
}
// NewSlippageProtector creates a new slippage protection manager
func NewSlippageProtector(logger *logger.Logger) *SlippageProtector {
return &SlippageProtector{
logger: logger,
maxSlippageBps: 500, // 5% maximum slippage
liquidityBuffer: 0.8, // Use 80% of available liquidity for calculations
}
}
// AnalyzeSlippage performs comprehensive slippage analysis for a potential trade
func (sp *SlippageProtector) AnalyzeSlippage(
tradeAmount *big.Float,
poolLiquidity *big.Float,
currentPrice *big.Float,
) *SlippageAnalysis {
if tradeAmount == nil || poolLiquidity == nil || currentPrice == nil {
return &SlippageAnalysis{
IsAcceptable: false,
RiskLevel: "Extreme",
Recommendation: "Insufficient data for slippage calculation",
}
}
// Calculate trade size as percentage of pool liquidity
tradeSizeRatio := new(big.Float).Quo(tradeAmount, poolLiquidity)
tradeSizeFloat, _ := tradeSizeRatio.Float64()
// Estimate slippage using simplified AMM formula
// For constant product AMMs: slippage ≈ trade_size / (2 * liquidity)
estimatedSlippage := tradeSizeFloat / 2.0
// Apply curve adjustment for larger trades (non-linear slippage)
if tradeSizeFloat > 0.1 { // > 10% of pool
// Quadratic increase for large trades
estimatedSlippage = estimatedSlippage * (1 + tradeSizeFloat)
}
slippageBps := int64(estimatedSlippage * 10000)
// Calculate price impact (similar to slippage but different calculation)
priceImpact := sp.calculatePriceImpact(tradeSizeFloat)
// Calculate effective price after slippage
slippageFactor := 1.0 - estimatedSlippage
effectivePrice := new(big.Float).Mul(currentPrice, big.NewFloat(slippageFactor))
// Calculate minimum amount out with slippage protection
minAmountOut := new(big.Float).Quo(tradeAmount, effectivePrice)
// Determine risk level and acceptability
riskLevel, isAcceptable := sp.assessRiskLevel(slippageBps, tradeSizeFloat)
recommendation := sp.generateRecommendation(slippageBps, tradeSizeFloat, riskLevel)
analysis := &SlippageAnalysis{
TradeAmount: tradeAmount,
PoolLiquidity: poolLiquidity,
EstimatedSlippage: estimatedSlippage,
SlippageBps: slippageBps,
PriceImpact: priceImpact,
EffectivePrice: effectivePrice,
MinAmountOut: minAmountOut,
IsAcceptable: isAcceptable,
RiskLevel: riskLevel,
Recommendation: recommendation,
}
sp.logger.Debug(fmt.Sprintf("Slippage analysis: Trade=%s, Liquidity=%s, Slippage=%.2f%%, Risk=%s",
tradeAmount.String(), poolLiquidity.String(), estimatedSlippage*100, riskLevel))
return analysis
}
// calculatePriceImpact calculates price impact using AMM mechanics
func (sp *SlippageProtector) calculatePriceImpact(tradeSizeRatio float64) float64 {
// For constant product AMMs (like Uniswap V2):
// Price impact = trade_size / (1 + trade_size)
priceImpact := tradeSizeRatio / (1.0 + tradeSizeRatio)
// Cap at 100%
if priceImpact > 1.0 {
priceImpact = 1.0
}
return priceImpact
}
// assessRiskLevel determines risk level based on slippage and trade size
func (sp *SlippageProtector) assessRiskLevel(slippageBps int64, tradeSizeRatio float64) (string, bool) {
isAcceptable := slippageBps <= sp.maxSlippageBps
var riskLevel string
switch {
case slippageBps <= 50: // <= 0.5%
riskLevel = "Low"
case slippageBps <= 200: // <= 2%
riskLevel = "Medium"
case slippageBps <= 500: // <= 5%
riskLevel = "High"
default:
riskLevel = "Extreme"
isAcceptable = false
}
// Additional checks for trade size
if tradeSizeRatio > 0.5 { // > 50% of pool
riskLevel = "Extreme"
isAcceptable = false
} else if tradeSizeRatio > 0.2 { // > 20% of pool
if riskLevel == "Low" {
riskLevel = "Medium"
} else if riskLevel == "Medium" {
riskLevel = "High"
}
}
return riskLevel, isAcceptable
}
// generateRecommendation provides trading recommendations based on analysis
func (sp *SlippageProtector) generateRecommendation(slippageBps int64, tradeSizeRatio float64, riskLevel string) string {
switch riskLevel {
case "Low":
return "Safe to execute - low slippage expected"
case "Medium":
if tradeSizeRatio > 0.1 {
return "Consider splitting trade into smaller parts"
}
return "Proceed with caution - moderate slippage expected"
case "High":
return "High slippage risk - consider reducing trade size or finding alternative routes"
case "Extreme":
if tradeSizeRatio > 0.5 {
return "Trade too large for pool - split into multiple smaller trades"
}
return "Excessive slippage - avoid this trade"
default:
return "Unable to assess - insufficient data"
}
}
// CalculateOptimalTradeSize calculates optimal trade size to stay within slippage limits
func (sp *SlippageProtector) CalculateOptimalTradeSize(
poolLiquidity *big.Float,
maxSlippageBps int64,
) *big.Float {
if poolLiquidity == nil || poolLiquidity.Sign() <= 0 {
return big.NewFloat(0)
}
// Convert max slippage to ratio
maxSlippageRatio := float64(maxSlippageBps) / 10000.0
// For simplified AMM: optimal_trade_size = 2 * liquidity * max_slippage
optimalRatio := 2.0 * maxSlippageRatio
// Apply safety factor
safetyFactor := 0.8 // Use 80% of optimal to be conservative
optimalRatio *= safetyFactor
optimalSize := new(big.Float).Mul(poolLiquidity, big.NewFloat(optimalRatio))
sp.logger.Debug(fmt.Sprintf("Calculated optimal trade size: %s (%.2f%% of pool) for max slippage %d bps",
optimalSize.String(), optimalRatio*100, maxSlippageBps))
return optimalSize
}
// EstimateGasForSlippage estimates additional gas needed for slippage protection
func (sp *SlippageProtector) EstimateGasForSlippage(analysis *SlippageAnalysis) uint64 {
baseGas := uint64(0)
// Higher slippage might require more complex routing
switch analysis.RiskLevel {
case "Low":
baseGas = 0 // No additional gas
case "Medium":
baseGas = 20000 // Additional gas for price checks
case "High":
baseGas = 50000 // Additional gas for complex routing
case "Extreme":
baseGas = 100000 // Maximum additional gas for emergency handling
}
return baseGas
}
// SetMaxSlippage updates the maximum allowed slippage
func (sp *SlippageProtector) SetMaxSlippage(bps int64) {
sp.maxSlippageBps = bps
sp.logger.Info(fmt.Sprintf("Updated maximum slippage to %d bps (%.2f%%)", bps, float64(bps)/100))
}
// GetMaxSlippage returns the current maximum slippage setting
func (sp *SlippageProtector) GetMaxSlippage() int64 {
return sp.maxSlippageBps
}
// ValidateTradeParameters performs comprehensive validation of trade parameters
func (sp *SlippageProtector) ValidateTradeParameters(
tradeAmount *big.Float,
poolLiquidity *big.Float,
minLiquidity *big.Float,
) error {
if tradeAmount == nil || tradeAmount.Sign() <= 0 {
return fmt.Errorf("invalid trade amount: must be positive")
}
if poolLiquidity == nil || poolLiquidity.Sign() <= 0 {
return fmt.Errorf("invalid pool liquidity: must be positive")
}
if minLiquidity != nil && poolLiquidity.Cmp(minLiquidity) < 0 {
return fmt.Errorf("insufficient pool liquidity: %s < %s required",
poolLiquidity.String(), minLiquidity.String())
}
// Check if trade is reasonable relative to pool size
tradeSizeRatio := new(big.Float).Quo(tradeAmount, poolLiquidity)
tradeSizeFloat, _ := tradeSizeRatio.Float64()
if tradeSizeFloat > 0.9 { // > 90% of pool
return fmt.Errorf("trade size too large: %.1f%% of pool liquidity", tradeSizeFloat*100)
}
return nil
}
// CalculateSlippageAdjustedProfit adjusts profit calculations for slippage
func (sp *SlippageProtector) CalculateSlippageAdjustedProfit(
grossProfit *big.Float,
analysis *SlippageAnalysis,
) *big.Float {
if grossProfit == nil || analysis == nil {
return big.NewFloat(0)
}
// Reduce profit by estimated slippage impact
slippageImpact := big.NewFloat(analysis.EstimatedSlippage)
slippageReduction := new(big.Float).Mul(grossProfit, slippageImpact)
adjustedProfit := new(big.Float).Sub(grossProfit, slippageReduction)
// Ensure profit doesn't go negative due to slippage
if adjustedProfit.Sign() < 0 {
adjustedProfit = big.NewFloat(0)
}
sp.logger.Debug(fmt.Sprintf("Slippage-adjusted profit: %s -> %s (reduction: %s)",
grossProfit.String(), adjustedProfit.String(), slippageReduction.String()))
return adjustedProfit
}