Files
ftdt-quant-lab/strategies/gueant.py
T

176 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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