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>
263 lines
7.1 KiB
Go
263 lines
7.1 KiB
Go
package calculation_validation
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// LogParser parses arbitrage opportunity data from log files
|
|
type LogParser struct {
|
|
executablePattern *regexp.Regexp
|
|
profitPattern *regexp.Regexp
|
|
thresholdPattern *regexp.Regexp
|
|
v3CalcPattern *regexp.Regexp
|
|
opportunityPattern *regexp.Regexp
|
|
}
|
|
|
|
// NewLogParser creates a new log parser
|
|
func NewLogParser() *LogParser {
|
|
return &LogParser{
|
|
executablePattern: regexp.MustCompile(`EXECUTABLE OPPORTUNITY: ID=([^,]+), Profit=([0-9.]+) ETH \(threshold=([0-9.]+) ETH\)`),
|
|
profitPattern: regexp.MustCompile(`Estimated Profit: \$([0-9,.]+) USD`),
|
|
thresholdPattern: regexp.MustCompile(`Profit threshold check: netProfit=([0-9.]+) ETH, minThreshold=([0-9.]+) ETH`),
|
|
v3CalcPattern: regexp.MustCompile(`V3 calculation: amountIn=([0-9]+), amountOut=([0-9]+), fee=([0-9]+), finalOut=([0-9]+)`),
|
|
opportunityPattern: regexp.MustCompile(`ARBITRAGE OPPORTUNITY DETECTED`),
|
|
}
|
|
}
|
|
|
|
// ParseExecutableOpportunities parses executable opportunities from log file
|
|
func (lp *LogParser) ParseExecutableOpportunities(logFile string) ([]*OpportunityTestData, error) {
|
|
file, err := os.Open(logFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var opportunities []*OpportunityTestData
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Parse executable opportunity
|
|
if matches := lp.executablePattern.FindStringSubmatch(line); matches != nil {
|
|
opp := &OpportunityTestData{
|
|
ID: matches[1],
|
|
Timestamp: time.Now(), // Would need to parse from log timestamp
|
|
IsExecutable: true,
|
|
}
|
|
|
|
// Parse profit
|
|
profit, err := strconv.ParseFloat(matches[2], 64)
|
|
if err == nil {
|
|
opp.NetProfitETH = big.NewFloat(profit)
|
|
}
|
|
|
|
// Parse threshold
|
|
threshold, err := strconv.ParseFloat(matches[3], 64)
|
|
if err == nil {
|
|
opp.ThresholdCheck = &ThresholdCheck{
|
|
NetProfit: big.NewFloat(profit),
|
|
MinThreshold: big.NewFloat(threshold),
|
|
Passed: profit >= threshold,
|
|
}
|
|
}
|
|
|
|
opportunities = append(opportunities, opp)
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading log file: %w", err)
|
|
}
|
|
|
|
return opportunities, nil
|
|
}
|
|
|
|
// ParseV3Calculations parses V3 swap calculations from log file
|
|
func (lp *LogParser) ParseV3Calculations(logFile string) ([]SwapCalculation, error) {
|
|
file, err := os.Open(logFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var calculations []SwapCalculation
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if matches := lp.v3CalcPattern.FindStringSubmatch(line); matches != nil {
|
|
amountIn, _ := new(big.Int).SetString(matches[1], 10)
|
|
amountOut, _ := new(big.Int).SetString(matches[2], 10)
|
|
fee, _ := strconv.ParseUint(matches[3], 10, 32)
|
|
finalOut, _ := new(big.Int).SetString(matches[4], 10)
|
|
|
|
calculations = append(calculations, SwapCalculation{
|
|
AmountIn: amountIn,
|
|
AmountOut: amountOut,
|
|
Fee: uint32(fee),
|
|
FinalOut: finalOut,
|
|
})
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading log file: %w", err)
|
|
}
|
|
|
|
return calculations, nil
|
|
}
|
|
|
|
// ParseThresholdChecks parses profit threshold validation checks
|
|
func (lp *LogParser) ParseThresholdChecks(logFile string) ([]*ThresholdCheck, error) {
|
|
file, err := os.Open(logFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var checks []*ThresholdCheck
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if matches := lp.thresholdPattern.FindStringSubmatch(line); matches != nil {
|
|
netProfit, _ := strconv.ParseFloat(matches[1], 64)
|
|
minThreshold, _ := strconv.ParseFloat(matches[2], 64)
|
|
|
|
checks = append(checks, &ThresholdCheck{
|
|
NetProfit: big.NewFloat(netProfit),
|
|
MinThreshold: big.NewFloat(minThreshold),
|
|
Passed: netProfit >= minThreshold,
|
|
})
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading log file: %w", err)
|
|
}
|
|
|
|
return checks, nil
|
|
}
|
|
|
|
// ExtractProfitValues extracts all profit values from opportunities
|
|
func ExtractProfitValues(opportunities []*OpportunityTestData) []*big.Float {
|
|
var profits []*big.Float
|
|
for _, opp := range opportunities {
|
|
if opp.NetProfitETH != nil {
|
|
profits = append(profits, opp.NetProfitETH)
|
|
}
|
|
}
|
|
return profits
|
|
}
|
|
|
|
// CalculateStatistics calculates statistics from profit values
|
|
func CalculateStatistics(profits []*big.Float) (total, average, max, min *big.Float) {
|
|
if len(profits) == 0 {
|
|
return big.NewFloat(0), big.NewFloat(0), big.NewFloat(0), big.NewFloat(0)
|
|
}
|
|
|
|
total = big.NewFloat(0)
|
|
max = new(big.Float).Set(profits[0])
|
|
min = new(big.Float).Set(profits[0])
|
|
|
|
for _, profit := range profits {
|
|
total.Add(total, profit)
|
|
if profit.Cmp(max) > 0 {
|
|
max = new(big.Float).Set(profit)
|
|
}
|
|
if profit.Cmp(min) < 0 {
|
|
min = new(big.Float).Set(profit)
|
|
}
|
|
}
|
|
|
|
average = new(big.Float).Quo(total, big.NewFloat(float64(len(profits))))
|
|
return total, average, max, min
|
|
}
|
|
|
|
// ParseOpportunityDetails parses detailed opportunity information from multi-line log entries
|
|
func (lp *LogParser) ParseOpportunityDetails(logFile string) ([]*OpportunityTestData, error) {
|
|
file, err := os.Open(logFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var opportunities []*OpportunityTestData
|
|
var currentOpp *OpportunityTestData
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Start of new opportunity
|
|
if lp.opportunityPattern.MatchString(line) {
|
|
if currentOpp != nil {
|
|
opportunities = append(opportunities, currentOpp)
|
|
}
|
|
currentOpp = &OpportunityTestData{
|
|
Timestamp: time.Now(),
|
|
}
|
|
continue
|
|
}
|
|
|
|
if currentOpp == nil {
|
|
continue
|
|
}
|
|
|
|
// Parse various fields
|
|
if strings.Contains(line, "Transaction:") {
|
|
parts := strings.Split(line, ":")
|
|
if len(parts) > 1 {
|
|
currentOpp.TransactionHash = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
|
|
if matches := lp.profitPattern.FindStringSubmatch(line); matches != nil {
|
|
profitStr := strings.ReplaceAll(matches[1], ",", "")
|
|
profitUSD, _ := strconv.ParseFloat(profitStr, 64)
|
|
currentOpp.NetProfitUSD = big.NewFloat(profitUSD)
|
|
}
|
|
|
|
if strings.Contains(line, "netProfitETH:") {
|
|
parts := strings.Split(line, ":")
|
|
if len(parts) > 1 {
|
|
profitStr := strings.TrimSpace(parts[1])
|
|
profit, _ := strconv.ParseFloat(profitStr, 64)
|
|
currentOpp.NetProfitETH = big.NewFloat(profit)
|
|
}
|
|
}
|
|
|
|
if strings.Contains(line, "isExecutable:") {
|
|
currentOpp.IsExecutable = strings.Contains(line, "isExecutable:true")
|
|
}
|
|
|
|
if strings.Contains(line, "rejectReason:") {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) > 1 {
|
|
currentOpp.RejectReason = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't forget the last opportunity
|
|
if currentOpp != nil {
|
|
opportunities = append(opportunities, currentOpp)
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading log file: %w", err)
|
|
}
|
|
|
|
return opportunities, nil
|
|
}
|