Why not just send it all
Buy 200,000 shares in one market order and you will eat through the order book: the first shares fill at the best ask, the next at a worse price, and so on, and everyone who sees you buying raises their prices. This market impact is a real cost, often larger than the commission. An execution algorithm cuts the order into smaller child orders sent over time so each is absorbed with less impact. The simplest is TWAP, the time-weighted average price algorithm: divide the quantity into equal pieces and send one at each of evenly spaced times between a start and an end.
# 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 roundingThe schedule: how much and when
A TWAP schedule has two parts. The sizes split the total into n whole-share pieces that differ by at most one share and add up exactly to the order: divmod(total, n) gives the base size and how many pieces get one extra share. The times are n start times evenly spaced between the start and the end: start + i * (end - start) / n. A day-long order of 200,000 shares in 40 slices is 5,000 shares every ten minutes, sent by a scheduler like the one in the previous topic. Real implementations randomise the sizes and times a little, since a perfectly regular pattern is easy for others to spot and trade against.
# 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 timeMeasuring it: average fill price and slippage
After the slices have filled, the average fill price is the quantity-weighted mean of the fill prices, sum(price * quantity) / sum(quantity): a big fill counts for more than a small one, so a plain average of prices would be wrong. To judge the result compare it with the arrival price, the market price at the moment the decision was made. The gap is the slippage, quoted in basis points (1 bp is 0.01 %): for a buy (average - arrival) / arrival * 10,000, for a sell the opposite sign, so a positive number is always a cost. This is the core of implementation shortfall, the standard way to score an execution.
# 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- Large orders move the price; an execution algorithm sends smaller child orders over time. TWAP sends equal pieces at evenly spaced times.
- Sizes:
divmod(total, n)with the extra shares in the first pieces; times:start + i * (end - start) / n. - Score an execution by its quantity-weighted average fill price against the arrival price, in basis points; a positive number is a cost.
Next up: VWAP execution, following the day's volume instead of the clock.
Lab · what's graded
- Size each piece with
twap_schedule25 pts - Space the slices in time with
slice_times25 pts - Average your fills with
average_fill_price25 pts - Price the cost with
slippage_bps25 pts