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>
This commit is contained in:
481
orig/pkg/exchanges/arbitrage_finder.go
Normal file
481
orig/pkg/exchanges/arbitrage_finder.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package exchanges
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// ArbitrageOpportunity represents a cross-exchange arbitrage opportunity
|
||||
type ArbitrageOpportunity struct {
|
||||
TokenIn common.Address
|
||||
TokenOut common.Address
|
||||
BuyExchange math.ExchangeType
|
||||
SellExchange math.ExchangeType
|
||||
BuyAmount *big.Int
|
||||
SellAmount *big.Int
|
||||
BuyPrice *big.Float
|
||||
SellPrice *big.Float
|
||||
Spread *big.Float // Price difference (SellPrice - BuyPrice)
|
||||
SpreadPercent *big.Float // Spread as percentage
|
||||
Profit *big.Int // Estimated profit in wei
|
||||
GasCost *big.Int // Estimated gas cost
|
||||
NetProfit *big.Int // Net profit after gas
|
||||
ROI float64 // Return on investment percentage
|
||||
Confidence float64 // Confidence score (0.0 to 1.0)
|
||||
MaxSlippage float64 // Maximum acceptable slippage
|
||||
ExecutionTime time.Duration // Estimated execution time
|
||||
Path []string // Execution path description
|
||||
Timestamp time.Time // When the opportunity was discovered
|
||||
}
|
||||
|
||||
// CrossExchangeArbitrageFinder finds arbitrage opportunities between exchanges
|
||||
type CrossExchangeArbitrageFinder struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
registry *ExchangeRegistry
|
||||
engine *math.ExchangePricingEngine
|
||||
minSpread *big.Float // Minimum required spread percentage
|
||||
minProfit *big.Int // Minimum required profit in wei
|
||||
maxSlippage float64 // Maximum acceptable slippage
|
||||
}
|
||||
|
||||
// NewCrossExchangeArbitrageFinder creates a new cross-exchange arbitrage finder
|
||||
func NewCrossExchangeArbitrageFinder(
|
||||
client *ethclient.Client,
|
||||
logger *logger.Logger,
|
||||
registry *ExchangeRegistry,
|
||||
engine *math.ExchangePricingEngine,
|
||||
) *CrossExchangeArbitrageFinder {
|
||||
return &CrossExchangeArbitrageFinder{
|
||||
client: client,
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
engine: engine,
|
||||
minSpread: big.NewFloat(0.003), // 0.3% minimum spread after fees
|
||||
minProfit: big.NewInt(10000000000000000), // 0.01 ETH minimum profit
|
||||
maxSlippage: 0.01, // 1% maximum slippage
|
||||
}
|
||||
}
|
||||
|
||||
// FindArbitrageOpportunities finds cross-exchange arbitrage opportunities for a token pair
|
||||
func (a *CrossExchangeArbitrageFinder) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*ArbitrageOpportunity, error) {
|
||||
// Get all exchanges that support this token pair
|
||||
exchanges := a.registry.GetExchangesForPair(tokenIn, tokenOut)
|
||||
|
||||
if len(exchanges) < 2 {
|
||||
return nil, nil // Need at least 2 exchanges to find arbitrage
|
||||
}
|
||||
|
||||
var opportunities []*ArbitrageOpportunity
|
||||
|
||||
// Compare all exchange pairs for arbitrage opportunities
|
||||
for i, buyExchangeConfig := range exchanges {
|
||||
for j, sellExchangeConfig := range exchanges {
|
||||
if i == j {
|
||||
continue // Skip same exchange
|
||||
}
|
||||
|
||||
// Check if we can buy on buyExchange and sell on sellExchange
|
||||
opportunity, err := a.findDirectArbitrage(ctx, tokenIn, tokenOut, buyExchangeConfig.Type, sellExchangeConfig.Type)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if opportunity != nil {
|
||||
// Validate the opportunity
|
||||
if a.isValidOpportunity(opportunity) {
|
||||
opportunities = append(opportunities, opportunity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort opportunities by net profit descending
|
||||
sort.Slice(opportunities, func(i, j int) bool {
|
||||
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0
|
||||
})
|
||||
|
||||
return opportunities, nil
|
||||
}
|
||||
|
||||
// findDirectArbitrage finds a direct arbitrage opportunity between two exchanges
|
||||
func (a *CrossExchangeArbitrageFinder) findDirectArbitrage(
|
||||
ctx context.Context,
|
||||
tokenIn, tokenOut common.Address,
|
||||
buyExchangeType, sellExchangeType math.ExchangeType,
|
||||
) (*ArbitrageOpportunity, error) {
|
||||
|
||||
// Get swap routers for both exchanges
|
||||
buyRouter := a.registry.GetSwapRouter(buyExchangeType)
|
||||
sellRouter := a.registry.GetSwapRouter(sellExchangeType)
|
||||
|
||||
if buyRouter == nil || sellRouter == nil {
|
||||
return nil, fmt.Errorf("missing swap router for one of the exchanges")
|
||||
}
|
||||
|
||||
// Use a standard amount for comparison (1 ETH equivalent)
|
||||
standardAmount := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
// Calculate how much we'd get if we buy on buyExchange
|
||||
amountAfterBuy, err := buyRouter.CalculateSwap(tokenIn, tokenOut, standardAmount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calculating buy swap: %w", err)
|
||||
}
|
||||
|
||||
// Calculate how much we'd get if we sell the result on sellExchange
|
||||
amountAfterSell, err := sellRouter.CalculateSwap(tokenOut, tokenIn, amountAfterBuy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calculating sell swap: %w", err)
|
||||
}
|
||||
|
||||
// Calculate the spread and profit
|
||||
spread := new(big.Float).Sub(
|
||||
new(big.Float).SetInt(amountAfterSell),
|
||||
new(big.Float).SetInt(standardAmount),
|
||||
)
|
||||
|
||||
// Calculate spread percentage
|
||||
spreadPercent := new(big.Float).Quo(
|
||||
spread,
|
||||
new(big.Float).SetInt(standardAmount),
|
||||
)
|
||||
|
||||
// If the spread is positive, we have an opportunity
|
||||
if spread.Sign() > 0 {
|
||||
// Calculate profit in terms of the original token
|
||||
profit := new(big.Int).Sub(amountAfterSell, standardAmount)
|
||||
|
||||
// Calculate estimated gas cost (this would be more sophisticated in production)
|
||||
gasCost := big.NewInt(500000000000000) // 0.0005 ETH in wei as estimate
|
||||
|
||||
// Calculate net profit
|
||||
netProfit := new(big.Int).Sub(profit, gasCost)
|
||||
|
||||
// Calculate ROI
|
||||
roi := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(netProfit),
|
||||
new(big.Float).SetInt(standardAmount),
|
||||
)
|
||||
roiFloat, _ := roi.Float64()
|
||||
|
||||
// Get exchange config names for the path
|
||||
buyConfig := a.registry.GetExchangeByType(buyExchangeType)
|
||||
sellConfig := a.registry.GetExchangeByType(sellExchangeType)
|
||||
buyExchangeName := "Unknown"
|
||||
sellExchangeName := "Unknown"
|
||||
if buyConfig != nil {
|
||||
buyExchangeName = buyConfig.Name
|
||||
}
|
||||
if sellConfig != nil {
|
||||
sellExchangeName = sellConfig.Name
|
||||
}
|
||||
|
||||
// Create opportunity
|
||||
opportunity := &ArbitrageOpportunity{
|
||||
TokenIn: tokenIn,
|
||||
TokenOut: tokenOut,
|
||||
BuyExchange: buyExchangeType,
|
||||
SellExchange: sellExchangeType,
|
||||
BuyAmount: standardAmount,
|
||||
SellAmount: amountAfterSell,
|
||||
BuyPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterBuy), new(big.Float).SetInt(standardAmount)),
|
||||
SellPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterSell), new(big.Float).SetInt(amountAfterBuy)),
|
||||
Spread: spread,
|
||||
SpreadPercent: spreadPercent,
|
||||
Profit: profit,
|
||||
GasCost: gasCost,
|
||||
NetProfit: netProfit,
|
||||
ROI: roiFloat * 100, // Convert to percentage
|
||||
Confidence: 0.9, // High confidence for basic calculation
|
||||
MaxSlippage: a.maxSlippage,
|
||||
Path: []string{buyExchangeName, sellExchangeName},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
return opportunity, nil
|
||||
}
|
||||
|
||||
return nil, nil // No arbitrage opportunity
|
||||
}
|
||||
|
||||
// isValidOpportunity checks if an arbitrage opportunity meets our criteria
|
||||
func (a *CrossExchangeArbitrageFinder) isValidOpportunity(opportunity *ArbitrageOpportunity) bool {
|
||||
// Check if spread is above minimum
|
||||
spreadPercentFloat, _ := opportunity.SpreadPercent.Float64()
|
||||
minSpreadFloat, _ := a.minSpread.Float64()
|
||||
if spreadPercentFloat < minSpreadFloat {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if net profit is above minimum
|
||||
if opportunity.NetProfit.Cmp(a.minProfit) < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if ROI is reasonable
|
||||
if opportunity.ROI < 0.1 { // 0.1% minimum ROI
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional validations could go here
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// FindTriangleArbitrage finds triangular arbitrage opportunities across exchanges
|
||||
func (a *CrossExchangeArbitrageFinder) FindTriangleArbitrage(ctx context.Context, tokens []common.Address) ([]*ArbitrageOpportunity, error) {
|
||||
if len(tokens) < 3 {
|
||||
return nil, fmt.Errorf("need at least 3 tokens for triangular arbitrage")
|
||||
}
|
||||
|
||||
var opportunities []*ArbitrageOpportunity
|
||||
|
||||
// For each combination of 3 tokens
|
||||
for i := 0; i < len(tokens)-2; i++ {
|
||||
for j := i + 1; j < len(tokens)-1; j++ {
|
||||
for k := j + 1; k < len(tokens); k++ {
|
||||
// Try A -> B -> C -> A cycle
|
||||
cycleOpportunities, err := a.findTriangleCycle(ctx, tokens[i], tokens[j], tokens[k])
|
||||
if err == nil && cycleOpportunities != nil {
|
||||
opportunities = append(opportunities, cycleOpportunities...)
|
||||
}
|
||||
|
||||
// Try A -> C -> B -> A cycle (reverse)
|
||||
cycleOpportunities, err = a.findTriangleCycle(ctx, tokens[i], tokens[k], tokens[j])
|
||||
if err == nil && cycleOpportunities != nil {
|
||||
opportunities = append(opportunities, cycleOpportunities...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by net profit descending
|
||||
sort.Slice(opportunities, func(i, j int) bool {
|
||||
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0
|
||||
})
|
||||
|
||||
return opportunities, nil
|
||||
}
|
||||
|
||||
// findTriangleCycle finds opportunities for a specific 3-token cycle
|
||||
func (a *CrossExchangeArbitrageFinder) findTriangleCycle(
|
||||
ctx context.Context,
|
||||
tokenA, tokenB, tokenC common.Address,
|
||||
) ([]*ArbitrageOpportunity, error) {
|
||||
// Find if there are exchanges supporting A->B, B->C, C->A
|
||||
// This would be complex to implement completely, so here's a simplified approach
|
||||
|
||||
var opportunities []*ArbitrageOpportunity
|
||||
|
||||
// Get exchanges for each pair
|
||||
exchangesAB := a.registry.GetExchangesForPair(tokenA, tokenB)
|
||||
exchangesBC := a.registry.GetExchangesForPair(tokenB, tokenC)
|
||||
exchangesCA := a.registry.GetExchangesForPair(tokenC, tokenA)
|
||||
|
||||
if len(exchangesAB) == 0 || len(exchangesBC) == 0 || len(exchangesCA) == 0 {
|
||||
return nil, nil // Can't form a cycle without all pairs
|
||||
}
|
||||
|
||||
// Use first available exchange for each leg of the cycle
|
||||
// In a real implementation, we'd try all combinations
|
||||
if len(exchangesAB) > 0 && len(exchangesBC) > 0 && len(exchangesCA) > 0 {
|
||||
// For this simplified version, we'll just use the first available exchanges
|
||||
// More complex implementations would iterate through all combinations
|
||||
|
||||
// Start with 1 of tokenA
|
||||
startAmount := big.NewInt(1000000000000000000) // 1 tokenA equivalent
|
||||
|
||||
// Get the three exchanges we'll use
|
||||
exchangeAB := exchangesAB[0].Type
|
||||
exchangeBC := exchangesBC[0].Type
|
||||
exchangeCA := exchangesCA[0].Type
|
||||
|
||||
// Get swap routers
|
||||
routerAB := a.registry.GetSwapRouter(exchangeAB)
|
||||
routerBC := a.registry.GetSwapRouter(exchangeBC)
|
||||
routerCA := a.registry.GetSwapRouter(exchangeCA)
|
||||
|
||||
if routerAB == nil || routerBC == nil || routerCA == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Execute the cycle: A -> B -> C -> A
|
||||
amountB, err := routerAB.CalculateSwap(tokenA, tokenB, startAmount)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
amountC, err := routerBC.CalculateSwap(tokenB, tokenC, amountB)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
finalAmount, err := routerCA.CalculateSwap(tokenC, tokenA, amountC)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Calculate profit
|
||||
profit := new(big.Int).Sub(finalAmount, startAmount)
|
||||
|
||||
// If we made a profit, we have an opportunity
|
||||
if profit.Sign() > 0 {
|
||||
// Calculate spread percentage
|
||||
spreadPercent := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(profit),
|
||||
new(big.Float).SetInt(startAmount),
|
||||
)
|
||||
|
||||
// Calculate ROI
|
||||
roi := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(profit),
|
||||
new(big.Float).SetInt(startAmount),
|
||||
)
|
||||
roiFloat, _ := roi.Float64()
|
||||
|
||||
// Calculate estimated gas cost for the complex multi-swap
|
||||
gasCost := big.NewInt(1500000000000000) // 0.0015 ETH in wei for 3 swaps
|
||||
|
||||
// Calculate net profit
|
||||
netProfit := new(big.Int).Sub(profit, gasCost)
|
||||
|
||||
// Create opportunity
|
||||
opportunity := &ArbitrageOpportunity{
|
||||
TokenIn: tokenA,
|
||||
TokenOut: tokenA, // We start and end with the same token
|
||||
BuyExchange: exchangeAB,
|
||||
SellExchange: exchangeCA, // This is somewhat of a convention for this type of arbitrage
|
||||
BuyAmount: startAmount,
|
||||
SellAmount: finalAmount,
|
||||
Spread: new(big.Float).SetInt(profit),
|
||||
SpreadPercent: spreadPercent,
|
||||
Profit: profit,
|
||||
GasCost: gasCost,
|
||||
NetProfit: netProfit,
|
||||
ROI: roiFloat * 100,
|
||||
Confidence: 0.7, // Lower confidence for complex triangle arbitrage
|
||||
MaxSlippage: a.maxSlippage,
|
||||
Path: []string{exchangesAB[0].Name, exchangesBC[0].Name, exchangesCA[0].Name},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
opportunities = append(opportunities, opportunity)
|
||||
}
|
||||
}
|
||||
|
||||
return opportunities, nil
|
||||
}
|
||||
|
||||
// ValidateOpportunity checks if an arbitrage opportunity is still valid
|
||||
func (a *CrossExchangeArbitrageFinder) ValidateOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) (bool, error) {
|
||||
// Check if the opportunity is still fresh (not too old)
|
||||
if time.Since(opportunity.Timestamp) > 100*time.Millisecond { // 100ms expiration
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Recalculate the opportunity to see if it's still profitable
|
||||
recalculated, err := a.findDirectArbitrage(ctx, opportunity.TokenIn, opportunity.TokenOut, opportunity.BuyExchange, opportunity.SellExchange)
|
||||
if err != nil || recalculated == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if the recalculated profit is still above our minimum
|
||||
if recalculated.NetProfit.Cmp(a.minProfit) < 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// EstimateExecutionGas estimates the gas cost for executing an arbitrage opportunity
|
||||
func (a *CrossExchangeArbitrageFinder) EstimateExecutionGas(opportunity *ArbitrageOpportunity) (*big.Int, error) {
|
||||
// In a real implementation, this would estimate gas based on complexity
|
||||
// For this implementation, we'll use a basic estimation based on the type of arbitrage
|
||||
|
||||
baseGas := big.NewInt(100000) // Base gas for a single swap
|
||||
|
||||
// For each exchange in the path, add more gas
|
||||
gasPerSwap := big.NewInt(100000)
|
||||
totalGas := new(big.Int).Add(baseGas, new(big.Int).Mul(gasPerSwap, big.NewInt(int64(len(opportunity.Path)))))
|
||||
|
||||
return totalGas, nil
|
||||
}
|
||||
|
||||
// ConvertToCanonicalOpportunity converts our internal opportunity to the canonical type
|
||||
func (a *CrossExchangeArbitrageFinder) ConvertToCanonicalOpportunity(opportunity *ArbitrageOpportunity) *types.ArbitrageOpportunity {
|
||||
// Convert our internal opportunity struct to the canonical ArbitrageOpportunity type
|
||||
roiFloat, _ := opportunity.ROI, opportunity.ROI
|
||||
gasEstimate := big.NewInt(200000) // Default gas estimate
|
||||
|
||||
return &types.ArbitrageOpportunity{
|
||||
Path: opportunity.Path,
|
||||
Pools: []string{}, // Would be populated with actual pool addresses
|
||||
AmountIn: opportunity.BuyAmount,
|
||||
Profit: opportunity.Profit,
|
||||
NetProfit: opportunity.NetProfit,
|
||||
GasEstimate: gasEstimate,
|
||||
ROI: roiFloat,
|
||||
Protocol: "cross-exchange",
|
||||
ExecutionTime: int64(opportunity.ExecutionTime.Milliseconds()),
|
||||
Confidence: opportunity.Confidence,
|
||||
PriceImpact: 0.005, // 0.5% estimated price impact
|
||||
MaxSlippage: opportunity.MaxSlippage,
|
||||
TokenIn: opportunity.TokenIn,
|
||||
TokenOut: opportunity.TokenOut,
|
||||
Timestamp: opportunity.Timestamp.Unix(),
|
||||
Risk: 0.1, // Low risk for simple arbitrage
|
||||
}
|
||||
}
|
||||
|
||||
// FindHighPriorityArbitrage looks for opportunities with high probability and profit
|
||||
func (a *CrossExchangeArbitrageFinder) FindHighPriorityArbitrage(ctx context.Context) ([]*ArbitrageOpportunity, error) {
|
||||
// Get high priority tokens from the registry
|
||||
highPriorityTokens := a.registry.GetHighPriorityTokens(10) // Top 10 tokens
|
||||
|
||||
var allOpportunities []*ArbitrageOpportunity
|
||||
|
||||
// Create token address array from high priority tokens
|
||||
var tokenAddresses []common.Address
|
||||
for _, token := range highPriorityTokens {
|
||||
tokenAddresses = append(tokenAddresses, common.HexToAddress(token.Address))
|
||||
}
|
||||
|
||||
// Find arbitrage opportunities between high priority tokens
|
||||
for i, tokenA := range tokenAddresses {
|
||||
for j, tokenB := range tokenAddresses {
|
||||
if i < j { // Only check each pair once
|
||||
opportunities, err := a.FindArbitrageOpportunities(ctx, tokenA, tokenB)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allOpportunities = append(allOpportunities, opportunities...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by profitability and confidence
|
||||
sort.Slice(allOpportunities, func(i, j int) bool {
|
||||
// Sort by net profit first, then by confidence
|
||||
if allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) == 0 {
|
||||
return allOpportunities[i].Confidence > allOpportunities[j].Confidence
|
||||
}
|
||||
return allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) > 0
|
||||
})
|
||||
|
||||
// Return only the top opportunities (top 20)
|
||||
if len(allOpportunities) > 20 {
|
||||
allOpportunities = allOpportunities[:20]
|
||||
}
|
||||
|
||||
return allOpportunities, nil
|
||||
}
|
||||
Reference in New Issue
Block a user