Files
mev-beta/orig/pkg/dex/analyzer.go
Administrator 803de231ba feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor:

**Structure Changes:**
- Moved all V1 code to orig/ folder (preserved with git mv)
- Created docs/planning/ directory
- Added orig/README_V1.md explaining V1 preservation

**Planning Documents:**
- 00_V2_MASTER_PLAN.md: Complete architecture overview
  - Executive summary of critical V1 issues
  - High-level component architecture diagrams
  - 5-phase implementation roadmap
  - Success metrics and risk mitigation

- 07_TASK_BREAKDOWN.md: Atomic task breakdown
  - 99+ hours of detailed tasks
  - Every task < 2 hours (atomic)
  - Clear dependencies and success criteria
  - Organized by implementation phase

**V2 Key Improvements:**
- Per-exchange parsers (factory pattern)
- Multi-layer strict validation
- Multi-index pool cache
- Background validation pipeline
- Comprehensive observability

**Critical Issues Addressed:**
- Zero address tokens (strict validation + cache enrichment)
- Parsing accuracy (protocol-specific parsers)
- No audit trail (background validation channel)
- Inefficient lookups (multi-index cache)
- Stats disconnection (event-driven metrics)

Next Steps:
1. Review planning documents
2. Begin Phase 1: Foundation (P1-001 through P1-010)
3. Implement parsers in Phase 2
4. Build cache system in Phase 3
5. Add validation pipeline in Phase 4
6. Migrate and test in Phase 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:14:26 +01:00

444 lines
12 KiB
Go

package dex
import (
"context"
"fmt"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
// CrossDEXAnalyzer finds arbitrage opportunities across multiple DEXes
type CrossDEXAnalyzer struct {
registry *Registry
client *ethclient.Client
mu sync.RWMutex
}
// NewCrossDEXAnalyzer creates a new cross-DEX analyzer
func NewCrossDEXAnalyzer(registry *Registry, client *ethclient.Client) *CrossDEXAnalyzer {
return &CrossDEXAnalyzer{
registry: registry,
client: client,
}
}
// FindArbitrageOpportunities finds arbitrage opportunities for a token pair
func (a *CrossDEXAnalyzer) FindArbitrageOpportunities(
ctx context.Context,
tokenA, tokenB common.Address,
amountIn *big.Int,
minProfitETH float64,
) ([]*ArbitragePath, error) {
dexes := a.registry.GetAll()
if len(dexes) < 2 {
return nil, fmt.Errorf("need at least 2 active DEXes for arbitrage")
}
type quoteResult struct {
dex DEXProtocol
quote *PriceQuote
err error
}
opportunities := make([]*ArbitragePath, 0)
// Get quotes from all DEXes in parallel for A -> B
buyQuotes := make(map[DEXProtocol]*PriceQuote)
buyResults := make(chan quoteResult, len(dexes))
for _, dex := range dexes {
go func(d *DEXInfo) {
quote, err := d.Decoder.GetQuote(ctx, a.client, tokenA, tokenB, amountIn)
buyResults <- quoteResult{dex: d.Protocol, quote: quote, err: err}
}(dex)
}
// Collect buy quotes
for i := 0; i < len(dexes); i++ {
res := <-buyResults
if res.err == nil && res.quote != nil {
buyQuotes[res.dex] = res.quote
}
}
// For each successful buy quote, get sell quotes on other DEXes
for buyDEX, buyQuote := range buyQuotes {
// Get amount out from buy
intermediateAmount := buyQuote.ExpectedOut
sellResults := make(chan quoteResult, len(dexes)-1)
sellCount := 0
// Query all other DEXes for selling B -> A
for _, dex := range dexes {
if dex.Protocol == buyDEX {
continue // Skip same DEX
}
sellCount++
go func(d *DEXInfo) {
quote, err := d.Decoder.GetQuote(ctx, a.client, tokenB, tokenA, intermediateAmount)
sellResults <- quoteResult{dex: d.Protocol, quote: quote, err: err}
}(dex)
}
// Check each sell quote for profitability
for i := 0; i < sellCount; i++ {
res := <-sellResults
if res.err != nil || res.quote == nil {
continue
}
sellQuote := res.quote
// Calculate profit
finalAmount := sellQuote.ExpectedOut
profit := new(big.Int).Sub(finalAmount, amountIn)
// Estimate gas cost (rough estimate)
gasUnits := buyQuote.GasEstimate + sellQuote.GasEstimate
gasPrice := big.NewInt(100000000) // 0.1 gwei (rough estimate)
gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice)
netProfit := new(big.Int).Sub(profit, gasCost)
// Convert to ETH
profitETH := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(big.NewInt(1e18)),
)
profitFloat, _ := profitETH.Float64()
// Only consider profitable opportunities
if profitFloat > minProfitETH {
roi := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(amountIn),
)
roiFloat, _ := roi.Float64()
path := &ArbitragePath{
Hops: []*PathHop{
{
DEX: buyDEX,
PoolAddress: buyQuote.PoolAddress,
TokenIn: tokenA,
TokenOut: tokenB,
AmountIn: amountIn,
AmountOut: buyQuote.ExpectedOut,
Fee: buyQuote.Fee,
},
{
DEX: res.dex,
PoolAddress: sellQuote.PoolAddress,
TokenIn: tokenB,
TokenOut: tokenA,
AmountIn: intermediateAmount,
AmountOut: sellQuote.ExpectedOut,
Fee: sellQuote.Fee,
},
},
TotalProfit: profit,
ProfitETH: profitFloat,
ROI: roiFloat,
GasCost: gasCost,
NetProfit: netProfit,
Confidence: a.calculateConfidence(buyQuote, sellQuote),
}
opportunities = append(opportunities, path)
}
}
}
return opportunities, nil
}
// FindMultiHopOpportunities finds arbitrage opportunities with multiple hops
func (a *CrossDEXAnalyzer) FindMultiHopOpportunities(
ctx context.Context,
startToken common.Address,
intermediateTokens []common.Address,
amountIn *big.Int,
maxHops int,
minProfitETH float64,
) ([]*ArbitragePath, error) {
if maxHops < 2 || maxHops > 4 {
return nil, fmt.Errorf("maxHops must be between 2 and 4")
}
opportunities := make([]*ArbitragePath, 0)
// For 3-hop: Start -> Token1 -> Token2 -> Start
if maxHops >= 3 {
for _, token1 := range intermediateTokens {
for _, token2 := range intermediateTokens {
if token1 == token2 || token1 == startToken || token2 == startToken {
continue
}
path, err := a.evaluate3HopPath(ctx, startToken, token1, token2, amountIn, minProfitETH)
if err == nil && path != nil {
opportunities = append(opportunities, path)
}
}
}
}
// For 4-hop: Start -> Token1 -> Token2 -> Token3 -> Start
if maxHops >= 4 {
for _, token1 := range intermediateTokens {
for _, token2 := range intermediateTokens {
for _, token3 := range intermediateTokens {
if token1 == token2 || token1 == token3 || token2 == token3 ||
token1 == startToken || token2 == startToken || token3 == startToken {
continue
}
path, err := a.evaluate4HopPath(ctx, startToken, token1, token2, token3, amountIn, minProfitETH)
if err == nil && path != nil {
opportunities = append(opportunities, path)
}
}
}
}
}
return opportunities, nil
}
// evaluate3HopPath evaluates a 3-hop arbitrage path
func (a *CrossDEXAnalyzer) evaluate3HopPath(
ctx context.Context,
token0, token1, token2 common.Address,
amountIn *big.Int,
minProfitETH float64,
) (*ArbitragePath, error) {
// Hop 1: token0 -> token1
quote1, err := a.registry.GetBestQuote(ctx, token0, token1, amountIn)
if err != nil {
return nil, err
}
// Hop 2: token1 -> token2
quote2, err := a.registry.GetBestQuote(ctx, token1, token2, quote1.ExpectedOut)
if err != nil {
return nil, err
}
// Hop 3: token2 -> token0 (back to start)
quote3, err := a.registry.GetBestQuote(ctx, token2, token0, quote2.ExpectedOut)
if err != nil {
return nil, err
}
// Calculate profit
finalAmount := quote3.ExpectedOut
profit := new(big.Int).Sub(finalAmount, amountIn)
// Estimate gas cost
gasUnits := quote1.GasEstimate + quote2.GasEstimate + quote3.GasEstimate
gasPrice := big.NewInt(100000000) // 0.1 gwei
gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice)
netProfit := new(big.Int).Sub(profit, gasCost)
profitETH := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(big.NewInt(1e18)),
)
profitFloat, _ := profitETH.Float64()
if profitFloat < minProfitETH {
return nil, fmt.Errorf("insufficient profit")
}
roi := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(amountIn),
)
roiFloat, _ := roi.Float64()
return &ArbitragePath{
Hops: []*PathHop{
{
DEX: quote1.DEX,
PoolAddress: quote1.PoolAddress,
TokenIn: token0,
TokenOut: token1,
AmountIn: amountIn,
AmountOut: quote1.ExpectedOut,
Fee: quote1.Fee,
},
{
DEX: quote2.DEX,
PoolAddress: quote2.PoolAddress,
TokenIn: token1,
TokenOut: token2,
AmountIn: quote1.ExpectedOut,
AmountOut: quote2.ExpectedOut,
Fee: quote2.Fee,
},
{
DEX: quote3.DEX,
PoolAddress: quote3.PoolAddress,
TokenIn: token2,
TokenOut: token0,
AmountIn: quote2.ExpectedOut,
AmountOut: quote3.ExpectedOut,
Fee: quote3.Fee,
},
},
TotalProfit: profit,
ProfitETH: profitFloat,
ROI: roiFloat,
GasCost: gasCost,
NetProfit: netProfit,
Confidence: 0.6, // Lower confidence for 3-hop
}, nil
}
// evaluate4HopPath evaluates a 4-hop arbitrage path
func (a *CrossDEXAnalyzer) evaluate4HopPath(
ctx context.Context,
token0, token1, token2, token3 common.Address,
amountIn *big.Int,
minProfitETH float64,
) (*ArbitragePath, error) {
// Similar to evaluate3HopPath but with 4 hops
// Hop 1: token0 -> token1
quote1, err := a.registry.GetBestQuote(ctx, token0, token1, amountIn)
if err != nil {
return nil, err
}
// Hop 2: token1 -> token2
quote2, err := a.registry.GetBestQuote(ctx, token1, token2, quote1.ExpectedOut)
if err != nil {
return nil, err
}
// Hop 3: token2 -> token3
quote3, err := a.registry.GetBestQuote(ctx, token2, token3, quote2.ExpectedOut)
if err != nil {
return nil, err
}
// Hop 4: token3 -> token0 (back to start)
quote4, err := a.registry.GetBestQuote(ctx, token3, token0, quote3.ExpectedOut)
if err != nil {
return nil, err
}
// Calculate profit
finalAmount := quote4.ExpectedOut
profit := new(big.Int).Sub(finalAmount, amountIn)
// Estimate gas cost
gasUnits := quote1.GasEstimate + quote2.GasEstimate + quote3.GasEstimate + quote4.GasEstimate
gasPrice := big.NewInt(100000000)
gasCost := new(big.Int).Mul(big.NewInt(int64(gasUnits)), gasPrice)
netProfit := new(big.Int).Sub(profit, gasCost)
profitETH := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(big.NewInt(1e18)),
)
profitFloat, _ := profitETH.Float64()
if profitFloat < minProfitETH {
return nil, fmt.Errorf("insufficient profit")
}
roi := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(amountIn),
)
roiFloat, _ := roi.Float64()
return &ArbitragePath{
Hops: []*PathHop{
{DEX: quote1.DEX, PoolAddress: quote1.PoolAddress, TokenIn: token0, TokenOut: token1, AmountIn: amountIn, AmountOut: quote1.ExpectedOut, Fee: quote1.Fee},
{DEX: quote2.DEX, PoolAddress: quote2.PoolAddress, TokenIn: token1, TokenOut: token2, AmountIn: quote1.ExpectedOut, AmountOut: quote2.ExpectedOut, Fee: quote2.Fee},
{DEX: quote3.DEX, PoolAddress: quote3.PoolAddress, TokenIn: token2, TokenOut: token3, AmountIn: quote2.ExpectedOut, AmountOut: quote3.ExpectedOut, Fee: quote3.Fee},
{DEX: quote4.DEX, PoolAddress: quote4.PoolAddress, TokenIn: token3, TokenOut: token0, AmountIn: quote3.ExpectedOut, AmountOut: quote4.ExpectedOut, Fee: quote4.Fee},
},
TotalProfit: profit,
ProfitETH: profitFloat,
ROI: roiFloat,
GasCost: gasCost,
NetProfit: netProfit,
Confidence: 0.4, // Lower confidence for 4-hop
}, nil
}
// calculateConfidence calculates confidence score based on liquidity and price impact
func (a *CrossDEXAnalyzer) calculateConfidence(quotes ...*PriceQuote) float64 {
if len(quotes) == 0 {
return 0
}
totalImpact := 0.0
for _, quote := range quotes {
totalImpact += quote.PriceImpact
}
avgImpact := totalImpact / float64(len(quotes))
// Confidence decreases with price impact
// High impact (>5%) = low confidence
// Low impact (<1%) = high confidence
if avgImpact > 0.05 {
return 0.3
} else if avgImpact > 0.03 {
return 0.5
} else if avgImpact > 0.01 {
return 0.7
}
return 0.9
}
// GetPriceComparison compares prices across all DEXes for a token pair
func (a *CrossDEXAnalyzer) GetPriceComparison(
ctx context.Context,
tokenIn, tokenOut common.Address,
amountIn *big.Int,
) (map[DEXProtocol]*PriceQuote, error) {
dexes := a.registry.GetAll()
quotes := make(map[DEXProtocol]*PriceQuote)
type result struct {
protocol DEXProtocol
quote *PriceQuote
err error
}
results := make(chan result, len(dexes))
// Query all DEXes in parallel
for _, dex := range dexes {
go func(d *DEXInfo) {
quote, err := d.Decoder.GetQuote(ctx, a.client, tokenIn, tokenOut, amountIn)
results <- result{protocol: d.Protocol, quote: quote, err: err}
}(dex)
}
// Collect results
for i := 0; i < len(dexes); i++ {
res := <-results
if res.err == nil && res.quote != nil {
quotes[res.protocol] = res.quote
}
}
if len(quotes) == 0 {
return nil, fmt.Errorf("no valid quotes found")
}
return quotes, nil
}