Por que não enviar tudo de uma vez
Compre 200.000 ações com uma única ordem a mercado e você vai devorar o livro de ofertas: as primeiras ações executam no melhor ask, as seguintes a um preço pior, e assim por diante, e todos que veem você comprar sobem seus preços. Esse impacto de mercado é um custo real, muitas vezes maior que a corretagem. Um algoritmo de execução corta a ordem em ordens filhas menores enviadas ao longo do tempo para que cada uma seja absorvida com menos impacto. O mais simples é o TWAP, o algoritmo de preço médio ponderado pelo tempo: divide a quantidade em partes iguais e envia uma em cada um dos instantes igualmente espaçados entre um início e um fim.
# 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 roundingO cronograma: quanto e quando
Um cronograma TWAP tem duas partes. Os tamanhos dividem o total em n partes de ações inteiras que diferem no máximo em uma ação e somam exatamente a ordem: divmod(total, n) dá o tamanho base e quantas partes recebem uma ação extra. Os horários são n horas de início igualmente espaçadas entre o início e o fim: start + i * (end - start) / n. Uma ordem de um dia inteiro de 200.000 ações em 40 partes são 5.000 ações a cada dez minutos, enviadas por um agendador como o do tópico anterior. As implementações reais randomizam um pouco tamanhos e horários, pois um padrão perfeitamente regular é fácil de notar e de explorar por outros.
# 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 timeMedindo: preço médio de execução e slippage
Depois que as partes são executadas, o preço médio de execução é a média dos preços ponderada pelas quantidades, sum(price * quantity) / sum(quantity): uma execução grande pesa mais que uma pequena, então uma média simples dos preços estaria errada. Para julgar o resultado compare-o com o preço de chegada, o preço de mercado no momento da decisão. A diferença é o slippage, expresso em pontos-base (1 pb é 0,01 %): numa compra (médio - chegada) / chegada * 10.000, numa venda o sinal oposto, de modo que um número positivo é sempre um custo. É o núcleo do implementation shortfall, a forma padrão de avaliar uma execução.
# 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- Ordens grandes movem o preço; um algoritmo de execução envia ordens filhas menores ao longo do tempo. O TWAP envia partes iguais em horários igualmente espaçados.
- Tamanhos:
divmod(total, n)com as ações extras nas primeiras partes; horários:start + i * (end - start) / n. - Avalie uma execução pelo preço médio ponderado por quantidades contra o preço de chegada, em pontos-base; um número positivo é um custo.
A seguir: execução VWAP, seguir o volume do dia em vez do relógio.
Lab: o que é avaliado
- Dimensionar cada parte com
twap_schedule25 pts - Espaçar as partes no tempo com
slice_times25 pts - Calcular a média das execuções com
average_fill_price25 pts - Precificar o custo com
slippage_bps25 pts