なぜ一度に全部出さないのか
20万株を1回の成行注文で買うと、板を食い進むことになります。最初の株は最良のアスクで約定し、次はより悪い価格で、と続き、あなたが買っていると見た参加者は価格を引き上げます。このマーケットインパクトは実際のコストで、手数料より大きいことも珍しくありません。執行アルゴリズムは注文を、時間をかけて送る小さな子注文に分割し、それぞれがより小さなインパクトで吸収されるようにします。最も単純なのが TWAP(時間加重平均価格)アルゴリズムで、数量を等しい部分に分け、開始から終了までの等間隔の時刻ごとに1つずつ送ります。
# 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 のスケジュールは2つの部分からなります。数量は、合計を、差が高々1株の整数株の n 個に分け、合計がちょうど注文数量になるようにします。divmod(total, n) が基本のサイズと、1株多く受け取る個数を与えます。時刻は、開始と終了の間に等間隔に並ぶ n 個の開始時刻 start + i * (end - start) / n です。1日がかりで20万株を40スライスにすると、10分ごとに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) です。大きな約定は小さな約定より重いので、価格の単純平均は誤りです。結果を評価するには、意思決定の時点の市場価格である到着価格と比べます。その差がスリッページで、ベーシスポイント(1bp は 0.01%)で表します。買いなら (平均 - 到着) / 到着 * 10,000、売りは符号が逆なので、正の値は常にコストです。これがインプリメンテーション・ショートフォール、つまり執行を採点する標準的な方法の核心です。
# 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 は等しい量を等間隔の時刻に送る
- 数量は
divmod(total, n)で余りの株を先頭のスライスに、時刻はstart + i * (end - start) / n - 執行は、到着価格に対する数量加重平均約定価格をベーシスポイントで評価する。正の値はコスト
次は VWAP 執行です。時計ではなく、その日の出来高に従います。
ラボ:採点される内容
twap_scheduleで各スライスの数量を決める25点slice_timesでスライスを時間に配置する25点average_fill_priceで約定を平均する25点slippage_bpsでコストを見積もる25点