package pricing import ( "context" "fmt" "math/big" "sync" "time" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/internal/logger" pkgerrors "github.com/fraktal/mev-beta/pkg/errors" oraclepkg "github.com/fraktal/mev-beta/pkg/oracle" "github.com/fraktal/mev-beta/pkg/types" ) // ExchangePricer handles real-time pricing across multiple DEX protocols type ExchangePricer struct { logger *logger.Logger oracles map[string]*oraclepkg.PriceOracle priceCache map[string]*PriceEntry cacheMutex sync.RWMutex lastUpdate time.Time updateInterval time.Duration } // PriceEntry represents cached price data with timestamp type PriceEntry struct { Price *big.Float Timestamp time.Time Validity time.Duration } // ExchangePrice represents pricing data from a specific exchange type ExchangePrice struct { Exchange string Pair string BidPrice *big.Float AskPrice *big.Float Liquidity *big.Int Timestamp time.Time Confidence float64 } // PricingOpportunity represents pricing-specific arbitrage data (extends canonical ArbitrageOpportunity) type PricingOpportunity struct { *types.ArbitrageOpportunity BuyExchange string SellExchange string BuyPrice *big.Float SellPrice *big.Float Spread *big.Float RequiredCapital *big.Int Expiration time.Time } // NewExchangePricer creates a new cross-exchange pricer func NewExchangePricer(logger *logger.Logger) *ExchangePricer { return &ExchangePricer{ logger: logger, oracles: make(map[string]*oraclepkg.PriceOracle), priceCache: make(map[string]*PriceEntry), updateInterval: 500 * time.Millisecond, // Update every 500ms for real-time pricing } } // AddExchangeOracle adds a price oracle for a specific exchange func (ep *ExchangePricer) AddExchangeOracle(exchange string, oracle *oraclepkg.PriceOracle) { ep.oracles[exchange] = oracle ep.logger.Info(fmt.Sprintf("Added price oracle for exchange: %s", exchange)) } // GetCrossExchangePrices retrieves prices for a token pair across all exchanges func (ep *ExchangePricer) GetCrossExchangePrices(ctx context.Context, tokenIn, tokenOut common.Address) (map[string]*ExchangePrice, error) { prices := make(map[string]*ExchangePrice) for exchange, oracle := range ep.oracles { select { case <-ctx.Done(): return nil, pkgerrors.WrapContextError(ctx.Err(), "GetCrossExchangePrices", map[string]interface{}{ "tokenIn": tokenIn.Hex(), "tokenOut": tokenOut.Hex(), "currentExchange": exchange, "pricesFetched": len(prices), }) default: priceReq := &oraclepkg.PriceRequest{ TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(1e18), // 1 token for reference price Timestamp: time.Now(), } priceResp, err := oracle.GetPrice(ctx, priceReq) if err != nil { ep.logger.Debug(fmt.Sprintf("Failed to get price from %s: %v", exchange, err)) continue } if priceResp.Valid && priceResp.AmountOut != nil { exchangePrice := &ExchangePrice{ Exchange: exchange, Pair: fmt.Sprintf("%s/%s", tokenIn.Hex()[:6], tokenOut.Hex()[:6]), BidPrice: new(big.Float).SetInt(priceResp.AmountOut), AskPrice: new(big.Float).SetInt(priceResp.AmountOut), // Simplified - in production would have bid/ask spread Liquidity: priceResp.Liquidity, // Estimated liquidity Timestamp: time.Now(), Confidence: 0.9, // High confidence for direct oracle data } prices[exchange] = exchangePrice } } } return prices, nil } // FindArbitrageOpportunities identifies cross-exchange arbitrage possibilities func (ep *ExchangePricer) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*types.ArbitrageOpportunity, error) { prices, err := ep.GetCrossExchangePrices(ctx, tokenIn, tokenOut) if err != nil { return nil, fmt.Errorf("failed to get cross-exchange prices: %w", err) } if len(prices) < 2 { return nil, nil // Need at least 2 exchanges to find arbitrage } var opportunities []*types.ArbitrageOpportunity // Compare all exchange pairs for arbitrage opportunities for buyExchange, buyPrice := range prices { for sellExchange, sellPrice := range prices { if buyExchange == sellExchange { continue } // Calculate price spread spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice) if spread.Sign() <= 0 { continue // No arbitrage opportunity } // Calculate spread percentage spreadPct := new(big.Float).Quo(spread, buyPrice.AskPrice) spreadPctFloat, _ := spreadPct.Float64() // Only consider opportunities with > 0.3% spread (after fees) if spreadPctFloat < 0.003 { continue } // Estimate required capital (use smaller liquidity as constraint) requiredCapital := buyPrice.Liquidity if sellPrice.Liquidity.Cmp(requiredCapital) < 0 { requiredCapital = sellPrice.Liquidity } // Estimate profit (simplified - real implementation would be more complex) estimatedProfit := new(big.Float).Mul(spread, new(big.Float).SetInt(requiredCapital)) estimatedProfit.Quo(estimatedProfit, big.NewFloat(1e18)) // Convert to ETH terms // Estimate gas costs (simplified) gasEstimate := big.NewInt(300000) // ~300k gas for complex arbitrage // Calculate net profit after gas gasCostEth := new(big.Float).Quo(new(big.Float).SetInt(gasEstimate), big.NewFloat(1e18)) gasCostUsd := new(big.Float).Mul(gasCostEth, big.NewFloat(2000)) // Assume $2000/ETH for gas pricing netProfit := new(big.Float).Sub(estimatedProfit, gasCostUsd) if netProfit.Sign() <= 0 { continue // Unprofitable after gas costs } // Calculate risk score (simplified) riskScore := 0.1 // Low base risk if spreadPctFloat > 0.1 { // > 10% spread riskScore += 0.3 // Higher volatility risk } // Create 100 ETH value using string to avoid overflow hundredETH := new(big.Int) hundredETH.SetString("100000000000000000000", 10) // 100 * 1e18 if requiredCapital.Cmp(hundredETH) > 0 { // > 100 ETH liquidity riskScore -= 0.05 // Lower slippage risk with deep liquidity } // Convert to canonical ArbitrageOpportunity profitWei := new(big.Int) estimatedProfit.Int(profitWei) netProfitWei := new(big.Int) netProfit.Int(netProfitWei) opportunity := &types.ArbitrageOpportunity{ Path: []string{buyExchange, sellExchange}, Pools: []string{buyExchange + "-pool", sellExchange + "-pool"}, AmountIn: requiredCapital, Profit: profitWei, NetProfit: netProfitWei, GasEstimate: gasEstimate, ROI: spreadPctFloat * 100, Protocol: "cross-exchange", ExecutionTime: 15000, // 15 seconds in milliseconds Confidence: (buyPrice.Confidence + sellPrice.Confidence) / 2, PriceImpact: 0.005, // 0.5% estimated MaxSlippage: 0.01, // 1% max slippage TokenIn: tokenIn, TokenOut: tokenOut, Timestamp: time.Now().Unix(), Risk: riskScore, } opportunities = append(opportunities, opportunity) } } // Sort by net profit descending ep.sortOpportunitiesByProfit(opportunities) return opportunities, nil } // sortOpportunitiesByProfit sorts arbitrage opportunities by net profit func (ep *ExchangePricer) sortOpportunitiesByProfit(opportunities []*types.ArbitrageOpportunity) { // Simple bubble sort for small arrays for i := 0; i < len(opportunities)-1; i++ { for j := 0; j < len(opportunities)-i-1; j++ { profitI := new(big.Float).SetInt(opportunities[j].NetProfit) profitJ := new(big.Float).SetInt(opportunities[j+1].NetProfit) profitIFloat, _ := profitI.Float64() profitJFloat, _ := profitJ.Float64() if profitIFloat < profitJFloat { opportunities[j], opportunities[j+1] = opportunities[j+1], opportunities[j] } } } } // ValidateOpportunity validates an arbitrage opportunity is still profitable func (ep *ExchangePricer) ValidateOpportunity(ctx context.Context, opportunity *types.ArbitrageOpportunity) (bool, error) { // Check expiration (using ExecutionTime as expiration window) expiration := time.Unix(opportunity.Timestamp, 0).Add(time.Duration(opportunity.ExecutionTime) * time.Millisecond) if time.Now().After(expiration) { return false, nil } // Revalidate prices prices, err := ep.GetCrossExchangePrices(ctx, opportunity.TokenIn, opportunity.TokenOut) if err != nil { return false, fmt.Errorf("failed to revalidate prices: %w", err) } // Extract buy/sell exchanges from path if len(opportunity.Path) < 2 { return false, nil } buyPrice, buyExists := prices[opportunity.Path[0]] sellPrice, sellExists := prices[opportunity.Path[1]] if !buyExists || !sellExists { return false, nil } // Recalculate spread spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice) if spread.Sign() <= 0 { return false, nil // No longer profitable } return true, nil } // GetPriceCacheStats returns statistics about the price cache func (ep *ExchangePricer) GetPriceCacheStats() map[string]interface{} { ep.cacheMutex.RLock() defer ep.cacheMutex.RUnlock() stats := make(map[string]interface{}) stats["cached_prices"] = len(ep.priceCache) stats["last_update"] = ep.lastUpdate stats["update_interval"] = ep.updateInterval return stats }