""" Funding rate whipsaw trader — premium index decay in final seconds of funding epoch. Hyperliquid funding settles every 8 hours (UTC 00:00, 08:00, 16:00). In the final 60 seconds before settlement, the premium index (Mark - Oracle) must converge to prevent arbitrage. HFTs trade this convergence deterministically. The strategy: - If premium is positive with <60s until funding → SHORT perp (price will drop) - If premium is negative with <60s until funding → LONG perp (price will rise) - Scale position based on premium magnitude and time remaining - Close position at funding settlement (T+0) """ from __future__ import annotations import time from datetime import datetime, timezone from typing import Optional class FundingWhipsawTrader: """Trade the deterministic decay of the premium index in the final seconds before Hyperliquid's funding settlement. Funding epochs: 00:00, 08:00, 16:00 UTC every day. Premium index = (mark_price - oracle_price) / oracle_price Usage: trader = FundingWhipsawTrader() trader.update(mark_px=64500, oracle_px=64480) signal = trader.signal() if signal['action'] != 'none': # place order: signal['side'], signal['size'], signal['confidence'] """ def __init__( self, min_premium_bps: float = 0.5, # minimum premium in bps to trigger max_size: float = 0.001, # max position size enter_seconds_before: float = 60.0, # seconds before funding to enter close_seconds_after: float = 5.0, # seconds after funding to close ): self._min_premium_bps = min_premium_bps self._max_size = max_size self._enter_seconds = enter_seconds_before self._close_seconds = close_seconds_after self._mark_px: float = 0.0 self._oracle_px: float = 0.0 self._premium_bps: float = 0.0 self._last_update: float = 0.0 self._in_position: bool = False self._position_side: str = "" self._entry_time: float = 0.0 def update(self, mark_px: float, oracle_px: float): """Feed current mark and oracle prices.""" self._mark_px = mark_px self._oracle_px = oracle_px self._last_update = time.time() if oracle_px > 0: self._premium_bps = (mark_px - oracle_px) / oracle_px * 10000 def seconds_to_funding(self) -> float: """Seconds until the next funding settlement (every 8 hours, UTC).""" now = datetime.now(timezone.utc) epoch_hours = [0, 8, 16] # Funding at 00:00, 08:00, 16:00 UTC current_hour = now.hour # Find next funding epoch next_epoch_hour = None for h in epoch_hours: if h > current_hour or (h == current_hour and now.minute == 0 and now.second < 5): next_epoch_hour = h break if next_epoch_hour is None: # After 16:00, next is 00:00 tomorrow next_epoch = now.replace(hour=0, minute=0, second=0, microsecond=0) from datetime import timedelta next_epoch += timedelta(days=1) else: next_epoch = now.replace(hour=next_epoch_hour, minute=0, second=0, microsecond=0) delta = (next_epoch - now).total_seconds() return max(0.0, delta) def seconds_since_funding(self) -> float: """Seconds since the most recent funding settlement.""" seconds_to = self.seconds_to_funding() if seconds_to < 3600: # <1 hour to next return 8 * 3600 - seconds_to return 8 * 3600 + (3600 - seconds_to % 3600) # Approximate def signal(self) -> dict: """Generate trading signal based on premium and time to funding. Returns: action: "enter_long", "enter_short", "close", "none" side: "buy" or "sell" (for orders) size: position size (scaled by time remaining) confidence: 0-1 confidence in the signal premium_bps: current premium in bps seconds_to_funding: time until settlement """ secs = self.seconds_to_funding() premium = self._premium_bps # After funding + small delay: close any position secs_since = self.seconds_since_funding() if self._in_position and secs_since < self._close_seconds: self._in_position = False return { "action": "close", "side": "sell" if self._position_side == "buy" else "buy", "size": self._max_size, "confidence": 1.0, "premium_bps": round(premium, 2), "seconds_to_funding": round(secs, 1), "reason": "funding_settled", } # Outside entry window: no action if secs > self._enter_seconds or secs < 0: return { "action": "none", "side": "", "size": 0.0, "confidence": 0.0, "premium_bps": round(premium, 2), "seconds_to_funding": round(secs, 1), "reason": "outside_entry_window", } # Already in position if self._in_position: return { "action": "hold", "side": self._position_side, "size": self._max_size, "confidence": 0.8, "premium_bps": round(premium, 2), "seconds_to_funding": round(secs, 1), "reason": "holding", } # Check premium threshold if abs(premium) < self._min_premium_bps: return { "action": "none", "side": "", "size": 0.0, "confidence": 0.0, "premium_bps": round(premium, 2), "seconds_to_funding": round(secs, 1), "reason": "premium_too_small", } # Scale size by time remaining (more remaining = more uncertainty = smaller size) time_factor = max(0.3, secs / self._enter_seconds) scaled_size = self._max_size * (1.0 - time_factor * 0.5) if premium > 0: # Premium positive → perp is expensive → short it side = "sell" action = "enter_short" confidence = min(1.0, abs(premium) / self._min_premium_bps * 0.3) else: # Premium negative → perp is cheap → long it side = "buy" action = "enter_long" confidence = min(1.0, abs(premium) / self._min_premium_bps * 0.3) self._in_position = True self._position_side = side self._entry_time = time.time() return { "action": action, "side": side, "size": round(scaled_size, 8), "confidence": round(confidence, 3), "premium_bps": round(premium, 2), "seconds_to_funding": round(secs, 1), "reason": f"premium_{premium:.1f}bps_{secs:.0f}s", } @property def premium_bps(self) -> float: return self._premium_bps @property def in_position(self) -> bool: return self._in_position