Files
mev-beta/orig/tools/math-audit/profit_regression_test.go
Administrator c54c569f30 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>
2025-11-10 10:53:05 +01:00

405 lines
15 KiB
Go

package main
import (
"fmt"
"math/big"
"testing"
pkgmath "github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/tools/math-audit/internal"
)
// ProfitRegressionTestCase defines a test case for profit regression validation
type ProfitRegressionTestCase struct {
Name string
Exchange string
TestData *internal.PricingTest
ExpectedProfit float64 // Expected profit margin in basis points
MinProfit float64 // Minimum acceptable profit in basis points
MaxProfit float64 // Maximum acceptable profit in basis points
Description string
}
// TestProfitRegressionValidation tests that profit calculations remain within expected ranges
func TestProfitRegressionValidation(t *testing.T) {
converter := pkgmath.NewDecimalConverter()
auditor := internal.NewMathAuditor(converter, 0.0001) // 1bp tolerance
testCases := []ProfitRegressionTestCase{
{
Name: "Uniswap_V2_ETH_USDC_Precision",
Exchange: "uniswap_v2",
TestData: &internal.PricingTest{
TestName: "ETH_USDC_Standard_Pool",
Description: "Standard ETH/USDC pool price calculation",
Reserve0: "1000000000000000000000", // 1000 ETH
Reserve1: "2000000000000", // 2M USDC
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH
Tolerance: 1.0,
},
ExpectedProfit: 0.0, // Perfect pricing should have 0 profit difference
MinProfit: -5.0, // Allow 5bp negative
MaxProfit: 5.0, // Allow 5bp positive
Description: "Validates Uniswap V2 pricing precision for profit calculations",
},
{
Name: "Uniswap_V3_ETH_USDC_Precision",
Exchange: "uniswap_v3",
TestData: &internal.PricingTest{
TestName: "ETH_USDC_V3_Basic",
Description: "ETH/USDC V3 price from sqrtPriceX96",
SqrtPriceX96: "3543191142285914327220224", // Corrected value
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH
Tolerance: 1.0,
},
ExpectedProfit: 0.0, // Perfect pricing should have 0 profit difference
MinProfit: -5.0, // Allow 5bp negative
MaxProfit: 5.0, // Allow 5bp positive
Description: "Validates Uniswap V3 pricing precision for profit calculations",
},
{
Name: "Curve_USDC_USDT_Stable_Precision",
Exchange: "curve",
TestData: &internal.PricingTest{
TestName: "Stable_USDC_USDT",
Description: "Stable swap USDC/USDT pricing",
Reserve0: "1000000000000", // 1M USDC
Reserve1: "1000000000000", // 1M USDT
ExpectedPrice: "1000000000000000000", // 1:1 ratio
Tolerance: 0.5,
},
ExpectedProfit: 0.0, // Stable swaps should have minimal profit difference
MinProfit: -2.0, // Allow 2bp negative
MaxProfit: 2.0, // Allow 2bp positive
Description: "Validates Curve stable swap pricing precision",
},
{
Name: "Balancer_80_20_Weighted_Precision",
Exchange: "balancer",
TestData: &internal.PricingTest{
TestName: "Weighted_80_20_Pool",
Description: "80/20 weighted pool pricing",
Reserve0: "800000000000000000000", // 800 ETH
Reserve1: "400000000000", // 400k USDC
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH (corrected)
Tolerance: 2.0,
},
ExpectedProfit: 0.0, // Proper weighted calculation should be precise
MinProfit: -10.0, // Allow 10bp negative for weighted pools
MaxProfit: 10.0, // Allow 10bp positive for weighted pools
Description: "Validates Balancer weighted pool pricing precision",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
// Run profit regression validation
// Run the pricing test using the auditor
result := runPricingTestForProfit(auditor, tc.Exchange, tc.TestData)
// Calculate profit based on error
profitBP := result.ErrorBP
// Validate profit is within expected range
if profitBP < tc.MinProfit {
t.Errorf("Profit %.4f bp below minimum threshold %.4f bp", profitBP, tc.MinProfit)
}
if profitBP > tc.MaxProfit {
t.Errorf("Profit %.4f bp above maximum threshold %.4f bp", profitBP, tc.MaxProfit)
}
// Check if the test passed the original validation
if !result.Passed {
t.Errorf("Underlying pricing test failed with %.4f bp error", result.ErrorBP)
}
t.Logf("✓ %s: Error=%.4f bp (expected ~%.4f bp, range: %.1f to %.1f bp)",
tc.Name, profitBP, tc.ExpectedProfit, tc.MinProfit, tc.MaxProfit)
t.Logf(" %s", tc.Description)
})
}
}
// runPricingTestForProfit runs a pricing test and returns the result for profit analysis
func runPricingTestForProfit(auditor *internal.MathAuditor, exchangeType string, test *internal.PricingTest) *internal.IndividualTestResult {
// This is a simplified version of the runPricingTest method from auditor.go
// We use the same logic to ensure consistency
// Determine decimals based on test name patterns
reserve0Decimals := uint8(18) // Default to 18 decimals
reserve1Decimals := uint8(18) // Default to 18 decimals
if test.TestName == "ETH_USDC_Standard_Pool" || test.TestName == "ETH_USDC_Basic" {
reserve0Decimals = 18 // ETH
reserve1Decimals = 6 // USDC
} else if test.TestName == "WBTC_ETH_High_Value" || test.TestName == "WBTC_ETH_Basic" {
reserve0Decimals = 8 // WBTC
reserve1Decimals = 18 // ETH
} else if test.TestName == "Small_Pool_Precision" {
reserve0Decimals = 18 // ETH
reserve1Decimals = 6 // USDC
} else if test.TestName == "Weighted_80_20_Pool" {
reserve0Decimals = 18 // ETH
reserve1Decimals = 6 // USDC
} else if test.TestName == "Stable_USDC_USDT" {
reserve0Decimals = 6 // USDC
reserve1Decimals = 6 // USDT
} else if test.TestName == "ETH_USDC_V3_Basic" {
reserve0Decimals = 18 // ETH
reserve1Decimals = 6 // USDC
}
converter := pkgmath.NewDecimalConverter()
// Calculate price using the same methods as the auditor
var calculatedPrice *pkgmath.UniversalDecimal
var err error
switch exchangeType {
case "uniswap_v2":
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
calculatedPrice, err = calculateUniswapV2PriceForRegression(converter, reserve0, reserve1)
case "uniswap_v3":
calculatedPrice, err = calculateUniswapV3PriceForRegression(converter, test)
case "curve":
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
calculatedPrice, err = calculateCurvePriceForRegression(converter, reserve0, reserve1)
case "balancer":
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
calculatedPrice, err = calculateBalancerPriceForRegression(converter, reserve0, reserve1)
default:
err = fmt.Errorf("unknown exchange type: %s", exchangeType)
}
if err != nil {
return &internal.IndividualTestResult{
TestName: test.TestName,
Passed: false,
ErrorBP: 10000, // Max error
Description: fmt.Sprintf("Calculation failed: %v", err),
}
}
// Compare with expected result
expectedPrice, _ := converter.FromString(test.ExpectedPrice, 18, "PRICE")
errorBP := calculateErrorBPForRegression(expectedPrice, calculatedPrice)
passed := errorBP <= 1.0 // 1bp tolerance for regression tests
return &internal.IndividualTestResult{
TestName: test.TestName,
Passed: passed,
ErrorBP: errorBP,
}
}
// Price calculation functions (simplified versions of auditor functions)
func calculateUniswapV2PriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
return nil, fmt.Errorf("reserve0 cannot be zero")
}
// Normalize both to 18 decimals for calculation
reserve0Normalized := new(big.Int).Set(reserve0.Value)
reserve1Normalized := new(big.Int).Set(reserve1.Value)
// Scale to 18 decimals
if reserve0.Decimals < 18 {
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
reserve0Normalized.Mul(reserve0Normalized, scale0)
}
if reserve1.Decimals < 18 {
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
reserve1Normalized.Mul(reserve1Normalized, scale1)
}
// Calculate price = reserve1 / reserve0 in 18 decimal precision
priceInt := new(big.Int).Mul(reserve1Normalized, new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
priceInt.Div(priceInt, reserve0Normalized)
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
}
func calculateUniswapV3PriceForRegression(converter *pkgmath.DecimalConverter, test *internal.PricingTest) (*pkgmath.UniversalDecimal, error) {
if test.SqrtPriceX96 == "" {
return nil, fmt.Errorf("sqrtPriceX96 is required for V3")
}
sqrtPriceX96 := new(big.Int)
_, success := sqrtPriceX96.SetString(test.SqrtPriceX96, 10)
if !success {
return nil, fmt.Errorf("invalid sqrtPriceX96 format")
}
// Convert sqrtPriceX96 to price: price = (sqrtPriceX96 / 2^96)^2
q96 := new(big.Int).Lsh(big.NewInt(1), 96) // 2^96
// Calculate raw price
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
q96Float := new(big.Float).SetInt(q96)
sqrtPriceFloat.Quo(sqrtPriceFloat, q96Float)
// Square to get the price
priceFloat := new(big.Float).Mul(sqrtPriceFloat, sqrtPriceFloat)
// Account for decimal differences (ETH/USDC: 18 vs 6 decimals)
if test.TestName == "ETH_USDC_V3_Basic" {
decimalAdjustment := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(12), nil))
priceFloat.Mul(priceFloat, decimalAdjustment)
}
// Convert to UniversalDecimal with 18 decimal precision
scaleFactor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
priceFloat.Mul(priceFloat, scaleFactor)
priceInt, _ := priceFloat.Int(nil)
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
}
func calculateCurvePriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
// For stable swaps, price = reserve1 / reserve0 with decimal normalization
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
return nil, fmt.Errorf("reserve0 cannot be zero")
}
reserve0Normalized := new(big.Int).Set(reserve0.Value)
reserve1Normalized := new(big.Int).Set(reserve1.Value)
// Scale to 18 decimals
if reserve0.Decimals < 18 {
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
reserve0Normalized.Mul(reserve0Normalized, scale0)
}
if reserve1.Decimals < 18 {
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
reserve1Normalized.Mul(reserve1Normalized, scale1)
}
priceInt := new(big.Int).Mul(reserve1Normalized, new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
priceInt.Div(priceInt, reserve0Normalized)
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
}
func calculateBalancerPriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
// For 80/20 weighted pools: price = (reserve1 * weight0) / (reserve0 * weight1)
// weight0 = 80%, weight1 = 20%
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
return nil, fmt.Errorf("reserve0 cannot be zero")
}
reserve0Normalized := new(big.Int).Set(reserve0.Value)
reserve1Normalized := new(big.Int).Set(reserve1.Value)
// Scale to 18 decimals
if reserve0.Decimals < 18 {
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
reserve0Normalized.Mul(reserve0Normalized, scale0)
}
if reserve1.Decimals < 18 {
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
reserve1Normalized.Mul(reserve1Normalized, scale1)
}
// Calculate weighted price using big.Float for precision
reserve1Float := new(big.Float).SetInt(reserve1Normalized)
reserve0Float := new(big.Float).SetInt(reserve0Normalized)
weight0Float := big.NewFloat(80.0)
weight1Float := big.NewFloat(20.0)
numerator := new(big.Float).Mul(reserve1Float, weight0Float)
denominator := new(big.Float).Mul(reserve0Float, weight1Float)
priceFloat := new(big.Float).Quo(numerator, denominator)
// Convert back to big.Int with 18 decimal precision
scaleFactor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
priceFloat.Mul(priceFloat, scaleFactor)
priceInt, _ := priceFloat.Int(nil)
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
}
func calculateErrorBPForRegression(expected, actual *pkgmath.UniversalDecimal) float64 {
if expected.Value.Cmp(big.NewInt(0)) == 0 {
if actual.Value.Cmp(big.NewInt(0)) == 0 {
return 0.0
}
return 10000.0 // Max error if expected is 0 but actual is not
}
// Calculate relative error: |actual - expected| / expected
diff := new(big.Int).Sub(actual.Value, expected.Value)
if diff.Sign() < 0 {
diff.Neg(diff)
}
// Convert to float for percentage calculation
diffFloat, _ := new(big.Float).SetInt(diff).Float64()
expectedFloat, _ := new(big.Float).SetInt(expected.Value).Float64()
if expectedFloat == 0 {
return 10000.0
}
return (diffFloat / expectedFloat) * 10000.0
}
// TestArbitrageSpreadStability tests that arbitrage spread calculations remain stable
func TestArbitrageSpreadStability(t *testing.T) {
converter := pkgmath.NewDecimalConverter()
// Test that spread calculations don't introduce rounding errors
testSpread := func(name string, price1Str, price2Str string, expectedSpreadBP, toleranceBP float64) {
t.Run(name, func(t *testing.T) {
price1, _ := converter.FromString(price1Str, 18, "PRICE1")
price2, _ := converter.FromString(price2Str, 18, "PRICE2")
// Calculate spread: |price2 - price1| / ((price1 + price2) / 2) * 10000
diff := new(big.Int).Sub(price2.Value, price1.Value)
if diff.Sign() < 0 {
diff.Neg(diff)
}
sum := new(big.Int).Add(price1.Value, price2.Value)
avgPrice := new(big.Int).Div(sum, big.NewInt(2))
if avgPrice.Cmp(big.NewInt(0)) == 0 {
t.Fatal("Average price cannot be zero")
}
spreadFloat := new(big.Float).SetInt(diff)
avgFloat := new(big.Float).SetInt(avgPrice)
spreadFloat.Quo(spreadFloat, avgFloat)
spreadFloat.Mul(spreadFloat, big.NewFloat(10000.0)) // Convert to basis points
actualSpreadBP, _ := spreadFloat.Float64()
errorBP := absRegression(actualSpreadBP - expectedSpreadBP)
if errorBP > toleranceBP {
t.Errorf("Spread error %.4f bp exceeds tolerance %.4f bp (expected %.4f bp, got %.4f bp)",
errorBP, toleranceBP, expectedSpreadBP, actualSpreadBP)
}
t.Logf("✓ %s: Spread=%.4f bp (expected %.4f bp, error=%.4f bp)",
name, actualSpreadBP, expectedSpreadBP, errorBP)
})
}
// Test various spread scenarios
testSpread("Small_Spread", "2000000000000000000000", "2001000000000000000000", 50.0, 1.0) // 0.05% spread
testSpread("Medium_Spread", "2000000000000000000000", "2010000000000000000000", 498.8, 5.0) // ~0.5% spread
testSpread("Large_Spread", "2000000000000000000000", "2100000000000000000000", 4878.0, 10.0) // ~4.9% spread
testSpread("Minimal_Spread", "2000000000000000000000", "2000100000000000000000", 5.0, 0.1) // 0.005% spread
}
func absRegression(x float64) float64 {
if x < 0 {
return -x
}
return x
}