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>
240 lines
6.9 KiB
Go
240 lines
6.9 KiB
Go
package calculation_validation
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
)
|
|
|
|
// ProfitValidator validates arbitrage profit calculations
|
|
type ProfitValidator struct {
|
|
weiPerEth *big.Int
|
|
tolerance float64 // Acceptable percentage difference
|
|
}
|
|
|
|
// NewProfitValidator creates a new profit validator
|
|
func NewProfitValidator(tolerancePercent float64) *ProfitValidator {
|
|
return &ProfitValidator{
|
|
weiPerEth: big.NewInt(1e18),
|
|
tolerance: tolerancePercent,
|
|
}
|
|
}
|
|
|
|
// ValidateOpportunity validates an opportunity's profit calculation
|
|
func (pv *ProfitValidator) ValidateOpportunity(opp *OpportunityTestData) *ValidationResult {
|
|
result := &ValidationResult{
|
|
OpportunityID: opp.ID,
|
|
IsValid: true,
|
|
Errors: []string{},
|
|
Warnings: []string{},
|
|
}
|
|
|
|
// Validate threshold check
|
|
if opp.ThresholdCheck != nil {
|
|
thresholdValid := pv.validateThresholdCheck(opp.ThresholdCheck)
|
|
if !thresholdValid {
|
|
result.Errors = append(result.Errors, "threshold check failed")
|
|
result.IsValid = false
|
|
}
|
|
}
|
|
|
|
// Validate profit values
|
|
if opp.NetProfitETH != nil {
|
|
result.ExpectedProfit = opp.NetProfitETH
|
|
|
|
// Check if profit is positive
|
|
if opp.NetProfitETH.Sign() <= 0 && opp.IsExecutable {
|
|
result.Errors = append(result.Errors, "executable opportunity has non-positive profit")
|
|
result.IsValid = false
|
|
}
|
|
|
|
// Check if profit meets threshold
|
|
if opp.ThresholdCheck != nil && opp.IsExecutable {
|
|
if opp.NetProfitETH.Cmp(opp.ThresholdCheck.MinThreshold) < 0 {
|
|
result.Errors = append(result.Errors, "executable opportunity below minimum threshold")
|
|
result.IsValid = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate rejection reason consistency
|
|
if opp.IsExecutable && opp.RejectReason != "" {
|
|
result.Warnings = append(result.Warnings, "executable opportunity has reject reason")
|
|
}
|
|
|
|
if !opp.IsExecutable && opp.RejectReason == "" {
|
|
result.Warnings = append(result.Warnings, "non-executable opportunity missing reject reason")
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// validateThresholdCheck validates a threshold check calculation
|
|
func (pv *ProfitValidator) validateThresholdCheck(check *ThresholdCheck) bool {
|
|
if check == nil {
|
|
return false
|
|
}
|
|
|
|
// Verify the comparison is correct
|
|
expected := check.NetProfit.Cmp(check.MinThreshold) >= 0
|
|
if expected != check.Passed {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ValidateV3Calculation validates a V3 swap calculation
|
|
func (pv *ProfitValidator) ValidateV3Calculation(calc SwapCalculation) *ValidationResult {
|
|
result := &ValidationResult{
|
|
OpportunityID: "v3_calculation",
|
|
IsValid: true,
|
|
Errors: []string{},
|
|
Warnings: []string{},
|
|
}
|
|
|
|
// Check for zero outputs
|
|
if calc.AmountOut.Sign() == 0 && calc.AmountIn.Sign() > 0 {
|
|
result.Warnings = append(result.Warnings, "zero output with non-zero input")
|
|
}
|
|
|
|
// Check if finalOut matches amountOut (should be slightly less due to fees)
|
|
if calc.FinalOut.Cmp(calc.AmountOut) > 0 {
|
|
result.Errors = append(result.Errors, "finalOut greater than amountOut (impossible)")
|
|
result.IsValid = false
|
|
}
|
|
|
|
// Calculate expected fee deduction
|
|
expectedFee := pv.calculateV3Fee(calc.AmountOut, calc.Fee)
|
|
expectedFinalOut := new(big.Int).Sub(calc.AmountOut, expectedFee)
|
|
|
|
// Check if finalOut is within tolerance
|
|
diff := new(big.Int).Sub(expectedFinalOut, calc.FinalOut)
|
|
diff.Abs(diff)
|
|
|
|
// Allow 1% difference for rounding
|
|
tolerance := new(big.Int).Div(calc.FinalOut, big.NewInt(100))
|
|
if diff.Cmp(tolerance) > 0 && calc.FinalOut.Sign() > 0 {
|
|
result.Warnings = append(result.Warnings,
|
|
fmt.Sprintf("finalOut differs from expected: got %s, expected ~%s",
|
|
calc.FinalOut.String(), expectedFinalOut.String()))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// calculateV3Fee calculates the fee for a V3 swap
|
|
func (pv *ProfitValidator) calculateV3Fee(amount *big.Int, feeTier uint32) *big.Int {
|
|
// Fee tiers: 500 = 0.05%, 3000 = 0.3%, 10000 = 1%
|
|
fee := new(big.Int).Mul(amount, big.NewInt(int64(feeTier)))
|
|
fee.Div(fee, big.NewInt(1000000))
|
|
return fee
|
|
}
|
|
|
|
// ValidateBatch validates multiple opportunities and generates a report
|
|
func (pv *ProfitValidator) ValidateBatch(opportunities []*OpportunityTestData) *TestReport {
|
|
report := &TestReport{
|
|
ValidationResults: []*ValidationResult{},
|
|
}
|
|
|
|
var totalError float64
|
|
errorCount := 0
|
|
|
|
for _, opp := range opportunities {
|
|
result := pv.ValidateOpportunity(opp)
|
|
report.ValidationResults = append(report.ValidationResults, result)
|
|
|
|
if result.IsValid {
|
|
report.ValidCalculations++
|
|
} else {
|
|
report.InvalidCalculations++
|
|
}
|
|
|
|
if result.PercentError > 0 {
|
|
totalError += result.PercentError
|
|
errorCount++
|
|
}
|
|
|
|
// Track max error
|
|
if result.Difference != nil {
|
|
if report.MaxError == nil || result.Difference.Cmp(report.MaxError) > 0 {
|
|
report.MaxError = new(big.Float).Set(result.Difference)
|
|
}
|
|
}
|
|
}
|
|
|
|
report.TotalOpportunities = len(opportunities)
|
|
if errorCount > 0 {
|
|
report.AveragePercentError = totalError / float64(errorCount)
|
|
}
|
|
|
|
return report
|
|
}
|
|
|
|
// CompareETHtoWei validates ETH to wei conversion
|
|
func (pv *ProfitValidator) CompareETHtoWei(ethValue *big.Float, weiValue *big.Int) *ValidationResult {
|
|
result := &ValidationResult{
|
|
OpportunityID: "eth_wei_conversion",
|
|
IsValid: true,
|
|
Errors: []string{},
|
|
}
|
|
|
|
// Convert ETH to wei
|
|
expectedWei := new(big.Float).Mul(ethValue, new(big.Float).SetInt(pv.weiPerEth))
|
|
expectedWeiInt, _ := expectedWei.Int(nil)
|
|
|
|
// Compare
|
|
if expectedWeiInt.Cmp(weiValue) != 0 {
|
|
result.IsValid = false
|
|
result.Errors = append(result.Errors,
|
|
fmt.Sprintf("ETH to wei conversion mismatch: %s ETH should be %s wei, got %s wei",
|
|
ethValue.String(), expectedWeiInt.String(), weiValue.String()))
|
|
|
|
// Calculate difference
|
|
diff := new(big.Int).Sub(expectedWeiInt, weiValue)
|
|
result.Difference = new(big.Float).SetInt(diff)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ValidateThresholdComparison validates that a profit threshold comparison was done correctly
|
|
// This is the CRITICAL validation for the bug we fixed
|
|
func (pv *ProfitValidator) ValidateThresholdComparison(
|
|
netProfitETH *big.Float,
|
|
minProfitThresholdWei *big.Int,
|
|
wasExecutable bool,
|
|
) *ValidationResult {
|
|
result := &ValidationResult{
|
|
OpportunityID: "threshold_comparison",
|
|
IsValid: true,
|
|
Errors: []string{},
|
|
}
|
|
|
|
// Convert threshold from wei to ETH for comparison
|
|
minProfitETH := new(big.Float).Quo(
|
|
new(big.Float).SetInt(minProfitThresholdWei),
|
|
new(big.Float).SetInt(pv.weiPerEth),
|
|
)
|
|
|
|
// Expected result: netProfitETH >= minProfitETH
|
|
expectedExecutable := netProfitETH.Cmp(minProfitETH) >= 0
|
|
|
|
if expectedExecutable != wasExecutable {
|
|
result.IsValid = false
|
|
result.Errors = append(result.Errors,
|
|
fmt.Sprintf("threshold comparison incorrect: %.6f ETH vs %.6f ETH threshold, should be executable=%v, got executable=%v",
|
|
mustFloat64(netProfitETH), mustFloat64(minProfitETH), expectedExecutable, wasExecutable))
|
|
}
|
|
|
|
result.ExpectedProfit = netProfitETH
|
|
result.CalculatedProfit = minProfitETH
|
|
|
|
return result
|
|
}
|
|
|
|
// mustFloat64 safely converts big.Float to float64
|
|
func mustFloat64(f *big.Float) float64 {
|
|
val, _ := f.Float64()
|
|
return val
|
|
}
|