Measuring the market impact of your own orders
Every backtest has a silent partner who fills any order at the last price and never charges for size. Real markets let that partner go a long time ago.
When you buy, you take the cheapest shares on offer first and then the next cheapest, and the price you leave behind is higher than the one you found. That is market impact, and it's the cost that turns a strategy that worked on paper into a strategy that only ever worked on paper.
Measuring it on real data is awkward, because you can't see the market where you didn't trade. Practitioners compare against the price when the order arrived, or the day's average, and hope. A simulated market has no such problem. It can run the same day twice.
Cost against the market you didn't trade in
tf.tca.analyse runs a seed with your orders and again without them, and prices each fill against the untraded run, with the same companies, the same news and the same everything, minus you. The script below measures two things: what one order costs as it grows, and how much spreading a large order out saves.
article-market-impact.py
"""What your own orders cost: market impact, measured against the market
where you never traded.
tca.analyse runs a seed twice, with your orders and without them, and prices
every fill against the untraded run. Part one sweeps the size of a single
market order. Part two buys the same quantity three ways: all at once, spread
over one day, and spread over three days.
"""
import statistics
import tradefloor as tf
SEEDS = [1, 2, 3, 4, 5, 6]
market = tf.Universe.random(10, seed=5)
class Buy:
"""Buy `share` of each name's average daily volume, split into equal
slices at the given (day, step) points."""
def __init__(self, share, when):
self.share, self.when = share, when
def act(self, obs):
if (obs.day, obs.step_of_day) not in self.when:
return None
part = self.share / len(self.when)
return {t: round(part * obs.avg_volume(t)) for t in obs.tickers}
def cost(share, when, days):
costs = []
for seed in SEEDS:
ex = tf.tca.analyse(Buy(share, when), seed=seed, universe=market, days=days,
cash=1e12, max_leverage=None)
costs.append(ex.shortfall_bps())
return round(statistics.median(costs), 1)
# Part one: one order, bigger and bigger.
SIZES = {"1%": 0.01, "5%": 0.05, "10%": 0.10, "25%": 0.25, "50%": 0.50}
by_size = {label: cost(share, {(1, 0)}, days=2) for label, share in SIZES.items()}
# Part two: 30% of a day's volume, three ways.
SHARE = 0.30
by_schedule = {
"at_once": cost(SHARE, {(1, 0)}, days=2),
"over_a_day": cost(SHARE, {(1, s) for s in range(6)}, days=2),
"over_three_days": cost(SHARE, {(d, s) for d in (1, 2, 3) for s in range(6)}, days=4),
}
ten_times = round(by_size["50%"] / by_size["5%"], 1)
print("cost of one market order, median over", len(SEEDS), "seeds")
for label, bps in by_size.items():
print(f" {label:>4} of a day's volume {bps:6.1f} bp")
print(f"ten times the size costs {ten_times} times as much per share")
print(f"buying {SHARE:.0%} of a day's volume:")
for label, bps in by_schedule.items():
print(f" {label.replace('_', ' '):16} {bps:6.1f} bp")Output on tradefloor 0.10.2
cost of one market order, median over 6 seeds
1% of a day's volume 3.0 bp
5% of a day's volume 9.3 bp
10% of a day's volume 15.9 bp
25% of a day's volume 26.0 bp
50% of a day's volume 36.2 bp
ten times the size costs 3.9 times as much per share
buying 30% of a day's volume:
at once 28.5 bp
over a day 18.2 bp
over three days 14.9 bpCost of one market order, by size
Basis points, hundredths of a percent, against the same market without the order
Bigger costs more per share
An order for 1% of a day's volume cost 3.0 basis points. One for half a day's volume cost 36.2. Each share of the big order costs more than each share of the small one, because the big order eats further into the book.
The growth slows as the order grows, though. Ten times the size cost 3.9 times as much per share. A straight line would say ten, and the square-root law that real large orders roughly follow would say a little over three. Your total bill is size times that cost per share. It grows a lot faster than you'd like.
The price of impatience
Buying 30% of a day's volume, three ways
Basis points against the same market without the order
Buying 30% of a day's volume in one go cost 28.5 basis points. Spreading the same order over the day cut that to 18.2, and over three days to 14.9. Between slices the book refills, and each slice meets fresher depth. The trade-off is time. A three-day order spends three days exposed to the market moving without you, which is a different way of paying.
One honest caveat. tradefloor's cost for a sliced order sits at the low end of what published studies of real metaorders find. Real markets will usually reward patience a little less than this chart does. The single-order curve is the better calibrated of the two.
Running it yourself
Install the library with pip install tradefloor and run the script. It takes about fifteen seconds. Change SIZES or the schedules to match how you actually trade, and point it at your own strategy by replacing Buy. Execution cost has the details of tca.analyse, and How prices are made explains the order book behind the numbers.