diff --git a/clp_hedger.py b/clp_hedger.py index 9b53f35..b1e0620 100644 --- a/clp_hedger.py +++ b/clp_hedger.py @@ -349,6 +349,34 @@ class ScalperHedger: logger.info(f"[DELTA] Delta-Zero Scalper Hedger initialized. Agent: {self.account.address}") + def calculate_volatility(self) -> Decimal: + """ + Calculate volatility (Standard Deviation %) of price history. + Uses standard deviation of the last N prices relative to the mean. + Returns: Decimal percentage (e.g., 0.001 = 0.1% volatility) + """ + if not self.price_history or len(self.price_history) < 30: + return Decimal("0.0") + + try: + # 1. Mean + n = len(self.price_history) + mean = sum(self.price_history) / n + + # 2. Variance (Sum of squared diffs) + variance = sum([pow(p - mean, 2) for p in self.price_history]) / n + + # 3. Std Dev + std_dev = variance.sqrt() + + # 4. Volatility % + if mean == 0: return Decimal("0.0") + return std_dev / mean + + except Exception as e: + logger.error(f"Error calculating volatility: {e}") + return Decimal("0.0") + def get_dynamic_edge_proximity(self, price: Decimal) -> Decimal: """ Calculate dynamic edge proximity based on position value. @@ -685,14 +713,51 @@ class ScalperHedger: calc = self.strategy.calculate_rebalance(price, current_size) diff_abs = abs(calc['diff']) + # Update Price History (Max 300 items = 5 mins @ 1s) + self.price_history.append(price) + if len(self.price_history) > 300: + self.price_history.pop(0) + # 4. Thresholds sqrt_Pa = self.strategy.low_range.sqrt() sqrt_Pb = self.strategy.high_range.sqrt() max_potential_eth = self.strategy.L * ((Decimal("1")/sqrt_Pa) - (Decimal("1")/sqrt_Pb)) - rebalance_threshold = max(MIN_THRESHOLD_ETH, max_potential_eth * Decimal("0.05")) + # --- Dynamic Threshold Optimization (ATR/Vol Based) --- - # Volatility Adjustment + # 1. Calculate Volatility + vol_pct = self.calculate_volatility() + + # 2. Volatility Multiplier + # Base Vol assumption: 0.05% (0.0005) per window. + # If Vol is 0.15%, mult = 3x. Cap at 3.0x. Min 1.0x. + base_vol_ref = Decimal("0.0005") + vol_multiplier = Decimal("1.0") + if vol_pct > 0: + vol_multiplier = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol_ref)) + + # 3. Base Threshold Calculation (Range Dependent) + range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range + + # Ensure we satisfy PRICE_BUFFER_PCT (0.15%) minimum + base_threshold_pct = max(Decimal("0.05"), PRICE_BUFFER_PCT / range_width_pct if range_width_pct > 0 else Decimal("0.05")) + + # 4. Apply Multiplier + target_threshold_pct = base_threshold_pct * vol_multiplier + + # 5. Safety Cap + # Limit threshold to 20% of the total range width (relative) to prevent staying unhedged too long + # e.g. if range is 1% wide, max threshold is 0.2% deviation. + # If range is 10% wide, max threshold is 2% deviation. + # Absolute hard cap at 15% delta deviation. + safety_cap = min(Decimal("0.15"), Decimal("0.20")) + + final_threshold_pct = min(target_threshold_pct, safety_cap) + + rebalance_threshold = max(MIN_THRESHOLD_ETH, max_potential_eth * final_threshold_pct) + + # Volatility Adjustment (Instantaneous Shock) + # Keep this for sudden spikes that haven't affected the 5-min average yet if self.last_price: pct_change = abs(price - self.last_price) / self.last_price if pct_change > Decimal("0.003"): @@ -765,7 +830,7 @@ class ScalperHedger: exec_price = levels['ask'] * Decimal("1.001") if is_buy else levels['bid'] * Decimal("0.999") urgency = "URGENT" if bypass_cooldown else "NORMAL" - logger.info(f"[TRIG] Rebalance ({urgency}): {calc['action']} {diff_abs:.4f} (Diff > {rebalance_threshold:.4f})") + logger.info(f"[TRIG] Rebalance ({urgency}): {calc['action']} {diff_abs:.4f} > {rebalance_threshold:.4f} (Vol: {vol_pct*100:.3f}% x{vol_multiplier:.1f} | Thresh: {final_threshold_pct*100:.1f}%)") oid = self.place_limit_order(COIN_SYMBOL, is_buy, diff_abs, exec_price, "Ioc") if oid: @@ -777,7 +842,7 @@ class ScalperHedger: self.last_idle_log_time = time.time() else: if time.time() - self.last_idle_log_time > 30: - logger.info(f"[IDLE] Px: {price:.2f} | Diff: {diff_abs:.4f} < {rebalance_threshold:.4f} | PnL: {current_pnl:.2f}") + logger.info(f"[IDLE] Px: {price:.2f} | Diff: {diff_abs:.4f} < {rebalance_threshold:.4f} (Vol: {vol_pct*100:.3f}% x{vol_multiplier:.1f} | Thresh: {final_threshold_pct*100:.1f}%) | PnL: {current_pnl:.2f}") self.last_idle_log_time = time.time() self.track_fills_and_pnl()