A fill is not a certainty
A naive backtest assumes that an order for any size fills at the price in the data. Real orders face three questions. How deep is the book: a market order takes the best offer first, then the next, so a big order pays a worse average price. Did the market reach my limit: a limit order gets the price you asked for or better, but only if trading reaches it. And how much can I take: a large order can only be a fraction of the bar's volume, so it fills in pieces over time.
def fill_market_order(book, quantity):
remaining = quantity
cost = 0.0
filled = 0
for price, size in book: # cheapest offer first
if remaining <= 0:
break
take = min(size, remaining)
cost += take * price
filled += take
remaining -= take
if filled == 0:
return 0.0, 0
return cost / filled, filled
book = [(50.00, 300), (50.02, 500), (50.05, 1000)]
print(fill_market_order(book, 200)) # (50.0, 200) inside the first level
print(fill_market_order(book, 600)) # (50.01, 600) 300 at 50.00 and 300 at 50.02
print(fill_market_order(book, 5000)) # (50.0333, 1800) the whole book; 3,200 shares stay unfilled# The average price and the slippage against the best offer grow with the size
def average_price(book, quantity):
remaining, cost, filled = quantity, 0.0, 0
for price, size in book:
take = min(size, remaining)
cost += take * price
filled += take
remaining -= take
return cost / filled
book = [(50.00, 300), (50.02, 500), (50.05, 1000)]
for quantity in (100, 300, 800, 1800):
avg = average_price(book, quantity)
print(quantity, round(avg, 4), round((avg - 50.00) / 50.00 * 10000, 2), "bps above the best offer")Limit orders and gaps
With bar data (open, low, high) you cannot see the path inside the bar, only its extremes. A buy limit at L fills if the bar's low reached L or below. The price is L, unless the bar opened below L: then the market gapped through your limit and you fill at the open, which is better. A sell limit is the mirror image. A limit order that is merely touched may not fill in reality, because the queue ahead of you was served first, so this is the optimistic bound.
def limit_fill(side, limit, bar_open, bar_low, bar_high):
if side == "buy" and bar_low <= limit:
return min(bar_open, limit) # a gap below the limit fills at the better open
if side == "sell" and bar_high >= limit:
return max(bar_open, limit)
return None
print(limit_fill("buy", 49.95, 50.10, 49.90, 50.20)) # 49.95 traded down through the limit
print(limit_fill("buy", 49.95, 49.90, 49.80, 50.20)) # 49.9 opened below: filled at the open
print(limit_fill("buy", 49.95, 50.10, 50.00, 50.20)) # None never reached
print(limit_fill("sell", 50.15, 50.00, 49.90, 50.20)) # 50.15Working a large order
A rule used by many execution desks is a participation limit: take no more than, say, 10% of the volume. In a bar with 40,000 shares traded, that is 4,000, rounded down. An order for 5,000 shares then takes two bars, not one. Working through the bars is a loop that carries the remaining quantity: each bar fills min(remaining, int(volume * participation)), and bars after the order is done fill nothing.
def partial_fill(quantity, bar_volume, participation):
return min(quantity, int(bar_volume * participation))
def fill_over_bars(quantity, volumes, participation):
remaining = quantity
fills = []
for volume in volumes:
filled = partial_fill(remaining, volume, participation)
fills.append(filled)
remaining -= filled
return fills
fills = fill_over_bars(5000, [40000, 30000, 80000, 100000], 0.1)
print(fills, sum(fills)) # [4000, 1000, 0, 0] 5000- A market order walks the book level by level; the average price is volume-weighted and gets worse with size, and a thin book fills only part.
- A limit order fills if the bar reaches the limit, at the open if the bar gapped through it; a touch may not fill in reality.
- Participation caps limit each bar to
int(volume * participation); a loop carries the remaining quantity over the bars.
Next up: CAPM and beta, measuring how much of a return comes from the market.
Lab · what's graded
- Walk the book with
fill_market_order25 pts - Decide if the limit is reached with
limit_fill25 pts - Respect the volume with
partial_fill25 pts - Work the order over time with
fill_over_bars25 pts