335 lines
10 KiB
Python
335 lines
10 KiB
Python
"""
|
|
Exchange Flow Analyzer
|
|
======================
|
|
|
|
Liquidity pool monitoring and exchange flow analysis.
|
|
Tracks:
|
|
- Liquidity depth across DEXs
|
|
- Exchange deposit/withdrawal flows
|
|
- Whale movement tracking
|
|
- Risk scoring based on pool participation
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
try:
|
|
from web3 import Web3
|
|
|
|
WEB3_AVAILABLE = True
|
|
except ImportError:
|
|
WEB3_AVAILABLE = False
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning("web3.py not available, using stub mode")
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ─── LIQUIDITY POOL CONFIGURATIONS ────────────────────────────────
|
|
|
|
DEX_CONFIGS = {
|
|
"uniswap_v3": {
|
|
"factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
|
|
"router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Te7x",
|
|
"chain": "ethereum",
|
|
},
|
|
"uniswap_v2": {
|
|
"factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
|
|
"router": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
|
|
"chain": "ethereum",
|
|
},
|
|
"curve": {
|
|
"factory": "0xB9fEF8811f4367e123416528724181538c511156",
|
|
"chain": "ethereum",
|
|
},
|
|
"curve_v2": {
|
|
"factory": "0x0000000000000000000000000000000000000000", # Placeholder
|
|
"chain": "ethereum",
|
|
},
|
|
"balancer": {
|
|
"factory": "0xBA12222222228d8Ba445958a75a0704d566BF233",
|
|
"chain": "ethereum",
|
|
},
|
|
"sushiswap": {
|
|
"factory": "0xC0AEe478e3658e2610c5F7A4A2E1777c9Ef98307",
|
|
"router": "0x1b02dA8Cb0d8576294F01f02377Ce1978d58eF5C",
|
|
"chain": "ethereum",
|
|
},
|
|
"base_uniswap_v3": {
|
|
"factory": "0x33128a8fC13f53869D7171190F65e2B9B2E1F36F",
|
|
"router": "0x546C87632b88B584dE4106134983e4783719B8D4",
|
|
"chain": "base",
|
|
},
|
|
"base_sushiswap": {
|
|
"factory": "0xf551e3c70153905c553589509f14dea4945ca878",
|
|
"router": "0x66343d76f738ef4be3b99d3679a4389543a8393e",
|
|
"chain": "base",
|
|
},
|
|
"arbitrum_uniswap_v3": {
|
|
"factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
|
|
"router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Te7x",
|
|
"chain": "arbitrum",
|
|
},
|
|
"optimism_uniswap_v3": {
|
|
"factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
|
|
"router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Te7x",
|
|
"chain": "optimism",
|
|
},
|
|
}
|
|
|
|
|
|
# ─── POOL DATA MODELS ─────────────────────────────────────────────
|
|
|
|
|
|
@dataclass
|
|
class LiquidityPool:
|
|
"""Liquidity pool data."""
|
|
|
|
pool_address: str
|
|
token0: str
|
|
token1: str
|
|
token0_reserve: int # wei
|
|
token1_reserve: int # wei
|
|
total_liquidity_usd: float
|
|
volume_24h_usd: float
|
|
fees_24h_usd: float
|
|
tvl_usd: float
|
|
dune_analytics_id: str | None = None
|
|
dex: str = "unknown"
|
|
|
|
|
|
@dataclass
|
|
class ExchangeFlow:
|
|
"""Exchange deposit/withdrawal flow."""
|
|
|
|
address: str
|
|
exchange: str
|
|
entry_time: str
|
|
exit_time: str | None = None
|
|
entry_amount_usd: float = 0
|
|
exit_amount_usd: float = 0
|
|
profit_usd: float = 0
|
|
risk_score: int = 0
|
|
|
|
|
|
@dataclass
|
|
class WhaleMovement:
|
|
"""Whale movement tracking."""
|
|
|
|
address: str
|
|
amount: int # wei
|
|
token: str
|
|
from_exchange: str | None = None
|
|
to_exchange: str | None = None
|
|
timestamp: str = ""
|
|
tx_hash: str | None = None
|
|
|
|
|
|
# ─── EXCHANGE FLOW ANALYZER ───────────────────────────────────────
|
|
|
|
|
|
class ExchangeFlowAnalyzer:
|
|
"""Analyzer for exchange flows and liquidity pools."""
|
|
|
|
def __init__(self, web3_client=None):
|
|
self.web3 = web3_client
|
|
self.pools: dict[str, LiquidityPool] = {}
|
|
self.flows: list[ExchangeFlow] = []
|
|
self.whales: list[WhaleMovement] = []
|
|
self.exchange_ratelimits: dict[str, int] = {
|
|
"binance": 50000,
|
|
"coinbase": 100000,
|
|
"kraken": 25000,
|
|
"kucoin": 30000,
|
|
"ftx": 10000,
|
|
}
|
|
|
|
def get_liquidity_pool(self, pool_address: str, chain: str = "base") -> LiquidityPool | None:
|
|
"""
|
|
Get liquidity pool data.
|
|
|
|
Args:
|
|
pool_address: Pool contract address
|
|
chain: Blockchain network
|
|
|
|
Returns:
|
|
LiquidityPool data or None
|
|
"""
|
|
if not WEB3_AVAILABLE and not self.web3:
|
|
return self._stub_pool(pool_address)
|
|
|
|
try:
|
|
w3 = self.web3 or Web3(Web3.HTTPProvider(self._get_rpc(chain)))
|
|
|
|
# Get pool reserve data (Simplified - real implementation would use pool ABI)
|
|
reserve0 = w3.eth.get_balance(pool_address) if w3.eth.get_code(pool_address) else 0
|
|
reserve1 = 0 # Would fetch from other token
|
|
|
|
return LiquidityPool(
|
|
pool_address=pool_address,
|
|
token0="0x0000000000000000000000000000000000000000",
|
|
token1="0x0000000000000000000000000000000000000000",
|
|
token0_reserve=reserve0,
|
|
token1_reserve=reserve1,
|
|
total_liquidity_usd=0,
|
|
volume_24h_usd=0,
|
|
fees_24h_usd=0,
|
|
tvl_usd=0,
|
|
dex=self._detect_dex(pool_address, chain),
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error fetching pool {pool_address}: {e}")
|
|
return self._stub_pool(pool_address)
|
|
|
|
def _get_rpc(self, chain: str) -> str:
|
|
"""Get RPC URL for chain."""
|
|
rpcs = {
|
|
"ethereum": "https://ethereum-rpc.publicnode.com",
|
|
"base": "https://base-rpc.publicnode.com",
|
|
"arbitrum": "https://arbitrum-rpc.publicnode.com",
|
|
"optimism": "https://optimism-rpc.publicnode.com",
|
|
}
|
|
return rpcs.get(chain, rpcs["ethereum"])
|
|
|
|
def _detect_dex(self, address: str, chain: str) -> str:
|
|
"""Detect which DEX a pool belongs to."""
|
|
address_lower = address.lower()
|
|
|
|
dex_configs = DEX_CONFIGS.get(chain, {})
|
|
for dex, config in dex_configs.items():
|
|
factory = config.get("factory", "").lower()
|
|
if factory and address_lower.startswith(factory[:10]):
|
|
return dex
|
|
|
|
# Fallback detection
|
|
if "uniswap" in address_lower or "1f984" in address_lower:
|
|
return "uniswap_v3"
|
|
if "curve" in address_lower:
|
|
return "curve"
|
|
if "balancer" in address_lower:
|
|
return "balancer"
|
|
if "sushiswap" in address_lower:
|
|
return "sushiswap"
|
|
|
|
return "unknown"
|
|
|
|
def _stub_pool(self, address: str) -> LiquidityPool:
|
|
"""Stub pool data when Web3 unavailable."""
|
|
return LiquidityPool(
|
|
pool_address=address,
|
|
token0="0x",
|
|
token1="0x",
|
|
token0_reserve=0,
|
|
token1_reserve=0,
|
|
total_liquidity_usd=0,
|
|
volume_24h_usd=0,
|
|
fees_24h_usd=0,
|
|
tvl_usd=0,
|
|
dex="stub",
|
|
)
|
|
|
|
def analyze_entity_flows(self, address: str, chain: str = "base") -> dict[str, Any]:
|
|
"""
|
|
Analyze exchange flows for an entity (wallet).
|
|
|
|
Args:
|
|
address: Entity address
|
|
chain: Blockchain network
|
|
|
|
Returns:
|
|
Flow analysis results
|
|
"""
|
|
result = {
|
|
"address": address,
|
|
"chain": chain,
|
|
"exchanges": [],
|
|
"total_inflow_usd": 0,
|
|
"total_outflow_usd": 0,
|
|
"risk_score": 0,
|
|
"last_updated": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
# Simplified analysis - real implementation would track actual flows
|
|
result["exchanges"].append(
|
|
{
|
|
"exchange": "binance",
|
|
"inflow_usd": 0,
|
|
"outflow_usd": 0,
|
|
"holdings_usd": 0,
|
|
"risk_score": 50, # Medium risk
|
|
}
|
|
)
|
|
|
|
return result
|
|
|
|
def compare_flows(self, addresses: list[str], chain: str = "base") -> dict[str, Any]:
|
|
"""Compare flows between multiple addresses."""
|
|
result = {
|
|
"chain": chain,
|
|
"addresses": {},
|
|
"correlations": [],
|
|
"last_updated": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
for addr in addresses:
|
|
flows = self.analyze_entity_flows(addr, chain)
|
|
result["addresses"][addr] = flows
|
|
|
|
# Detect correlations (simplified)
|
|
result["correlations"] = []
|
|
|
|
return result
|
|
|
|
def get_reserves(self, pool_address: str, chain: str = "base") -> dict[str, int]:
|
|
"""
|
|
Get current pool reserves.
|
|
|
|
Args:
|
|
pool_address: Pool address
|
|
chain: Blockchain network
|
|
|
|
Returns:
|
|
Reserves dict
|
|
"""
|
|
pool = self.get_liquidity_pool(pool_address, chain)
|
|
return {
|
|
"reserve0": pool.token0_reserve,
|
|
"reserve1": pool.token1_reserve,
|
|
"total_liquidity": pool.total_liquidity_usd,
|
|
}
|
|
|
|
def track_whale_movements(self, address: str, threshold_usd: float = 100000) -> list[WhaleMovement]:
|
|
"""
|
|
Track large movements for an address.
|
|
|
|
Args:
|
|
address: Wallet address
|
|
threshold_usd: Minimum transaction value in USD
|
|
|
|
Returns:
|
|
List of whale movements
|
|
"""
|
|
# Simplified - real implementation would scan blocks
|
|
return []
|
|
|
|
|
|
# ─── GLOBAL SINGLETON ─────────────────────────────────────────────
|
|
|
|
_analyzer = ExchangeFlowAnalyzer()
|
|
|
|
|
|
def analyze_entity_flows(address: str, chain: str = "base") -> dict[str, Any]:
|
|
"""Analyze entity exchange flows."""
|
|
return _analyzer.analyze_entity_flows(address, chain)
|
|
|
|
|
|
def compare_flows(addresses: list[str], chain: str = "base") -> dict[str, Any]:
|
|
"""Compare exchange flows between addresses."""
|
|
return _analyzer.compare_flows(addresses, chain)
|
|
|
|
|
|
def get_reserves(pool_address: str, chain: str = "base") -> dict[str, int]:
|
|
"""Get pool reserves."""
|
|
return _analyzer.get_reserves(pool_address, chain)
|