⚠️【非推奨】FIX API完全版②|リアルタイム価格取得→ Python cBotを使おう
🚨 重要:FIX APIは非推奨です
この記事で紹介しているFIX APIは、以下の理由から非推奨です:
- 😱 17個のハマりポイントがあり、ドキュメントに書いてない罠が多すぎる
- 💀 実装に1週間以上かかる(Python cBotなら数時間)
- 🤯 800行以上のコードが必要(Python cBotなら数十行)
- ❌ コントラクトサイズ等の情報が取得できない
🐍 代わりにPython cBotを使いましょう!
cTrader 5.4以降、ネイティブPythonサポートが追加されました。
👉 Python cBot入門ガイド - 1行で注文できる世界へ
👉 cTrader 5.4 Python完全ガイド - 環境構築から実践まで
以下の内容は参考資料として残しています。FIX APIをどうしても使いたい方のみお読みください。
😤「Bid/Askが取れない!」3時間の格闘
FIX APIで接続成功した!ログオンも通った!
...でも価格が取れない。
「MarketDataRequest送ったのにレスポンスがない」 「Rejectが返ってくるけど理由がわからない」 「QUOTEセッションって何?」
私も最初は意味不明でした。TRADEセッションでMarketDataRequestを送って「なんで動かんのや!」と叫んでましたから 😭
実は、cTrader FIX APIで価格データを取得するにはQUOTEセッションという別のセッションを使う必要があります。この記事ではその全てを解説します!
この記事で学べること
- QUOTEセッションとは何か(TRADEセッションとの違い)
- MarketDataRequestの正しい送り方
- レスポンスのパース方法(Bid/Ask抽出)
- スプレッド計算の実装
- 完全動作するPythonコード
前回の記事「FIX API完全版①」で2セッション構成を理解している前提で進めます。
QUOTEセッションの基礎知識
2セッションおさらい
cTrader FIX APIには2つのセッションがあります:
| セッション | ポート | 用途 |
|---|---|---|
| TRADE | 5212 | 注文・ポジション管理 |
| QUOTE | 5211 | 価格データ(Bid/Ask) |
超重要: 価格データはQUOTEセッションでしか取得できません!
TRADEセッションでMarketDataRequestを送っても無視されるか、リジェクトされます。
なぜ分離されているのか
理由:├── 負荷分散: 価格データは高頻度(秒間数十回更新)├── セキュリティ: 価格は読み取り専用、注文は書き込み└── 信頼性: 価格系が落ちても注文は継続可能プロップファームやブローカーにとって、これは理にかなった設計です。
QUOTEセッションへの接続
ログオンメッセージ
TRADEセッションとほぼ同じですが、TargetSubIDをQUOTEに変更:
def send_quote_logon(): """QUOTEセッションにログオン""" fields = [ "98=0", # EncryptMethod "108=30", # HeartBtInt "141=Y", # ResetSeqNumFlag "553=5006639", # Username (口座番号) "554=YourFIXPassword", # Password (FIX専用) ]
# ヘッダーのSubIDをQUOTEに! header = { "49": "live.fundoratrading.5006639", # SenderCompID "56": "cServer", # TargetCompID "57": "QUOTE", # TargetSubID ← これ重要! "50": "QUOTE", # SenderSubID }
msg = build_fix_message("A", fields, header) quote_socket.send(msg)接続シーケンス
推奨接続順序:1. TRADEセッション接続2. TRADEログオン成功確認3. QUOTEセッション接続4. QUOTEログオン成功確認5. マーケットデータ購読開始TRADE→QUOTEの順番を守ると安定します。
MarketDataRequest(35=V)の送信
必須フィールド
QUOTEセッションにログオン成功したら、価格を取得したいシンボルを「購読」します:
┌─────┬──────────────────────────┬───────────────────────────┐│ Tag │ 名称 │ 値・説明 │├─────┼──────────────────────────┼───────────────────────────┤│ 262 │ MDReqID │ 一意のリクエストID ││ 263 │ SubscriptionRequestType │ 1=購読, 2=購読解除 ││ 264 │ MarketDepth │ 1 (Top of Book) ││ 265 │ MDUpdateType │ 1 (Incremental Refresh) ││ 267 │ NoMDEntryTypes │ 2 (Bid + Ask) ││ 269 │ MDEntryType │ 0 (Bid) ││ 269 │ MDEntryType │ 1 (Offer/Ask) ││ 146 │ NoRelatedSym │ 1 ││ 55 │ Symbol │ シンボルID(数値!) │└─────┴──────────────────────────┴───────────────────────────┘ハマりポイント:シンボルIDは数値
# ❌ これは動かない"55=XAUUSD"
# ✅ 正しい(数値IDを指定)"55=41"シンボルIDの調べ方は前回記事を参照してください。
Python実装
def subscribe_market_data(symbol_id: int) -> str: """ マーケットデータを購読
Args: symbol_id: シンボルID(数値)例: 41=XAUUSD
Returns: MDReqID(レスポンス照合用) """ # 一意のリクエストID md_req_id = f"MD{int(time.time())}"
fields = [ f"262={md_req_id}", # MDReqID "263=1", # Subscribe (購読開始) "264=1", # MarketDepth: Top of book "265=1", # MDUpdateType: Incremental "267=2", # NoMDEntryTypes: 2種類 "269=0", # MDEntryType: Bid "269=1", # MDEntryType: Ask/Offer "146=1", # NoRelatedSym: 1シンボル f"55={symbol_id}", # Symbol: 数値ID ]
msg = build_fix_message("V", fields, "QUOTE") send_to_quote_session(msg)
print(f"✅ マーケットデータ購読: Symbol ID={symbol_id}") return md_req_id購読解除
def unsubscribe_market_data(symbol_id: int): """マーケットデータ購読解除""" md_req_id = f"MDU{int(time.time())}"
fields = [ f"262={md_req_id}", "263=2", # ← 2 = 購読解除 "264=1", "265=1", "267=2", "269=0", "269=1", "146=1", f"55={symbol_id}", ]
msg = build_fix_message("V", fields, "QUOTE") send_to_quote_session(msg)レスポンスのパース
2種類のレスポンス
購読後、サーバーから価格データが送られてきます:
| MsgType | 名称 | 説明 |
|---|---|---|
| W | MarketDataSnapshotFullRefresh | 最初の完全スナップショット |
| X | MarketDataIncrementalRefresh | 以降の差分更新 |
どちらも同じ形式でBid/Ask価格が含まれています。
レスポンス構造
35=W (または X)├── 55 = シンボルID├── 268 = NoMDEntries (エントリー数)├── 269 = MDEntryType (0=Bid, 1=Ask)├── 270 = MDEntryPx (価格)├── 269 = MDEntryType (次のエントリー)└── 270 = MDEntryPx (価格)実際のレスポンス例
8=FIX.4.4|9=143|35=W|49=cServer|56=live.fundoratrading.5006639|34=5|52=20260107-03:45:12.123|55=41|262=MD1736222712|268=2|269=0|270=2648.31|269=1|270=2648.61|10=187|解読:
- シンボルID: 41 (XAUUSD)
- Bid: 2648.31
- Ask: 2648.61
- スプレッド: 30ポイント
Pythonパース実装
from dataclasses import dataclassfrom datetime import datetime, timezonefrom typing import Optional
@dataclassclass Quote: """価格情報""" symbol_id: int bid: Optional[float] = None ask: Optional[float] = None timestamp: Optional[datetime] = None
@property def spread(self) -> Optional[float]: """スプレッド(ポイント)""" if self.bid is None or self.ask is None: return None return (self.ask - self.bid) * 100 # ポイント換算
def parse_market_data(raw_msg: str, quotes_store: dict) -> Optional[Quote]: """ MarketDataメッセージをパース
Args: raw_msg: 生のFIXメッセージ quotes_store: 価格保存用辞書 {symbol_id: Quote}
Returns: 更新されたQuoteオブジェクト """ SOH = "\x01"
# フィールドをパース fields = {} for field in raw_msg.split(SOH): if "=" in field: tag, value = field.split("=", 1) fields[tag] = value
# メッセージタイプ確認 msg_type = fields.get("35", "") if msg_type not in ("W", "X"): return None
# シンボルID取得 symbol_id = int(fields.get("55", "0")) if symbol_id <= 0: return None
# 既存のQuoteを取得または新規作成 quote = quotes_store.get(symbol_id, Quote(symbol_id=symbol_id))
# 269タグ(MDEntryType)を解析 # 複数のエントリーがあるため、rawメッセージを分割 entries = raw_msg.split("269=")
for entry in entries[1:]: # 最初の要素はヘッダー if entry.startswith("0"): # Bid # 270=xxxを抽出 px_start = entry.find("270=") if px_start != -1: px_end = entry.find(SOH, px_start) if px_end != -1: try: quote.bid = float(entry[px_start+4:px_end]) except ValueError: pass
elif entry.startswith("1"): # Ask px_start = entry.find("270=") if px_start != -1: px_end = entry.find(SOH, px_start) if px_end != -1: try: quote.ask = float(entry[px_start+4:px_end]) except ValueError: pass
# タイムスタンプ更新 quote.timestamp = datetime.now(timezone.utc)
# ストアに保存 quotes_store[symbol_id] = quote
return quoteリアルタイム価格取得の完全実装
Quoteクラスの活用
# グローバルな価格ストアquotes: dict = {}
def on_quote_message(raw_msg: str): """QUOTEセッションからのメッセージ処理""" fields = parse_fields(raw_msg) msg_type = fields.get("35", "")
if msg_type in ("W", "X"): # MarketData quote = parse_market_data(raw_msg, quotes) if quote: print(f"💰 {quote.symbol_id}: Bid={quote.bid} Ask={quote.ask} Spread={quote.spread:.1f}pt")
# コールバック実行 if on_quote_callback: on_quote_callback(quote)
elif msg_type == "Y": # MarketDataRequestReject reason = fields.get("58", "Unknown") print(f"❌ マーケットデータ拒否: {reason}")
# 価格取得ヘルパー関数def get_bid(symbol_id: int) -> Optional[float]: """Bid価格を取得""" quote = quotes.get(symbol_id) return quote.bid if quote else None
def get_ask(symbol_id: int) -> Optional[float]: """Ask価格を取得""" quote = quotes.get(symbol_id) return quote.ask if quote else None
def get_spread(symbol_id: int) -> Optional[float]: """スプレッド(ポイント)を取得""" quote = quotes.get(symbol_id) return quote.spread if quote else None動作確認コード
import time
def main(): # 1. TRADEセッション接続 trade_socket = connect_session("TRADE", 5212) send_logon(trade_socket, "TRADE") wait_for_logon("TRADE")
# 2. QUOTEセッション接続 quote_socket = connect_session("QUOTE", 5211) send_logon(quote_socket, "QUOTE") wait_for_logon("QUOTE")
# 3. 受信スレッド開始 start_receiver_thread(quote_socket, "QUOTE")
# 4. マーケットデータ購読 subscribe_market_data(41) # XAUUSD
# 5. 価格表示ループ print("\n📊 リアルタイム価格モニタリング開始...") for i in range(30): quote = quotes.get(41) if quote and quote.bid and quote.ask: print(f"XAUUSD: Bid={quote.bid:.2f} Ask={quote.ask:.2f} Spread={quote.spread:.1f}pt") time.sleep(1)
# 6. 購読解除・切断 unsubscribe_market_data(41) disconnect()
if __name__ == "__main__": main()実行結果(Fundoraライブ口座で検証)
✅ TRADEセッション ログオン成功✅ QUOTEセッション ログオン成功✅ マーケットデータ購読: Symbol ID=41
📊 リアルタイム価格モニタリング開始...💰 41: Bid=2648.31 Ask=2648.61 Spread=30.0pt💰 41: Bid=2648.33 Ask=2648.63 Spread=30.0pt💰 41: Bid=2648.35 Ask=2648.65 Spread=30.0pt...よくあるエラーと対処法
1. MarketDataRequestReject (35=Y)
原因:├── シンボルIDが文字列("XAUUSD")になっている├── シンボルIDが存在しない├── QUOTEセッションにログインしていない└── TRADEセッションに送信している対処: シンボルIDを数値で指定し、QUOTEセッションに送信
2. レスポンスが来ない
# 確認事項:# 1. QUOTEセッションにログオンしているか?if not quote_logged_in: print("QUOTEセッション未接続!")
# 2. 受信スレッドが動いているか?# 3. ソケットがブロッキングモードになっていないか?3. Bid/Askが片方だけNone
原因:├── パースロジックのバグ├── サーバーがまだ完全なスナップショットを送っていない└── 市場が閉まっている(週末など)対処: 数秒待ってから再度確認、週末は価格更新がないことを考慮
スプレッド監視Bot例
実用的な例として、スプレッド監視Botを作ってみましょう:
class SpreadMonitor: """スプレッド監視クラス"""
def __init__(self, client, symbol_id: int, alert_threshold: float = 50.0): self.client = client self.symbol_id = symbol_id self.alert_threshold = alert_threshold # ポイント self.max_spread = 0.0 self.min_spread = float('inf')
def on_quote(self, quote: Quote): """価格更新時のコールバック""" if quote.symbol_id != self.symbol_id: return
spread = quote.spread if spread is None: return
# 最大・最小更新 self.max_spread = max(self.max_spread, spread) self.min_spread = min(self.min_spread, spread)
# アラート if spread > self.alert_threshold: print(f"⚠️ スプレッド拡大警告! {spread:.1f}pt")
def report(self): """レポート出力""" print(f"\n📊 スプレッドレポート:") print(f" 最小: {self.min_spread:.1f}pt") print(f" 最大: {self.max_spread:.1f}pt")
# 使用例monitor = SpreadMonitor(client, symbol_id=41, alert_threshold=40.0)client.set_on_quote(monitor.on_quote)
# 1時間監視後monitor.report()まとめ
今日学んだこと
- QUOTEセッションで価格データを取得
- **MarketDataRequest (35=V)**で購読開始
- シンボルIDは数値で指定
- W/XメッセージでBid/Askを受信
- スプレッド計算:
(Ask - Bid) × 100
次回予告
次回「FIX API完全版③ ポジション・オーダー照会編」では:
- RequestForPositions(ポジション照会)
- OrderMassStatusRequest(オーダー照会)
- cTrader独自の制限と対処法
を解説します!
関連記事
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。