feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
239
tests/calculation-validation/validator.go
Normal file
239
tests/calculation-validation/validator.go
Normal file
@@ -0,0 +1,239 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user