Por qué no enviarlo todo de golpe
Compra 200.000 acciones con una sola orden de mercado y te comerás el libro de órdenes: las primeras acciones se ejecutan al mejor ask, las siguientes a un precio peor, y así sucesivamente, y todos los que te ven comprar suben sus precios. Este impacto de mercado es un coste real, a menudo mayor que la comisión. Un algoritmo de ejecución corta la orden en órdenes hijas más pequeñas enviadas a lo largo del tiempo para que cada una se absorba con menos impacto. El más simple es el TWAP, el algoritmo de precio medio ponderado por tiempo: divide la cantidad en partes iguales y envía una en cada uno de los instantes igualmente espaciados entre un inicio y un final.
# 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 roundingEl calendario: cuánto y cuándo
Un calendario TWAP tiene dos partes. Los tamaños dividen el total en n partes de acciones enteras que difieren como mucho en una acción y suman exactamente la orden: divmod(total, n) da el tamaño base y cuántas partes reciben una acción extra. Los tiempos son n horas de inicio igualmente espaciadas entre el inicio y el final: start + i * (end - start) / n. Una orden de todo un día de 200.000 acciones en 40 partes son 5.000 acciones cada diez minutos, enviadas por un planificador como el del tema anterior. Las implementaciones reales aleatorizan un poco tamaños y tiempos, pues un patrón perfectamente regular es fácil de detectar y de aprovechar por otros.
# 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 timeMedirlo: precio medio de ejecución y deslizamiento
Una vez ejecutadas las partes, el precio medio de ejecución es la media de los precios ponderada por cantidades, sum(price * quantity) / sum(quantity): una ejecución grande cuenta más que una pequeña, así que un promedio simple de precios sería erróneo. Para juzgar el resultado compáralo con el precio de llegada, el precio de mercado en el momento de decidir. La diferencia es el deslizamiento (slippage), expresado en puntos básicos (1 pb es 0,01 %): en una compra (medio - llegada) / llegada * 10.000, en una venta el signo contrario, de modo que un número positivo es siempre un coste. Es el núcleo del implementation shortfall, la forma estándar de puntuar una ejecución.
# 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- Las órdenes grandes mueven el precio; un algoritmo de ejecución envía órdenes hijas más pequeñas a lo largo del tiempo. TWAP envía partes iguales en tiempos igualmente espaciados.
- Tamaños:
divmod(total, n)con las acciones extra en las primeras partes; tiempos:start + i * (end - start) / n. - Puntúa una ejecución por su precio medio ponderado por cantidades frente al precio de llegada, en puntos básicos; un número positivo es un coste.
A continuación: ejecución VWAP, seguir el volumen del día en vez del reloj.
Lab: qué se califica
- Dimensionar cada parte con
twap_schedule25 pts - Espaciar las partes en el tiempo con
slice_times25 pts - Promediar tus ejecuciones con
average_fill_price25 pts - Valorar el coste con
slippage_bps25 pts