# Profit Optimization API Reference ## Quick Reference for Developers **Date:** October 26, 2025 **Version:** 1.0.0 **Status:** Production Ready ✅ --- ## Table of Contents 1. [Reserve Cache API](#reserve-cache-api) 2. [MultiHopScanner Updates](#multihopscanner-updates) 3. [Scanner Integration](#scanner-integration) 4. [SwapAnalyzer Enhancements](#swapanalyzer-enhancements) 5. [Migration Guide](#migration-guide) 6. [Code Examples](#code-examples) 7. [Testing Utilities](#testing-utilities) --- ## Reserve Cache API ### Package: `pkg/cache` The reserve cache provides intelligent caching of pool reserve data with TTL-based expiration and event-driven invalidation. ### Types #### `ReserveData` ```go type ReserveData struct { Reserve0 *big.Int // Token0 reserve amount Reserve1 *big.Int // Token1 reserve amount Liquidity *big.Int // Pool liquidity (V3 only) SqrtPriceX96 *big.Int // Square root price X96 (V3 only) Tick int // Current tick (V3 only) LastUpdated time.Time // Cache timestamp IsV3 bool // True if Uniswap V3 pool } ``` #### `ReserveCache` ```go type ReserveCache struct { // Internal fields (private) } ``` ### Constructor #### `NewReserveCache` ```go func NewReserveCache( client *ethclient.Client, logger *logger.Logger, ttl time.Duration, ) *ReserveCache ``` **Parameters:** - `client` - Ethereum RPC client for fetching reserve data - `logger` - Logger instance for debug/error messages - `ttl` - Time-to-live duration (recommended: 45 seconds) **Returns:** Initialized `*ReserveCache` with background cleanup running **Example:** ```go import ( "time" "github.com/fraktal/mev-beta/pkg/cache" ) reserveCache := cache.NewReserveCache(ethClient, logger, 45*time.Second) ``` --- ### Methods #### `GetOrFetch` Retrieves cached reserve data or fetches from RPC if cache miss/expired. ```go func (rc *ReserveCache) GetOrFetch( ctx context.Context, poolAddress common.Address, isV3 bool, ) (*ReserveData, error) ``` **Parameters:** - `ctx` - Context for RPC calls (with timeout recommended) - `poolAddress` - Pool contract address - `isV3` - `true` for Uniswap V3, `false` for V2 **Returns:** - `*ReserveData` - Pool reserve information - `error` - RPC or decoding errors **Example:** ```go ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() reserveData, err := reserveCache.GetOrFetch(ctx, poolAddr, true) if err != nil { logger.Error("Failed to fetch reserves", "pool", poolAddr.Hex(), "error", err) return err } logger.Info("Reserve data", "reserve0", reserveData.Reserve0.String(), "reserve1", reserveData.Reserve1.String(), "tick", reserveData.Tick, ) ``` --- #### `Invalidate` Manually invalidates cached data for a specific pool. ```go func (rc *ReserveCache) Invalidate(poolAddress common.Address) ``` **Parameters:** - `poolAddress` - Pool to invalidate **Use Cases:** - Pool state changed (Swap, AddLiquidity, RemoveLiquidity events) - Manual cache clearing - Testing scenarios **Example:** ```go // Event-driven invalidation if event.Type == events.Swap { reserveCache.Invalidate(event.PoolAddress) logger.Debug("Cache invalidated due to Swap event", "pool", event.PoolAddress.Hex()) } ``` --- #### `InvalidateMultiple` Invalidates multiple pools in a single call. ```go func (rc *ReserveCache) InvalidateMultiple(poolAddresses []common.Address) ``` **Parameters:** - `poolAddresses` - Slice of pool addresses to invalidate **Example:** ```go affectedPools := []common.Address{pool1, pool2, pool3} reserveCache.InvalidateMultiple(affectedPools) ``` --- #### `GetMetrics` Returns cache performance metrics. ```go func (rc *ReserveCache) GetMetrics() (hits, misses uint64, hitRate float64, size int) ``` **Returns:** - `hits` - Total cache hits - `misses` - Total cache misses - `hitRate` - Hit rate as decimal (0.0-1.0) - `size` - Current number of cached entries **Example:** ```go hits, misses, hitRate, size := reserveCache.GetMetrics() logger.Info("Cache metrics", "hits", hits, "misses", misses, "hitRate", fmt.Sprintf("%.2f%%", hitRate*100), "entries", size, ) // Alert if hit rate drops below threshold if hitRate < 0.60 { logger.Warn("Low cache hit rate", "hitRate", hitRate) } ``` --- #### `Clear` Clears all cached entries. ```go func (rc *ReserveCache) Clear() ``` **Use Cases:** - Testing cleanup - Manual cache reset - Emergency cache invalidation **Example:** ```go // Clear cache during testing reserveCache.Clear() ``` --- #### `Stop` Stops the background cleanup goroutine. ```go func (rc *ReserveCache) Stop() ``` **Important:** Call during graceful shutdown to prevent goroutine leaks. **Example:** ```go // In main application shutdown defer reserveCache.Stop() ``` --- ### Helper Functions #### `CalculateV3ReservesFromState` Calculates approximate V3 reserves from liquidity and price (fallback when RPC fails). ```go func CalculateV3ReservesFromState( liquidity *big.Int, sqrtPriceX96 *big.Int, ) (reserve0, reserve1 *big.Int) ``` **Parameters:** - `liquidity` - Pool liquidity value - `sqrtPriceX96` - Square root price in X96 format **Returns:** - `reserve0` - Calculated token0 reserve - `reserve1` - Calculated token1 reserve **Example:** ```go reserve0, reserve1 := cache.CalculateV3ReservesFromState( poolData.Liquidity.ToBig(), poolData.SqrtPriceX96.ToBig(), ) ``` --- ## MultiHopScanner Updates ### Package: `pkg/arbitrage` ### Constructor Changes #### `NewMultiHopScanner` (Updated Signature) ```go func NewMultiHopScanner( logger *logger.Logger, client *ethclient.Client, // NEW PARAMETER marketMgr interface{}, ) *MultiHopScanner ``` **New Parameter:** - `client` - Ethereum RPC client for reserve cache initialization **Example:** ```go import ( "github.com/fraktal/mev-beta/pkg/arbitrage" "github.com/ethereum/go-ethereum/ethclient" ) ethClient, err := ethclient.Dial(rpcEndpoint) if err != nil { log.Fatal(err) } scanner := arbitrage.NewMultiHopScanner(logger, ethClient, marketManager) ``` --- ### Updated Fields ```go type MultiHopScanner struct { logger *logger.Logger client *ethclient.Client // NEW: RPC client reserveCache *cache.ReserveCache // NEW: Reserve cache // ... existing fields } ``` --- ### Reserve Fetching The scanner now automatically uses the reserve cache when calculating profits. No changes needed in existing code that calls `CalculateProfit()` or similar methods. **Internal Change (developers don't need to modify):** ```go // OLD (in multihop.go): k := new(big.Float).SetInt(pool.Liquidity.ToBig()) k.Mul(k, k) reserve0Float := new(big.Float).Sqrt(new(big.Float).Mul(k, priceInv)) // NEW (automatic with cache): reserveData, err := mhs.reserveCache.GetOrFetch(ctx, pool.Address, isV3) reserve0 = reserveData.Reserve0 reserve1 = reserveData.Reserve1 ``` --- ## Scanner Integration ### Package: `pkg/scanner` ### Constructor Changes #### `NewScanner` (Updated Signature) ```go func NewScanner( cfg *config.BotConfig, logger *logger.Logger, contractExecutor *contracts.ContractExecutor, db *database.Database, reserveCache *cache.ReserveCache, // NEW PARAMETER ) *Scanner ``` **New Parameter:** - `reserveCache` - Optional reserve cache instance (can be `nil`) **Backward Compatible:** Pass `nil` if not using cache. **Example:** ```go // With cache reserveCache := cache.NewReserveCache(client, logger, 45*time.Second) scanner := scanner.NewScanner(cfg, logger, executor, db, reserveCache) // Without cache (backward compatible) scanner := scanner.NewScanner(cfg, logger, executor, db, nil) ``` --- #### `NewMarketScanner` (Variadic Wrapper) ```go func NewMarketScanner( cfg *config.BotConfig, log *logger.Logger, extras ...interface{}, ) *Scanner ``` **Variadic Parameters:** - `extras[0]` - `*contracts.ContractExecutor` - `extras[1]` - `*database.Database` - `extras[2]` - `*cache.ReserveCache` (NEW, optional) **Example:** ```go // With all parameters scanner := scanner.NewMarketScanner(cfg, logger, executor, db, reserveCache) // Without cache (backward compatible) scanner := scanner.NewMarketScanner(cfg, logger, executor, db) // Minimal (backward compatible) scanner := scanner.NewMarketScanner(cfg, logger) ``` --- ### Event-Driven Cache Invalidation The scanner automatically invalidates the cache when pool state changes. This happens internally in the event processing pipeline. **Internal Implementation (in `concurrent.go`):** ```go // EVENT-DRIVEN CACHE INVALIDATION if w.scanner.reserveCache != nil { switch event.Type { case events.Swap, events.AddLiquidity, events.RemoveLiquidity: w.scanner.reserveCache.Invalidate(event.PoolAddress) } } ``` **Developers don't need to call `Invalidate()` manually** - it's automatic! --- ## SwapAnalyzer Enhancements ### Package: `pkg/scanner/swap` ### New Method: `calculatePriceAfterSwap` Calculates the price after a swap using Uniswap V3's concentrated liquidity formula. ```go func (s *SwapAnalyzer) calculatePriceAfterSwap( poolData *market.CachedData, amount0 *big.Int, amount1 *big.Int, priceBefore *big.Float, ) (*big.Float, int) ``` **Parameters:** - `poolData` - Pool state data with liquidity - `amount0` - Swap amount for token0 (negative if out) - `amount1` - Swap amount for token1 (negative if out) - `priceBefore` - Price before the swap **Returns:** - `*big.Float` - Price after the swap - `int` - Tick after the swap **Formula:** ``` Uniswap V3: Δ√P = Δx / L Where: - Δ√P = Change in square root of price - Δx = Amount of token swapped - L = Pool liquidity Token0 in (Token1 out): sqrtPriceAfter = sqrtPriceBefore - (amount0 / L) Token1 in (Token0 out): sqrtPriceAfter = sqrtPriceBefore + (amount1 / L) ``` **Example:** ```go priceAfter, tickAfter := swapAnalyzer.calculatePriceAfterSwap( poolData, event.Amount0, event.Amount1, priceBefore, ) logger.Info("Swap price movement", "priceBefore", priceBefore.String(), "priceAfter", priceAfter.String(), "tickBefore", poolData.Tick, "tickAfter", tickAfter, ) ``` --- ### Updated Price Impact Calculation Price impact is now calculated based on liquidity depth, not swap amount ratios. **New Formula:** ```go // Determine swap direction var amountIn *big.Int if amount0.Sign() > 0 && amount1.Sign() < 0 { amountIn = abs(amount0) // Token0 in, Token1 out } else if amount0.Sign() < 0 && amount1.Sign() > 0 { amountIn = abs(amount1) // Token1 in, Token0 out } // Calculate impact as percentage of liquidity priceImpact = amountIn / (liquidity / 2) ``` **Developers don't need to change code** - this is internal to `SwapAnalyzer.Process()`. --- ## Migration Guide ### For Existing Code #### If You're Using `MultiHopScanner`: **Old Code:** ```go scanner := arbitrage.NewMultiHopScanner(logger, marketManager) ``` **New Code:** ```go ethClient, _ := ethclient.Dial(rpcEndpoint) scanner := arbitrage.NewMultiHopScanner(logger, ethClient, marketManager) ``` --- #### If You're Using `NewScanner` Directly: **Old Code:** ```go scanner := scanner.NewScanner(cfg, logger, executor, db) ``` **New Code (with cache):** ```go reserveCache := cache.NewReserveCache(ethClient, logger, 45*time.Second) scanner := scanner.NewScanner(cfg, logger, executor, db, reserveCache) ``` **New Code (without cache, backward compatible):** ```go scanner := scanner.NewScanner(cfg, logger, executor, db, nil) ``` --- #### If You're Using `NewMarketScanner`: **Old Code:** ```go scanner := scanner.NewMarketScanner(cfg, logger, executor, db) ``` **New Code:** ```go // Option 1: Add cache reserveCache := cache.NewReserveCache(ethClient, logger, 45*time.Second) scanner := scanner.NewMarketScanner(cfg, logger, executor, db, reserveCache) // Option 2: No changes needed (backward compatible) scanner := scanner.NewMarketScanner(cfg, logger, executor, db) ``` --- ## Code Examples ### Complete Integration Example ```go package main import ( "context" "fmt" "log" "time" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/arbitrage" "github.com/fraktal/mev-beta/pkg/cache" "github.com/fraktal/mev-beta/pkg/scanner" ) func main() { // Initialize configuration cfg, err := config.LoadConfig() if err != nil { log.Fatal(err) } // Initialize logger logger := logger.NewLogger("info") // Connect to Ethereum RPC ethClient, err := ethclient.Dial(cfg.ArbitrumRPCEndpoint) if err != nil { log.Fatal(err) } defer ethClient.Close() // Create reserve cache with 45-second TTL reserveCache := cache.NewReserveCache(ethClient, logger, 45*time.Second) defer reserveCache.Stop() // Initialize scanner with cache marketScanner := scanner.NewMarketScanner(cfg, logger, nil, nil, reserveCache) // Initialize arbitrage scanner arbScanner := arbitrage.NewMultiHopScanner(logger, ethClient, marketScanner) // Monitor cache performance go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { hits, misses, hitRate, size := reserveCache.GetMetrics() logger.Info("Cache metrics", "hits", hits, "misses", misses, "hitRate", fmt.Sprintf("%.2f%%", hitRate*100), "entries", size, ) } }() // Start scanning logger.Info("MEV bot started with profit optimizations enabled") // ... rest of application logic } ``` --- ### Manual Cache Invalidation Example ```go package handlers import ( "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/pkg/cache" "github.com/fraktal/mev-beta/pkg/events" ) type EventHandler struct { reserveCache *cache.ReserveCache } func (h *EventHandler) HandleSwapEvent(event *events.Event) { // Process swap event // ... // Invalidate cache for affected pool h.reserveCache.Invalidate(event.PoolAddress) // If multiple pools affected affectedPools := []common.Address{pool1, pool2, pool3} h.reserveCache.InvalidateMultiple(affectedPools) } ``` --- ### Testing with Cache Example ```go package arbitrage_test import ( "context" "math/big" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/pkg/cache" "github.com/stretchr/testify/assert" ) func TestReserveCache(t *testing.T) { // Setup client := setupMockClient() logger := setupTestLogger() reserveCache := cache.NewReserveCache(client, logger, 5*time.Second) defer reserveCache.Stop() poolAddr := common.HexToAddress("0x123...") // Test cache miss (first fetch) ctx := context.Background() data1, err := reserveCache.GetOrFetch(ctx, poolAddr, true) assert.NoError(t, err) assert.NotNil(t, data1) // Verify metrics hits, misses, hitRate, size := reserveCache.GetMetrics() assert.Equal(t, uint64(0), hits, "Should have 0 hits on first fetch") assert.Equal(t, uint64(1), misses, "Should have 1 miss on first fetch") assert.Equal(t, 1, size, "Should have 1 cached entry") // Test cache hit (second fetch) data2, err := reserveCache.GetOrFetch(ctx, poolAddr, true) assert.NoError(t, err) assert.Equal(t, data1.Reserve0, data2.Reserve0) hits, misses, hitRate, _ = reserveCache.GetMetrics() assert.Equal(t, uint64(1), hits, "Should have 1 hit on second fetch") assert.Equal(t, uint64(1), misses, "Misses should remain 1") assert.Greater(t, hitRate, 0.0, "Hit rate should be > 0") // Test invalidation reserveCache.Invalidate(poolAddr) data3, err := reserveCache.GetOrFetch(ctx, poolAddr, true) assert.NoError(t, err) hits, misses, _, _ = reserveCache.GetMetrics() assert.Equal(t, uint64(1), hits, "Hits should remain 1 after invalidation") assert.Equal(t, uint64(2), misses, "Misses should increase to 2") // Test cache expiration time.Sleep(6 * time.Second) // Wait for TTL expiration data4, err := reserveCache.GetOrFetch(ctx, poolAddr, true) assert.NoError(t, err) hits, misses, _, _ = reserveCache.GetMetrics() assert.Equal(t, uint64(3), misses, "Misses should increase after expiration") } ``` --- ## Testing Utilities ### Mock Reserve Cache ```go package testutils import ( "context" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/fraktal/mev-beta/pkg/cache" ) type MockReserveCache struct { data map[common.Address]*cache.ReserveData } func NewMockReserveCache() *MockReserveCache { return &MockReserveCache{ data: make(map[common.Address]*cache.ReserveData), } } func (m *MockReserveCache) GetOrFetch( ctx context.Context, poolAddress common.Address, isV3 bool, ) (*cache.ReserveData, error) { if data, ok := m.data[poolAddress]; ok { return data, nil } // Return mock data return &cache.ReserveData{ Reserve0: big.NewInt(1000000000000000000), // 1 ETH Reserve1: big.NewInt(2000000000000), // 2000 USDC Liquidity: big.NewInt(5000000000000000000), SqrtPriceX96: big.NewInt(1234567890), Tick: 100, IsV3: isV3, }, nil } func (m *MockReserveCache) Invalidate(poolAddress common.Address) { delete(m.data, poolAddress) } func (m *MockReserveCache) SetMockData(poolAddress common.Address, data *cache.ReserveData) { m.data[poolAddress] = data } ``` **Usage in Tests:** ```go func TestArbitrageWithMockCache(t *testing.T) { mockCache := testutils.NewMockReserveCache() // Set custom reserve data poolAddr := common.HexToAddress("0x123...") mockCache.SetMockData(poolAddr, &cache.ReserveData{ Reserve0: big.NewInt(10000000000000000000), // 10 ETH Reserve1: big.NewInt(20000000000000), // 20000 USDC IsV3: true, }) // Use in scanner scanner := scanner.NewScanner(cfg, logger, nil, nil, mockCache) // ... run tests } ``` --- ## Performance Monitoring ### Recommended Metrics to Track ```go package monitoring import ( "fmt" "time" "github.com/fraktal/mev-beta/pkg/cache" ) type CacheMonitor struct { cache *cache.ReserveCache logger Logger alertChan chan CacheAlert } type CacheAlert struct { Level string Message string HitRate float64 } func (m *CacheMonitor) StartMonitoring(interval time.Duration) { ticker := time.NewTicker(interval) go func() { for range ticker.C { m.checkMetrics() } }() } func (m *CacheMonitor) checkMetrics() { hits, misses, hitRate, size := m.cache.GetMetrics() // Log metrics m.logger.Info("Cache performance", "hits", hits, "misses", misses, "hitRate", fmt.Sprintf("%.2f%%", hitRate*100), "entries", size, ) // Alert on low hit rate if hitRate < 0.60 && (hits+misses) > 100 { m.alertChan <- CacheAlert{ Level: "WARNING", Message: "Cache hit rate below 60%", HitRate: hitRate, } } // Alert on excessive cache size if size > 500 { m.alertChan <- CacheAlert{ Level: "WARNING", Message: fmt.Sprintf("Cache size exceeds threshold: %d entries", size), HitRate: hitRate, } } } ``` --- ## Troubleshooting ### Common Issues and Solutions #### 1. Low Cache Hit Rate (<60%) **Symptoms:** - `hitRate` metric consistently below 0.60 - High RPC call volume **Possible Causes:** - TTL too short (increase from 45s to 60s) - Too many cache invalidations (check event frequency) - High pool diversity (many unique pools queried) **Solutions:** ```go // Increase TTL reserveCache := cache.NewReserveCache(client, logger, 60*time.Second) // Check invalidation frequency invalidationCount := 0 // ... in event handler if event.Type == events.Swap { invalidationCount++ if invalidationCount > 100 { logger.Warn("High invalidation frequency", "count", invalidationCount) } } ``` --- #### 2. RPC Timeouts **Symptoms:** - Errors: "context deadline exceeded" - Slow cache fetches **Solutions:** ```go // Increase RPC timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() reserveData, err := reserveCache.GetOrFetch(ctx, poolAddr, isV3) if err != nil { logger.Error("RPC timeout", "pool", poolAddr.Hex(), "error", err) // Use fallback calculation } ``` --- #### 3. Memory Usage Growth **Symptoms:** - Cache size growing unbounded - Memory leaks **Solutions:** ```go // Monitor cache size hits, misses, hitRate, size := reserveCache.GetMetrics() if size > 1000 { logger.Warn("Cache size excessive, clearing old entries", "size", size) // Cache auto-cleanup should handle this, but can manually clear if needed } // Reduce TTL to increase cleanup frequency reserveCache := cache.NewReserveCache(client, logger, 30*time.Second) ``` --- ## API Summary Cheat Sheet ### Reserve Cache Quick Reference | Method | Purpose | Parameters | Returns | |--------|---------|------------|---------| | `NewReserveCache()` | Create cache | client, logger, ttl | `*ReserveCache` | | `GetOrFetch()` | Get/fetch reserves | ctx, poolAddr, isV3 | `*ReserveData, error` | | `Invalidate()` | Clear one entry | poolAddr | - | | `InvalidateMultiple()` | Clear many entries | poolAddrs | - | | `GetMetrics()` | Performance stats | - | hits, misses, hitRate, size | | `Clear()` | Clear all entries | - | - | | `Stop()` | Stop cleanup | - | - | --- ### Constructor Changes Quick Reference | Component | Old Signature | New Signature | Breaking? | |-----------|--------------|---------------|-----------| | `MultiHopScanner` | `(logger, marketMgr)` | `(logger, client, marketMgr)` | **YES** | | `NewScanner` | `(cfg, logger, exec, db)` | `(cfg, logger, exec, db, cache)` | NO (nil supported) | | `NewMarketScanner` | `(cfg, logger, ...)` | `(cfg, logger, ..., cache)` | NO (optional) | --- ## Additional Resources - **Complete Implementation**: `docs/PROFIT_CALCULATION_FIXES_APPLIED.md` - **Event-Driven Cache**: `docs/EVENT_DRIVEN_CACHE_IMPLEMENTATION.md` - **Deployment Guide**: `docs/DEPLOYMENT_GUIDE_PROFIT_OPTIMIZATIONS.md` - **Full Optimization Summary**: `docs/COMPLETE_PROFIT_OPTIMIZATION_SUMMARY.md` --- **Last Updated:** October 26, 2025 **Author:** Claude Code **Version:** 1.0.0 **Status:** Production Ready ✅