你拿不到的价格
回测按数据里的价格成交,而真实订单在别处成交。滑点就是这个差别:你买得比所见价格略高,卖得略低。它有两个来源。一是价差:市价单按卖一价买、按买一价卖,所以相对中间价,每个方向要付一半的价差。二是市场冲击:大单会吃掉最好的报价,并把价格推向对自己不利的方向。
def fill_price(mid, side, slippage_bps):
if side == "buy":
return mid * (1 + slippage_bps / 10000)
if side == "sell":
return mid * (1 - slippage_bps / 10000)
raise ValueError("side must be 'buy' or 'sell'")
print(round(fill_price(50.0, "buy", 4), 4)) # 50.02 pays more
print(round(fill_price(50.0, "sell", 4), 4)) # 49.98 receives less# Crossing the spread: a market buy pays the ask, a market sell receives the bid
bid, ask = 49.98, 50.02
mid = (bid + ask) / 2
half_spread_bps = (ask - bid) / 2 / mid * 10000
print(mid, round(half_spread_bps, 2)) # 50.0 4.0 each way, relative to the mid price
print(round((ask - mid) / mid * 10000, 2), round((mid - bid) / mid * 10000, 2)) # 4.0 4.0对照基准来度量
要度量滑点,需要一个参照:到达价,即下单决定时的中间价。差额带符号,使正数总是表示你吃了亏:买入为 fill - arrival,卖出为 arrival - fill,以到达价的基点表示。对买方不利的同一笔成交对卖方有利,所以符号取决于方向。大单会分批成交,因此应报告按成交量加权的平均成交价。
import numpy as np
def slippage_bps(fill, arrival, side):
if side == "buy":
return (fill - arrival) / arrival * 10000
if side == "sell":
return (arrival - fill) / arrival * 10000
raise ValueError("side must be 'buy' or 'sell'")
def average_fill(prices, quantities):
prices, quantities = np.asarray(prices, dtype=float), np.asarray(quantities, dtype=float)
return float(np.sum(prices * quantities) / np.sum(quantities))
# Buying 600 shares in three pieces; the price at arrival was 50.00
avg = average_fill([50.0, 50.1, 50.3], [100, 200, 300])
print(round(avg, 4)) # 50.1833
print(round(slippage_bps(avg, 50.0, "buy"), 2)) # 36.67 bps: it cost us
print(round(slippage_bps(avg, 50.0, "sell"), 2)) # -36.67: the same fills would have helped a seller冲击随规模增长
价差是固定费用,而冲击取决于你交易多少。一条经过充分检验的经验法则是平方根定律:预期冲击占价格的比例约为 k * volatility * sqrt(quantity / ADV),其中 ADV 是日均成交量,k 是接近 1 的常数。订单放大到 4 倍,冲击只增加到 2 倍,所以把大单拆开分时间执行有帮助,但帮助没有想象的那么大。
import math
def sqrt_impact_bps(volatility, quantity, adv, k=1.0):
return k * volatility * math.sqrt(quantity / adv) * 10000
# A stock with 2% daily volatility and 1,000,000 shares of average daily volume
for shares in (10_000, 50_000, 200_000, 800_000):
print(shares, round(sqrt_impact_bps(0.02, shares, 1_000_000), 1), "bps")
# 10,000: 20.0, 50,000: 44.7, 200,000: 89.4, 800,000: 178.9每个回测都要加上滑点哪怕是粗糙的模型也比没有强:每笔交易收取报价价差的一半,再加一个随订单规模增长的冲击项,然后看策略能否存活。如果只有在零滑点时才有效,那它就是无效的。诚实的做法是把假设的滑点与实盘成交对比,只有数据允许时才下调假设。
- 买入成交在中间价之上,卖出在其下:
mid * (1 +/- bps / 10000);仅价差每个方向就要花其宽度的一半。 - 对照到达价度量,符号使正数表示成本;部分成交用成交量加权平均合并。
- 冲击遵循平方根定律
k * volatility * sqrt(quantity / ADV):规模 4 倍,冲击 2 倍。
下一节:逼真的订单成交,模拟订单如何真正对照订单簿和K线数据成交。
实验:评分内容
- 用
fill_price计算成交价25 分 - 用
slippage_bps衡量差额25 分 - 用
average_fill合并部分成交25 分 - 用
sqrt_impact_bps预测冲击25 分