Warum nicht alles auf einmal senden
Kaufen Sie 200.000 Aktien mit einer einzigen Market-Order, und Sie fressen sich durch das Orderbuch: Die ersten Aktien werden zum besten Ask ausgeführt, die nächsten zu einem schlechteren Preis, und so weiter, und jeder, der Sie kaufen sieht, hebt seine Preise an. Dieser Marktimpact ist ein echter Kostenfaktor, oft größer als die Gebühr. Ein Ausführungsalgorithmus zerlegt die Order in kleinere Child-Orders, die über die Zeit gesendet werden, sodass jede mit weniger Impact aufgenommen wird. Der einfachste ist TWAP, der zeitgewichtete Durchschnittspreis-Algorithmus: Man teilt die Menge in gleiche Teile und sendet zu jedem von gleichmäßig verteilten Zeitpunkten zwischen Start und Ende einen davon.
# 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 roundingDer Plan: wie viel und wann
Ein TWAP-Plan hat zwei Teile. Die Größen teilen die Gesamtmenge in n Teile ganzer Aktien, die sich um höchstens eine Aktie unterscheiden und genau die Order ergeben: divmod(total, n) liefert die Basisgröße und wie viele Teile eine Aktie mehr bekommen. Die Zeiten sind n gleichmäßig zwischen Start und Ende verteilte Startzeiten: start + i * (end - start) / n. Eine ganztägige Order über 200.000 Aktien in 40 Teilen sind 5.000 Aktien alle zehn Minuten, gesendet von einem Scheduler wie dem aus dem vorigen Thema. Echte Implementierungen randomisieren Größen und Zeiten ein wenig, da ein perfekt regelmäßiges Muster für andere leicht zu erkennen und auszunutzen ist.
# 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 timeMessen: durchschnittlicher Ausführungspreis und Slippage
Sind die Teile ausgeführt, ist der durchschnittliche Ausführungspreis der mengengewichtete Mittelwert der Fill-Preise, sum(price * quantity) / sum(quantity): Ein großer Fill zählt mehr als ein kleiner, ein einfacher Mittelwert der Preise wäre also falsch. Um das Ergebnis zu beurteilen, vergleichen Sie es mit dem Ankunftspreis, dem Marktpreis im Moment der Entscheidung. Die Differenz ist der Slippage, angegeben in Basispunkten (1 bp sind 0,01 %): bei einem Kauf (Durchschnitt - Ankunft) / Ankunft * 10.000, bei einem Verkauf das umgekehrte Vorzeichen, sodass eine positive Zahl stets Kosten bedeutet. Das ist der Kern des Implementation Shortfall, der Standardmethode, eine Ausführung zu bewerten.
# 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- Große Orders bewegen den Preis; ein Ausführungsalgorithmus sendet kleinere Child-Orders über die Zeit. TWAP sendet gleiche Teile zu gleichmäßig verteilten Zeiten.
- Größen:
divmod(total, n)mit den Extra-Aktien in den ersten Teilen; Zeiten:start + i * (end - start) / n. - Bewerten Sie eine Ausführung nach ihrem mengengewichteten Durchschnittspreis gegenüber dem Ankunftspreis in Basispunkten; eine positive Zahl sind Kosten.
Als Nächstes: VWAP-Ausführung, dem Tagesvolumen statt der Uhr folgen.
Lab: Was bewertet wird
- Jedes Teilstück mit
twap_schedulebemessen25 Pkt. - Die Teile mit
slice_timeszeitlich verteilen25 Pkt. - Mit
average_fill_priceIhre Fills mitteln25 Pkt. - Die Kosten mit
slippage_bpsbeziffern25 Pkt.