176 lines
5.9 KiB
Python
176 lines
5.9 KiB
Python
"""
|
||
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
|