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>
512 lines
16 KiB
Go
512 lines
16 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/fraktal/mev-beta/pkg/math"
|
|
)
|
|
|
|
// Config holds configuration for the profitability auditor
|
|
type Config struct {
|
|
MinProfitBP float64
|
|
MaxSlippageBP float64
|
|
OutputDir string
|
|
Verbose bool
|
|
ScenariosFile string
|
|
}
|
|
|
|
// ProfitabilityAuditor audits profit calculations and arbitrage opportunities
|
|
type ProfitabilityAuditor struct {
|
|
config *Config
|
|
converter *math.DecimalConverter
|
|
calculator *math.ArbitrageCalculator
|
|
scenarios *ProfitabilityScenarios
|
|
results map[string]*ExchangeProfitResult
|
|
}
|
|
|
|
// ProfitabilityScenarios defines test scenarios for profit validation
|
|
type ProfitabilityScenarios struct {
|
|
Version string `json:"version"`
|
|
Scenarios map[string]*ProfitabilityTest `json:"scenarios"`
|
|
}
|
|
|
|
// ProfitabilityTest represents a profit calculation test case
|
|
type ProfitabilityTest struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Exchange string `json:"exchange"`
|
|
AmountIn string `json:"amount_in"`
|
|
TokenIn string `json:"token_in"`
|
|
TokenOut string `json:"token_out"`
|
|
ExpectedProfit string `json:"expected_profit"`
|
|
MaxSlippage float64 `json:"max_slippage"`
|
|
GasCost string `json:"gas_cost"`
|
|
MinROI float64 `json:"min_roi"`
|
|
TestType string `json:"test_type"` // "static", "stress", "edge_case"
|
|
}
|
|
|
|
// ExchangeProfitResult holds profitability audit results for an exchange
|
|
type ExchangeProfitResult struct {
|
|
Exchange string `json:"exchange"`
|
|
TotalTests int `json:"total_tests"`
|
|
PassedTests int `json:"passed_tests"`
|
|
FailedTests int `json:"failed_tests"`
|
|
AverageProfitBP float64 `json:"average_profit_bp"`
|
|
MaxProfitBP float64 `json:"max_profit_bp"`
|
|
MinProfitBP float64 `json:"min_profit_bp"`
|
|
AverageROI float64 `json:"average_roi"`
|
|
TestResults []*ProfitTestResult `json:"test_results"`
|
|
FailedCases []*ProfitTestFailure `json:"failed_cases"`
|
|
Duration time.Duration `json:"duration"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// ProfitTestResult represents the result of a single profit test
|
|
type ProfitTestResult struct {
|
|
TestName string `json:"test_name"`
|
|
Passed bool `json:"passed"`
|
|
ActualProfitBP float64 `json:"actual_profit_bp"`
|
|
ExpectedProfitBP float64 `json:"expected_profit_bp"`
|
|
ActualROI float64 `json:"actual_roi"`
|
|
ExpectedROI float64 `json:"expected_roi"`
|
|
SlippageBP float64 `json:"slippage_bp"`
|
|
GasCostETH string `json:"gas_cost_eth"`
|
|
NetProfitETH string `json:"net_profit_eth"`
|
|
Duration time.Duration `json:"duration"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
// ProfitTestFailure represents a failed profit test
|
|
type ProfitTestFailure struct {
|
|
TestName string `json:"test_name"`
|
|
Reason string `json:"reason"`
|
|
Expected float64 `json:"expected"`
|
|
Actual float64 `json:"actual"`
|
|
Difference float64 `json:"difference"`
|
|
Severity string `json:"severity"` // "low", "medium", "high", "critical"
|
|
}
|
|
|
|
// NewProfitabilityAuditor creates a new profitability auditor
|
|
func NewProfitabilityAuditor(config *Config) (*ProfitabilityAuditor, error) {
|
|
converter := math.NewDecimalConverter()
|
|
calculator := math.NewArbitrageCalculator(nil) // TODO: Add proper gas estimator
|
|
|
|
// Load test scenarios
|
|
scenarios, err := loadProfitabilityScenarios(config.ScenariosFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load scenarios: %w", err)
|
|
}
|
|
|
|
return &ProfitabilityAuditor{
|
|
config: config,
|
|
converter: converter,
|
|
calculator: calculator,
|
|
scenarios: scenarios,
|
|
results: make(map[string]*ExchangeProfitResult),
|
|
}, nil
|
|
}
|
|
|
|
// AuditExchange performs profitability audit for a specific exchange
|
|
func (pa *ProfitabilityAuditor) AuditExchange(ctx context.Context, exchange string) error {
|
|
startTime := time.Now()
|
|
|
|
if pa.config.Verbose {
|
|
fmt.Printf("Starting profitability audit for %s...\n", exchange)
|
|
}
|
|
|
|
result := &ExchangeProfitResult{
|
|
Exchange: exchange,
|
|
TestResults: []*ProfitTestResult{},
|
|
FailedCases: []*ProfitTestFailure{},
|
|
Timestamp: startTime,
|
|
}
|
|
|
|
// Get scenarios for this exchange
|
|
exchangeScenarios := pa.getExchangeScenarios(exchange)
|
|
result.TotalTests = len(exchangeScenarios)
|
|
|
|
var totalProfitBP, totalROI float64
|
|
maxProfitBP := -1000.0
|
|
minProfitBP := 1000.0
|
|
|
|
for _, scenario := range exchangeScenarios {
|
|
testResult, err := pa.runProfitTest(ctx, scenario)
|
|
if err != nil {
|
|
if pa.config.Verbose {
|
|
fmt.Printf(" Failed test %s: %v\n", scenario.Name, err)
|
|
}
|
|
|
|
failure := &ProfitTestFailure{
|
|
TestName: scenario.Name,
|
|
Reason: err.Error(),
|
|
Severity: "high",
|
|
}
|
|
result.FailedCases = append(result.FailedCases, failure)
|
|
result.FailedTests++
|
|
continue
|
|
}
|
|
|
|
result.TestResults = append(result.TestResults, testResult)
|
|
|
|
if testResult.Passed {
|
|
result.PassedTests++
|
|
totalProfitBP += testResult.ActualProfitBP
|
|
totalROI += testResult.ActualROI
|
|
|
|
if testResult.ActualProfitBP > maxProfitBP {
|
|
maxProfitBP = testResult.ActualProfitBP
|
|
}
|
|
if testResult.ActualProfitBP < minProfitBP {
|
|
minProfitBP = testResult.ActualProfitBP
|
|
}
|
|
} else {
|
|
result.FailedTests++
|
|
|
|
failure := &ProfitTestFailure{
|
|
TestName: scenario.Name,
|
|
Reason: testResult.ErrorMessage,
|
|
Expected: testResult.ExpectedProfitBP,
|
|
Actual: testResult.ActualProfitBP,
|
|
Difference: testResult.ActualProfitBP - testResult.ExpectedProfitBP,
|
|
Severity: pa.calculateSeverity(testResult.ActualProfitBP, testResult.ExpectedProfitBP),
|
|
}
|
|
result.FailedCases = append(result.FailedCases, failure)
|
|
}
|
|
|
|
if pa.config.Verbose {
|
|
status := "✓"
|
|
if !testResult.Passed {
|
|
status = "✗"
|
|
}
|
|
fmt.Printf(" %s %s: Profit=%.2f bp, ROI=%.2f%%\n",
|
|
status, scenario.Name, testResult.ActualProfitBP, testResult.ActualROI)
|
|
}
|
|
}
|
|
|
|
// Calculate averages
|
|
if result.PassedTests > 0 {
|
|
result.AverageProfitBP = totalProfitBP / float64(result.PassedTests)
|
|
result.AverageROI = totalROI / float64(result.PassedTests)
|
|
result.MaxProfitBP = maxProfitBP
|
|
result.MinProfitBP = minProfitBP
|
|
}
|
|
|
|
result.Duration = time.Since(startTime)
|
|
pa.results[exchange] = result
|
|
|
|
if pa.config.Verbose {
|
|
fmt.Printf("Completed %s audit: %d/%d tests passed (%.1f%%)\n",
|
|
exchange, result.PassedTests, result.TotalTests,
|
|
float64(result.PassedTests)/float64(result.TotalTests)*100)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runProfitTest executes a single profit test
|
|
func (pa *ProfitabilityAuditor) runProfitTest(ctx context.Context, scenario *ProfitabilityTest) (*ProfitTestResult, error) {
|
|
startTime := time.Now()
|
|
|
|
// Convert input amounts to UniversalDecimal
|
|
amountIn, err := pa.converter.FromString(scenario.AmountIn, 18, scenario.TokenIn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid amount_in: %w", err)
|
|
}
|
|
|
|
gasCost, err := pa.converter.FromString(scenario.GasCost, 18, "ETH")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid gas_cost: %w", err)
|
|
}
|
|
|
|
expectedProfit, err := pa.converter.FromString(scenario.ExpectedProfit, 18, "ETH")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid expected_profit: %w", err)
|
|
}
|
|
|
|
// Simulate arbitrage calculation
|
|
// TODO: Integrate with actual exchange data and calculation engine
|
|
actualProfit, netProfit, slippage, err := pa.simulateProfitCalculation(scenario, amountIn, gasCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("profit simulation failed: %w", err)
|
|
}
|
|
|
|
// Calculate metrics
|
|
actualProfitBP := pa.calculateProfitBP(actualProfit, amountIn)
|
|
expectedProfitBP := pa.calculateProfitBP(expectedProfit, amountIn)
|
|
actualROI := actualProfitBP / 100.0 // Convert BP to percentage
|
|
|
|
// Determine if test passed
|
|
profitDiff := actualProfitBP - expectedProfitBP
|
|
profitPassed := actualProfitBP >= pa.config.MinProfitBP
|
|
slippagePassed := slippage <= pa.config.MaxSlippageBP
|
|
roiPassed := actualROI >= scenario.MinROI
|
|
|
|
passed := profitPassed && slippagePassed && roiPassed
|
|
|
|
result := &ProfitTestResult{
|
|
TestName: scenario.Name,
|
|
Passed: passed,
|
|
ActualProfitBP: actualProfitBP,
|
|
ExpectedProfitBP: expectedProfitBP,
|
|
ActualROI: actualROI,
|
|
ExpectedROI: scenario.MinROI,
|
|
SlippageBP: slippage,
|
|
GasCostETH: pa.converter.ToHumanReadable(gasCost),
|
|
NetProfitETH: pa.converter.ToHumanReadable(netProfit),
|
|
Duration: time.Since(startTime),
|
|
}
|
|
|
|
if !passed {
|
|
var reasons []string
|
|
if !profitPassed {
|
|
reasons = append(reasons, fmt.Sprintf("profit %.2f bp below minimum %.2f bp", actualProfitBP, pa.config.MinProfitBP))
|
|
}
|
|
if !slippagePassed {
|
|
reasons = append(reasons, fmt.Sprintf("slippage %.2f bp exceeds maximum %.2f bp", slippage, pa.config.MaxSlippageBP))
|
|
}
|
|
if !roiPassed {
|
|
reasons = append(reasons, fmt.Sprintf("ROI %.2f%% below minimum %.2f%%", actualROI, scenario.MinROI))
|
|
}
|
|
result.ErrorMessage = fmt.Sprintf("Test failed: %v", reasons)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// simulateProfitCalculation simulates profit calculation for a scenario
|
|
func (pa *ProfitabilityAuditor) simulateProfitCalculation(scenario *ProfitabilityTest, amountIn, gasCost *math.UniversalDecimal) (*math.UniversalDecimal, *math.UniversalDecimal, float64, error) {
|
|
// This is a simplified simulation - in production, this would integrate with real exchange APIs
|
|
|
|
// Simulate price impact and slippage based on exchange type
|
|
var slippageBP float64
|
|
var profitMultiplier float64
|
|
|
|
switch scenario.Exchange {
|
|
case "uniswap_v2":
|
|
slippageBP = 15.0 // Simulate 15 bp slippage
|
|
profitMultiplier = 1.025 // 2.5% profit
|
|
case "uniswap_v3":
|
|
slippageBP = 8.0 // Lower slippage for V3
|
|
profitMultiplier = 1.032 // 3.2% profit
|
|
case "curve":
|
|
slippageBP = 3.0 // Very low slippage for stable swaps
|
|
profitMultiplier = 1.008 // 0.8% profit
|
|
case "balancer":
|
|
slippageBP = 25.0 // Higher slippage for weighted pools
|
|
profitMultiplier = 1.045 // 4.5% profit
|
|
default:
|
|
return nil, nil, 0, fmt.Errorf("unsupported exchange: %s", scenario.Exchange)
|
|
}
|
|
|
|
// Calculate gross profit
|
|
profitAmount := new(big.Int).Mul(amountIn.Value, big.NewInt(int64(profitMultiplier*1000)))
|
|
profitAmount.Div(profitAmount, big.NewInt(1000))
|
|
profitAmount.Sub(profitAmount, amountIn.Value)
|
|
|
|
grossProfit, err := math.NewUniversalDecimal(profitAmount, 18, "ETH")
|
|
if err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
// Calculate net profit (gross - gas)
|
|
netProfitAmount := new(big.Int).Sub(grossProfit.Value, gasCost.Value)
|
|
netProfit, err := math.NewUniversalDecimal(netProfitAmount, 18, "ETH")
|
|
if err != nil {
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
return grossProfit, netProfit, slippageBP, nil
|
|
}
|
|
|
|
// calculateProfitBP calculates profit in basis points
|
|
func (pa *ProfitabilityAuditor) calculateProfitBP(profit, amountIn *math.UniversalDecimal) float64 {
|
|
if amountIn.Value.Cmp(big.NewInt(0)) == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// Convert to float for calculation
|
|
profitFloat, _ := new(big.Float).SetInt(profit.Value).Float64()
|
|
amountFloat, _ := new(big.Float).SetInt(amountIn.Value).Float64()
|
|
|
|
return (profitFloat / amountFloat) * 10000.0 // Convert to basis points
|
|
}
|
|
|
|
// calculateSeverity determines the severity of a test failure
|
|
func (pa *ProfitabilityAuditor) calculateSeverity(actual, expected float64) string {
|
|
diff := expected - actual
|
|
diffPercent := (diff / expected) * 100
|
|
|
|
switch {
|
|
case diffPercent > 50:
|
|
return "critical"
|
|
case diffPercent > 25:
|
|
return "high"
|
|
case diffPercent > 10:
|
|
return "medium"
|
|
default:
|
|
return "low"
|
|
}
|
|
}
|
|
|
|
// getExchangeScenarios returns test scenarios for a specific exchange
|
|
func (pa *ProfitabilityAuditor) getExchangeScenarios(exchange string) []*ProfitabilityTest {
|
|
var scenarios []*ProfitabilityTest
|
|
|
|
for _, scenario := range pa.scenarios.Scenarios {
|
|
if scenario.Exchange == exchange || scenario.Exchange == "all" {
|
|
scenarios = append(scenarios, scenario)
|
|
}
|
|
}
|
|
|
|
// If no scenarios found, create default ones
|
|
if len(scenarios) == 0 {
|
|
scenarios = pa.createDefaultScenarios(exchange)
|
|
}
|
|
|
|
return scenarios
|
|
}
|
|
|
|
// createDefaultScenarios creates default test scenarios for an exchange
|
|
func (pa *ProfitabilityAuditor) createDefaultScenarios(exchange string) []*ProfitabilityTest {
|
|
baseScenarios := []*ProfitabilityTest{
|
|
{
|
|
Name: fmt.Sprintf("%s_small_arbitrage", exchange),
|
|
Description: "Small arbitrage opportunity",
|
|
Exchange: exchange,
|
|
AmountIn: "1000000000000000000", // 1 ETH
|
|
TokenIn: "ETH",
|
|
TokenOut: "USDC",
|
|
ExpectedProfit: "25000000000000000", // 0.025 ETH
|
|
MaxSlippage: 50.0,
|
|
GasCost: "5000000000000000", // 0.005 ETH
|
|
MinROI: 1.0,
|
|
TestType: "static",
|
|
},
|
|
{
|
|
Name: fmt.Sprintf("%s_medium_arbitrage", exchange),
|
|
Description: "Medium arbitrage opportunity",
|
|
Exchange: exchange,
|
|
AmountIn: "10000000000000000000", // 10 ETH
|
|
TokenIn: "ETH",
|
|
TokenOut: "USDC",
|
|
ExpectedProfit: "300000000000000000", // 0.3 ETH
|
|
MaxSlippage: 100.0,
|
|
GasCost: "8000000000000000", // 0.008 ETH
|
|
MinROI: 2.5,
|
|
TestType: "static",
|
|
},
|
|
{
|
|
Name: fmt.Sprintf("%s_large_arbitrage", exchange),
|
|
Description: "Large arbitrage opportunity",
|
|
Exchange: exchange,
|
|
AmountIn: "100000000000000000000", // 100 ETH
|
|
TokenIn: "ETH",
|
|
TokenOut: "USDC",
|
|
ExpectedProfit: "5000000000000000000", // 5 ETH
|
|
MaxSlippage: 200.0,
|
|
GasCost: "15000000000000000", // 0.015 ETH
|
|
MinROI: 4.5,
|
|
TestType: "stress",
|
|
},
|
|
}
|
|
|
|
return baseScenarios
|
|
}
|
|
|
|
// MonitorRealTimeProfit monitors real-time profit opportunities
|
|
func (pa *ProfitabilityAuditor) MonitorRealTimeProfit(ctx context.Context, exchange string) error {
|
|
// TODO: Implement real-time monitoring with live market data
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
fmt.Printf("Monitoring real-time profitability for %s...\n", exchange)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
// Simulate real-time profit check
|
|
if pa.config.Verbose {
|
|
fmt.Printf("[%s] Checking real-time opportunities for %s...\n",
|
|
time.Now().Format("15:04:05"), exchange)
|
|
}
|
|
|
|
// TODO: Integrate with live market data and opportunity detection
|
|
}
|
|
}
|
|
}
|
|
|
|
// GenerateReport generates comprehensive profitability audit reports
|
|
func (pa *ProfitabilityAuditor) GenerateReport() error {
|
|
// Generate JSON report
|
|
reportPath := filepath.Join(pa.config.OutputDir, "profitability_audit.json")
|
|
if err := pa.generateJSONReport(reportPath); err != nil {
|
|
return fmt.Errorf("failed to generate JSON report: %w", err)
|
|
}
|
|
|
|
// Generate Markdown report
|
|
markdownPath := filepath.Join(pa.config.OutputDir, "profitability_audit.md")
|
|
if err := pa.generateMarkdownReport(markdownPath); err != nil {
|
|
return fmt.Errorf("failed to generate Markdown report: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateJSONReport generates a JSON format report
|
|
func (pa *ProfitabilityAuditor) generateJSONReport(path string) error {
|
|
report := map[string]interface{}{
|
|
"timestamp": time.Now(),
|
|
"config": pa.config,
|
|
"exchange_results": pa.results,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(report, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
// generateMarkdownReport generates a Markdown format report
|
|
func (pa *ProfitabilityAuditor) generateMarkdownReport(path string) error {
|
|
// TODO: Implement detailed Markdown report generation
|
|
content := fmt.Sprintf("# Profitability Audit Report\n\nGenerated: %s\n\n", time.Now().Format(time.RFC3339))
|
|
|
|
for exchange, result := range pa.results {
|
|
content += fmt.Sprintf("## %s\n\n", exchange)
|
|
content += fmt.Sprintf("- Total Tests: %d\n", result.TotalTests)
|
|
content += fmt.Sprintf("- Passed: %d\n", result.PassedTests)
|
|
content += fmt.Sprintf("- Failed: %d\n", result.FailedTests)
|
|
content += fmt.Sprintf("- Average Profit: %.2f bp\n", result.AverageProfitBP)
|
|
content += fmt.Sprintf("- Average ROI: %.2f%%\n\n", result.AverageROI)
|
|
}
|
|
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
// loadProfitabilityScenarios loads test scenarios from file
|
|
func loadProfitabilityScenarios(filename string) (*ProfitabilityScenarios, error) {
|
|
// If filename is "default", create default scenarios
|
|
if filename == "default" {
|
|
return &ProfitabilityScenarios{
|
|
Version: "1.0.0",
|
|
Scenarios: make(map[string]*ProfitabilityTest),
|
|
}, nil
|
|
}
|
|
|
|
// TODO: Load from actual file
|
|
return &ProfitabilityScenarios{
|
|
Version: "1.0.0",
|
|
Scenarios: make(map[string]*ProfitabilityTest),
|
|
}, nil
|
|
}
|