feat: disable vol multiplier inside range & add edge cleanup logic

This commit is contained in:
2025-12-26 22:36:03 +01:00
parent 27edce0085
commit 4f7bb429b7
2 changed files with 85 additions and 38 deletions

View File

@ -86,13 +86,13 @@ MIN_ORDER_VALUE_USD = Decimal("10.0")
# Capital Safety
DYNAMIC_THRESHOLD_MULTIPLIER = Decimal("1.2")
MIN_TIME_BETWEEN_TRADES = 25
MIN_TIME_BETWEEN_TRADES = 60
MAX_HEDGE_MULTIPLIER = Decimal("1.25")
# Rebalance Threshold (Base)
# Adjust this based on expected volatility:
# - Low Volatility (Weekend/Chop): 0.08 - 0.10 (8-10%) to reduce churn
# - High Volatility (Events): 0.05 (5%) to track price closely
BASE_REBALANCE_THRESHOLD_PCT = Decimal("0.12") # 12%
BASE_REBALANCE_THRESHOLD_PCT = Decimal("0.09") # 9%
# Edge Protection
EDGE_PROXIMITY_PCT = Decimal("0.04")
@ -101,10 +101,15 @@ POSITION_OPEN_EDGE_PROXIMITY_PCT = Decimal("0.06")
POSITION_CLOSED_EDGE_PROXIMITY_PCT = Decimal("0.025")
LARGE_HEDGE_MULTIPLIER = Decimal("2.8")
# Edge Cleanup Settings
ENABLE_EDGE_CLEANUP = True
EDGE_CLEANUP_MARGIN_PCT = Decimal("0.02") # 2% of range width
# Fishing Order (Maker "Fishing" at Entry Price)
ENABLE_FISHING = True
FISHING_ORDER_SIZE_PCT = Decimal("0.10") # 10% of hedge size
MAKER_ORDER_TIMEOUT = 400
MAKER_ORDER_TIMEOUT = 600
SHADOW_ORDER_TIMEOUT = 600
# --- HELPER FUNCTIONS ---
@ -354,6 +359,7 @@ class ScalperHedger:
# Logging Rate Limiting
self.last_idle_log_time = 0
self.last_pending_log_time = 0
self.api_backoff_until = 0
# Order Tracking
self.original_order_side = None
@ -638,7 +644,11 @@ class ScalperHedger:
logger.error(f"Order Failed: {order_result}")
except Exception as e:
logger.error(f"Exception during trade: {e}")
if "429" in str(e):
logger.warning(f"Rate limit hit during trade (429). Backing off for 30s.")
self.api_backoff_until = time.time() + 30
else:
logger.error(f"Exception during trade: {e}")
return None
def manage_orders(self) -> bool:
@ -697,7 +707,8 @@ class ScalperHedger:
def track_fills_and_pnl(self, force: bool = False):
try:
now = time.time()
if not force and now - self.last_pnl_check_time < 10:
# Increase interval to 60s to avoid 429 Rate Limits
if not force and now - self.last_pnl_check_time < 60:
return
self.last_pnl_check_time = now
@ -722,8 +733,15 @@ class ScalperHedger:
if new_activity:
logger.info(f"Fills tracked. Total Price PnL: {self.accumulated_pnl:.2f} | Total Fees: {self.accumulated_fees:.2f}")
except Exception as e:
logger.error(f"Error tracking fills: {e}")
# Handle 429 specifically
if "429" in str(e):
logger.warning(f"Rate limit hitting while tracking fills (429). Backing off.")
# Add a small penalty delay to last_check to prevent immediate retry
self.last_pnl_check_time = time.time() + 30
else:
logger.error(f"Error tracking fills: {e}")
def close_all_positions(self, force_taker: bool = False):
logger.info("Closing all positions...")
@ -775,6 +793,14 @@ class ScalperHedger:
while True:
try:
# API Backoff Check
if time.time() < self.api_backoff_until:
wait_time = self.api_backoff_until - time.time()
if int(wait_time) % 5 == 0: # Log every 5s
logger.warning(f"Backing off due to 429... ({wait_time:.1f}s)")
time.sleep(1)
continue
active_pos = get_active_automatic_position()
# Check Global Disable or Missing Position
@ -858,6 +884,10 @@ class ScalperHedger:
if vol_pct > 0:
vol_multiplier = max(Decimal("1.0"), min(Decimal("3.0"), vol_pct / base_vol_ref))
# Disable volatility index when price is strictly inside edges (top and bottom) of range
if self.strategy.low_range < price < self.strategy.high_range:
vol_multiplier = Decimal("1.0")
# 3. Base Threshold Calculation (Range Dependent)
range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range
@ -885,6 +915,21 @@ class ScalperHedger:
if pct_change > Decimal("0.003"):
rebalance_threshold *= DYNAMIC_THRESHOLD_MULTIPLIER
# --- FORCE EDGE CLEANUP (Dynamic Margin) ---
# Ensure we trade at the X% line if we haven't already.
if ENABLE_EDGE_CLEANUP:
dist_bottom_pct = (price - self.strategy.low_range) / self.strategy.low_range
dist_top_pct = (self.strategy.high_range - price) / self.strategy.high_range
range_width_pct = (self.strategy.high_range - self.strategy.low_range) / self.strategy.low_range
safety_margin_pct = range_width_pct * EDGE_CLEANUP_MARGIN_PCT
if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
if rebalance_threshold > MIN_THRESHOLD_ETH:
if rebalance_threshold > MIN_THRESHOLD_ETH * Decimal("1.5"):
logger.info(f"[EDGE] Inside {EDGE_CLEANUP_MARGIN_PCT*100}% Safety Zone. Forcing tight threshold: {rebalance_threshold:.4f} -> {MIN_THRESHOLD_ETH:.4f}")
rebalance_threshold = MIN_THRESHOLD_ETH
self.last_price = price
# 5. Check Zones
@ -1017,56 +1062,57 @@ class ScalperHedger:
# Price where Diff reaches +threshold (SELL)
p_sell = price - (rebalance_threshold - calc['diff']) / gamma
# Recalculate triggers if outside range (Safety Buffer: Dynamic Margin)
# User Request: "at range is too late", "recalculated to be bellow it"
r_width = self.strategy.high_range - self.strategy.low_range
safety_margin = r_width * EDGE_CLEANUP_MARGIN_PCT
if p_buy > self.strategy.high_range:
p_buy = self.strategy.high_range - safety_margin
if p_sell < self.strategy.low_range:
p_sell = self.strategy.low_range + safety_margin
net_pnl = self.accumulated_pnl - self.accumulated_fees
logger.info(f"[IDLE] Px: {price:.2f} | M: {p_mid:.1f} | B: {p_buy:.1f} / S: {p_sell:.1f} (Vol: {vol_pct*100:.3f}% x{vol_multiplier:.1f} | Thresh: {final_threshold_pct*100:.1f}%) | TotPnL: {net_pnl:.2f}")
self.last_idle_log_time = time.time()
# --- FISHING ORDER LOGIC (SAFE ZONE) ---
# Always keep a maker order open at Entry Price for 10% of hedge size
if self.fishing_oid is None and not self.get_open_orders():
if ENABLE_FISHING and self.fishing_oid is None and not self.get_open_orders():
try:
# Use REAL Hedge Entry Price from Hyperliquid, not LP Entry
hedge_entry = pos_data.get('entry_price', Decimal("0"))
# Only fish if we actually have a position
if hedge_entry > 0 and current_size != 0:
# Determine side:
# We are SHORT.
# If Px < Entry: We are in profit. We want to BUY to take profit/scale out.
# If Px > Entry: We are in loss. We want to SELL to average up/scale in.
# WAIT. "Fishing" usually means closing.
# User said: "Maker position ... at price of entry price"
# If we are Short, placing a BUY at Entry (when Px < Entry) acts as Take Profit.
# If we are Short, placing a SELL at Entry (when Px > Entry) acts as Averaging Up.
# Standard Grid logic:
# If Px > Entry: Place SELL at Entry? No, price is above entry. Limit Sell at Entry would fill instantly as Taker.
# If Px < Entry: Place BUY at Entry? No, price is below entry. Limit Buy at Entry would fill instantly.
# SYMMETRIC FISHING LOGIC:
# We want an order that SITS on the book at the entry price.
# If Price > Entry: Place BUY at Entry (Waiting for a dip to reduce short).
# If Price < Entry: Place SELL at Entry (Waiting for a pump to increase short).
# CORRECT LOGIC for "Return to Entry":
# We want an order that SITS on the book.
# If Current Price < Entry: We want to SELL if price goes back UP to Entry. (Add to Short / Re-enter).
# If Current Price > Entry: We want to BUY if price goes back DOWN to Entry. (Reduce Short / Escape).
if price > hedge_entry:
is_buy = True
target_price = hedge_entry
action_desc = "Reducing Short"
elif price < hedge_entry:
is_buy = False
target_price = hedge_entry
action_desc = "Increasing Short"
else:
# Exactly at entry, skip to avoid immediate Taker matching
is_buy = None
# --- ALWAYS BUY LOGIC ---
# We only place a Limit BUY at Entry Price.
# This only works as a "Fishing" Maker order if Px > Entry (Waiting for a dip).
target_price = hedge_entry
is_buy = True
# Only place if it's a valid Maker order (Target < Current Price)
if price > target_price:
if is_buy is not None:
# Size = 10% of Target Delta
fishing_size = abs(calc['target_short']) * FISHING_ORDER_SIZE_PCT
# Check distance - only place if price is not literally on top of entry
dist_pct = (price - target_price) / price
if dist_pct > Decimal("0.001"): # 0.1% buffer
logger.info(f"[FISHING] Placing Maker BUY {fishing_size:.4f} at Hedge Entry: {target_price:.2f} (Reducing Short at break-even)")
# 0.1% buffer to ensure it stays as a Maker order
dist_pct = abs(price - target_price) / price
if dist_pct > Decimal("0.001"):
logger.info(f"[FISHING] Placing Maker {'BUY' if is_buy else 'SELL'} {fishing_size:.4f} at Hedge Entry: {target_price:.2f} ({action_desc})")
self.fishing_oid = self.place_limit_order(COIN_SYMBOL, is_buy, fishing_size, target_price, "Alo")
else:
# Price is already below entry (In profit).
# Placing a BUY at entry would be a Taker trade at a worse price.
pass
except Exception as e:
logger.error(f"Error placing fishing order: {e}")