背景知識:Execution Simulator実装
「終値でBacktestingすることは、地図上の直線距離を使ってハイキングルートを計画するようなものです - すべての実際の障害物を無視しています。」
I. なぜExecution Simulatorが必要なのか?
1.1 BacktestとLive Tradingのギャップ
| Backtest仮定 | Live現実 |
|---|---|
| シグナル発火 = 即座の実行 | シグナルから実行までの遅延 |
| Close価格で実行 | Order bookレベルを歩かなければならない |
| 無制限の流動性 | Order book深度は限定的 |
| 注文は市場に影響しない | 大きな注文は価格を動かす |
| 100% Fill rate | Limit orderはフィルしない可能性 |
Execution Simulator目標:Backtesting中にこれらの実世界の制約をシミュレートして、「理想的な世界でのみ利益を出す」戦略をフィルタリングします。
1.2 Simulatorレベル
II. Level 1:Fixed Slippage Model
2.1 原理
最も単純なシミュレーション:取引ごとに固定コストを差し引く。
実際の実行価格 = 理論価格 x (1 + スリッページ方向 x スリッページ率)
買い:実際価格 = 理論価格 x (1 + slippage)
売り:実際価格 = 理論価格 x (1 - slippage)
2.2 典型的なパラメータ
| 市場 | 推奨Slippage | 適用シナリオ |
|---|---|---|
| US Large Cap | 0.02-0.05% | AAPL、MSFT、SPY |
| US Small Cap | 0.1-0.3% | ADV < $10M |
| A-Shares | 0.05-0.1% | CSI 300構成銘柄 |
| Crypto Major | 0.03-0.1% | BTC、ETH |
| Crypto Altcoins | 0.3-1% | 小さな時価総額コイン |
2.3 コード実装
class FixedSlippageSimulator:
"""固定Slippage実行Simulator"""
def __init__(self, slippage_rate: float = 0.001,
commission_rate: float = 0.0003):
self.slippage_rate = slippage_rate
self.commission_rate = commission_rate
def execute(self, order: dict) -> dict:
"""
注文実行をシミュレート
order = {
'symbol': 'AAPL',
'side': 'buy', # 'buy'または'sell'
'quantity': 100,
'price': 185.0, # 理論価格(例:Close)
}
"""
price = order['price']
quantity = order['quantity']
# Slippage調整
if order['side'] == 'buy':
exec_price = price * (1 + self.slippage_rate)
else:
exec_price = price * (1 - self.slippage_rate)
# コストを計算
notional = exec_price * quantity
commission = notional * self.commission_rate
slippage_cost = abs(exec_price - price) * quantity
return {
'exec_price': exec_price,
'exec_quantity': quantity,
'fill_rate': 1.0, # 固定Modelは100% fillを仮定
'commission': commission,
'slippage_cost': slippage_cost,
'total_cost': commission + slippage_cost
}
2.4 制限
- 注文サイズの影響を反映しない
- 流動性品質を区別しない
- 部分フィルをモデル化しない
- 楽観的すぎるか悲観的すぎる(パラメータ設定に依存)
III. Level 2:Square-Root Impact Model
3.1 理論的基礎
Almgren-Chrissや他の研究によると、市場影響は注文サイズの平方根に比例します:
Slippage = k x sigma x sqrt(Q / ADV)
Where:
k = 影響係数(経験値0.5-1.5)
sigma = 日次ボラティリティ
Q = 注文金額
ADV = Average Daily Volume
直感的な説明:
- 大きな注文はより深いOrder bookレベルを「消費」
- しかし関係は線形ではない - 最初のレベルの影響は後のものより大きい
- より高いボラティリティは市場を影響に対してより敏感にする
3.2 コード実装
import numpy as np
class SquareRootImpactSimulator:
"""平方根影響Model実行Simulator"""
def __init__(self,
impact_coef: float = 1.0,
commission_rate: float = 0.0003,
min_slippage: float = 0.0001):
self.impact_coef = impact_coef
self.commission_rate = commission_rate
self.min_slippage = min_slippage
def execute(self, order: dict, market_data: dict) -> dict:
"""
order = {
'symbol': 'AAPL',
'side': 'buy',
'quantity': 100,
'price': 185.0,
}
market_data = {
'volatility': 0.015, # 日次ボラティリティ
'adv': 10_000_000_000, # 平均日次ボリューム(ドル)
'spread': 0.0001, # Bid-ask spread比率
}
"""
price = order['price']
quantity = order['quantity']
notional = price * quantity
vol = market_data['volatility']
adv = market_data['adv']
spread = market_data.get('spread', 0.0001)
# 平方根影響Model
participation = notional / adv
impact = self.impact_coef * vol * np.sqrt(participation)
# 半分のspreadを追加(spreadを横断)
total_slippage = max(impact + spread / 2, self.min_slippage)
# 実行価格
if order['side'] == 'buy':
exec_price = price * (1 + total_slippage)
else:
exec_price = price * (1 - total_slippage)
# コスト計算
commission = notional * self.commission_rate
slippage_cost = abs(exec_price - price) * quantity
return {
'exec_price': exec_price,
'exec_quantity': quantity,
'fill_rate': 1.0,
'commission': commission,
'slippage_cost': slippage_cost,
'total_cost': commission + slippage_cost,
'impact_bps': impact * 10000, # Impactをベーシスポイントで
}
3.3 紙演習
シナリオ:$1,000,000の株を購入
| 株 | ボラティリティ | ADV | Participation | 期待Slippage |
|---|---|---|---|---|
| AAPL | 1.5% | $10B | 0.01% | 1.5% x sqrt(0.0001) = 0.015% |
| TSLA | 3% | $3B | 0.033% | 3% x sqrt(0.00033) = 0.055% |
| Small Cap X | 4% | $10M | 10% | 4% x sqrt(0.1) = 1.26% |
発見:同じ$1M注文がSmall CapではAAPLと比較して84倍高いSlippageを持っています!
IV. Level 3:Order Book Replay Model
4.1 原理
実際の履歴Level-2データを使用して、注文がOrder bookを「歩く」ことをシミュレートします。
Order Book Snapshot(t=09:30:01.123):
Ask 5: $100.10 x 500
Ask 4: $100.08 x 300
Ask 3: $100.06 x 200
Ask 2: $100.04 x 100
Ask 1: $100.02 x 50
---------------------
Bid 1: $100.00 x 80
Bid 2: $99.98 x 150
...
Market Buy 400株:
50株 @ $100.02 (Ask 1をクリア)
100株 @ $100.04 (Ask 2をクリア)
200株 @ $100.06 (Ask 3をクリア)
50株 @ $100.08 (Ask 4の一部)
VWAP = (50x100.02 + 100x100.04 + 200x100.06 + 50x100.08) / 400
= $100.055
Mid price = ($100.02 + $100.00) / 2 = $100.01
Slippage = ($100.055 - $100.01) / $100.01 = 0.045%
4.2 完全実装
from typing import List, Tuple, Optional
from dataclasses import dataclass
@dataclass
class OrderBookLevel:
"""単一Order bookレベル"""
price: float
size: float # 株数またはドル金額
@dataclass
class OrderBook:
"""Order bookスナップショット"""
timestamp: float
bids: List[OrderBookLevel] # 買い側、価格降順
asks: List[OrderBookLevel] # 売り側、価格昇順
@property
def mid_price(self) -> float:
if self.bids and self.asks:
return (self.bids[0].price + self.asks[0].price) / 2
return 0.0
@property
def spread(self) -> float:
if self.bids and self.asks:
return self.asks[0].price - self.bids[0].price
return float('inf')
class OrderBookReplaySimulator:
"""Order book replay実行Simulator"""
def __init__(self,
commission_rate: float = 0.0003,
partial_fill_enabled: bool = True):
self.commission_rate = commission_rate
self.partial_fill_enabled = partial_fill_enabled
def execute_market_order(self,
order_book: OrderBook,
side: str,
quantity: float) -> dict:
"""
Market orderがOrder bookを歩くことをシミュレート
"""
if side == 'buy':
levels = order_book.asks
else:
levels = order_book.bids
if not levels:
return self._empty_fill(quantity)
mid_price = order_book.mid_price
remaining = quantity
total_cost = 0.0
filled = 0.0
fills = []
for level in levels:
if remaining <= 0:
break
fill_qty = min(remaining, level.size)
fill_price = level.price
fills.append({
'price': fill_price,
'quantity': fill_qty
})
total_cost += fill_qty * fill_price
filled += fill_qty
remaining -= fill_qty
if filled == 0:
return self._empty_fill(quantity)
# 結果を計算
avg_price = total_cost / filled
if side == 'buy':
slippage = (avg_price - mid_price) / mid_price
else:
slippage = (mid_price - avg_price) / mid_price
commission = total_cost * self.commission_rate
slippage_cost = abs(avg_price - mid_price) * filled
return {
'exec_price': avg_price,
'exec_quantity': filled,
'fill_rate': filled / quantity,
'unfilled': remaining,
'commission': commission,
'slippage_cost': slippage_cost,
'total_cost': commission + slippage_cost,
'slippage_bps': slippage * 10000,
'fills': fills,
'levels_consumed': len(fills),
}
def execute_limit_order(self,
order_book: OrderBook,
side: str,
quantity: float,
limit_price: float,
queue_position: float = 0.5) -> dict:
"""
Limit order実行をシミュレート
queue_position: その価格レベルでのキュー位置(0=前、1=後ろ)
"""
if side == 'buy':
# 買い注文:limit >= ask 1なら、即座に一部フィル
if order_book.asks and limit_price >= order_book.asks[0].price:
return self.execute_market_order(order_book, side, quantity)
# そうでなければキューに入る
return self._simulate_queue(order_book, side, quantity,
limit_price, queue_position)
else:
# 売り注文:limit <= bid 1なら、即座に一部フィル
if order_book.bids and limit_price <= order_book.bids[0].price:
return self.execute_market_order(order_book, side, quantity)
return self._simulate_queue(order_book, side, quantity,
limit_price, queue_position)
def _simulate_queue(self, order_book: OrderBook, side: str,
quantity: float, limit_price: float,
queue_position: float) -> dict:
"""
Limit orderキューをシミュレート(簡略版)
実際には、フィルを決定するために後続のOrder flowデータが必要
これは推定フィル確率を返す
"""
mid = order_book.mid_price
spread = order_book.spread
if side == 'buy':
# limit_priceでの買い注文
distance_from_mid = (mid - limit_price) / mid
else:
distance_from_mid = (limit_price - mid) / mid
# 簡略化されたフィル確率推定
# より遠い距離 = より低いフィル確率
fill_prob = max(0, 1 - distance_from_mid * 100)
fill_prob *= (1 - queue_position * 0.5) # キュー位置ペナルティ
return {
'exec_price': limit_price,
'exec_quantity': 0, # 即座にフィルされない
'fill_rate': 0,
'fill_probability': fill_prob,
'status': 'pending',
'queue_position': queue_position,
}
def _empty_fill(self, quantity: float) -> dict:
return {
'exec_price': 0,
'exec_quantity': 0,
'fill_rate': 0,
'unfilled': quantity,
'commission': 0,
'slippage_cost': 0,
'total_cost': 0,
'error': 'no_liquidity'
}
4.3 使用例
# Order bookを構築
order_book = OrderBook(
timestamp=1704067200.123,
bids=[
OrderBookLevel(100.00, 80),
OrderBookLevel(99.98, 150),
OrderBookLevel(99.96, 400),
],
asks=[
OrderBookLevel(100.02, 50),
OrderBookLevel(100.04, 100),
OrderBookLevel(100.06, 200),
OrderBookLevel(100.08, 300),
OrderBookLevel(100.10, 500),
]
)
simulator = OrderBookReplaySimulator()
# 小さな注文:ask 1のみを消費
result_small = simulator.execute_market_order(order_book, 'buy', 30)
print(f"小さな注文30株:exec price ${result_small['exec_price']:.4f}、"
f"slippage {result_small['slippage_bps']:.2f} bps")
# 中程度の注文:最初の3レベルを消費
result_medium = simulator.execute_market_order(order_book, 'buy', 300)
print(f"中程度の注文300株:exec price ${result_medium['exec_price']:.4f}、"
f"slippage {result_medium['slippage_bps']:.2f} bps")
# 大きな注文:すべてのレベルを歩いてもまだ不十分
result_large = simulator.execute_market_order(order_book, 'buy', 2000)
print(f"大きな注文2000株:filled {result_large['exec_quantity']} 株、"
f"fill rate {result_large['fill_rate']:.1%}")
出力:
小さな注文30株:exec price $100.0200、slippage 1.00 bps
中程度の注文300株:exec price $100.0467、slippage 3.67 bps
大きな注文2000株:filled 1150 株、fill rate 57.5%
V. Level 4:Full Simulation環境
5.1 考慮される追加要因
| 要因 | 説明 | 実装複雑さ |
|---|---|---|
| キュー位置 | その価格レベルでのあなたの注文の位置 | 高 |
| 時間減衰 | より長いキュー時間 = より多くの人が先にフィル | 中 |
| キャンセルシミュレーション | 他の人がキャンセルし、あなたの位置が前進する可能性 | 高 |
| Hidden orders | Iceberg order、Dark pool orderは見えない | 非常に高 |
| 自己影響 | あなたの注文が後続の価格に影響 | 高 |
5.2 設計フレームワーク
class FullSimulationEngine:
"""
Full simulation実行Engine(フレームワーク例示)
実際の実装には必要:
- 完全なTickデータストリーム
- イベント駆動アーキテクチャ
- 注文状態マシン
"""
def __init__(self):
self.order_book = None
self.pending_orders = {}
self.fills = []
self.clock = 0
def on_market_data(self, event: dict):
"""市場データ更新を処理"""
if event['type'] == 'order_book_update':
self._update_order_book(event)
self._check_pending_fills()
elif event['type'] == 'trade':
self._process_trade(event)
def submit_order(self, order: dict) -> str:
"""注文を送信、注文IDを返す"""
order_id = self._generate_order_id()
order['status'] = 'pending'
order['submit_time'] = self.clock
order['queue_position'] = self._estimate_queue_position(order)
self.pending_orders[order_id] = order
return order_id
def _check_pending_fills(self):
"""保留注文がフィルできるかチェック"""
for order_id, order in list(self.pending_orders.items()):
fill = self._try_fill(order)
if fill:
self.fills.append(fill)
if fill['remaining'] == 0:
del self.pending_orders[order_id]
def _try_fill(self, order: dict) -> Optional[dict]:
"""注文をフィルしようと試みる"""
# 価格に到達したかチェック
# キュー位置に到達したかチェック
# フィル可能な数量を計算
# フィル結果を返す
pass
def _estimate_queue_position(self, order: dict) -> int:
"""Order bookでのキュー位置を推定"""
# その価格レベルでの既存の注文をカウント
pass
VI. Simulator較正
6.1 Liveデータでの較正
最も正確なSimulatorパラメータは、あなた自身のLive Trading記録から来ます。
較正プロセス:
1. Live実行データを収集
- 注文送信時刻、価格、数量
- 実際の実行時刻、価格、数量
- 時刻のOrder bookスナップショット(利用可能な場合)
2. 実際のSlippage分布を計算
- Slippage = (実際exec価格 - 送信時のmid価格) / mid価格
- 注文サイズでグループ統計
3. Modelパラメータをフィット
- 平方根Modelの場合:k値をフィット
- Order book Modelの場合:ウォークスルーロジックを検証
4. Model予測vs.実際を検証
- 予測誤差分布を計算
- 反復的にパラメータを調整
6.2 保守的原則
パラメータが不確実な場合、コストを過大評価することを好む:
class ConservativeSimulator:
"""保守的Simulator:コストを過大評価することを好む"""
def __init__(self,
base_simulator,
safety_margin: float = 1.5):
self.base = base_simulator
self.margin = safety_margin
def execute(self, order: dict, market_data: dict) -> dict:
result = self.base.execute(order, market_data)
# Slippageを増幅
result['slippage_cost'] *= self.margin
# Fill rateを減らす
result['fill_rate'] = min(1.0, result['fill_rate'] / self.margin)
# 総コストを再計算
result['total_cost'] = (result['commission'] +
result['slippage_cost'])
return result
VII. Multi-Agent視点
Multi-AgentシステムにおけるExecution Simulatorの位置:
VIII. 一般的な誤解
誤解1:より複雑なSimulatorは常により良い
必ずしもそうではありません。複雑なSimulatorはより多くのデータ、より多くのパラメータを必要とし、新しい不確実性を導入する可能性があります。あなたの戦略頻度に合ったSimulatorを選択してください:
| 戦略頻度 | 推奨Simulator |
|---|---|
| 日次/週次 | Level 2(平方根Model) |
| 分レベル | Level 2-3 |
| 秒/Tickレベル | Level 3-4 |
誤解2:Simulatorパラメータは一度設定すれば終わり
市場流動性は変化します:
- 市場パニック中、Slippageは数倍になる
- 個別株イベント(決算、ニュース)は短期流動性に影響
- 市場構造の変化(例:Market maker戦略調整)
誤解3:部分フィルを無視
Limit orderの部分フィルは例外ではなく、規範です。これは意味します:
- ポジションがアンバランスになる可能性
- チェイスロジックが必要
- リスクエクスポージャーが期待と異なる
IX. 実践的推奨事項
9.1 段階的実装
フェーズ1:基本検証
- 迅速な戦略スクリーニングのためにLevel 1(固定Slippage)を使用
- 総利益 < 0.5%/取引の戦略を排除
フェーズ2:改良
- Level 2(平方根Model)にアップグレード
- 資産流動性ごとに異なるパラメータを設定
- 純利益 < 0の戦略を排除
フェーズ3:Live較正
- 小資本Live Tradingでデータを収集
- LiveデータでSimulatorを較正
- 閉ループを形成
フェーズ4:継続的最適化
- 新しいLiveデータで定期的にパラメータを更新
- シミュレーションvs.実際の偏差を監視
- 異常時にアラートをトリガー
9.2 主要メトリクス監視
def compare_simulated_vs_actual(simulated: dict, actual: dict) -> dict:
"""シミュレート結果と実際の実行を比較"""
slippage_error = (actual['slippage_bps'] -
simulated['slippage_bps'])
fill_rate_error = (actual['fill_rate'] -
simulated['fill_rate'])
return {
'slippage_error_bps': slippage_error,
'fill_rate_error': fill_rate_error,
'is_conservative': slippage_error < 0, # シミュレーションが実際より悲観的
'needs_recalibration': abs(slippage_error) > 5, # 5bps以上は再較正が必要
}
X. まとめ
| 重要なポイント | 説明 |
|---|---|
| コア目的 | Backtesting中に実際の実行制約をシミュレート |
| レベル選択 | より高い戦略頻度はより洗練されたSimulatorを必要とする |
| 保守的原則 | パラメータが不確実な場合、コストを過大評価することを好む |
| 閉ループ較正 | LiveデータでSimulatorを継続的に較正 |
| 最終目標 | Backtest結果をLiveパフォーマンスに近づける |
さらなる読み物
- レッスン18:Trading Costモデリングと取引可能性 - Costモデル理論
- レッスン19:Execution System - シグナルから実際のフィルまで - Execution System設計
- 背景知識:Tick-Level Backtesting Framework - イベント駆動Backtestingとキューシミュレーション
- 背景知識:HF Market Microstructure - Kyle's LambdaとOrder flow
- 背景知識:ExchangesとOrder Book Mechanics - Order book基礎