Le prix que vous n'obtenez pas
Un backtest négocie au prix de ses données ; un ordre réel est exécuté ailleurs. Le glissement (slippage) est cette différence : on achète un peu plus haut que le prix vu, et on vend un peu plus bas. Il a deux sources. L'écart : un ordre au marché achète à l'ask et vend au bid, donc on paie la moitié de l'écart de chaque côté par rapport au prix milieu. Et l'impact de marché : un gros ordre épuise les meilleures cotations et pousse le prix contre lui.
def fill_price(mid, side, slippage_bps):
if side == "buy":
return mid * (1 + slippage_bps / 10000)
if side == "sell":
return mid * (1 - slippage_bps / 10000)
raise ValueError("side must be 'buy' or 'sell'")
print(round(fill_price(50.0, "buy", 4), 4)) # 50.02 pays more
print(round(fill_price(50.0, "sell", 4), 4)) # 49.98 receives less# Crossing the spread: a market buy pays the ask, a market sell receives the bid
bid, ask = 49.98, 50.02
mid = (bid + ask) / 2
half_spread_bps = (ask - bid) / 2 / mid * 10000
print(mid, round(half_spread_bps, 2)) # 50.0 4.0 each way, relative to the mid price
print(round((ask - mid) / mid * 10000, 2), round((mid - bid) / mid * 10000, 2)) # 4.0 4.0La mesurer par rapport à une référence
Pour mesurer le glissement, il faut une référence : le prix d'arrivée, le prix milieu quand l'ordre a été décidé. Le manque à gagner est signé de sorte que positif signifie toujours un coût : pour un achat, fill - arrival ; pour une vente, arrival - fill, en points de base du prix d'arrivée. La même exécution mauvaise pour un acheteur est bonne pour un vendeur, d'où la dépendance du signe au sens. Les gros ordres sont exécutés par morceaux, donc le chiffre à rapporter est le prix moyen d'exécution pondéré par les volumes.
import numpy as np
def slippage_bps(fill, arrival, side):
if side == "buy":
return (fill - arrival) / arrival * 10000
if side == "sell":
return (arrival - fill) / arrival * 10000
raise ValueError("side must be 'buy' or 'sell'")
def average_fill(prices, quantities):
prices, quantities = np.asarray(prices, dtype=float), np.asarray(quantities, dtype=float)
return float(np.sum(prices * quantities) / np.sum(quantities))
# Buying 600 shares in three pieces; the price at arrival was 50.00
avg = average_fill([50.0, 50.1, 50.3], [100, 200, 300])
print(round(avg, 4)) # 50.1833
print(round(slippage_bps(avg, 50.0, "buy"), 2)) # 36.67 bps: it cost us
print(round(slippage_bps(avg, 50.0, "sell"), 2)) # -36.67: the same fills would have helped a sellerL'impact croît avec la taille
L'écart est un coût fixe ; l'impact dépend de ce que l'on négocie. Une règle empirique bien testée est la loi de la racine carrée : l'impact attendu en fraction du prix vaut environ k * volatility * sqrt(quantity / ADV), où ADV est le volume quotidien moyen et k une constante proche de 1. Quadrupler l'ordre ne fait que doubler l'impact, donc fractionner un gros ordre dans le temps aide, mais moins qu'on ne l'espère.
import math
def sqrt_impact_bps(volatility, quantity, adv, k=1.0):
return k * volatility * math.sqrt(quantity / adv) * 10000
# A stock with 2% daily volatility and 1,000,000 shares of average daily volume
for shares in (10_000, 50_000, 200_000, 800_000):
print(shares, round(sqrt_impact_bps(0.02, shares, 1_000_000), 1), "bps")
# 10,000: 20.0, 50,000: 44.7, 200,000: 89.4, 800,000: 178.9- Un achat est exécuté au-dessus du prix milieu et une vente en dessous :
mid * (1 +/- bps / 10000); l'écart seul coûte la moitié de sa largeur dans chaque sens. - Mesurez par rapport au prix d'arrivée, signé pour que positif signifie coût ; les exécutions partielles se combinent par une moyenne pondérée par les volumes.
- L'impact suit la loi de la racine carrée
k * volatility * sqrt(quantity / ADV): quatre fois la taille, deux fois l'impact.
À suivre : des exécutions d'ordres réalistes, simuler comment les ordres sont réellement exécutés contre un carnet et des barres.
Lab : ce qui est noté
- Chiffrer l'exécution avec
fill_price25 pts - Mesurer le manque à gagner avec
slippage_bps25 pts - Fusionner les exécutions partielles avec
average_fill25 pts - Prévoir l'impact avec
sqrt_impact_bps25 pts