working version, before optimalization
This commit is contained in:
192
florida/tools/pool_scanner.py
Normal file
192
florida/tools/pool_scanner.py
Normal file
@ -0,0 +1,192 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import pandas as pd
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from web3 import Web3
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(__file__), "pool_scanner_config.json")
|
||||
STATE_FILE = os.path.join("market_data", "pool_scanner_state.json")
|
||||
HISTORY_FILE = os.path.join("market_data", "pool_history.csv")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# RPC MAP
|
||||
RPC_MAP = {
|
||||
"ARBITRUM": os.environ.get("MAINNET_RPC_URL"),
|
||||
"BSC": os.environ.get("BNB_RPC_URL"),
|
||||
"BASE": os.environ.get("BASE_RPC_URL")
|
||||
}
|
||||
|
||||
# ABIS
|
||||
POOL_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint8", "name": "feeProtocol", "type": "uint8"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
# PancakeSwap V3 uses uint32 for feeProtocol
|
||||
PANCAKE_POOL_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "uint32", "name": "feeProtocol", "type": "uint32"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
AERODROME_POOL_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "slot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "observationIndex", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinality", "type": "uint16"}, {"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"}, {"internalType": "bool", "name": "unlocked", "type": "bool"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal0X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "feeGrowthGlobal1X128", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
ERC20_ABI = json.loads('[{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}]')
|
||||
|
||||
def get_w3(chain):
|
||||
url = RPC_MAP.get(chain)
|
||||
if not url: return None
|
||||
return Web3(Web3.HTTPProvider(url))
|
||||
|
||||
def load_state():
|
||||
if os.path.exists(STATE_FILE):
|
||||
with open(STATE_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_state(state):
|
||||
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||
with open(STATE_FILE, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
def append_history(data):
|
||||
df = pd.DataFrame([data])
|
||||
header = not os.path.exists(HISTORY_FILE)
|
||||
df.to_csv(HISTORY_FILE, mode='a', header=header, index=False)
|
||||
|
||||
def get_liquidity_for_amount(amount, sqrt_price_x96, tick_lower, tick_upper, decimal_diff):
|
||||
# Simplified Liquidity Calc for 50/50 deposit simulation
|
||||
# L = Amount / (sqrt(P) - sqrt(Pa)) for one side...
|
||||
# For now, we assume simple V3 math or just track Fee Growth per Unit Liquidity
|
||||
# Real simulation is complex.
|
||||
# TRICK: We will track "Fee Growth per 1 Unit of Liquidity" directly (Raw X128).
|
||||
# Then user can multiply by their theoretical L later.
|
||||
return 1
|
||||
|
||||
def main():
|
||||
print("Starting Pool Scanner...")
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
pools = json.load(f)
|
||||
|
||||
state = load_state()
|
||||
|
||||
# Init Web3 cache
|
||||
w3_instances = {}
|
||||
|
||||
for pool in pools:
|
||||
name = pool['name']
|
||||
chain = pool['chain']
|
||||
# Fix Checksum
|
||||
try:
|
||||
addr = Web3.to_checksum_address(pool['pool_address'])
|
||||
except Exception:
|
||||
print(f" ❌ Invalid Address: {pool['pool_address']}")
|
||||
continue
|
||||
|
||||
is_aero = pool.get('is_aerodrome', False)
|
||||
|
||||
print(f"Scanning {name} ({chain})...")
|
||||
|
||||
if chain not in w3_instances:
|
||||
w3_instances[chain] = get_w3(chain)
|
||||
|
||||
w3 = w3_instances[chain]
|
||||
if not w3 or not w3.is_connected():
|
||||
print(f" ❌ RPC Error for {chain}")
|
||||
continue
|
||||
|
||||
try:
|
||||
if is_aero:
|
||||
abi = AERODROME_POOL_ABI
|
||||
elif chain == "BSC":
|
||||
abi = PANCAKE_POOL_ABI
|
||||
else:
|
||||
abi = POOL_ABI
|
||||
|
||||
contract = w3.eth.contract(address=addr, abi=abi)
|
||||
|
||||
# Fetch Data
|
||||
slot0 = contract.functions.slot0().call()
|
||||
tick = slot0[1]
|
||||
sqrt_price = slot0[0]
|
||||
|
||||
fg0 = contract.functions.feeGrowthGlobal0X128().call()
|
||||
fg1 = contract.functions.feeGrowthGlobal1X128().call()
|
||||
|
||||
# Fetch Decimals (Once)
|
||||
if name not in state:
|
||||
t0 = contract.functions.token0().call()
|
||||
t1 = contract.functions.token1().call()
|
||||
d0 = w3.eth.contract(address=t0, abi=ERC20_ABI).functions.decimals().call()
|
||||
d1 = w3.eth.contract(address=t1, abi=ERC20_ABI).functions.decimals().call()
|
||||
state[name] = {
|
||||
"init_tick": tick,
|
||||
"init_fg0": fg0,
|
||||
"init_fg1": fg1,
|
||||
"decimals": [d0, d1],
|
||||
"cumulative_fees_usd": 0.0,
|
||||
"last_fg0": fg0,
|
||||
"last_fg1": fg1
|
||||
}
|
||||
|
||||
# Update State
|
||||
prev = state[name]
|
||||
diff0 = fg0 - prev['last_fg0']
|
||||
diff1 = fg1 - prev['last_fg1']
|
||||
|
||||
# Calculate USD Value of Fees (Approx)
|
||||
# Need Liquidity.
|
||||
# If we assume 1 unit of Liquidity?
|
||||
# Fee = Diff * L / 2^128
|
||||
|
||||
# Update Last
|
||||
prev['last_fg0'] = fg0
|
||||
prev['last_fg1'] = fg1
|
||||
prev['last_tick'] = tick
|
||||
prev['last_update'] = datetime.now().isoformat()
|
||||
|
||||
# Save History
|
||||
record = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"pool_name": name,
|
||||
"chain": chain,
|
||||
"tick": tick,
|
||||
"sqrtPriceX96": str(sqrt_price),
|
||||
"feeGrowth0": str(fg0),
|
||||
"feeGrowth1": str(fg1)
|
||||
}
|
||||
append_history(record)
|
||||
print(f" ✅ Data recorded. Tick: {tick}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
|
||||
save_state(state)
|
||||
print("Scan complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
main()
|
||||
time.sleep(600) # 10 minutes
|
||||
Reference in New Issue
Block a user