diff --git a/clp_hedger/clp_scalper_hedger.py b/clp_hedger/clp_scalper_hedger.py index 467174f..811f6a4 100644 --- a/clp_hedger/clp_scalper_hedger.py +++ b/clp_hedger/clp_scalper_hedger.py @@ -30,7 +30,7 @@ setup_logging("normal", "SCALPER_HEDGER") # --- CONFIGURATION --- COIN_SYMBOL = "ETH" -CHECK_INTERVAL = 1 # Faster check for scalper +CHECK_INTERVAL = 5 # Optimized for cost/noise reduction (was 1) LEVERAGE = 5 # 3x Leverage STATUS_FILE = "hedge_status.json" @@ -39,8 +39,8 @@ STATUS_FILE = "hedge_status.json" ZONE_BOTTOM_HEDGE_LIMIT = 0.5 # Close Zone: 15% to 20% -> Close All Hedges (Flatten) -ZONE_CLOSE_START = 0.51 -ZONE_CLOSE_END = 0.52 +ZONE_CLOSE_START = 0.52 +ZONE_CLOSE_END = 0.54 # Middle Zone: 20% to 85% -> Idle (No new orders, keep existing) # Implied by gaps between other zones. @@ -49,8 +49,8 @@ ZONE_CLOSE_END = 0.52 ZONE_TOP_HEDGE_START = 0.8 # --- ORDER SETTINGS --- -PRICE_BUFFER_PCT = 0.0005 # 0.05% price move triggers order update -MIN_THRESHOLD_ETH = 0.01 # Minimum trade size in ETH +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) MIN_ORDER_VALUE_USD = 10.0 # Minimum order value for API safety def get_active_automatic_position(): @@ -286,6 +286,24 @@ class ScalperHedger: return 4 except: return 4 + def get_order_book_levels(self, coin): + try: + l2_snapshot = self.info.l2_snapshot(coin) + if l2_snapshot and 'levels' in l2_snapshot: + bids = l2_snapshot['levels'][0] + asks = l2_snapshot['levels'][1] + if bids and asks: + best_bid = float(bids[0]['px']) + best_ask = float(asks[0]['px']) + mid = (best_bid + best_ask) / 2 + return {'bid': best_bid, 'ask': best_ask, 'mid': mid} + # Fallback + px = self.get_market_price(coin) + return {'bid': px, 'ask': px, 'mid': px} + except: + px = self.get_market_price(coin) + return {'bid': px, 'ask': px, 'mid': px} + def get_market_price(self, coin): try: mids = self.info.all_mids() @@ -321,9 +339,12 @@ class ScalperHedger: user_state = self.info.user_state(self.vault_address or self.account.address) for pos in user_state["assetPositions"]: if pos["position"]["coin"] == coin: - return float(pos["position"]["szi"]) - return 0.0 - except: return 0.0 + return { + 'size': float(pos["position"]["szi"]), + 'pnl': float(pos["position"]["unrealizedPnl"]) + } + return {'size': 0.0, 'pnl': 0.0} + except: return {'size': 0.0, 'pnl': 0.0} def get_open_orders(self): try: @@ -341,10 +362,12 @@ class ScalperHedger: logging.info(f"🕒 PLACING LIMIT: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price:.2f}") reduce_only = is_buy try: - # Gtc order (Maker) + # Gtc order (Maker) -> Changed to Alo to force Maker limit_px = round_to_sig_figs(price, 5) - order_result = self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Gtc"}}, reduce_only=reduce_only) + # Use 'Alo' (Add Liquidity Only) to ensure Maker rebate. + # If price crosses spread, order is rejected (safe cost-wise). + order_result = self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Alo"}}, reduce_only=reduce_only) status = order_result["status"] if status == "ok": response_data = order_result["response"]["data"] @@ -421,18 +444,69 @@ class ScalperHedger: self.cancel_order(COIN_SYMBOL, o['oid']) price = self.get_market_price(COIN_SYMBOL) - current_pos = self.get_current_position(COIN_SYMBOL) + pos_data = self.get_current_position(COIN_SYMBOL) + current_pos = pos_data['size'] + if current_pos == 0: return is_buy = current_pos < 0 final_size = round_to_sz_decimals(abs(current_pos), self.sz_decimals) if final_size == 0: return - # Market order for closing - self.exchange.order(COIN_SYMBOL, is_buy, final_size, round_to_sig_figs(price * (1.05 if is_buy else 0.95), 5), {"limit": {"tif": "Ioc"}}, reduce_only=True) + price = self.get_market_price(COIN_SYMBOL) # Get mid price for safety fallback + pos_data = self.get_current_position(COIN_SYMBOL) + current_pos = pos_data['size'] + + if current_pos == 0: return + + is_buy_to_close = current_pos < 0 + final_size = round_to_sz_decimals(abs(current_pos), self.sz_decimals) + if final_size == 0: return + + # --- ATTEMPT MAKER CLOSE (Alo) --- + try: + book_levels = self.get_order_book_levels(COIN_SYMBOL) + TICK_SIZE = 0.1 + + if is_buy_to_close: # We are short, need to buy to close + maker_price = book_levels['bid'] - TICK_SIZE + else: # We are long, need to sell to close + maker_price = book_levels['ask'] + TICK_SIZE + + logging.info(f"Attempting MAKER CLOSE (Alo): {COIN_SYMBOL} {'BUY' if is_buy_to_close else 'SELL'} {final_size} @ {maker_price:.2f}") + order_result = self.exchange.order(COIN_SYMBOL, is_buy_to_close, final_size, round_to_sig_figs(maker_price, 5), {"limit": {"tif": "Alo"}}, reduce_only=True) + + status = order_result["status"] + if status == "ok": + response_data = order_result["response"]["data"] + if "statuses" in response_data and "resting" in response_data["statuses"][0]: + logging.info(f"✅ MAKER CLOSE Order Placed (Alo). OID: {response_data['statuses'][0]['resting']['oid']}") + return + elif "statuses" in response_data and "filled" in response_data["statuses"][0]: + logging.info(f"✅ MAKER CLOSE Order Filled (Alo). OID: {response_data['statuses'][0]['filled']['oid']}") + return + else: + # Fallback if Alo didn't rest or fill immediately in an expected way + logging.warning(f"Alo order result unclear: {order_result}. Falling back to Market Close.") + + elif status == "error": + if "Post only order would have immediately matched" in order_result["response"]["data"]["statuses"][0].get("error", ""): + logging.warning("Alo order would have immediately matched. Falling back to Market Close for guaranteed fill.") + else: + logging.error(f"Alo order failed with unknown error: {order_result}. Falling back to Market Close.") + else: + logging.warning(f"Alo order failed with status {status}. Falling back to Market Close.") + + except Exception as e: + logging.error(f"Exception during Alo close attempt: {e}. Falling back to Market Close.", exc_info=True) + + # --- FALLBACK TO MARKET CLOSE (Ioc) for guaranteed fill --- + logging.info(f"Falling back to MARKET CLOSE (Ioc): {COIN_SYMBOL} {'BUY' if is_buy_to_close else 'SELL'} {final_size} @ {price:.2f} (guaranteed)") + self.exchange.order(COIN_SYMBOL, is_buy_to_close, final_size, round_to_sig_figs(price * (1.05 if is_buy_to_close else 0.95), 5), {"limit": {"tif": "Ioc"}}, reduce_only=True) self.active_position_id = None + logging.info("✅ MARKET CLOSE Order Placed (Ioc).") except Exception as e: - logging.error(f"Error closing: {e}") + logging.error(f"Error closing positions: {e}", exc_info=True) def run(self): logging.info(f"Starting Scalper Monitor Loop. Interval: {CHECK_INTERVAL}s") @@ -467,13 +541,17 @@ class ScalperHedger: continue # 2. Market Data - price = self.get_order_book_mid(COIN_SYMBOL) + book_levels = self.get_order_book_levels(COIN_SYMBOL) + price = book_levels['mid'] + if price is None: time.sleep(5) continue funding_rate = self.get_funding_rate(COIN_SYMBOL) - current_pos_size = self.get_current_position(COIN_SYMBOL) + pos_data = self.get_current_position(COIN_SYMBOL) + current_pos_size = pos_data['size'] + current_pnl = pos_data['pnl'] # 3. Calculate Logic calc = self.strategy.calculate_rebalance(price, current_pos_size) @@ -498,8 +576,8 @@ class ScalperHedger: 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) - # Update JSON with zone prices if missing - if 'zone_bottom_limit_price' not in active_pos: + # 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), @@ -513,7 +591,7 @@ class ScalperHedger: # --- Execute Logic --- if in_close_zone: - logging.info(f"ZONE: CLOSE ({price:.2f} in {zone_close_bottom_price:.2f}-{zone_close_top_price:.2f}). Closing all hedge positions.") + logging.info(f"ZONE: CLOSE ({price:.2f} in {zone_close_bottom_price:.2f}-{zone_close_top_price:.2f}). PNL: ${current_pnl:.2f}. Closing all hedge positions.") self.close_all_positions() time.sleep(CHECK_INTERVAL) continue @@ -532,20 +610,31 @@ class ScalperHedger: 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})") + logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${MIN_ORDER_VALUE_USD:.2f}). PNL: ${current_pnl:.2f}") elif trade_size > 0: - logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone.") + logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone. PNL: ${current_pnl:.2f}") + # 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 + TICK_SIZE = 0.1 + is_buy = (calc['action'] == "BUY") - self.place_limit_order(COIN_SYMBOL, is_buy, trade_size, price) + + if is_buy: + exec_price = book_levels['bid'] - TICK_SIZE + else: + exec_price = book_levels['ask'] + TICK_SIZE + + self.place_limit_order(COIN_SYMBOL, is_buy, trade_size, exec_price) else: - logging.info("Trade size rounds to 0. Skipping.") + logging.info(f"Trade size rounds to 0. Skipping. PNL: ${current_pnl:.2f}") else: - logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone.") + logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone. PNL: ${current_pnl:.2f}") else: # MIDDLE ZONE (IDLE) pct_position = (price - clp_low_range) / range_width - logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). No Actions.") + logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). PNL: ${current_pnl:.2f}. No Actions.") time.sleep(CHECK_INTERVAL) diff --git a/clp_hedger/hedge_status.json b/clp_hedger/hedge_status.json index 7fb9df3..87efb3f 100644 --- a/clp_hedger/hedge_status.json +++ b/clp_hedger/hedge_status.json @@ -392,5 +392,81 @@ "zone_close_start_price": 3083.4622618522967, "zone_close_end_price": 3084.080897853553, "zone_top_start_price": 3102.6399778912387 + }, + { + "type": "AUTOMATIC", + "token_id": 5157680, + "opened": "22:21 14/12/25", + "status": "CLOSED", + "entry_price": 3090.84, + "target_value": 1979.52, + "amount0_initial": 0.3137, + "amount1_initial": 1009.93, + "range_upper": 3121.29, + "zone_top_start_price": 3108.93, + "zone_close_top_price": 3092.24, + "zone_close_bottom_price": 3091.0, + "zone_bottom_limit_price": 3090.39, + "range_lower": 3059.48, + "static_long": 0.0, + "timestamp_open": 1765747295, + "timestamp_close": 1765755472 + }, + { + "type": "AUTOMATIC", + "token_id": 5157819, + "opened": "00:45 15/12/25", + "status": "CLOSED", + "entry_price": 3058.26, + "target_value": 1980.8, + "amount0_initial": 0.3044, + "amount1_initial": 1049.83, + "range_upper": 3087.14, + "zone_top_start_price": 3074.92, + "zone_close_top_price": 3059.02, + "zone_close_bottom_price": 3057.8, + "zone_bottom_limit_price": 3056.58, + "range_lower": 3026.02, + "static_long": 0.0, + "timestamp_open": 1765755940, + "timestamp_close": 1765762761 + }, + { + "type": "AUTOMATIC", + "token_id": 5157922, + "opened": "02:47 15/12/25", + "status": "CLOSED", + "entry_price": 3104.56, + "target_value": 1980.84, + "amount0_initial": 0.2967, + "amount1_initial": 1059.84, + "range_upper": 3133.8, + "zone_top_start_price": 3121.39, + "zone_close_top_price": 3105.26, + "zone_close_bottom_price": 3104.02, + "zone_bottom_limit_price": 3102.78, + "range_lower": 3071.75, + "static_long": 0.0, + "timestamp_open": 1765763228, + "timestamp_close": 1765765504 + }, + { + "type": "AUTOMATIC", + "token_id": 5158011, + "opened": "03:32 15/12/25", + "status": "OPEN", + "entry_price": 3135.31, + "target_value": 1983.24, + "amount0_initial": 0.3009, + "amount1_initial": 1039.86, + "range_upper": 3165.29, + "zone_top_start_price": 3152.76, + "zone_close_top_price": 3136.46, + "zone_close_bottom_price": 3135.21, + "zone_bottom_limit_price": 3133.95, + "range_lower": 3102.62, + "static_long": 0.0, + "timestamp_open": 1765765971, + "timestamp_close": null } ] \ No newline at end of file diff --git a/clp_hedger/uniswap_manager.py b/clp_hedger/uniswap_manager.py index 9027d88..c4aca59 100644 --- a/clp_hedger/uniswap_manager.py +++ b/clp_hedger/uniswap_manager.py @@ -644,6 +644,11 @@ def main(): unclaimed0 = from_wei(fees_sim[0], pos_details['token0_decimals']) unclaimed1 = from_wei(fees_sim[1], pos_details['token1_decimals']) except: pass + + # Calculate Total Fee Value in Token1 (USDC) + # Get Current Price from Pool Data + current_price = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], pos_details['token0_decimals'], pos_details['token1_decimals']) + total_fees_usd = (unclaimed0 * current_price) + unclaimed1 # Check Range is_out_of_range = False @@ -657,7 +662,7 @@ def main(): print(f"\nID: {token_id} | Type: {pos_type} | Status: {status_str}") print(f" Range: {position['range_lower']:.2f} - {position['range_upper']:.2f}") - print(f" Fees: {unclaimed0:.4f} {pos_details['token0_symbol']} / {unclaimed1:.4f} {pos_details['token1_symbol']}") + print(f" Fees: {unclaimed0:.4f} {pos_details['token0_symbol']} / {unclaimed1:.4f} {pos_details['token1_symbol']} (~${total_fees_usd:.2f})") # --- AUTO CLOSE LOGIC (AUTOMATIC ONLY) --- if pos_type == 'AUTOMATIC' and CLOSE_POSITION_ENABLED and is_out_of_range: