⚠️【非推奨】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をどうしても使いたい方のみお読みください。
😭「ポジション取得できない!」の原因
FIX 4.4の仕様書を見て、RequestForPositions(35=AN)を組み立てた。
Account, PartyID, PosReqType...全部入れた。完璧だ!
...送信。
Business Reject: Invalid tag (XXX)「は???」
これ、私が3日間ハマった罠です。
cTrader FIX APIは標準FIX 4.4とは大きく異なります。この記事ではその違いと正しい実装方法を解説します!
この記事で学べること
- cTrader特有の制限(標準FIXとの違い)
- RequestForPositionsの正しい実装
- OrderMassStatusRequestの正しい実装
- レスポンスのパース方法
- 「ポジションなし」の判定方法
- シンボル情報取得(SecurityListRequest)← 2026-01-08追加!
cTrader FIX APIの衝撃の事実
標準FIXとの違い一覧
| 機能 | 標準FIX 4.4 | cTrader FIX API |
|---|---|---|
| ポジション照会 | 多数のタグをサポート | タグ710のみ |
| オーダー照会 | Account指定可能 | 全オーダー取得のみ |
| 全シンボル一括取得 | 559=4 (All Securities) | 非対応 |
| 個別シンボル照会 | 559=0 + 55=ID | 対応 ✅ |
RequestForPositionsの衝撃
標準FIX 4.4では以下のタグが使えます:
標準FIX 4.4:├── 1 Account(口座番号)├── 263 SubscriptionRequestType├── 447 PartyIDSource├── 448 PartyID├── 452 PartyRole├── 453 NoPartyIDs├── 710 PosReqID(リクエストID)└── 724 PosReqTypecTrader FIX APIでは:
cTrader FIX API:└── 710 PosReqID(リクエストID)← これだけ!マジで710だけです。
他のタグを入れるとBusiness Rejectが返ってきます。
ポジション照会の実装
RequestForPositions(35=AN)
def request_positions(position_id: str = "") -> str: """ ポジション照会
Args: position_id: 特定ポジションのみ取得する場合(オプション)
Returns: PosReqID(レスポンス照合用) """ pos_req_id = str(int(time.time()))
fields = [ f"710={pos_req_id}", # PosReqID - これだけ! ]
# 特定ポジションのみ取得する場合 if position_id: fields.append(f"721={position_id}")
msg = build_fix_message("AN", fields, "TRADE") send_to_trade_session(msg)
print(f"📋 ポジション照会送信: PosReqID={pos_req_id}") return pos_req_idやってはいけないこと
# ❌ これは全部エラーになるfields = [ "710=12345", "1=5006639", # ← Business Reject! "263=0", # ← Business Reject! "724=0", # ← Business Reject! "453=1", # ← Business Reject! "448=5006639", # ← Business Reject! "447=D", # ← Business Reject! "452=3", # ← Business Reject!]本当に710だけです。 信じてください。
ポジションレスポンスのパース
2種類のレスポンス
ポジション照会には2種類のレスポンスがあります:
| MsgType | 名称 | 状況 |
|---|---|---|
| AP | PositionReport | ポジションがある場合 |
| AO | RequestForPositionsAck | ポジションがない場合 |
PositionReport(35=AP)
主要タグ:┌─────┬────────────────────┬─────────────────────────────┐│ Tag │ 名称 │ 説明 │├─────┼────────────────────┼─────────────────────────────┤│ 55 │ Symbol │ シンボルID(数値) ││ 710 │ PosReqID │ リクエストID ││ 721 │ PosMaintRptID │ ポジションID ││ 727 │ TotalNumPosReports │ 総ポジション数 ││ 728 │ PosReqResult │ 0=成功, 2=ポジションなし ││ 704 │ LongQty │ ロングポジション数量 ││ 705 │ ShortQty │ ショートポジション数量 ││ 730 │ SettlPrice │ 約定価格 │└─────┴────────────────────┴─────────────────────────────┘Positionデータクラス
from dataclasses import dataclassfrom typing import Optional
@dataclassclass Position: """ポジション情報""" position_id: str symbol_id: int side: str # "BUY" or "SELL" volume: float # 数量(Volumeベース) entry_price: float symbol_name: Optional[str] = None
@property def is_long(self) -> bool: return self.side == "BUY"
@property def is_short(self) -> bool: return self.side == "SELL"パース実装
# ポジション保存用positions: dict = {}position_received = threading.Event()
def handle_position_report(fields: dict): """PositionReport(35=AP)処理"""
# 結果確認 pos_req_result = fields.get("728", "") if pos_req_result == "2": # No positions print("📭 ポジションなし") position_received.set() return
# ポジション情報取得 pos_id = fields.get("721", "") symbol_id = int(fields.get("55", "0")) long_qty = float(fields.get("704", "0")) short_qty = float(fields.get("705", "0")) settle_price = float(fields.get("730", "0"))
# 方向判定 if long_qty > 0: side = "BUY" volume = long_qty elif short_qty > 0: side = "SELL" volume = short_qty else: # どちらも0の場合(決済済み等) position_received.set() return
# Positionオブジェクト作成 pos = Position( position_id=pos_id, symbol_id=symbol_id, side=side, volume=volume, entry_price=settle_price, ) positions[pos_id] = pos
print(f"📊 ポジション: ID={pos_id} Symbol={symbol_id} {side} Vol={volume} Price={settle_price}")
# 全ポジション受信完了チェック total = int(fields.get("727", "1")) if len(positions) >= total: position_received.set()
def handle_position_ack(fields: dict): """RequestForPositionsAck(35=AO)処理""" total = int(fields.get("727", "0")) if total == 0: print("📭 ポジションなし") position_received.set()ポジション取得の完全実装
def get_all_positions(timeout: float = 5.0) -> list: """ 全ポジションを取得
Args: timeout: タイムアウト秒数
Returns: list[Position]: ポジションリスト """ positions.clear() position_received.clear()
# リクエスト送信 request_positions()
# レスポンス待機 position_received.wait(timeout)
return list(positions.values())
# 使用例all_positions = get_all_positions()print(f"\n保有ポジション数: {len(all_positions)}")for pos in all_positions: print(f" {pos.side} {pos.volume} @ {pos.entry_price}")オーダー照会の実装
OrderMassStatusRequest(35=AF)
待機中の注文(指値・逆指値)を取得します。
def request_orders(issue_date: str = "") -> str: """ 全オーダー状態取得
Args: issue_date: この日付より前に作成された注文のみ(任意) フォーマット: YYYYMMDD
Returns: MassStatusReqID(レスポンス照合用) """ req_id = str(int(time.time()))
fields = [ f"584={req_id}", # MassStatusReqID "585=7", # MassStatusReqType: All orders ]
# 日付フィルター(オプション) if issue_date: fields.append(f"225={issue_date}")
msg = build_fix_message("AF", fields, "TRADE") send_to_trade_session(msg)
print(f"📋 オーダー照会送信: MassStatusReqID={req_id}") return req_idOrderStatusRequest(35=H)
特定のオーダーのみ取得する場合:
def request_order_status(cl_ord_id: str, symbol_id: int, side: str): """ 特定オーダー状態取得
Args: cl_ord_id: クライアント注文ID symbol_id: シンボルID side: "1"=Buy, "2"=Sell """ fields = [ f"11={cl_ord_id}", f"55={symbol_id}", f"54={side}", ]
msg = build_fix_message("H", fields, "TRADE") send_to_trade_session(msg)レスポンス:ExecutionReport(35=8)
オーダー照会のレスポンスはExecutionReport(35=8)で返ってきます。
@dataclassclass Order: """注文情報""" order_id: str cl_ord_id: str symbol_id: int side: str # "BUY" or "SELL" ord_type: str # "MARKET", "LIMIT", "STOP" volume: float price: Optional[float] = None # 指値価格 stop_price: Optional[float] = None # 逆指値価格 status: str = "" # "NEW", "FILLED", "CANCELED"
# オーダー保存用orders: dict = {}order_received = threading.Event()
def handle_execution_report(fields: dict): """ExecutionReport(35=8)処理""" exec_type = fields.get("150", "") cl_ord_id = fields.get("11", "") order_id = fields.get("37", "") symbol_id = int(fields.get("55", "0")) side = fields.get("54", "") ord_status = fields.get("39", "")
# 方向変換 side_str = "BUY" if side == "1" else "SELL"
# 注文タイプ変換 ord_type = fields.get("40", "") ord_type_str = { "1": "MARKET", "2": "LIMIT", "3": "STOP", "4": "STOP_LIMIT", }.get(ord_type, ord_type)
# Orderオブジェクト作成 order = Order( order_id=order_id, cl_ord_id=cl_ord_id, symbol_id=symbol_id, side=side_str, ord_type=ord_type_str, volume=float(fields.get("38", "0")), price=float(fields.get("44", "0")) if fields.get("44") else None, stop_price=float(fields.get("99", "0")) if fields.get("99") else None, status=ord_status, ) orders[cl_ord_id] = order
# ExecType別ログ if exec_type == "F": # Trade (約定) price = fields.get("31", fields.get("6", "")) qty = fields.get("32", "") pos_id = fields.get("721", "") print(f"✅ 約定: {side_str} {qty}@{price} PosID={pos_id}")
elif exec_type == "0": # New print(f"📝 注文受付: ClOrdID={cl_ord_id}") order_received.set()
elif exec_type == "8": # Rejected reason = fields.get("58", "Unknown") print(f"❌ 注文拒否: {reason}")
elif exec_type == "4": # Canceled print(f"🗑️ 注文キャンセル: ClOrdID={cl_ord_id}")オーダーがない場合
オーダーがない場合はBusiness Rejectが返ります:
35=j (BusinessReject)58=ORDER_NOT_FOUND:no orders founddef handle_business_reject(fields: dict): """BusinessReject(35=j)処理""" reason = fields.get("58", "Unknown")
if "no orders found" in reason.lower(): print("📭 待機中オーダーなし") order_received.set() else: print(f"⚠️ ビジネスリジェクト: {reason}")シンボル情報取得(SecurityListRequest)
衝撃の発見:シンボル名は取得できる!
最初は「全シンボル一覧が取れない」と絶望していましたが、個別シンボルの情報は取得可能でした。
SecurityListRequest(35=x)
def request_symbol_info(symbol_id: int) -> str: """ シンボル情報を取得
Args: symbol_id: シンボルID(例: 41=XAUUSD)
Returns: SecurityReqID(レスポンス照合用) """ req_id = str(int(time.time() * 1000))
fields = [ f"320={req_id}", # SecurityReqID "559=0", # SecurityListRequestType: Symbol f"55={symbol_id}", # Symbol (ID指定) ]
msg = build_fix_message("x", fields, "TRADE") send_to_trade_session(msg)
return req_idSecurityList(35=y)レスポンス
主要タグ:┌──────┬─────────────────┬─────────────────────────────┐│ Tag │ 名称 │ 説明 │├──────┼─────────────────┼─────────────────────────────┤│ 55 │ Symbol │ シンボルID ││ 1007 │ SymbolName │ シンボル名(例: XAUUSD) ││ 1008 │ Digits │ 小数点以下桁数 ││ 320 │ SecurityReqID │ リクエストID │└──────┴─────────────────┴─────────────────────────────┘シンボル情報データクラス
@dataclassclass SymbolInfo: """シンボル情報""" symbol_id: int symbol_name: str digits: int
def handle_security_list(fields: dict) -> Optional[SymbolInfo]: """SecurityList(35=y)処理""" symbol_id = int(fields.get("55", "0")) symbol_name = fields.get("1007", "") digits = int(fields.get("1008", "0"))
if symbol_name: return SymbolInfo( symbol_id=symbol_id, symbol_name=symbol_name, digits=digits, ) return Noneシンボルスキャナーの実装
どのシンボルIDが何の通貨ペアかを調べるスキャナー:
def scan_symbols(start_id: int = 1, end_id: int = 200): """シンボルIDをスキャンして一覧を取得"""
found_symbols = []
for symbol_id in range(start_id, end_id + 1): info = request_and_wait_symbol_info(symbol_id)
if info and info.symbol_name: found_symbols.append(info) print(f"ID={info.symbol_id:3d} {info.symbol_name:15s} Digits={info.digits}")
time.sleep(0.1) # レート制限対策
return found_symbols
# 実行結果(Fundoraの場合)# ID= 1 EURUSD Digits=5# ID= 2 GBPUSD Digits=5# ID= 41 XAUUSD Digits=2# ...Fundoraの主要シンボルID一覧
実際にスキャンした結果(2026-01-08時点):
| ID | シンボル | Digits | コントラクトサイズ |
|---|---|---|---|
| 1 | EURUSD | 5 | 100,000 |
| 2 | GBPUSD | 5 | 100,000 |
| 3 | EURJPY | 3 | 100,000 |
| 4 | USDJPY | 3 | 100,000 |
| 41 | XAUUSD | 2 | 100 |
| 42 | XAGUSD | 3 | 5,000 |
| 87 | BTCUSD | 2 | 1 |
| 88 | ETHUSD | 2 | 1 |
注意: コントラクトサイズはFIX APIでは取得できません!
上記の値は別途調査した結果です。
取得できる情報 vs 取得できない情報
| 情報 | FIX API | 備考 |
|---|---|---|
| シンボルID | ✅ | Tag 55 |
| シンボル名 | ✅ | Tag 1007 |
| 桁数(Digits) | ✅ | Tag 1008 |
| コントラクトサイズ | ❌ | Open APIでのみ取得可能 |
| ロットステップ | ❌ | Open APIでのみ取得可能 |
| スワップレート | ❌ | Open APIでのみ取得可能 |
完全動作するサンプルコード
import timeimport threading
def demo_position_order_query(): """ポジション・オーダー照会デモ"""
# 接続 connect() send_logon("TRADE") wait_for_logon("TRADE")
# 受信スレッド開始 start_receiver_thread()
print("\n" + "="*50) print("📋 ポジション照会") print("="*50)
positions.clear() position_received.clear() request_positions() position_received.wait(5.0)
if positions: for pos in positions.values(): lot_size = pos.volume / 100 # XAUUSDの場合 print(f" 📊 {pos.side} {lot_size:.2f}ロット @ {pos.entry_price}") else: print(" 📭 保有ポジションなし")
print("\n" + "="*50) print("📋 オーダー照会") print("="*50)
orders.clear() order_received.clear() request_orders()
# オーダーはBusinessRejectが返ることもあるので少し待つ time.sleep(2)
if orders: for order in orders.values(): print(f" 📝 {order.side} {order.ord_type} @ {order.price or order.stop_price}") else: print(" 📭 待機中オーダーなし")
# 切断 disconnect()
if __name__ == "__main__": demo_position_order_query()実行結果(Fundoraライブ口座)
✅ TRADEセッション ログオン成功
==================================================📋 ポジション照会==================================================📊 ポジション: ID=123456789 Symbol=41 BUY Vol=1 Price=2645.50 📊 BUY 0.01ロット @ 2645.50
==================================================📋 オーダー照会================================================== 📭 待機中オーダーなし
FIX切断完了よくあるエラーと対処法
1. Business Reject: Invalid tag
原因: 標準FIXのタグを使っている対処: RequestForPositionsはタグ710のみ使用2. タイムアウト(レスポンスが来ない)
# 確認事項:# 1. TRADEセッションにログオンしているか?# 2. 受信スレッドが動いているか?# 3. メッセージタイプ(AP, AO, j)を正しく処理しているか?3. ポジション数が合わない
# TotalNumPosReports (タグ727) を確認# 複数ポジションがある場合、複数のAPメッセージが返るtotal = int(fields.get("727", "1"))if len(positions) >= total: # 全ポジション受信完了 position_received.set()Volume ↔ ロット変換
PositionReportのLongQty/ShortQtyはVolume(基準単位数)で返ってきます。
# ロット数に変換するには契約サイズで割るCONTRACT_SIZES = { "XAUUSD": 100, "FX": 100000, "XAGUSD": 5000,}
def volume_to_lot(volume: float, symbol_name: str) -> float: """VolumeからLotに変換""" contract_size = CONTRACT_SIZES.get(symbol_name, 100000) return volume / contract_size
# 例: XAUUSD Volume=1 → 0.01ロットlot = volume_to_lot(1, "XAUUSD") # 0.01まとめ
今日学んだこと
- cTrader FIX APIは標準FIXと大きく異なる
- RequestForPositionsはタグ710のみ(他のタグは全てエラー)
- ポジションなしはAO(RequestForPositionsAck)または728=2で判定
- オーダーなしはBusinessReject (35=j)で返る
- Volume→ロット変換には契約サイズが必要
- シンボル名はSecurityListRequest(35=x)で取得可能(コントラクトサイズは不可)
次回予告
次回「FIX API完全版④ エントリー・決済完全実装」では:
- NewOrderSingle(新規注文)
- 成行/指値/逆指値の違い
- ポジション決済の方法
- ロット数→Volume変換の落とし穴
を解説します!
関連記事
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。