From b1a86b81fc89ab8b9a3240103c5537687b701a8c Mon Sep 17 00:00:00 2001 From: DiTus Date: Thu, 1 Jan 2026 20:08:57 +0100 Subject: [PATCH] Update scripts with optimizations from florida directory --- clp_config.py | 3 +- clp_hedger.py | 312 ++++++++++++++++++-------------------------------- 2 files changed, 114 insertions(+), 201 deletions(-) diff --git a/clp_config.py b/clp_config.py index a4807ee..42f9c55 100644 --- a/clp_config.py +++ b/clp_config.py @@ -45,7 +45,7 @@ DEFAULT_STRATEGY = { "POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions "LARGE_HEDGE_MULTIPLIER": Decimal("5.0"), # Multiplier to bypass trade cooldown for big moves "ENABLE_EDGE_CLEANUP": True, # Force rebalances when price is at range boundaries - "EDGE_CLEANUP_MARGIN_PCT": Decimal("0.02"), # % of range width used for edge detection + "EDGE_CLEANUP_MARGIN_PCT": Decimal("0.03"), # % of range width used for edge detection "MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds) "SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking "ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced) @@ -98,6 +98,7 @@ CLP_PROFILES = { "TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT "WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", "POOL_FEE": 100, + "EDGE_CLEANUP_MARGIN_PCT": Decimal("0.1875"), # 0.1875 only for asymmetric shedge % of range width used for edge detection "RANGE_WIDTH_PCT": Decimal("0.004"), "TARGET_INVESTMENT_AMOUNT": 1000, "MIN_HEDGE_THRESHOLD": Decimal("0.015"), diff --git a/clp_hedger.py b/clp_hedger.py index f83c4cc..fd6aca1 100644 --- a/clp_hedger.py +++ b/clp_hedger.py @@ -275,6 +275,7 @@ class UnifiedHedger: self.last_prices = {} self.price_history = {} # Symbol -> List[Decimal] self.last_trade_times = {} # Symbol -> timestamp + self.last_idle_log_times = {} # Symbol -> timestamp # Shadow Orders (Global List) self.shadow_orders = [] @@ -667,7 +668,7 @@ class UnifiedHedger: calc = strat.calculate_rebalance(price, Decimal("0")) if coin not in aggregates: - aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False} + aggregates[coin] = {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'is_at_bottom_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False} if status == 'CLOSING': # If Closing, we want target to be 0 for this strategy @@ -693,6 +694,8 @@ class UnifiedHedger: if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct: aggregates[coin]['is_at_edge'] = True + if dist_bottom_pct < safety_margin_pct: + aggregates[coin]['is_at_bottom_edge'] = True # Check Shadow Orders (Pre-Execution) self.check_shadow_orders(l2_snapshots) @@ -706,255 +709,164 @@ class UnifiedHedger: for coin in coins_to_process: data = aggregates.get(coin, {'target_short': Decimal("0"), 'contributors': 0, 'is_at_edge': False, 'adj_pct': Decimal("0"), 'is_closing': False}) - price = self.last_prices.get(coin, Decimal("0")) # FIX: Explicitly get price for this coin + price = self.last_prices.get(coin, Decimal("0")) if price == 0: continue - target_short_abs = data['target_short'] # Always positive (it's a magnitude of short) - target_position = -target_short_abs # We want to be Short, so negative size - + target_short_abs = data['target_short'] + target_position = -target_short_abs current_pos = current_positions.get(coin, Decimal("0")) - - diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2) + diff = target_position - current_pos diff_abs = abs(diff) # Thresholds config = self.coin_configs.get(coin, {}) - min_thresh = config.get("min_threshold", Decimal("0.008")) - - # Volatility Multiplier + min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008")) vol_pct = self.calculate_volatility(coin) base_vol = Decimal("0.0005") vol_mult = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol)) if vol_pct > 0 else Decimal("1.0") - base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20")) thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult) dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct) - # FORCE EDGE CLEANUP - enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True) - if data['is_at_edge'] and enable_edge_cleanup: - if dynamic_thresh > min_thresh: - # logger.info(f"[EDGE] {coin} forced to min threshold.") - dynamic_thresh = min_thresh + if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True): + if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh - # Check Trigger action_needed = diff_abs > dynamic_thresh - - # Determine Intent (Moved UP for Order Logic) is_buy_bool = diff > 0 side_str = "BUY" if is_buy_bool else "SELL" # Manage Existing Orders 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_side = o['side'] 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 - - # Maker Timeout Check (General) - maker_timeout = config.get("MAKER_ORDER_TIMEOUT", 300) order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0 - if is_same_side and order_age_sec > maker_timeout: - logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.") + if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300): + logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.") self.cancel_order(coin, o_oid) continue - # Fishing Timeout Check - 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.") + if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30): + logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.") self.cancel_order(coin, o_oid) force_taker_retry = True - continue # Do not mark matched, let it flow to execution + continue - if is_same_side and dist_pct < price_buffer_pct: + if is_same_side and (abs(price - o_price) / price) < 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") + if int(time.time()) % 15 == 0: + logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | 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) - - # --- EXECUTION LOGIC --- - if not order_matched: - if action_needed or force_taker_retry: - bypass_cooldown = 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 - elif data.get('is_closing', False): - bypass_cooldown = True - logger.info(f"[URGENT] {coin} Closing Strategy -> Force Taker Exit") - - # 2. Ghost/Cleanup -> Maker - elif data.get('contributors', 0) == 0: - if time.time() - self.startup_time > 5: - force_maker = True - logger.info(f"[CLEANUP] {coin} Ghost Position -> Force Maker Reduce") - else: - logger.info(f"[STARTUP] Skipping Ghost Cleanup for {coin} (Grace Period)") - continue # Skip execution for this coin - - # Large Hedge Check (Only Force Taker if AT EDGE) - large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0")) - if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False): - bypass_cooldown = True - logger.info(f"[WARN] LARGE HEDGE (Edge Protection): {diff_abs:.4f} > {dynamic_thresh:.4f} (x{large_hedge_mult})") - elif diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker: - # Large hedge but safe zone -> Maker is fine, but maybe log it - logger.info(f"[INFO] Large Hedge (Safe Zone): {diff_abs:.4f}. Using Standard Execution.") - - last_trade = self.last_trade_times.get(coin, 0) - - min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60) - can_trade = False - if bypass_cooldown: - can_trade = True - elif time.time() - last_trade > min_time_trade: - can_trade = True - - if can_trade: - # Get Orderbook for Price - if coin not in l2_snapshots: - l2_snapshots[coin] = self.info.l2_snapshot(coin) - - levels = l2_snapshots[coin]['levels'] - if not levels[0] or not levels[1]: continue - - bid = to_decimal(levels[0][0]['px']) - ask = to_decimal(levels[1][0]['px']) - - # Price logic - 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: - exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999") - order_type = "Ioc" - create_shadow = True - else: - # Fishing / Standard Maker - exec_price = bid if is_buy_bool else ask - 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} | Type: {order_type}") - - oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type) - if oid: - self.last_trade_times[coin] = time.time() - - # Shadow Order - if create_shadow: - shadow_price = bid if is_buy_bool else ask - shadow_timeout = config.get("SHADOW_ORDER_TIMEOUT", 600) - self.shadow_orders.append({ - 'coin': coin, - 'side': side_str, - 'price': shadow_price, - 'expires_at': time.time() + shadow_timeout - }) - logger.info(f"[SHADOW] Created Maker {side_str} @ {shadow_price:.2f}") - - # UPDATED: Sleep for API Lag (Phase 5.1) - logger.info("Sleeping 10s to allow position update...") - time.sleep(10) - - # --- UPDATE CLOSED PnL FROM API --- - self._update_closed_pnl(coin) - else: - # Cooldown log - pass + # Determine Urgency / Bypass Cooldown + bypass_cooldown = False + force_maker = False + if not order_matched and (action_needed or force_taker_retry): + if force_taker_retry: bypass_cooldown = True + elif data.get('is_closing', False): bypass_cooldown = True + elif data.get('contributors', 0) == 0: + if time.time() - self.startup_time > 5: force_maker = True + else: continue # Skip startup ghost positions - else: - # Action NOT needed - # Cleanup any dangling orders - if existing_orders: - for o in existing_orders: - logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})") - self.cancel_order(coin, o['oid']) + large_hedge_mult = config.get("LARGE_HEDGE_MULTIPLIER", Decimal("5.0")) + if diff_abs > (dynamic_thresh * large_hedge_mult) and not force_maker and data.get('is_at_edge', False): + # Prevent IOC for BUYs at bottom edge + if not (is_buy_bool and data.get('is_at_bottom_edge', False)): + bypass_cooldown = True - # --- IDLE LOGGING (Restored Format) --- - # Calculate aggregate Gamma to estimate triggers - # Gamma = 0.5 * Sum(L) * P^-1.5 - # We need Sum(L) for this coin. - total_L = Decimal("0") - # We need to re-iterate or cache L. - # Simpler: Just re-sum L from active strats for this coin. - for key, strat in self.strategies.items(): - if self.strategy_states[key]['coin'] == coin: - total_L += strat.L - - if total_L > 0 and price > 0: - gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5"))) - if gamma > 0: - # Equilibrium Price (Diff = 0) - p_mid = price + (diff / gamma) + # --- ASYMMETRIC HEDGE CHECK --- + is_asymmetric_blocked = False + p_mid_asym = Decimal("0") + if is_buy_bool and not bypass_cooldown: + total_L_asym = Decimal("0") + for k_strat, strat_inst in self.strategies.items(): + if self.strategy_states[k_strat]['coin'] == coin: + total_L_asym += strat_inst.L + + gamma_asym = (Decimal("0.5") * total_L_asym * (price ** Decimal("-1.5"))) + if gamma_asym > 0: + p_mid_asym = price - (diff_abs / gamma_asym) + if not data.get('is_at_edge', False) and price >= p_mid_asym: + is_asymmetric_blocked = True + + # --- EXECUTION --- + if not order_matched and not is_asymmetric_blocked: + if action_needed or force_taker_retry: + last_trade = self.last_trade_times.get(coin, 0) + min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60) - # Triggers - p_buy = price + (dynamic_thresh + diff) / gamma - p_sell = price - (dynamic_thresh - diff) / gamma - - if int(time.time()) % 30 == 0: - pad = " " if coin == "BNB" else "" - adj_val = data.get('adj_pct', Decimal("0")) * 100 - - # PnL Calc - unrealized = current_pnls.get(coin, Decimal("0")) - closed_pnl_total = Decimal("0") - fees_total = Decimal("0") - for k, s_state in self.strategy_states.items(): - if s_state['coin'] == coin: - closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0")) - fees_total += s_state.get('fees', Decimal("0")) + if bypass_cooldown or (time.time() - last_trade > min_time): + if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin) + levels = l2_snapshots[coin]['levels'] + if levels[0] and levels[1]: + bid, ask = to_decimal(levels[0][0]['px']), to_decimal(levels[1][0]['px']) + if bypass_cooldown and not force_maker: + exec_price = ask * Decimal("1.001") if is_buy_bool else bid * Decimal("0.999") + order_type = "Ioc" + else: + exec_price = bid if is_buy_bool else ask + order_type = "Alo" + + logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}") + oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type) + if oid: + self.last_trade_times[coin] = time.time() + if order_type == "Ioc": + shadow_price = bid if is_buy_bool else ask + self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)}) - total_pnl = (closed_pnl_total - fees_total) + unrealized - - pnl_pad = " " if unrealized >= 0 else "" - tot_pnl_pad = " " if total_pnl >= 0 else "" - - logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {adj_val:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f}{pnl_pad} | TotPnL: {total_pnl:.2f}{tot_pnl_pad}") + logger.info("Sleeping 10s for position update...") + time.sleep(10) + self._update_closed_pnl(coin) else: - if int(time.time()) % 30 == 0: + # Idle Cleanup + if existing_orders and not order_matched: + for o in existing_orders: self.cancel_order(coin, o['oid']) + + # --- THROTTLED STATUS LOGGING --- + now = time.time() + last_log = self.last_idle_log_times.get(coin, 0) + monitor_interval = config.get("MONITOR_INTERVAL_SECONDS", 60) + + if now - last_log >= monitor_interval: + self.last_idle_log_times[coin] = now + if is_asymmetric_blocked: + logger.info(f"[ASYMMETRIC] Blocking BUY. Px ({price:.2f}) >= Eq ({p_mid_asym:.2f}) & Not Edge") + + total_L_log = Decimal("0") + for k_strat, strat_inst in self.strategies.items(): + if self.strategy_states[k_strat]['coin'] == coin: + total_L_log += strat_inst.L + + if total_L_log > 0 and price > 0: + gamma_log = (Decimal("0.5") * total_L_log * (price ** Decimal("-1.5"))) + if gamma_log > 0: + p_mid_log = price - (diff / gamma_log) # Corrected equilibrium formula + p_buy = price + (dynamic_thresh + diff) / gamma_log + p_sell = price - (dynamic_thresh - diff) / gamma_log + pad = " " if coin == "BNB" else "" + + unrealized = current_pnls.get(coin, Decimal("0")) + closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin) + fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin) + total_pnl = (closed_pnl - fees) + unrealized + + logger.info(f"[IDLE] {coin} | Px: {price:.2f}{pad} | M: {p_mid_log:.1f}{pad} | B: {p_buy:.1f}{pad} / S: {p_sell:.1f}{pad} | delta: {target_position:.4f}({diff:+.4f}) | Adj: {data.get('adj_pct',0)*100:+.2f}%, Vol: {vol_mult:.2f}, Thr: {dynamic_thresh:.4f} | PnL: {unrealized:.2f} | TotPnL: {total_pnl:.2f}") + else: logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})") - else: - if int(time.time()) % 30 == 0: - logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})") + else: + logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}") time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))