clp hedge zones
This commit is contained in:
@ -30,13 +30,28 @@ setup_logging("normal", "SCALPER_HEDGER")
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
COIN_SYMBOL = "ETH"
|
||||
CHECK_INTERVAL = 10 # Faster check for scalper
|
||||
CHECK_INTERVAL = 1 # Faster check for scalper
|
||||
LEVERAGE = 5
|
||||
STATUS_FILE = "hedge_status.json"
|
||||
|
||||
# Gap Recovery Configuration
|
||||
PRICE_BUFFER_PCT = 0.002 # 0.25% buffer
|
||||
TIME_BUFFER_SECONDS = 120 # 2 minutes wait
|
||||
# --- STRATEGY ZONES (Percent of Range Width) ---
|
||||
# Bottom Hedge Zone: 0% to 15% -> Active Hedging
|
||||
ZONE_BOTTOM_HEDGE_LIMIT = 0.1
|
||||
|
||||
# Close Zone: 15% to 20% -> Close All Hedges (Flatten)
|
||||
ZONE_CLOSE_START = 0.18
|
||||
ZONE_CLOSE_END = 0.20
|
||||
|
||||
# Middle Zone: 20% to 85% -> Idle (No new orders, keep existing)
|
||||
# Implied by gaps between other zones.
|
||||
|
||||
# Top Hedge Zone: 85% to 100% -> Active Hedging
|
||||
ZONE_TOP_HEDGE_START = 0.8
|
||||
|
||||
# --- ORDER SETTINGS ---
|
||||
PRICE_BUFFER_PCT = 0.0005 # 0.05% price move triggers order update
|
||||
MIN_THRESHOLD_ETH = 0.005 # Minimum trade size in ETH
|
||||
MIN_ORDER_VALUE_USD = 10.0 # Minimum order value for API safety
|
||||
|
||||
def get_active_automatic_position():
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
@ -51,6 +66,29 @@ def get_active_automatic_position():
|
||||
logging.error(f"ERROR reading status file: {e}")
|
||||
return None
|
||||
|
||||
def update_position_zones_in_json(token_id, zones_data):
|
||||
"""Updates the active position in JSON with calculated zone prices."""
|
||||
if not os.path.exists(STATUS_FILE): return
|
||||
try:
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
updated = False
|
||||
for entry in data:
|
||||
if entry.get('type') == 'AUTOMATIC' and entry.get('status') == 'OPEN' and entry.get('token_id') == token_id:
|
||||
# Update keys
|
||||
for k, v in zones_data.items():
|
||||
entry[k] = v
|
||||
updated = True
|
||||
break
|
||||
|
||||
if updated:
|
||||
with open(STATUS_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
logging.info(f"Updated JSON with Zone Prices for Position {token_id}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating JSON zones: {e}")
|
||||
|
||||
def round_to_sig_figs(x, sig_figs=5):
|
||||
if x == 0: return 0.0
|
||||
return round(x, sig_figs - int(math.floor(math.log10(abs(x)))) - 1)
|
||||
@ -85,7 +123,6 @@ class HyperliquidStrategy:
|
||||
self.L = 0.0
|
||||
|
||||
# Method 1: Use Amount0 (WETH)
|
||||
# Formula: L = amount0 / (1/sqrtP - 1/sqrtPb)
|
||||
if entry_amount0 > 0:
|
||||
amount0_eth = entry_amount0 / 10**18
|
||||
denom0 = (1/sqrt_P) - (1/sqrt_Pb)
|
||||
@ -93,20 +130,15 @@ class HyperliquidStrategy:
|
||||
self.L = amount0_eth / denom0
|
||||
logging.info(f"Calculated L from Amount0: {self.L:.4f}")
|
||||
|
||||
# Method 2: Use Amount1 (USDC) if Method 1 failed or yielded 0
|
||||
# Formula: L = amount1 / (sqrtP - sqrtPa)
|
||||
# Note: Price in formula is Token1/Token0? No, sqrtPrice is sqrt(Token1/Token0).
|
||||
# Yes. Amount1 = L * (sqrtP - sqrtPa)
|
||||
# Method 2: Use Amount1 (USDC)
|
||||
if self.L == 0.0 and entry_amount1 > 0:
|
||||
amount1_usdc = entry_amount1 / 10**6 # USDC is 6 decimals
|
||||
amount1_usdc = entry_amount1 / 10**6
|
||||
denom1 = sqrt_P - sqrt_Pa
|
||||
if denom1 > 0.00000001:
|
||||
self.L = amount1_usdc / denom1
|
||||
logging.info(f"Calculated L from Amount1: {self.L:.4f}")
|
||||
|
||||
# Method 3: Fallback Heuristic using Target Value
|
||||
# Max ETH = Value / LowerPrice.
|
||||
# L = MaxETH / (1/sqrtPa - 1/sqrtPb)
|
||||
# Method 3: Fallback Heuristic
|
||||
if self.L == 0.0:
|
||||
logging.warning("Amounts missing or 0. Using Target Value Heuristic.")
|
||||
max_eth_heuristic = target_value / low_range
|
||||
@ -137,33 +169,7 @@ class HyperliquidStrategy:
|
||||
pool_delta = self.get_pool_delta(current_price)
|
||||
raw_target_short = pool_delta + self.static_long
|
||||
|
||||
entry_upper = self.entry_price * (1 + PRICE_BUFFER_PCT)
|
||||
entry_lower = self.entry_price * (1 - PRICE_BUFFER_PCT)
|
||||
|
||||
desired_mode = self.current_mode
|
||||
|
||||
if self.current_mode == "NORMAL":
|
||||
if current_price > entry_upper and current_price < self.recovery_target:
|
||||
desired_mode = "RECOVERY"
|
||||
elif self.current_mode == "RECOVERY":
|
||||
if current_price < entry_lower or current_price >= self.recovery_target:
|
||||
desired_mode = "NORMAL"
|
||||
|
||||
now = time.time()
|
||||
if desired_mode != self.current_mode:
|
||||
if (now - self.last_switch_time) >= TIME_BUFFER_SECONDS:
|
||||
logging.info(f"🔄 MODE SWITCH: {self.current_mode} -> {desired_mode} (Px: {current_price:.2f})")
|
||||
self.current_mode = desired_mode
|
||||
self.last_switch_time = now
|
||||
else:
|
||||
logging.info(f"⏳ Mode Switch Delayed (Time Buffer). Pending: {desired_mode}")
|
||||
|
||||
if self.current_mode == "RECOVERY":
|
||||
target_short_size = 0.0
|
||||
logging.info(f"🩹 RECOVERY MODE ACTIVE (0% Hedge). Target: {self.recovery_target:.2f}")
|
||||
else:
|
||||
target_short_size = raw_target_short
|
||||
|
||||
target_short_size = raw_target_short
|
||||
diff = target_short_size - abs(current_short_position_size)
|
||||
|
||||
return {
|
||||
@ -173,7 +179,7 @@ class HyperliquidStrategy:
|
||||
"current_short": abs(current_short_position_size),
|
||||
"diff": diff,
|
||||
"action": "SELL" if diff > 0 else "BUY",
|
||||
"mode": self.current_mode
|
||||
"mode": "NORMAL"
|
||||
}
|
||||
|
||||
class ScalperHedger:
|
||||
@ -198,6 +204,8 @@ class ScalperHedger:
|
||||
self.strategy = None
|
||||
self.sz_decimals = self._get_sz_decimals(COIN_SYMBOL)
|
||||
self.active_position_id = None
|
||||
self.active_order = None
|
||||
|
||||
logging.info(f"Scalper Hedger initialized. Agent: {self.account.address}")
|
||||
|
||||
def _init_strategy(self, position_data):
|
||||
@ -249,6 +257,20 @@ class ScalperHedger:
|
||||
except: pass
|
||||
return None
|
||||
|
||||
def get_order_book_mid(self, coin):
|
||||
try:
|
||||
l2_snapshot = self.info.l2_snapshot(coin)
|
||||
if l2_snapshot and 'levels' in l2_snapshot:
|
||||
bids = l2_snapshot['levels'][0]
|
||||
asks = l2_snapshot['levels'][1]
|
||||
if bids and asks:
|
||||
best_bid = float(bids[0]['px'])
|
||||
best_ask = float(asks[0]['px'])
|
||||
return (best_bid + best_ask) / 2
|
||||
return self.get_market_price(coin)
|
||||
except:
|
||||
return self.get_market_price(coin)
|
||||
|
||||
def get_funding_rate(self, coin):
|
||||
try:
|
||||
meta, asset_ctxs = self.info.meta_and_asset_ctxs()
|
||||
@ -267,29 +289,101 @@ class ScalperHedger:
|
||||
return 0.0
|
||||
except: return 0.0
|
||||
|
||||
def execute_trade(self, coin, is_buy, size, price):
|
||||
logging.info(f"🚀 EXECUTING: {coin} {'BUY' if is_buy else 'SELL'} {size} @ ~{price}")
|
||||
def get_open_orders(self):
|
||||
try:
|
||||
return self.info.open_orders(self.vault_address or self.account.address)
|
||||
except: return []
|
||||
|
||||
def cancel_order(self, coin, oid):
|
||||
logging.info(f"Cancelling order {oid}...")
|
||||
try:
|
||||
return self.exchange.cancel(coin, oid)
|
||||
except Exception as e:
|
||||
logging.error(f"Error cancelling order: {e}")
|
||||
|
||||
def place_limit_order(self, coin, is_buy, size, price):
|
||||
logging.info(f"🕒 PLACING LIMIT: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price:.2f}")
|
||||
reduce_only = is_buy
|
||||
try:
|
||||
raw_limit_px = price * (1.05 if is_buy else 0.95)
|
||||
limit_px = round_to_sig_figs(raw_limit_px, 5)
|
||||
# Gtc order (Maker)
|
||||
limit_px = round_to_sig_figs(price, 5)
|
||||
|
||||
order_result = self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Ioc"}}, reduce_only=reduce_only)
|
||||
order_result = self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Gtc"}}, reduce_only=reduce_only)
|
||||
status = order_result["status"]
|
||||
if status == "ok":
|
||||
response_data = order_result["response"]["data"]
|
||||
if "statuses" in response_data and "error" in response_data["statuses"][0]:
|
||||
logging.error(f"Order API Error: {response_data['statuses'][0]['error']}")
|
||||
else:
|
||||
logging.info(f"✅ Trade Success")
|
||||
if "statuses" in response_data:
|
||||
status_obj = response_data["statuses"][0]
|
||||
|
||||
if "error" in status_obj:
|
||||
logging.error(f"Order API Error: {status_obj['error']}")
|
||||
return None
|
||||
|
||||
# Parse OID from nested structure
|
||||
oid = None
|
||||
if "resting" in status_obj:
|
||||
oid = status_obj["resting"]["oid"]
|
||||
elif "filled" in status_obj:
|
||||
oid = status_obj["filled"]["oid"]
|
||||
logging.info("Order filled immediately.")
|
||||
|
||||
if oid:
|
||||
logging.info(f"✅ Limit Order Placed: OID {oid}")
|
||||
return oid
|
||||
else:
|
||||
logging.warning(f"Order placed but OID not found in: {status_obj}")
|
||||
return None
|
||||
else:
|
||||
logging.error(f"Order Failed: {order_result}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"Exception during trade: {e}")
|
||||
return None
|
||||
|
||||
def manage_orders(self):
|
||||
"""
|
||||
Checks open orders.
|
||||
Returns: True if an order exists and is valid (don't trade), False if no order (can trade).
|
||||
"""
|
||||
open_orders = self.get_open_orders()
|
||||
my_orders = [o for o in open_orders if o['coin'] == COIN_SYMBOL]
|
||||
|
||||
if not my_orders:
|
||||
self.active_order = None
|
||||
return False
|
||||
|
||||
if len(my_orders) > 1:
|
||||
logging.warning("Multiple open orders found. Cancelling all for safety.")
|
||||
for o in my_orders:
|
||||
self.cancel_order(COIN_SYMBOL, o['oid'])
|
||||
self.active_order = None
|
||||
return False
|
||||
|
||||
order = my_orders[0]
|
||||
oid = order['oid']
|
||||
order_price = float(order['limitPx'])
|
||||
|
||||
current_mid = self.get_order_book_mid(COIN_SYMBOL)
|
||||
pct_diff = abs(current_mid - order_price) / order_price
|
||||
|
||||
if pct_diff > PRICE_BUFFER_PCT:
|
||||
logging.info(f"Price moved {pct_diff*100:.3f}% > {PRICE_BUFFER_PCT*100}%. Cancelling/Replacing order {oid}.")
|
||||
self.cancel_order(COIN_SYMBOL, oid)
|
||||
self.active_order = None
|
||||
return False
|
||||
else:
|
||||
logging.info(f"Pending Order {oid} @ {order_price:.2f} is within range ({pct_diff*100:.3f}%). Waiting.")
|
||||
return True
|
||||
|
||||
def close_all_positions(self):
|
||||
logging.info("Closing all positions (Safety/Closed State)...")
|
||||
logging.info("Closing all positions (Market Order)...")
|
||||
try:
|
||||
# Cancel open orders first
|
||||
open_orders = self.get_open_orders()
|
||||
for o in open_orders:
|
||||
if o['coin'] == COIN_SYMBOL:
|
||||
self.cancel_order(COIN_SYMBOL, o['oid'])
|
||||
|
||||
price = self.get_market_price(COIN_SYMBOL)
|
||||
current_pos = self.get_current_position(COIN_SYMBOL)
|
||||
if current_pos == 0: return
|
||||
@ -298,7 +392,8 @@ class ScalperHedger:
|
||||
final_size = round_to_sz_decimals(abs(current_pos), self.sz_decimals)
|
||||
if final_size == 0: return
|
||||
|
||||
self.execute_trade(COIN_SYMBOL, is_buy, final_size, price)
|
||||
# Market order for closing
|
||||
self.exchange.order(COIN_SYMBOL, is_buy, final_size, round_to_sig_figs(price * (1.05 if is_buy else 0.95), 5), {"limit": {"tif": "Ioc"}}, reduce_only=True)
|
||||
self.active_position_id = None
|
||||
except Exception as e:
|
||||
logging.error(f"Error closing: {e}")
|
||||
@ -321,7 +416,6 @@ class ScalperHedger:
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
continue
|
||||
|
||||
# Initialize Strategy if needed
|
||||
if self.strategy is None or self.active_position_id != active_pos['token_id']:
|
||||
logging.info(f"New position {active_pos['token_id']} detected or strategy not initialized. Initializing strategy.")
|
||||
self._init_strategy(active_pos)
|
||||
@ -329,12 +423,15 @@ class ScalperHedger:
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
continue
|
||||
|
||||
# Double Check Strategy validity
|
||||
if self.strategy is None:
|
||||
if self.strategy is None: continue
|
||||
|
||||
# --- ORDER MANAGEMENT ---
|
||||
if self.manage_orders():
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
continue
|
||||
|
||||
# 2. Market Data
|
||||
price = self.get_market_price(COIN_SYMBOL)
|
||||
price = self.get_order_book_mid(COIN_SYMBOL)
|
||||
if price is None:
|
||||
time.sleep(5)
|
||||
continue
|
||||
@ -342,7 +439,7 @@ class ScalperHedger:
|
||||
funding_rate = self.get_funding_rate(COIN_SYMBOL)
|
||||
current_pos_size = self.get_current_position(COIN_SYMBOL)
|
||||
|
||||
# 3. Calculate
|
||||
# 3. Calculate Logic
|
||||
calc = self.strategy.calculate_rebalance(price, current_pos_size)
|
||||
diff_abs = abs(calc['diff'])
|
||||
|
||||
@ -351,26 +448,68 @@ class ScalperHedger:
|
||||
sqrt_Pb = math.sqrt(self.strategy.high_range)
|
||||
max_potential_eth = self.strategy.L * ((1/sqrt_Pa) - (1/sqrt_Pb))
|
||||
|
||||
min_threshold = 0.001
|
||||
rebalance_threshold = max(min_threshold, max_potential_eth * 0.05)
|
||||
# Use MIN_THRESHOLD_ETH from config
|
||||
rebalance_threshold = max(MIN_THRESHOLD_ETH, max_potential_eth * 0.05)
|
||||
|
||||
# 5. Execute with Min Order Value check
|
||||
if diff_abs > rebalance_threshold:
|
||||
trade_size = round_to_sz_decimals(diff_abs, self.sz_decimals)
|
||||
|
||||
min_order_value_usd = 10.0
|
||||
min_trade_size = min_order_value_usd / price
|
||||
|
||||
if trade_size < min_trade_size:
|
||||
logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${min_order_value_usd})")
|
||||
elif trade_size > 0:
|
||||
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f})")
|
||||
is_buy = (calc['action'] == "BUY")
|
||||
self.execute_trade(COIN_SYMBOL, is_buy, trade_size, price)
|
||||
# 5. Determine Hedge Zone
|
||||
clp_low_range = self.strategy.low_range
|
||||
clp_high_range = self.strategy.high_range
|
||||
range_width = clp_high_range - clp_low_range
|
||||
|
||||
# Calculate Prices for Zones
|
||||
zone_bottom_limit_price = clp_low_range + (range_width * ZONE_BOTTOM_HEDGE_LIMIT)
|
||||
zone_close_start_price = clp_low_range + (range_width * ZONE_CLOSE_START)
|
||||
zone_close_end_price = clp_low_range + (range_width * ZONE_CLOSE_END)
|
||||
zone_top_start_price = clp_low_range + (range_width * ZONE_TOP_HEDGE_START)
|
||||
|
||||
# Update JSON with zone prices if missing
|
||||
if 'zone_bottom_limit_price' not in active_pos:
|
||||
update_position_zones_in_json(active_pos['token_id'], {
|
||||
'zone_bottom_limit_price': zone_bottom_limit_price,
|
||||
'zone_close_start_price': zone_close_start_price,
|
||||
'zone_close_end_price': zone_close_end_price,
|
||||
'zone_top_start_price': zone_top_start_price
|
||||
})
|
||||
|
||||
# Check Zones
|
||||
in_close_zone = (price >= zone_close_start_price and price <= zone_close_end_price)
|
||||
in_hedge_zone = (price <= zone_bottom_limit_price) or (price >= zone_top_start_price)
|
||||
|
||||
# --- Execute Logic ---
|
||||
if in_close_zone:
|
||||
logging.info(f"ZONE: CLOSE ({price:.2f} in {zone_close_start_price:.2f}-{zone_close_end_price:.2f}). Closing all hedge positions.")
|
||||
self.close_all_positions()
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
continue
|
||||
|
||||
elif in_hedge_zone:
|
||||
# HEDGE NORMALLY
|
||||
if diff_abs > rebalance_threshold:
|
||||
trade_size = round_to_sz_decimals(diff_abs, self.sz_decimals)
|
||||
|
||||
# --- SOFT START LOGIC (Bottom Zone Only) ---
|
||||
# If in Bottom Zone, opening a NEW Short (SELL), and current position is 0 -> Cut size by 50%
|
||||
if (price <= zone_bottom_limit_price) and (current_pos_size == 0) and (calc['action'] == "SELL"):
|
||||
logging.info(f"🔰 SOFT START: Reducing initial hedge size by 50% in Bottom Zone.")
|
||||
trade_size = round_to_sz_decimals(trade_size * 0.5, self.sz_decimals)
|
||||
|
||||
min_trade_size = MIN_ORDER_VALUE_USD / price
|
||||
|
||||
if trade_size < min_trade_size:
|
||||
logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${MIN_ORDER_VALUE_USD:.2f})")
|
||||
elif trade_size > 0:
|
||||
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone.")
|
||||
is_buy = (calc['action'] == "BUY")
|
||||
self.place_limit_order(COIN_SYMBOL, is_buy, trade_size, price)
|
||||
else:
|
||||
logging.info("Trade size rounds to 0. Skipping.")
|
||||
else:
|
||||
logging.info("Trade size rounds to 0. Skipping.")
|
||||
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone.")
|
||||
|
||||
else:
|
||||
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}")
|
||||
# MIDDLE ZONE (IDLE)
|
||||
pct_position = (price - clp_low_range) / range_width
|
||||
logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). No Actions.")
|
||||
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5154921,
|
||||
"status": "OPEN",
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3088.180203068298,
|
||||
"range_lower": 3071.745207606606,
|
||||
"range_upper": 3102.615208978462,
|
||||
@ -11,6 +11,368 @@
|
||||
"amount1_initial": 0,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765575924,
|
||||
"timestamp_close": 1765613747
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155502,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3105.4778071503983,
|
||||
"range_lower": 3090.230154007496,
|
||||
"range_upper": 3118.1663529424395,
|
||||
"target_value": 81.22159710646565,
|
||||
"amount0_initial": 0,
|
||||
"amount1_initial": 0,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765613789,
|
||||
"timestamp_close": 1765614083
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155511,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3122.1562247614547,
|
||||
"range_lower": 3105.7192207366634,
|
||||
"range_upper": 3136.930649460415,
|
||||
"target_value": 98.20653967768193,
|
||||
"amount0_initial": 0,
|
||||
"amount1_initial": 0,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765614124,
|
||||
"timestamp_close": 1765617105
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155580,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3120.03330314008,
|
||||
"range_lower": 3111.93656358668,
|
||||
"range_upper": 3124.4086137206154,
|
||||
"target_value": 258.2420686245357,
|
||||
"amount0_initial": 0,
|
||||
"amount1_initial": 0,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765617197,
|
||||
"timestamp_close": 1765617236
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155610,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3118.03462860249,
|
||||
"range_lower": 3056.425578524254,
|
||||
"range_upper": 3177.9749053788623,
|
||||
"target_value": 348.982123656927,
|
||||
"amount0_initial": 54654586929109032,
|
||||
"amount1_initial": 178567229,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765619246,
|
||||
"timestamp_close": null
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155618,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3120.854321555066,
|
||||
"range_lower": 3111.93656358668,
|
||||
"range_upper": 3127.5344286932063,
|
||||
"target_value": 342.45943993806645,
|
||||
"amount0_initial": 46935127322790001,
|
||||
"amount1_initial": 195981745,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765619616,
|
||||
"timestamp_close": 1765621159
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155660,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3129.521502331058,
|
||||
"range_lower": 3121.285922844486,
|
||||
"range_upper": 3136.930649460415,
|
||||
"target_value": 345.19101843135434,
|
||||
"amount0_initial": 52148054681776174,
|
||||
"amount1_initial": 181992560,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765621204,
|
||||
"timestamp_close": 1765625900
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155742,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3120.452464830275,
|
||||
"range_lower": 3111.93656358668,
|
||||
"range_upper": 3127.5344286932063,
|
||||
"target_value": 330.2607520468071,
|
||||
"amount0_initial": 45273020063291068,
|
||||
"amount1_initial": 188988445,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765625947,
|
||||
"timestamp_close": 1765629916
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155807,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3111.8306135157013,
|
||||
"range_lower": 3102.615208978462,
|
||||
"range_upper": 3118.1663529424395,
|
||||
"target_value": 342.2298529154781,
|
||||
"amount0_initial": 44749390699692539,
|
||||
"amount1_initial": 202977329,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765629968,
|
||||
"timestamp_close": null
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155828,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3116.7126648332624,
|
||||
"range_lower": 3099.514299525495,
|
||||
"range_upper": 3130.663370887762,
|
||||
"target_value": 347.83537144876755,
|
||||
"amount0_initial": 49847371623870561,
|
||||
"amount1_initial": 192475437,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765630905,
|
||||
"timestamp_close": 1765632623
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155863,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3097.40295247475,
|
||||
"range_lower": 3080.973817800786,
|
||||
"range_upper": 3111.93656358668,
|
||||
"target_value": 308.3116676933205,
|
||||
"amount0_initial": 39654626336294149,
|
||||
"amount1_initial": 185485311,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765632672,
|
||||
"timestamp_close": 1765634422
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5155882,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3112.8609359236384,
|
||||
"range_lower": 3096.4164892771637,
|
||||
"range_upper": 3127.5344286932063,
|
||||
"target_value": 343.5299941433273,
|
||||
"amount0_initial": 51896697111974758,
|
||||
"amount1_initial": 181982793,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765634468,
|
||||
"timestamp_close": 1765661569
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156323,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3083.0072388847652,
|
||||
"range_lower": 3065.6081631285606,
|
||||
"range_upper": 3096.4164892771637,
|
||||
"target_value": 312.46495296583043,
|
||||
"amount0_initial": 37786473705449745,
|
||||
"amount1_initial": 195968981,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765661623,
|
||||
"timestamp_close": 1765661755
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156327,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3099.025060823837,
|
||||
"range_lower": 3080.973817800786,
|
||||
"range_upper": 3111.93656358668,
|
||||
"target_value": 341.5043895497362,
|
||||
"amount0_initial": 44705050404757454,
|
||||
"amount1_initial": 202962318,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765661800,
|
||||
"timestamp_close": 1765663051
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156339,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3114.5494347315303,
|
||||
"range_lower": 3096.4164892771637,
|
||||
"range_upper": 3127.5344286932063,
|
||||
"target_value": 313.18766451496026,
|
||||
"amount0_initial": 47209859594870944,
|
||||
"amount1_initial": 166150223,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765663096,
|
||||
"timestamp_close": 1765675725,
|
||||
"zone_bottom_limit_price": 3099.528283218768,
|
||||
"zone_close_start_price": 3102.017718372051,
|
||||
"zone_close_end_price": 3102.640077160372,
|
||||
"zone_top_start_price": 3121.310840809998
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156507,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3128.29006521609,
|
||||
"range_lower": 3111.93656358668,
|
||||
"range_upper": 3143.2104745051906,
|
||||
"target_value": 347.15268590066694,
|
||||
"amount0_initial": 52797230582023401,
|
||||
"amount1_initial": 181987634,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765675770,
|
||||
"timestamp_close": 1765687389,
|
||||
"zone_bottom_limit_price": 3115.0639546785314,
|
||||
"zone_close_start_price": 3117.565867552012,
|
||||
"zone_close_end_price": 3118.191345770382,
|
||||
"zone_top_start_price": 3136.9556923214886
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156576,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3109.1484174484244,
|
||||
"range_lower": 3093.3217751359653,
|
||||
"range_upper": 3124.4086137206154,
|
||||
"target_value": 349.75269804513647,
|
||||
"amount0_initial": 55081765825023475,
|
||||
"amount1_initial": 178495313,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765687433,
|
||||
"timestamp_close": 1765712073,
|
||||
"zone_bottom_limit_price": 3096.4304589944304,
|
||||
"zone_close_start_price": 3098.9174060812024,
|
||||
"zone_close_end_price": 3099.539142852895,
|
||||
"zone_top_start_price": 3118.1912460036856
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156880,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3092.1804685415204,
|
||||
"range_lower": 3074.8183354682296,
|
||||
"range_upper": 3105.7192207366634,
|
||||
"target_value": 348.0802699013006,
|
||||
"amount0_initial": 49191436738181486,
|
||||
"amount1_initial": 195971470,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765712124,
|
||||
"timestamp_close": 1765712700,
|
||||
"zone_bottom_limit_price": 3077.908423995073,
|
||||
"zone_close_start_price": 3080.3804948165475,
|
||||
"zone_close_end_price": 3080.9985125219164,
|
||||
"zone_top_start_price": 3099.5390436829766
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156912,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3080.3709911881006,
|
||||
"range_lower": 3062.5442403757074,
|
||||
"range_upper": 3093.3217751359653,
|
||||
"target_value": 291.15223765283383,
|
||||
"amount0_initial": 47732710466839755,
|
||||
"amount1_initial": 144117781,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765712910,
|
||||
"timestamp_close": 1765714350,
|
||||
"zone_bottom_limit_price": 3065.6219938517334,
|
||||
"zone_close_start_price": 3068.084196632554,
|
||||
"zone_close_end_price": 3068.699747327759,
|
||||
"zone_top_start_price": 3087.166268183914
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5156972,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3090.0637108037877,
|
||||
"range_lower": 3074.8183354682296,
|
||||
"range_upper": 3102.615208978462,
|
||||
"target_value": 271.3892587233541,
|
||||
"amount0_initial": 51605992189032833,
|
||||
"amount1_initial": 111923455,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765714399,
|
||||
"timestamp_close": 1765715701,
|
||||
"zone_bottom_limit_price": 3077.598022819253,
|
||||
"zone_close_start_price": 3079.8217727000715,
|
||||
"zone_close_end_price": 3080.3777101702763,
|
||||
"zone_top_start_price": 3097.055834276415
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5157018,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3101.5146208910464,
|
||||
"range_lower": 3084.056178426586,
|
||||
"range_upper": 3115.0499008952183,
|
||||
"target_value": 334.88770454868376,
|
||||
"amount0_initial": 49662753969037209,
|
||||
"amount1_initial": 180857947,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765715747,
|
||||
"timestamp_close": 1765722919,
|
||||
"zone_bottom_limit_price": 3087.1555506734494,
|
||||
"zone_close_start_price": 3089.6350484709396,
|
||||
"zone_close_end_price": 3090.2549229203123,
|
||||
"zone_top_start_price": 3108.851156401492
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5157176,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3079.8157532039463,
|
||||
"range_lower": 3062.5442403757074,
|
||||
"range_upper": 3093.3217751359653,
|
||||
"target_value": 272.62430135026136,
|
||||
"amount0_initial": 24888578243851017,
|
||||
"amount1_initial": 195972066,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765722970,
|
||||
"timestamp_close": 1765729241,
|
||||
"zone_bottom_limit_price": 3065.6219938517334,
|
||||
"zone_close_start_price": 3068.084196632554,
|
||||
"zone_close_end_price": 3068.699747327759,
|
||||
"zone_top_start_price": 3087.166268183914
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5157312,
|
||||
"status": "CLOSED",
|
||||
"entry_price": 3093.971464080226,
|
||||
"range_lower": 3077.8945378409912,
|
||||
"range_upper": 3108.8263379038003,
|
||||
"target_value": 326.92184420403566,
|
||||
"amount0_initial": 46843176767023226,
|
||||
"amount1_initial": 181990392,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765729286,
|
||||
"timestamp_close": 1765733514,
|
||||
"zone_bottom_limit_price": 3080.987717847272,
|
||||
"zone_close_start_price": 3083.4622618522967,
|
||||
"zone_close_end_price": 3084.080897853553,
|
||||
"zone_top_start_price": 3102.6399778912387
|
||||
},
|
||||
{
|
||||
"type": "AUTOMATIC",
|
||||
"token_id": 5157395,
|
||||
"status": "OPEN",
|
||||
"entry_price": 3079.3931567773757,
|
||||
"range_lower": 3062.5442403757074,
|
||||
"range_upper": 3093.3217751359653,
|
||||
"target_value": 344.4599070677894,
|
||||
"amount0_initial": 50492037278704046,
|
||||
"amount1_initial": 188975073,
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": 1765733564,
|
||||
"timestamp_close": null,
|
||||
"zone_bottom_limit_price": 3065.6219938517334,
|
||||
"zone_close_start_price": 3068.084196632554,
|
||||
"zone_close_end_price": 3068.699747327759,
|
||||
"zone_top_start_price": 3087.166268183914
|
||||
}
|
||||
]
|
||||
@ -12,11 +12,13 @@ def clean_address(addr):
|
||||
|
||||
def price_from_sqrt_price_x96(sqrt_price_x96, token0_decimals, token1_decimals):
|
||||
price = (sqrt_price_x96 / (2**96))**2
|
||||
# Adjust for token decimals assuming price is Token1 per Token0
|
||||
price = price * (10**(token0_decimals - token1_decimals))
|
||||
return price
|
||||
|
||||
def price_from_tick(tick, token0_decimals, token1_decimals):
|
||||
price = 1.0001**tick
|
||||
# Adjust for token decimals assuming price is Token1 per Token0
|
||||
price = price * (10**(token0_decimals - token1_decimals))
|
||||
return price
|
||||
|
||||
@ -25,54 +27,71 @@ def from_wei(amount, decimals):
|
||||
|
||||
# --- V3 Math Helpers ---
|
||||
def get_sqrt_ratio_at_tick(tick):
|
||||
# Returns sqrt(price) as a Q96 number
|
||||
return int((1.0001 ** (tick / 2)) * (2 ** 96))
|
||||
|
||||
def get_liquidity_for_amount0(sqrt_ratio_a, sqrt_ratio_b, amount0):
|
||||
# This function is not used directly in the current calculate_mint_amounts logic,
|
||||
# but is a common V3 helper
|
||||
if sqrt_ratio_a > sqrt_ratio_b:
|
||||
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||
# This formula is for a single-sided deposit when current price is outside the range
|
||||
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):
|
||||
# This function is not used directly in the current calculate_mint_amounts logic,
|
||||
# but is a common V3 helper
|
||||
if sqrt_ratio_a > sqrt_ratio_b:
|
||||
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||
# This formula is for a single-sided deposit when current price is outside the range
|
||||
return int(amount1 / (sqrt_ratio_b - sqrt_ratio_a))
|
||||
|
||||
def get_amounts_for_liquidity(sqrt_ratio_current, sqrt_ratio_a, sqrt_ratio_b, liquidity):
|
||||
# Calculates the required amount of token0 and token1 for a given liquidity and price range
|
||||
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
|
||||
Q96 = 1 << 96 # 2^96
|
||||
|
||||
# Current price below the lower tick boundary
|
||||
if sqrt_ratio_current <= sqrt_ratio_a:
|
||||
amount0 = ((liquidity * Q96) // sqrt_ratio_a) - ((liquidity * Q96) // sqrt_ratio_b)
|
||||
amount1 = 0
|
||||
# Current price within the range
|
||||
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
|
||||
# Current price above the upper tick boundary
|
||||
else:
|
||||
amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96
|
||||
amount0 = 0
|
||||
|
||||
return amount0, amount1
|
||||
|
||||
# --- Configuration ---
|
||||
# RPC URL and Private Key are loaded from .env
|
||||
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")
|
||||
|
||||
# Script behavior flags
|
||||
MONITOR_INTERVAL_SECONDS = 30
|
||||
COLLECT_FEES_ENABLED = False
|
||||
CLOSE_POSITION_ENABLED = True
|
||||
CLOSE_IF_OUT_OF_RANGE_ONLY = True
|
||||
COLLECT_FEES_ENABLED = False # If True, will attempt to collect fees once and exit if no open auto position
|
||||
CLOSE_POSITION_ENABLED = True # If True, will attempt to close auto position when out of range
|
||||
CLOSE_IF_OUT_OF_RANGE_ONLY = True # If True, closes only if out of range; if False, closes immediately
|
||||
OPEN_POSITION_ENABLED = True # If True, will open a new position if no auto position exists
|
||||
|
||||
OPEN_POSITION_ENABLED = True
|
||||
TARGET_INVESTMENT_VALUE_TOKEN1 = 100.0
|
||||
RANGE_WIDTH_PCT = 0.005
|
||||
# New Position Parameters
|
||||
TARGET_INVESTMENT_VALUE_TOKEN1 = 350.0 # Target total investment value in Token1 terms (e.g. 350 USDC)
|
||||
RANGE_WIDTH_PCT = 0.005 # +/- 2% range for new positions
|
||||
|
||||
# JSON File for tracking position state
|
||||
STATUS_FILE = "hedge_status.json"
|
||||
|
||||
# --- JSON State Helpers ---
|
||||
def get_active_automatic_position():
|
||||
"""Reads hedge_status.json and returns the first OPEN AUTOMATIC position dict, or None."""
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
return None
|
||||
try:
|
||||
@ -98,6 +117,11 @@ def get_all_open_positions():
|
||||
return []
|
||||
|
||||
def update_hedge_status_file(action, position_data):
|
||||
"""
|
||||
Updates the hedge_status.json file.
|
||||
action: "OPEN" or "CLOSE"
|
||||
position_data: Dict containing details (token_id, entry_price, range, etc.)
|
||||
"""
|
||||
current_data = []
|
||||
if os.path.exists(STATUS_FILE):
|
||||
try:
|
||||
@ -114,9 +138,9 @@ def update_hedge_status_file(action, position_data):
|
||||
"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),
|
||||
"target_value": position_data.get('target_value', 0.0), # Save Actual Value as Target for hedging accuracy
|
||||
"amount0_initial": position_data.get('amount0_initial', 0),
|
||||
"amount1_initial": position_data.get('amount1_initial', 0),
|
||||
"static_long": 0.0,
|
||||
"timestamp_open": int(time.time()),
|
||||
"timestamp_close": None
|
||||
@ -148,6 +172,8 @@ def update_hedge_status_file(action, position_data):
|
||||
# Simplified for length, usually loaded from huge string
|
||||
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||
[
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
|
||||
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
|
||||
{"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"},
|
||||
@ -243,27 +269,27 @@ def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_valu
|
||||
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)
|
||||
# 1. Get Price of Token0 in terms of Token1
|
||||
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)
|
||||
# 2. Estimate Amounts
|
||||
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)
|
||||
# 3. Adjust for decimals
|
||||
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)
|
||||
# 4. Calculate Total Value of Test Position in Token1 terms
|
||||
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
|
||||
# 5. Scale
|
||||
scale = investment_value_token1 / value_test
|
||||
|
||||
# 6. Calculate Final Amounts (raw integer units for contract call)
|
||||
# 6. Final Amounts
|
||||
final_amt0 = int(amt0_test * scale)
|
||||
final_amt1 = int(amt1_test * scale)
|
||||
|
||||
@ -275,87 +301,73 @@ def check_and_swap(w3_instance, router_contract, account, token0, token1, amount
|
||||
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||
|
||||
# Debug Balances
|
||||
s0 = token0_contract.functions.symbol().call()
|
||||
s1 = token1_contract.functions.symbol().call()
|
||||
d0 = token0_contract.functions.decimals().call()
|
||||
d1 = token1_contract.functions.decimals().call()
|
||||
|
||||
print(f"\n--- WALLET CHECK ---")
|
||||
print(f"Required: {from_wei(amount0_needed, d0):.6f} {s0} | {from_wei(amount1_needed, d1):.2f} {s1}")
|
||||
print(f"Balance : {from_wei(bal0, d0):.6f} {s0} | {from_wei(bal1, d1):.2f} {s1}")
|
||||
|
||||
deficit0 = max(0, amount0_needed - bal0)
|
||||
deficit1 = max(0, amount1_needed - bal1)
|
||||
|
||||
if deficit0 > 0: print(f"Deficit {s0}: {from_wei(deficit0, d0):.6f}")
|
||||
if deficit1 > 0: print(f"Deficit {s1}: {from_wei(deficit1, d1):.2f}")
|
||||
|
||||
# --- 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)
|
||||
# Wrap for Token0 Deficit
|
||||
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
|
||||
gas_reserve = 5 * 10**15 # 0.005 ETH (Reduced for L2)
|
||||
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
|
||||
'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)}")
|
||||
print(f"Insufficient Native ETH to wrap. Need: {from_wei(deficit0, 18)}, Available: {from_wei(available_native, 18)}")
|
||||
|
||||
# Check Token1 (Deficit1) - Assuming Token1 could be WETH too
|
||||
# Wrap for Token1 Deficit (if Token1 is WETH)
|
||||
if deficit1 > 0 and token1.lower() == weth_addr_lower:
|
||||
native_bal = w3_instance.eth.get_balance(account.address)
|
||||
gas_reserve = 10**16
|
||||
gas_reserve = 5 * 10**15 # 0.005 ETH
|
||||
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
|
||||
'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)
|
||||
|
||||
@ -387,6 +399,12 @@ def check_and_swap(w3_instance, router_contract, account, token0, token1, amount
|
||||
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)
|
||||
|
||||
# Verify Balance After Swap
|
||||
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||
if bal0 < amount0_needed:
|
||||
print(f"❌ Swap insufficient. Have {bal0}, Need {amount0_needed}")
|
||||
return False
|
||||
return True
|
||||
|
||||
elif deficit1 > 0 and bal0 > amount0_needed:
|
||||
@ -414,6 +432,12 @@ def check_and_swap(w3_instance, router_contract, account, token0, token1, amount
|
||||
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)
|
||||
|
||||
# Verify Balance After Swap
|
||||
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||
if bal1 < amount1_needed:
|
||||
print(f"❌ Swap insufficient. Have {bal1}, Need {amount1_needed}")
|
||||
return False
|
||||
return True
|
||||
|
||||
print("❌ Insufficient funds for required amounts.")
|
||||
@ -421,8 +445,8 @@ def check_and_swap(w3_instance, router_contract, account, token0, token1, amount
|
||||
|
||||
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)
|
||||
token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
b0 = token0_contract.functions.balanceOf(account_address).call()
|
||||
b1 = token1_contract.functions.balanceOf(account_address).call()
|
||||
return b0, b1
|
||||
@ -486,36 +510,29 @@ def mint_new_position(w3_instance, npm_contract, account, token0, token1, amount
|
||||
|
||||
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", "")
|
||||
# Web3.py Event Processing to capture ID and Amounts
|
||||
try:
|
||||
# 1. Capture Token ID from Transfer event
|
||||
transfer_events = npm_contract.events.Transfer().process_receipt(receipt)
|
||||
for event in transfer_events:
|
||||
if event['args']['from'] == "0x0000000000000000000000000000000000000000":
|
||||
result_data['token_id'] = event['args']['tokenId']
|
||||
break
|
||||
|
||||
# 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]
|
||||
# 2. Capture Amounts from IncreaseLiquidity event
|
||||
inc_liq_events = npm_contract.events.IncreaseLiquidity().process_receipt(receipt)
|
||||
for event in inc_liq_events:
|
||||
if result_data['token_id'] and event['args']['tokenId'] == result_data['token_id']:
|
||||
result_data['amount0'] = event['args']['amount0']
|
||||
result_data['amount1'] = event['args']['amount1']
|
||||
result_data['liquidity'] = event['args']['liquidity']
|
||||
break
|
||||
|
||||
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")
|
||||
except Exception as e:
|
||||
print(f"Event Processing Warning: {e}")
|
||||
|
||||
if result_data['token_id']:
|
||||
print(f"Captured: ID {result_data['token_id']}, Amt0 {result_data['amount0']}, Amt1 {result_data['amount1']}")
|
||||
return result_data
|
||||
|
||||
return None
|
||||
@ -573,7 +590,7 @@ def main():
|
||||
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)
|
||||
active_automatic_position = next((p for p in all_positions if p['type'] == 'AUTOMATIC' and p['status'] == 'OPEN'), None)
|
||||
|
||||
if all_positions:
|
||||
print("\n" + "="*60)
|
||||
@ -625,8 +642,45 @@ def main():
|
||||
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.
|
||||
|
||||
# --- REBALANCE ON CLOSE (If Price Dropped) ---
|
||||
if status_str == "OUT OF RANGE (BELOW)":
|
||||
print("📉 Position closed BELOW range (100% ETH). Selling 50% of WETH inventory to USDC...")
|
||||
try:
|
||||
# Get WETH Balance
|
||||
token0_c = w3.eth.contract(address=pos_details['token0_address'], abi=ERC20_ABI)
|
||||
weth_bal = token0_c.functions.balanceOf(account.address).call()
|
||||
|
||||
amount_in = weth_bal // 2
|
||||
|
||||
if amount_in > 0:
|
||||
# Approve Router
|
||||
approve_txn = token0_c.functions.approve(router_contract.address, amount_in).build_transaction({
|
||||
'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address),
|
||||
'gas': 100000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee,
|
||||
'chainId': w3.eth.chain_id
|
||||
})
|
||||
signed = w3.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||
w3.eth.send_raw_transaction(raw)
|
||||
time.sleep(2)
|
||||
|
||||
# Swap WETH -> USDC
|
||||
params = (pos_details['token0_address'], pos_details['token1_address'], 500, account.address, int(time.time()) + 120, amount_in, 0, 0)
|
||||
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||
'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address),
|
||||
'gas': 300000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee,
|
||||
'chainId': w3.eth.chain_id
|
||||
})
|
||||
signed_swap = w3.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.eth.send_raw_transaction(raw_swap)
|
||||
print(f"⚖️ Rebalance Swap Sent: {tx_hash.hex()}")
|
||||
w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
print("✅ Rebalance Complete.")
|
||||
except Exception as e:
|
||||
print(f"Error during rebalance swap: {e}")
|
||||
|
||||
else:
|
||||
print("Liquidity 0. Marking closed.")
|
||||
update_hedge_status_file("CLOSE", {'token_id': token_id})
|
||||
@ -651,50 +705,47 @@ def main():
|
||||
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:
|
||||
token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||
token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||
d0 = token0_c.functions.decimals().call()
|
||||
d1 = token1_c.functions.decimals().call()
|
||||
except:
|
||||
print("Error fetching decimals")
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
print(f"Error fetching decimals: {e}")
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
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"
|
||||
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.")
|
||||
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)
|
||||
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,
|
||||
'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...")
|
||||
|
||||
@ -708,4 +759,4 @@ def main():
|
||||
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user