diff --git a/clp_hedger.py b/clp_hedger.py index 82e3ccf..dd67b1e 100644 --- a/clp_hedger.py +++ b/clp_hedger.py @@ -346,6 +346,7 @@ class ScalperHedger: # Order Tracking self.original_order_side = None + self.shadow_orders = [] # Store theoretical Maker orders for analysis logger.info(f"[DELTA] Delta-Zero Scalper Hedger initialized. Agent: {self.account.address}") @@ -500,6 +501,51 @@ class ScalperHedger: return self.info.open_orders(self.vault_address or self.account.address) except: return [] + def check_shadow_orders(self, levels: Dict[str, Decimal]): + """ + Check if pending shadow (theoretical Maker) orders would have been filled. + """ + if not self.shadow_orders or not levels: + return + + now = time.time() + remaining_orders = [] + + for order in self.shadow_orders: + # 1. Check Fill + filled = False + fill_time = now - (order['expires_at'] - order['timeout_duration']) + + if order['side'] == 'BUY': + # Filled if someone SOLD into our Bid (Current Ask <= Our Bid Price) + # Wait... Maker Buy sits at Bid. It fills if Market Price drops to it. + # Actually, we need to track if TRADE price hit it. + # Proxy: If Current Best Ask <= Our Shadow Bid, it DEFINITELY filled (crossed). + # Conservative Proxy: If Current Best Bid < Our Shadow Bid? No. + # Standard Sim: If Low Price <= Our Limit. + # Here we only have snapshots. + # If 'levels["bid"]' goes below our price, did we fill? Maybe not. + # If 'levels["ask"]' goes below our price, we definitely filled. + if levels['ask'] <= order['price']: + filled = True + else: # SELL + # Filled if Current Best Bid >= Our Shadow Ask + if levels['bid'] >= order['price']: + filled = True + + if filled: + logger.info(f"[SHADOW] ✅ SUCCESS: Maker {order['side']} @ {order['price']:.2f} filled in {fill_time:.1f}s (Timeout: {order['timeout_duration']:.0f}s)") + continue # Remove from list + + # 2. Check Expiry + if now > order['expires_at']: + logger.info(f"[SHADOW] ❌ FAILED: Maker {order['side']} @ {order['price']:.2f} timed out after {order['timeout_duration']:.0f}s") + continue # Remove from list + + remaining_orders.append(order) + + self.shadow_orders = remaining_orders + def cancel_order(self, coin: str, oid: int): logger.info(f"Cancelling order {oid}...") try: @@ -704,6 +750,9 @@ class ScalperHedger: time.sleep(0.1) continue + # Check Shadow Orders (Market Maker Simulation) + self.check_shadow_orders(levels) + price = levels['mid'] pos_data = self.get_current_position(COIN_SYMBOL) current_size = pos_data['size'] @@ -836,6 +885,32 @@ class ScalperHedger: if oid: self.last_trade_time = time.time() self.track_fills_and_pnl(force=True) + + # --- Shadow Order Creation --- + # Simulate: "What if we placed a Maker order instead?" + try: + # Dynamic Timeout: Inverse to Volatility + # Low Vol (0.025%) -> 60s wait. High Vol (0.15%) -> 10s wait. + base_vol = Decimal("0.0005") # 0.05% + # Avoid div by zero + safe_vol = max(Decimal("0.0001"), vol_pct) + calc_timeout = float(30 * (base_vol / safe_vol)) + dynamic_timeout = max(10.0, min(60.0, calc_timeout)) + + # Shadow Price (Passive) + # If we Taker BUY, we would have Maker BUY at BID + # If we Taker SELL, we would have Maker SELL at ASK + shadow_price = levels['bid'] if is_buy else levels['ask'] + + self.shadow_orders.append({ + 'side': 'BUY' if is_buy else 'SELL', + 'price': shadow_price, + 'timeout_duration': dynamic_timeout, + 'expires_at': time.time() + dynamic_timeout + }) + logger.info(f"[SHADOW] Created Maker {'BUY' if is_buy else 'SELL'} @ {shadow_price:.2f} (Timeout: {dynamic_timeout:.0f}s)") + except Exception as e: + logger.error(f"Shadow logic error: {e}") else: if time.time() - self.last_idle_log_time > 30: logger.info(f"[WAIT] Cooldown. Diff: {diff_abs:.4f}{cooldown_text}")