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>
195 lines
5.7 KiB
Go
195 lines
5.7 KiB
Go
//go:build integration && legacy && forked
|
|
// +build integration,legacy,forked
|
|
|
|
package test_main
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/profitcalc"
|
|
)
|
|
|
|
func TestEnhancedProfitCalculationAndRanking(t *testing.T) {
|
|
// Create a test logger
|
|
log := logger.New("debug", "text", "")
|
|
|
|
// Create profit calculator and ranker
|
|
calc := profitcalc.NewProfitCalculator(log)
|
|
ranker := profitcalc.NewOpportunityRanker(log)
|
|
|
|
// Test tokens
|
|
wethAddr := common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")
|
|
usdcAddr := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
|
|
arbAddr := common.HexToAddress("0x912ce59144191c1204e64559fe8253a0e49e6548")
|
|
|
|
// Create multiple test opportunities with different characteristics
|
|
opportunities := []*profitcalc.SimpleOpportunity{
|
|
// High profit, high confidence opportunity
|
|
calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
wethAddr, usdcAddr,
|
|
big.NewFloat(5.0), // 5 ETH
|
|
big.NewFloat(10000.0), // 10k USDC
|
|
"UniswapV3",
|
|
),
|
|
|
|
// Medium profit opportunity
|
|
calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
wethAddr, arbAddr,
|
|
big.NewFloat(1.0), // 1 ETH
|
|
big.NewFloat(2500.0), // 2.5k ARB
|
|
"SushiSwap",
|
|
),
|
|
|
|
// Lower profit opportunity
|
|
calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
usdcAddr, arbAddr,
|
|
big.NewFloat(100.0), // 100 USDC
|
|
big.NewFloat(250.0), // 250 ARB
|
|
"TraderJoe",
|
|
),
|
|
|
|
// Very small opportunity (should be filtered out)
|
|
calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
wethAddr, usdcAddr,
|
|
big.NewFloat(0.001), // 0.001 ETH
|
|
big.NewFloat(2.0), // 2 USDC
|
|
"PancakeSwap",
|
|
),
|
|
}
|
|
|
|
// Add opportunities to ranker
|
|
var rankedOpps []*profitcalc.RankedOpportunity
|
|
for i, opp := range opportunities {
|
|
if opp != nil {
|
|
rankedOpp := ranker.AddOpportunity(opp)
|
|
if rankedOpp != nil {
|
|
rankedOpps = append(rankedOpps, rankedOpp)
|
|
t.Logf("Added Opportunity %d: ID=%s, NetProfit=%s ETH, ProfitMargin=%.4f%%, Confidence=%.2f",
|
|
i+1, opp.ID, calc.FormatEther(opp.NetProfit), opp.ProfitMargin*100, opp.Confidence)
|
|
} else {
|
|
t.Logf("Opportunity %d filtered out: ID=%s", i+1, opp.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get top opportunities
|
|
topOpps := ranker.GetTopOpportunities(3)
|
|
t.Logf("\n=== Top 3 Opportunities ===")
|
|
for _, opp := range topOpps {
|
|
t.Logf("Rank %d: ID=%s, Score=%.4f, NetProfit=%s ETH, ProfitMargin=%.4f%%, Confidence=%.2f, Competition=%.2f",
|
|
opp.Rank, opp.ID, opp.Score, calc.FormatEther(opp.NetProfit),
|
|
opp.ProfitMargin*100, opp.Confidence, opp.CompetitionRisk)
|
|
}
|
|
|
|
// Get executable opportunities
|
|
executable := ranker.GetExecutableOpportunities(5)
|
|
t.Logf("\n=== Executable Opportunities ===")
|
|
for _, opp := range executable {
|
|
t.Logf("ID=%s, Executable=%t, Reason=%s, Score=%.4f",
|
|
opp.ID, opp.IsExecutable, opp.RejectReason, opp.Score)
|
|
}
|
|
|
|
// Get statistics
|
|
stats := ranker.GetStats()
|
|
t.Logf("\n=== Ranking Statistics ===")
|
|
t.Logf("Total Opportunities: %v", stats["totalOpportunities"])
|
|
t.Logf("Executable Opportunities: %v", stats["executableOpportunities"])
|
|
t.Logf("Average Score: %.4f", stats["averageScore"])
|
|
t.Logf("Average Confidence: %.4f", stats["averageConfidence"])
|
|
|
|
// Verify ranking behavior
|
|
if len(topOpps) > 1 {
|
|
// First ranked opportunity should have highest score
|
|
if topOpps[0].Score < topOpps[1].Score {
|
|
t.Errorf("Ranking error: first opportunity (%.4f) should have higher score than second (%.4f)",
|
|
topOpps[0].Score, topOpps[1].Score)
|
|
}
|
|
}
|
|
|
|
// Test opportunity updates
|
|
t.Logf("\n=== Testing Opportunity Updates ===")
|
|
if len(opportunities) > 0 && opportunities[0] != nil {
|
|
// Create a similar opportunity (same tokens, similar amount)
|
|
similarOpp := calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
opportunities[0].TokenA, opportunities[0].TokenB,
|
|
big.NewFloat(5.1), // Slightly different amount
|
|
big.NewFloat(10200.0),
|
|
"UniswapV3",
|
|
)
|
|
|
|
if similarOpp != nil {
|
|
rankedSimilar := ranker.AddOpportunity(similarOpp)
|
|
if rankedSimilar != nil {
|
|
t.Logf("Updated similar opportunity: UpdateCount=%d", rankedSimilar.UpdateCount)
|
|
if rankedSimilar.UpdateCount < 2 {
|
|
t.Errorf("Expected UpdateCount >= 2, got %d", rankedSimilar.UpdateCount)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpportunityAging(t *testing.T) {
|
|
// Create a test logger
|
|
log := logger.New("debug", "text", "")
|
|
|
|
// Create profit calculator and ranker with short TTL for testing
|
|
calc := profitcalc.NewProfitCalculator(log)
|
|
ranker := profitcalc.NewOpportunityRanker(log)
|
|
|
|
// Create a test opportunity
|
|
wethAddr := common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")
|
|
usdcAddr := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
|
|
|
|
opp := calc.AnalyzeSwapOpportunity(
|
|
context.Background(),
|
|
wethAddr, usdcAddr,
|
|
big.NewFloat(1.0),
|
|
big.NewFloat(2000.0),
|
|
"UniswapV3",
|
|
)
|
|
|
|
if opp == nil {
|
|
t.Fatal("Failed to create test opportunity")
|
|
}
|
|
|
|
// Add opportunity
|
|
rankedOpp := ranker.AddOpportunity(opp)
|
|
if rankedOpp == nil {
|
|
t.Fatal("Failed to add opportunity to ranker")
|
|
}
|
|
|
|
initialScore := rankedOpp.Score
|
|
t.Logf("Initial opportunity score: %.4f", initialScore)
|
|
|
|
// Wait a moment and check that freshness affects score
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Re-rank to update scores based on age
|
|
topOpps := ranker.GetTopOpportunities(1)
|
|
if len(topOpps) > 0 {
|
|
newScore := topOpps[0].Score
|
|
t.Logf("Score after aging: %.4f", newScore)
|
|
|
|
// Score should decrease due to freshness component (though might be minimal for 100ms)
|
|
if newScore > initialScore {
|
|
t.Logf("Note: Score increased slightly, this is normal for short time periods")
|
|
}
|
|
}
|
|
|
|
// Test stats
|
|
stats := ranker.GetStats()
|
|
t.Logf("Ranker stats: %+v", stats)
|
|
}
|