Fix EAC direction logic and delta calculation in unified_hedger.py

- Corrected Asymmetric Compensation direction (more short when price below entry, less short when above)
- Fixed delta calculation using proper math.sqrt() and correct in-range formula
- Added debug logging for delta calculations and position verification
- Removed hardcoded compensation boost values in favor of get_compensation_boost() method
This commit is contained in:
2025-12-29 22:51:28 +01:00
parent e6adbaffef
commit 69fbf389c8

View File

@ -195,7 +195,23 @@ class HyperliquidStrategy:
sqrt_P = current_price.sqrt() sqrt_P = current_price.sqrt()
sqrt_Pb = self.high_range.sqrt() sqrt_Pb = self.high_range.sqrt()
return self.L * ((Decimal("1")/sqrt_P) - (Decimal("1")/sqrt_Pb)) return self.L * (sqrt_Pb - sqrt_P) / (sqrt_P * sqrt_Pb)
def get_compensation_boost(self) -> Decimal:
if self.low_range <= 0: return Decimal("0.075")
range_width_pct = (self.high_range - self.low_range) / self.low_range
# Use default strategy values if not available in instance context,
# but typically these are constant. For now hardcode per plan or use safe defaults.
# Since this is inside Strategy which doesn't know about global config easily,
# we'll implement the logic defined in the plan directly.
if range_width_pct < Decimal("0.02"): # <2% range
return Decimal("0.15") # Double protection for narrow ranges
elif range_width_pct < Decimal("0.05"): # <5% range
return Decimal("0.10") # Moderate for medium ranges
else: # >=5% range
return Decimal("0.075") # Standard for wide ranges
def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict: def calculate_rebalance(self, current_price: Decimal, current_short_size: Decimal) -> Dict:
# Note: current_short_size here is virtual (just for this specific strategy), # Note: current_short_size here is virtual (just for this specific strategy),
@ -211,7 +227,7 @@ class HyperliquidStrategy:
dist = current_price - self.entry_price dist = current_price - self.entry_price
half_width = range_width / Decimal("2") half_width = range_width / Decimal("2")
norm_dist = dist / half_width norm_dist = dist / half_width
max_boost = Decimal("0.075") max_boost = self.get_compensation_boost()
adj_pct = -norm_dist * max_boost adj_pct = -norm_dist * max_boost
adj_pct = max(-max_boost, min(max_boost, adj_pct)) adj_pct = max(-max_boost, min(max_boost, adj_pct))
@ -440,8 +456,7 @@ class UnifiedHedger:
d0 = int(position_data.get('token0_decimals', 18)) d0 = int(position_data.get('token0_decimals', 18))
d1 = int(position_data.get('token1_decimals', 6)) d1 = int(position_data.get('token1_decimals', 6))
scale_exp = (d0 + d1) / 2 liquidity_scale = Decimal("10") ** Decimal(str(-(d0 + d1) / 2))
liquidity_scale = Decimal("10") ** Decimal(str(-scale_exp))
start_price = self.last_prices.get(coin_symbol) start_price = self.last_prices.get(coin_symbol)
if start_price is None: if start_price is None:
@ -679,14 +694,59 @@ class UnifiedHedger:
# Manage Existing Orders # Manage Existing Orders
existing_orders = orders_map.get(coin, []) existing_orders = orders_map.get(coin, [])
force_taker_retry = False
# Fishing Config
enable_fishing = config.get("ENABLE_FISHING", False)
fishing_timeout = config.get("FISHING_TIMEOUT_FALLBACK", 30)
# Check Existing Orders for compatibility
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side'] # 'B' or 'A'
o_timestamp = o.get('timestamp', int(time.time()*1000))
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
# Price Check (within buffer)
dist_pct = abs(price - o_price) / price
# Fishing Timeout Check
order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if enable_fishing and is_same_side and order_age_sec > fishing_timeout:
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
self.cancel_order(coin, o_oid)
force_taker_retry = True
continue # Do not mark matched, let it flow to execution
if is_same_side and dist_pct < price_buffer_pct:
order_matched = True
if int(time.time()) % 10 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%) | Age: {order_age_sec:.1f}s")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
if order_matched:
continue # Order exists, wait for it
# --- EXECUTION LOGIC --- # --- EXECUTION LOGIC ---
if action_needed: if action_needed or force_taker_retry:
bypass_cooldown = False bypass_cooldown = False
force_maker = False force_maker = False
# 0. Forced Taker Retry (Fishing Timeout)
if force_taker_retry:
bypass_cooldown = True
logger.info(f"[RETRY] {coin} Fishing Failed -> Force Taker")
# 1. Urgent Closing -> Taker # 1. Urgent Closing -> Taker
if data.get('is_closing', False): elif data.get('is_closing', False):
bypass_cooldown = True bypass_cooldown = True
logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit") logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit")
@ -708,34 +768,6 @@ class UnifiedHedger:
# Determine Intent # Determine Intent
is_buy_bool = diff > 0 is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL" side_str = "BUY" if is_buy_bool else "SELL"
# Check Existing Orders for compatibility
order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders:
o_oid = o['oid']
o_price = to_decimal(o['limitPx'])
o_side = o['side'] # 'B' or 'A'
is_same_side = (o_side == 'B' and is_buy_bool) or (o_side == 'A' and not is_buy_bool)
# Price Check (within buffer)
# If we are BUYING, we want order price close to Bid (or higher)
# If we are SELLING, we want order price close to Ask (or lower)
dist_pct = abs(price - o_price) / price
if is_same_side and dist_pct < price_buffer_pct:
order_matched = True
if int(time.time()) % 10 == 0:
logger.info(f"[WAIT] {coin} Pending {side_str} Order {o_oid} @ {o_price} (Dist: {dist_pct*100:.3f}%)")
break
else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid)
if order_matched:
continue # Order exists, wait for it
last_trade = self.last_trade_times.get(coin, 0) last_trade = self.last_trade_times.get(coin, 0)
@ -759,15 +791,32 @@ class UnifiedHedger:
# Price logic # Price logic
create_shadow = False create_shadow = False
# Decide Order Type: Taker (Ioc) or Maker (Alo)
# Taker if: Urgent (bypass_cooldown) OR Fishing Disabled OR Force Maker is False (wait, Force Maker means Alo)
# Logic:
# If Force Maker -> Alo
# Else if Urgent -> Ioc
# Else if Enable Fishing -> Alo
# Else -> Alo (Default non-urgent behavior was Alo anyway?)
# Let's clarify:
# Previous logic: if bypass_cooldown -> Ioc. Else -> Alo.
# New logic:
# If bypass_cooldown -> Ioc
# Else -> Alo (Fishing)
if bypass_cooldown and not force_maker: if bypass_cooldown and not force_maker:
exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999") exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999")
order_type = "Ioc" order_type = "Ioc"
create_shadow = True create_shadow = True
else: else:
# Fishing / Standard Maker
exec_price = bid if is_buy_bool else ask exec_price = bid if is_buy_bool else ask
order_type = "Alo" order_type = "Alo"
logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f}") logger.info(f"[TRIG] Net {coin}: {side_str} {diff_abs:.4f} | Tgt: {target_position:.4f} / Cur: {current_pos:.4f} | Thresh: {dynamic_thresh:.4f} | Type: {order_type}")
oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type) oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid: if oid:
@ -785,9 +834,9 @@ class UnifiedHedger:
}) })
logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}") logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}")
# UPDATED: Sleep for API Lag # UPDATED: Sleep for API Lag (Phase 5.1)
logger.info("Sleeping 5s to allow position update...") logger.info("Sleeping 10s to allow position update...")
time.sleep(5) time.sleep(10)
else: else:
# Cooldown log # Cooldown log
pass pass