为什么不一次全发出去
用一张市价单买入 20 万股,你会一路吃穿订单簿:最初的股票按最优卖价成交,后面的以更差的价格成交,依此类推,所有看到你在买的人还会抬高报价。这种市场冲击是真实的成本,往往比佣金还大。执行算法把订单切成更小的子订单,随时间发出,让每一笔以更小的冲击被消化。最简单的是 TWAP(时间加权平均价格)算法:把数量分成相等的份,在起止时间之间均匀分布的各个时刻各发一份。
# 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 rounding计划:多少量、何时
TWAP 计划有两部分。数量把总数分成 n 份整数股,各份最多相差一股,且恰好加起来等于订单量:divmod(total, n) 给出基础数量以及有多少份多得一股。时间是在起止之间均匀分布的 n 个开始时刻:start + i * (end - start) / n。一个全天 20 万股、分 40 份的订单,就是每十分钟 5,000 股,由类似上一节的调度器发送。真实的实现会把数量和时间稍作随机化,因为完全规律的模式容易被别人发现并加以利用。
# 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 time衡量:平均成交价与滑点
各份成交之后,平均成交价是按数量加权的成交价平均值 sum(price * quantity) / sum(quantity):大笔成交比小笔更重要,所以价格的简单平均是错的。要评判结果,把它与到达价格比较,也就是做出决定那一刻的市场价格。差额就是滑点,以基点表示(1 bp 等于 0.01%):买入时为 (平均价 - 到达价) / 到达价 * 10,000,卖出时符号相反,因此正数永远代表成本。这就是执行落差(implementation shortfall)的核心,也是给一次执行打分的标准方法。
# 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可预测就可被利用TWAP 无视市场:在安静的时刻和繁忙的时刻交易相同的数量,它规律的足迹可能被识别并被抢先交易。当你对成交量没有看法、又想要简单时,它是个合理的基准。改进是下一节的内容:跟随一天的成交量曲线(VWAP)、给你在每个区间成交量中所占的份额设上限、让数量和时间随机化,以及在价格离你而去时停下或放慢。在信任一个算法之前,务必在相同的日子里把它的滑点与一个简单基准做比较。
- 大订单会推动价格;执行算法把更小的子订单随时间发出。TWAP 在均匀分布的时刻发出相等的份额。
- 数量:用
divmod(total, n),多出的股数放在前面的份里;时间:start + i * (end - start) / n。 - 用按数量加权的平均成交价相对到达价格的基点数来给执行打分;正数表示成本。
下一节:VWAP 执行,跟随一天的成交量而不是时钟。
实验:评分内容
- 用
twap_schedule确定每一份的数量25 分 - 用
slice_times在时间上安排各份25 分 - 用
average_fill_price平均你的成交25 分 - 用
slippage_bps衡量成本25 分