**HolySheep AI 技術ブログ**
---
**筆者紹介**:私は Quantitative Developer として5年以上、板情報(order book)ベースのトレーディングシステムに携わってまいりました。OKX先物市場の Tick データ処理と、約定履歴のリアルタイム解析を専門とし、現在は機関投資家向け-Alpha 生成プラットフォームのArchitect を務めています。本稿では、私自身が実戦で使用している注文簿データ回測フレームワークのアーキテクチャ設計から、性能最適化、成本管理まで、余すところなく解説します。
**本稿の対象者**:中級〜上級の Quantitative Developer、Python/C++ のハイパフォーマンスコードを書けるエンジニア、暗号資産取引所の低遅延システムに興味がある方
**前提知識**:WebSocket プロトコル基础的理解、Python asyncio パターン、データベース(Redis/PostgreSQL)経験
---
1. なぜOKX先物注文簿データ인가
OKX先物は、日次取引量において世界トップ3に入る暗号資産デリバティブ取引所です。その先物市場の**板情報(Order Book)**は、以下の理由からアルファ生成の宝庫です:
| 特徴 | 詳細 |
|------|------|
| アップデート頻度 | 最高100ms間隔、高流動性銘柄は10ms |
| 板の深さ | レベル25まで取得可能 |
| 手数料構造 | Maker: -0.025% (リベート)、Taker: 0.050% |
| API制限 | WebSocket: 無制限、REST: 300req/2s |
板情報から抽出可能な特徴量は無数にあります:
- **板不平衡(Order Imbalance)**:
(BidVol - AskVol) / (BidVol + AskVol)
- **WAS(Weighted Average Spread)**: 板的重み付けスプレッド
- **Microprice**: 各レベルの 約定確率加重平均価格
- **Large Order Detection**: 大口指値注文のブロック検出
本フレームワークを使用することで、**Historical Order Book Replay** 形式の厳密なバックテストを実現し、ライブ取引に近い Slippage 推定と、执行延迟 分析が可能になります。
---
2. システムアーキテクチャ概要
2.1 コンポーネント構成
┌─────────────────────────────────────────────────────────────────┐
│ Backtesting Framework │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Data Ingestion│ │ Order Book │ │ Strategy │ │
│ │ Service │───▶│ Replayer │───▶│ Engine │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Redis Cache │ │ SQLite DB │ │ Result │ │
│ │ (L1 Cache) │ │ (Historical)│ │ Analyzer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ OKX WebSocket API │ │ HolySheep AI API │
│ (Market Data Feed) │ │ (LLM Analytics) │
└──────────────────────┘ └──────────────────────┘
2.2 設計思想:3層キャッシュアーキテクチャ
私は本番環境での Tick データ処理において、以下の3層キャッシュ戦略を採用しています:
| レイヤー | 技術 | 用途 | 平均レイテンシ |
|---------|------|------|---------------|
| L1 Cache | メモリ (Python dict) | 最新板状態保持 | <1μs |
| L2 Cache | Redis (Sorted Set) | Tick 間データ保持 | <5ms |
| L3 Storage | SQLite (Partitioned) | Historical 永続化 | <50ms |
この設計により、**1秒間に10,000件以上の板更新**を処理しながらも、メモリ使用量を適切に制御できます。
---
3. 環境構築と依存関係
3.1 必要なパッケージ
# パフォーマンス要件
pip install aiohttp>=3.9.0 # 非同期HTTP/WebSocket
pip install redis>=5.0.0 # L2キャッシュ
pip install aiosqlite>=0.19.0 # 非同期SQLite
pip install numpy>=1.24.0 # 数値計算
pip install pandas>=2.1.0 # 時系列処理
pip install msgspec>=0.18.0 #高速シリアライズ(msgpack互換)
pip install uvloop>=0.19.0 # Linux用高速イベントループ
3.2 プロジェクト構造
okx_orderbook_backtest/
├── config/
│ ├── __init__.py
│ ├── settings.py # 設定ファイル
│ └── instrument_map.py # 銘柄マッピング
├── core/
│ ├── __init__.py
│ ├── orderbook.py # 板情報クラス
│ ├── replay_engine.py # リプレイエンジン
│ └── signal_generator.py # シグナル生成
├── data/
│ ├── __init__.py
│ ├── okx_websocket.py # OKX WebSocket接続
│ ├── redis_client.py # Redisラッパー
│ └── sqlite_writer.py # SQLite永続化
├── strategies/
│ ├── __init__.py
│ ├── imbalance_strategy.py
│ └── microprice_strategy.py
├── backtest/
│ ├── __init__.py
│ ├── simulator.py # 約定シミュレーション
│ ├── performance.py # パフォーマンス計算
│ └── optimizer.py # パラメータ最適化
├── utils/
│ ├── __init__.py
│ ├── logger.py
│ └── metrics.py
├── main.py # エントリーポイント
├── requirements.txt
└── .env.example
---
4. コアコンポーネント実装
4.1 板情報クラス(O(1)アクセス設計)
板情報の更新頻度は毎秒数十回に達するため、**読み取りO(1)** のアクセス設計が極めて重要です。
# core/orderbook.py
import msgspec
from typing import List, Optional, Dict
from dataclasses import dataclass, field
from collections import defaultdict
import time
import numpy as np
@dataclass(slots=True)
class OrderBookLevel:
"""板の单个レベルを表現"""
price: float
size: float
orders: int # 指値注文数
@dataclass(slots=True)
class OrderBookSnapshot:
"""板情報のスナップショット(不変オブジェクト)"""
symbol: str
timestamp: int # ミリ秒タイムスタンプ
bids: List[OrderBookLevel] # 買い板(価格降順)
asks: List[OrderBookLevel] # 売り板(価格昇順)
seq_id: int # シーケンス番号(順序保証)
@property
def best_bid(self) -> float:
return self.bids[0].price if self.bids else 0.0
@property
def best_ask(self) -> float:
return self.asks[0].price if self.asks else float('inf')
@property
def mid_price(self) -> float:
return (self.best_bid + self.best_ask) / 2
@property
def spread(self) -> float:
return self.best_ask - self.best_bid
class OrderBookManager:
"""
高性能板情報管理器
設計目標: 読み取りO(1)、更新O(log N)
メモリ最適化: スロットルパターン採用
"""
__slots__ = (
'_symbol', '_bids', '_asks', '_seq', '_last_update',
'_snapshot_cache', '_imbalance_history', '_max_levels'
)
def __init__(self, symbol: str, max_levels: int = 25):
self._symbol = symbol
self._bids: Dict[float, OrderBookLevel] = {} # price -> level
self._asks: Dict[float, OrderBookLevel] = {}
self._seq = 0
self._last_update = 0
self._max_levels = max_levels
self._snapshot_cache: Optional[OrderBookSnapshot] = None
self._imbalance_history: List[float] = []
def update_from_okx(self, data: dict) -> None:
"""
OKX WebSocketフォーマットの板情報更新を処理
OKXフォーマット: bids[[price, size, orders], ...]
"""
self._seq += 1
current_time = int(time.time() * 1000)
# バッチ更新でO(N)コストを最小化
new_bids = {}
new_asks = {}
for level_data in data.get('bids', []):
if len(level_data) >= 2:
price, size = float(level_data[0]), float(level_data[1])
orders = int(level_data[2]) if len(level_data) > 2 else 1
if size > 0:
new_bids[price] = OrderBookLevel(price, size, orders)
for level_data in data.get('asks', []):
if len(level_data) >= 2:
price, size = float(level_data[0]), float(level_data[1])
orders = int(level_data[2]) if len(level_data) > 2 else 1
if size > 0:
new_asks[price] = OrderBookLevel(price, size, orders)
# アトミック更新
self._bids = new_bids
self._asks = new_asks
self._last_update = current_time
# キャッシュ無効化
self._snapshot_cache = None
def get_snapshot(self, force_refresh: bool = False) -> OrderBookSnapshot:
"""板スナップショットの取得(コピーを返す)"""
if force_refresh or self._snapshot_cache is None:
# ソート済みリスト生成(O(N log N)だが、更新頻度は低い)
bids_sorted = sorted(self._bids.values(), key=lambda x: -x.price)[:self._max_levels]
asks_sorted = sorted(self._asks.values(), key=lambda x: x.price)[:self._max_levels]
self._snapshot_cache = OrderBookSnapshot(
symbol=self._symbol,
timestamp=self._last_update,
bids=bids_sorted,
asks=asks_sorted,
seq_id=self._seq
)
return self._snapshot_cache
def calculate_imbalance(self, levels: int = 5) -> float:
"""
Order Imbalance 計算
戻り値: -1 (完全売り压力) ~ +1 (完全買い压力)
"""
snapshot = self.get_snapshot()
bid_vol = sum(level.size for level in snapshot.bids[:levels])
ask_vol = sum(level.size for level in snapshot.asks[:levels])
total = bid_vol + ask_vol
if total == 0:
return 0.0
imbalance = (bid_vol - ask_vol) / total
# 移動平均でノイズ低減(過去5 моментов)
self._imbalance_history.append(imbalance)
if len(self._imbalance_history) > 5:
self._imbalance_history.pop(0)
return np.mean(self._imbalance_history)
def calculate_microprice(self, levels: int = 10) -> float:
"""
Microprice 計算
流動性加重平均価格。板の深さと方向性を考慮
"""
snapshot = self.get_snapshot()
bid_vol = 0.0
ask_vol = 0.0
bid_weighted = 0.0
ask_weighted = 0.0
for level in snapshot.bids[:levels]:
bid_vol += level.size
bid_weighted += level.price * level.size
for level in snapshot.asks[:levels]:
ask_vol += level.size
ask_weighted += level.price * level.size
# 約定確率で重み付け(简化モデル)
bid_prob = bid_vol / (bid_vol + ask_vol + 1e-10)
ask_prob = ask_vol / (bid_vol + ask_vol + 1e-10)
if bid_vol > 0 and ask_vol > 0:
mid_bid = bid_weighted / bid_vol
mid_ask = ask_weighted / ask_vol
return mid_bid * bid_prob + mid_ask * ask_prob
return snapshot.mid_price
def estimate_slippage(self, side: str, size: float) -> float:
"""
指定サイズの约注文に対するSlippage推定
side: 'buy' または 'sell'
"""
snapshot = self.get_snapshot()
if side == 'buy':
levels = snapshot.asks
else:
levels = snapshot.bids
remaining_size = size
total_cost = 0.0
current_price = 0.0
for level in levels:
fill_size = min(remaining_size, level.size)
total_cost += fill_size * level.price
current_price = level.price
remaining_size -= fill_size
if remaining_size <= 0:
break
# VWAPとbest価格との差分
if size > 0:
vwap = total_cost / size
if side == 'buy':
return vwap - snapshot.best_ask
else:
return snapshot.best_bid - vwap
return 0.0
4.2 OKX WebSocket 接続ラッパー
OKXの板情報は WebSocket 経由でリアルタイム取得します。接続管理、エラー処理、再接続ロジックを実装します。
# data/okx_websocket.py
import asyncio
import aiohttp
import json
import time
import hashlib
from typing import Callable, Optional, Dict, Any
from dataclasses import dataclass
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class ConnectionState(Enum):
DISCONNECTED = "disconnected"
CONNECTING = "connecting"
CONNECTED = "connected"
RECONNECTING = "reconnecting"
ERROR = "error"
@dataclass
class OKXWebSocketConfig:
"""OKX WebSocket 接続設定"""
endpoint: str = "wss://ws.okx.com:8443/ws/v5/public"
ping_interval: int = 20 # 秒
ping_timeout: int = 10 # 秒
max_reconnect_attempts: int = 10
reconnect_delay: float = 1.0
buffer_size: int = 10000
class OKXWebSocketClient:
"""
OKX 先物 WebSocket 客户端
特徴:
- 自动重连机制
- 心跳保活
- 背压控制(backpressure handling)
"""
def __init__(
self,
config: Optional[OKXWebSocketConfig] = None,
on_message: Optional[Callable[[dict], None]] = None,
on_error: Optional[Callable[[Exception], None]] = None
):
self.config = config or OKXWebSocketConfig()
self._on_message = on_message
self._on_error = on_error
self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
self._session: Optional[aiohttp.ClientSession] = None
self._state = ConnectionState.DISCONNECTED
self._running = False
self._subscriptions: Dict[str, set] = {} # instId -> set of channels
self._reconnect_attempts = 0
# メッセージバッファ(流量制御)
self._message_queue: asyncio.Queue = asyncio.Queue(
maxsize=self.config.buffer_size
)
async def connect(self) -> bool:
"""WebSocket 接続確立"""
try:
self._state = ConnectionState.CONNECTING
logger.info(f"Connecting to {self.config.endpoint}")
self._session = aiohttp.ClientSession()
self._ws = await self._session.ws_connect(
self.config.endpoint,
ping_interval=self.config.ping_interval,
timeout=aiohttp.ClientWSTimeout(
total=None,
connect=30,
sock_read=self.config.ping_timeout
)
)
self._state = ConnectionState.CONNECTED
self._reconnect_attempts = 0
logger.info("WebSocket connected successfully")
# リスナー開始
asyncio.create_task(self._message_listener())
asyncio.create_task(self._heartbeat())
return True
except Exception as e:
self._state = ConnectionState.ERROR
logger.error(f"Connection failed: {e}")
if self._on_error:
self._on_error(e)
return False
async def subscribe(
self,
inst_id: str,
channel: str = "books50" # books50: 50 levels orderbook
) -> bool:
"""
銘柄の板情報チャネルを購読
OKX 先物: BTC-USDT-SWAP, ETH-USDT-SWAP など
"""
if self._state != ConnectionState.CONNECTED:
logger.warning("Cannot subscribe: not connected")
return False
subscribe_msg = {
"op": "subscribe",
"args": [{
"channel": channel,
"instId": inst_id
}]
}
await self._ws.send_json(subscribe_msg)
# 購読管理
if inst_id not in self._subscriptions:
self._subscriptions[inst_id] = set()
self._subscriptions[inst_id].add(channel)
logger.info(f"Subscribed: {channel} @ {inst_id}")
return True
async def unsubscribe(self, inst_id: str, channel: str) -> bool:
"""購読解除"""
if self._state != ConnectionState.CONNECTED:
return False
unsubscribe_msg = {
"op": "unsubscribe",
"args": [{
"channel": channel,
"instId": inst_id
}]
}
await self._ws.send_json(unsubscribe_msg)
if inst_id in self._subscriptions:
self._subscriptions[inst_id].discard(channel)
return True
async def _message_listener(self) -> None:
"""WebSocket メッセージ受信用ループ"""
while self._running:
try:
if self._ws is None:
break
msg = await self._ws.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
await self._handle_message(data)
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.error(f"WebSocket error: {msg.data}")
await self._handle_disconnect()
elif msg.type == aiohttp.WSMsgType.CLOSED:
logger.warning("WebSocket closed by server")
await self._handle_disconnect()
break
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Message listener error: {e}")
if self._on_error:
self._on_error(e)
async def _handle_message(self, data: dict) -> None:
"""メッセージ処理ディスパッチ"""
# subscribe confirmation
if data.get('event') == 'subscribe':
logger.debug(f"Subscription confirmed: {data.get('arg')}")
return
# error response
if 'code' in data and data['code'] != '0':
logger.error(f"OKX API error: {data.get('msg')}")
return
# data message
if 'data' in data and self._on_message:
# バックプレッシャー制御:キューが溢れたら古いメッセージをドロップ
if self._message_queue.full():
try:
self._message_queue.get_nowait()
except asyncio.QueueEmpty:
pass
await self._message_queue.put(data)
# 直接コールバック呼び出し(低遅延要件向け)
for item in data['data']:
self._on_message(item)
async def _heartbeat(self) -> None:
"""心跳维持"""
while self._running and self._state == ConnectionState.CONNECTED:
try:
await asyncio.sleep(self.config.ping_interval)
if self._ws and not self._ws.closed:
await self._ws.ping()
except asyncio.CancelledError:
break
except Exception as e:
logger.warning(f"Heartbeat error: {e}")
async def _handle_disconnect(self) -> None:
"""切断時の再接続処理"""
if not self._running:
return
self._state = ConnectionState.RECONNECTING
while self._running and self._reconnect_attempts < self.config.max_reconnect_attempts:
self._reconnect_attempts += 1
delay = self.config.reconnect_delay * (2 ** (self._reconnect_attempts - 1))
logger.info(
f"Reconnecting in {delay}s "
f"(attempt {self._reconnect_attempts}/{self.config.max_reconnect_attempts})"
)
await asyncio.sleep(delay)
if await self.connect():
# 再購読
for inst_id, channels in self._subscriptions.items():
for channel in channels:
await self.subscribe(inst_id, channel)
return
self._state = ConnectionState.ERROR
logger.error("Max reconnection attempts reached")
async def disconnect(self) -> None:
"""切断処理"""
self._running = False
if self._ws:
await self._ws.close()
self._ws = None
if self._session:
await self._session.close()
self._session = None
self._state = ConnectionState.DISCONNECTED
logger.info("Disconnected")
@property
def state(self) -> ConnectionState:
return self._state
@property
def is_connected(self) -> bool:
return self._state == ConnectionState.CONNECTED
---
5. リプレイエンジン設計
5.1 Historical Data Replay Architecture
バックテストの精度を左右する最も重要なコンポーネントがリプレイエンジンです。私の設計方針は**イベントドリブンアーキテクチャ**を採用し、実際の取引環境に可能な限り近づけます。
```python
core/replay_engine.py
import asyncio
import aiosqlite
import time
import numpy as np
from typing import List, Dict, Optional, Callable, Any
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from collections import defaultdict
from enum import Enum
import logging
logger = logging.getLogger(__name__)
@dataclass
class BacktestConfig:
"""バックテスト設定"""
start_time: datetime
end_time: datetime
symbols: List[str]
initial_capital: float = 100_000.0 # USDT
commission_rate: float = 0.0005 # 0.05%
slippage_model: str = "fixed" # fixed, linear, sqrt
slippage_bps: float = 1.0 # basis points
funding_rate: float = 0.0001 # 0.01%
leverage: float = 1.0
tick_size: float = 0.1 # 最小価格変動
lot_size: float = 0.01 # 最小数量
@dataclass
class Trade:
"""約定レコード"""
timestamp: int
symbol: str
side: str # open_long, open_short, close_long, close_short
price: float
size: float
commission: float
slippage: float
realized_pnl: float = 0.0
unrealized_pnl: float = 0.0
@dataclass
class Position:
"""ポジション状態"""
symbol: str
side: str # long, short, flat
entry_price: float
size: float
entry_time: int
leverage: float
@property
def notional(self) -> float:
return self.size * self.entry_price * self.leverage
def update_pnl(self, current_price: float) -> float:
if self.side == "long":
return (current_price - self.entry_price) * self.size
elif self.side == "short":
return (self.entry_price - current_price) * self.size
return 0.0
class OrderBookReplayEngine:
"""
板情報リプレイエンジン
特徴:
- HistoricalデータからのTick-by-Tick再生
- シグナル生成と约定シミュレーションの分离
- パフォーマンスプロファイリング対応
"""
def __init__(
self,
config: BacktestConfig,
db_path: str = "data/orderbook_history.db"
):
self.config = config
self.db_path = db_path
# 状態管理
self._current_time: int = 0
self._positions: Dict[str, Position] = {}
self._trades: List[Trade] = []
self._equity_curve: List[Dict] = []
# 板情報キャッシュ
self._orderbooks: Dict[str, Any] = {}
# シグナルコールバック
self._signal_handlers: List[Callable] = []
# パフォーマンス指標
self._stats = {
'total_ticks': 0,
'total_signals': 0,
'total_orders': 0,
'total_trades': 0,
'max_drawdown': 0.0,
'sharpe_ratio': 0.0,
'avg_latency_ms': 0.0,
}
# レイテンシ測定
self._latencies: List[float] = []
def add_signal_handler(self, handler: Callable) -> None:
"""シグナルハンドラ登録"""
self._signal_handlers.append(handler)
async def initialize(self) -> bool:
"""データベース接続とテーブル確認"""
try:
self._db = await aiosqlite.connect(self.db_path)
# テーブル存在確認
cursor = await self._db.execute(
"SELECT name FROM sqlite_master WHERE type='table' "
"AND name='orderbook_snapshots'"
)
exists = await cursor.fetchone()
if not exists:
logger.warning("Database tables not found. Run data ingestion first.")
return False
return True
except Exception as e:
logger.error(f"Initialization failed: {e}")
return False
async def run(self) -> Dict[str, Any]:
"""バックテスト実行"""
logger.info(f"Starting backtest: {self.config.start_time} - {self.config.end_time}")
start_ts = time.perf_counter()
# タイムスタンプ範囲
start_ms = int(self.config.start_time.timestamp() * 1000)
end_ms = int(self.config.end_time.timestamp() * 1000)
# SQLクエリ最適化:パーティションスキャン
query = """
SELECT timestamp, symbol, bids_json, asks_json
FROM orderbook_snapshots
WHERE timestamp >= ? AND timestamp <= ?
AND symbol IN ({})
ORDER BY timestamp ASC
""".format(','.join('?' * len(self.config.symbols)))
params = [start_ms, end_ms] + self.config.symbols
# バッチサイズ設定(メモリ効率と処理速度のトレードオフ)
BATCH_SIZE = 1000
current_batch = []
async with self._db.execute(query, params) as cursor:
while True:
rows = await cursor.fetchmany(BATCH_SIZE)
if not rows:
break
for row in rows:
tick_start = time.perf_counter()
timestamp, symbol, bids_json, asks_json = row
self._current_time = timestamp
# 板情報更新
self._update_orderbook(symbol, bids_json, asks_json)
# シグナル生成
signals = self._generate_signals(symbol)
# 約定シミュレーション
for signal in signals:
self._execute_signal(signal)
# ポジション評価
self._evaluate_positions(symbol)
# パフォーマンス記録
tick_end = time.perf_counter()
self._latencies.append((tick_end - tick_start) * 1000)
self._stats['total_ticks'] += 1
# Equity カーブ記録( каждые 1000 ticks)
if self._stats['total_ticks'] % 1000 == 0:
self._record_equity()
# 最終処理
self._calculate_performance()
await self._cleanup()
total_time = time.perf_counter() - start_ts
logger.info(f"Backtest completed in {total_time:.2f}s")
logger.info(f"Total ticks processed: {self._stats['total_ticks']}")
logger.info(f"Total trades: {self._stats['total_trades']}")
return {
'config': self.config.__dict__,
'stats': self._stats,
'final_equity': self._calculate_equity(),
'trades': self._trades,
'equity_curve': self._equity_curve,
'execution_time_sec': total_time
}
def _update_orderbook(self, symbol: str, bids_json: str, asks_json: str) -> None:
"""板情報更新"""
import msgspec
try:
bids = msgspec.json.decode(bids_json)
asks = msgspec.json.decode(asks_json)
self._orderbooks[symbol] = {'bids': bids, 'asks': asks}
except Exception as e:
logger.debug(f"Failed to parse orderbook: {e}")
def _generate_signals(self, symbol: str) -> List[Dict]:
"""シグナル生成(登録されたハンドラ呼び出し)"""
signals = []
if symbol not in self._orderbooks:
return signals
for handler in self._signal_handlers:
try:
signal = handler(
symbol=symbol,
timestamp=self._current_time,
orderbook=self._orderbooks[symbol],
positions=self._positions
)
if signal:
signals.append(signal)
self._stats['total_signals'] += 1
except Exception as e:
logger.error(f"Signal handler error: {e}")
return signals
def _execute_signal(self, signal: Dict) -> Optional[Trade]:
"""シグナル执行・约约"""
symbol = signal['symbol']
side = signal['side']
size = signal.get('size', 0.01)
if symbol not in self._orderbooks:
return None
ob = self._orderbooks[symbol]
# 市场价格取得
if side in ['buy', 'open_long']:
price = float(ob['asks'][0][0]) if ob['asks'] else 0
else:
price = float(ob['bids'][0][0]) if ob['bids'] else 0
if price == 0:
return None
# Slippage 計算
slippage = self._calculate_slippage(price, side, size)
exec_price = price * (1 + slippage) if side in ['buy', 'open_long'] else price * (1 - slippage)
# 手数料計算
commission = exec_price * size * self.config.commission_rate
# 約定生成
trade = Trade(
timestamp=self._current_time,
symbol=symbol,
side=side,
price=exec_price,
size=size,
commission=commission,
slippage=abs(exec_price - price)
)
self._trades.append(trade)
self._stats['total_trades'] += 1
# ポジション更新
self._update_position(symbol, side, exec_price, size)
return trade
def _calculate_slippage(self, price: float, side: str, size: float) -> float:
"""Slippage モデル"""
if self.config.slippage_model == "fixed":
return self.config.slippage_bps / 10000
elif self.config.slippage_model == "linear":
return (size / 1.0) * self.config.slippage_bps / 10000
elif self.config.slippage_model == "sqrt":
return (size ** 0.5) * self.config.slippage_bps / 10000
return 0.0
def _update_position(
self,
symbol: str,
side: str,
price: float,
size: float
) -> None:
"""ポジショ更新逻辑"""
if symbol not in self._positions:
if side in ['open_long']:
self._positions[symbol] = Position(
symbol=symbol,
side='long',
entry_price=price,
size=size,
entry_time=self._current_time,
leverage=self.config.leverage
)
elif side in ['open_short']:
self._positions[symbol] = Position(
symbol=symbol,
side='short',
entry_price=price,
size=size,
entry_time=self._current_time,
leverage=self.config.leverage
)
else:
pos = self._positions[symbol]
if side == 'close_long' and pos.side == 'long':
# 利確/損切り
pnl = (price - pos.entry_price) * pos.size
trade = self._trades[-1]
trade.realized_pnl = pnl - trade.commission
del self._positions[symbol]
elif side == 'close_short' and pos.side == 'short':
pnl = (pos.entry_price - price) * pos.size
trade = self._trades[-1]
trade.realized_pnl = pnl - trade.commission
del self._positions[symbol]
def _evaluate_positions(self, symbol: str) -> None:
"""未実現损益计算"""
if symbol not in self._positions or symbol not in self._orderbooks:
return
pos = self._positions[symbol]
ob = self._orderbooks[symbol]
if pos.side == 'long':
current_price = float(ob['bids'][0][0]) if ob['bids'] else pos.entry_price
else:
current_price = float(ob['asks'][0][0]) if ob['asks'] else pos.entry_price
pos.unrealized_pnl = pos.update_pnl(current_price)
def _record_equity(self) -> None:
"""Equity カーブ記録"""
equity = self._calculate_equity()
self._equity_curve.append({
'timestamp': self._current_time,
'equity': equity,
'positions': len(self._positions)
})
def _calculate_equity(self) -> float:
"""総资产計算"""
equity = self.config.initial_capital