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:
@@ -1,286 +0,0 @@
|
||||
package pricing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
pkgerrors "github.com/fraktal/mev-beta/pkg/errors"
|
||||
oraclepkg "github.com/fraktal/mev-beta/pkg/oracle"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// ExchangePricer handles real-time pricing across multiple DEX protocols
|
||||
type ExchangePricer struct {
|
||||
logger *logger.Logger
|
||||
oracles map[string]*oraclepkg.PriceOracle
|
||||
priceCache map[string]*PriceEntry
|
||||
cacheMutex sync.RWMutex
|
||||
lastUpdate time.Time
|
||||
updateInterval time.Duration
|
||||
}
|
||||
|
||||
// PriceEntry represents cached price data with timestamp
|
||||
type PriceEntry struct {
|
||||
Price *big.Float
|
||||
Timestamp time.Time
|
||||
Validity time.Duration
|
||||
}
|
||||
|
||||
// ExchangePrice represents pricing data from a specific exchange
|
||||
type ExchangePrice struct {
|
||||
Exchange string
|
||||
Pair string
|
||||
BidPrice *big.Float
|
||||
AskPrice *big.Float
|
||||
Liquidity *big.Int
|
||||
Timestamp time.Time
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
// PricingOpportunity represents pricing-specific arbitrage data (extends canonical ArbitrageOpportunity)
|
||||
type PricingOpportunity struct {
|
||||
*types.ArbitrageOpportunity
|
||||
BuyExchange string
|
||||
SellExchange string
|
||||
BuyPrice *big.Float
|
||||
SellPrice *big.Float
|
||||
Spread *big.Float
|
||||
RequiredCapital *big.Int
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// NewExchangePricer creates a new cross-exchange pricer
|
||||
func NewExchangePricer(logger *logger.Logger) *ExchangePricer {
|
||||
return &ExchangePricer{
|
||||
logger: logger,
|
||||
oracles: make(map[string]*oraclepkg.PriceOracle),
|
||||
priceCache: make(map[string]*PriceEntry),
|
||||
updateInterval: 500 * time.Millisecond, // Update every 500ms for real-time pricing
|
||||
}
|
||||
}
|
||||
|
||||
// AddExchangeOracle adds a price oracle for a specific exchange
|
||||
func (ep *ExchangePricer) AddExchangeOracle(exchange string, oracle *oraclepkg.PriceOracle) {
|
||||
ep.oracles[exchange] = oracle
|
||||
ep.logger.Info(fmt.Sprintf("Added price oracle for exchange: %s", exchange))
|
||||
}
|
||||
|
||||
// GetCrossExchangePrices retrieves prices for a token pair across all exchanges
|
||||
func (ep *ExchangePricer) GetCrossExchangePrices(ctx context.Context, tokenIn, tokenOut common.Address) (map[string]*ExchangePrice, error) {
|
||||
prices := make(map[string]*ExchangePrice)
|
||||
|
||||
for exchange, oracle := range ep.oracles {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, pkgerrors.WrapContextError(ctx.Err(), "GetCrossExchangePrices",
|
||||
map[string]interface{}{
|
||||
"tokenIn": tokenIn.Hex(),
|
||||
"tokenOut": tokenOut.Hex(),
|
||||
"currentExchange": exchange,
|
||||
"pricesFetched": len(prices),
|
||||
})
|
||||
default:
|
||||
priceReq := &oraclepkg.PriceRequest{
|
||||
TokenIn: tokenIn,
|
||||
TokenOut: tokenOut,
|
||||
AmountIn: big.NewInt(1e18), // 1 token for reference price
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
priceResp, err := oracle.GetPrice(ctx, priceReq)
|
||||
if err != nil {
|
||||
ep.logger.Debug(fmt.Sprintf("Failed to get price from %s: %v", exchange, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if priceResp.Valid && priceResp.AmountOut != nil {
|
||||
exchangePrice := &ExchangePrice{
|
||||
Exchange: exchange,
|
||||
Pair: fmt.Sprintf("%s/%s", tokenIn.Hex()[:6], tokenOut.Hex()[:6]),
|
||||
BidPrice: new(big.Float).SetInt(priceResp.AmountOut),
|
||||
AskPrice: new(big.Float).SetInt(priceResp.AmountOut), // Simplified - in production would have bid/ask spread
|
||||
Liquidity: priceResp.Liquidity, // Estimated liquidity
|
||||
Timestamp: time.Now(),
|
||||
Confidence: 0.9, // High confidence for direct oracle data
|
||||
}
|
||||
|
||||
prices[exchange] = exchangePrice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prices, nil
|
||||
}
|
||||
|
||||
// FindArbitrageOpportunities identifies cross-exchange arbitrage possibilities
|
||||
func (ep *ExchangePricer) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*types.ArbitrageOpportunity, error) {
|
||||
prices, err := ep.GetCrossExchangePrices(ctx, tokenIn, tokenOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cross-exchange prices: %w", err)
|
||||
}
|
||||
|
||||
if len(prices) < 2 {
|
||||
return nil, nil // Need at least 2 exchanges to find arbitrage
|
||||
}
|
||||
|
||||
var opportunities []*types.ArbitrageOpportunity
|
||||
|
||||
// Compare all exchange pairs for arbitrage opportunities
|
||||
for buyExchange, buyPrice := range prices {
|
||||
for sellExchange, sellPrice := range prices {
|
||||
if buyExchange == sellExchange {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate price spread
|
||||
spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice)
|
||||
if spread.Sign() <= 0 {
|
||||
continue // No arbitrage opportunity
|
||||
}
|
||||
|
||||
// Calculate spread percentage
|
||||
spreadPct := new(big.Float).Quo(spread, buyPrice.AskPrice)
|
||||
spreadPctFloat, _ := spreadPct.Float64()
|
||||
|
||||
// Only consider opportunities with > 0.3% spread (after fees)
|
||||
if spreadPctFloat < 0.003 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Estimate required capital (use smaller liquidity as constraint)
|
||||
requiredCapital := buyPrice.Liquidity
|
||||
if sellPrice.Liquidity.Cmp(requiredCapital) < 0 {
|
||||
requiredCapital = sellPrice.Liquidity
|
||||
}
|
||||
|
||||
// Estimate profit (simplified - real implementation would be more complex)
|
||||
estimatedProfit := new(big.Float).Mul(spread, new(big.Float).SetInt(requiredCapital))
|
||||
estimatedProfit.Quo(estimatedProfit, big.NewFloat(1e18)) // Convert to ETH terms
|
||||
|
||||
// Estimate gas costs (simplified)
|
||||
gasEstimate := big.NewInt(300000) // ~300k gas for complex arbitrage
|
||||
|
||||
// Calculate net profit after gas
|
||||
gasCostEth := new(big.Float).Quo(new(big.Float).SetInt(gasEstimate), big.NewFloat(1e18))
|
||||
gasCostUsd := new(big.Float).Mul(gasCostEth, big.NewFloat(2000)) // Assume $2000/ETH for gas pricing
|
||||
netProfit := new(big.Float).Sub(estimatedProfit, gasCostUsd)
|
||||
|
||||
if netProfit.Sign() <= 0 {
|
||||
continue // Unprofitable after gas costs
|
||||
}
|
||||
|
||||
// Calculate risk score (simplified)
|
||||
riskScore := 0.1 // Low base risk
|
||||
if spreadPctFloat > 0.1 { // > 10% spread
|
||||
riskScore += 0.3 // Higher volatility risk
|
||||
}
|
||||
// Create 100 ETH value using string to avoid overflow
|
||||
hundredETH := new(big.Int)
|
||||
hundredETH.SetString("100000000000000000000", 10) // 100 * 1e18
|
||||
if requiredCapital.Cmp(hundredETH) > 0 { // > 100 ETH liquidity
|
||||
riskScore -= 0.05 // Lower slippage risk with deep liquidity
|
||||
}
|
||||
|
||||
// Convert to canonical ArbitrageOpportunity
|
||||
profitWei := new(big.Int)
|
||||
estimatedProfit.Int(profitWei)
|
||||
netProfitWei := new(big.Int)
|
||||
netProfit.Int(netProfitWei)
|
||||
|
||||
opportunity := &types.ArbitrageOpportunity{
|
||||
Path: []string{buyExchange, sellExchange},
|
||||
Pools: []string{buyExchange + "-pool", sellExchange + "-pool"},
|
||||
AmountIn: requiredCapital,
|
||||
Profit: profitWei,
|
||||
NetProfit: netProfitWei,
|
||||
GasEstimate: gasEstimate,
|
||||
ROI: spreadPctFloat * 100,
|
||||
Protocol: "cross-exchange",
|
||||
ExecutionTime: 15000, // 15 seconds in milliseconds
|
||||
Confidence: (buyPrice.Confidence + sellPrice.Confidence) / 2,
|
||||
PriceImpact: 0.005, // 0.5% estimated
|
||||
MaxSlippage: 0.01, // 1% max slippage
|
||||
TokenIn: tokenIn,
|
||||
TokenOut: tokenOut,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Risk: riskScore,
|
||||
}
|
||||
|
||||
opportunities = append(opportunities, opportunity)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by net profit descending
|
||||
ep.sortOpportunitiesByProfit(opportunities)
|
||||
|
||||
return opportunities, nil
|
||||
}
|
||||
|
||||
// sortOpportunitiesByProfit sorts arbitrage opportunities by net profit
|
||||
func (ep *ExchangePricer) sortOpportunitiesByProfit(opportunities []*types.ArbitrageOpportunity) {
|
||||
// Simple bubble sort for small arrays
|
||||
for i := 0; i < len(opportunities)-1; i++ {
|
||||
for j := 0; j < len(opportunities)-i-1; j++ {
|
||||
profitI := new(big.Float).SetInt(opportunities[j].NetProfit)
|
||||
profitJ := new(big.Float).SetInt(opportunities[j+1].NetProfit)
|
||||
profitIFloat, _ := profitI.Float64()
|
||||
profitJFloat, _ := profitJ.Float64()
|
||||
if profitIFloat < profitJFloat {
|
||||
opportunities[j], opportunities[j+1] = opportunities[j+1], opportunities[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOpportunity validates an arbitrage opportunity is still profitable
|
||||
func (ep *ExchangePricer) ValidateOpportunity(ctx context.Context, opportunity *types.ArbitrageOpportunity) (bool, error) {
|
||||
// Check expiration (using ExecutionTime as expiration window)
|
||||
expiration := time.Unix(opportunity.Timestamp, 0).Add(time.Duration(opportunity.ExecutionTime) * time.Millisecond)
|
||||
if time.Now().After(expiration) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Revalidate prices
|
||||
prices, err := ep.GetCrossExchangePrices(ctx, opportunity.TokenIn, opportunity.TokenOut)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to revalidate prices: %w", err)
|
||||
}
|
||||
|
||||
// Extract buy/sell exchanges from path
|
||||
if len(opportunity.Path) < 2 {
|
||||
return false, nil
|
||||
}
|
||||
buyPrice, buyExists := prices[opportunity.Path[0]]
|
||||
sellPrice, sellExists := prices[opportunity.Path[1]]
|
||||
|
||||
if !buyExists || !sellExists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Recalculate spread
|
||||
spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice)
|
||||
if spread.Sign() <= 0 {
|
||||
return false, nil // No longer profitable
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetPriceCacheStats returns statistics about the price cache
|
||||
func (ep *ExchangePricer) GetPriceCacheStats() map[string]interface{} {
|
||||
ep.cacheMutex.RLock()
|
||||
defer ep.cacheMutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats["cached_prices"] = len(ep.priceCache)
|
||||
stats["last_update"] = ep.lastUpdate
|
||||
stats["update_interval"] = ep.updateInterval
|
||||
|
||||
return stats
|
||||
}
|
||||
Reference in New Issue
Block a user