Gagner l'écart, porter le risque
Un teneur de marché affiche un bid et un ask et attend. Quand un vendeur frappe le bid et un acheteur prend l'ask, le teneur achète bas et vend haut et garde l'écart. C'est le revenu. Le coût est le risque d'inventaire : entre les deux transactions, le teneur détient une position dont la valeur varie avec le prix, et il est plus souvent exécuté par quelqu'un qui en sait plus (sélection adverse) que par quelqu'un qui n'en sait pas. Bien tenir un marché, c'est équilibrer des cotations assez serrées pour être touchées souvent et assez larges pour rémunérer le risque, tout en gardant l'inventaire proche de zéro.
# The income: buy at the bid, sell at the ask, keep the spread
bid, ask, size = 99.95, 100.05, 100
cash = -bid * size + ask * size
print(round(cash, 2)) # 10.0 a round trip of 100 shares earned the 0.10 spread
# The risk: between the two trades the price moves against the inventory
inventory, mid_after = 100, 99.80
print(round(inventory * (mid_after - bid), 2)) # -15.0 the position lost more than the spread would have earnedAvellaneda-Stoikov : où coter
Le modèle d'Avellaneda-Stoikov donne une réponse en forme close. D'abord, le prix de réservation r = mid - q * gamma * sigma^2 * tau est l'endroit où le teneur serait content de négocier étant donné l'inventaire q : un teneur long (q > 0) doit valoriser l'actif sous le mid et coter plus bas pour attirer des acheteurs, un teneur short doit coter plus haut. L'aversion au risque gamma, la volatilité sigma et le temps restant tau fixent l'intensité de l'inclinaison. Ensuite, l'écart optimal gamma * sigma^2 * tau + (2 / gamma) * ln(1 + gamma / k) a deux parties : une charge pour le risque de garder la position jusqu'à l'horizon, et un terme pour le flux d'ordres, où k dit à quelle vitesse les exécutions se tarissent quand on cote plus loin du mid. Les cotations sont r - spread/2 et r + spread/2.
# The reservation price leans against inventory: long -> quote lower, short -> quote higher
def reservation_price(mid, inventory, gamma, sigma, tau):
return mid - inventory * gamma * sigma ** 2 * tau
for inventory in (-50, 0, 50):
print(inventory, round(reservation_price(100.0, inventory, 0.1, 0.2, 1.0), 4))
# -50 100.2 short: quote higher to buy back
# 0 100.0 flat: centred on the mid
# 50 99.8 long: quote lower to sell# The optimal spread and the two quotes around the reservation price
import math
def optimal_spread(gamma, sigma, tau, k):
return gamma * sigma ** 2 * tau + (2 / gamma) * math.log(1 + gamma / k)
def make_quotes(mid, inventory, gamma, sigma, tau, k):
centre = mid - inventory * gamma * sigma ** 2 * tau
half = optimal_spread(gamma, sigma, tau, k) / 2
return centre - half, centre + half
print(round(optimal_spread(0.1, 0.2, 1.0, 1.5), 4)) # 1.2948
bid, ask = make_quotes(100.0, 0, 0.1, 0.2, 1.0, 1.5)
print(round(bid, 4), round(ask, 4)) # 99.3526 100.6474 symmetric around the mid when flat
bid, ask = make_quotes(100.0, 50, 0.1, 0.2, 1.0, 1.5)
print(round(bid, 4), round(ask, 4)) # 99.1526 100.4474 long 50: both quotes move down by 0.2Inventaire, exécutions et P&L
Suivez l'état par deux nombres, l'inventaire et la trésorerie. Quand le bid est touché, vous avez acheté : l'inventaire monte et la trésorerie baisse de price * quantity ; quand l'ask est touché, vous avez vendu : l'inverse. Votre richesse à tout instant est la trésorerie plus l'inventaire valorisé au mid, cash + inventory * mid, et sa variation est votre P&L. Ce P&L vient de l'écart capté et des mouvements de prix pendant la détention d'inventaire ; coter autour du prix de réservation ramène l'inventaire vers zéro car le côté qui le réduit devient plus attractif. Les limites comptent en pratique : un plafond strict d'inventaire, une pause dans les marchés rapides, et la vérification à chaque cotation que le bid est sous l'ask.
# Booking fills and marking the book to market
def apply_fill(state, side, price, quantity):
inventory, cash = state
if side == "bid":
return inventory + quantity, cash - price * quantity
return inventory - quantity, cash + price * quantity
state = (0, 0.0)
state = apply_fill(state, "bid", 99.95, 100) # our bid is hit: we buy 100
print(state) # (100, -9995.0)
state = apply_fill(state, "ask", 100.05, 100) # our ask is hit: we sell 100
print(state) # (0, 10.0) flat again, and the spread is the profit
inventory, cash = apply_fill(state, "bid", 99.9, 50)
print(round(cash + inventory * 99.9, 2)) # 10.0 marked at the price we just paid, the equity is unchanged- Un teneur de marché gagne l'écart mais porte un risque d'inventaire et de sélection adverse ; le but : assez serré pour négocier, assez large pour être payé, inventaire proche de zéro.
- Le prix de réservation
mid - q * gamma * sigma^2 * taus'appuie contre l'inventaire ; l'écartgamma * sigma^2 * tau + (2/gamma) ln(1 + gamma/k)croît avec la volatilité pondérée par l'aversion au risque et le temps. - Suivez inventaire et trésorerie, valorisez au mid, imposez des limites d'inventaire strictes, et testez la stratégie dans un simulateur sur des données enregistrées avant de l'utiliser en réel.
À suivre : le modèle de Heston, une volatilité elle-même aléatoire.
Lab : ce qui est noté
- S'appuyer contre l'inventaire avec
reservation_price25 pts - Fixer la largeur avec
optimal_spread25 pts - Coter les deux côtés avec
make_quotes25 pts - Enregistrer une exécution avec
apply_fill25 pts