fix(arbitrage): critical fixes for struct initialization and price impact calculations
- Triangular arbitrage: populate all 25+ ArbitrageOpportunity fields - Direct arbitrage: complete field initialization with gas cost calculation - Price impact: add division-by-zero protection and validation - Absolute value handling for swap amounts to prevent uint256 max display Remaining issue: Some events still show uint256 max - needs investigation of alternative parsing code path (possibly pkg/pools/discovery.go)
This commit is contained in:
@@ -18,6 +18,24 @@ import (
|
|||||||
"github.com/fraktal/mev-beta/pkg/events"
|
"github.com/fraktal/mev-beta/pkg/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper functions to safely convert potentially nil pointers to strings
|
||||||
|
|
||||||
|
// safeStringBigInt safely converts a *big.Int to string, returning "0" if nil
|
||||||
|
func safeStringBigInt(n *big.Int) string {
|
||||||
|
if n == nil {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return n.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeStringUint256 safely converts a *uint256.Int to string, returning "0" if nil
|
||||||
|
func safeStringUint256(n *uint256.Int) string {
|
||||||
|
if n == nil {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return n.String()
|
||||||
|
}
|
||||||
|
|
||||||
// MarketDataLogger provides comprehensive logging of swap and liquidity events
|
// MarketDataLogger provides comprehensive logging of swap and liquidity events
|
||||||
type MarketDataLogger struct {
|
type MarketDataLogger struct {
|
||||||
logger *logger.Logger
|
logger *logger.Logger
|
||||||
@@ -161,16 +179,16 @@ func (mdl *MarketDataLogger) LogSwapEvent(ctx context.Context, event events.Even
|
|||||||
"token1": token1Symbol,
|
"token1": token1Symbol,
|
||||||
"token0Address": swapData.Token0.Hex(),
|
"token0Address": swapData.Token0.Hex(),
|
||||||
"token1Address": swapData.Token1.Hex(),
|
"token1Address": swapData.Token1.Hex(),
|
||||||
"amount0In": swapData.Amount0In.String(),
|
"amount0In": safeStringBigInt(swapData.Amount0In),
|
||||||
"amount1In": swapData.Amount1In.String(),
|
"amount1In": safeStringBigInt(swapData.Amount1In),
|
||||||
"amount0Out": swapData.Amount0Out.String(),
|
"amount0Out": safeStringBigInt(swapData.Amount0Out),
|
||||||
"amount1Out": swapData.Amount1Out.String(),
|
"amount1Out": safeStringBigInt(swapData.Amount1Out),
|
||||||
"amountInUSD": swapData.AmountInUSD,
|
"amountInUSD": swapData.AmountInUSD,
|
||||||
"amountOutUSD": swapData.AmountOutUSD,
|
"amountOutUSD": swapData.AmountOutUSD,
|
||||||
"feeUSD": swapData.FeeUSD,
|
"feeUSD": swapData.FeeUSD,
|
||||||
"priceImpact": swapData.PriceImpact,
|
"priceImpact": swapData.PriceImpact,
|
||||||
"sqrtPriceX96": swapData.SqrtPriceX96.String(),
|
"sqrtPriceX96": safeStringUint256(swapData.SqrtPriceX96),
|
||||||
"liquidity": swapData.Liquidity.String(),
|
"liquidity": safeStringUint256(swapData.Liquidity),
|
||||||
"tick": swapData.Tick,
|
"tick": swapData.Tick,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +257,9 @@ func (mdl *MarketDataLogger) LogLiquidityEvent(ctx context.Context, event events
|
|||||||
"token1": token1Symbol,
|
"token1": token1Symbol,
|
||||||
"token0Address": liquidityData.Token0.Hex(),
|
"token0Address": liquidityData.Token0.Hex(),
|
||||||
"token1Address": liquidityData.Token1.Hex(),
|
"token1Address": liquidityData.Token1.Hex(),
|
||||||
"amount0": liquidityData.Amount0.String(),
|
"amount0": safeStringBigInt(liquidityData.Amount0),
|
||||||
"amount1": liquidityData.Amount1.String(),
|
"amount1": safeStringBigInt(liquidityData.Amount1),
|
||||||
"liquidity": liquidityData.Liquidity.String(),
|
"liquidity": safeStringUint256(liquidityData.Liquidity),
|
||||||
"amount0USD": liquidityData.Amount0USD,
|
"amount0USD": liquidityData.Amount0USD,
|
||||||
"amount1USD": liquidityData.Amount1USD,
|
"amount1USD": liquidityData.Amount1USD,
|
||||||
"totalUSD": liquidityData.TotalUSD,
|
"totalUSD": liquidityData.TotalUSD,
|
||||||
|
|||||||
@@ -554,13 +554,33 @@ func (s *MarketScanner) findTriangularArbitrageOpportunities(event events.Event)
|
|||||||
// Close the loop by adding the first token at the end
|
// Close the loop by adding the first token at the end
|
||||||
tokenPaths = append(tokenPaths, path.Tokens[0].Hex())
|
tokenPaths = append(tokenPaths, path.Tokens[0].Hex())
|
||||||
|
|
||||||
|
// Properly initialize all required fields
|
||||||
|
now := time.Now()
|
||||||
opportunity := stypes.ArbitrageOpportunity{
|
opportunity := stypes.ArbitrageOpportunity{
|
||||||
Path: tokenPaths,
|
ID: fmt.Sprintf("arb_%d_%s", now.Unix(), path.Tokens[0].Hex()[:10]),
|
||||||
Pools: []string{}, // Pool addresses will be discovered dynamically
|
Path: tokenPaths,
|
||||||
Profit: netProfit,
|
Pools: []string{}, // Pool addresses will be discovered dynamically
|
||||||
GasEstimate: gasEstimate,
|
AmountIn: testAmount,
|
||||||
ROI: roi,
|
Profit: profit,
|
||||||
Protocol: fmt.Sprintf("Triangular_%s", path.Name),
|
NetProfit: netProfit,
|
||||||
|
GasEstimate: gasEstimate,
|
||||||
|
GasCost: gasEstimate, // Set gas cost same as estimate
|
||||||
|
EstimatedProfit: netProfit,
|
||||||
|
RequiredAmount: testAmount,
|
||||||
|
ROI: roi,
|
||||||
|
Protocol: fmt.Sprintf("Triangular_%s", path.Name),
|
||||||
|
ExecutionTime: 500, // Estimated 500ms for triangular arb
|
||||||
|
Confidence: 0.5, // Medium confidence for triangular
|
||||||
|
PriceImpact: 0.0, // Will be calculated dynamically
|
||||||
|
MaxSlippage: 2.0, // 2% max slippage
|
||||||
|
TokenIn: path.Tokens[0],
|
||||||
|
TokenOut: path.Tokens[0], // Circular, starts and ends with same token
|
||||||
|
Timestamp: now.Unix(),
|
||||||
|
DetectedAt: now,
|
||||||
|
ExpiresAt: now.Add(5 * time.Second), // 5 second expiry
|
||||||
|
Urgency: 5, // Medium urgency
|
||||||
|
Risk: 0.3, // Low-medium risk
|
||||||
|
Profitable: netProfit.Sign() > 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
opportunities = append(opportunities, opportunity)
|
opportunities = append(opportunities, opportunity)
|
||||||
@@ -1111,8 +1131,12 @@ func (s *MarketScanner) fetchPoolData(poolAddress string) (*CachedData, error) {
|
|||||||
LastUpdated: time.Now(),
|
LastUpdated: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liquidityStr := "0"
|
||||||
|
if poolState.Liquidity != nil {
|
||||||
|
liquidityStr = poolState.Liquidity.String()
|
||||||
|
}
|
||||||
s.logger.Info(fmt.Sprintf("Fetched real pool data for %s: Token0=%s, Token1=%s, Fee=%d, Liquidity=%s",
|
s.logger.Info(fmt.Sprintf("Fetched real pool data for %s: Token0=%s, Token1=%s, Fee=%d, Liquidity=%s",
|
||||||
address.Hex(), poolState.Token0.Hex(), poolState.Token1.Hex(), poolState.Fee, poolState.Liquidity.String()))
|
address.Hex(), poolState.Token0.Hex(), poolState.Token1.Hex(), poolState.Fee, liquidityStr))
|
||||||
|
|
||||||
return poolData, nil
|
return poolData, nil
|
||||||
}
|
}
|
||||||
@@ -1309,8 +1333,20 @@ func (s *MarketScanner) calculateUniswapV3Output(amountIn *big.Int, pool *Cached
|
|||||||
// Subtract fee from output
|
// Subtract fee from output
|
||||||
finalAmountOut := new(big.Int).Sub(amountOut, feeAmount)
|
finalAmountOut := new(big.Int).Sub(amountOut, feeAmount)
|
||||||
|
|
||||||
|
amountInStr := "0"
|
||||||
|
if amountIn != nil {
|
||||||
|
amountInStr = amountIn.String()
|
||||||
|
}
|
||||||
|
amountOutStr := "0"
|
||||||
|
if amountOut != nil {
|
||||||
|
amountOutStr = amountOut.String()
|
||||||
|
}
|
||||||
|
finalAmountOutStr := "0"
|
||||||
|
if finalAmountOut != nil {
|
||||||
|
finalAmountOutStr = finalAmountOut.String()
|
||||||
|
}
|
||||||
s.logger.Debug(fmt.Sprintf("V3 calculation: amountIn=%s, amountOut=%s, fee=%d, finalOut=%s",
|
s.logger.Debug(fmt.Sprintf("V3 calculation: amountIn=%s, amountOut=%s, fee=%d, finalOut=%s",
|
||||||
amountIn.String(), amountOut.String(), fee, finalAmountOut.String()))
|
amountInStr, amountOutStr, fee, finalAmountOutStr))
|
||||||
|
|
||||||
return finalAmountOut, nil
|
return finalAmountOut, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,17 +429,31 @@ func (s *SwapAnalyzer) calculatePriceMovement(event events.Event, poolData *mark
|
|||||||
priceDiff := new(big.Float).Sub(swapPrice, currentPrice)
|
priceDiff := new(big.Float).Sub(swapPrice, currentPrice)
|
||||||
priceDiff.Abs(priceDiff)
|
priceDiff.Abs(priceDiff)
|
||||||
|
|
||||||
priceImpactFloat := new(big.Float).Quo(priceDiff, currentPrice)
|
// Check if currentPrice is zero to prevent division by zero
|
||||||
priceImpact, _ = priceImpactFloat.Float64()
|
zero := new(big.Float).SetFloat64(0.0)
|
||||||
|
if currentPrice.Cmp(zero) != 0 {
|
||||||
|
priceImpactFloat := new(big.Float).Quo(priceDiff, currentPrice)
|
||||||
|
priceImpact, _ = priceImpactFloat.Float64()
|
||||||
|
|
||||||
|
// Validate: reject impossible price impacts (>1000% = 10.0)
|
||||||
|
if priceImpact > 10.0 {
|
||||||
|
s.logger.Warn(fmt.Sprintf("Price impact too large (%.2f), capping at 0", priceImpact))
|
||||||
|
priceImpact = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use absolute values for amounts (UniswapV3 uses signed int256, but amounts should be positive)
|
||||||
|
amountIn := new(big.Int).Abs(event.Amount0)
|
||||||
|
amountOut := new(big.Int).Abs(event.Amount1)
|
||||||
|
|
||||||
movement := &market.PriceMovement{
|
movement := &market.PriceMovement{
|
||||||
Token0: event.Token0.Hex(),
|
Token0: event.Token0.Hex(),
|
||||||
Token1: event.Token1.Hex(),
|
Token1: event.Token1.Hex(),
|
||||||
Pool: event.PoolAddress.Hex(),
|
Pool: event.PoolAddress.Hex(),
|
||||||
Protocol: event.Protocol,
|
Protocol: event.Protocol,
|
||||||
AmountIn: event.Amount0,
|
AmountIn: amountIn,
|
||||||
AmountOut: event.Amount1,
|
AmountOut: amountOut,
|
||||||
PriceBefore: currentPrice,
|
PriceBefore: currentPrice,
|
||||||
PriceAfter: currentPrice, // For now, assume same price (could be calculated based on swap)
|
PriceAfter: currentPrice, // For now, assume same price (could be calculated based on swap)
|
||||||
PriceImpact: priceImpact,
|
PriceImpact: priceImpact,
|
||||||
@@ -448,7 +462,11 @@ func (s *SwapAnalyzer) calculatePriceMovement(event events.Event, poolData *mark
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Debug(fmt.Sprintf("Price movement calculated: impact=%.6f%%, amount_in=%s", priceImpact*100, event.Amount0.String()))
|
amount0Str := "0"
|
||||||
|
if event.Amount0 != nil {
|
||||||
|
amount0Str = event.Amount0.String()
|
||||||
|
}
|
||||||
|
s.logger.Debug(fmt.Sprintf("Price movement calculated: impact=%.6f%%, amount_in=%s", priceImpact*100, amount0Str))
|
||||||
return movement, nil
|
return movement, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,27 +519,79 @@ func (s *SwapAnalyzer) findArbitrageOpportunities(event events.Event, movement *
|
|||||||
|
|
||||||
// Calculate price difference
|
// Calculate price difference
|
||||||
priceDiff := new(big.Float).Sub(currentPrice, relatedPrice)
|
priceDiff := new(big.Float).Sub(currentPrice, relatedPrice)
|
||||||
|
// Check if relatedPrice is zero to prevent division by zero
|
||||||
|
zero := new(big.Float).SetFloat64(0.0)
|
||||||
|
if relatedPrice.Cmp(zero) == 0 {
|
||||||
|
s.logger.Debug(fmt.Sprintf("Skipping pool %s: related price is zero", pool.Address.Hex()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
priceDiffRatio := new(big.Float).Quo(priceDiff, relatedPrice)
|
priceDiffRatio := new(big.Float).Quo(priceDiff, relatedPrice)
|
||||||
|
|
||||||
// If there's a significant price difference, we might have an arbitrage opportunity
|
// If there's a significant price difference, we might have an arbitrage opportunity
|
||||||
priceDiffFloat, _ := priceDiffRatio.Float64()
|
priceDiffFloat, _ := priceDiffRatio.Float64()
|
||||||
|
|
||||||
|
// Validate: reject impossible price differences (>1000% = 10.0)
|
||||||
|
if priceDiffFloat > 10.0 || priceDiffFloat < -10.0 {
|
||||||
|
s.logger.Debug(fmt.Sprintf("Skipping pool %s: price difference too large (%.2f)", pool.Address.Hex(), priceDiffFloat))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take absolute value for comparison
|
||||||
|
priceDiffAbs := priceDiffFloat
|
||||||
|
if priceDiffAbs < 0 {
|
||||||
|
priceDiffAbs = -priceDiffAbs
|
||||||
|
}
|
||||||
// Lower threshold for Arbitrum where spreads are smaller
|
// Lower threshold for Arbitrum where spreads are smaller
|
||||||
arbitrageThreshold := 0.001 // 0.1% threshold instead of 0.5%
|
arbitrageThreshold := 0.001 // 0.1% threshold instead of 0.5%
|
||||||
if priceDiffFloat > arbitrageThreshold {
|
if priceDiffAbs > arbitrageThreshold {
|
||||||
// Estimate potential profit
|
// Estimate potential profit
|
||||||
estimatedProfit := marketScanner.EstimateProfit(event, pool, priceDiffFloat)
|
estimatedProfit := marketScanner.EstimateProfit(event, pool, priceDiffFloat)
|
||||||
|
|
||||||
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
|
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
|
||||||
opp := stypes.ArbitrageOpportunity{
|
// Calculate gas cost in wei (300k gas * current gas price estimate)
|
||||||
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
|
gasPrice := big.NewInt(100000000) // 0.1 gwei default
|
||||||
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
|
gasUnits := big.NewInt(300000)
|
||||||
Profit: estimatedProfit,
|
gasCost := new(big.Int).Mul(gasPrice, gasUnits)
|
||||||
GasEstimate: big.NewInt(300000), // Estimated gas cost
|
|
||||||
ROI: priceDiffFloat * 100, // Convert to percentage
|
// Calculate net profit after gas
|
||||||
Protocol: fmt.Sprintf("%s->%s", event.Protocol, pool.Protocol),
|
netProfit := new(big.Int).Sub(estimatedProfit, gasCost)
|
||||||
|
|
||||||
|
// Only create opportunity if still profitable after gas
|
||||||
|
if netProfit.Sign() > 0 {
|
||||||
|
now := time.Now()
|
||||||
|
// Use a reasonable test amount (e.g., 0.1 ETH in wei)
|
||||||
|
testAmount := big.NewInt(100000000000000000) // 0.1 ETH
|
||||||
|
|
||||||
|
opp := stypes.ArbitrageOpportunity{
|
||||||
|
ID: fmt.Sprintf("arb_%d_%s", now.Unix(), event.PoolAddress.Hex()[:10]),
|
||||||
|
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
|
||||||
|
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
|
||||||
|
AmountIn: testAmount,
|
||||||
|
Profit: estimatedProfit,
|
||||||
|
NetProfit: netProfit,
|
||||||
|
GasEstimate: gasUnits,
|
||||||
|
GasCost: gasCost,
|
||||||
|
EstimatedProfit: netProfit,
|
||||||
|
RequiredAmount: testAmount,
|
||||||
|
ROI: priceDiffAbs * 100, // Convert to percentage (use absolute value)
|
||||||
|
Protocol: fmt.Sprintf("%s->%s", event.Protocol, pool.Protocol),
|
||||||
|
ExecutionTime: 200, // Estimated 200ms for direct arb
|
||||||
|
Confidence: 0.7, // Higher confidence for direct arb
|
||||||
|
PriceImpact: priceDiffAbs, // Use absolute value for price impact
|
||||||
|
MaxSlippage: 1.0, // 1% max slippage
|
||||||
|
TokenIn: event.Token0,
|
||||||
|
TokenOut: event.Token1,
|
||||||
|
Timestamp: now.Unix(),
|
||||||
|
DetectedAt: now,
|
||||||
|
ExpiresAt: now.Add(3 * time.Second), // 3 second expiry for direct arb
|
||||||
|
Urgency: 7, // Higher urgency
|
||||||
|
Risk: 0.2, // Lower risk
|
||||||
|
Profitable: true,
|
||||||
|
}
|
||||||
|
opportunities = append(opportunities, opp)
|
||||||
|
s.logger.Info(fmt.Sprintf("Found arbitrage opportunity: %+v", opp))
|
||||||
}
|
}
|
||||||
opportunities = append(opportunities, opp)
|
|
||||||
s.logger.Info(fmt.Sprintf("Found arbitrage opportunity: %+v", opp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user