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:
Administrator
2025-11-10 10:14:26 +01:00
parent 1773daffe7
commit 803de231ba
411 changed files with 20390 additions and 8680 deletions

View File

@@ -0,0 +1 @@
# Market Manager\n\nThe Market Manager is a core component of the MEV bot that handles market data collection, storage, and analysis to identify arbitrage opportunities across different DEX protocols on Arbitrum.\n\n## Features\n\n- **Market Data Management**: Store and manage market data for multiple DEX pools\n- **Data Verification**: Verify market data from sequencer against on-chain data\n- **Arbitrage Detection**: Detect arbitrage opportunities between markets\n- **Persistent Storage**: Save market data to database for historical analysis\n- **In-Memory Caching**: Fast access to frequently used market data\n\n## Installation\n\n```bash\ngo get github.com/fraktal/mev-beta/pkg/marketmanager\n```\n\n## Usage\n\n### Basic Market Manager Setup\n\n```go\npackage main\n\nimport (\n \"github.com/fraktal/mev-beta/pkg/marketmanager\"\n \"time\"\n)\n\nfunc main() {\n // Create a new market manager\n config := &marketmanager.MarketManagerConfig{\n VerificationWindow: 500 * time.Millisecond,\n MaxMarkets: 1000,\n }\n \n manager := marketmanager.NewMarketManager(config)\n \n // Create and add markets\n market := marketmanager.NewMarket(\n factoryAddress,\n poolAddress,\n token0Address,\n token1Address,\n fee,\n \"TOKEN0_TOKEN1\",\n \"0x..._0x...\",\n \"UniswapV3\",\n )\n \n manager.AddMarket(market)\n}\n```\n\n### Arbitrage Detection\n\n```go\n// Create arbitrage detector\nminProfit := big.NewInt(10000000000000000) // 0.01 ETH\nminROI := 0.1 // 0.1%\ndetector := marketmanager.NewArbitrageDetector(minProfit, minROI)\n\n// Get markets and detect opportunities\nmarkets, _ := manager.GetMarketsByRawTicker(\"TOKEN0_TOKEN1\")\nopportunities := detector.DetectArbitrageOpportunities(markets)\n\nfor _, opportunity := range opportunities {\n fmt.Printf(\"Arbitrage opportunity: %f%% ROI\\n\", opportunity.ROI)\n}\n```\n\n## Core Concepts\n\n### Market Structure\n\nThe `Market` struct contains all relevant information about a DEX pool:\n\n- **Addresses**: Factory, pool, and token addresses\n- **Fee**: Pool fee in basis points\n- **Ticker**: Formatted token pair symbols\n- **RawTicker**: Formatted token pair addresses\n- **Key**: Unique identifier generated from market parameters\n- **Price Data**: Current price, liquidity, and Uniswap V3 parameters\n- **Metadata**: Status, timestamps, and protocol information\n\n### Market Storage\n\nMarkets are organized in a two-level map structure:\n\n```go\ntype Markets map[string]map[string]*Market // map[rawTicker]map[marketKey]*Market\n```\n\nThis allows efficient retrieval of markets by token pair and unique identification.\n\n### Data Verification\n\nMarket data from the sequencer is initially marked as \"possible\" and then verified against on-chain data within a configurable time window (default 500ms).\n\n### Arbitrage Detection\n\nThe arbitrage detector:\n\n1. Sorts markets by price (lowest to highest)\n2. Checks each combination for profit opportunities\n3. Calculates price impact and gas costs\n4. Validates against minimum profit and ROI thresholds\n\n## Database Integration\n\nThe market manager includes a database adapter for persistent storage:\n\n- **Market Data**: Core market information\n- **Price Data**: Timestamped price and liquidity data with versioning\n- **Arbitrage Opportunities**: Detected opportunities for analysis\n- **Market Events**: Parsed DEX events (swaps, liquidity changes)\n\n## Performance Considerations\n\n- **In-Memory Caching**: Frequently accessed markets are cached for fast retrieval\n- **Batch Operations**: Database operations are batched for efficiency\n- **Connection Pooling**: Database connections are pooled for resource efficiency\n- **Data Eviction**: Old markets are evicted when storage limits are reached\n\n## Testing\n\nThe package includes comprehensive tests for all core functionality:\n\n```bash\ngo test ./pkg/marketmanager/...\n```\n\n## Contributing\n\nContributions are welcome! Please read our contributing guidelines before submitting pull requests.\n\n## License\n\nMIT License

View File

@@ -0,0 +1,215 @@
package marketmanager
import (
"math/big"
"sort"
"time"
"github.com/fraktal/mev-beta/pkg/types"
)
// ArbitrageDetector detects arbitrage opportunities between markets
type ArbitrageDetector struct {
minProfitThreshold *big.Int // Minimum profit threshold in wei
minROIPercentage float64 // Minimum ROI percentage
}
// NewArbitrageDetector creates a new arbitrage detector
func NewArbitrageDetector(minProfitThreshold *big.Int, minROIPercentage float64) *ArbitrageDetector {
return &ArbitrageDetector{
minProfitThreshold: minProfitThreshold,
minROIPercentage: minROIPercentage,
}
}
// MarketOpportunity represents market-specific arbitrage data (extends canonical ArbitrageOpportunity)
type MarketOpportunity struct {
*types.ArbitrageOpportunity
Market1 *Market
Market2 *Market
}
// DetectArbitrageOpportunities detects arbitrage opportunities among markets with the same rawTicker
func (ad *ArbitrageDetector) DetectArbitrageOpportunities(markets map[string]*Market) []*types.ArbitrageOpportunity {
var opportunities []*types.ArbitrageOpportunity
// Convert map to slice for sorting
marketList := make([]*Market, 0, len(markets))
for _, market := range markets {
if market.IsValid() {
marketList = append(marketList, market)
}
}
// Sort markets by price (lowest to highest)
sort.Slice(marketList, func(i, j int) bool {
return marketList[i].Price.Cmp(marketList[j].Price) < 0
})
// Check each combination for arbitrage opportunities
for i := 0; i < len(marketList); i++ {
for j := i + 1; j < len(marketList); j++ {
market1 := marketList[i]
market2 := marketList[j]
// Check if there's an arbitrage opportunity
opportunity := ad.checkArbitrageOpportunity(market1, market2)
if opportunity != nil {
opportunities = append(opportunities, opportunity)
}
}
}
return opportunities
}
// checkArbitrageOpportunity checks if there's an arbitrage opportunity between two markets
func (ad *ArbitrageDetector) checkArbitrageOpportunity(market1, market2 *Market) *types.ArbitrageOpportunity {
// Calculate price difference
priceDiff := new(big.Float).Sub(market2.Price, market1.Price)
if priceDiff.Sign() <= 0 {
return nil // No profit opportunity
}
// Calculate relative price difference (profit margin)
relativeDiff := new(big.Float).Quo(priceDiff, market1.Price)
// Estimate optimal trade size based on liquidity
optimalTradeSize := ad.calculateOptimalTradeSize(market1, market2)
// Calculate price impact on both markets
impact1 := ad.calculatePriceImpact(optimalTradeSize, market1)
impact2 := ad.calculatePriceImpact(optimalTradeSize, market2)
// Adjusted profit after price impact
adjustedRelativeDiff := new(big.Float).Sub(relativeDiff, new(big.Float).Add(impact1, impact2))
if adjustedRelativeDiff.Sign() <= 0 {
return nil // No profit after price impact
}
// Calculate gross profit
grossProfit := new(big.Float).Mul(new(big.Float).SetInt(optimalTradeSize), adjustedRelativeDiff)
// Convert to wei for integer calculations
grossProfitWei := new(big.Int)
grossProfit.Int(grossProfitWei)
// Estimate gas costs
gasCost := ad.estimateGasCost(market1, market2)
// Calculate net profit
netProfit := new(big.Int).Sub(grossProfitWei, gasCost)
// Check if profit meets minimum threshold
if netProfit.Cmp(ad.minProfitThreshold) < 0 {
return nil // Profit too low
}
// Calculate ROI
var roi float64
if optimalTradeSize.Sign() > 0 {
roiFloat := new(big.Float).Quo(new(big.Float).SetInt(netProfit), new(big.Float).SetInt(optimalTradeSize))
roi, _ = roiFloat.Float64()
roi *= 100 // Convert to percentage
}
// Check if ROI meets minimum threshold
if roi < ad.minROIPercentage {
return nil // ROI too low
}
// Create canonical arbitrage opportunity
return &types.ArbitrageOpportunity{
Path: []string{market1.Token0.Hex(), market1.Token1.Hex()},
Pools: []string{"market1-pool", "market2-pool"},
AmountIn: optimalTradeSize,
Profit: netProfit,
NetProfit: netProfit,
GasEstimate: gasCost,
ROI: roi,
Protocol: "market-arbitrage",
ExecutionTime: 5000, // 5 seconds
Confidence: 0.9, // High confidence for market data
PriceImpact: 0.01, // 1% estimated
MaxSlippage: 0.02, // 2% max slippage
TokenIn: market1.Token0,
TokenOut: market1.Token1,
Timestamp: time.Now().Unix(),
Risk: 0.1, // Low risk for market arbitrage
}
}
// calculateOptimalTradeSize calculates the optimal trade size for maximum profit
func (ad *ArbitrageDetector) calculateOptimalTradeSize(market1, market2 *Market) *big.Int {
// Use a simple approach: 1% of the smaller liquidity
liquidity1 := market1.Liquidity
liquidity2 := market2.Liquidity
minLiquidity := liquidity1
if liquidity2.Cmp(liquidity1) < 0 {
minLiquidity = liquidity2
}
// Calculate 1% of minimum liquidity
optimalSize := new(big.Int).Div(minLiquidity, big.NewInt(100))
// Ensure minimum trade size (0.001 ETH)
minTradeSize := big.NewInt(1000000000000000) // 0.001 ETH in wei
if optimalSize.Cmp(minTradeSize) < 0 {
optimalSize = minTradeSize
}
// Ensure maximum trade size (10 ETH to avoid overflow)
maxTradeSize := new(big.Int).SetInt64(1000000000000000000) // 1 ETH in wei
maxTradeSize.Mul(maxTradeSize, big.NewInt(10)) // 10 ETH
if optimalSize.Cmp(maxTradeSize) > 0 {
optimalSize = maxTradeSize
}
return optimalSize
}
// calculatePriceImpact calculates the price impact for a given trade size
func (ad *ArbitrageDetector) calculatePriceImpact(tradeSize *big.Int, market *Market) *big.Float {
if market.Liquidity.Sign() == 0 {
return big.NewFloat(0.01) // 1% default impact for unknown liquidity
}
// Calculate utilization ratio
utilizationRatio := new(big.Float).Quo(new(big.Float).SetInt(tradeSize), new(big.Float).SetInt(market.Liquidity))
// Apply quadratic model for impact: utilizationRatio * (1 + utilizationRatio)
impact := new(big.Float).Mul(utilizationRatio, new(big.Float).Add(big.NewFloat(1), utilizationRatio))
// Cap impact at 10%
if impact.Cmp(big.NewFloat(0.1)) > 0 {
impact = big.NewFloat(0.1)
}
return impact
}
// estimateGasCost estimates the gas cost for an arbitrage transaction
func (ad *ArbitrageDetector) estimateGasCost(market1, market2 *Market) *big.Int {
// Base gas costs for different operations
baseGas := big.NewInt(250000) // Base gas for arbitrage transaction
// Get current gas price (simplified - in production would fetch from network)
gasPrice := big.NewInt(2000000000) // 2 gwei base
// Add priority fee for MEV transactions
priorityFee := big.NewInt(5000000000) // 5 gwei priority
totalGasPrice := new(big.Int).Add(gasPrice, priorityFee)
// Calculate total gas cost
gasCost := new(big.Int).Mul(baseGas, totalGasPrice)
return gasCost
}
// GetFeePercentage calculates the total fee percentage for two markets
func (ad *ArbitrageDetector) GetFeePercentage(market1, market2 *Market) float64 {
fee1 := market1.GetFeePercentage()
fee2 := market2.GetFeePercentage()
return fee1 + fee2
}

View File

@@ -0,0 +1,254 @@
package marketmanager
import (
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
)
func TestArbitrageDetectorCreation(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 1.0 // 1%
detector := NewArbitrageDetector(minProfit, minROI)
if detector.minProfitThreshold.Cmp(minProfit) != 0 {
t.Errorf("Expected minProfitThreshold %v, got %v", minProfit, detector.minProfitThreshold)
}
if detector.minROIPercentage != minROI {
t.Errorf("Expected minROIPercentage %f, got %f", minROI, detector.minROIPercentage)
}
}
func TestArbitrageDetectionNoOpportunity(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 1.0 // 1%
detector := NewArbitrageDetector(minProfit, minROI)
// Create two markets with the same price (no arbitrage opportunity)
market1 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2000.0),
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2505414483750470000),
Tick: 200000,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
}
market2 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2000.0), // Same price
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2505414483750470000),
Tick: 200000,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
}
markets := map[string]*Market{
"market1": market1,
"market2": market2,
}
opportunities := detector.DetectArbitrageOpportunities(markets)
if len(opportunities) != 0 {
t.Errorf("Expected 0 opportunities, got %d", len(opportunities))
}
}
func TestArbitrageDetectionWithOpportunity(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 0.1 // 0.1%
detector := NewArbitrageDetector(minProfit, minROI)
// Create two markets with different prices (arbitrage opportunity)
market1 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2000.0),
Liquidity: new(big.Int).Mul(big.NewInt(1000000000000000000), big.NewInt(10)), // 10 ETH - more liquidity for better profit
SqrtPriceX96: big.NewInt(2505414483750470000),
Tick: 200000,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
Key: "market1",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
}
market2 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2100.0), // 5% higher price
Liquidity: new(big.Int).Mul(big.NewInt(1000000000000000000), big.NewInt(10)), // 10 ETH - more liquidity for better profit
SqrtPriceX96: big.NewInt(2568049845844280000),
Tick: 205000,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
Key: "market2",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
}
markets := map[string]*Market{
"market1": market1,
"market2": market2,
}
opportunities := detector.DetectArbitrageOpportunities(markets)
// Print some debug information
fmt.Printf("Found %d opportunities\n", len(opportunities))
if len(opportunities) > 0 {
fmt.Printf("Opportunity ROI: %f\n", opportunities[0].ROI)
fmt.Printf("Opportunity Profit: %s\n", opportunities[0].Profit.String())
}
// We should find at least one opportunity
// Note: This test might fail if the profit calculation doesn't meet thresholds
// That's okay for now, we're just testing that the code runs without errors
}
func TestArbitrageDetectionBelowThreshold(t *testing.T) {
minProfit := big.NewInt(1000000000000000000) // 1 ETH (high threshold)
minROI := 10.0 // 10% (high threshold)
detector := NewArbitrageDetector(minProfit, minROI)
// Create two markets with a small price difference
market1 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2000.0),
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2505414483750470000),
Tick: 200000,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
}
market2 := &Market{
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Price: big.NewFloat(2001.0), // Very small price difference
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2506667190854350000),
Tick: 200050,
Status: StatusConfirmed,
Fee: 3000, // 0.3%
}
markets := map[string]*Market{
"market1": market1,
"market2": market2,
}
opportunities := detector.DetectArbitrageOpportunities(markets)
// With high thresholds, we should find no opportunities
if len(opportunities) != 0 {
t.Errorf("Expected 0 opportunities due to high thresholds, got %d", len(opportunities))
}
}
func TestCalculateOptimalTradeSize(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 1.0 // 1%
detector := NewArbitrageDetector(minProfit, minROI)
market1 := &Market{
Liquidity: big.NewInt(1000000000000000000), // 1 ETH
}
market2 := &Market{
Liquidity: big.NewInt(500000000000000000), // 0.5 ETH
}
// Test optimal trade size calculation
optimalSize := detector.calculateOptimalTradeSize(market1, market2)
// Should be 1% of the smaller liquidity (0.5 ETH * 0.01 = 0.005 ETH)
expected := big.NewInt(5000000000000000) // 0.005 ETH in wei
if optimalSize.Cmp(expected) != 0 {
t.Errorf("Expected optimal size %v, got %v", expected, optimalSize)
}
// Test with very small liquidity
market3 := &Market{
Liquidity: big.NewInt(100000000000000000), // 0.1 ETH
}
market4 := &Market{
Liquidity: big.NewInt(200000000000000000), // 0.2 ETH
}
optimalSize = detector.calculateOptimalTradeSize(market3, market4)
// Should be minimum trade size (0.001 ETH) since 1% of 0.1 ETH is 0.001 ETH
minTradeSize := big.NewInt(1000000000000000) // 0.001 ETH in wei
if optimalSize.Cmp(minTradeSize) != 0 {
t.Errorf("Expected minimum trade size %v, got %v", minTradeSize, optimalSize)
}
}
func TestCalculatePriceImpact(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 1.0 // 1%
detector := NewArbitrageDetector(minProfit, minROI)
market := &Market{
Liquidity: big.NewInt(1000000000000000000), // 1 ETH
}
// Test price impact calculation
tradeSize := big.NewInt(100000000000000000) // 0.1 ETH
impact := detector.calculatePriceImpact(tradeSize, market)
// 0.1 ETH / 1 ETH = 0.1 (10%) utilization
// Impact should be 0.1 * (1 + 0.1) = 0.11 (11%)
// But we cap at 10% so it should be 0.1
expected := 0.1
actual, _ := impact.Float64()
// Allow for small floating point differences
if actual < expected*0.99 || actual > expected*1.01 {
t.Errorf("Expected impact ~%f, got %f", expected, actual)
}
// Test with zero liquidity (should return default impact)
marketZero := &Market{
Liquidity: big.NewInt(0),
}
impact = detector.calculatePriceImpact(tradeSize, marketZero)
expectedDefault := 0.01 // 1% default
actual, _ = impact.Float64()
if actual != expectedDefault {
t.Errorf("Expected default impact %f, got %f", expectedDefault, actual)
}
}
func TestEstimateGasCost(t *testing.T) {
minProfit := big.NewInt(10000000000000000) // 0.01 ETH
minROI := 1.0 // 1%
detector := NewArbitrageDetector(minProfit, minROI)
market1 := &Market{}
market2 := &Market{}
// Test gas cost estimation
gasCost := detector.estimateGasCost(market1, market2)
// Base gas (250000) * (2 gwei + 5 gwei priority) = 250000 * 7 gwei
// 250000 * 7000000000 = 1750000000000000 wei = 0.00175 ETH
expected := big.NewInt(1750000000000000)
if gasCost.Cmp(expected) != 0 {
t.Errorf("Expected gas cost %v, got %v", expected, gasCost)
}
}

View File

@@ -0,0 +1,353 @@
package marketmanager
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
_ "github.com/lib/pq" // PostgreSQL driver
)
// DatabaseAdapter handles persistence of market data
type DatabaseAdapter struct {
db *sql.DB
}
// NewDatabaseAdapter creates a new database adapter
func NewDatabaseAdapter(connectionString string) (*DatabaseAdapter, error) {
db, err := sql.Open("postgres", connectionString)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
// Test the connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &DatabaseAdapter{db: db}, nil
}
// InitializeSchema creates the necessary tables if they don't exist
func (da *DatabaseAdapter) InitializeSchema() error {
schema := `
CREATE TABLE IF NOT EXISTS markets (
key VARCHAR(66) PRIMARY KEY,
factory_address VARCHAR(42) NOT NULL,
pool_address VARCHAR(42) NOT NULL,
token0_address VARCHAR(42) NOT NULL,
token1_address VARCHAR(42) NOT NULL,
fee INTEGER NOT NULL,
ticker VARCHAR(50) NOT NULL,
raw_ticker VARCHAR(90) NOT NULL,
protocol VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS market_data (
id SERIAL PRIMARY KEY,
market_key VARCHAR(66) NOT NULL REFERENCES markets(key) ON DELETE CASCADE,
price NUMERIC NOT NULL,
liquidity NUMERIC NOT NULL,
sqrt_price_x96 NUMERIC,
tick INTEGER,
status VARCHAR(20) NOT NULL,
timestamp BIGINT NOT NULL,
block_number BIGINT NOT NULL,
tx_hash VARCHAR(66) NOT NULL,
source VARCHAR(10) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_market_data_market_key_timestamp ON market_data(market_key, timestamp);
CREATE INDEX IF NOT EXISTS idx_market_data_status ON market_data(status);
CREATE INDEX IF NOT EXISTS idx_market_data_block_number ON market_data(block_number);
CREATE TABLE IF NOT EXISTS arbitrage_opportunities (
id SERIAL PRIMARY KEY,
market_key_1 VARCHAR(66) NOT NULL REFERENCES markets(key) ON DELETE CASCADE,
market_key_2 VARCHAR(66) NOT NULL REFERENCES markets(key) ON DELETE CASCADE,
path TEXT NOT NULL,
profit NUMERIC NOT NULL,
gas_estimate NUMERIC NOT NULL,
roi DECIMAL(10, 6) NOT NULL,
status VARCHAR(20) NOT NULL,
detection_timestamp BIGINT NOT NULL,
execution_timestamp BIGINT,
tx_hash VARCHAR(66),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_arbitrage_opportunities_detection_timestamp ON arbitrage_opportunities(detection_timestamp);
CREATE INDEX IF NOT EXISTS idx_arbitrage_opportunities_status ON arbitrage_opportunities(status);
CREATE TABLE IF NOT EXISTS market_events (
id SERIAL PRIMARY KEY,
market_key VARCHAR(66) NOT NULL REFERENCES markets(key) ON DELETE CASCADE,
event_type VARCHAR(20) NOT NULL,
amount0 NUMERIC,
amount1 NUMERIC,
transaction_hash VARCHAR(66) NOT NULL,
block_number BIGINT NOT NULL,
log_index INTEGER NOT NULL,
timestamp BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_market_events_market_key_timestamp ON market_events(market_key, timestamp);
CREATE INDEX IF NOT EXISTS idx_market_events_event_type ON market_events(event_type);
CREATE INDEX IF NOT EXISTS idx_market_events_block_number ON market_events(block_number);
`
_, err := da.db.Exec(schema)
return err
}
// SaveMarket saves a market to the database
func (da *DatabaseAdapter) SaveMarket(ctx context.Context, market *Market) error {
query := `
INSERT INTO markets (
key, factory_address, pool_address, token0_address, token1_address,
fee, ticker, raw_ticker, protocol, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO UPDATE SET
factory_address = EXCLUDED.factory_address,
pool_address = EXCLUDED.pool_address,
token0_address = EXCLUDED.token0_address,
token1_address = EXCLUDED.token1_address,
fee = EXCLUDED.fee,
ticker = EXCLUDED.ticker,
raw_ticker = EXCLUDED.raw_ticker,
protocol = EXCLUDED.protocol,
updated_at = CURRENT_TIMESTAMP
`
_, err := da.db.ExecContext(ctx, query,
market.Key,
market.Factory.Hex(),
market.PoolAddress.Hex(),
market.Token0.Hex(),
market.Token1.Hex(),
market.Fee,
market.Ticker,
market.RawTicker,
market.Protocol,
)
return err
}
// SaveMarketData saves market data to the database
func (da *DatabaseAdapter) SaveMarketData(ctx context.Context, market *Market, source string) error {
query := `
INSERT INTO market_data (
market_key, price, liquidity, sqrt_price_x96, tick,
status, timestamp, block_number, tx_hash, source, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`
// Convert big.Float to string for storage
priceStr := "0"
if market.Price != nil {
priceStr = market.Price.Text('f', -1)
}
// Convert big.Int to string for storage
liquidityStr := "0"
if market.Liquidity != nil {
liquidityStr = market.Liquidity.String()
}
sqrtPriceStr := "0"
if market.SqrtPriceX96 != nil {
sqrtPriceStr = market.SqrtPriceX96.String()
}
_, err := da.db.ExecContext(ctx, query,
market.Key,
priceStr,
liquidityStr,
sqrtPriceStr,
market.Tick,
string(market.Status),
market.Timestamp,
market.BlockNumber,
market.TxHash.Hex(),
source,
)
return err
}
// GetMarket retrieves a market from the database
func (da *DatabaseAdapter) GetMarket(ctx context.Context, key string) (*Market, error) {
query := `
SELECT key, factory_address, pool_address, token0_address, token1_address,
fee, ticker, raw_ticker, protocol
FROM markets
WHERE key = $1
`
row := da.db.QueryRowContext(ctx, query, key)
var market Market
var factoryAddr, poolAddr, token0Addr, token1Addr string
err := row.Scan(
&market.Key,
&factoryAddr,
&poolAddr,
&token0Addr,
&token1Addr,
&market.Fee,
&market.Ticker,
&market.RawTicker,
&market.Protocol,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("market not found: %w", err)
}
return nil, fmt.Errorf("failed to query market: %w", err)
}
// Convert string addresses back to common.Address
market.Factory = common.HexToAddress(factoryAddr)
market.PoolAddress = common.HexToAddress(poolAddr)
market.Token0 = common.HexToAddress(token0Addr)
market.Token1 = common.HexToAddress(token1Addr)
// Initialize price data
market.Price = big.NewFloat(0)
market.Liquidity = big.NewInt(0)
market.SqrtPriceX96 = big.NewInt(0)
return &market, nil
}
// GetLatestMarketData retrieves the latest market data from the database
func (da *DatabaseAdapter) GetLatestMarketData(ctx context.Context, marketKey string) (*Market, error) {
query := `
SELECT price, liquidity, sqrt_price_x96, tick, status, timestamp, block_number, tx_hash
FROM market_data
WHERE market_key = $1
ORDER BY timestamp DESC
LIMIT 1
`
row := da.db.QueryRowContext(ctx, query, marketKey)
var priceStr, liquidityStr, sqrtPriceStr string
var market Market
err := row.Scan(
&priceStr,
&liquidityStr,
&sqrtPriceStr,
&market.Tick,
&market.Status,
&market.Timestamp,
&market.BlockNumber,
&market.TxHash,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("no market data found: %w", err)
}
return nil, fmt.Errorf("failed to query market data: %w", err)
}
// Convert strings back to big numbers
if priceStr != "" {
if price, ok := new(big.Float).SetString(priceStr); ok {
market.Price = price
}
}
if liquidityStr != "" {
if liquidity, ok := new(big.Int).SetString(liquidityStr, 10); ok {
market.Liquidity = liquidity
}
}
if sqrtPriceStr != "" {
if sqrtPrice, ok := new(big.Int).SetString(sqrtPriceStr, 10); ok {
market.SqrtPriceX96 = sqrtPrice
}
}
return &market, nil
}
// SaveArbitrageOpportunity saves an arbitrage opportunity to the database
func (da *DatabaseAdapter) SaveArbitrageOpportunity(ctx context.Context, opportunity *DatabaseArbitrageOpportunity) error {
// Serialize path to JSON
pathJSON, err := json.Marshal(opportunity.Path)
if err != nil {
return fmt.Errorf("failed to serialize path: %w", err)
}
query := `
INSERT INTO arbitrage_opportunities (
market_key_1, market_key_2, path, profit, gas_estimate, roi,
status, detection_timestamp, execution_timestamp, tx_hash,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`
profitStr := "0"
if opportunity.Profit != nil {
profitStr = opportunity.Profit.String()
}
gasEstimateStr := "0"
if opportunity.GasEstimate != nil {
gasEstimateStr = opportunity.GasEstimate.String()
}
_, err = da.db.ExecContext(ctx, query,
opportunity.MarketKey1,
opportunity.MarketKey2,
string(pathJSON),
profitStr,
gasEstimateStr,
opportunity.ROI,
string(opportunity.Status),
opportunity.DetectionTimestamp,
opportunity.ExecutionTimestamp,
opportunity.TxHash,
)
return err
}
// Close closes the database connection
func (da *DatabaseAdapter) Close() error {
return da.db.Close()
}
// DatabaseArbitrageOpportunity represents a detected arbitrage opportunity for database storage
type DatabaseArbitrageOpportunity struct {
MarketKey1 string
MarketKey2 string
Path []string
Profit *big.Int
GasEstimate *big.Int
ROI float64
Status string
DetectionTimestamp int64
ExecutionTimestamp int64
TxHash string
}

View File

@@ -0,0 +1,267 @@
package marketmanager
import (
"context"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/ethclient"
)
// MarketManager handles market data collection, storage, and retrieval
type MarketManager struct {
// In-memory storage
markets Markets
mutex sync.RWMutex
// Ethereum client for on-chain verification
client *ethclient.Client
// Configuration
verificationWindow time.Duration // Time window for on-chain verification
maxMarkets int // Maximum number of markets to store
}
// MarketManagerConfig holds configuration for the MarketManager
type MarketManagerConfig struct {
EthereumClient *ethclient.Client
VerificationWindow time.Duration
MaxMarkets int
}
// NewMarketManager creates a new MarketManager instance
func NewMarketManager(config *MarketManagerConfig) *MarketManager {
if config.VerificationWindow == 0 {
config.VerificationWindow = 500 * time.Millisecond // Default 500ms
}
if config.MaxMarkets == 0 {
config.MaxMarkets = 10000 // Default 10,000 markets
}
return &MarketManager{
markets: make(Markets),
client: config.EthereumClient,
verificationWindow: config.VerificationWindow,
maxMarkets: config.MaxMarkets,
}
}
// AddMarket adds a new market to the manager
func (mm *MarketManager) AddMarket(market *Market) error {
mm.mutex.Lock()
defer mm.mutex.Unlock()
// Check if we need to evict old markets
if len(mm.markets) >= mm.maxMarkets {
mm.evictOldestMarkets()
}
// Initialize the rawTicker map if it doesn't exist
if mm.markets[market.RawTicker] == nil {
mm.markets[market.RawTicker] = make(map[string]*Market)
}
// Add the market
mm.markets[market.RawTicker][market.Key] = market
return nil
}
// GetMarket retrieves a market by rawTicker and marketKey
func (mm *MarketManager) GetMarket(rawTicker, marketKey string) (*Market, error) {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
if marketsForTicker, exists := mm.markets[rawTicker]; exists {
if market, exists := marketsForTicker[marketKey]; exists {
return market, nil
}
}
return nil, fmt.Errorf("market not found for rawTicker: %s, marketKey: %s", rawTicker, marketKey)
}
// GetMarketsByRawTicker retrieves all markets for a given rawTicker
func (mm *MarketManager) GetMarketsByRawTicker(rawTicker string) (map[string]*Market, error) {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
if markets, exists := mm.markets[rawTicker]; exists {
// Return a copy to avoid external modification
result := make(map[string]*Market)
for key, market := range markets {
result[key] = market.Clone()
}
return result, nil
}
return nil, fmt.Errorf("no markets found for rawTicker: %s", rawTicker)
}
// GetAllMarkets retrieves all markets
func (mm *MarketManager) GetAllMarkets() Markets {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
// Return a deep copy to avoid external modification
result := make(Markets)
for rawTicker, markets := range mm.markets {
result[rawTicker] = make(map[string]*Market)
for key, market := range markets {
result[rawTicker][key] = market.Clone()
}
}
return result
}
// UpdateMarket updates an existing market
func (mm *MarketManager) UpdateMarket(market *Market) error {
mm.mutex.Lock()
defer mm.mutex.Unlock()
if mm.markets[market.RawTicker] == nil {
return fmt.Errorf("no markets found for rawTicker: %s", market.RawTicker)
}
if _, exists := mm.markets[market.RawTicker][market.Key]; !exists {
return fmt.Errorf("market not found for rawTicker: %s, marketKey: %s", market.RawTicker, market.Key)
}
// Update the market
mm.markets[market.RawTicker][market.Key] = market
return nil
}
// RemoveMarket removes a market by rawTicker and marketKey
func (mm *MarketManager) RemoveMarket(rawTicker, marketKey string) error {
mm.mutex.Lock()
defer mm.mutex.Unlock()
if mm.markets[rawTicker] == nil {
return fmt.Errorf("no markets found for rawTicker: %s", rawTicker)
}
if _, exists := mm.markets[rawTicker][marketKey]; !exists {
return fmt.Errorf("market not found for rawTicker: %s, marketKey: %s", rawTicker, marketKey)
}
delete(mm.markets[rawTicker], marketKey)
// Clean up empty rawTicker maps
if len(mm.markets[rawTicker]) == 0 {
delete(mm.markets, rawTicker)
}
return nil
}
// VerifyMarket verifies a market's transaction on-chain
func (mm *MarketManager) VerifyMarket(ctx context.Context, market *Market) (bool, error) {
if mm.client == nil {
return false, fmt.Errorf("ethereum client not configured")
}
// Check if the transaction exists on-chain
_, err := mm.client.TransactionReceipt(ctx, market.TxHash)
if err != nil {
return false, nil // Transaction not found, but not an error
}
// Transaction exists, market is confirmed
return true, nil
}
// ScheduleVerification schedules verification of a market within the verification window
func (mm *MarketManager) ScheduleVerification(market *Market) {
go func() {
// Wait for the verification window
time.Sleep(mm.verificationWindow)
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Verify the market
confirmed, err := mm.VerifyMarket(ctx, market)
if err != nil {
// Log error but don't fail
fmt.Printf("Error verifying market %s: %v\n", market.Key, err)
return
}
if confirmed {
// Update market status to confirmed
market.Status = StatusConfirmed
// Update the market in storage
mm.mutex.Lock()
if mm.markets[market.RawTicker] != nil {
if existingMarket, exists := mm.markets[market.RawTicker][market.Key]; exists {
existingMarket.Status = StatusConfirmed
existingMarket.Timestamp = time.Now().Unix()
}
}
mm.mutex.Unlock()
} else {
// Mark as invalid if not confirmed
mm.mutex.Lock()
if mm.markets[market.RawTicker] != nil {
if existingMarket, exists := mm.markets[market.RawTicker][market.Key]; exists {
existingMarket.Status = StatusInvalid
}
}
mm.mutex.Unlock()
}
}()
}
// GetMarketCount returns the total number of markets
func (mm *MarketManager) GetMarketCount() int {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
count := 0
for _, markets := range mm.markets {
count += len(markets)
}
return count
}
// GetRawTickerCount returns the number of unique rawTickers
func (mm *MarketManager) GetRawTickerCount() int {
mm.mutex.RLock()
defer mm.mutex.RUnlock()
return len(mm.markets)
}
// evictOldestMarkets removes the oldest markets when the limit is reached
func (mm *MarketManager) evictOldestMarkets() {
// This is a simple implementation that removes the first rawTicker
// A more sophisticated implementation might remove based on last access time
for rawTicker := range mm.markets {
delete(mm.markets, rawTicker)
break // Remove just one to make space
}
}
// GetValidMarketsByRawTicker retrieves all valid markets for a given rawTicker
func (mm *MarketManager) GetValidMarketsByRawTicker(rawTicker string) (map[string]*Market, error) {
markets, err := mm.GetMarketsByRawTicker(rawTicker)
if err != nil {
return nil, err
}
validMarkets := make(map[string]*Market)
for key, market := range markets {
if market.IsValid() {
validMarkets[key] = market
}
}
return validMarkets, nil
}

View File

@@ -0,0 +1,288 @@
package marketmanager
import (
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
)
func TestMarketManagerCreation(t *testing.T) {
config := &MarketManagerConfig{
VerificationWindow: 500 * time.Millisecond,
MaxMarkets: 1000,
}
manager := NewMarketManager(config)
if manager == nil {
t.Error("Expected MarketManager to be created")
}
if manager.verificationWindow != 500*time.Millisecond {
t.Errorf("Expected verificationWindow 500ms, got %v", manager.verificationWindow)
}
if manager.maxMarkets != 1000 {
t.Errorf("Expected maxMarkets 1000, got %d", manager.maxMarkets)
}
}
func TestMarketManagerAddAndGetMarket(t *testing.T) {
manager := NewMarketManager(&MarketManagerConfig{})
market := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
Key: "test_key",
Price: big.NewFloat(2000.5),
Protocol: "UniswapV3",
}
// Add market
err := manager.AddMarket(market)
if err != nil {
t.Errorf("Expected no error when adding market, got %v", err)
}
// Get market
retrievedMarket, err := manager.GetMarket(market.RawTicker, market.Key)
if err != nil {
t.Errorf("Expected no error when getting market, got %v", err)
}
if retrievedMarket.Ticker != market.Ticker {
t.Errorf("Expected ticker %s, got %s", market.Ticker, retrievedMarket.Ticker)
}
// Try to get non-existent market
_, err = manager.GetMarket("non_existent", "non_existent")
if err == nil {
t.Error("Expected error when getting non-existent market")
}
}
func TestMarketManagerGetMarketsByRawTicker(t *testing.T) {
manager := NewMarketManager(&MarketManagerConfig{})
// Add multiple markets with the same rawTicker
rawTicker := "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
market1 := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH_3000",
RawTicker: rawTicker,
Key: "test_key_1",
Price: big.NewFloat(2000.5),
Protocol: "UniswapV3",
}
market2 := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x7BeA39867e4169DBe237d55C8242a8f2fDcD53F0"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 500,
Ticker: "USDC_WETH_500",
RawTicker: rawTicker,
Key: "test_key_2",
Price: big.NewFloat(2001.0),
Protocol: "UniswapV3",
}
// Add markets
manager.AddMarket(market1)
manager.AddMarket(market2)
// Get markets by rawTicker
markets, err := manager.GetMarketsByRawTicker(rawTicker)
if err != nil {
t.Errorf("Expected no error when getting markets by rawTicker, got %v", err)
}
if len(markets) != 2 {
t.Errorf("Expected 2 markets, got %d", len(markets))
}
if markets[market1.Key].Ticker != market1.Ticker {
t.Errorf("Expected ticker %s, got %s", market1.Ticker, markets[market1.Key].Ticker)
}
if markets[market2.Key].Ticker != market2.Ticker {
t.Errorf("Expected ticker %s, got %s", market2.Ticker, markets[market2.Key].Ticker)
}
// Try to get markets for non-existent rawTicker
_, err = manager.GetMarketsByRawTicker("non_existent")
if err == nil {
t.Error("Expected error when getting markets for non-existent rawTicker")
}
}
func TestMarketManagerUpdateMarket(t *testing.T) {
manager := NewMarketManager(&MarketManagerConfig{})
market := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
Key: "test_key",
Price: big.NewFloat(2000.5),
Protocol: "UniswapV3",
}
// Add market
manager.AddMarket(market)
// Update market price
newPrice := big.NewFloat(2100.0)
market.Price = newPrice
// Update market
err := manager.UpdateMarket(market)
if err != nil {
t.Errorf("Expected no error when updating market, got %v", err)
}
// Get updated market
updatedMarket, err := manager.GetMarket(market.RawTicker, market.Key)
if err != nil {
t.Errorf("Expected no error when getting updated market, got %v", err)
}
if updatedMarket.Price.Cmp(newPrice) != 0 {
t.Errorf("Expected price %v, got %v", newPrice, updatedMarket.Price)
}
// Try to update non-existent market
nonExistentMarket := &Market{
RawTicker: "non_existent",
Key: "non_existent",
}
err = manager.UpdateMarket(nonExistentMarket)
if err == nil {
t.Error("Expected error when updating non-existent market")
}
}
func TestMarketManagerRemoveMarket(t *testing.T) {
manager := NewMarketManager(&MarketManagerConfig{})
market := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
Key: "test_key",
Price: big.NewFloat(2000.5),
Protocol: "UniswapV3",
}
// Add market
manager.AddMarket(market)
// Remove market
err := manager.RemoveMarket(market.RawTicker, market.Key)
if err != nil {
t.Errorf("Expected no error when removing market, got %v", err)
}
// Try to get removed market
_, err = manager.GetMarket(market.RawTicker, market.Key)
if err == nil {
t.Error("Expected error when getting removed market")
}
// Try to remove non-existent market
err = manager.RemoveMarket("non_existent", "non_existent")
if err == nil {
t.Error("Expected error when removing non-existent market")
}
}
func TestMarketManagerGetCounts(t *testing.T) {
manager := NewMarketManager(&MarketManagerConfig{})
// Initially should be zero
if manager.GetMarketCount() != 0 {
t.Errorf("Expected market count 0, got %d", manager.GetMarketCount())
}
if manager.GetRawTickerCount() != 0 {
t.Errorf("Expected raw ticker count 0, got %d", manager.GetRawTickerCount())
}
// Add markets
rawTicker1 := "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
rawTicker2 := "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" // USDC_WBTC
market1 := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH_3000",
RawTicker: rawTicker1,
Key: "test_key_1",
Price: big.NewFloat(2000.5),
Protocol: "UniswapV3",
}
market2 := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x7BeA39867e4169DBe237d55C8242a8f2fDcD53F0"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 500,
Ticker: "USDC_WETH_500",
RawTicker: rawTicker1,
Key: "test_key_2",
Price: big.NewFloat(2001.0),
Protocol: "UniswapV3",
}
market3 := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"),
Fee: 3000,
Ticker: "USDC_WBTC",
RawTicker: rawTicker2,
Key: "test_key_3",
Price: big.NewFloat(50000.0),
Protocol: "UniswapV3",
}
manager.AddMarket(market1)
manager.AddMarket(market2)
manager.AddMarket(market3)
// Check counts
if manager.GetMarketCount() != 3 {
t.Errorf("Expected market count 3, got %d", manager.GetMarketCount())
}
if manager.GetRawTickerCount() != 2 {
t.Errorf("Expected raw ticker count 2, got %d", manager.GetRawTickerCount())
}
}

View File

@@ -0,0 +1,148 @@
package marketmanager
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// Market represents a DEX pool with its associated data
type Market struct {
Factory common.Address `json:"factory"` // DEX factory contract address
PoolAddress common.Address `json:"poolAddress"` // Pool contract address
Token0 common.Address `json:"token0"` // First token in pair
Token1 common.Address `json:"token1"` // Second token in pair
Fee uint32 `json:"fee"` // Pool fee (e.g., 500 for 0.05%)
Ticker string `json:"ticker"` // Formatted as <symbol>_<symbol> (e.g., "WETH_USDC")
RawTicker string `json:"rawTicker"` // Formatted as <token0>_<token1> (e.g., "0x..._0x...")
Key string `json:"key"` // <keccak256ofToken0Token1FeeFactoryPoolAddress>
// Price and liquidity data
Price *big.Float `json:"price"` // Current price of token1/token0
Liquidity *big.Int `json:"liquidity"` // Current liquidity in the pool
SqrtPriceX96 *big.Int `json:"sqrtPriceX96"` // sqrtPriceX96 from Uniswap V3
Tick int32 `json:"tick"` // Current tick from Uniswap V3
// Status and metadata
Status MarketStatus `json:"status"` // Status of the market data
Timestamp int64 `json:"timestamp"` // Last update timestamp
BlockNumber uint64 `json:"blockNumber"` // Block number of last update
TxHash common.Hash `json:"txHash"` // Transaction hash of last update
Protocol string `json:"protocol"` // DEX protocol (UniswapV2, UniswapV3, etc.)
}
// MarketStatus represents the verification status of market data
type MarketStatus string
const (
StatusPossible MarketStatus = "possible" // Data from sequencer, not yet verified
StatusConfirmed MarketStatus = "confirmed" // Data verified on-chain
StatusStale MarketStatus = "stale" // Data older than threshold
StatusInvalid MarketStatus = "invalid" // Data deemed invalid
)
// Markets represents a collection of markets organized by rawTicker and marketKey
type Markets map[string]map[string]*Market // map[rawTicker]map[marketKey]*Market
// NewMarket creates a new Market instance with proper initialization
func NewMarket(
factory, poolAddress, token0, token1 common.Address,
fee uint32,
ticker, rawTicker, protocol string,
) *Market {
// Generate the market key using keccak256
key := generateMarketKey(factory, poolAddress, token0, token1, fee)
return &Market{
Factory: factory,
PoolAddress: poolAddress,
Token0: token0,
Token1: token1,
Fee: fee,
Ticker: ticker,
RawTicker: rawTicker,
Key: key,
Price: big.NewFloat(0),
Liquidity: big.NewInt(0),
SqrtPriceX96: big.NewInt(0),
Tick: 0,
Status: StatusPossible,
Timestamp: 0,
BlockNumber: 0,
TxHash: common.Hash{},
Protocol: protocol,
}
}
// generateMarketKey creates a unique key for a market using keccak256
func generateMarketKey(factory, poolAddress, token0, token1 common.Address, fee uint32) string {
// Concatenate all relevant fields
data := fmt.Sprintf("%s%s%s%s%d%s",
factory.Hex(),
poolAddress.Hex(),
token0.Hex(),
token1.Hex(),
fee,
poolAddress.Hex()) // Include poolAddress again for uniqueness
// Generate keccak256 hash
hash := crypto.Keccak256([]byte(data))
return common.Bytes2Hex(hash)
}
// generateRawTicker creates a raw ticker string from two token addresses
func GenerateRawTicker(token0, token1 common.Address) string {
return fmt.Sprintf("%s_%s", token0.Hex(), token1.Hex())
}
// generateTicker creates a formatted ticker string from token symbols
// This would typically require a token registry to resolve symbols
func GenerateTicker(token0Symbol, token1Symbol string) string {
return fmt.Sprintf("%s_%s", token0Symbol, token1Symbol)
}
// UpdatePriceData updates the price-related fields of a market
func (m *Market) UpdatePriceData(price *big.Float, liquidity, sqrtPriceX96 *big.Int, tick int32) {
m.Price = price
m.Liquidity = liquidity
m.SqrtPriceX96 = sqrtPriceX96
m.Tick = tick
}
// UpdateMetadata updates the metadata fields of a market
func (m *Market) UpdateMetadata(timestamp int64, blockNumber uint64, txHash common.Hash, status MarketStatus) {
m.Timestamp = timestamp
m.BlockNumber = blockNumber
m.TxHash = txHash
m.Status = status
}
// IsValid checks if the market data is valid for arbitrage calculations
func (m *Market) IsValid() bool {
return m.Status == StatusConfirmed &&
m.Price.Sign() > 0 &&
m.Liquidity.Sign() > 0 &&
m.SqrtPriceX96.Sign() > 0
}
// Clone creates a deep copy of the market
func (m *Market) Clone() *Market {
clone := *m
if m.Price != nil {
clone.Price = new(big.Float).Copy(m.Price)
}
if m.Liquidity != nil {
clone.Liquidity = new(big.Int).Set(m.Liquidity)
}
if m.SqrtPriceX96 != nil {
clone.SqrtPriceX96 = new(big.Int).Set(m.SqrtPriceX96)
}
return &clone
}
// GetFeePercentage returns the fee as a percentage
func (m *Market) GetFeePercentage() float64 {
return float64(m.Fee) / 10000.0 // Convert basis points to percentage
}

View File

@@ -0,0 +1,205 @@
package marketmanager
import (
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
)
func TestMarketCreation(t *testing.T) {
factory := common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")
poolAddress := common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
market := NewMarket(
factory,
poolAddress,
token0,
token1,
3000, // 0.3% fee
"USDC_WETH",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"UniswapV3",
)
if market.Factory != factory {
t.Errorf("Expected factory %s, got %s", factory.Hex(), market.Factory.Hex())
}
if market.PoolAddress != poolAddress {
t.Errorf("Expected poolAddress %s, got %s", poolAddress.Hex(), market.PoolAddress.Hex())
}
if market.Token0 != token0 {
t.Errorf("Expected token0 %s, got %s", token0.Hex(), market.Token0.Hex())
}
if market.Token1 != token1 {
t.Errorf("Expected token1 %s, got %s", token1.Hex(), market.Token1.Hex())
}
if market.Fee != 3000 {
t.Errorf("Expected fee 3000, got %d", market.Fee)
}
if market.Ticker != "USDC_WETH" {
t.Errorf("Expected ticker USDC_WETH, got %s", market.Ticker)
}
if market.Protocol != "UniswapV3" {
t.Errorf("Expected protocol UniswapV3, got %s", market.Protocol)
}
if market.Key == "" {
t.Error("Expected market key to be generated")
}
}
func TestMarketPriceData(t *testing.T) {
market := &Market{
Price: big.NewFloat(0),
Liquidity: big.NewInt(0),
SqrtPriceX96: big.NewInt(0),
Tick: 0,
}
price := big.NewFloat(2000.5)
liquidity := big.NewInt(1000000000000000000) // 1 ETH in wei
sqrtPriceX96 := big.NewInt(2505414483750470000)
tick := int32(200000)
market.UpdatePriceData(price, liquidity, sqrtPriceX96, tick)
if market.Price.Cmp(price) != 0 {
t.Errorf("Expected price %v, got %v", price, market.Price)
}
if market.Liquidity.Cmp(liquidity) != 0 {
t.Errorf("Expected liquidity %v, got %v", liquidity, market.Liquidity)
}
if market.SqrtPriceX96.Cmp(sqrtPriceX96) != 0 {
t.Errorf("Expected sqrtPriceX96 %v, got %v", sqrtPriceX96, market.SqrtPriceX96)
}
if market.Tick != tick {
t.Errorf("Expected tick %d, got %d", tick, market.Tick)
}
}
func TestMarketMetadata(t *testing.T) {
market := &Market{
Status: StatusPossible,
Timestamp: 0,
BlockNumber: 0,
TxHash: common.Hash{},
}
timestamp := int64(1620000000)
blockNumber := uint64(12345678)
txHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
market.UpdateMetadata(timestamp, blockNumber, txHash, StatusConfirmed)
if market.Timestamp != timestamp {
t.Errorf("Expected timestamp %d, got %d", timestamp, market.Timestamp)
}
if market.BlockNumber != blockNumber {
t.Errorf("Expected blockNumber %d, got %d", blockNumber, market.BlockNumber)
}
if market.TxHash != txHash {
t.Errorf("Expected txHash %s, got %s", txHash.Hex(), market.TxHash.Hex())
}
if market.Status != StatusConfirmed {
t.Errorf("Expected status StatusConfirmed, got %s", market.Status)
}
}
func TestMarketValidation(t *testing.T) {
// Test invalid market
invalidMarket := &Market{
Status: StatusPossible,
Price: big.NewFloat(0),
}
if invalidMarket.IsValid() {
t.Error("Expected invalid market to return false for IsValid()")
}
// Test valid market
validMarket := &Market{
Status: StatusConfirmed,
Price: big.NewFloat(2000.5),
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2505414483750470000),
}
if !validMarket.IsValid() {
t.Error("Expected valid market to return true for IsValid()")
}
}
func TestGenerateRawTicker(t *testing.T) {
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
expected := "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
actual := GenerateRawTicker(token0, token1)
if actual != expected {
t.Errorf("Expected raw ticker %s, got %s", expected, actual)
}
}
func TestMarketClone(t *testing.T) {
original := &Market{
Factory: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
PoolAddress: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Fee: 3000,
Ticker: "USDC_WETH",
RawTicker: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48_0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
Key: "test_key",
Price: big.NewFloat(2000.5),
Liquidity: big.NewInt(1000000000000000000),
SqrtPriceX96: big.NewInt(2505414483750470000),
Tick: 200000,
Status: StatusConfirmed,
Timestamp: time.Now().Unix(),
BlockNumber: 12345678,
TxHash: common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"),
Protocol: "UniswapV3",
}
clone := original.Clone()
// Check that all fields are equal
if clone.Factory != original.Factory {
t.Error("Factory addresses do not match")
}
if clone.Price.Cmp(original.Price) != 0 {
t.Error("Price values do not match")
}
if clone.Liquidity.Cmp(original.Liquidity) != 0 {
t.Error("Liquidity values do not match")
}
if clone.SqrtPriceX96.Cmp(original.SqrtPriceX96) != 0 {
t.Error("SqrtPriceX96 values do not match")
}
// Check that they are different objects
original.Price = big.NewFloat(3000.0)
if clone.Price.Cmp(big.NewFloat(2000.5)) != 0 {
t.Error("Clone was not a deep copy")
}
}