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:
Administrator
2025-11-10 10:53:05 +01:00
parent 803de231ba
commit c54c569f30
718 changed files with 8304 additions and 8281 deletions

View 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
)

View 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=

View File

@@ -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)
}

View 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)
}