package main import ( "encoding/json" "flag" "fmt" "log" "math" "math/big" "os" "path/filepath" "sort" "strings" "time" ) type simulationMetadata struct { Network string `json:"network"` Window string `json:"window"` Sources []string `json:"sources"` Notes string `json:"notes"` } type opportunityVector struct { ID string `json:"id"` Timestamp string `json:"timestamp"` Exchange string `json:"exchange"` ExpectedProfitWei string `json:"expected_profit_wei"` GasCostWei string `json:"gas_cost_wei"` Executed bool `json:"executed"` RealizedProfitWei string `json:"realized_profit_wei"` SlippageLossWei string `json:"slippage_loss_wei"` SkipReason string `json:"skip_reason"` } type simulationVectors struct { Metadata simulationMetadata `json:"metadata"` Opportunities []opportunityVector `json:"opportunities"` } type exchangeBreakdown struct { Exchange string `json:"exchange"` Executed int `json:"executed"` Successful int `json:"successful"` HitRate float64 `json:"hit_rate"` GrossProfitETH string `json:"gross_profit_eth"` NetProfitETH string `json:"net_profit_eth"` GasCostETH string `json:"gas_cost_eth"` } type skipReason struct { Reason string `json:"reason"` Count int `json:"count"` } type simulationSummary struct { GeneratedAt string `json:"generated_at"` VectorPath string `json:"vector_path"` Network string `json:"network"` Window string `json:"window"` Sources []string `json:"sources"` Attempts int `json:"attempts"` Executed int `json:"executed"` ConversionRate float64 `json:"conversion_rate"` Successful int `json:"successful"` Failed int `json:"failed"` HitRate float64 `json:"hit_rate"` GrossProfitETH string `json:"gross_profit_eth"` GasCostETH string `json:"gas_cost_eth"` NetProfitETH string `json:"net_profit_eth"` AverageProfitETH string `json:"average_profit_per_trade_eth"` AverageGasCostETH string `json:"average_gas_cost_eth"` ProfitFactor float64 `json:"profit_factor"` ExchangeBreakdown []exchangeBreakdown `json:"exchange_breakdown"` SkipReasons []skipReason `json:"skip_reasons"` } type payloadAnalysisReport struct { GeneratedAt string `json:"generated_at"` Directory string `json:"directory"` FileCount int `json:"file_count"` TimeRange payloadTimeRange `json:"time_range"` Protocols []namedCount `json:"protocols"` Contracts []namedCount `json:"contracts"` Functions []namedCount `json:"functions"` MissingBlockNumber int `json:"missing_block_number"` MissingRecipient int `json:"missing_recipient"` NonZeroValueCount int `json:"non_zero_value_count"` AverageInputBytes float64 `json:"average_input_bytes"` SampleTransactionHashes []string `json:"sample_transaction_hashes"` } type payloadTimeRange struct { Earliest string `json:"earliest"` Latest string `json:"latest"` } type namedCount struct { Name string `json:"name"` Count int `json:"count"` Percentage float64 `json:"percentage"` } type payloadEntry struct { BlockNumber string `json:"block_number"` Contract string `json:"contract_name"` From string `json:"from"` Function string `json:"function"` FunctionSig string `json:"function_sig"` Hash string `json:"hash"` InputData string `json:"input_data"` Protocol string `json:"protocol"` Recipient string `json:"to"` Value string `json:"value"` } var weiToEthScale = big.NewRat(1, 1_000_000_000_000_000_000) func main() { vectorsPath := flag.String("vectors", "tools/simulation/vectors/default.json", "Path to simulation vector file") reportDir := flag.String("report", "reports/simulation/latest", "Directory for generated reports") payloadDir := flag.String("payload-dir", "", "Directory containing captured opportunity payloads to analyse") flag.Parse() var payloadAnalysis *payloadAnalysisReport if payloadDir != nil && *payloadDir != "" { analysis, err := analyzePayloads(*payloadDir) if err != nil { log.Fatalf("failed to analyse payload captures: %v", err) } payloadAnalysis = &analysis } dataset, err := loadVectors(*vectorsPath) if err != nil { log.Fatalf("failed to load vectors: %v", err) } summary, err := computeSummary(*vectorsPath, dataset) if err != nil { log.Fatalf("failed to compute summary: %v", err) } if err := writeReports(summary, payloadAnalysis, *reportDir); err != nil { log.Fatalf("failed to write reports: %v", err) } printSummary(summary, *reportDir) if payloadAnalysis != nil { printPayloadAnalysis(*payloadAnalysis, *reportDir) } } func loadVectors(path string) (simulationVectors, error) { raw, err := os.ReadFile(path) if err != nil { return simulationVectors{}, err } var dataset simulationVectors if err := json.Unmarshal(raw, &dataset); err != nil { return simulationVectors{}, err } return dataset, nil } type accumulator struct { executed int successful int grossWei *big.Int gasWei *big.Int netWei *big.Int } func newAccumulator() *accumulator { return &accumulator{ grossWei: big.NewInt(0), gasWei: big.NewInt(0), netWei: big.NewInt(0), } } func computeSummary(vectorPath string, dataset simulationVectors) (simulationSummary, error) { totalOpportunities := len(dataset.Opportunities) var executed, successful int var grossWei, gasWei, netWei big.Int exchangeStats := make(map[string]*accumulator) skipReasonCounts := make(map[string]int) for _, opp := range dataset.Opportunities { exchangeKey := strings.ToLower(opp.Exchange) if exchangeKey == "" { exchangeKey = "unknown" } if _, ok := exchangeStats[exchangeKey]; !ok { exchangeStats[exchangeKey] = newAccumulator() } if !opp.Executed { reason := opp.SkipReason if reason == "" { reason = "unspecified" } skipReasonCounts[reason]++ continue } executed++ acc := exchangeStats[exchangeKey] acc.executed++ expected := parseBigInt(opp.ExpectedProfitWei) realized := parseBigInt(opp.RealizedProfitWei) if opp.RealizedProfitWei == "" { realized = expected } if opp.SlippageLossWei != "" { realized.Sub(realized, parseBigInt(opp.SlippageLossWei)) if realized.Sign() < 0 { realized = big.NewInt(0) } } gas := parseBigInt(opp.GasCostWei) grossWei.Add(&grossWei, realized) gasWei.Add(&gasWei, gas) net := new(big.Int).Sub(realized, gas) netWei.Add(&netWei, net) acc.grossWei.Add(acc.grossWei, realized) acc.gasWei.Add(acc.gasWei, gas) acc.netWei.Add(acc.netWei, net) if net.Sign() > 0 { successful++ acc.successful++ } } failed := executed - successful conversionRate := safeRatio(float64(executed), float64(totalOpportunities)) hitRate := safeRatio(float64(successful), float64(executed)) avgProfit := big.NewRat(0, 1) if executed > 0 { avgProfit.SetFrac(&netWei, big.NewInt(int64(executed))) } avgGas := big.NewRat(0, 1) if executed > 0 { avgGas.SetFrac(&gasWei, big.NewInt(int64(executed))) } profitFactor := 0.0 if gasWei.Sign() > 0 { gasRat := new(big.Rat).SetInt(&gasWei) netRat := new(big.Rat).SetInt(&netWei) profitFactor, _ = new(big.Rat).Quo(netRat, gasRat).Float64() } breakdown := make([]exchangeBreakdown, 0, len(exchangeStats)) for name, acc := range exchangeStats { if acc.executed == 0 { continue } breakdown = append(breakdown, exchangeBreakdown{ Exchange: name, Executed: acc.executed, Successful: acc.successful, HitRate: safeRatio(float64(acc.successful), float64(acc.executed)), GrossProfitETH: weiToEthString(acc.grossWei), NetProfitETH: weiToEthString(acc.netWei), GasCostETH: weiToEthString(acc.gasWei), }) } sort.Slice(breakdown, func(i, j int) bool { return breakdown[i].Exchange < breakdown[j].Exchange }) skipList := make([]skipReason, 0, len(skipReasonCounts)) for reason, count := range skipReasonCounts { skipList = append(skipList, skipReason{Reason: reason, Count: count}) } sort.Slice(skipList, func(i, j int) bool { return skipList[i].Count > skipList[j].Count }) summary := simulationSummary{ GeneratedAt: time.Now().UTC().Format(time.RFC3339), VectorPath: vectorPath, Network: dataset.Metadata.Network, Window: dataset.Metadata.Window, Sources: dataset.Metadata.Sources, Attempts: totalOpportunities, Executed: executed, ConversionRate: conversionRate, Successful: successful, Failed: failed, HitRate: hitRate, GrossProfitETH: weiToEthString(&grossWei), GasCostETH: weiToEthString(&gasWei), NetProfitETH: weiToEthString(&netWei), AverageProfitETH: weiRatToEthString(avgProfit), AverageGasCostETH: weiRatToEthString(avgGas), ProfitFactor: profitFactor, ExchangeBreakdown: breakdown, SkipReasons: skipList, } return summary, nil } func analyzePayloads(dir string) (payloadAnalysisReport, error) { entries, err := os.ReadDir(dir) if err != nil { return payloadAnalysisReport{}, fmt.Errorf("read payload directory: %w", err) } report := payloadAnalysisReport{ GeneratedAt: time.Now().UTC().Format(time.RFC3339), Directory: dir, } protocolCounts := make(map[string]int) contractCounts := make(map[string]int) functionCounts := make(map[string]int) var ( totalInputBytes int earliest time.Time latest time.Time haveTimestamp bool samples []string ) for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { continue } payloadPath := filepath.Join(dir, entry.Name()) raw, err := os.ReadFile(payloadPath) if err != nil { return payloadAnalysisReport{}, fmt.Errorf("read payload %s: %w", payloadPath, err) } var payload payloadEntry if err := json.Unmarshal(raw, &payload); err != nil { return payloadAnalysisReport{}, fmt.Errorf("decode payload %s: %w", payloadPath, err) } report.FileCount++ timestampToken := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) if idx := strings.Index(timestampToken, "_"); idx != -1 { timestampToken = timestampToken[:idx] } if ts, err := parseCaptureTimestamp(timestampToken); err == nil { if !haveTimestamp || ts.Before(earliest) { earliest = ts } if !haveTimestamp || ts.After(latest) { latest = ts } haveTimestamp = true } protocol := strings.TrimSpace(payload.Protocol) if protocol == "" { protocol = "unknown" } protocolCounts[protocol]++ contract := strings.TrimSpace(payload.Contract) if contract == "" { contract = "unknown" } contractCounts[contract]++ function := strings.TrimSpace(payload.Function) if function == "" { function = "unknown" } functionCounts[function]++ if payload.BlockNumber == "" { report.MissingBlockNumber++ } if payload.Recipient == "" { report.MissingRecipient++ } value := strings.TrimSpace(payload.Value) if value != "" && value != "0" && value != "0x0" { report.NonZeroValueCount++ } totalInputBytes += estimateHexBytes(payload.InputData) if payload.Hash != "" && len(samples) < 5 { samples = append(samples, payload.Hash) } } if report.FileCount == 0 { return payloadAnalysisReport{}, fmt.Errorf("no payload JSON files found in %s", dir) } report.Protocols = buildNamedCounts(protocolCounts, report.FileCount) report.Contracts = buildNamedCounts(contractCounts, report.FileCount) report.Functions = buildNamedCounts(functionCounts, report.FileCount) if haveTimestamp { report.TimeRange = payloadTimeRange{ Earliest: earliest.UTC().Format(time.RFC3339), Latest: latest.UTC().Format(time.RFC3339), } } report.AverageInputBytes = math.Round((float64(totalInputBytes)/float64(report.FileCount))*100) / 100 report.SampleTransactionHashes = samples return report, nil } func parseCaptureTimestamp(token string) (time.Time, error) { layouts := []string{ "20060102T150405.000Z", "20060102T150405Z", } for _, layout := range layouts { if ts, err := time.Parse(layout, token); err == nil { return ts, nil } } return time.Time{}, fmt.Errorf("unrecognised timestamp token %q", token) } func buildNamedCounts(counts map[string]int, total int) []namedCount { items := make([]namedCount, 0, len(counts)) for name, count := range counts { percentage := 0.0 if total > 0 { percentage = (float64(count) / float64(total)) * 100 percentage = math.Round(percentage*100) / 100 // 2 decimal places } items = append(items, namedCount{ Name: name, Count: count, Percentage: percentage, }) } sort.Slice(items, func(i, j int) bool { if items[i].Count == items[j].Count { return items[i].Name < items[j].Name } return items[i].Count > items[j].Count }) return items } func parseBigInt(value string) *big.Int { if value == "" { return big.NewInt(0) } if strings.HasPrefix(value, "0x") { val := new(big.Int) val.SetString(value[2:], 16) return val } val := new(big.Int) val.SetString(value, 10) return val } func safeRatio(num, den float64) float64 { if den == 0 { return 0 } return num / den } func weiToEthString(val *big.Int) string { if val == nil { return "0.000000" } rat := new(big.Rat).SetInt(val) eth := new(big.Rat).Mul(rat, weiToEthScale) return eth.FloatString(6) } func weiRatToEthString(rat *big.Rat) string { if rat.Sign() == 0 { return "0" } eth := new(big.Rat).Mul(rat, weiToEthScale) return eth.FloatString(6) } func writeReports(summary simulationSummary, payload *payloadAnalysisReport, reportDir string) error { if err := os.MkdirAll(reportDir, 0o755); err != nil { return err } if err := writeSimulationReports(summary, reportDir); err != nil { return err } if payload != nil { if err := writePayloadReports(*payload, reportDir); err != nil { return err } } return nil } func writeSimulationReports(summary simulationSummary, reportDir string) error { if err := os.MkdirAll(reportDir, 0o755); err != nil { return err } jsonPath := filepath.Join(reportDir, "summary.json") jsonBytes, err := json.MarshalIndent(summary, "", " ") if err != nil { return err } if err := os.WriteFile(jsonPath, jsonBytes, 0o644); err != nil { return err } markdownPath := filepath.Join(reportDir, "summary.md") if err := os.WriteFile(markdownPath, []byte(buildSimulationMarkdown(summary)), 0o644); err != nil { return err } return nil } func writePayloadReports(analysis payloadAnalysisReport, reportDir string) error { jsonPath := filepath.Join(reportDir, "payload_analysis.json") jsonBytes, err := json.MarshalIndent(analysis, "", " ") if err != nil { return err } if err := os.WriteFile(jsonPath, jsonBytes, 0o644); err != nil { return err } mdPath := filepath.Join(reportDir, "payload_analysis.md") if err := os.WriteFile(mdPath, []byte(buildPayloadMarkdown(analysis)), 0o644); err != nil { return err } return nil } func buildSimulationMarkdown(summary simulationSummary) string { var b strings.Builder b.WriteString("# Profitability Simulation Report\n\n") b.WriteString(fmt.Sprintf("- Generated at: %s\n", summary.GeneratedAt)) b.WriteString(fmt.Sprintf("- Vector source: `%s`\n", summary.VectorPath)) if summary.Network != "" { b.WriteString(fmt.Sprintf("- Network: **%s**\n", summary.Network)) } if summary.Window != "" { b.WriteString(fmt.Sprintf("- Window: %s\n", summary.Window)) } if len(summary.Sources) > 0 { b.WriteString(fmt.Sprintf("- Exchanges: %s\n", strings.Join(summary.Sources, ", "))) } b.WriteString("\n## Summary\n\n") b.WriteString(fmt.Sprintf("- Opportunities analysed: **%d**\n", summary.Attempts)) b.WriteString(fmt.Sprintf("- Executed: **%d** (conversion %.1f%%)\n", summary.Executed, summary.ConversionRate*100)) b.WriteString(fmt.Sprintf("- Successes: **%d** / %d (hit rate %.1f%%)\n", summary.Successful, summary.Executed, summary.HitRate*100)) b.WriteString(fmt.Sprintf("- Gross profit: **%s ETH**\n", summary.GrossProfitETH)) b.WriteString(fmt.Sprintf("- Gas spent: **%s ETH**\n", summary.GasCostETH)) b.WriteString(fmt.Sprintf("- Net profit after gas: **%s ETH**\n", summary.NetProfitETH)) b.WriteString(fmt.Sprintf("- Avg profit per trade: **%s ETH**\n", summary.AverageProfitETH)) b.WriteString(fmt.Sprintf("- Avg gas cost per trade: **%s ETH**\n", summary.AverageGasCostETH)) b.WriteString(fmt.Sprintf("- Profit factor (net/gas): **%.2f**\n", summary.ProfitFactor)) if len(summary.ExchangeBreakdown) > 0 { b.WriteString("\n## Exchange Breakdown\n\n") b.WriteString("| Exchange | Executed | Success | Hit Rate | Gross Profit (ETH) | Gas (ETH) | Net Profit (ETH) |\n") b.WriteString("| --- | ---:| ---:| ---:| ---:| ---:| ---:|\n") for _, ex := range summary.ExchangeBreakdown { b.WriteString(fmt.Sprintf("| %s | %d | %d | %.1f%% | %s | %s | %s |\n", ex.Exchange, ex.Executed, ex.Successful, ex.HitRate*100, ex.GrossProfitETH, ex.GasCostETH, ex.NetProfitETH)) } } if len(summary.SkipReasons) > 0 { b.WriteString("\n## Skip Reasons\n\n") for _, reason := range summary.SkipReasons { b.WriteString(fmt.Sprintf("- %s: %d\n", reason.Reason, reason.Count)) } } return b.String() } func buildPayloadMarkdown(analysis payloadAnalysisReport) string { var b strings.Builder b.WriteString("# Payload Capture Analysis\n\n") b.WriteString(fmt.Sprintf("- Generated at: %s\n", analysis.GeneratedAt)) b.WriteString(fmt.Sprintf("- Source directory: `%s`\n", analysis.Directory)) b.WriteString(fmt.Sprintf("- Files analysed: **%d**\n", analysis.FileCount)) if analysis.TimeRange.Earliest != "" || analysis.TimeRange.Latest != "" { b.WriteString(fmt.Sprintf("- Capture window: %s → %s\n", analysis.TimeRange.Earliest, analysis.TimeRange.Latest)) } b.WriteString(fmt.Sprintf("- Average calldata size: %.2f bytes\n", analysis.AverageInputBytes)) b.WriteString(fmt.Sprintf("- Payloads with non-zero value: %d\n", analysis.NonZeroValueCount)) b.WriteString(fmt.Sprintf("- Missing block numbers: %d\n", analysis.MissingBlockNumber)) b.WriteString(fmt.Sprintf("- Missing recipients: %d\n", analysis.MissingRecipient)) if len(analysis.Protocols) > 0 { b.WriteString("\n## Protocol Distribution\n\n") b.WriteString("| Protocol | Count | Share |\n") b.WriteString("| --- | ---:| ---:|\n") for _, item := range analysis.Protocols { b.WriteString(fmt.Sprintf("| %s | %d | %.2f%% |\n", item.Name, item.Count, item.Percentage)) } } if len(analysis.Contracts) > 0 { b.WriteString("\n## Contract Names\n\n") b.WriteString("| Contract | Count | Share |\n") b.WriteString("| --- | ---:| ---:|\n") for _, item := range analysis.Contracts { b.WriteString(fmt.Sprintf("| %s | %d | %.2f%% |\n", item.Name, item.Count, item.Percentage)) } } if len(analysis.Functions) > 0 { b.WriteString("\n## Function Signatures\n\n") b.WriteString("| Function | Count | Share |\n") b.WriteString("| --- | ---:| ---:|\n") for _, item := range analysis.Functions { b.WriteString(fmt.Sprintf("| %s | %d | %.2f%% |\n", item.Name, item.Count, item.Percentage)) } } if len(analysis.SampleTransactionHashes) > 0 { b.WriteString("\n## Sample Transactions\n\n") for _, hash := range analysis.SampleTransactionHashes { b.WriteString(fmt.Sprintf("- `%s`\n", hash)) } } return b.String() } func printSummary(summary simulationSummary, reportDir string) { fmt.Println("Profitability Simulation Summary") fmt.Println("================================") fmt.Printf("Opportunities: %d\n", summary.Attempts) fmt.Printf("Executed: %d (conversion %.1f%%)\n", summary.Executed, summary.ConversionRate*100) fmt.Printf("Hit Rate: %.1f%% (%d successes, %d failures)\n", summary.HitRate*100, summary.Successful, summary.Failed) fmt.Printf("Gross Profit: %s ETH\n", summary.GrossProfitETH) fmt.Printf("Gas Spent: %s ETH\n", summary.GasCostETH) fmt.Printf("Net Profit: %s ETH\n", summary.NetProfitETH) fmt.Printf("Average Profit/Trade: %s ETH\n", summary.AverageProfitETH) fmt.Printf("Average Gas/Trade: %s ETH\n", summary.AverageGasCostETH) fmt.Printf("Profit Factor: %.2f\n", summary.ProfitFactor) if len(summary.ExchangeBreakdown) > 0 { fmt.Println("\nPer Exchange:") for _, ex := range summary.ExchangeBreakdown { fmt.Printf("- %s: executed %d, hit rate %.1f%%, net %s ETH\n", ex.Exchange, ex.Executed, ex.HitRate*100, ex.NetProfitETH) } } fmt.Println("\nReports written to", reportDir) } func printPayloadAnalysis(analysis payloadAnalysisReport, reportDir string) { fmt.Println("\nPayload Capture Analysis") fmt.Println("========================") fmt.Printf("Files analysed: %d\n", analysis.FileCount) if analysis.TimeRange.Earliest != "" || analysis.TimeRange.Latest != "" { fmt.Printf("Capture window: %s → %s\n", analysis.TimeRange.Earliest, analysis.TimeRange.Latest) } fmt.Printf("Average calldata size: %.2f bytes\n", analysis.AverageInputBytes) fmt.Printf("Payloads with non-zero value: %d\n", analysis.NonZeroValueCount) fmt.Printf("Missing block numbers: %d\n", analysis.MissingBlockNumber) fmt.Printf("Missing recipients: %d\n", analysis.MissingRecipient) if len(analysis.Protocols) > 0 { fmt.Println("\nTop Protocols:") for _, item := range analysis.Protocols { fmt.Printf("- %s: %d (%.2f%%)\n", item.Name, item.Count, item.Percentage) } } if len(analysis.SampleTransactionHashes) > 0 { fmt.Println("\nSample transaction hashes:") for _, hash := range analysis.SampleTransactionHashes { fmt.Printf("- %s\n", hash) } } fmt.Printf("\nPayload analysis saved as payload_analysis.json and payload_analysis.md in %s\n", reportDir) } func estimateHexBytes(value string) int { if value == "" { return 0 } trimmed := strings.TrimSpace(value) trimmed = strings.TrimPrefix(trimmed, "0x") if len(trimmed) == 0 { return 0 } if len(trimmed)%2 != 0 { trimmed = "0" + trimmed } return len(trimmed) / 2 }