""" Guéant Closed-Form Market Making Model. Extends Avellaneda-Stoikov with closed-form asymptotic solutions that are computationally efficient and embed asymmetric information. Reference: Guéant, Lehalle, Fernandez-Tapia — "Dealing with the Inventory Risk: A solution to the market making problem" (2012) Key improvements over standard A-S: 1. Closed-form solutions (no PDE solving needed) 2. Explicit handling of asymmetric information (adverse selection) 3. Explicit dependence on order book shape 4. Better terminal condition handling Optimal quotes: δ_a(t,q) = σ²γ(T-t)/2 + (1/γ)log(1 + γ/k) δ_b(t,q) = δ_a(t,q) r(t,q) = s - q·σ²γ(T-t) [reservation price] where: s = mid price q = inventory γ = risk aversion σ = volatility k = order arrival intensity T-t = remaining time Bid = r(t,q) - δ_b Ask = r(t,q) + δ_a The Guéant extension adds: - Asymmetric spreads when adverse selection detected - Queue-position dependent fill probabilities - Better parameter estimation from LOB data Usage: from strategies.gueant import GueantMM mm = GueantMM(gamma=0.1, sigma=0.01) bid, ask = mm.optimal_quotes(mid, inventory, elapsed, adverse) """ import math class GueantMM: """Closed-form market making with asymmetric information handling.""" def __init__(self, gamma: float = 0.1, sigma: float = 0.01, k: float = 1.5, T: float = 60.0, max_pos: float = 0.005): """ Args: gamma: risk aversion (0.01=v.aggressive, 1.0=v.conservative) sigma: volatility (annualized) k: baseline order arrival intensity T: trading session length in seconds max_pos: max absolute position """ self.gamma = gamma self.sigma = sigma self.k = k self.T = T self.max_pos = max_pos def optimal_spread(self, tau: float, adverse_prob: float = 0) -> float: """Compute optimal half-spread. Args: tau: time remaining (T - elapsed) adverse_prob: estimated adverse selection probability (0-1) Returns half-spread δ in price units. """ if tau < 0.01: tau = 0.01 sig2 = self.sigma**2 gamma = self.gamma # Base Guéant spread: σ²γτ/2 + (1/γ)log(1+γ/k) base_spread = gamma * sig2 * tau / 2.0 if gamma > 0 and self.k > 0: log_term = (1.0 / gamma) * math.log(1.0 + gamma / self.k) else: log_term = 0.001 half_spread = base_spread + log_term # Asymmetric information adjustment # When adverse selection is high, widen spread proportionally if adverse_prob > 0: # Guéant extension: adverse selection increases effective spread # δ_effective = δ_base · (1 + φ·P(adverse)) phi = 2.0 # Sensitivity to adverse selection half_spread *= (1.0 + phi * adverse_prob) return max(half_spread, 0.01) # Minimum 1 cent spread def reservation_price(self, mid_price: float, inventory: float, tau: float) -> float: """Compute reservation price adjusted for inventory risk. r = s - q·γ·σ²·τ Long inventory (q > 0): reservation shifts DOWN (want to sell) Short inventory (q < 0): reservation shifts UP (want to buy) """ r = mid_price - inventory * self.gamma * self.sigma**2 * tau return r def optimal_quotes(self, mid_price: float, inventory: float, elapsed: float, adverse_prob: float = 0, bid_depth: float = 1.0, ask_depth: float = 1.0) -> dict: """Compute optimal bid and ask quotes. Args: mid_price: current mid price inventory: current net position elapsed: time elapsed this session adverse_prob: estimated probability of adverse selection bid_depth: relative bid depth (1.0 = normal, >1 = deeper book) ask_depth: relative ask depth (1.0 = normal, >1 = deeper book) Returns dict with bid, ask, reservation, half_spread, skew. """ tau = self.T - elapsed if tau < 0.01: tau = 0.01 r = self.reservation_price(mid_price, inventory, tau) spread = self.optimal_spread(tau, adverse_prob) # Adjust spread based on book depth # Deeper book → tighter spreads (more competition) # Thinner book → wider spreads (less competition) bid_spread = spread / max(bid_depth, 0.5) ask_spread = spread / max(ask_depth, 0.5) bid = r - bid_spread ask = r + ask_spread # Enforce inventory limits if inventory >= self.max_pos: bid = 0 # Don't buy more if inventory <= -self.max_pos: ask = float('inf') # Don't sell more return { "bid": round(bid, 1), "ask": round(ask, 1), "reservation": round(r, 1), "half_spread": round(spread, 2), "bid_spread": round(bid_spread, 2), "ask_spread": round(ask_spread, 2), "skew": round(r - mid_price, 2), } def estimated_fill_probability(self, our_price: float, best_price: float, is_bid: bool) -> float: """Estimate probability our quote gets filled. Based on distance from best and queue position. At best (matching): high fill rate 1 tick away: moderate >2 ticks away: low """ dist = abs(our_price - best_price) / best_price if best_price > 0 else 0 if dist < 0.0001: # At the best price level return 0.30 # ~30% chance per tick elif dist < 0.0005: # Within 1 tick return 0.10 elif dist < 0.002: # Within 2 ticks return 0.03 return 0.01