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>
This commit is contained in:
723
orig/tools/simulation/main.go
Normal file
723
orig/tools/simulation/main.go
Normal file
@@ -0,0 +1,723 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user