Update scripts with optimizations from florida directory

This commit is contained in:
2026-01-01 20:08:57 +01:00
parent 95f514b563
commit b1a86b81fc
2 changed files with 114 additions and 201 deletions

View File

@ -45,7 +45,7 @@ DEFAULT_STRATEGY = {
"POSITION_CLOSED_EDGE_PROXIMITY_PCT": Decimal("0.025"), # Safety margin for closing positions "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 "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 "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) "MAKER_ORDER_TIMEOUT": 600, # Timeout for resting Maker orders (seconds)
"SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking "SHADOW_ORDER_TIMEOUT": 600, # Timeout for theoretical shadow order tracking
"ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced) "ENABLE_FISHING": False, # Use passive maker orders for rebalancing (advanced)
@ -98,6 +98,7 @@ CLP_PROFILES = {
"TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT "TOKEN_B_ADDRESS": "0x55d398326f99059fF775485246999027B3197955", # USDT
"WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", "WRAPPED_NATIVE_ADDRESS": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"POOL_FEE": 100, "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"), "RANGE_WIDTH_PCT": Decimal("0.004"),
"TARGET_INVESTMENT_AMOUNT": 1000, "TARGET_INVESTMENT_AMOUNT": 1000,
"MIN_HEDGE_THRESHOLD": Decimal("0.015"), "MIN_HEDGE_THRESHOLD": Decimal("0.015"),

View File

@ -275,6 +275,7 @@ class UnifiedHedger:
self.last_prices = {} self.last_prices = {}
self.price_history = {} # Symbol -> List[Decimal] self.price_history = {} # Symbol -> List[Decimal]
self.last_trade_times = {} # Symbol -> timestamp self.last_trade_times = {} # Symbol -> timestamp
self.last_idle_log_times = {} # Symbol -> timestamp
# Shadow Orders (Global List) # Shadow Orders (Global List)
self.shadow_orders = [] self.shadow_orders = []
@ -667,7 +668,7 @@ class UnifiedHedger:
calc = strat.calculate_rebalance(price, Decimal("0")) calc = strat.calculate_rebalance(price, Decimal("0"))
if coin not in aggregates: 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 status == 'CLOSING':
# If Closing, we want target to be 0 for this strategy # 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: if dist_bottom_pct < safety_margin_pct or dist_top_pct < safety_margin_pct:
aggregates[coin]['is_at_edge'] = True 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) # Check Shadow Orders (Pre-Execution)
self.check_shadow_orders(l2_snapshots) self.check_shadow_orders(l2_snapshots)
@ -706,255 +709,164 @@ class UnifiedHedger:
for coin in coins_to_process: 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}) 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 if price == 0: continue
target_short_abs = data['target_short'] # Always positive (it's a magnitude of short) target_short_abs = data['target_short']
target_position = -target_short_abs # We want to be Short, so negative size target_position = -target_short_abs
current_pos = current_positions.get(coin, Decimal("0")) current_pos = current_positions.get(coin, Decimal("0"))
diff = target_position - current_pos
diff = target_position - current_pos # e.g. -1.0 - (-0.8) = -0.2 (Sell 0.2)
diff_abs = abs(diff) diff_abs = abs(diff)
# Thresholds # Thresholds
config = self.coin_configs.get(coin, {}) config = self.coin_configs.get(coin, {})
min_thresh = config.get("min_threshold", Decimal("0.008")) min_thresh = config.get("MIN_HEDGE_THRESHOLD", Decimal("0.008"))
# Volatility Multiplier
vol_pct = self.calculate_volatility(coin) vol_pct = self.calculate_volatility(coin)
base_vol = Decimal("0.0005") 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") 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")) base_rebalance_pct = config.get("BASE_REBALANCE_THRESHOLD_PCT", Decimal("0.20"))
thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult) thresh_pct = min(Decimal("0.15"), base_rebalance_pct * vol_mult)
dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct) dynamic_thresh = max(min_thresh, abs(target_position) * thresh_pct)
# FORCE EDGE CLEANUP if data['is_at_edge'] and config.get("ENABLE_EDGE_CLEANUP", True):
enable_edge_cleanup = config.get("ENABLE_EDGE_CLEANUP", True) if dynamic_thresh > min_thresh: dynamic_thresh = min_thresh
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
# Check Trigger
action_needed = diff_abs > dynamic_thresh action_needed = diff_abs > dynamic_thresh
# Determine Intent (Moved UP for Order Logic)
is_buy_bool = diff > 0 is_buy_bool = diff > 0
side_str = "BUY" if is_buy_bool else "SELL" side_str = "BUY" if is_buy_bool else "SELL"
# Manage Existing Orders # Manage Existing Orders
existing_orders = orders_map.get(coin, []) existing_orders = orders_map.get(coin, [])
force_taker_retry = False 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 order_matched = False
price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015")) price_buffer_pct = config.get("PRICE_BUFFER_PCT", Decimal("0.0015"))
for o in existing_orders: for o in existing_orders:
o_oid = o['oid'] o_oid = o['oid']
o_price = to_decimal(o['limitPx']) 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)) 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) 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 order_age_sec = (int(time.time()*1000) - o_timestamp) / 1000.0
if is_same_side and order_age_sec > maker_timeout: if is_same_side and order_age_sec > config.get("MAKER_ORDER_TIMEOUT", 300):
logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired ({order_age_sec:.1f}s > {maker_timeout}s). Cancelling to refresh.") logger.info(f"[TIMEOUT] {coin} Order {o_oid} expired. Cancelling.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
continue continue
# Fishing Timeout Check if config.get("ENABLE_FISHING", False) and is_same_side and order_age_sec > config.get("FISHING_TIMEOUT_FALLBACK", 30):
if enable_fishing and is_same_side and order_age_sec > fishing_timeout: logger.info(f"[FISHING] {coin} Order {o_oid} timed out. Retrying as Taker.")
logger.info(f"[FISHING] {coin} Order {o_oid} timed out ({order_age_sec:.1f}s > {fishing_timeout}s). Cancelling for Taker retry.")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
force_taker_retry = True 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 order_matched = True
if int(time.time()) % 10 == 0: if int(time.time()) % 15 == 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") logger.info(f"[WAIT] {coin} Pending {side_str} @ {o_price} | Age: {order_age_sec:.1f}s")
break break
else: else:
logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})") logger.info(f"Cancelling stale order {o_oid} ({o_side} @ {o_price})")
self.cancel_order(coin, o_oid) self.cancel_order(coin, o_oid)
# --- EXECUTION LOGIC --- # Determine Urgency / Bypass Cooldown
if not order_matched: 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
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
# --- 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: 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) last_trade = self.last_trade_times.get(coin, 0)
min_time = config.get("MIN_TIME_BETWEEN_TRADES", 60)
min_time_trade = config.get("MIN_TIME_BETWEEN_TRADES", 60) if bypass_cooldown or (time.time() - last_trade > min_time):
can_trade = False if coin not in l2_snapshots: l2_snapshots[coin] = self.info.l2_snapshot(coin)
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'] levels = l2_snapshots[coin]['levels']
if not levels[0] or not levels[1]: continue 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"
bid = to_decimal(levels[0][0]['px']) logger.info(f"[TRIG] {coin} {side_str} {diff_abs:.4f} | Cur: {current_pos:.4f} | Type: {order_type}")
ask = to_decimal(levels[1][0]['px']) oid = self.place_limit_order(coin, is_buy_bool, diff_abs, exec_price, order_type)
if oid:
# Price logic self.last_trade_times[coin] = time.time()
create_shadow = False if order_type == "Ioc":
shadow_price = bid if is_buy_bool else ask
# Decide Order Type: Taker (Ioc) or Maker (Alo) self.shadow_orders.append({'coin': coin, 'side': side_str, 'price': shadow_price, 'expires_at': time.time() + config.get("SHADOW_ORDER_TIMEOUT", 600)})
# 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
logger.info("Sleeping 10s for position update...")
time.sleep(10)
self._update_closed_pnl(coin)
else: else:
# Action NOT needed # Idle Cleanup
# Cleanup any dangling orders if existing_orders and not order_matched:
if existing_orders: for o in existing_orders: self.cancel_order(coin, o['oid'])
for o in existing_orders:
logger.info(f"Cancelling idle order {o['oid']} ({o['side']} @ {o['limitPx']})")
self.cancel_order(coin, o['oid'])
# --- IDLE LOGGING (Restored Format) --- # --- THROTTLED STATUS LOGGING ---
# Calculate aggregate Gamma to estimate triggers now = time.time()
# Gamma = 0.5 * Sum(L) * P^-1.5 last_log = self.last_idle_log_times.get(coin, 0)
# We need Sum(L) for this coin. monitor_interval = config.get("MONITOR_INTERVAL_SECONDS", 60)
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: if now - last_log >= monitor_interval:
gamma = (Decimal("0.5") * total_L * (price ** Decimal("-1.5"))) self.last_idle_log_times[coin] = now
if gamma > 0: if is_asymmetric_blocked:
# Equilibrium Price (Diff = 0) logger.info(f"[ASYMMETRIC] Blocking BUY. Px ({price:.2f}) >= Eq ({p_mid_asym:.2f}) & Not Edge")
p_mid = price + (diff / gamma)
# Triggers total_L_log = Decimal("0")
p_buy = price + (dynamic_thresh + diff) / gamma for k_strat, strat_inst in self.strategies.items():
p_sell = price - (dynamic_thresh - diff) / gamma if self.strategy_states[k_strat]['coin'] == coin:
total_L_log += strat_inst.L
if int(time.time()) % 30 == 0: 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 "" pad = " " if coin == "BNB" else ""
adj_val = data.get('adj_pct', Decimal("0")) * 100
# PnL Calc
unrealized = current_pnls.get(coin, Decimal("0")) unrealized = current_pnls.get(coin, Decimal("0"))
closed_pnl_total = Decimal("0") closed_pnl = sum(s['hedge_TotPnL'] for s in self.strategy_states.values() if s['coin'] == coin)
fees_total = Decimal("0") fees = sum(s['fees'] for s in self.strategy_states.values() if s['coin'] == coin)
for k, s_state in self.strategy_states.items(): total_pnl = (closed_pnl - fees) + unrealized
if s_state['coin'] == coin:
closed_pnl_total += s_state.get('hedge_TotPnL', Decimal("0"))
fees_total += s_state.get('fees', Decimal("0"))
total_pnl = (closed_pnl_total - fees_total) + 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:
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}")
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})") logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
else: else:
if int(time.time()) % 30 == 0: logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f}")
logger.info(f"[IDLE] {coin} | Px: {price:.2f} | delta: {target_position:.4f} | Diff: {diff:.4f} (Thresh: {dynamic_thresh:.4f})")
time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1)) time.sleep(DEFAULT_STRATEGY.get("CHECK_INTERVAL", 1))