Execution · Intermediate
TWAP, slicing and jitter
A trade’s cost is measured against the price when it was decided; splitting an order keeps that cost down, and an irregular schedule makes it harder for others to trade ahead of it.
Educational material only. Not investment advice.
Contents
Abstract
The cost that matters is implementation shortfall: the gap between the price when the order was decided and the prices actually paid. Impact grows roughly with the square root of order size relative to daily volume, and the Almgren–Chriss trade-off between impact and timing risk sets how an order should be split. Jitter makes a schedule harder to anticipate, at a small and measurable cost. Execution quality becomes visible only across many orders, and at realistic timing risk a comparison of two algorithms needs well over a thousand of them.
Key takeaways
- Measure execution against the arrival price; TWAP and VWAP describe how an order was worked, not what it cost.
- Impact grows roughly with the square root of size relative to volume, so participation matters more than share count.
- Splitting an order exchanges impact for timing risk; with risk ignored and liquidity even through the window, an even split (TWAP) minimises expected cost, and when volume has a strong shape, following it (VWAP) costs less.
- Regular schedules are easy to time; jitter makes child times much less predictable at a small expected cost, though it cannot hide a large order that keeps buying, or selling, all day.
- Execution quality is only visible across many orders, normalised by volatility and participation.
Before you start
- Bid, ask, mid-quote and spread
- Volatility and the square root of time
- The series notation, in the Introduction to the series
A signal decides what to hold; execution decides what it costs to get there. Any order larger than the liquidity resting at the best prices moves the price against the trader if it is sent all at once, so larger orders are split and worked over time. How they are split, and what they are measured against, can decide whether a modest edge survives trading.
Parent orders and implementation shortfall
Call the order the trader wants filled the parent, and the pieces actually sent to the market its children. The cost of a parent order is best measured against the price at the moment the decision was made, the arrival price, usually the mid-quote. For a buy of intended size Q, filled in pieces qi at prices pi, with mend the mid when the order ends:
Perold introduced implementation shortfall as the gap between a paper portfolio, traded instantly at decision prices, and the real one.1 In practice it is split into four parts: the half-spread paid to cross the quote, the market impact of the trader’s own demand, the timing risk of prices moving while the order is worked, and the opportunity cost of any part left unfilled. Splitting an order trades the second against the third: smaller children each pay less impact, but sending them takes time, and the part not yet filled is exposed to the market meanwhile.
Arrival, TWAP and VWAP
Three benchmarks are in common use, and each rewards different behaviour. Choosing one is choosing what the execution is judged on.
| Benchmark | Definition | What it rewards | Weakness |
|---|---|---|---|
| Arrival price (implementation shortfall) | Mid-quote when the parent order arrived | Finishing fast when impact is low; the true economic cost | Noisy: dominated by market moves on any single order |
| TWAP | Time-weighted average price over the execution window | Trading evenly through the window | Can be matched exactly by trading evenly, whatever it costs |
| VWAP | Volume-weighted average price over the window | Trading in proportion to market volume | The trader’s own volume is inside the benchmark; easy to match, easy to game |
VWAP was proposed as a cost benchmark by Berkowitz, Logue and Noser (1988).
VWAP became popular because it is simple to compute and hard to miss by much: an algorithm that follows the market’s volume curve will land close to it.2 That is also its weakness. A large order moves VWAP towards its own fills, so beating VWAP says little about whether the trade was cheap. Arrival-price cost measures what the trade actually cost, and it is the measure this note uses; TWAP and VWAP are best treated as descriptions of how an order was worked.
Market impact and the cost of waiting
The square-root law
How much does demand move the price? Kyle’s model gives a linear answer: price moves in proportion to net order flow, with a slope that rises with the uncertainty about the asset’s value and falls with the volume of uninformed trading that hides informed orders.3 Empirically, the total impact of a large order grows more slowly than its size. Across markets and decades, a good approximation is
where σ is volatility (daily here, unlike the annualised σ of the series notation), V daily volume and Y a constant of order one. Almgren, Thum, Hauptmann and Li estimated impact from large US equity orders and found temporary impact growing with the rate of trading raised to a power of about 0.6, close to the square root;4 Tóth and co-authors found the square root across many markets and argued that it reflects how thin liquidity is close to the current price.5,6 Two consequences follow: the cost of an order depends on its size relative to volume more than on its size in shares, and doubling an order raises its cost per share by about 41%, where a linear model would say 100%.
The cost of waiting
Spreading an order over time lowers impact but leaves the unfilled part exposed to price moves. Bertsimas and Lo showed that, with linear impact and no concern for risk, splitting the order evenly over the available time minimises expected cost: that even split is a TWAP.7 Almgren and Chriss added the variance of cost: a risk-averse trader trades faster at the start, and the set of schedules that minimise variance for each level of expected cost forms an efficient frontier, from TWAP at one end to immediate execution at the other.8 Obizhaeva and Wang showed that when the order book refills gradually after each trade, the optimal schedule combines discrete trades at the start and end with continuous trading in between.9
Schedules
A TWAP schedule divides a parent of size Q into N children of size Q/N, one every W/N minutes across a window W. Three numbers describe it. The participation rate, the child’s share of the market volume trading while it works, drives impact. The window drives timing risk. The child size relative to the quoted size decides whether a child can cross the spread without walking the book.
| Schedule | Child sizes follow | Best when |
|---|---|---|
| TWAP | Equal slices in time | Volume is fairly even, or predictability of volume is poor |
| VWAP | The expected intraday volume curve | Volume has a strong, stable shape (heavy at the open and close) |
| Participation (POV) | A fixed share of volume as it prints | Size is uncertain relative to the day’s volume; the order must never dominate |
| Implementation shortfall | Front-loaded, per Almgren–Chriss | Timing risk is large relative to impact |
Why schedules are randomised
A perfectly regular schedule, the same size at the same interval, is a signal. Anyone watching the tape who sees clips of the same size print every few minutes can infer that a larger order is being worked, estimate its direction and remaining size, and trade ahead of it. Brunnermeier and Pedersen formalised the incentive: when a trader is known to need to buy or sell, others profit by trading in the same direction first and reversing later.10 The defence is to make the schedule harder to read. Jitter randomises each child’s time within its slot and scales its size by a random factor, while still completing the parent within the window.
def schedule(slices: int, window: int, jitter: float, rng: np.random.Generator):
"""Child times (minutes after the window opens) and sizes (fractions of the parent).
jitter = 0 is a plain TWAP: equal sizes at the start of equal intervals, so one slice is an
immediate order. jitter in (0, 1] delays each child by up to jitter x its interval and scales
each size by 1 + jitter * U(-0.5, 0.5), renormalised so the parent is filled exactly.
"""
dt = window / slices
times = (np.arange(slices) + jitter * rng.uniform(0.0, 1.0, slices)) * dt
sizes = 1.0 + jitter * rng.uniform(-0.5, 0.5, slices)
return np.sort(times), sizes / sizes.sum()In the simulator, an observer who predicts each child’s time as the previous child’s time plus the median gap seen so far gets 100% of an unjittered TWAP’s children after the second (20 children over two hours) right to within 30 seconds; at 50% jitter the figure falls to 27%, and at full jitter to 14%. The price is small but not zero: 0.36 bp of expected cost at full jitter in this model, against a timing risk of 46 bp. It arises because a child’s total impact cost grows with its size to the power 1.5, so uneven sizes cost more than even ones, and because delayed children move along the intraday volume curve. The measure is deliberately narrow: it only watches the clock. Jitter does not hide one-sided flow, which a careful observer sees in the tape whatever the timing, and it helps little when children are about a minute apart. Randomising order types and venues, and never trading the same size at the same second of the minute, follow from the same logic.
def detectability(slices: int, window: int, jitter: float, rng: np.random.Generator,
tolerance: float = 0.5) -> float:
"""Share of child orders whose time an observer predicts to within `tolerance` minutes by
adding the median of the gaps seen SO FAR to the previous child's time. 1.0 for an exact
TWAP. The observer only watches the clock; size and direction are other giveaways."""
t, _ = schedule(slices, window, jitter, rng)
gaps = np.diff(t)
if len(gaps) < 2:
return 0.0
guess = np.array([t[i - 1] + np.median(gaps[: i - 1]) for i in range(2, len(t))])
return float(np.mean(np.abs(guess - t[2:]) <= tolerance))An execution laboratory
The laboratory below simulates a buy order under the model in the code listing: a random-walk mid with the chosen daily volatility, a half-spread of 1 bp, temporary impact following the square-root law in each child’s share of the volume while it works (one minute, or its slot if shorter), linear permanent impact (small at this size, under 1 bp), and a U-shaped intraday volume curve. Each child starts its slot, so a single child is an immediate order; jitter delays children within their slots. The impact coefficient η = 0.15 applies to one minute’s participation. With it, a full-day order worked as 390 one-minute children pays 3.55 bp in all, a temporary impact of about 0.15 · σ√(Q/V): roughly a sixth of the square-root law’s constant of order one, so the model sits at the cheap end of published estimates. The permanent-impact coefficient γ is 0.3. The two are close to the values Almgren and co-authors fitted, though their temporary impact grows with the trading rate to the power 0.6.4 They are illustrative: the shapes of the results matter here, and their levels do not. Expected costs and timing risks below are exact under the model (the random-walk mid is integrated out); only the spread of outcomes comes from simulated days.
Each point keeps the current pace, one child every 6.0 minutes, and stretches the window. Longer windows at this pace split the order into more, smaller children, so expected cost falls; they also leave it exposed for longer, so timing risk rises. Both axes are exact expectations under the model, not simulation averages.
Toy model: random-walk mid, half-spread 1 bp, square-root temporary impact on each child’s share of the volume while it works (one minute, or its slot if shorter), small linear permanent impact, U-shaped intraday volume. Children start their slots and jitter delays them. Coefficients are illustrative, not a calibration.
The reference grid computed by the Python model (an order of 1% of daily volume, 1.5% daily volatility) shows the two forces separately. Holding the window fixed, more children cut the cost; the timing risk depends mainly on the window:
| Window | 1 slice | 5 slices | 20 slices | 100 slices | Timing risk (standard deviation) |
|---|---|---|---|---|---|
| 30 min | 35 bp | 17 bp | 9 bp | 8 bp | 23 bp |
| 120 min | 35 bp | 19 bp | 10 bp | 5 bp | 46 bp |
| 390 min | 35 bp | 22 bp | 11 bp | 6 bp | 83 bp |
Expected implementation shortfall, in bp of the arrival mid, exact under the toy model. Timing risk shown for 20 slices; it varies little with the slice count beyond a few children.
The trade-off appears when the pace is held and the window stretched. At one child every six minutes, the Python model gives:
| Window | Children | Expected cost | Timing risk (standard deviation) |
|---|---|---|---|
| 6 min | 1 | 35 bp | 0 bp |
| 30 min | 5 | 17 bp | 20 bp |
| 60 min | 10 | 13 bp | 31 bp |
| 120 min | 20 | 10 bp | 46 bp |
| 240 min | 40 | 9 bp | 67 bp |
| 390 min | 65 | 7 bp | 86 bp |
Same model and assumptions as the grid above; one child every six minutes.
Stretching the window from 6 minutes to the full day cuts the expected cost from 35 bp to 7 bp and raises the timing risk from 0 bp to 86 bp, the risk growing roughly with the square root of the window. In this toy the saving comes from the number of children, not from the time: each child’s impact is gone within its minute, so the same 65 children sent one a minute would cost 6 bp with a timing risk of 35 bp, better on both axes than spreading them over the day. In real markets impact decays over minutes to hours, so trading the same amount faster costs more; that is the trade-off the Almgren–Chriss frontier prices, and choosing a point on it is a statement about how much price risk a strategy is willing to carry to save impact. The toy has no such memory, so this table shows what more children buy, and the frontier itself needs a model in which impact decays.
def volume_profile(minutes: int = MINUTES) -> np.ndarray:
"""A U-shaped intraday volume profile: busier at the open and the close. Sums to one."""
t = np.arange(minutes) + 0.5
f = 1.0 + 1.5 * ((t - minutes / 2) / (minutes / 2)) ** 2
return f / f.sum()
def _volume(prof: np.ndarray, a: float, b: float) -> float:
"""Market volume (fraction of the day) between minutes a and b, pro rata within a minute."""
i, j = int(np.floor(a)), int(np.floor(b))
if i == j:
return prof[min(i, MINUTES - 1)] * (b - a)
total = prof[i] * (i + 1 - a) + prof[i + 1:j].sum()
return total + (prof[j] * (b - j) if j < MINUTES else 0.0)
def expected_cost(size=0.01, sigma=150.0, slices=10, window=120, jitter=0.0, spread=2.0,
eta=0.15, gamma=0.3, child_minutes=1.0, start=0, schedules=400, seed=7) -> dict:
"""Expected implementation shortfall and timing risk, with the price path integrated out.
The mid is a driftless random walk, so its expected contribution to the cost is zero and
the expected shortfall is the expected impact plus half the spread. The timing risk is
exact too: child i fills after m_i one-minute steps, so the price part of the cost has
variance vol_min^2 * sum_ij q_i q_j min(m_i, m_j). Only the schedule is random (jitter),
and it is averaged over `schedules` draws from a fixed seed, so neighbouring settings share
their draws and the numbers move smoothly. With no jitter one schedule is exact.
"""
window = min(window, MINUTES - start)
rng = np.random.default_rng(seed)
prof = volume_profile()
vol_min = sigma / np.sqrt(MINUTES)
work = min(child_minutes, window / slices)
n = 1 if jitter == 0 else schedules
impact, var_price = np.empty(n), np.empty(n)
for k in range(n):
t, q = schedule(slices, window, jitter, rng)
m = np.minimum(start + t.astype(int), MINUTES - 1) - start # price steps before each fill
v = np.array([_volume(prof, *_working(x, work)) for x in start + t])
child = q * size
temp = eta * sigma * np.sqrt(child / v)
perm = gamma * sigma * np.concatenate([[0.0], np.cumsum(child)[:-1]])
impact[k] = (q * (perm + temp + spread / 2)).sum()
later = q[::-1].cumsum()[::-1] - q # weight filled after child i
var_price[k] = vol_min ** 2 * (m * (q * q + 2 * q * later)).sum()
sd = np.sqrt(var_price.mean() + (impact.var() if n > 1 else 0.0))
return {"expected": impact.mean(), "sd": sd}
def simulate(size=0.01, sigma=150.0, slices=10, window=120, jitter=0.0, spread=2.0,
eta=0.15, gamma=0.3, child_minutes=1.0, start=0, paths=2000, seed=7) -> dict:
"""Implementation shortfall (vs the arrival mid) of many paths, and its parts."""
window = min(window, MINUTES - start)
rng = np.random.default_rng(seed)
prof = volume_profile()
vol_min = sigma / np.sqrt(MINUTES)
cost = np.empty(paths)
impact = np.empty(paths)
for k in range(paths):
t, q = schedule(slices, window, jitter, rng)
minute = np.minimum(start + t.astype(int), MINUTES - 1)
steps = rng.normal(0.0, vol_min, MINUTES)
path = np.concatenate([[0.0], np.cumsum(steps)])
mid = path[minute] - path[start] # random walk from arrival
work = min(child_minutes, window / slices)
a = start + t
v = np.array([_volume(prof, *_working(x, work)) for x in a])
child = q * size
temp = eta * sigma * np.sqrt(child / v)
perm = gamma * sigma * np.concatenate([[0.0], np.cumsum(child)[:-1]])
fills = mid + perm + temp + spread / 2
cost[k] = implementation_shortfall(fills, q, 0.0)
impact[k] = (q * (perm + temp + spread / 2)).sum()
return {"mean": cost.mean(), "sd": cost.std(ddof=1), "p05": np.percentile(cost, 5),
"p95": np.percentile(cost, 95), "expected_impact": impact.mean()}Measuring execution quality
The tables give each schedule’s expected cost and its timing risk. A single order delivers one number, its measured shortfall, and that number mixes the two, as one simulated order from the reference case shows.
- Step 1 of 4
The arrival price
The order is the reference case of the tables above: a buy of 1% of the day’s volume, worked as 20 children over the first 120 minutes. The decision fixes the arrival mid, the zero line. On this simulated day the market fell while the order was worked: when the last child traded, the mid stood at −63.4 bp.
- Step 2 of 4
What each child pays
Each child crosses half the spread, 1 bp, and pays a temporary impact for its share of the volume trading while it works: 7.7 bp for the first child, rising to 10.9 bp for the last as the morning’s volume thins. The children before it have also left a small permanent impact. Every fill sits that far above the mid it met.
- Step 3 of 4
The shortfall, taken apart
Averaged over the children, spread and impact came to 10.5 bp: the expected cost the grid gives for 20 children over two hours, and the part the schedule controls. The market’s own move while the order was worked contributed −46.4 bp. The measured shortfall, average fill minus arrival mid, was −35.9 bp: on paper, the order made money.
- Step 4 of 4
One day among many
Repeated over many days, the shortfall scatters around its expected 10.5 bp with a standard deviation of 46.2 bp, and 41% of days come out below zero. Sent as one immediate child, the same order would cost 35.5 bp every day, with no timing risk. A single order’s measured cost says little about its schedule; telling two schedules apart takes many orders.
Synthetic: one simulated day of the note’s execution model with the reference grid’s settings (a buy of 1% of daily volume, 1.5% daily volatility, a half-spread of 1 bp, 20 children over 120 minutes, no jitter). The day shown is the one, of the first two hundred simulated, whose market move is closest to one standard deviation in the buyer’s favour. Without script, or with reduced motion, the figure shows its final state.
Execution quality can only be judged in aggregate. With a timing risk of about 46 bp per order in the example above, telling apart two algorithms whose true costs differ by 5 bp needs roughly 1,300 orders for each algorithm to detect the difference reliably (a two-sided 5% test with 80% power). Removing the market’s move from each order’s cost, or splitting the same parent orders between the two algorithms, so that both face the same market move, cuts that number considerably.
The practical consequences:
- Record everything at the child level: decision time, arrival mid, each child’s send time, fill price, size and venue, and the quote at the time.
- Measure against arrival first, then decompose: spread paid, temporary impact (the drift during the order that reverses afterwards), permanent impact (the part that persists), and timing (the market’s own moves while the order was worked, which a benchmark such as the index or the sector over the same minutes estimates).
- Normalise by volatility and participation before comparing orders: cost divided by σ√(Q/V) is comparable across days and instruments; raw basis points are not.
- Reconcile against the backtest. The backtest’s cost assumption is a hypothesis; live fills test it. QuantConnect and LEAN describes how a backtesting engine models fills; the gap between modelled and live fills is a common source of live underperformance, and Live versus backtest reconciliation measures it fill by fill against the cost model.
def implementation_shortfall(fill_prices, fill_sizes, arrival_mid, side=1, intended=None, end_mid=None):
"""Cost of a parent order against the mid at the moment it arrived, per unit of the size the
trader intended, in the price's units.
side = +1 for a buy, -1 for a sell; positive is a cost. `intended` is the parent's size Q
(default: everything filled). If less than Q was filled, the unfilled part is charged its
opportunity cost, the move from the arrival mid to `end_mid`, the mid when the order ended:
IS = [ sum q_i (p_i - m_arrival) + (Q - sum q_i) (m_end - m_arrival) ] / Q
Divide by the arrival mid (and multiply by 1e4) for basis points when prices are levels
rather than bp offsets.
"""
fill_prices, fill_sizes = np.asarray(fill_prices, float), np.asarray(fill_sizes, float)
filled = fill_sizes.sum()
q = filled if intended is None else float(intended)
paid = (fill_sizes * (fill_prices - arrival_mid)).sum()
missed = q - filled
if missed > 1e-12 * q:
if end_mid is None:
raise ValueError("a partly filled order needs end_mid to price what was not filled")
paid += missed * (end_mid - arrival_mid)
return side * paid / qReferences
- Perold, A. F. (1988). The implementation shortfall: Paper versus reality. Journal of Portfolio Management, 14(3), 4–9. https://doi.org/10.3905/jpm.1988.409150
- Berkowitz, S. A., Logue, D. E., & Noser, E. A. (1988). The total cost of transactions on the NYSE. Journal of Finance, 43(1), 97–112. https://doi.org/10.1111/j.1540-6261.1988.tb02591.x
- Kyle, A. S. (1985). Continuous auctions and insider trading. Econometrica, 53(6), 1315–1335. https://doi.org/10.2307/1913210
- Almgren, R., Thum, C., Hauptmann, E., & Li, H. (2005). Direct estimation of equity market impact. Risk, 18(7), 58–62.
- Tóth, B., Lempérière, Y., Deremble, C., de Lataillade, J., Kockelkoren, J., & Bouchaud, J.-P. (2011). Anomalous price impact and the critical nature of liquidity in financial markets. Physical Review X, 1(2), 021006. https://doi.org/10.1103/physrevx.1.021006
- Bouchaud, J.-P., Bonart, J., Donier, J., & Gould, M. (2018). Trades, quotes and prices: Financial markets under the microscope. Cambridge University Press. https://doi.org/10.1017/9781316659335
- Bertsimas, D., & Lo, A. W. (1998). Optimal control of execution costs. Journal of Financial Markets, 1(1), 1–50. https://doi.org/10.1016/s1386-4181(97)00012-8
- Almgren, R., & Chriss, N. (2000/2001). Optimal execution of portfolio transactions. Journal of Risk, 3(2), 5–39. https://doi.org/10.21314/jor.2001.041
- Obizhaeva, A. A., & Wang, J. (2013). Optimal trading strategy and supply/demand dynamics. Journal of Financial Markets, 16(1), 1–32. https://doi.org/10.1016/j.finmar.2012.09.001
- Brunnermeier, M. K., & Pedersen, L. H. (2005). Predatory trading. Journal of Finance, 60(4), 1825–1863. https://doi.org/10.1111/j.1540-6261.2005.00781.x