Trade when the market trades
The volume-weighted average price is the average price of everything that traded, each trade weighted by its size: sum(price * volume) / sum(volume). It is the most common benchmark in institutional trading because it is what an average participant paid that day. A VWAP algorithm tries to match or beat it by trading *in proportion to the market's volume*: a lot when the market is busy (the open, the close), little when it is quiet (lunchtime). Where TWAP follows the clock, VWAP follows the crowd, so each of your orders is a similar fraction of what everyone else is doing and moves the price less.
# The benchmark: each price counts as much as the volume that traded at it
def vwap(prices, volumes):
return sum(p * v for p, v in zip(prices, volumes)) / sum(volumes)
prices = [100.0, 101.0, 102.0]
volumes = [100, 300, 600]
print(round(vwap(prices, volumes), 4)) # 101.5 most volume traded near 102, so the VWAP leans that way
print(sum(prices) / len(prices)) # 101.0 the plain average ignores how much traded at each priceA volume profile and a schedule that follows it
You cannot know today's volume in advance, so a VWAP algorithm uses a volume profile estimated from history: the average share of the day's volume that traded in each interval over, say, the last 20 sessions. Intraday volume is typically U-shaped, heavy at the open and the close. The schedule then gives each interval total * share shares. These are rarely whole numbers, and simply rounding each can lose or add shares. The largest remainder method fixes that: give every interval the floor, then hand the leftover shares, one each, to the intervals with the largest fractional parts, so the schedule always adds up to the order.
# From volumes to a profile, then to whole shares by the largest remainder method
import math
def volume_profile(volumes):
total = sum(volumes)
return [v / total for v in volumes]
def vwap_schedule(total, profile):
exact = [total * share for share in profile]
shares = [math.floor(value + 1e-9) for value in exact]
leftover = total - sum(shares)
for index in sorted(range(len(profile)), key=lambda i: (-(exact[i] - shares[i]), i))[:leftover]:
shares[index] += 1
return shares
profile = volume_profile([300, 100, 100, 200, 500, 800]) # a U-shape: busy at the open and the close
print([round(p, 3) for p in profile]) # [0.15, 0.05, 0.05, 0.1, 0.25, 0.4]
print(vwap_schedule(1003, profile)) # [151, 50, 50, 100, 251, 401] adds up to 1003
print(sum(vwap_schedule(1003, profile))) # 1003Participation limits and tracking the benchmark
A schedule is a plan; the market may be quieter than the profile predicted. A participation cap limits your trading in each interval to a fraction of the market's actual volume in it, say 10 %, so you never become most of the flow. Shares you could not trade are carried forward to later intervals, which may leave some unexecuted at the end; then you decide whether to take more risk, extend the window or stop. After the day, compare your average price with the market VWAP: the difference, in basis points, is the tracking error of the algorithm. Beating VWAP by trading late in a rising market is not skill; judge an algorithm by its slippage over many days.
# A participation cap: never more than a fraction of the market's volume, carrying the rest forward
import math
def cap_participation(schedule, market_volumes, rate):
trades, carry = [], 0
for scheduled, volume in zip(schedule, market_volumes):
wanted = scheduled + carry
done = min(wanted, math.floor(rate * volume + 1e-9))
trades.append(done)
carry = wanted - done
return trades, carry
print(cap_participation([100, 100, 100], [500, 1000, 1000], 0.1)) # ([50, 100, 100], 50)
print(cap_participation([100, 0, 0, 0], [500, 1000, 400, 800], 0.1)) # ([50, 50, 0, 0], 0) the carry is traded when the market allows it# Following the clock (TWAP) and following the volume (VWAP) give different average prices on the same day
prices = [100.0, 100.2, 100.1, 100.4, 100.3, 100.5]
volumes = [300, 100, 100, 200, 500, 800] # busy at the open and the close
market_vwap = sum(p * v for p, v in zip(prices, volumes)) / sum(volumes)
twap_average = sum(prices) / len(prices) # equal pieces at every time
print(round(market_vwap, 4), round(twap_average, 4)) # 100.33 100.25
print(round((twap_average - market_vwap) / market_vwap * 10000, 1)) # -8.0 a TWAP buy beat the VWAP here, by trading light when volume was heavy- VWAP is
sum(price * volume) / sum(volume), the common benchmark; a VWAP algorithm trades in proportion to the market's volume. - Estimate a volume profile from history and split the order along it with the largest remainder method so whole shares add up exactly.
- Cap your participation in each interval and carry the rest forward; judge the algorithm by its tracking error and slippage over many days.
Next up: market making, quoting both sides and managing the inventory that results.
Lab · what's graded
- Compute the benchmark with
vwap25 pts - Shape the day with
volume_profile25 pts - Split the order with
vwap_schedule25 pts - Respect the market with
cap_participation25 pts