saving in place
This commit is contained in:
585
pkg/scanner/swap/analyzer.go
Normal file
585
pkg/scanner/swap/analyzer.go
Normal file
@@ -0,0 +1,585 @@
|
||||
package swap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/events"
|
||||
"github.com/fraktal/mev-beta/pkg/marketdata"
|
||||
"github.com/fraktal/mev-beta/pkg/profitcalc"
|
||||
"github.com/fraktal/mev-beta/pkg/scanner/market"
|
||||
stypes "github.com/fraktal/mev-beta/pkg/types"
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// SwapAnalyzer handles analysis of swap events for price movement opportunities
|
||||
type SwapAnalyzer struct {
|
||||
logger *logger.Logger
|
||||
marketDataLogger *marketdata.MarketDataLogger
|
||||
profitCalculator *profitcalc.ProfitCalculator
|
||||
opportunityRanker *profitcalc.OpportunityRanker
|
||||
}
|
||||
|
||||
// NewSwapAnalyzer creates a new swap analyzer
|
||||
func NewSwapAnalyzer(
|
||||
logger *logger.Logger,
|
||||
marketDataLogger *marketdata.MarketDataLogger,
|
||||
profitCalculator *profitcalc.ProfitCalculator,
|
||||
opportunityRanker *profitcalc.OpportunityRanker,
|
||||
) *SwapAnalyzer {
|
||||
return &SwapAnalyzer{
|
||||
logger: logger,
|
||||
marketDataLogger: marketDataLogger,
|
||||
profitCalculator: profitCalculator,
|
||||
opportunityRanker: opportunityRanker,
|
||||
}
|
||||
}
|
||||
|
||||
// AnalyzeSwapEvent analyzes a swap event for arbitrage opportunities
|
||||
func (s *SwapAnalyzer) AnalyzeSwapEvent(event events.Event, marketScanner *market.MarketScanner) {
|
||||
s.logger.Debug(fmt.Sprintf("Analyzing swap event in pool %s", event.PoolAddress))
|
||||
|
||||
// Get comprehensive pool data to determine factory and fee
|
||||
poolInfo, poolExists := s.marketDataLogger.GetPoolInfo(event.PoolAddress)
|
||||
factory := common.Address{}
|
||||
fee := uint32(3000) // Default 0.3%
|
||||
if poolExists {
|
||||
factory = poolInfo.Factory
|
||||
fee = poolInfo.Fee
|
||||
} else {
|
||||
// Determine factory from known DEX protocols
|
||||
factory = marketScanner.GetFactoryForProtocol(event.Protocol)
|
||||
}
|
||||
|
||||
// Create comprehensive swap event data for market data logger
|
||||
swapData := &marketdata.SwapEventData{
|
||||
TxHash: event.TransactionHash,
|
||||
BlockNumber: event.BlockNumber,
|
||||
LogIndex: uint(0), // Default log index (would need to be extracted from receipt)
|
||||
Timestamp: time.Now(),
|
||||
PoolAddress: event.PoolAddress,
|
||||
Factory: factory,
|
||||
Protocol: event.Protocol,
|
||||
Token0: event.Token0,
|
||||
Token1: event.Token1,
|
||||
Sender: common.Address{}, // Default sender (would need to be extracted from transaction)
|
||||
Recipient: common.Address{}, // Default recipient (would need to be extracted from transaction)
|
||||
SqrtPriceX96: event.SqrtPriceX96,
|
||||
Liquidity: event.Liquidity,
|
||||
Tick: int32(event.Tick),
|
||||
}
|
||||
|
||||
// Extract swap amounts from event (handle signed amounts correctly)
|
||||
if event.Amount0 != nil && event.Amount1 != nil {
|
||||
amount0Float := new(big.Float).SetInt(event.Amount0)
|
||||
amount1Float := new(big.Float).SetInt(event.Amount1)
|
||||
|
||||
// Determine input/output based on sign (negative means token was removed from pool = output)
|
||||
if amount0Float.Sign() < 0 {
|
||||
// Token0 out, Token1 in
|
||||
swapData.Amount0Out = new(big.Int).Abs(event.Amount0)
|
||||
swapData.Amount1In = event.Amount1
|
||||
swapData.Amount0In = big.NewInt(0)
|
||||
swapData.Amount1Out = big.NewInt(0)
|
||||
} else if amount1Float.Sign() < 0 {
|
||||
// Token0 in, Token1 out
|
||||
swapData.Amount0In = event.Amount0
|
||||
swapData.Amount1Out = new(big.Int).Abs(event.Amount1)
|
||||
swapData.Amount0Out = big.NewInt(0)
|
||||
swapData.Amount1In = big.NewInt(0)
|
||||
} else {
|
||||
// Both positive (shouldn't happen in normal swaps, but handle gracefully)
|
||||
swapData.Amount0In = event.Amount0
|
||||
swapData.Amount1In = event.Amount1
|
||||
swapData.Amount0Out = big.NewInt(0)
|
||||
swapData.Amount1Out = big.NewInt(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate USD values using profit calculator's price oracle
|
||||
swapData.AmountInUSD, swapData.AmountOutUSD, swapData.FeeUSD = s.calculateSwapUSDValues(swapData, fee)
|
||||
|
||||
// Calculate price impact based on pool liquidity and swap amounts
|
||||
swapData.PriceImpact = s.calculateSwapPriceImpact(event, swapData)
|
||||
|
||||
// Log comprehensive swap event to market data logger
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.marketDataLogger.LogSwapEvent(ctx, event, swapData); err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Failed to log swap event to market data logger: %v", err))
|
||||
}
|
||||
|
||||
// Log the swap event to database (legacy)
|
||||
marketScanner.LogSwapEvent(event)
|
||||
|
||||
// Get pool data with caching
|
||||
poolData, err := marketScanner.GetPoolData(event.PoolAddress.Hex())
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Error getting pool data for %s: %v", event.PoolAddress, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate price impact
|
||||
priceMovement, err := s.calculatePriceMovement(event, poolData)
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Error calculating price movement for pool %s: %v", event.PoolAddress, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Log opportunity with actual swap amounts from event (legacy)
|
||||
s.logSwapOpportunity(event, poolData, priceMovement, marketScanner)
|
||||
|
||||
// Check if the movement is significant
|
||||
if marketScanner.IsSignificantMovement(priceMovement, marketScanner.Config().MinProfitThreshold) {
|
||||
s.logger.Info(fmt.Sprintf("Significant price movement detected in pool %s: %+v", event.PoolAddress, priceMovement))
|
||||
|
||||
// Look for arbitrage opportunities
|
||||
opportunities := s.findArbitrageOpportunities(event, priceMovement, marketScanner)
|
||||
if len(opportunities) > 0 {
|
||||
s.logger.Info(fmt.Sprintf("Found %d arbitrage opportunities for pool %s", len(opportunities), event.PoolAddress))
|
||||
for _, opp := range opportunities {
|
||||
s.logger.Info(fmt.Sprintf("Arbitrage opportunity: %+v", opp))
|
||||
// Execute the arbitrage opportunity
|
||||
marketScanner.ExecuteArbitrageOpportunity(opp)
|
||||
}
|
||||
} else {
|
||||
s.logger.Debug(fmt.Sprintf("Price movement in pool %s is not significant: %f", event.PoolAddress, priceMovement.PriceImpact))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logSwapOpportunity logs swap opportunities using actual amounts from events
|
||||
func (s *SwapAnalyzer) logSwapOpportunity(event events.Event, poolData *market.CachedData, priceMovement *market.PriceMovement, marketScanner *market.MarketScanner) {
|
||||
// Convert amounts from big.Int to big.Float for profit calculation
|
||||
amountInFloat := big.NewFloat(0)
|
||||
amountOutFloat := big.NewFloat(0)
|
||||
amountInDisplay := float64(0)
|
||||
amountOutDisplay := float64(0)
|
||||
|
||||
// For swap events, Amount0 and Amount1 represent the actual swap amounts
|
||||
// The sign indicates direction (positive = token added to pool, negative = token removed from pool)
|
||||
if event.Amount0 != nil {
|
||||
amount0Float := new(big.Float).SetInt(event.Amount0)
|
||||
if event.Amount1 != nil {
|
||||
amount1Float := new(big.Float).SetInt(event.Amount1)
|
||||
|
||||
// Determine input/output based on sign (negative means token was removed from pool = output)
|
||||
if amount0Float.Sign() < 0 {
|
||||
// Token0 out, Token1 in
|
||||
amountOutFloat = new(big.Float).Abs(amount0Float)
|
||||
amountInFloat = amount1Float
|
||||
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
|
||||
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
|
||||
} else {
|
||||
// Token0 in, Token1 out
|
||||
amountInFloat = amount0Float
|
||||
amountOutFloat = new(big.Float).Abs(amount1Float)
|
||||
amountInDisplay, _ = new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)).Float64()
|
||||
amountOutDisplay, _ = new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)).Float64()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze arbitrage opportunity using the profit calculator
|
||||
var estimatedProfitUSD float64 = 0.0
|
||||
var profitData map[string]interface{}
|
||||
|
||||
if amountInFloat.Sign() > 0 && amountOutFloat.Sign() > 0 {
|
||||
opportunity := s.profitCalculator.AnalyzeSwapOpportunity(
|
||||
context.Background(),
|
||||
event.Token0,
|
||||
event.Token1,
|
||||
new(big.Float).Quo(amountInFloat, big.NewFloat(1e18)), // Convert to ETH units
|
||||
new(big.Float).Quo(amountOutFloat, big.NewFloat(1e18)), // Convert to ETH units
|
||||
event.Protocol,
|
||||
)
|
||||
|
||||
if opportunity != nil {
|
||||
// Add opportunity to ranking system
|
||||
rankedOpp := s.opportunityRanker.AddOpportunity(opportunity)
|
||||
|
||||
// Use the calculated profit for logging
|
||||
if opportunity.NetProfit != nil {
|
||||
estimatedProfitFloat, _ := opportunity.NetProfit.Float64()
|
||||
estimatedProfitUSD = estimatedProfitFloat * 2000 // Assume 1 ETH = $2000 for USD conversion
|
||||
}
|
||||
|
||||
// Add detailed profit analysis to additional data
|
||||
profitData = map[string]interface{}{
|
||||
"arbitrageId": opportunity.ID,
|
||||
"isExecutable": opportunity.IsExecutable,
|
||||
"rejectReason": opportunity.RejectReason,
|
||||
"confidence": opportunity.Confidence,
|
||||
"profitMargin": opportunity.ProfitMargin,
|
||||
"netProfitETH": s.profitCalculator.FormatEther(opportunity.NetProfit),
|
||||
"gasCostETH": s.profitCalculator.FormatEther(opportunity.GasCost),
|
||||
"estimatedProfitETH": s.profitCalculator.FormatEther(opportunity.EstimatedProfit),
|
||||
}
|
||||
|
||||
// Add ranking data if available
|
||||
if rankedOpp != nil {
|
||||
profitData["opportunityScore"] = rankedOpp.Score
|
||||
profitData["opportunityRank"] = rankedOpp.Rank
|
||||
profitData["competitionRisk"] = rankedOpp.CompetitionRisk
|
||||
profitData["updateCount"] = rankedOpp.UpdateCount
|
||||
}
|
||||
}
|
||||
} else if priceMovement != nil {
|
||||
// Fallback to simple price impact calculation
|
||||
estimatedProfitUSD = priceMovement.PriceImpact * 100
|
||||
}
|
||||
|
||||
// Resolve token symbols
|
||||
tokenIn := s.resolveTokenSymbol(event.Token0.Hex())
|
||||
tokenOut := s.resolveTokenSymbol(event.Token1.Hex())
|
||||
|
||||
// Create additional data with profit analysis
|
||||
additionalData := map[string]interface{}{
|
||||
"poolAddress": event.PoolAddress.Hex(),
|
||||
"protocol": event.Protocol,
|
||||
"token0": event.Token0.Hex(),
|
||||
"token1": event.Token1.Hex(),
|
||||
"tokenIn": tokenIn,
|
||||
"tokenOut": tokenOut,
|
||||
"blockNumber": event.BlockNumber,
|
||||
}
|
||||
|
||||
// Add price impact if available
|
||||
if priceMovement != nil {
|
||||
additionalData["priceImpact"] = priceMovement.PriceImpact
|
||||
}
|
||||
|
||||
// Merge profit analysis data
|
||||
if profitData != nil {
|
||||
for k, v := range profitData {
|
||||
additionalData[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Log the opportunity using actual swap amounts and profit analysis
|
||||
s.logger.Opportunity(event.TransactionHash.Hex(), "", event.PoolAddress.Hex(), "Swap", event.Protocol,
|
||||
amountInDisplay, amountOutDisplay, 0.0, estimatedProfitUSD, additionalData)
|
||||
}
|
||||
|
||||
// resolveTokenSymbol converts token address to human-readable symbol
|
||||
func (s *SwapAnalyzer) resolveTokenSymbol(tokenAddress string) string {
|
||||
// Convert to lowercase for consistent lookup
|
||||
addr := strings.ToLower(tokenAddress)
|
||||
|
||||
// Known Arbitrum token mappings (same as in L2 parser)
|
||||
tokenMap := map[string]string{
|
||||
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1": "WETH",
|
||||
"0xaf88d065e77c8cc2239327c5edb3a432268e5831": "USDC",
|
||||
"0xff970a61a04b1ca14834a43f5de4533ebddb5cc8": "USDC.e",
|
||||
"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9": "USDT",
|
||||
"0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f": "WBTC",
|
||||
"0x912ce59144191c1204e64559fe8253a0e49e6548": "ARB",
|
||||
"0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a": "GMX",
|
||||
"0xf97f4df75117a78c1a5a0dbb814af92458539fb4": "LINK",
|
||||
"0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0": "UNI",
|
||||
"0xba5ddd1f9d7f570dc94a51479a000e3bce967196": "AAVE",
|
||||
"0x0de59c86c306b9fead9fb67e65551e2b6897c3f6": "KUMA",
|
||||
"0x6efa9b8883dfb78fd75cd89d8474c44c3cbda469": "DIA",
|
||||
"0x440017a1b021006d556d7fc06a54c32e42eb745b": "G@ARB",
|
||||
"0x11cdb42b0eb46d95f990bedd4695a6e3fa034978": "CRV",
|
||||
"0x040d1edc9569d4bab2d15287dc5a4f10f56a56b8": "BAL",
|
||||
"0x354a6da3fcde098f8389cad84b0182725c6c91de": "COMP",
|
||||
"0x2e9a6df78e42c50b0cefcf9000d0c3a4d34e1dd5": "MKR",
|
||||
}
|
||||
|
||||
if symbol, exists := tokenMap[addr]; exists {
|
||||
return symbol
|
||||
}
|
||||
|
||||
// Return truncated address if not in mapping
|
||||
if len(tokenAddress) > 10 {
|
||||
return tokenAddress[:6] + "..." + tokenAddress[len(tokenAddress)-4:]
|
||||
}
|
||||
return tokenAddress
|
||||
}
|
||||
|
||||
// calculatePriceMovement calculates the price movement from a swap event using cached mathematical functions
|
||||
func (s *SwapAnalyzer) calculatePriceMovement(event events.Event, poolData *market.CachedData) (*market.PriceMovement, error) {
|
||||
s.logger.Debug(fmt.Sprintf("Calculating price movement for pool %s", event.PoolAddress))
|
||||
|
||||
// Get current price from pool data using cached function
|
||||
currentPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
|
||||
if currentPrice == nil {
|
||||
return nil, fmt.Errorf("failed to calculate current price from sqrtPriceX96")
|
||||
}
|
||||
|
||||
// Calculate price impact based on swap amounts
|
||||
var priceImpact float64
|
||||
if event.Amount0.Sign() > 0 && event.Amount1.Sign() > 0 {
|
||||
// Both amounts are positive, calculate the impact
|
||||
amount0Float := new(big.Float).SetInt(event.Amount0)
|
||||
amount1Float := new(big.Float).SetInt(event.Amount1)
|
||||
|
||||
// Price impact = |amount1 / amount0 - current_price| / current_price
|
||||
swapPrice := new(big.Float).Quo(amount1Float, amount0Float)
|
||||
priceDiff := new(big.Float).Sub(swapPrice, currentPrice)
|
||||
priceDiff.Abs(priceDiff)
|
||||
|
||||
priceImpactFloat := new(big.Float).Quo(priceDiff, currentPrice)
|
||||
priceImpact, _ = priceImpactFloat.Float64()
|
||||
}
|
||||
|
||||
movement := &market.PriceMovement{
|
||||
Token0: event.Token0.Hex(),
|
||||
Token1: event.Token1.Hex(),
|
||||
Pool: event.PoolAddress.Hex(),
|
||||
Protocol: event.Protocol,
|
||||
AmountIn: event.Amount0,
|
||||
AmountOut: event.Amount1,
|
||||
PriceBefore: currentPrice,
|
||||
PriceAfter: currentPrice, // For now, assume same price (could be calculated based on swap)
|
||||
PriceImpact: priceImpact,
|
||||
TickBefore: poolData.Tick,
|
||||
TickAfter: poolData.Tick, // For now, assume same tick
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
s.logger.Debug(fmt.Sprintf("Price movement calculated: impact=%.6f%%, amount_in=%s", priceImpact*100, event.Amount0.String()))
|
||||
return movement, nil
|
||||
}
|
||||
|
||||
// findArbitrageOpportunities looks for arbitrage opportunities based on price movements
|
||||
func (s *SwapAnalyzer) findArbitrageOpportunities(event events.Event, movement *market.PriceMovement, marketScanner *market.MarketScanner) []stypes.ArbitrageOpportunity {
|
||||
s.logger.Debug(fmt.Sprintf("Searching for arbitrage opportunities for pool %s", event.PoolAddress))
|
||||
|
||||
opportunities := make([]stypes.ArbitrageOpportunity, 0)
|
||||
|
||||
// Get related pools for the same token pair
|
||||
relatedPools := marketScanner.FindRelatedPools(event.Token0, event.Token1)
|
||||
|
||||
// If we have related pools, compare prices
|
||||
if len(relatedPools) > 0 {
|
||||
// Get the current price in this pool
|
||||
currentPrice := movement.PriceBefore
|
||||
|
||||
// Compare with prices in related pools
|
||||
for _, pool := range relatedPools {
|
||||
// Skip the same pool
|
||||
if pool.Address == event.PoolAddress {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get pool data
|
||||
poolData, err := marketScanner.GetPoolData(pool.Address.Hex())
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Error getting pool data for related pool %s: %v", pool.Address.Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if poolData.SqrtPriceX96 is nil to prevent panic
|
||||
if poolData.SqrtPriceX96 == nil {
|
||||
s.logger.Error(fmt.Sprintf("Pool data for %s has nil SqrtPriceX96", pool.Address.Hex()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate price in the related pool using cached function
|
||||
relatedPrice := uniswap.SqrtPriceX96ToPriceCached(poolData.SqrtPriceX96.ToBig())
|
||||
|
||||
// Check if currentPrice or relatedPrice is nil to prevent panic
|
||||
if currentPrice == nil || relatedPrice == nil {
|
||||
s.logger.Error(fmt.Sprintf("Nil price detected for pool comparison"))
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate price difference
|
||||
priceDiff := new(big.Float).Sub(currentPrice, relatedPrice)
|
||||
priceDiffRatio := new(big.Float).Quo(priceDiff, relatedPrice)
|
||||
|
||||
// If there's a significant price difference, we might have an arbitrage opportunity
|
||||
priceDiffFloat, _ := priceDiffRatio.Float64()
|
||||
// Lower threshold for Arbitrum where spreads are smaller
|
||||
arbitrageThreshold := 0.001 // 0.1% threshold instead of 0.5%
|
||||
if priceDiffFloat > arbitrageThreshold {
|
||||
// Estimate potential profit
|
||||
estimatedProfit := marketScanner.EstimateProfit(event, pool, priceDiffFloat)
|
||||
|
||||
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
|
||||
opp := stypes.ArbitrageOpportunity{
|
||||
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
|
||||
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
|
||||
Profit: estimatedProfit,
|
||||
GasEstimate: big.NewInt(300000), // Estimated gas cost
|
||||
ROI: priceDiffFloat * 100, // Convert to percentage
|
||||
Protocol: fmt.Sprintf("%s->%s", event.Protocol, pool.Protocol),
|
||||
}
|
||||
opportunities = append(opportunities, opp)
|
||||
s.logger.Info(fmt.Sprintf("Found arbitrage opportunity: %+v", opp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also look for triangular arbitrage opportunities
|
||||
triangularOpps := marketScanner.FindTriangularArbitrageOpportunities(event)
|
||||
opportunities = append(opportunities, triangularOpps...)
|
||||
|
||||
return opportunities
|
||||
}
|
||||
|
||||
// calculateSwapUSDValues calculates USD values for swap amounts using the profit calculator's price oracle
|
||||
func (s *SwapAnalyzer) calculateSwapUSDValues(swapData *marketdata.SwapEventData, fee uint32) (amountInUSD, amountOutUSD, feeUSD float64) {
|
||||
if s.profitCalculator == nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
// Get token prices in USD
|
||||
token0Price := s.getTokenPriceUSD(swapData.Token0)
|
||||
token1Price := s.getTokenPriceUSD(swapData.Token1)
|
||||
|
||||
// Calculate decimals for proper conversion
|
||||
token0Decimals := s.getTokenDecimals(swapData.Token0)
|
||||
token1Decimals := s.getTokenDecimals(swapData.Token1)
|
||||
|
||||
// Calculate amount in USD
|
||||
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
|
||||
amount0InFloat := s.bigIntToFloat(swapData.Amount0In, token0Decimals)
|
||||
amountInUSD = amount0InFloat * token0Price
|
||||
} else if swapData.Amount1In != nil && swapData.Amount1In.Sign() > 0 {
|
||||
amount1InFloat := s.bigIntToFloat(swapData.Amount1In, token1Decimals)
|
||||
amountInUSD = amount1InFloat * token1Price
|
||||
}
|
||||
|
||||
// Calculate amount out USD
|
||||
if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
|
||||
amount0OutFloat := s.bigIntToFloat(swapData.Amount0Out, token0Decimals)
|
||||
amountOutUSD = amount0OutFloat * token0Price
|
||||
} else if swapData.Amount1Out != nil && swapData.Amount1Out.Sign() > 0 {
|
||||
amount1OutFloat := s.bigIntToFloat(swapData.Amount1Out, token1Decimals)
|
||||
amountOutUSD = amount1OutFloat * token1Price
|
||||
}
|
||||
|
||||
// Calculate fee USD (fee tier as percentage of input amount)
|
||||
feePercent := float64(fee) / 1000000.0 // Convert from basis points
|
||||
feeUSD = amountInUSD * feePercent
|
||||
|
||||
return amountInUSD, amountOutUSD, feeUSD
|
||||
}
|
||||
|
||||
// calculateSwapPriceImpact calculates the price impact of a swap based on pool liquidity and amounts
|
||||
func (s *SwapAnalyzer) calculateSwapPriceImpact(event events.Event, swapData *marketdata.SwapEventData) float64 {
|
||||
if event.SqrtPriceX96 == nil || event.Liquidity == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Get pre-swap price from sqrtPriceX96
|
||||
prePrice := s.sqrtPriceX96ToPrice(event.SqrtPriceX96)
|
||||
if prePrice == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Calculate effective swap size in token0 terms
|
||||
var swapSize *big.Int
|
||||
if swapData.Amount0In != nil && swapData.Amount0In.Sign() > 0 {
|
||||
swapSize = swapData.Amount0In
|
||||
} else if swapData.Amount0Out != nil && swapData.Amount0Out.Sign() > 0 {
|
||||
swapSize = swapData.Amount0Out
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Calculate price impact as percentage of pool liquidity
|
||||
liquidity := event.Liquidity.ToBig()
|
||||
if liquidity.Sign() == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Proper price impact calculation for AMMs: impact = swapSize / (liquidity + swapSize)
|
||||
// This is more accurate than the quadratic approximation for real AMMs
|
||||
swapSizeFloat := new(big.Float).SetInt(swapSize)
|
||||
liquidityFloat := new(big.Float).SetInt(liquidity)
|
||||
|
||||
// Calculate the price impact ratio
|
||||
priceImpactRatio := new(big.Float).Quo(swapSizeFloat, new(big.Float).Add(liquidityFloat, swapSizeFloat))
|
||||
|
||||
// Convert to percentage
|
||||
priceImpactPercent, _ := priceImpactRatio.Float64()
|
||||
return priceImpactPercent * 100.0
|
||||
}
|
||||
|
||||
// getTokenPriceUSD gets the USD price of a token using various price sources
|
||||
func (s *SwapAnalyzer) getTokenPriceUSD(tokenAddr common.Address) float64 {
|
||||
// Known token prices (in a production system, this would query price oracles)
|
||||
knownPrices := map[common.Address]float64{
|
||||
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 2000.0, // WETH
|
||||
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 1.0, // USDC
|
||||
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 1.0, // USDC.e
|
||||
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 1.0, // USDT
|
||||
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 43000.0, // WBTC
|
||||
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 0.75, // ARB
|
||||
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 45.0, // GMX
|
||||
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 12.0, // LINK
|
||||
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 8.0, // UNI
|
||||
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 85.0, // AAVE
|
||||
}
|
||||
|
||||
if price, exists := knownPrices[tokenAddr]; exists {
|
||||
return price
|
||||
}
|
||||
|
||||
// For unknown tokens, return 0 (in production, would query price oracle or DEX)
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// getTokenDecimals returns the decimal places for a token
|
||||
func (s *SwapAnalyzer) getTokenDecimals(tokenAddr common.Address) uint8 {
|
||||
// Known token decimals
|
||||
knownDecimals := map[common.Address]uint8{
|
||||
common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1"): 18, // WETH
|
||||
common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831"): 6, // USDC
|
||||
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): 6, // USDC.e
|
||||
common.HexToAddress("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"): 6, // USDT
|
||||
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): 8, // WBTC
|
||||
common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548"): 18, // ARB
|
||||
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): 18, // GMX
|
||||
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): 18, // LINK
|
||||
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): 18, // UNI
|
||||
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): 18, // AAVE
|
||||
}
|
||||
|
||||
if decimals, exists := knownDecimals[tokenAddr]; exists {
|
||||
return decimals
|
||||
}
|
||||
|
||||
// Default to 18 for unknown tokens
|
||||
return 18
|
||||
}
|
||||
|
||||
// bigIntToFloat converts a big.Int amount to float64 accounting for token decimals
|
||||
func (s *SwapAnalyzer) bigIntToFloat(amount *big.Int, decimals uint8) float64 {
|
||||
if amount == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)
|
||||
amountFloat := new(big.Float).SetInt(amount)
|
||||
divisorFloat := new(big.Float).SetInt(divisor)
|
||||
|
||||
result := new(big.Float).Quo(amountFloat, divisorFloat)
|
||||
resultFloat, _ := result.Float64()
|
||||
|
||||
return resultFloat
|
||||
}
|
||||
|
||||
// sqrtPriceX96ToPrice converts sqrtPriceX96 to a regular price using cached mathematical functions
|
||||
func (s *SwapAnalyzer) sqrtPriceX96ToPrice(sqrtPriceX96 *uint256.Int) float64 {
|
||||
if sqrtPriceX96 == nil {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Use cached function for optimized calculation
|
||||
price := uniswap.SqrtPriceX96ToPriceCached(sqrtPriceX96.ToBig())
|
||||
|
||||
priceFloat, _ := price.Float64()
|
||||
return priceFloat
|
||||
}
|
||||
Reference in New Issue
Block a user