CLP auto hedge
This commit is contained in:
711
clp_hedger/uniswap_manager.py
Normal file
711
clp_hedger/uniswap_manager.py
Normal file
@ -0,0 +1,711 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
from web3 import Web3
|
||||
from eth_account import Account
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# --- Helper Functions ---
|
||||
def clean_address(addr):
|
||||
return re.sub(r'[^0-9a-fA-FxX]', '', addr)
|
||||
|
||||
def price_from_sqrt_price_x96(sqrt_price_x96, token0_decimals, token1_decimals):
|
||||
price = (sqrt_price_x96 / (2**96))**2
|
||||
price = price * (10**(token0_decimals - token1_decimals))
|
||||
return price
|
||||
|
||||
def price_from_tick(tick, token0_decimals, token1_decimals):
|
||||
price = 1.0001**tick
|
||||
price = price * (10**(token0_decimals - token1_decimals))
|
||||
return price
|
||||
|
||||
def from_wei(amount, decimals):
|
||||
return amount / (10**decimals)
|
||||
|
||||
# --- V3 Math Helpers ---
|
||||
def get_sqrt_ratio_at_tick(tick):
|
||||
return int((1.0001 ** (tick / 2)) * (2 ** 96))
|
||||
|
||||
def get_liquidity_for_amount0(sqrt_ratio_a, sqrt_ratio_b, amount0):
|
||||
if sqrt_ratio_a > sqrt_ratio_b:
|
||||
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||
return int(amount0 * sqrt_ratio_a * sqrt_ratio_b / (sqrt_ratio_b - sqrt_ratio_a))
|
||||
|
||||
def get_liquidity_for_amount1(sqrt_ratio_a, sqrt_ratio_b, amount1):
|
||||
if sqrt_ratio_a > sqrt_ratio_b:
|
||||
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||
return int(amount1 / (sqrt_ratio_b - sqrt_ratio_a))
|
||||
|
||||
def get_amounts_for_liquidity(sqrt_ratio_current, sqrt_ratio_a, sqrt_ratio_b, liquidity):
|
||||
if sqrt_ratio_a > sqrt_ratio_b:
|
||||
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||
|
||||
amount0 = 0
|
||||
amount1 = 0
|
||||
Q96 = 1 << 96
|
||||
|
||||
if sqrt_ratio_current <= sqrt_ratio_a:
|
||||
amount0 = ((liquidity * Q96) // sqrt_ratio_a) - ((liquidity * Q96) // sqrt_ratio_b)
|
||||
elif sqrt_ratio_current < sqrt_ratio_b:
|
||||
amount0 = ((liquidity * Q96) // sqrt_ratio_current) - ((liquidity * Q96) // sqrt_ratio_b)
|
||||
amount1 = (liquidity * (sqrt_ratio_current - sqrt_ratio_a)) // Q96
|
||||
else:
|
||||
amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96
|
||||
|
||||
return amount0, amount1
|
||||
|
||||
# --- Configuration ---
|
||||
RPC_URL = os.environ.get("MAINNET_RPC_URL")
|
||||
POSITION_TOKEN_ID = int(os.environ.get("POSITION_TOKEN_ID", "0"))
|
||||
PRIVATE_KEY = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
|
||||
|
||||
MONITOR_INTERVAL_SECONDS = 30
|
||||
COLLECT_FEES_ENABLED = False
|
||||
CLOSE_POSITION_ENABLED = True
|
||||
CLOSE_IF_OUT_OF_RANGE_ONLY = True
|
||||
|
||||
OPEN_POSITION_ENABLED = True
|
||||
TARGET_INVESTMENT_VALUE_TOKEN1 = 100.0
|
||||
RANGE_WIDTH_PCT = 0.005
|
||||
|
||||
STATUS_FILE = "hedge_status.json"
|
||||
|
||||
# --- JSON State Helpers ---
|
||||
def get_active_automatic_position():
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
return None
|
||||
try:
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
for entry in data:
|
||||
if entry.get('type') == 'AUTOMATIC' and entry.get('status') == 'OPEN':
|
||||
return entry
|
||||
except Exception as e:
|
||||
print(f"ERROR reading status file: {e}")
|
||||
return None
|
||||
|
||||
def get_all_open_positions():
|
||||
"""Reads hedge_status.json and returns a list of all OPEN positions (Manual and Automatic)."""
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
return []
|
||||
try:
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
return [entry for entry in data if entry.get('status') == 'OPEN']
|
||||
except Exception as e:
|
||||
print(f"ERROR reading status file: {e}")
|
||||
return []
|
||||
|
||||
def update_hedge_status_file(action, position_data):
|
||||
current_data = []
|
||||
if os.path.exists(STATUS_FILE):
|
||||
try:
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
current_data = json.load(f)
|
||||
except:
|
||||
current_data = []
|
||||
|
||||
if action == "OPEN":
|
||||
new_entry = {
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": position_data['token_id'],
|
||||
"status": "OPEN",
|
||||
"entry_price": position_data['entry_price'],
|
||||
"range_lower": position_data['range_lower'],
|
||||
"range_upper": position_data['range_upper'],
|
||||
"target_value": position_data.get('target_value', 0.0),
|
||||
"amount0_initial": position_data.get('amount0', 0),
|
||||
"amount1_initial": position_data.get('amount1', 0),
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": int(time.time()),
|
||||
"timestamp_close": None
|
||||
}
|
||||
current_data.append(new_entry)
|
||||
print(f"Recorded new AUTOMATIC position {position_data['token_id']} in {STATUS_FILE}")
|
||||
|
||||
elif action == "CLOSE":
|
||||
found = False
|
||||
for entry in current_data:
|
||||
if (
|
||||
entry.get('type') == "AUTOMATIC" and
|
||||
entry.get('status') == "OPEN" and
|
||||
entry.get('token_id') == position_data['token_id']
|
||||
):
|
||||
|
||||
entry['status'] = "CLOSED"
|
||||
entry['timestamp_close'] = int(time.time())
|
||||
found = True
|
||||
print(f"Marked position {entry['token_id']} as CLOSED in {STATUS_FILE}")
|
||||
break
|
||||
if not found:
|
||||
print(f"WARNING: Could not find open AUTOMATIC position {position_data['token_id']} to close.")
|
||||
|
||||
with open(STATUS_FILE, 'w') as f:
|
||||
json.dump(current_data, f, indent=2)
|
||||
|
||||
# --- ABIs ---
|
||||
# Simplified for length, usually loaded from huge string
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_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": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
ERC20_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
||||
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
UNISWAP_V3_FACTORY_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
SWAP_ROUTER_ABI = json.loads('''
|
||||
[
|
||||
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
WETH9_ABI = json.loads('''
|
||||
[
|
||||
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
|
||||
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
|
||||
]
|
||||
''')
|
||||
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = bytes.fromhex("C36442b4" + "a4522E87" + "1399CD71" + "7aBDD847" + "Ab11FE88")
|
||||
UNISWAP_V3_SWAP_ROUTER_ADDRESS = bytes.fromhex("E592427A0AEce92De3Edee1F18E0157C05861564")
|
||||
WETH_ADDRESS = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # Arbitrum WETH
|
||||
|
||||
# --- Core Logic Functions ---
|
||||
def get_position_details(w3_instance, npm_c, factory_c, token_id):
|
||||
try:
|
||||
position_data = npm_c.functions.positions(token_id).call()
|
||||
(nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper, liquidity,
|
||||
feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data
|
||||
|
||||
token0_contract = w3_instance.eth.contract(address=token0_address, abi=ERC20_ABI)
|
||||
token1_contract = w3_instance.eth.contract(address=token1_address, abi=ERC20_ABI)
|
||||
token0_symbol = token0_contract.functions.symbol().call()
|
||||
token1_symbol = token1_contract.functions.symbol().call()
|
||||
token0_decimals = token0_contract.functions.decimals().call()
|
||||
token1_decimals = token1_contract.functions.decimals().call()
|
||||
|
||||
pool_address = factory_c.functions.getPool(token0_address, token1_address, fee).call()
|
||||
if pool_address == '0x0000000000000000000000000000000000000000':
|
||||
return None, None
|
||||
|
||||
pool_contract = w3_instance.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
|
||||
|
||||
return {
|
||||
"token0_address": token0_address, "token1_address": token1_address,
|
||||
"token0_symbol": token0_symbol, "token1_symbol": token1_symbol,
|
||||
"token0_decimals": token0_decimals, "token1_decimals": token1_decimals,
|
||||
"fee": fee, "tickLower": tickLower, "tickUpper": tickUpper, "liquidity": liquidity,
|
||||
"pool_address": pool_address
|
||||
}, pool_contract
|
||||
except Exception as e:
|
||||
print(f"ERROR fetching position details: {e}")
|
||||
return None, None
|
||||
|
||||
def get_pool_dynamic_data(pool_c):
|
||||
try:
|
||||
slot0_data = pool_c.functions.slot0().call()
|
||||
return {"sqrtPriceX96": slot0_data[0], "tick": slot0_data[1]}
|
||||
except Exception as e:
|
||||
print(f"ERROR fetching pool dynamic data: {e}")
|
||||
return None
|
||||
|
||||
def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_value_token1, decimals0, decimals1, sqrt_price_current_x96):
|
||||
sqrt_price_current = get_sqrt_ratio_at_tick(current_tick)
|
||||
sqrt_price_lower = get_sqrt_ratio_at_tick(tick_lower)
|
||||
sqrt_price_upper = get_sqrt_ratio_at_tick(tick_upper)
|
||||
|
||||
# 1. Get Price of Token0 in terms of Token1 (e.g., WETH price in USDC)
|
||||
price_of_token0_in_token1_units = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1)
|
||||
|
||||
# 2. Estimate Amounts for a Test Liquidity (L_test)
|
||||
L_test = 1 << 128
|
||||
amt0_test, amt1_test = get_amounts_for_liquidity(sqrt_price_current, sqrt_price_lower, sqrt_price_upper, L_test)
|
||||
|
||||
# 3. Adjust test amounts for decimals to get "Real Units" (e.g., 0.1 WETH, 500 USDC)
|
||||
real_amt0_test = amt0_test / (10**decimals0)
|
||||
real_amt1_test = amt1_test / (10**decimals1)
|
||||
|
||||
# 4. Calculate Total Value of Test Position in Token1 terms (e.g., Total in USDC)
|
||||
value_test = (real_amt0_test * price_of_token0_in_token1_units) + real_amt1_test
|
||||
|
||||
if value_test == 0:
|
||||
return 0, 0
|
||||
|
||||
# 5. Scale to Target Investment Value
|
||||
scale = investment_value_token1 / value_test
|
||||
|
||||
# 6. Calculate Final Amounts (raw integer units for contract call)
|
||||
final_amt0 = int(amt0_test * scale)
|
||||
final_amt1 = int(amt1_test * scale)
|
||||
|
||||
return final_amt0, final_amt1
|
||||
|
||||
def check_and_swap(w3_instance, router_contract, account, token0, token1, amount0_needed, amount1_needed):
|
||||
token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||
|
||||
deficit0 = max(0, amount0_needed - bal0)
|
||||
deficit1 = max(0, amount1_needed - bal1)
|
||||
|
||||
# --- AUTO-WRAP ETH LOGIC ---
|
||||
# Check if we need WETH and have Native ETH
|
||||
# WETH Address Check (Case insensitive)
|
||||
weth_addr_lower = WETH_ADDRESS.lower()
|
||||
|
||||
# Check Token0 (Deficit0)
|
||||
if (deficit0 > 0 or deficit1 > 0) and token0.lower() == weth_addr_lower:
|
||||
native_bal = w3_instance.eth.get_balance(account.address)
|
||||
gas_reserve = 2 * 10**16 # 0.02 ETH gas reserve
|
||||
available_native = max(0, native_bal - gas_reserve)
|
||||
|
||||
# Determine how much to wrap
|
||||
# If we have deficit1 (need USDC), we likely need to wrap more WETH to swap it.
|
||||
# Strategy: If deficit1 > 0, wrap ALL available native ETH (up to reasonable limit?).
|
||||
# Or just wrap what we have.
|
||||
|
||||
amount_to_wrap = 0
|
||||
if deficit0 > 0:
|
||||
amount_to_wrap = deficit0
|
||||
|
||||
if deficit1 > 0:
|
||||
# We need to buy Token1. We need surplus Token0.
|
||||
# Wrap all remaining available native ETH to facilitate swap.
|
||||
amount_to_wrap = available_native
|
||||
|
||||
# Safety clamp
|
||||
amount_to_wrap = min(amount_to_wrap, available_native)
|
||||
|
||||
if amount_to_wrap > 0:
|
||||
print(f"Auto-Wrapping {from_wei(amount_to_wrap, 18)} ETH to WETH...")
|
||||
weth_contract = w3_instance.eth.contract(address=token0, abi=WETH9_ABI)
|
||||
wrap_txn = weth_contract.functions.deposit().build_transaction({
|
||||
'from': account.address,
|
||||
'value': amount_to_wrap,
|
||||
'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000,
|
||||
'maxFeePerGas': w3_instance.eth.gas_price * 2,
|
||||
'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key)
|
||||
raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap)
|
||||
print(f"Wrap Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
# Refresh Balance
|
||||
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||
deficit0 = max(0, amount0_needed - bal0)
|
||||
else:
|
||||
if deficit0 > 0:
|
||||
print(f"Insufficient Native ETH to wrap. Need: {from_wei(deficit0, 18)}, Have: {from_wei(available_native, 18)}")
|
||||
|
||||
# Check Token1 (Deficit1) - Assuming Token1 could be WETH too
|
||||
if deficit1 > 0 and token1.lower() == weth_addr_lower:
|
||||
native_bal = w3_instance.eth.get_balance(account.address)
|
||||
gas_reserve = 10**16
|
||||
available_native = max(0, native_bal - gas_reserve)
|
||||
|
||||
if available_native >= deficit1:
|
||||
print(f"Auto-Wrapping {from_wei(deficit1, 18)} ETH to WETH...")
|
||||
weth_contract = w3_instance.eth.contract(address=token1, abi=WETH9_ABI)
|
||||
wrap_txn = weth_contract.functions.deposit().build_transaction({
|
||||
'from': account.address,
|
||||
'value': deficit1,
|
||||
'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000,
|
||||
'maxFeePerGas': w3_instance.eth.gas_price * 2,
|
||||
'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key)
|
||||
raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap)
|
||||
print(f"Wrap Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
# Refresh Balance
|
||||
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||
deficit1 = max(0, amount1_needed - bal1)
|
||||
|
||||
if deficit0 == 0 and deficit1 == 0:
|
||||
return True
|
||||
|
||||
if deficit0 > 0 and bal1 > amount1_needed:
|
||||
surplus1 = bal1 - amount1_needed
|
||||
print(f"Swapping surplus Token1 ({surplus1}) for Token0...")
|
||||
|
||||
approve_txn = token1_contract.functions.approve(router_contract.address, surplus1).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||
w3_instance.eth.send_raw_transaction(raw)
|
||||
time.sleep(2)
|
||||
|
||||
params = (token1, token0, 500, account.address, int(time.time()) + 120, surplus1, 0, 0)
|
||||
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key)
|
||||
raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw_swap)
|
||||
print(f"Swap Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
return True
|
||||
|
||||
elif deficit1 > 0 and bal0 > amount0_needed:
|
||||
surplus0 = bal0 - amount0_needed
|
||||
print(f"Swapping surplus Token0 ({surplus0}) for Token1...")
|
||||
|
||||
approve_txn = token0_contract.functions.approve(router_contract.address, surplus0).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||
w3_instance.eth.send_raw_transaction(raw)
|
||||
time.sleep(2)
|
||||
|
||||
params = (token0, token1, 500, account.address, int(time.time()) + 120, surplus0, 0, 0)
|
||||
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||
'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key)
|
||||
raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw_swap)
|
||||
print(f"Swap Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
return True
|
||||
|
||||
print("❌ Insufficient funds for required amounts.")
|
||||
return False
|
||||
|
||||
def get_token_balances(w3_instance, account_address, token0_address, token1_address):
|
||||
try:
|
||||
token0_contract = w3_instance.eth.contract(address=token0_address, abi=ERC20_ABI)
|
||||
token1_contract = w3_instance.eth.contract(address=token1_address, abi=ERC20_ABI)
|
||||
b0 = token0_contract.functions.balanceOf(account_address).call()
|
||||
b1 = token1_contract.functions.balanceOf(account_address).call()
|
||||
return b0, b1
|
||||
except: return 0, 0
|
||||
|
||||
def decrease_liquidity(w3_instance, npm_contract, account, position_id, liquidity_amount):
|
||||
try:
|
||||
txn = npm_contract.functions.decreaseLiquidity((position_id, liquidity_amount, 0, 0, int(time.time()) + 180)).build_transaction({
|
||||
'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key)
|
||||
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw)
|
||||
print(f"Decrease Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error decreasing: {e}")
|
||||
return False
|
||||
|
||||
def mint_new_position(w3_instance, npm_contract, account, token0, token1, amount0, amount1, tick_lower, tick_upper):
|
||||
print(f"\n--- Attempting to Mint ---")
|
||||
try:
|
||||
token0_c = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
token1_c = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
|
||||
# Approve 0
|
||||
txn0 = token0_c.functions.approve(npm_contract.address, amount0).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed0 = w3_instance.eth.account.sign_transaction(txn0, private_key=account.key)
|
||||
raw0 = signed0.rawTransaction if hasattr(signed0, 'rawTransaction') else signed0.raw_transaction
|
||||
w3_instance.eth.send_raw_transaction(raw0)
|
||||
time.sleep(2)
|
||||
|
||||
# Approve 1
|
||||
txn1 = token1_c.functions.approve(npm_contract.address, amount1).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed1 = w3_instance.eth.account.sign_transaction(txn1, private_key=account.key)
|
||||
raw1 = signed1.rawTransaction if hasattr(signed1, 'rawTransaction') else signed1.raw_transaction
|
||||
w3_instance.eth.send_raw_transaction(raw1)
|
||||
time.sleep(2)
|
||||
|
||||
# Mint
|
||||
params = (token0, token1, 500, tick_lower, tick_upper, amount0, amount1, 0, 0, account.address, int(time.time()) + 180)
|
||||
mint_txn = npm_contract.functions.mint(params).build_transaction({
|
||||
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||
'gas': 800000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed_mint = w3_instance.eth.account.sign_transaction(mint_txn, private_key=account.key)
|
||||
raw_mint = signed_mint.rawTransaction if hasattr(signed_mint, 'rawTransaction') else signed_mint.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw_mint)
|
||||
print(f"Mint Sent: {tx_hash.hex()}")
|
||||
|
||||
receipt = w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
if receipt.status == 1:
|
||||
print("✅ Mint Successful!")
|
||||
|
||||
result_data = {'token_id': None, 'liquidity': 0, 'amount0': 0, 'amount1': 0}
|
||||
|
||||
# Event Topics
|
||||
transfer_topic = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||
increase_liquidity_topic = "3067048beee31b25b2f1681f88dac838c8bba36af25bfb2b7cf7473a5847e35f"
|
||||
|
||||
for log in receipt['logs']:
|
||||
topic0 = log['topics'][0].hex().replace("0x", "")
|
||||
|
||||
# Parse Token ID from Transfer
|
||||
if topic0 == transfer_topic and len(log['topics']) > 3:
|
||||
result_data['token_id'] = int(log['topics'][3].hex(), 16)
|
||||
|
||||
# Parse Amounts from IncreaseLiquidity
|
||||
# Event: IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
|
||||
# Indexed args are in topics. Non-indexed in data.
|
||||
# tokenId is indexed (Topic 1).
|
||||
# Data: liquidity (32 bytes), amount0 (32 bytes), amount1 (32 bytes) = 96 bytes total
|
||||
if topic0 == increase_liquidity_topic:
|
||||
data_hex = log['data'].hex().replace("0x", "")
|
||||
if len(data_hex) >= 192: # 3 * 64 chars
|
||||
# Split data into 3 chunks of 64 chars (32 bytes)
|
||||
liquidity_hex = data_hex[0:64]
|
||||
amount0_hex = data_hex[64:128]
|
||||
amount1_hex = data_hex[128:192]
|
||||
|
||||
result_data['liquidity'] = int(liquidity_hex, 16)
|
||||
result_data['amount0'] = int(amount0_hex, 16)
|
||||
result_data['amount1'] = int(amount1_hex, 16)
|
||||
print(f"Captured Actual Mint Amounts: {result_data['amount0']} Token0 / {result_data['amount1']} Token1")
|
||||
|
||||
if result_data['token_id']:
|
||||
return result_data
|
||||
|
||||
return None
|
||||
else:
|
||||
print("❌ Mint Failed!")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Mint Error: {e}")
|
||||
return None
|
||||
|
||||
def collect_fees(w3_instance, npm_contract, account, position_id):
|
||||
try:
|
||||
txn = npm_contract.functions.collect((position_id, account.address, 2**128-1, 2**128-1)).build_transaction({
|
||||
'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id
|
||||
})
|
||||
signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key)
|
||||
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||
tx_hash = w3_instance.eth.send_raw_transaction(raw)
|
||||
print(f"Collect Sent: {tx_hash.hex()}")
|
||||
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||
return True
|
||||
except: return False
|
||||
|
||||
def main():
|
||||
print(f"CWD: {os.getcwd()}")
|
||||
# Load .env from current directory
|
||||
load_dotenv(override=True)
|
||||
|
||||
rpc_url = os.environ.get("MAINNET_RPC_URL")
|
||||
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
|
||||
|
||||
if not rpc_url or not private_key:
|
||||
print("Missing RPC or Private Key.")
|
||||
return
|
||||
|
||||
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
||||
if not w3.is_connected():
|
||||
print("RPC Connection Failed")
|
||||
return
|
||||
print(f"Connected to Chain ID: {w3.eth.chain_id}")
|
||||
|
||||
account = Account.from_key(private_key)
|
||||
w3.eth.default_account = account.address
|
||||
print(f"Wallet: {account.address}")
|
||||
|
||||
npm_contract = w3.eth.contract(address=NONFUNGIBLE_POSITION_MANAGER_ADDRESS, abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
|
||||
factory_addr = npm_contract.functions.factory().call()
|
||||
factory_contract = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
|
||||
router_contract = w3.eth.contract(address=UNISWAP_V3_SWAP_ROUTER_ADDRESS, abi=SWAP_ROUTER_ABI)
|
||||
|
||||
print("\n--- STARTING LIFECYCLE MANAGER ---")
|
||||
while True:
|
||||
try:
|
||||
# 1. Get All Open Positions
|
||||
all_positions = get_all_open_positions()
|
||||
|
||||
# Check if we have an active AUTOMATIC position
|
||||
active_automatic_position = next((p for p in all_positions if p['type'] == 'AUTOMATIC'), None)
|
||||
|
||||
if all_positions:
|
||||
print("\n" + "="*60)
|
||||
print(f"Monitoring at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}")
|
||||
|
||||
for position in all_positions:
|
||||
token_id = position['token_id']
|
||||
pos_type = position['type']
|
||||
|
||||
# Fetch Details
|
||||
pos_details, pool_c = get_position_details(w3, npm_contract, factory_contract, token_id)
|
||||
if not pos_details:
|
||||
print(f"ERROR: Could not get details for Position {token_id}. Skipping.")
|
||||
continue
|
||||
|
||||
pool_data = get_pool_dynamic_data(pool_c)
|
||||
current_tick = pool_data['tick']
|
||||
|
||||
# Calculate Fees (Simulation)
|
||||
unclaimed0 = 0
|
||||
unclaimed1 = 0
|
||||
try:
|
||||
fees_sim = npm_contract.functions.collect((token_id, "0x0000000000000000000000000000000000000000", 2**128-1, 2**128-1)).call()
|
||||
unclaimed0 = from_wei(fees_sim[0], pos_details['token0_decimals'])
|
||||
unclaimed1 = from_wei(fees_sim[1], pos_details['token1_decimals'])
|
||||
except: pass
|
||||
|
||||
# Check Range
|
||||
is_out_of_range = False
|
||||
status_str = "IN RANGE"
|
||||
if current_tick < pos_details['tickLower']:
|
||||
is_out_of_range = True
|
||||
status_str = "OUT OF RANGE (BELOW)"
|
||||
elif current_tick >= pos_details['tickUpper']:
|
||||
is_out_of_range = True
|
||||
status_str = "OUT OF RANGE (ABOVE)"
|
||||
|
||||
print(f"\nID: {token_id} | Type: {pos_type} | Status: {status_str}")
|
||||
print(f" Range: {position['range_lower']:.2f} - {position['range_upper']:.2f}")
|
||||
print(f" Fees: {unclaimed0:.4f} {pos_details['token0_symbol']} / {unclaimed1:.4f} {pos_details['token1_symbol']}")
|
||||
|
||||
# --- AUTO CLOSE LOGIC (AUTOMATIC ONLY) ---
|
||||
if pos_type == 'AUTOMATIC' and CLOSE_POSITION_ENABLED and is_out_of_range:
|
||||
print(f"⚠️ Automatic Position {token_id} is OUT OF RANGE! Initiating Close...")
|
||||
liq = pos_details['liquidity']
|
||||
if liq > 0:
|
||||
if decrease_liquidity(w3, npm_contract, account, token_id, liq):
|
||||
time.sleep(5)
|
||||
collect_fees(w3, npm_contract, account, token_id)
|
||||
update_hedge_status_file("CLOSE", {'token_id': token_id})
|
||||
print("Position Closed & Status Updated.")
|
||||
# We don't break loop here, let it finish monitoring others,
|
||||
# but next main loop iteration will see it closed.
|
||||
else:
|
||||
print("Liquidity 0. Marking closed.")
|
||||
update_hedge_status_file("CLOSE", {'token_id': token_id})
|
||||
|
||||
# 2. Opening Logic (If no active automatic position)
|
||||
if not active_automatic_position and OPEN_POSITION_ENABLED:
|
||||
print("\n[OPENING] No active automatic position. Starting Open Sequence...")
|
||||
# Get Pool (WETH/USDC)
|
||||
token0 = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH
|
||||
token1 = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC
|
||||
pool_addr = factory_contract.functions.getPool(token0, token1, 500).call()
|
||||
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
|
||||
|
||||
pool_data = get_pool_dynamic_data(pool_c)
|
||||
tick = pool_data['tick']
|
||||
|
||||
# Range +/- 2%
|
||||
import math
|
||||
tick_delta = int(math.log(1 + RANGE_WIDTH_PCT) / math.log(1.0001))
|
||||
spacing = 10
|
||||
lower = (tick - tick_delta) // spacing * spacing
|
||||
upper = (tick + tick_delta) // spacing * spacing
|
||||
|
||||
# Amounts
|
||||
token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
try:
|
||||
d0 = token0_c.functions.decimals().call()
|
||||
d1 = token1_c.functions.decimals().call()
|
||||
except:
|
||||
print("Error fetching decimals")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
amt0, amt1 = calculate_mint_amounts(tick, lower, upper, TARGET_INVESTMENT_VALUE_TOKEN1, d0, d1, pool_data['sqrtPriceX96'])
|
||||
amt0_buf, amt1_buf = int(amt0 * 1.02), int(amt1 * 1.02)
|
||||
|
||||
# 4. Swap & Mint
|
||||
if check_and_swap(w3, router_contract, account, token0, token1, amt0_buf, amt1_buf):
|
||||
mint_result = mint_new_position(w3, npm_contract, account, token0, token1, amt0, amt1, lower, upper)
|
||||
|
||||
if mint_result:
|
||||
# Calculate Actual Value
|
||||
try:
|
||||
s0 = token0_c.functions.symbol().call()
|
||||
s1 = token1_c.functions.symbol().call()
|
||||
except:
|
||||
s0, s1 = "T0", "T1"
|
||||
|
||||
real_amt0 = from_wei(mint_result['amount0'], d0)
|
||||
real_amt1 = from_wei(mint_result['amount1'], d1)
|
||||
entry_price = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||
|
||||
# Value in Token1 terms (e.g. USDC)
|
||||
actual_value = (real_amt0 * entry_price) + real_amt1
|
||||
print(f"ACTUAL MINT VALUE: {actual_value:.2f} {s1}/{s0}")
|
||||
|
||||
pos_data = {
|
||||
'token_id': mint_result['token_id'],
|
||||
'entry_price': entry_price,
|
||||
'range_lower': price_from_tick(lower, d0, d1),
|
||||
'range_upper': price_from_tick(upper, d0, d1),
|
||||
'target_value': actual_value, # Save Actual Value as Target for hedging accuracy
|
||||
'amount0_initial': mint_result['amount0'],
|
||||
'amount1_initial': mint_result['amount1']
|
||||
}
|
||||
update_hedge_status_file("OPEN", pos_data)
|
||||
print("Cycle Complete. Monitoring.")
|
||||
elif not all_positions:
|
||||
print("No open positions (Manual or Automatic). Waiting...")
|
||||
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nManager stopped.")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in Main Loop: {e}")
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user