From b3cdf98161b3a51b6ae2478a1565511b1cc69dda Mon Sep 17 00:00:00 2001 From: DiTus Date: Mon, 22 Dec 2025 14:19:30 +0100 Subject: [PATCH] Implement 'Always Buy' Fishing Order at Hedge Entry --- clp_hedger.py | 96 +++++++++++++++++++++++++++++++++++++++++++++--- doc/CHANGELOG.md | 5 +++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/clp_hedger.py b/clp_hedger.py index f08367d..b53035c 100644 --- a/clp_hedger.py +++ b/clp_hedger.py @@ -92,7 +92,7 @@ MAX_HEDGE_MULTIPLIER = Decimal("1.25") # 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.08") +BASE_REBALANCE_THRESHOLD_PCT = Decimal("0.12") # 12% # Edge Protection EDGE_PROXIMITY_PCT = Decimal("0.04") @@ -101,6 +101,9 @@ POSITION_OPEN_EDGE_PROXIMITY_PCT = Decimal("0.06") POSITION_CLOSED_EDGE_PROXIMITY_PCT = Decimal("0.025") LARGE_HEDGE_MULTIPLIER = Decimal("2.8") +# Fishing Order (Maker "Fishing" at Entry Price) +FISHING_ORDER_SIZE_PCT = Decimal("0.10") # 10% of hedge size + MAKER_ORDER_TIMEOUT = 400 SHADOW_ORDER_TIMEOUT = 600 @@ -355,6 +358,7 @@ class ScalperHedger: # Order Tracking self.original_order_side = None self.shadow_orders = [] # Store theoretical Maker orders for analysis + self.fishing_oid = None # Track the resting "fishing" order logger.info(f"[DELTA] Delta-Zero Scalper Hedger initialized. Agent: {self.account.address}") @@ -475,6 +479,18 @@ class ScalperHedger: logger.info(f"[DELTA] Strat Init: Pos {self.active_position_id} | Range: {lower}-{upper} | Entry: {entry_price} | Start Px: {start_price:.2f} | Resumed PnL: {self.accumulated_pnl:.2f}") + # --- Adopt Orphaned Fishing Order --- + # Check if there is already an order at the entry price and adopt it + open_orders = self.get_open_orders() + for o in open_orders: + if o['coin'] == COIN_SYMBOL: + # Check if price matches entry_price (with small tolerance) + o_px = to_decimal(o['limitPx']) + if abs(o_px - entry_price) / entry_price < Decimal("0.0001"): + logger.info(f"[FISHING] Adopted existing fishing order {o['oid']} @ {o_px}") + self.fishing_oid = o['oid'] + break + except Exception as e: logger.error(f"Failed to init strategy: {e}") self.strategy = None @@ -524,10 +540,11 @@ class ScalperHedger: return { 'size': to_decimal(pos["position"]["szi"]), 'pnl': to_decimal(pos["position"]["unrealizedPnl"]), + 'entry_price': to_decimal(pos["position"]["entryPx"]), 'equity': equity } - return {'size': Decimal("0"), 'pnl': Decimal("0"), 'equity': equity} - except: return {'size': Decimal("0"), 'pnl': Decimal("0"), 'equity': Decimal("0")} + return {'size': Decimal("0"), 'pnl': Decimal("0"), 'entry_price': Decimal("0"), 'equity': equity} + except: return {'size': Decimal("0"), 'pnl': Decimal("0"), 'entry_price': Decimal("0"), 'equity': Decimal("0")} def get_open_orders(self) -> List[Dict]: try: @@ -627,8 +644,14 @@ class ScalperHedger: def manage_orders(self) -> bool: """Returns True if there is an active order that should prevent new trades.""" open_orders = self.get_open_orders() - my_orders = [o for o in open_orders if o['coin'] == COIN_SYMBOL] + # Filter out the fishing order from active management + my_orders = [o for o in open_orders if o['coin'] == COIN_SYMBOL and o['oid'] != self.fishing_oid] + # Verify if fishing_oid is still alive + all_oids = [o['oid'] for o in open_orders] + if self.fishing_oid and self.fishing_oid not in all_oids: + self.fishing_oid = None + if not my_orders: return False @@ -914,6 +937,12 @@ class ScalperHedger: cooldown_text = "" if diff_abs > rebalance_threshold: + # CANCEL FISHING ORDER BEFORE REBALANCE + if self.fishing_oid: + logger.info(f"[FISHING] Cancelling fishing order {self.fishing_oid} to rebalance.") + self.cancel_order(COIN_SYMBOL, self.fishing_oid) + self.fishing_oid = None + if bypass_cooldown: can_trade = True logger.info(f"[WARN] COOLDOWN BYPASSED: {override_reason}") @@ -975,15 +1004,72 @@ class ScalperHedger: # Calculate approximate trigger prices (Linear Approximation) # G = 0.5 * L * P^-1.5 gamma = (Decimal("0.5") * self.strategy.L * (price ** Decimal("-1.5"))) + + # Equilibrium Price (where diff would be 0) + # If diff > 0 (Need Sell), we need Target to drop, so Price must Rise. + # Gamma is positive absolute value here, but delta/price relationship is inverse. + # Delta ~ 1/sqrt(P). Slope is negative. + # So P_target = P_current + (Diff / Gamma) + p_mid = price + (calc['diff'] / gamma) + # Price where Diff reaches -threshold (BUY) p_buy = price + (rebalance_threshold + calc['diff']) / gamma # Price where Diff reaches +threshold (SELL) p_sell = price - (rebalance_threshold - calc['diff']) / gamma net_pnl = self.accumulated_pnl - self.accumulated_fees - logger.info(f"[IDLE] Px: {price:.2f} | 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() + # --- 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(): + 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. + + # 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). + + # --- 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: + # 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)") + 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}") + self.track_fills_and_pnl() time.sleep(CHECK_INTERVAL) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 2629bd7..366de9e 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file. - This change aims to improve net profitability by ensuring that rebalancing fees are only paid when the delta imbalance is statistically significant. - **Logging Improvements**: - Updated `[IDLE]` log format to show estimated **BUY (B)** and **SELL (S)** trigger price levels instead of raw delta difference. This provides better visual feedback on how far the price is from the next rebalance. +- **Fishing Order Logic**: + - Implemented a "Fishing Order" mechanism that keeps a Maker order (10% of hedge size) resting at the `entry_price` while in the safe zone. + - **Logic Update:** Configured to always place a **Limit BUY** at the entry price (when Px > Entry) to reduce the hedge size at break-even. + - **Fix:** Updated position tracking to use the actual Hedge Entry Price from Hyperliquid instead of the LP Strategy entry price. + - Integrated fishing order tracking to distinguish it from standard rebalance orders. ## [2025-12-20]