feat: disable vol multiplier inside range & add edge cleanup logic
This commit is contained in:
120
clp_hedger.py
120
clp_hedger.py
@ -86,13 +86,13 @@ MIN_ORDER_VALUE_USD = Decimal("10.0")
|
|||||||
|
|
||||||
# Capital Safety
|
# Capital Safety
|
||||||
DYNAMIC_THRESHOLD_MULTIPLIER = Decimal("1.2")
|
DYNAMIC_THRESHOLD_MULTIPLIER = Decimal("1.2")
|
||||||
MIN_TIME_BETWEEN_TRADES = 25
|
MIN_TIME_BETWEEN_TRADES = 60
|
||||||
MAX_HEDGE_MULTIPLIER = Decimal("1.25")
|
MAX_HEDGE_MULTIPLIER = Decimal("1.25")
|
||||||
# Rebalance Threshold (Base)
|
# Rebalance Threshold (Base)
|
||||||
# Adjust this based on expected volatility:
|
# Adjust this based on expected volatility:
|
||||||
# - Low Volatility (Weekend/Chop): 0.08 - 0.10 (8-10%) to reduce churn
|
# - Low Volatility (Weekend/Chop): 0.08 - 0.10 (8-10%) to reduce churn
|
||||||
# - High Volatility (Events): 0.05 (5%) to track price closely
|
# - High Volatility (Events): 0.05 (5%) to track price closely
|
||||||
BASE_REBALANCE_THRESHOLD_PCT = Decimal("0.12") # 12%
|
BASE_REBALANCE_THRESHOLD_PCT = Decimal("0.09") # 9%
|
||||||
|
|
||||||
# Edge Protection
|
# Edge Protection
|
||||||
EDGE_PROXIMITY_PCT = Decimal("0.04")
|
EDGE_PROXIMITY_PCT = Decimal("0.04")
|
||||||
@ -101,10 +101,15 @@ POSITION_OPEN_EDGE_PROXIMITY_PCT = Decimal("0.06")
|
|||||||
POSITION_CLOSED_EDGE_PROXIMITY_PCT = Decimal("0.025")
|
POSITION_CLOSED_EDGE_PROXIMITY_PCT = Decimal("0.025")
|
||||||
LARGE_HEDGE_MULTIPLIER = Decimal("2.8")
|
LARGE_HEDGE_MULTIPLIER = Decimal("2.8")
|
||||||
|
|
||||||
|
# Edge Cleanup Settings
|
||||||
|
ENABLE_EDGE_CLEANUP = True
|
||||||
|
EDGE_CLEANUP_MARGIN_PCT = Decimal("0.02") # 2% of range width
|
||||||
|
|
||||||
# Fishing Order (Maker "Fishing" at Entry Price)
|
# Fishing Order (Maker "Fishing" at Entry Price)
|
||||||
|
ENABLE_FISHING = True
|
||||||
FISHING_ORDER_SIZE_PCT = Decimal("0.10") # 10% of hedge size
|
FISHING_ORDER_SIZE_PCT = Decimal("0.10") # 10% of hedge size
|
||||||
|
|
||||||
MAKER_ORDER_TIMEOUT = 400
|
MAKER_ORDER_TIMEOUT = 600
|
||||||
SHADOW_ORDER_TIMEOUT = 600
|
SHADOW_ORDER_TIMEOUT = 600
|
||||||
|
|
||||||
# --- HELPER FUNCTIONS ---
|
# --- HELPER FUNCTIONS ---
|
||||||
@ -354,6 +359,7 @@ class ScalperHedger:
|
|||||||
# Logging Rate Limiting
|
# Logging Rate Limiting
|
||||||
self.last_idle_log_time = 0
|
self.last_idle_log_time = 0
|
||||||
self.last_pending_log_time = 0
|
self.last_pending_log_time = 0
|
||||||
|
self.api_backoff_until = 0
|
||||||
|
|
||||||
# Order Tracking
|
# Order Tracking
|
||||||
self.original_order_side = None
|
self.original_order_side = None
|
||||||
@ -638,7 +644,11 @@ class ScalperHedger:
|
|||||||
logger.error(f"Order Failed: {order_result}")
|
logger.error(f"Order Failed: {order_result}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Exception during trade: {e}")
|
if "429" in str(e):
|
||||||
|
logger.warning(f"Rate limit hit during trade (429). Backing off for 30s.")
|
||||||
|
self.api_backoff_until = time.time() + 30
|
||||||
|
else:
|
||||||
|
logger.error(f"Exception during trade: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def manage_orders(self) -> bool:
|
def manage_orders(self) -> bool:
|
||||||
@ -697,7 +707,8 @@ class ScalperHedger:
|
|||||||
def track_fills_and_pnl(self, force: bool = False):
|
def track_fills_and_pnl(self, force: bool = False):
|
||||||
try:
|
try:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if not force and now - self.last_pnl_check_time < 10:
|
# Increase interval to 60s to avoid 429 Rate Limits
|
||||||
|
if not force and now - self.last_pnl_check_time < 60:
|
||||||
return
|
return
|
||||||
self.last_pnl_check_time = now
|
self.last_pnl_check_time = now
|
||||||
|
|
||||||
@ -722,8 +733,15 @@ class ScalperHedger:
|
|||||||
|
|
||||||
if new_activity:
|
if new_activity:
|
||||||
logger.info(f"Fills tracked. Total Price PnL: {self.accumulated_pnl:.2f} | Total Fees: {self.accumulated_fees:.2f}")
|
logger.info(f"Fills tracked. Total Price PnL: {self.accumulated_pnl:.2f} | Total Fees: {self.accumulated_fees:.2f}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error tracking fills: {e}")
|
# Handle 429 specifically
|
||||||
|
if "429" in str(e):
|
||||||
|
logger.warning(f"Rate limit hitting while tracking fills (429). Backing off.")
|
||||||
|
# Add a small penalty delay to last_check to prevent immediate retry
|
||||||
|
self.last_pnl_check_time = time.time() + 30
|
||||||
|
else:
|
||||||
|
logger.error(f"Error tracking fills: {e}")
|
||||||
|
|
||||||
def close_all_positions(self, force_taker: bool = False):
|
def close_all_positions(self, force_taker: bool = False):
|
||||||
logger.info("Closing all positions...")
|
logger.info("Closing all positions...")
|
||||||
@ -775,6 +793,14 @@ class ScalperHedger:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
# API Backoff Check
|
||||||
|
if time.time() < self.api_backoff_until:
|
||||||
|
wait_time = self.api_backoff_until - time.time()
|
||||||
|
if int(wait_time) % 5 == 0: # Log every 5s
|
||||||
|
logger.warning(f"Backing off due to 429... ({wait_time:.1f}s)")
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
active_pos = get_active_automatic_position()
|
active_pos = get_active_automatic_position()
|
||||||
|
|
||||||
# Check Global Disable or Missing Position
|
# Check Global Disable or Missing Position
|
||||||
@ -858,6 +884,10 @@ class ScalperHedger:
|
|||||||
if vol_pct > 0:
|
if vol_pct > 0:
|
||||||
vol_multiplier = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol_ref))
|
vol_multiplier = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol_ref))
|
||||||
|
|
||||||
|
# Disable volatility index when price is strictly inside edges (top and bottom) of range
|
||||||
|
if self.strategy.low_range < price < self.strategy.high_range:
|
||||||
|
vol_multiplier = Decimal("1.0")
|
||||||
|
|
||||||
# 3. Base Threshold Calculation (Range Dependent)
|
# 3. Base Threshold Calculation (Range Dependent)
|
||||||
range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range
|
range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range
|
||||||
|
|
||||||
@ -885,6 +915,21 @@ class ScalperHedger:
|
|||||||
if pct_change > Decimal("0.003"):
|
if pct_change > Decimal("0.003"):
|
||||||
rebalance_threshold *= DYNAMIC_THRESHOLD_MULTIPLIER
|
rebalance_threshold *= DYNAMIC_THRESHOLD_MULTIPLIER
|
||||||
|
|
||||||
|
# --- FORCE EDGE CLEANUP (Dynamic Margin) ---
|
||||||
|
# Ensure we trade at the X% line if we haven't already.
|
||||||
|
if ENABLE_EDGE_CLEANUP:
|
||||||
|
dist_bottom_pct = (price - self.strategy.low_range) / self.strategy.low_range
|
||||||
|
dist_top_pct = (self.strategy.high_range - price) / self.strategy.high_range
|
||||||
|
|
||||||
|
range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range
|
||||||
|
safety_margin_pct = range_width_pct * EDGE_CLEANUP_MARGIN_PCT
|
||||||
|
|
||||||
|
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
|
||||||
|
if rebalance_threshold > MIN_THRESHOLD_ETH:
|
||||||
|
if rebalance_threshold > MIN_THRESHOLD_ETH * Decimal("1.5"):
|
||||||
|
logger.info(f"[EDGE] Inside {EDGE_CLEANUP_MARGIN_PCT*100}% Safety Zone. Forcing tight threshold: {rebalance_threshold:.4f} -> {MIN_THRESHOLD_ETH:.4f}")
|
||||||
|
rebalance_threshold = MIN_THRESHOLD_ETH
|
||||||
|
|
||||||
self.last_price = price
|
self.last_price = price
|
||||||
|
|
||||||
# 5. Check Zones
|
# 5. Check Zones
|
||||||
@ -1017,56 +1062,57 @@ class ScalperHedger:
|
|||||||
# Price where Diff reaches +threshold (SELL)
|
# Price where Diff reaches +threshold (SELL)
|
||||||
p_sell = price - (rebalance_threshold - calc['diff']) / gamma
|
p_sell = price - (rebalance_threshold - calc['diff']) / gamma
|
||||||
|
|
||||||
|
# Recalculate triggers if outside range (Safety Buffer: Dynamic Margin)
|
||||||
|
# User Request: "at range is too late", "recalculated to be bellow it"
|
||||||
|
r_width = self.strategy.high_range - self.strategy.low_range
|
||||||
|
safety_margin = r_width * EDGE_CLEANUP_MARGIN_PCT
|
||||||
|
|
||||||
|
if p_buy > self.strategy.high_range:
|
||||||
|
p_buy = self.strategy.high_range - safety_margin
|
||||||
|
|
||||||
|
if p_sell < self.strategy.low_range:
|
||||||
|
p_sell = self.strategy.low_range + safety_margin
|
||||||
|
|
||||||
net_pnl = self.accumulated_pnl - self.accumulated_fees
|
net_pnl = self.accumulated_pnl - self.accumulated_fees
|
||||||
logger.info(f"[IDLE] Px: {price:.2f} | M: {p_mid:.1f} | B: {p_buy:.1f} / S: {p_sell:.1f} (Vol: {vol_pct*100:.3f}% x{vol_multiplier:.1f} | Thresh: {final_threshold_pct*100:.1f}%) | TotPnL: {net_pnl:.2f}")
|
logger.info(f"[IDLE] Px: {price:.2f} | M: {p_mid:.1f} | B: {p_buy:.1f} / S: {p_sell:.1f} (Vol: {vol_pct*100:.3f}% x{vol_multiplier:.1f} | Thresh: {final_threshold_pct*100:.1f}%) | TotPnL: {net_pnl:.2f}")
|
||||||
self.last_idle_log_time = time.time()
|
self.last_idle_log_time = time.time()
|
||||||
|
|
||||||
# --- FISHING ORDER LOGIC (SAFE ZONE) ---
|
# --- FISHING ORDER LOGIC (SAFE ZONE) ---
|
||||||
# Always keep a maker order open at Entry Price for 10% of hedge size
|
# Always keep a maker order open at Entry Price for 10% of hedge size
|
||||||
if self.fishing_oid is None and not self.get_open_orders():
|
if ENABLE_FISHING and self.fishing_oid is None and not self.get_open_orders():
|
||||||
try:
|
try:
|
||||||
# Use REAL Hedge Entry Price from Hyperliquid, not LP Entry
|
# Use REAL Hedge Entry Price from Hyperliquid, not LP Entry
|
||||||
hedge_entry = pos_data.get('entry_price', Decimal("0"))
|
hedge_entry = pos_data.get('entry_price', Decimal("0"))
|
||||||
|
|
||||||
# Only fish if we actually have a position
|
# Only fish if we actually have a position
|
||||||
if hedge_entry > 0 and current_size != 0:
|
if hedge_entry > 0 and current_size != 0:
|
||||||
# Determine side:
|
# SYMMETRIC FISHING LOGIC:
|
||||||
# We are SHORT.
|
# We want an order that SITS on the book at the entry price.
|
||||||
# If Px < Entry: We are in profit. We want to BUY to take profit/scale out.
|
# If Price > Entry: Place BUY at Entry (Waiting for a dip to reduce short).
|
||||||
# If Px > Entry: We are in loss. We want to SELL to average up/scale in.
|
# If Price < Entry: Place SELL at Entry (Waiting for a pump to increase short).
|
||||||
# WAIT. "Fishing" usually means closing.
|
|
||||||
# User said: "Maker position ... at price of entry price"
|
|
||||||
# If we are Short, placing a BUY at Entry (when Px < Entry) acts as Take Profit.
|
|
||||||
# If we are Short, placing a SELL at Entry (when Px > Entry) acts as Averaging Up.
|
|
||||||
# Standard Grid logic:
|
|
||||||
# If Px > Entry: Place SELL at Entry? No, price is above entry. Limit Sell at Entry would fill instantly as Taker.
|
|
||||||
# If Px < Entry: Place BUY at Entry? No, price is below entry. Limit Buy at Entry would fill instantly.
|
|
||||||
|
|
||||||
# CORRECT LOGIC for "Return to Entry":
|
if price > hedge_entry:
|
||||||
# We want an order that SITS on the book.
|
is_buy = True
|
||||||
# If Current Price < Entry: We want to SELL if price goes back UP to Entry. (Add to Short / Re-enter).
|
target_price = hedge_entry
|
||||||
# If Current Price > Entry: We want to BUY if price goes back DOWN to Entry. (Reduce Short / Escape).
|
action_desc = "Reducing Short"
|
||||||
|
elif price < hedge_entry:
|
||||||
|
is_buy = False
|
||||||
|
target_price = hedge_entry
|
||||||
|
action_desc = "Increasing Short"
|
||||||
|
else:
|
||||||
|
# Exactly at entry, skip to avoid immediate Taker matching
|
||||||
|
is_buy = None
|
||||||
|
|
||||||
# --- ALWAYS BUY LOGIC ---
|
if is_buy is not None:
|
||||||
# We only place a Limit BUY at Entry Price.
|
|
||||||
# This only works as a "Fishing" Maker order if Px > Entry (Waiting for a dip).
|
|
||||||
target_price = hedge_entry
|
|
||||||
is_buy = True
|
|
||||||
|
|
||||||
# Only place if it's a valid Maker order (Target < Current Price)
|
|
||||||
if price > target_price:
|
|
||||||
# Size = 10% of Target Delta
|
# Size = 10% of Target Delta
|
||||||
fishing_size = abs(calc['target_short']) * FISHING_ORDER_SIZE_PCT
|
fishing_size = abs(calc['target_short']) * FISHING_ORDER_SIZE_PCT
|
||||||
|
|
||||||
# Check distance - only place if price is not literally on top of entry
|
# Check distance - only place if price is not literally on top of entry
|
||||||
dist_pct = (price - target_price) / price
|
# 0.1% buffer to ensure it stays as a Maker order
|
||||||
if dist_pct > Decimal("0.001"): # 0.1% buffer
|
dist_pct = abs(price - target_price) / price
|
||||||
logger.info(f"[FISHING] Placing Maker BUY {fishing_size:.4f} at Hedge Entry: {target_price:.2f} (Reducing Short at break-even)")
|
if dist_pct > Decimal("0.001"):
|
||||||
|
logger.info(f"[FISHING] Placing Maker {'BUY' if is_buy else 'SELL'} {fishing_size:.4f} at Hedge Entry: {target_price:.2f} ({action_desc})")
|
||||||
self.fishing_oid = self.place_limit_order(COIN_SYMBOL, is_buy, fishing_size, target_price, "Alo")
|
self.fishing_oid = self.place_limit_order(COIN_SYMBOL, is_buy, fishing_size, target_price, "Alo")
|
||||||
else:
|
|
||||||
# Price is already below entry (In profit).
|
|
||||||
# Placing a BUY at entry would be a Taker trade at a worse price.
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error placing fishing order: {e}")
|
logger.error(f"Error placing fishing order: {e}")
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# GEMINI Project Context & Setup
|
# GEMINI Project Context & Setup
|
||||||
|
|
||||||
**Last Updated:** 2025-12-19
|
**Last Updated:** 2025-12-26
|
||||||
**Project:** Uniswap V3 Automated Concentrated Liquidity Pool (CLP) Hedger
|
**Project:** Uniswap V3 Automated Concentrated Liquidity Pool (CLP) Hedger
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@ -38,6 +38,7 @@ This project automates the management and hedging of Uniswap V3 Concentrated Liq
|
|||||||
* **Refactoring:** Removed `clp_scalper_hedger.py` after merging its advanced features into `clp_hedger.py`.
|
* **Refactoring:** Removed `clp_scalper_hedger.py` after merging its advanced features into `clp_hedger.py`.
|
||||||
* **Logging:** Fixed duplicate terminal output by disabling logger propagation.
|
* **Logging:** Fixed duplicate terminal output by disabling logger propagation.
|
||||||
* **Feature:** Implemented "Comprehensive Edge Protection" in `clp_hedger.py` (Dynamic Proximity + Large Hedge Override).
|
* **Feature:** Implemented "Comprehensive Edge Protection" in `clp_hedger.py` (Dynamic Proximity + Large Hedge Override).
|
||||||
|
* **Logic:** Disabled Volatility Multiplier when price is strictly inside range edges (to prevent hedging pauses during volatility spikes when safe).
|
||||||
|
|
||||||
## 4. Key Files
|
## 4. Key Files
|
||||||
* `uniswap_manager.py`: Core logic for Uniswap V3 interaction.
|
* `uniswap_manager.py`: Core logic for Uniswap V3 interaction.
|
||||||
|
|||||||
Reference in New Issue
Block a user