From 4f7bb429b7188cf9313bf335e2ce54a49f31a62d Mon Sep 17 00:00:00 2001 From: DiTus Date: Fri, 26 Dec 2025 22:36:03 +0100 Subject: [PATCH] feat: disable vol multiplier inside range & add edge cleanup logic --- clp_hedger.py | 120 ++++++++++++++++++++++++++++++++++---------------- doc/GEMINI.md | 3 +- 2 files changed, 85 insertions(+), 38 deletions(-) diff --git a/clp_hedger.py b/clp_hedger.py index b53035c..977fef9 100644 --- a/clp_hedger.py +++ b/clp_hedger.py @@ -86,13 +86,13 @@ MIN_ORDER_VALUE_USD = Decimal("10.0") # Capital Safety DYNAMIC_THRESHOLD_MULTIPLIER = Decimal("1.2") -MIN_TIME_BETWEEN_TRADES = 25 +MIN_TIME_BETWEEN_TRADES = 60 MAX_HEDGE_MULTIPLIER = Decimal("1.25") # Rebalance Threshold (Base) # Adjust this based on expected volatility: # - Low Volatility (Weekend/Chop): 0.08 - 0.10 (8-10%) to reduce churn # - 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_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") 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) +ENABLE_FISHING = True FISHING_ORDER_SIZE_PCT = Decimal("0.10") # 10% of hedge size -MAKER_ORDER_TIMEOUT = 400 +MAKER_ORDER_TIMEOUT = 600 SHADOW_ORDER_TIMEOUT = 600 # --- HELPER FUNCTIONS --- @@ -354,6 +359,7 @@ class ScalperHedger: # Logging Rate Limiting self.last_idle_log_time = 0 self.last_pending_log_time = 0 + self.api_backoff_until = 0 # Order Tracking self.original_order_side = None @@ -638,7 +644,11 @@ class ScalperHedger: logger.error(f"Order Failed: {order_result}") 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 def manage_orders(self) -> bool: @@ -697,7 +707,8 @@ class ScalperHedger: def track_fills_and_pnl(self, force: bool = False): try: 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 self.last_pnl_check_time = now @@ -722,8 +733,15 @@ class ScalperHedger: if new_activity: logger.info(f"Fills tracked. Total Price PnL: {self.accumulated_pnl:.2f} | Total Fees: {self.accumulated_fees:.2f}") + 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): logger.info("Closing all positions...") @@ -775,6 +793,14 @@ class ScalperHedger: while True: 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() # Check Global Disable or Missing Position @@ -858,6 +884,10 @@ class ScalperHedger: if vol_pct > 0: 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) 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"): 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 # 5. Check Zones @@ -1017,56 +1062,57 @@ class ScalperHedger: # Price where Diff reaches +threshold (SELL) 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 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() # --- FISHING ORDER LOGIC (SAFE ZONE) --- # 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: # Use REAL Hedge Entry Price from Hyperliquid, not LP Entry hedge_entry = pos_data.get('entry_price', Decimal("0")) # Only fish if we actually have a position if hedge_entry > 0 and current_size != 0: - # Determine side: - # We are SHORT. - # If Px < Entry: We are in profit. We want to BUY to take profit/scale out. - # If Px > Entry: We are in loss. We want to SELL to average up/scale in. - # 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. + # SYMMETRIC FISHING LOGIC: + # We want an order that SITS on the book at the entry price. + # If Price > Entry: Place BUY at Entry (Waiting for a dip to reduce short). + # If Price < Entry: Place SELL at Entry (Waiting for a pump to increase short). - # CORRECT LOGIC for "Return to Entry": - # We want an order that SITS on the book. - # If Current Price < Entry: We want to SELL if price goes back UP to Entry. (Add to Short / Re-enter). - # If Current Price > Entry: We want to BUY if price goes back DOWN to Entry. (Reduce Short / Escape). + if price > hedge_entry: + is_buy = True + target_price = hedge_entry + 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 --- - # 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: + if is_buy is not None: # Size = 10% of Target Delta fishing_size = abs(calc['target_short']) * FISHING_ORDER_SIZE_PCT # Check distance - only place if price is not literally on top of entry - dist_pct = (price - target_price) / price - if dist_pct > Decimal("0.001"): # 0.1% buffer - logger.info(f"[FISHING] Placing Maker BUY {fishing_size:.4f} at Hedge Entry: {target_price:.2f} (Reducing Short at break-even)") + # 0.1% buffer to ensure it stays as a Maker order + dist_pct = abs(price - target_price) / price + 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") - 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: logger.error(f"Error placing fishing order: {e}") diff --git a/doc/GEMINI.md b/doc/GEMINI.md index e06bcc0..4e4d2af 100644 --- a/doc/GEMINI.md +++ b/doc/GEMINI.md @@ -1,6 +1,6 @@ # GEMINI Project Context & Setup -**Last Updated:** 2025-12-19 +**Last Updated:** 2025-12-26 **Project:** Uniswap V3 Automated Concentrated Liquidity Pool (CLP) Hedger ## 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`. * **Logging:** Fixed duplicate terminal output by disabling logger propagation. * **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 * `uniswap_manager.py`: Core logic for Uniswap V3 interaction.