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
orig/pkg/marketmanager/README.md
Normal file
1
orig/pkg/marketmanager/README.md
Normal 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
|
||||
215
orig/pkg/marketmanager/arbitrage.go
Normal file
215
orig/pkg/marketmanager/arbitrage.go
Normal 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
|
||||
}
|
||||
254
orig/pkg/marketmanager/arbitrage_test.go
Normal file
254
orig/pkg/marketmanager/arbitrage_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
353
orig/pkg/marketmanager/database.go
Normal file
353
orig/pkg/marketmanager/database.go
Normal 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
|
||||
}
|
||||
267
orig/pkg/marketmanager/manager.go
Normal file
267
orig/pkg/marketmanager/manager.go
Normal 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
|
||||
}
|
||||
288
orig/pkg/marketmanager/manager_test.go
Normal file
288
orig/pkg/marketmanager/manager_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
148
orig/pkg/marketmanager/types.go
Normal file
148
orig/pkg/marketmanager/types.go
Normal 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
|
||||
}
|
||||
205
orig/pkg/marketmanager/types_test.go
Normal file
205
orig/pkg/marketmanager/types_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user