インプリメンテーション・ショートフォールとDIY TCA:執行が実際にいくらかかっているかを測る
機関投資家のトレーディングデスクは、どこでも取引コスト分析(TCA)のパイプラインを持っている。暗号資産ボットを運用している人でそれを持っている人はほとんどいない。典型的なセットアップは、約定を記録し、手数料を合計し、バックテストのPnLとライブのPnLの差を「スリッページ」と呼ぶ — レイテンシ、スプレッド、インパクト、逆選択、そして価格に置いていかれた未約定注文のすべてを吸収する、説明のつかない単一の残差だ。単一の数字として測るコストは直せない。それを適切に測る仕組みは1988年から存在しており、専有的なものでもなく、自分の約定ログに対しておおよそ200行のPythonにすぎない。本稿ではそれを組み立てる。
見返りは、より見栄えのよいダッシュボードではない。バックテストにはコストモデル — スリッページ定数、約定確率、インパクト係数 — が含まれており、その中のあらゆるパラメータは現状では推測だ。ライブ約定に対するTCAは、それらのパラメータを較正できる唯一のグラウンドトゥルースである。このループのシミュレーション側は約定シミュレーション:終値ファンタジーからキュー考慮の現実へ至るはしごで組み立てた。本稿では測定側を組み立てる。
ペーパー対現実:Peroldが実際に測ったもの
この基礎的な仕掛けはAndré Perold(1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9)によるものだ。2つのポートフォリオを並行して走らせる。ペーパーポートフォリオは、あらゆる意思決定を即座に、無制限のサイズで、ゼロコストで、意思決定の瞬間に成立していた価格で執行する。リアルポートフォリオはボットが実際に行ったもの:部分約定、追いかけたクォート、キャンセルした残り、手数料。インプリメンテーション・ショートフォールは、両者のリターンの差である。
この定義が重要なのは、それが隠すことを拒むものにある。手数料明細は手数料を示す。約定対指値価格のレポートは何一つ示さない(構造上、指値より悪い価格で約定することはない)。ペーパーポートフォリオはすべての代金を請求する:意思決定と到着の間のドリフト、越えたスプレッド、引き起こしたインパクト、そして — 決定的に — 価格が逃げていく間に一度も約定しなかった注文。Wagner and Edwards(1993, "Best Execution," Financial Analysts Journal 49(1), 65–71)は、目に見える手数料を氷山の一角と呼んだ。回転のあるものについては、水面下の部分が支配的である。
記号を固定しよう。親注文:サイド (買い/売り)、サイズ 。意思決定価格 は、シグナルが発火したときに戦略が見ていたミッドである。約定は として到着し、総約定数量は 。ホライズン (親が完了、キャンセル、またはタイムアウト)において、ミッドは である。明示的手数料は 。通貨建てのインプリメンテーション・ショートフォールは:
ペーパー想定元本 で割ってベーシスポイントに正規化する。正の値は支払ったことを意味する。第1項はペーパーポートフォリオに対して約定がいくらかかったか。第2項はPeroldの機会コスト — 未約定の残りを、それが逃げていった価格で評価したもの。第3項は取引所明細が唯一認める部分である。
慣習について一言:業界では「到着価格(arrival price)」と言い、大半の株式TCAでは到着とは注文が市場に到達したときのミッドを指す。ボットの場合、意思決定時刻と到着時刻は、自分自身の内部レイテンシにレートリミットのキューイングを加えた分だけ異なる — これは現実の、測定可能なコストだ。そこで我々は両方のタイムスタンプと両方の価格を保持し、分解によってそれらを分離させる。
分解:遅延、インパクト、タイミング、機会、手数料
単一のIS数値は、執行が高くつくことを教えてくれる。それがなぜかは教えてくれず、異なる成分に対する対処は完全に異なる — 遅延コストとインパクトコストは同じ変更では解決できない。Robert Kissellの拡張インプリメンテーション・ショートフォール(Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16;書籍の長さで展開されたのは The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013)は、合計を成分に分解し、それぞれを注文ライフサイクルの異なる段階に帰属させる。実務家版は、到着ミッド (最初の取引所確認時のミッド)を用いて:
この恒等式は、Peroldの定義へ正確にたたみ込まれる — 項を展開すると が相殺される。各成分には明確な担い手がいる:
遅延コスト :シグナルの発火から、最初の子注文が取引所上でライブになるまでの価格ドリフト。これは自分のインフラである — シリアライゼーション、ネットワーク、レートリミットのキュー、リスクチェック。シグナルに本物の短期ホライズンのアルファがあるなら、遅延コストはそれが最初に漏れる場所だ。一貫して正の平均は、到着する前に市場が自分の有利な方向へ動いていることを示す — 減衰していくモメンタムアルファか、同じシグナルをより速く取引している誰かの存在である。
トレーディングコスト :到着に対して約定がいくら支払ったか — 越えたスプレッドに、マーケットインパクト、スケジュール内ドリフトを加えたもの。これは執行アルゴリズムの通信簿であり、執行研究が実際にモデル化する量である。Almgren, Thum, Hauptmann and Li(2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62)は、Citigroupのデスクから得たおよそ700,000件の米国株注文(2001年12月〜2003年6月)についてこれを測定し、トレーディングコストが日次ボラティリティと参加率とともにスケールすることを見出した — 一時的インパクトは、指数がおよそ3/5のべき乗則にしたがい、恒久的インパクトはサイズにほぼ線形である。較正の際には彼らの関数形を再利用する。
タイミングリスク:平均の分解における項ではなく、その周りの分散である。親注文を時間にわたって広げると期待インパクトは減るが、ボラティリティにさらされる。残ポジション のスケジュールについては、コストの標準偏差は としてスケールする。これはまさにAlmgren–Chriss枠組みが最適化するトレードオフである。TCAレポートでは、親注文間のISのばらつきとして現れる — すべての平均の隣に標準偏差を報告すること。さもなければ平均がすべての注目を集め、テールがあなたの資金をすべて集めることになる。
機会コスト :未約定数量を終端価格で評価したもの。パッシブ戦略では、これは日常的に最大かつ最も精査されない成分であり、枠組み全体を誠実にする項である — これについては罠のセクションでさらに述べる。なぜならこれを省くことは、人々がTCAで自分自身を欺く最も一般的な方法だからだ。
手数料 :明示的な部分。暗号資産では、割引後の手数料を記録し(VIPティア、約定時価格で換算されたトークンリベート)、メイカーリベートは符号付きで保持する — 負の手数料はデータであってノイズではない。
具体例
BTC を買う。シグナルは で発火;ペーパー想定元本は$600,000。最初の子注文は のミッドで確認される。次の2分間で、8 BTCがVWAP 60,072で約定;価格は逃げていく傾向にあり、アルゴは自らの指値を尊重し、残り2 BTCはミッドが のところでキャンセルされる。約定した想定元本に対するブレンド手数料は2.5 bps。
| 成分 | 式 | USD | ペーパーのbps |
|---|---|---|---|
| 遅延 | 120 | 2.0 | |
| トレーディングコスト | 480 | 8.0 | |
| 機会 | 456 | 7.6 | |
| 手数料 | 120 | 2.0 | |
| 合計 IS | 1,176 | 19.6 |
取引所明細は$120を示す。意思決定を実装する実際のコストは$1,176 — 10倍であり、最大の2成分は手数料ベースの会計には見えない。そのおよそ40%は、一度も取引されなかった数量から来た。約定だけを分析するTCAレポートは、この親注文を8 bpsと採点し、問題ないと言うだろう。

マークアウト:逆選択を価格づける
インプリメンテーション・ショートフォールは親注文を採点する。それは個々の約定の質については何も言わない — 具体的には、自分が知らない何かを知っているカウンターパーティと体系的に取引しているかどうかについてだ。それを測るのがマークアウトである:各約定を、それが起きた後の固定ホライズンでのミッドで評価する。
ここで は時刻 での約定価格、 は時刻 でのミッドである。これはホライズン における約定の単位あたり時価評価PnLであり、 において明快な解剖学を持つ:メイカー約定は ハーフスプレッドから始まる(ビッドで買い、ミッドは自分より上にある);テイカー約定は ハーフスプレッドから始まる。 が増えるにつれて何が起きるかが、取引の情報内容である:
- s はスナイピングと陳腐化クォートの拾い上げを分離する。メイカー約定が約定の1秒後にすでに含み損なら、より速い参加者があなたのクォートがミスプライスになった瞬間にそれを叩いている — あなたのクォート更新ループが彼らのトリガーループより遅いのだ。このマークアウトはレイテンシの診断であって、戦略の診断ではない。
- s は古典的な逆選択を測る:約定の後に、自分の価格を突き抜けて動きが継続すること。マーケットメイカーにとって、これはスプレッド獲得が上回らなければならないコストである;株式マイクロストラクチャーは関連量をリアライズド・スプレッドと呼び、5分ホライズンでのSEC規則605報告として制度化されている。暗号資産はより速く動く;10〜60sが等価な帯域である。
- s は、マイクロストラクチャーのホライズンを超えて約定が自分に不利なモメンタムを帯びているかどうかを教える — テイカー戦略については、60sでのシグナルのアルファが、エントリーのために支払ったスプレッド+インパクトを上回るかどうか。 bpsから始まってゼロを一度も越えないテイカーのマークアウト曲線は、アルファが賄えないエントリーに代金を払っている戦略である。
メイカー/テイカーの非対称性は、2本の曲線の中にパッシブ取引の経済学のすべてがある。 bps(ハーフスプレッド)から始まり60sまでに bpsへ減衰するメイカー曲線はこう言う:あなたはスプレッドを獲得し、それ以上を返しており、どのリベートティアもそれを直さない。同じ曲線が bpsで落ち着けば、クォーティングエンジンがその働きに見合っている。これはまさに、素朴なタッチ約定バックテストが仮定によって消し去るものでもある — 価格が指値に触れるたびに約定させるシミュレータは、約定されることが誤っていることと相関するという事実を無視する。だからこそ、約定シミュレーションのはしごのキュー考慮の段は、測定されたマークアウトを仮定ではなく入力として必要とするのだ。

計算は、自分のミッドストリームに対する merge_asof である:
import pandas as pd
def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
horizons=("1s", "10s", "60s")) -> pd.DataFrame:
"""fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
Both UTC-indexed and sorted. Mid stream must be from YOUR captured
feed, not candles reconstructed later."""
fills = fills.sort_values("ts").reset_index(drop=True)
mids = mids.sort_values("ts")
out = fills.copy()
for h in horizons:
probe = fills[["ts"]].copy()
probe["ts"] = probe["ts"] + pd.Timedelta(h)
m = pd.merge_asof(probe, mids, on="ts", direction="backward")
out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
/ fills["price"] * 1e4)
return out
def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
"""Qty-weighted markouts by liquidity flag. Weighting matters:
a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
cols = [c for c in mo.columns if c.startswith("mo_")]
def agg(g):
w = g["qty"] / g["qty"].sum()
return pd.Series({c: (g[c] * w).sum() for c in cols}
| {"n": len(g), "qty": g["qty"].sum()})
return mo.groupby("liquidity").apply(agg)
レポートをさらに、銘柄、時刻(時)、ミッドからのクォート距離で切り分けよう。メイカー戦略にとって最も実行に移しやすい切り口は、約定時のキュー位置別のマークアウトである:フレッシュキューの先頭での約定は、レベルが自分を突き抜けて掃かれた約定とはまったく異なる価格づけになる。
パイプライン:何をログするか
TCAは数式のレイヤーではなく、ログのレイヤーで死ぬ。上の数式は、大半のボットが捨ててしまう数字を必要とし、そのどれもが後から取引所の履歴では再構成できない。譲れないもの:
- 自分自身のフィードから、シグナル時刻での意思決定ミッド。 取引所のロウソク足の終値ではなく、後の再構成でもない。ベンチマークは「意思決定した瞬間に自分の戦略が信じていた価格」だ — その瞬間の自分のプロセスだけがそれを知っている。
- 両方のタイムスタンプ:意思決定時刻と初回確認時刻。さもなければ遅延コストは測定不能で、静かにトレーディングコストへ紛れ込む。
- 取引所タイムスタンプ、手数料、メイカー/テイカーフラグ付きのすべての約定 — 自分のローカル受信時刻は、自分自身のインバウンドレイテンシで汚染されている。
- キャンセルおよび失効した親注文を、他のすべてと同じようにログする。 約定ゼロの親注文こそ、テーブルの中で最も高くつく行である。
- 永続的なミッドストリーム(またはL1ストリーム)を100〜250msの粒度で、マークアウトを計算できる十分な長さだけ保持する。約定シミュレータのためにすでに板を記録しているなら、これはタダである。
2つのフラットなテーブルで十分だ:
PARENTS = {
"parent_id": "str",
"strategy": "str",
"symbol": "str",
"venue": "str",
"algo": "str", # twap | pov | sniper | quote | ...
"side": "int8", # +1 buy, -1 sell
"qty": "float64", # parent size, base units
"limit_px": "float64", # NaN for unconstrained
"decision_ts": "datetime64[ns, UTC]",
"decision_mid": "float64", # mid your feed showed at decision_ts
"arrival_ts": "datetime64[ns, UTC]", # first exchange ack
"arrival_mid": "float64",
"end_ts": "datetime64[ns, UTC]", # filled / cancelled / expired
"terminal_mid": "float64",
"mkt_volume": "float64", # market volume over [arrival_ts, end_ts]
"sigma_bps": "float64", # realized vol estimate at decision time
}
FILLS = {
"parent_id": "str",
"ts": "datetime64[ns, UTC]", # exchange timestamp
"price": "float64",
"qty": "float64",
"fee": "float64", # quote ccy, post-discount, signed
"liquidity": "str", # maker | taker
"venue": "str",
}
帰属関数は、分解をそのまま書き起こしたものである:
import numpy as np
def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
"""Expanded implementation shortfall for one parent, bps of paper
notional. Sign convention: positive = cost."""
s, X = p["side"], p["qty"]
paper = X * p["decision_mid"]
x = fills["qty"].sum()
delay = s * X * (p["arrival_mid"] - p["decision_mid"])
trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
fees = fills["fee"].sum()
bps = lambda v: 1e4 * v / paper
return {
"parent_id": p["parent_id"],
"delay_bps": bps(delay), "trade_bps": bps(trade),
"oppty_bps": bps(oppty), "fees_bps": bps(fees),
"is_bps": bps(delay + trade + oppty + fees),
"fill_ratio": x / X,
"participation": x / max(p["mkt_volume"], x),
}
def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
fg = dict(tuple(fills.groupby("parent_id")))
empty = fills.iloc[0:0]
rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
for _, p in parents.iterrows()]
return parents.merge(pd.DataFrame(rows), on="parent_id")
そして帰属クエリ、これこそTCAが会計であることをやめ、研究になり始める場所である:
tca = tca_table(parents, fills)
def wavg(g: pd.DataFrame, col: str) -> float:
w = g["qty"] * g["decision_mid"] # notional weights
return (g[col] * w).sum() / w.sum()
comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
lambda g: pd.Series({c: wavg(g, c) for c in comp}
| {"is_std": g["is_bps"].std(), "n": len(g)}))
done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))
tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))
by_venue = tca.groupby("venue").apply(
lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
"is_std": g["is_bps"].std(),
"fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))
クエリ4は、スマートオーダールーターが最適化する入力テーブルである — 銘柄・会場別のTCAなしにルーティングすることは、手数料表によってルーティングすること、すなわち最小のコスト成分によってルーティングすることだ(暗号資産におけるスマートオーダールーティングはこのテーブルを出発点とする)。マークアウトモジュールと合わせて、これが約束の約200行である。
ループを閉じる:バックテストのコストモデルを較正する
ここがパイプラインが元を取る場所だ。バックテストは数字を断言する:slippage_bps = 5、約定確率曲線、インパクト係数。そのどれもがライブ執行についての主張であり、TCAテーブルはライブ執行そのものである。ループはこうだ:TCAで測り、コストモデルをフィットし、フィットしたモデルでバックテストを走らせ、取引し、再測定する。

テイカーコストについては、自分で発明するのではなくAlmgren et al.(2005)から関数形を借りよう。彼らの結果 — コストは日次ボラティリティ掛ける参加率のべき乗に比例する — は2パラメータのモデルを与える:
株式の推定値 を事前分布とする。生の親注文ではなく、ビン化された平均でフィットすること — 個々の親注文コストはノイズに支配されており(それがタイミングリスク項である)、生データに対する対数対数回帰はよろこんでノイズにフィットしてしまう:
done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0] # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)
def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
return a * sigma_bps * participation ** b
それから誤差範囲について誠実であること。Almgrenのチームは700,000件の注文を持っていながら、一時的インパクトの指数を と報告した;2,000件の親注文を持つボットが、銘柄ごとに自由に を推定できるわけがない。実務上の流儀: で正規化したあと銘柄を横断してプールし、 を 0.6 へ強く縮小し(あるいは固定して だけをフィットし)、月次で再フィットする。フィットした が上へドリフトしたなら、フットプリントが大きくなったか市場が薄くなったかである — いずれにせよバックテストはそれを知る必要があった。
メイカー戦略については、較正のターゲットが異なり、他の2つのモジュールから来る:
- 約定確率:約定シミュレータのキューモデルは、(スプレッド距離、キュー位置)のセルごとに を予測する;親注文ログは、セルごとに実現した約定率を提供する。不一致は、グリッド参照が付随したシミュレータのバグである。
- 逆選択:シミュレータの暗黙の「約定は交換可能である」という仮定を、測定されたマークアウトテーブルで置き換える — 時刻 における距離 でのシミュレートされたメイカー約定は、即時の時価評価ヘアカットとして測定された を帯びる。この単一の変更が、幻覚を見るメイカーバックテストと、現実を追跡するバックテストの違いである;それは約定シミュレーションの記事で指摘された、欠けていた較正入力だった。
- コストのばらつき:ISの平均ではなく分布をバックテストへ供給する。平均だけをシフトさせるコストモデルは、タイミングリスクが生み出すドローダウンを再現できない;親注文ごとのISにフィットさせた対数正規分布でさえ、定数よりはましである。
スリッページ曲線の完全な取り扱い — 関数形、レジーム条件付け、べき乗則が破綻するのはいつか — はそれ自体で一つの記事だ:バックテスト向けのスリッページ曲線とコストモデル。ここでの要点はアーキテクチャ上のものである:その記事のモデルは、本稿のテーブルなしにはフィットできない。
罠
TCAの文献は数十年前からあり、それを出し抜く方法も同じく古い。3つの失敗モードが、自己欺瞞の大半を占める。
ベンチマークのゲーミング。 到着価格以外のどんなベンチマークも、抱き込むことができる。古典はVWAPだ:区間VWAPに対して採点されるアルゴリズムは、ポジションが到着に対して20 bps血を流している間に、1ベーシスポイント以内でそれを追跡できる。なぜならベンチマークは、自分が押している価格とともにドリフトするからだ — そして意味のある参加率では、自分の約定こそがVWAPであり、それを追跡することは自己採点の宿題である。ベンチマークの政治学はTWAP対VWAP対POVで解剖した;TCA側のルールはもっと単純だ:ベンチマークは取引の前に選ばれ、スケジューラがスケジューリングのベンチマークに対して採点されるときでも、IS対到着は常に計算される。より微妙な変種は到着のゲーミングである:decision_ts を設定する成分が短期モメンタムを見られるなら、遅延項を美しく見せるように「意思決定」のタイミングを合わせられる。意思決定タイムスタンプはシグナルレイヤーに属し、いかなる執行ロジックが走る前にもログされる。
約定のみの分析における生存者バイアス。 コスト分析を約定に条件付けると、パッシブ執行はタダに見える。具体的には:100件のパッシブ買い親注文、ミッドの1ティック下。60件が約定し — パッシブであるがゆえに — 到着より平均3 bps良い価格で約定する:測定された「コスト」は bps、誇らしいレポートだ。一度も約定しなかった40件は、まさに価格が跳ね上がって逃げたものである;キャンセル時に25 bps不利と評価すれば、誠実な数字は bps である。約定のみのレポートと真のレポートは、11 bpsかつ符号で異なる。これはコーナーケースではない — これはパッシブ取引のメカニズムそのものだ:約定されることは価格が自分を突き抜けて来ることと相関しており、それはタッチ約定バックテストをファンタジーにする条件付けと同じである。機会コストはISのオプションの洗練ではない;それは測定全体を選択から守る項である。同じバイアスにはテイカーの変種がある:外れたIOC注文、レートリミットやリスクチェックで拒否された注文 — 外れがログされなければ、外れのコストは測定されず、そしてそれはあなたの戦略が最も取引を欲した速い市場でこそ最大なのだ。
各種の自爆装置、手短に:取引所のロウソク足から再構成された到着ミッド(あなたのフィードとロウソク足は、まさに重要なときに食い違う);想定元本の重みなしに親注文ごとのbpsを平均すること(千件のダスト約定が1件の本物の注文を票で上回る);割引前でログされた手数料;約定とは別の会場から取られたマークアウトミッド(会場間ベーシスが逆選択のふりをする);そしてパープでは、ファンディングの発生を執行ウィンドウへ滲ませること(ファンディングはコストだが、執行コストではなく、両者を混ぜることは両方の分析を毒する)。
今週やるべきこと
ボットに2つのログテーブルを追加する — 上のスキーマは列のリストであってプロジェクトではない。バックフィルは何もしない;2週間の誠実なログは、1年分の再構成に勝る。分解と3つのマークアウトホライズンを走らせる。どの成分が支配的か(ほぼ手数料ではない)、パッシブ約定がスプレッド獲得を超えて逆選択されているか、そしてバックテストのコスト定数が測定された曲線からどれだけ離れているかを学ぶだろう。それからフィットした曲線をシミュレータへ配線し直し、そもそもこの戦略を走らせろと言ったバックテストを再実行する。Peroldのペーパーポートフォリオは、38年を経てなお、あなたのリアルポートフォリオが持つ唯一の誠実な対戦相手である。
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.