refactor: move all remaining files to orig/ directory
Completed clean root directory structure: - Root now contains only: .git, .env, docs/, orig/ - Moved all remaining files and directories to orig/: - Config files (.claude, .dockerignore, .drone.yml, etc.) - All .env variants (except active .env) - Git config (.gitconfig, .github, .gitignore, etc.) - Tool configs (.golangci.yml, .revive.toml, etc.) - Documentation (*.md files, @prompts) - Build files (Dockerfiles, Makefile, go.mod, go.sum) - Docker compose files - All source directories (scripts, tests, tools, etc.) - Runtime directories (logs, monitoring, reports) - Dependency files (node_modules, lib, cache) - Special files (--delete) - Removed empty runtime directories (bin/, data/) V2 structure is now clean: - docs/planning/ - V2 planning documents - orig/ - Complete V1 codebase preserved - .env - Active environment config (not in git) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
31
orig/tools/opportunity-validator/go.mod
Normal file
31
orig/tools/opportunity-validator/go.mod
Normal file
@@ -0,0 +1,31 @@
|
||||
module github.com/fraktal/mev-beta/tools/opportunity-validator
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/fraktal/mev-beta => ../../
|
||||
|
||||
require github.com/fraktal/mev-beta v0.0.0-00010101000000-000000000000
|
||||
|
||||
require (
|
||||
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.0 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
|
||||
github.com/ethereum/go-ethereum v1.16.3 // indirect
|
||||
github.com/ethereum/go-verkle v0.2.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
46
orig/tools/opportunity-validator/go.sum
Normal file
46
orig/tools/opportunity-validator/go.sum
Normal file
@@ -0,0 +1,46 @@
|
||||
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
|
||||
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA=
|
||||
github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
|
||||
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/ethereum/go-ethereum v1.16.3 h1:nDoBSrmsrPbrDIVLTkDQCy1U9KdHN+F2PzvMbDoS42Q=
|
||||
github.com/ethereum/go-ethereum v1.16.3/go.mod h1:Lrsc6bt9Gm9RyvhfFK53vboCia8kpF9nv+2Ukntnl+8=
|
||||
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
|
||||
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,602 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/arbitrage"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
)
|
||||
|
||||
type ValidatorConfig struct {
|
||||
Exchanges string
|
||||
MinProfitBP float64
|
||||
MaxSlippage float64
|
||||
OutputDir string
|
||||
Verbose bool
|
||||
DryRun bool
|
||||
TestMode bool
|
||||
}
|
||||
|
||||
type OpportunityValidator struct {
|
||||
config *ValidatorConfig
|
||||
supportedExchanges []string
|
||||
detectionEngine *arbitrage.DetectionEngine
|
||||
calculator *math.ArbitrageCalculator
|
||||
results *ValidationResults
|
||||
}
|
||||
|
||||
type ValidationResults struct {
|
||||
TotalOpportunities int `json:"total_opportunities"`
|
||||
ValidOpportunities int `json:"valid_opportunities"`
|
||||
InvalidOpportunities int `json:"invalid_opportunities"`
|
||||
AverageProfitBP float64 `json:"average_profit_bp"`
|
||||
MaxProfitBP float64 `json:"max_profit_bp"`
|
||||
OpportunityDetails []OpportunityResult `json:"opportunity_details"`
|
||||
ExchangeBreakdown map[string]int `json:"exchange_breakdown"`
|
||||
ValidationErrors []ValidationError `json:"validation_errors"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
type OpportunityResult struct {
|
||||
ID string `json:"id"`
|
||||
Exchange1 string `json:"exchange1"`
|
||||
Exchange2 string `json:"exchange2"`
|
||||
TokenA string `json:"token_a"`
|
||||
TokenB string `json:"token_b"`
|
||||
ProfitBP float64 `json:"profit_bp"`
|
||||
SlippageBP float64 `json:"slippage_bp"`
|
||||
GasCostETH float64 `json:"gas_cost_eth"`
|
||||
NetProfitBP float64 `json:"net_profit_bp"`
|
||||
ExecutionTime time.Time `json:"execution_time"`
|
||||
Valid bool `json:"valid"`
|
||||
ValidationNotes []string `json:"validation_notes"`
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
Exchange string `json:"exchange,omitempty"`
|
||||
TokenPair string `json:"token_pair,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
func NewOpportunityValidator(config *ValidatorConfig) (*OpportunityValidator, error) {
|
||||
// Parse supported exchanges
|
||||
exchanges := strings.Split(config.Exchanges, ",")
|
||||
for i, exchange := range exchanges {
|
||||
exchanges[i] = strings.TrimSpace(exchange)
|
||||
}
|
||||
|
||||
// Initialize detection engine
|
||||
detectionEngine, err := arbitrage.NewDetectionEngine(&arbitrage.DetectionEngineConfig{
|
||||
MinProfitBasisPoints: config.MinProfitBP,
|
||||
MaxSlippageBasisPoints: config.MaxSlippage,
|
||||
EnabledExchanges: exchanges,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize detection engine: %w", err)
|
||||
}
|
||||
|
||||
// Initialize arbitrage calculator
|
||||
calculator := math.NewArbitrageCalculator()
|
||||
|
||||
return &OpportunityValidator{
|
||||
config: config,
|
||||
supportedExchanges: exchanges,
|
||||
detectionEngine: detectionEngine,
|
||||
calculator: calculator,
|
||||
results: &ValidationResults{
|
||||
OpportunityDetails: make([]OpportunityResult, 0),
|
||||
ExchangeBreakdown: make(map[string]int),
|
||||
ValidationErrors: make([]ValidationError, 0),
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) ValidateOpportunities(ctx context.Context) error {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
ov.results.DurationMs = time.Since(startTime).Milliseconds()
|
||||
}()
|
||||
|
||||
if ov.config.TestMode {
|
||||
return ov.validateTestOpportunities(ctx)
|
||||
}
|
||||
|
||||
return ov.validateRealOpportunities(ctx)
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateTestOpportunities(ctx context.Context) error {
|
||||
// Create simulated test opportunities
|
||||
testOpportunities := ov.generateTestOpportunities()
|
||||
|
||||
for _, opportunity := range testOpportunities {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
result := ov.validateSingleOpportunity(opportunity)
|
||||
ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result)
|
||||
ov.updateStatistics(result)
|
||||
|
||||
if ov.config.Verbose {
|
||||
fmt.Printf("Validated opportunity %s: Valid=%t, Profit=%.2f bp\n",
|
||||
result.ID, result.Valid, result.ProfitBP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateRealOpportunities(ctx context.Context) error {
|
||||
// In real mode, we would connect to live data sources
|
||||
// For now, implement basic validation framework
|
||||
|
||||
if ov.config.Verbose {
|
||||
fmt.Println("Scanning for real-time arbitrage opportunities...")
|
||||
}
|
||||
|
||||
// This would integrate with the actual market scanner
|
||||
// For demonstration, create a few real-world scenarios
|
||||
realOpportunities := ov.generateRealWorldScenarios()
|
||||
|
||||
for _, opportunity := range realOpportunities {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
result := ov.validateSingleOpportunity(opportunity)
|
||||
ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result)
|
||||
ov.updateStatistics(result)
|
||||
|
||||
if ov.config.Verbose {
|
||||
fmt.Printf("Validated real opportunity %s: Valid=%t, Profit=%.2f bp\n",
|
||||
result.ID, result.Valid, result.ProfitBP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateSingleOpportunity(opportunity *TestOpportunity) OpportunityResult {
|
||||
result := OpportunityResult{
|
||||
ID: opportunity.ID,
|
||||
Exchange1: opportunity.Exchange1,
|
||||
Exchange2: opportunity.Exchange2,
|
||||
TokenA: opportunity.TokenA,
|
||||
TokenB: opportunity.TokenB,
|
||||
ExecutionTime: time.Now(),
|
||||
ValidationNotes: make([]string, 0),
|
||||
}
|
||||
|
||||
// Calculate profit using arbitrage calculator
|
||||
profitCalculation, err := ov.calculateProfit(opportunity)
|
||||
if err != nil {
|
||||
ov.addValidationError("profit_calculation", err.Error(), opportunity.Exchange1,
|
||||
fmt.Sprintf("%s/%s", opportunity.TokenA, opportunity.TokenB))
|
||||
result.Valid = false
|
||||
result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Profit calculation failed: %v", err))
|
||||
return result
|
||||
}
|
||||
|
||||
result.ProfitBP = profitCalculation.ProfitBP
|
||||
result.SlippageBP = profitCalculation.SlippageBP
|
||||
result.GasCostETH = profitCalculation.GasCostETH
|
||||
result.NetProfitBP = profitCalculation.NetProfitBP
|
||||
|
||||
// Validate profit threshold
|
||||
if result.ProfitBP < ov.config.MinProfitBP {
|
||||
result.Valid = false
|
||||
result.ValidationNotes = append(result.ValidationNotes,
|
||||
fmt.Sprintf("Profit %.2f bp below minimum threshold %.2f bp", result.ProfitBP, ov.config.MinProfitBP))
|
||||
}
|
||||
|
||||
// Validate slippage threshold
|
||||
if result.SlippageBP > ov.config.MaxSlippage {
|
||||
result.Valid = false
|
||||
result.ValidationNotes = append(result.ValidationNotes,
|
||||
fmt.Sprintf("Slippage %.2f bp exceeds maximum %.2f bp", result.SlippageBP, ov.config.MaxSlippage))
|
||||
}
|
||||
|
||||
// Validate net profit after gas costs
|
||||
if result.NetProfitBP <= 0 {
|
||||
result.Valid = false
|
||||
result.ValidationNotes = append(result.ValidationNotes,
|
||||
fmt.Sprintf("Net profit %.2f bp not profitable after gas costs", result.NetProfitBP))
|
||||
}
|
||||
|
||||
// Additional exchange-specific validations
|
||||
if err := ov.validateExchangeSpecific(opportunity); err != nil {
|
||||
result.Valid = false
|
||||
result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Exchange validation failed: %v", err))
|
||||
}
|
||||
|
||||
// If no validation errors, mark as valid
|
||||
if len(result.ValidationNotes) == 0 {
|
||||
result.Valid = true
|
||||
result.ValidationNotes = append(result.ValidationNotes, "All validations passed")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type TestOpportunity struct {
|
||||
ID string
|
||||
Exchange1 string
|
||||
Exchange2 string
|
||||
TokenA string
|
||||
TokenB string
|
||||
Price1 *big.Float
|
||||
Price2 *big.Float
|
||||
Liquidity *big.Float
|
||||
GasLimit uint64
|
||||
}
|
||||
|
||||
type ProfitCalculation struct {
|
||||
ProfitBP float64
|
||||
SlippageBP float64
|
||||
GasCostETH float64
|
||||
NetProfitBP float64
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) calculateProfit(opportunity *TestOpportunity) (*ProfitCalculation, error) {
|
||||
// Convert prices to UniversalDecimal for precise calculations
|
||||
price1UD, err := math.NewUniversalDecimal(opportunity.Price1, 18)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert price1: %w", err)
|
||||
}
|
||||
|
||||
price2UD, err := math.NewUniversalDecimal(opportunity.Price2, 18)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert price2: %w", err)
|
||||
}
|
||||
|
||||
// Calculate profit basis points
|
||||
priceDiff := price2UD.Sub(price1UD)
|
||||
profitRatio := priceDiff.Div(price1UD)
|
||||
profitBP := profitRatio.Mul(math.NewUniversalDecimalFromFloat(10000, 18)).Float64()
|
||||
|
||||
// Estimate slippage based on liquidity
|
||||
liquidityUD, err := math.NewUniversalDecimal(opportunity.Liquidity, 18)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Simple slippage model: inversely proportional to liquidity
|
||||
baseSlippage := 10.0 // 10 bp base slippage
|
||||
liquidityFactor := liquidityUD.Float64() / 1000000.0 // Normalize to millions
|
||||
slippageBP := baseSlippage / (1.0 + liquidityFactor)
|
||||
|
||||
// Estimate gas cost (simplified)
|
||||
gasPriceGwei := 0.5 // 0.5 gwei on Arbitrum
|
||||
gasCostETH := float64(opportunity.GasLimit) * gasPriceGwei * 1e-9
|
||||
|
||||
// Convert gas cost to basis points (assuming 1 ETH trade size)
|
||||
gasCostBP := gasCostETH * 10000.0
|
||||
|
||||
// Calculate net profit
|
||||
netProfitBP := profitBP - slippageBP - gasCostBP
|
||||
|
||||
return &ProfitCalculation{
|
||||
ProfitBP: profitBP,
|
||||
SlippageBP: slippageBP,
|
||||
GasCostETH: gasCostETH,
|
||||
NetProfitBP: netProfitBP,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateExchangeSpecific(opportunity *TestOpportunity) error {
|
||||
// Validate exchange-specific constraints
|
||||
switch opportunity.Exchange1 {
|
||||
case "uniswap_v2":
|
||||
return ov.validateUniswapV2(opportunity)
|
||||
case "uniswap_v3":
|
||||
return ov.validateUniswapV3(opportunity)
|
||||
case "curve":
|
||||
return ov.validateCurve(opportunity)
|
||||
case "balancer":
|
||||
return ov.validateBalancer(opportunity)
|
||||
default:
|
||||
return fmt.Errorf("unsupported exchange: %s", opportunity.Exchange1)
|
||||
}
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateUniswapV2(opportunity *TestOpportunity) error {
|
||||
// Uniswap V2 specific validations
|
||||
if opportunity.Price1.Cmp(big.NewFloat(0)) <= 0 {
|
||||
return fmt.Errorf("invalid price for Uniswap V2")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateUniswapV3(opportunity *TestOpportunity) error {
|
||||
// Uniswap V3 specific validations
|
||||
if opportunity.Liquidity.Cmp(big.NewFloat(0)) <= 0 {
|
||||
return fmt.Errorf("insufficient liquidity for Uniswap V3")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateCurve(opportunity *TestOpportunity) error {
|
||||
// Curve specific validations
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) validateBalancer(opportunity *TestOpportunity) error {
|
||||
// Balancer specific validations
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) generateTestOpportunities() []*TestOpportunity {
|
||||
opportunities := make([]*TestOpportunity, 0)
|
||||
|
||||
// ETH/USDC opportunities across different exchanges
|
||||
opportunities = append(opportunities, &TestOpportunity{
|
||||
ID: "test_eth_usdc_uniswap_v2_v3",
|
||||
Exchange1: "uniswap_v2",
|
||||
Exchange2: "uniswap_v3",
|
||||
TokenA: "ETH",
|
||||
TokenB: "USDC",
|
||||
Price1: big.NewFloat(2000.0),
|
||||
Price2: big.NewFloat(2002.5),
|
||||
Liquidity: big.NewFloat(5000000.0),
|
||||
GasLimit: 150000,
|
||||
})
|
||||
|
||||
opportunities = append(opportunities, &TestOpportunity{
|
||||
ID: "test_eth_usdc_curve_balancer",
|
||||
Exchange1: "curve",
|
||||
Exchange2: "balancer",
|
||||
TokenA: "ETH",
|
||||
TokenB: "USDC",
|
||||
Price1: big.NewFloat(2001.0),
|
||||
Price2: big.NewFloat(2004.0),
|
||||
Liquidity: big.NewFloat(10000000.0),
|
||||
GasLimit: 180000,
|
||||
})
|
||||
|
||||
// WBTC/ETH opportunities
|
||||
opportunities = append(opportunities, &TestOpportunity{
|
||||
ID: "test_wbtc_eth_uniswap",
|
||||
Exchange1: "uniswap_v2",
|
||||
Exchange2: "uniswap_v3",
|
||||
TokenA: "WBTC",
|
||||
TokenB: "ETH",
|
||||
Price1: big.NewFloat(15.5),
|
||||
Price2: big.NewFloat(15.52),
|
||||
Liquidity: big.NewFloat(2000000.0),
|
||||
GasLimit: 200000,
|
||||
})
|
||||
|
||||
// Low profit opportunity (should fail validation)
|
||||
opportunities = append(opportunities, &TestOpportunity{
|
||||
ID: "test_low_profit",
|
||||
Exchange1: "uniswap_v2",
|
||||
Exchange2: "curve",
|
||||
TokenA: "USDC",
|
||||
TokenB: "USDT",
|
||||
Price1: big.NewFloat(1.0),
|
||||
Price2: big.NewFloat(1.0005),
|
||||
Liquidity: big.NewFloat(1000000.0),
|
||||
GasLimit: 120000,
|
||||
})
|
||||
|
||||
// High slippage opportunity (should fail validation)
|
||||
opportunities = append(opportunities, &TestOpportunity{
|
||||
ID: "test_high_slippage",
|
||||
Exchange1: "uniswap_v3",
|
||||
Exchange2: "balancer",
|
||||
TokenA: "ETH",
|
||||
TokenB: "RARE_TOKEN",
|
||||
Price1: big.NewFloat(100.0),
|
||||
Price2: big.NewFloat(105.0),
|
||||
Liquidity: big.NewFloat(10000.0), // Low liquidity
|
||||
GasLimit: 250000,
|
||||
})
|
||||
|
||||
return opportunities
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) generateRealWorldScenarios() []*TestOpportunity {
|
||||
// Generate more realistic scenarios based on actual market conditions
|
||||
return []*TestOpportunity{
|
||||
{
|
||||
ID: "real_eth_usdc_arbitrum",
|
||||
Exchange1: "uniswap_v3",
|
||||
Exchange2: "curve",
|
||||
TokenA: "ETH",
|
||||
TokenB: "USDC",
|
||||
Price1: big.NewFloat(2456.78),
|
||||
Price2: big.NewFloat(2459.12),
|
||||
Liquidity: big.NewFloat(8500000.0),
|
||||
GasLimit: 165000,
|
||||
},
|
||||
{
|
||||
ID: "real_wbtc_eth_arbitrum",
|
||||
Exchange1: "balancer",
|
||||
Exchange2: "uniswap_v2",
|
||||
TokenA: "WBTC",
|
||||
TokenB: "ETH",
|
||||
Price1: big.NewFloat(16.234),
|
||||
Price2: big.NewFloat(16.251),
|
||||
Liquidity: big.NewFloat(3200000.0),
|
||||
GasLimit: 195000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) updateStatistics(result OpportunityResult) {
|
||||
ov.results.TotalOpportunities++
|
||||
|
||||
if result.Valid {
|
||||
ov.results.ValidOpportunities++
|
||||
} else {
|
||||
ov.results.InvalidOpportunities++
|
||||
}
|
||||
|
||||
// Update exchange breakdown
|
||||
ov.results.ExchangeBreakdown[result.Exchange1]++
|
||||
ov.results.ExchangeBreakdown[result.Exchange2]++
|
||||
|
||||
// Update profit statistics
|
||||
if result.Valid && result.ProfitBP > 0 {
|
||||
if ov.results.MaxProfitBP < result.ProfitBP {
|
||||
ov.results.MaxProfitBP = result.ProfitBP
|
||||
}
|
||||
|
||||
// Calculate running average
|
||||
totalProfit := ov.results.AverageProfitBP * float64(ov.results.ValidOpportunities-1)
|
||||
ov.results.AverageProfitBP = (totalProfit + result.ProfitBP) / float64(ov.results.ValidOpportunities)
|
||||
}
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) addValidationError(errorType, message, exchange, tokenPair string) {
|
||||
ov.results.ValidationErrors = append(ov.results.ValidationErrors, ValidationError{
|
||||
Type: errorType,
|
||||
Message: message,
|
||||
Exchange: exchange,
|
||||
TokenPair: tokenPair,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) MonitorOpportunities(ctx context.Context) error {
|
||||
if ov.config.Verbose {
|
||||
fmt.Println("Starting real-time opportunity monitoring...")
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
// Reset results for new monitoring cycle
|
||||
ov.results = &ValidationResults{
|
||||
OpportunityDetails: make([]OpportunityResult, 0),
|
||||
ExchangeBreakdown: make(map[string]int),
|
||||
ValidationErrors: make([]ValidationError, 0),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if err := ov.validateRealOpportunities(ctx); err != nil {
|
||||
log.Printf("Error during opportunity validation: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ov.GenerateReport(); err != nil {
|
||||
log.Printf("Error generating report: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ov.config.Verbose {
|
||||
fmt.Printf("Monitoring cycle complete. Found %d valid opportunities out of %d total\n",
|
||||
ov.results.ValidOpportunities, ov.results.TotalOpportunities)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) GenerateReport() error {
|
||||
// Sort opportunities by profit (descending)
|
||||
sort.Slice(ov.results.OpportunityDetails, func(i, j int) bool {
|
||||
return ov.results.OpportunityDetails[i].ProfitBP > ov.results.OpportunityDetails[j].ProfitBP
|
||||
})
|
||||
|
||||
// Generate JSON report
|
||||
jsonReport, err := json.MarshalIndent(ov.results, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal results: %w", err)
|
||||
}
|
||||
|
||||
// Save JSON report
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
jsonPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_validation_%s.json", timestamp))
|
||||
if err := os.WriteFile(jsonPath, jsonReport, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write JSON report: %w", err)
|
||||
}
|
||||
|
||||
// Generate summary report
|
||||
summaryPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_summary_%s.txt", timestamp))
|
||||
if err := ov.generateSummaryReport(summaryPath); err != nil {
|
||||
return fmt.Errorf("failed to generate summary report: %w", err)
|
||||
}
|
||||
|
||||
if ov.config.Verbose {
|
||||
fmt.Printf("Reports generated:\n")
|
||||
fmt.Printf(" JSON: %s\n", jsonPath)
|
||||
fmt.Printf(" Summary: %s\n", summaryPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ov *OpportunityValidator) generateSummaryReport(filePath string) error {
|
||||
summary := fmt.Sprintf(`Arbitrage Opportunity Validation Report
|
||||
Generated: %s
|
||||
Duration: %d ms
|
||||
|
||||
SUMMARY
|
||||
=======
|
||||
Total Opportunities Analyzed: %d
|
||||
Valid Opportunities: %d
|
||||
Invalid Opportunities: %d
|
||||
Success Rate: %.1f%%
|
||||
|
||||
PROFIT STATISTICS
|
||||
================
|
||||
Average Profit: %.2f bp
|
||||
Maximum Profit: %.2f bp
|
||||
|
||||
EXCHANGE BREAKDOWN
|
||||
==================
|
||||
`, ov.results.Timestamp.Format("2006-01-02 15:04:05"),
|
||||
ov.results.DurationMs,
|
||||
ov.results.TotalOpportunities,
|
||||
ov.results.ValidOpportunities,
|
||||
ov.results.InvalidOpportunities,
|
||||
float64(ov.results.ValidOpportunities)/float64(ov.results.TotalOpportunities)*100,
|
||||
ov.results.AverageProfitBP,
|
||||
ov.results.MaxProfitBP)
|
||||
|
||||
for exchange, count := range ov.results.ExchangeBreakdown {
|
||||
summary += fmt.Sprintf("%s: %d opportunities\n", exchange, count)
|
||||
}
|
||||
|
||||
summary += "\nTOP OPPORTUNITIES\n=================\n"
|
||||
for i, opp := range ov.results.OpportunityDetails {
|
||||
if i >= 5 { // Show top 5
|
||||
break
|
||||
}
|
||||
status := "INVALID"
|
||||
if opp.Valid {
|
||||
status = "VALID"
|
||||
}
|
||||
summary += fmt.Sprintf("%d. %s (%s/%s) - %.2f bp - %s\n",
|
||||
i+1, opp.ID, opp.Exchange1, opp.Exchange2, opp.ProfitBP, status)
|
||||
}
|
||||
|
||||
if len(ov.results.ValidationErrors) > 0 {
|
||||
summary += "\nVALIDATION ERRORS\n================\n"
|
||||
for _, err := range ov.results.ValidationErrors {
|
||||
summary += fmt.Sprintf("- %s: %s\n", err.Type, err.Message)
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, []byte(summary), 0644)
|
||||
}
|
||||
69
orig/tools/opportunity-validator/main.go
Normal file
69
orig/tools/opportunity-validator/main.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/tools/opportunity-validator/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
exchanges = flag.String("exchanges", "uniswap_v2,uniswap_v3,curve,balancer", "Comma-separated list of exchanges")
|
||||
minProfitBP = flag.Float64("min-profit", 10.0, "Minimum profit threshold in basis points")
|
||||
maxSlippage = flag.Float64("max-slippage", 100.0, "Maximum slippage in basis points")
|
||||
outputDir = flag.String("output", "reports/opportunities", "Output directory")
|
||||
verbose = flag.Bool("verbose", false, "Enable verbose output")
|
||||
realtime = flag.Bool("realtime", false, "Enable real-time opportunity monitoring")
|
||||
duration = flag.Duration("duration", 10*time.Minute, "Duration for real-time monitoring")
|
||||
dryRun = flag.Bool("dry-run", true, "Perform dry run without actual execution")
|
||||
testMode = flag.Bool("test", false, "Run in test mode with simulated data")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Create output directory
|
||||
if err := os.MkdirAll(*outputDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
// Initialize opportunity validator
|
||||
validator, err := internal.NewOpportunityValidator(&internal.ValidatorConfig{
|
||||
Exchanges: *exchanges,
|
||||
MinProfitBP: *minProfitBP,
|
||||
MaxSlippage: *maxSlippage,
|
||||
OutputDir: *outputDir,
|
||||
Verbose: *verbose,
|
||||
DryRun: *dryRun,
|
||||
TestMode: *testMode,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize validator: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if *realtime {
|
||||
fmt.Printf("Starting real-time opportunity validation for %v...\n", *duration)
|
||||
ctx, cancel := context.WithTimeout(ctx, *duration)
|
||||
defer cancel()
|
||||
|
||||
if err := validator.MonitorOpportunities(ctx); err != nil {
|
||||
log.Fatalf("Real-time monitoring failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Running opportunity validation audit...\n")
|
||||
if err := validator.ValidateOpportunities(ctx); err != nil {
|
||||
log.Fatalf("Opportunity validation failed: %v", err)
|
||||
}
|
||||
|
||||
if err := validator.GenerateReport(); err != nil {
|
||||
log.Fatalf("Report generation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Opportunity validation complete. Reports saved to: %s\n", *outputDir)
|
||||
}
|
||||
Reference in New Issue
Block a user