feat(hedger): implement dynamic rebalance threshold based on volatility

This commit is contained in:
2025-12-20 09:16:06 +01:00
parent 271bea4653
commit 7c72dd3a1f

View File

@ -349,6 +349,34 @@ class ScalperHedger:
logger.info(f"[DELTA] Delta-Zero Scalper Hedger initialized. Agent: {self.account.address}") 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: def get_dynamic_edge_proximity(self, price: Decimal) -> Decimal:
""" """
Calculate dynamic edge proximity based on position value. Calculate dynamic edge proximity based on position value.
@ -685,14 +713,51 @@ class ScalperHedger:
calc = self.strategy.calculate_rebalance(price, current_size) calc = self.strategy.calculate_rebalance(price, current_size)
diff_abs = abs(calc['diff']) 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 # 4. Thresholds
sqrt_Pa = self.strategy.low_range.sqrt() sqrt_Pa = self.strategy.low_range.sqrt()
sqrt_Pb = self.strategy.high_range.sqrt() sqrt_Pb = self.strategy.high_range.sqrt()
max_potential_eth = self.strategy.L * ((Decimal("1")/sqrt_Pa) - (Decimal("1")/sqrt_Pb)) 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: if self.last_price:
pct_change = abs(price - self.last_price) / self.last_price pct_change = abs(price - self.last_price) / self.last_price
if pct_change > Decimal("0.003"): 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") exec_price = levels['ask'] * Decimal("1.001") if is_buy else levels['bid'] * Decimal("0.999")
urgency = "URGENT" if bypass_cooldown else "NORMAL" 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") oid = self.place_limit_order(COIN_SYMBOL, is_buy, diff_abs, exec_price, "Ioc")
if oid: if oid:
@ -777,7 +842,7 @@ class ScalperHedger:
self.last_idle_log_time = time.time() self.last_idle_log_time = time.time()
else: else:
if time.time() - self.last_idle_log_time > 30: 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.last_idle_log_time = time.time()
self.track_fills_and_pnl() self.track_fills_and_pnl()