hedge and auto hedger in separate folders
This commit is contained in:
@ -4,7 +4,9 @@ import logging
|
||||
import sys
|
||||
import math
|
||||
import json
|
||||
import threading
|
||||
from dotenv import load_dotenv
|
||||
from web3 import Web3
|
||||
|
||||
# --- FIX: Add project root to sys.path to import local modules ---
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@ -30,29 +32,75 @@ setup_logging("normal", "SCALPER_HEDGER")
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
COIN_SYMBOL = "ETH"
|
||||
CHECK_INTERVAL = 5 # Optimized for cost/noise reduction (was 1)
|
||||
CHECK_INTERVAL = 4 # Optimized for speed (was 5)
|
||||
LEVERAGE = 5 # 3x Leverage
|
||||
STATUS_FILE = "hedge_status.json"
|
||||
RPC_URL = os.environ.get("MAINNET_RPC_URL") # Required for Uniswap Monitor
|
||||
|
||||
# Uniswap V3 Pool (Arbitrum WETH/USDC 0.05%)
|
||||
UNISWAP_POOL_ADDRESS = "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"
|
||||
UNISWAP_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"}]')
|
||||
|
||||
# --- STRATEGY ZONES (Percent of Range Width) ---
|
||||
# Bottom Hedge Zone: 0% to 15% -> Active Hedging
|
||||
ZONE_BOTTOM_HEDGE_LIMIT = 0.5
|
||||
# Bottom Hedge Zone: Covers entire range (0.0 to 1.5) -> Always Active
|
||||
ZONE_BOTTOM_HEDGE_LIMIT = 1
|
||||
|
||||
# Close Zone: 15% to 20% -> Close All Hedges (Flatten)
|
||||
ZONE_CLOSE_START = 0.52
|
||||
ZONE_CLOSE_END = 0.54
|
||||
# Close Zone: Disabled (Set > 1.0)
|
||||
ZONE_CLOSE_START = 10.0
|
||||
ZONE_CLOSE_END = 11.0
|
||||
|
||||
# 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
|
||||
# Top Hedge Zone: Disabled/Redundant
|
||||
ZONE_TOP_HEDGE_START = 10.0
|
||||
|
||||
# --- ORDER SETTINGS ---
|
||||
PRICE_BUFFER_PCT = 0.002 # 0.2% price move triggers order update (Relaxed for cost)
|
||||
MIN_THRESHOLD_ETH = 0.02 # Minimum trade size in ETH (~$60, Reduced frequency)
|
||||
PRICE_BUFFER_PCT = 0.0001 # 0.2% price move triggers order update (Relaxed for cost)
|
||||
MIN_THRESHOLD_ETH = 0.0025 # Minimum trade size in ETH (~$60, Reduced frequency)
|
||||
MIN_ORDER_VALUE_USD = 10.0 # Minimum order value for API safety
|
||||
|
||||
class UniswapPriceMonitor:
|
||||
def __init__(self, rpc_url, pool_address):
|
||||
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
||||
self.pool_contract = self.w3.eth.contract(address=pool_address, abi=UNISWAP_POOL_ABI)
|
||||
self.latest_price = None
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def _loop(self):
|
||||
logging.info("Uniswap Monitor Started.")
|
||||
while self.running:
|
||||
try:
|
||||
slot0 = self.pool_contract.functions.slot0().call()
|
||||
sqrt_price_x96 = slot0[0]
|
||||
# Price = (sqrtPriceX96 / 2^96)^2 * 10^(18-6) (WETH/USDC)
|
||||
# But typically WETH is token1? Let's verify standard Arbitrum Pool.
|
||||
# 0xC31E... Token0=WETH, Token1=USDC.
|
||||
# Price = (sqrt / 2^96)^2 * (10^12) -> This gives USDC per ETH? No, Token1/Token0.
|
||||
# Wait, usually Token0 is WETH (18) and Token1 is USDC (6).
|
||||
# P = (1.0001^tick) * 10^(decimals0 - decimals1)? No.
|
||||
# Standard conversion: Price = (sqrtRatioX96 / Q96) ** 2
|
||||
# Adjusted for decimals: Price = Price_raw / (10**(Dec0 - Dec1)) ? No.
|
||||
# Price (Quote/Base) = (sqrt / Q96)^2 * 10^(BaseDec - QuoteDec)
|
||||
|
||||
# Let's rely on standard logic: Price = (sqrt / 2^96)^2 * 10^(12) for ETH(18)/USDC(6)
|
||||
raw_price = (sqrt_price_x96 / (2**96)) ** 2
|
||||
price = raw_price * (10**(18-6)) # 10^12
|
||||
# If Token0 is WETH, price is USDC per WETH.
|
||||
# Note: If the pool is inverted (USDC/WETH), we invert.
|
||||
# On Arb, WETH is usually Token0?
|
||||
# 0x82aF... < 0xaf88... (WETH < USDC). So WETH is Token0.
|
||||
# Price is Token1 per Token0.
|
||||
|
||||
self.latest_price = 1 / price if price < 1 else price # Sanity check, ETH should be > 2000
|
||||
|
||||
except Exception as e:
|
||||
# logging.error(f"Uniswap Monitor Error: {e}")
|
||||
pass
|
||||
time.sleep(5)
|
||||
|
||||
def get_price(self):
|
||||
return self.latest_price
|
||||
|
||||
def get_active_automatic_position():
|
||||
if not os.path.exists(STATUS_FILE):
|
||||
return None
|
||||
@ -203,9 +251,25 @@ class HyperliquidStrategy:
|
||||
|
||||
def calculate_rebalance(self, current_price, current_short_position_size):
|
||||
pool_delta = self.get_pool_delta(current_price)
|
||||
|
||||
# --- Over-Hedge Logic ---
|
||||
overhedge_pct = 0.0
|
||||
range_width = self.high_range - self.low_range
|
||||
if range_width > 0:
|
||||
price_pct = (current_price - self.low_range) / range_width
|
||||
|
||||
# If below 0.8 (80%) of range
|
||||
if price_pct < 0.8:
|
||||
# Formula: 0.75% boost for every 0.1 drop below 0.8
|
||||
# Example: At 0.6 (60%), diff is 0.2. (0.2/0.1)*0.0075 = 0.015 (1.5%)
|
||||
overhedge_pct = ((0.8 - max(0.0, price_pct)) / 0.1) * 0.0075
|
||||
|
||||
raw_target_short = pool_delta + self.static_long
|
||||
|
||||
target_short_size = raw_target_short
|
||||
# Apply Boost
|
||||
adjusted_target_short = raw_target_short * (1.0 + overhedge_pct)
|
||||
|
||||
target_short_size = adjusted_target_short
|
||||
diff = target_short_size - abs(current_short_position_size)
|
||||
|
||||
return {
|
||||
@ -215,7 +279,8 @@ class HyperliquidStrategy:
|
||||
"current_short": abs(current_short_position_size),
|
||||
"diff": diff,
|
||||
"action": "SELL" if diff > 0 else "BUY",
|
||||
"mode": "NORMAL"
|
||||
"mode": "OVERHEDGE" if overhedge_pct > 0 else "NORMAL",
|
||||
"overhedge_pct": overhedge_pct
|
||||
}
|
||||
|
||||
class ScalperHedger:
|
||||
@ -242,6 +307,9 @@ class ScalperHedger:
|
||||
self.active_position_id = None
|
||||
self.active_order = None
|
||||
|
||||
# --- Start Uniswap Monitor ---
|
||||
self.uni_monitor = UniswapPriceMonitor(RPC_URL, UNISWAP_POOL_ADDRESS)
|
||||
|
||||
logging.info(f"Scalper Hedger initialized. Agent: {self.account.address}")
|
||||
|
||||
def _init_strategy(self, position_data):
|
||||
@ -553,10 +621,23 @@ class ScalperHedger:
|
||||
current_pos_size = pos_data['size']
|
||||
current_pnl = pos_data['pnl']
|
||||
|
||||
# --- SPREAD MONITOR LOG ---
|
||||
uni_price = self.uni_monitor.get_price()
|
||||
spread_text = ""
|
||||
if uni_price:
|
||||
diff = price - uni_price
|
||||
pct = (diff / uni_price) * 100
|
||||
spread_text = f" | Sprd: {pct:+.2f}% (H:{price:.0f}/U:{uni_price:.0f})"
|
||||
|
||||
# 3. Calculate Logic
|
||||
calc = self.strategy.calculate_rebalance(price, current_pos_size)
|
||||
diff_abs = abs(calc['diff'])
|
||||
|
||||
# --- LOGGING OVERHEDGE ---
|
||||
oh_text = ""
|
||||
if calc.get('overhedge_pct', 0) > 0:
|
||||
oh_text = f" | 🔥 OH: +{calc['overhedge_pct']*100:.2f}%"
|
||||
|
||||
# 4. Dynamic Threshold Calculation
|
||||
sqrt_Pa = math.sqrt(self.strategy.low_range)
|
||||
sqrt_Pb = math.sqrt(self.strategy.high_range)
|
||||
@ -571,23 +652,32 @@ class ScalperHedger:
|
||||
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_bottom_price = clp_low_range + (range_width * ZONE_CLOSE_START)
|
||||
zone_close_top_price = clp_low_range + (range_width * ZONE_CLOSE_END)
|
||||
zone_top_start_price = clp_low_range + (range_width * ZONE_TOP_HEDGE_START)
|
||||
# If config > 9, set to None (Disabled Zone)
|
||||
zone_bottom_limit_price = (clp_low_range + (range_width * ZONE_BOTTOM_HEDGE_LIMIT)) if ZONE_BOTTOM_HEDGE_LIMIT <= 9 else None
|
||||
zone_close_bottom_price = (clp_low_range + (range_width * ZONE_CLOSE_START)) if ZONE_CLOSE_START <= 9 else None
|
||||
zone_close_top_price = (clp_low_range + (range_width * ZONE_CLOSE_END)) if ZONE_CLOSE_END <= 9 else None
|
||||
zone_top_start_price = (clp_low_range + (range_width * ZONE_TOP_HEDGE_START)) if ZONE_TOP_HEDGE_START <= 9 else None
|
||||
|
||||
# Update JSON with zone prices if they are None (initially set by uniswap_manager.py)
|
||||
if active_pos.get('zone_bottom_limit_price') is None:
|
||||
update_position_zones_in_json(active_pos['token_id'], {
|
||||
'zone_top_start_price': round(zone_top_start_price, 2),
|
||||
'zone_close_top_price': round(zone_close_top_price, 2),
|
||||
'zone_close_bottom_price': round(zone_close_bottom_price, 2),
|
||||
'zone_bottom_limit_price': round(zone_bottom_limit_price, 2)
|
||||
'zone_top_start_price': round(zone_top_start_price, 2) if zone_top_start_price else None,
|
||||
'zone_close_top_price': round(zone_close_top_price, 2) if zone_close_top_price else None,
|
||||
'zone_close_bottom_price': round(zone_close_bottom_price, 2) if zone_close_bottom_price else None,
|
||||
'zone_bottom_limit_price': round(zone_bottom_limit_price, 2) if zone_bottom_limit_price else None
|
||||
})
|
||||
|
||||
# Check Zones
|
||||
in_close_zone = (price >= zone_close_bottom_price and price <= zone_close_top_price)
|
||||
in_hedge_zone = (price <= zone_bottom_limit_price) or (price >= zone_top_start_price)
|
||||
# Check Zones (Handle None)
|
||||
# If zone price is None, condition fails safe (False)
|
||||
in_close_zone = False
|
||||
if zone_close_bottom_price is not None and zone_close_top_price is not None:
|
||||
in_close_zone = (price >= zone_close_bottom_price and price <= zone_close_top_price)
|
||||
|
||||
in_hedge_zone = False
|
||||
if zone_bottom_limit_price is not None and price <= zone_bottom_limit_price:
|
||||
in_hedge_zone = True
|
||||
if zone_top_start_price is not None and price >= zone_top_start_price:
|
||||
in_hedge_zone = True
|
||||
|
||||
# --- Execute Logic ---
|
||||
if in_close_zone:
|
||||
@ -601,18 +691,12 @@ class ScalperHedger:
|
||||
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}). PNL: ${current_pnl:.2f}")
|
||||
logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${MIN_ORDER_VALUE_USD:.2f}). PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
|
||||
elif trade_size > 0:
|
||||
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone. PNL: ${current_pnl:.2f}")
|
||||
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
|
||||
# Execute Passively for Alo
|
||||
# Force 1 tick offset (0.1) away from BBO to ensure rounding doesn't cause cross
|
||||
# Sell at Ask + 0.1, Buy at Bid - 0.1
|
||||
@ -627,14 +711,14 @@ class ScalperHedger:
|
||||
|
||||
self.place_limit_order(COIN_SYMBOL, is_buy, trade_size, exec_price)
|
||||
else:
|
||||
logging.info(f"Trade size rounds to 0. Skipping. PNL: ${current_pnl:.2f}")
|
||||
logging.info(f"Trade size rounds to 0. Skipping. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
|
||||
else:
|
||||
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone. PNL: ${current_pnl:.2f}")
|
||||
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
|
||||
|
||||
else:
|
||||
# MIDDLE ZONE (IDLE)
|
||||
pct_position = (price - clp_low_range) / range_width
|
||||
logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). PNL: ${current_pnl:.2f}. No Actions.")
|
||||
logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). PNL: ${current_pnl:.2f}{spread_text}{oh_text}. No Actions.")
|
||||
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user