""" Proper Avellaneda-Stoikov market making for the live node. Key formulas (Avellaneda & Stoikov, 2008): Reservation price: r = s - q * gamma * sigma^2 * tau Optimal spread: spread = gamma * sigma^2 * tau + (2/gamma) * ln(1 + gamma/k) Bid = r - spread/2 Ask = r + spread/2 Where: s = mid price, q = inventory, gamma = risk aversion sigma = volatility, tau = remaining session time, k = order intensity Production adaptations: - Rolling volatility estimation (5-min window) - Circuit breaker: pause quoting when price jump exceeds 3σ - Inventory bounds: stop quoting on over-exposed side - Virtual session clock: 1-hour windows since crypto is 24/7 """ import math from collections import deque class ASQuoter: """Stateless per-tick quote generator using A-S optimal control.""" def __init__( self, gamma: float = 0.1, # Risk aversion — higher = more aggressive inventory redux k: float = 1.5, # Order flow sensitivity — higher = tighter market tau: float = 1.0, # Virtual session length (hours, for 24/7 crypto) min_spread: float = 0.0001, # 1 bp minimum spread max_inventory: float = 0.001, # Max position before stopping one side vol_window: int = 300, # Number of price ticks for rolling vol (5 min @ 1s) cb_mult: float = 3.0, # Circuit breaker multiplier (3σ jump threshold) ): self.gamma = gamma self.k = k self.tau = tau self.min_spread = min_spread self.max_inventory = max_inventory self.vol_window = vol_window self.cb_mult = cb_mult self._mid_prices: deque[float] = deque(maxlen=vol_window) self._current_sigma: float = 0.02 # fallback: ~32% annualized for crypto self._session_start: float = 0.0 def observe(self, mid: float) -> None: """Feed a new mid-price observation. Updates rolling volatility.""" self._mid_prices.append(mid) if len(self._mid_prices) >= 2: prices = list(self._mid_prices) returns = [ (prices[i] - prices[i - 1]) / prices[i - 1] for i in range(1, len(prices)) ] mu = sum(returns) / len(returns) var = sum((r - mu) ** 2 for r in returns) / len(returns) sigma = math.sqrt(var) if var > 0 else 0.02 self._current_sigma = sigma @property def sigma(self) -> float: return self._current_sigma def circuit_breaker(self) -> bool: """Check if recent price jump exceeds threshold. If true, pause quoting.""" if len(self._mid_prices) < 5: return False recent = list(self._mid_prices)[-5:] move_pct = abs(recent[-1] - recent[0]) / recent[0] threshold = self.cb_mult * self._current_sigma * math.sqrt(5) return move_pct > threshold def quotes(self, mid: float, inventory: float, t: float) -> dict | None: """ Generate bid/ask quotes given current state. Args: mid: current mid-price inventory: current net position (positive = long) t: elapsed session time in hours (0 to tau) Returns: {"bid": ..., "ask": ..., "reservation": ..., "spread": ...} or None if paused """ self.observe(mid) if self.circuit_breaker(): return None # Pause quoting — price jump in progress # Reservation price: skew center by inventory risk tau_remaining = max(self.tau - t, 0.01) reservation = mid - inventory * self.gamma * (self._current_sigma ** 2) * tau_remaining # Optimal spread: balance risk compensation vs flow capture try: log_term = math.log(1.0 + self.gamma / self.k) except ValueError: log_term = 0.0 spread = ( self.gamma * (self._current_sigma ** 2) * tau_remaining + (2.0 / max(self.gamma, 0.001)) * log_term ) spread = max(spread, self.min_spread) half = spread / 2.0 bid = reservation - half ask = reservation + half return { "bid": max(bid, 1.0), # Never negative/zero "ask": max(ask, 1.0), "reservation": reservation, "spread": spread, }