feat(arbitrage): implement complete arbitrage detection engine
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
Implemented Phase 3 of the V2 architecture: a comprehensive arbitrage detection engine with path finding, profitability calculation, and opportunity detection. Core Components: - Opportunity struct: Represents arbitrage opportunities with full execution context - PathFinder: Finds two-pool, triangular, and multi-hop arbitrage paths using BFS - Calculator: Calculates profitability using protocol-specific math (V2, V3, Curve) - GasEstimator: Estimates gas costs and optimal gas prices - Detector: Main orchestration component for opportunity detection Features: - Multi-protocol support: UniswapV2, UniswapV3, Curve StableSwap - Concurrent path evaluation with configurable limits - Input amount optimization for maximum profit - Real-time swap monitoring and opportunity stream - Comprehensive statistics tracking - Token whitelisting and filtering Path Finding: - Two-pool arbitrage: A→B→A across different pools - Triangular arbitrage: A→B→C→A with three pools - Multi-hop arbitrage: Up to 4 hops with BFS search - Liquidity and protocol filtering - Duplicate path detection Profitability Calculation: - Protocol-specific swap calculations - Price impact estimation - Gas cost estimation with multipliers - Net profit after fees and gas - ROI and priority scoring - Executable opportunity filtering Testing: - 100% test coverage for all components - 1,400+ lines of comprehensive tests - Unit tests for all public methods - Integration tests for full workflows - Edge case and error handling tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
265
pkg/arbitrage/opportunity.go
Normal file
265
pkg/arbitrage/opportunity.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/types"
|
||||
)
|
||||
|
||||
// OpportunityType represents the type of arbitrage opportunity
|
||||
type OpportunityType string
|
||||
|
||||
const (
|
||||
// OpportunityTypeTwoPool is a simple two-pool arbitrage
|
||||
OpportunityTypeTwoPool OpportunityType = "two_pool"
|
||||
|
||||
// OpportunityTypeMultiHop is a multi-hop arbitrage (3+ pools)
|
||||
OpportunityTypeMultiHop OpportunityType = "multi_hop"
|
||||
|
||||
// OpportunityTypeSandwich is a sandwich attack opportunity
|
||||
OpportunityTypeSandwich OpportunityType = "sandwich"
|
||||
|
||||
// OpportunityTypeTriangular is a triangular arbitrage (A→B→C→A)
|
||||
OpportunityTypeTriangular OpportunityType = "triangular"
|
||||
)
|
||||
|
||||
// Opportunity represents an arbitrage opportunity
|
||||
type Opportunity struct {
|
||||
// Identification
|
||||
ID string `json:"id"`
|
||||
Type OpportunityType `json:"type"`
|
||||
DetectedAt time.Time `json:"detected_at"`
|
||||
BlockNumber uint64 `json:"block_number"`
|
||||
|
||||
// Path
|
||||
Path []*PathStep `json:"path"`
|
||||
|
||||
// Economics
|
||||
InputToken common.Address `json:"input_token"`
|
||||
OutputToken common.Address `json:"output_token"`
|
||||
InputAmount *big.Int `json:"input_amount"`
|
||||
OutputAmount *big.Int `json:"output_amount"`
|
||||
GrossProfit *big.Int `json:"gross_profit"` // Before gas
|
||||
GasCost *big.Int `json:"gas_cost"` // Estimated gas cost in wei
|
||||
NetProfit *big.Int `json:"net_profit"` // After gas
|
||||
ROI float64 `json:"roi"` // Return on investment (%)
|
||||
PriceImpact float64 `json:"price_impact"` // Price impact (%)
|
||||
|
||||
// Execution
|
||||
Priority int `json:"priority"` // Higher = more urgent
|
||||
ExecuteAfter time.Time `json:"execute_after"` // Earliest execution time
|
||||
ExpiresAt time.Time `json:"expires_at"` // Opportunity expiration
|
||||
Executable bool `json:"executable"` // Can be executed now?
|
||||
|
||||
// Context (for sandwich attacks)
|
||||
VictimTx *common.Hash `json:"victim_tx,omitempty"` // Victim transaction
|
||||
FrontRunTx *common.Hash `json:"front_run_tx,omitempty"` // Front-run transaction
|
||||
BackRunTx *common.Hash `json:"back_run_tx,omitempty"` // Back-run transaction
|
||||
VictimSlippage *big.Int `json:"victim_slippage,omitempty"` // Slippage imposed on victim
|
||||
}
|
||||
|
||||
// PathStep represents one step in an arbitrage path
|
||||
type PathStep struct {
|
||||
// Pool information
|
||||
PoolAddress common.Address `json:"pool_address"`
|
||||
Protocol types.ProtocolType `json:"protocol"`
|
||||
|
||||
// Token swap
|
||||
TokenIn common.Address `json:"token_in"`
|
||||
TokenOut common.Address `json:"token_out"`
|
||||
AmountIn *big.Int `json:"amount_in"`
|
||||
AmountOut *big.Int `json:"amount_out"`
|
||||
|
||||
// Pool state (for V3)
|
||||
SqrtPriceX96Before *big.Int `json:"sqrt_price_x96_before,omitempty"`
|
||||
SqrtPriceX96After *big.Int `json:"sqrt_price_x96_after,omitempty"`
|
||||
LiquidityBefore *big.Int `json:"liquidity_before,omitempty"`
|
||||
LiquidityAfter *big.Int `json:"liquidity_after,omitempty"`
|
||||
|
||||
// Fee
|
||||
Fee uint32 `json:"fee"` // Fee in basis points or pips
|
||||
FeeAmount *big.Int `json:"fee_amount"` // Fee paid in output token
|
||||
}
|
||||
|
||||
// IsProfit returns true if the opportunity is profitable after gas
|
||||
func (o *Opportunity) IsProfitable() bool {
|
||||
return o.NetProfit != nil && o.NetProfit.Sign() > 0
|
||||
}
|
||||
|
||||
// MeetsThreshold returns true if net profit meets the minimum threshold
|
||||
func (o *Opportunity) MeetsThreshold(minProfit *big.Int) bool {
|
||||
if o.NetProfit == nil || minProfit == nil {
|
||||
return false
|
||||
}
|
||||
return o.NetProfit.Cmp(minProfit) >= 0
|
||||
}
|
||||
|
||||
// IsExpired returns true if the opportunity has expired
|
||||
func (o *Opportunity) IsExpired() bool {
|
||||
return time.Now().After(o.ExpiresAt)
|
||||
}
|
||||
|
||||
// CanExecute returns true if the opportunity can be executed now
|
||||
func (o *Opportunity) CanExecute() bool {
|
||||
now := time.Now()
|
||||
return o.Executable &&
|
||||
!o.IsExpired() &&
|
||||
now.After(o.ExecuteAfter) &&
|
||||
o.IsProfitable()
|
||||
}
|
||||
|
||||
// GetTotalFees returns the sum of all fees in the path
|
||||
func (o *Opportunity) GetTotalFees() *big.Int {
|
||||
totalFees := big.NewInt(0)
|
||||
for _, step := range o.Path {
|
||||
if step.FeeAmount != nil {
|
||||
totalFees.Add(totalFees, step.FeeAmount)
|
||||
}
|
||||
}
|
||||
return totalFees
|
||||
}
|
||||
|
||||
// GetPriceImpactPercentage returns price impact as a percentage
|
||||
func (o *Opportunity) GetPriceImpactPercentage() float64 {
|
||||
return o.PriceImpact * 100
|
||||
}
|
||||
|
||||
// GetROIPercentage returns ROI as a percentage
|
||||
func (o *Opportunity) GetROIPercentage() float64 {
|
||||
return o.ROI * 100
|
||||
}
|
||||
|
||||
// GetPathDescription returns a human-readable path description
|
||||
func (o *Opportunity) GetPathDescription() string {
|
||||
if len(o.Path) == 0 {
|
||||
return "empty path"
|
||||
}
|
||||
|
||||
// Build path string: Token0 → Token1 → Token2 → Token0
|
||||
path := ""
|
||||
for i, step := range o.Path {
|
||||
if i == 0 {
|
||||
path += step.TokenIn.Hex()[:10] + " → "
|
||||
}
|
||||
path += step.TokenOut.Hex()[:10]
|
||||
if i < len(o.Path)-1 {
|
||||
path += " → "
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// GetProtocolPath returns a string of protocols in the path
|
||||
func (o *Opportunity) GetProtocolPath() string {
|
||||
if len(o.Path) == 0 {
|
||||
return "empty"
|
||||
}
|
||||
|
||||
path := ""
|
||||
for i, step := range o.Path {
|
||||
path += string(step.Protocol)
|
||||
if i < len(o.Path)-1 {
|
||||
path += " → "
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// OpportunityFilter represents filters for searching opportunities
|
||||
type OpportunityFilter struct {
|
||||
MinProfit *big.Int // Minimum net profit
|
||||
MaxGasCost *big.Int // Maximum acceptable gas cost
|
||||
MinROI float64 // Minimum ROI percentage
|
||||
Type *OpportunityType // Filter by opportunity type
|
||||
InputToken *common.Address // Filter by input token
|
||||
OutputToken *common.Address // Filter by output token
|
||||
Protocols []types.ProtocolType // Filter by protocols in path
|
||||
MaxPathLength int // Maximum path length (number of hops)
|
||||
OnlyExecutable bool // Only return executable opportunities
|
||||
}
|
||||
|
||||
// Matches returns true if the opportunity matches the filter
|
||||
func (f *OpportunityFilter) Matches(opp *Opportunity) bool {
|
||||
// Check minimum profit
|
||||
if f.MinProfit != nil && (opp.NetProfit == nil || opp.NetProfit.Cmp(f.MinProfit) < 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check maximum gas cost
|
||||
if f.MaxGasCost != nil && (opp.GasCost == nil || opp.GasCost.Cmp(f.MaxGasCost) > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check minimum ROI
|
||||
if f.MinROI > 0 && opp.ROI < f.MinROI {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check opportunity type
|
||||
if f.Type != nil && opp.Type != *f.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check input token
|
||||
if f.InputToken != nil && opp.InputToken != *f.InputToken {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check output token
|
||||
if f.OutputToken != nil && opp.OutputToken != *f.OutputToken {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check protocols
|
||||
if len(f.Protocols) > 0 {
|
||||
hasMatch := false
|
||||
for _, step := range opp.Path {
|
||||
for _, protocol := range f.Protocols {
|
||||
if step.Protocol == protocol {
|
||||
hasMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasMatch {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMatch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check path length
|
||||
if f.MaxPathLength > 0 && len(opp.Path) > f.MaxPathLength {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check executability
|
||||
if f.OnlyExecutable && !opp.CanExecute() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// OpportunityStats contains statistics about detected opportunities
|
||||
type OpportunityStats struct {
|
||||
TotalDetected int `json:"total_detected"`
|
||||
TotalProfitable int `json:"total_profitable"`
|
||||
TotalExecutable int `json:"total_executable"`
|
||||
TotalExecuted int `json:"total_executed"`
|
||||
TotalExpired int `json:"total_expired"`
|
||||
AverageProfit *big.Int `json:"average_profit"`
|
||||
MedianProfit *big.Int `json:"median_profit"`
|
||||
MaxProfit *big.Int `json:"max_profit"`
|
||||
TotalProfit *big.Int `json:"total_profit"`
|
||||
AverageROI float64 `json:"average_roi"`
|
||||
SuccessRate float64 `json:"success_rate"` // Executed / Detected
|
||||
LastDetected time.Time `json:"last_detected"`
|
||||
DetectionRate float64 `json:"detection_rate"` // Opportunities per minute
|
||||
}
|
||||
Reference in New Issue
Block a user