"""
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,
        }
