Pourquoi ne pas tout envoyer d'un coup
Achetez 200 000 actions avec un seul ordre au marché et vous allez dévorer le carnet d'ordres : les premières actions s'exécutent au meilleur ask, les suivantes à un prix moins bon, et ainsi de suite, et tous ceux qui vous voient acheter relèvent leurs prix. Cet impact de marché est un vrai coût, souvent plus grand que la commission. Un algorithme d'exécution découpe l'ordre en ordres enfants plus petits envoyés dans le temps, chacun étant absorbé avec moins d'impact. Le plus simple est le TWAP, l'algorithme de prix moyen pondéré par le temps : on divise la quantité en parts égales et on en envoie une à chacun des instants régulièrement espacés entre un début et une fin.
# Splitting 10 shares into 3 child orders: whole shares, earlier pieces take the extra
def twap_schedule(total, slices):
size, extra = divmod(total, slices)
return [size + (1 if index < extra else 0) for index in range(slices)]
print(twap_schedule(10, 3)) # [4, 3, 3]
print(twap_schedule(200_000, 8)) # [25000, 25000, 25000, 25000, 25000, 25000, 25000, 25000]
print(sum(twap_schedule(10_001, 7))) # 10001 nothing is lost to roundingLe calendrier : combien et quand
Un calendrier TWAP a deux parties. Les tailles découpent le total en n parts d'actions entières qui diffèrent d'au plus une action et totalisent exactement l'ordre : divmod(total, n) donne la taille de base et le nombre de parts qui reçoivent une action de plus. Les instants sont n heures de début régulièrement espacées entre le début et la fin : start + i * (end - start) / n. Un ordre d'une journée de 200 000 actions en 40 tranches fait 5 000 actions toutes les dix minutes, envoyées par un ordonnanceur comme celui du sujet précédent. Les vraies implémentations randomisent un peu tailles et instants, car un motif parfaitement régulier est facile à repérer et à exploiter par les autres.
# When each slice starts: a trading day from 09:30 to 16:00 in 13 half-hour slices (seconds since midnight)
def slice_times(start, end, slices):
step = (end - start) / slices
return [start + index * step for index in range(slices)]
times = slice_times(9.5 * 3600, 16 * 3600, 13)
print(times[0], times[1] - times[0]) # 34200.0 1800.0 09:30 and a slice every 30 minutes
print(times[-1] / 3600) # 15.5 the last slice starts at 15:30, the end (16:00) is not a start timeLa mesurer : prix moyen d'exécution et glissement
Une fois les tranches exécutées, le prix moyen d'exécution est la moyenne des prix pondérée par les quantités, sum(price * quantity) / sum(quantity) : une grosse exécution compte plus qu'une petite, donc une moyenne simple des prix serait fausse. Pour juger le résultat, comparez-le au prix d'arrivée, le prix du marché au moment de la décision. L'écart est le glissement (slippage), exprimé en points de base (1 pb vaut 0,01 %) : pour un achat (moyen - arrivée) / arrivée * 10 000, pour une vente le signe inverse, de sorte qu'un nombre positif est toujours un coût. C'est le cœur du shortfall d'implémentation, la façon standard de noter une exécution.
# Average fill price (quantity-weighted) and slippage against the arrival price, in basis points
def average_fill_price(fills):
return sum(price * quantity for price, quantity in fills) / sum(quantity for _, quantity in fills)
def slippage_bps(avg_price, arrival_price, side):
sign = 1 if side == "buy" else -1
return sign * (avg_price - arrival_price) / arrival_price * 10000
fills = [(100.0, 400), (100.2, 300), (100.4, 300)]
average = average_fill_price(fills)
print(round(average, 2), sum(price for price, _ in fills) / 3) # 100.18 100.2 the plain mean of the prices is not the answer
print(round(slippage_bps(average, 100.0, "buy"), 2)) # 18.0 a buy that averaged 100.18 against an arrival of 100.00 cost 18 bp
print(round(slippage_bps(average, 100.0, "sell"), 2)) # -18.0 the same prices would have been a gain for a seller# A toy run: the price drifts up while we buy 10 slices; the slippage is what the drift cost us
prices = [100.00, 100.03, 100.05, 100.04, 100.08, 100.10, 100.09, 100.12, 100.15, 100.14] # price at each slice time
pieces = [100] * 10 # TWAP: equal child orders
arrival = prices[0]
fills = list(zip(prices, pieces))
average = sum(p * q for p, q in fills) / sum(pieces)
print(round(average, 4)) # 100.08
print(round((average - arrival) / arrival * 10000, 1)) # 8.0 bp of slippage for buying into a rising market- Les gros ordres font bouger le prix ; un algorithme d'exécution envoie de plus petits ordres enfants dans le temps. Le TWAP envoie des parts égales à intervalles réguliers.
- Tailles :
divmod(total, n)avec les actions en plus dans les premières parts ; instants :start + i * (end - start) / n. - Notez une exécution par son prix moyen pondéré par les quantités face au prix d'arrivée, en points de base ; un nombre positif est un coût.
À suivre : l'exécution VWAP, suivre le volume de la journée plutôt que l'horloge.
Lab : ce qui est noté
- Dimensionner chaque tranche avec
twap_schedule25 pts - Espacer les tranches dans le temps avec
slice_times25 pts - Moyenner vos exécutions avec
average_fill_price25 pts - Chiffrer le coût avec
slippage_bps25 pts