Web Lukes
📄

⚠️【非推奨】FIX API完全版③|ポジション照会→ Python cBotを使おう

公開日: 2026/1/8

🚨 重要: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...全部入れた。完璧だ!

...送信。

Code
Business Reject: Invalid tag (XXX)

「は???」

これ、私が3日間ハマった罠です。

cTrader FIX APIは標準FIX 4.4とは大きく異なります。この記事ではその違いと正しい実装方法を解説します!


この記事で学べること

  1. cTrader特有の制限(標準FIXとの違い)
  2. RequestForPositionsの正しい実装
  3. OrderMassStatusRequestの正しい実装
  4. レスポンスのパース方法
  5. 「ポジションなし」の判定方法
  6. シンボル情報取得(SecurityListRequest)← 2026-01-08追加!

cTrader FIX APIの衝撃の事実

標準FIXとの違い一覧

機能標準FIX 4.4cTrader FIX API
ポジション照会多数のタグをサポートタグ710のみ
オーダー照会Account指定可能全オーダー取得のみ
全シンボル一括取得559=4 (All Securities)非対応
個別シンボル照会559=0 + 55=ID対応 ✅

RequestForPositionsの衝撃

標準FIX 4.4では以下のタグが使えます:

Code
標準FIX 4.4:
├── 1 Account(口座番号)
├── 263 SubscriptionRequestType
├── 447 PartyIDSource
├── 448 PartyID
├── 452 PartyRole
├── 453 NoPartyIDs
├── 710 PosReqID(リクエストID)
└── 724 PosReqType

cTrader FIX APIでは:

Code
cTrader FIX API:
└── 710 PosReqID(リクエストID)← これだけ!

マジで710だけです。

他のタグを入れるとBusiness Rejectが返ってきます。


ポジション照会の実装

RequestForPositions(35=AN)

Python
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

やってはいけないこと

Python
# ❌ これは全部エラーになる
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名称状況
APPositionReportポジションがある場合
AORequestForPositionsAckポジションがない場合

PositionReport(35=AP)

Code
主要タグ:
┌─────┬────────────────────┬─────────────────────────────┐
│ Tag │ 名称 │ 説明 │
├─────┼────────────────────┼─────────────────────────────┤
│ 55 │ Symbol │ シンボルID(数値) │
│ 710 │ PosReqID │ リクエストID │
│ 721 │ PosMaintRptID │ ポジションID │
│ 727 │ TotalNumPosReports │ 総ポジション数 │
│ 728 │ PosReqResult │ 0=成功, 2=ポジションなし │
│ 704 │ LongQty │ ロングポジション数量 │
│ 705 │ ShortQty │ ショートポジション数量 │
│ 730 │ SettlPrice │ 約定価格 │
└─────┴────────────────────┴─────────────────────────────┘

Positionデータクラス

Python
from dataclasses import dataclass
from typing import Optional
@dataclass
class 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"

パース実装

Python
# ポジション保存用
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()

ポジション取得の完全実装

Python
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)

待機中の注文(指値・逆指値)を取得します。

Python
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_id

OrderStatusRequest(35=H)

特定のオーダーのみ取得する場合:

Python
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)で返ってきます。

Python
@dataclass
class 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が返ります:

Code
35=j (BusinessReject)
58=ORDER_NOT_FOUND:no orders found
Python
def 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)

Python
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_id

SecurityList(35=y)レスポンス

Code
主要タグ:
┌──────┬─────────────────┬─────────────────────────────┐
│ Tag │ 名称 │ 説明 │
├──────┼─────────────────┼─────────────────────────────┤
│ 55 │ Symbol │ シンボルID │
│ 1007 │ SymbolName │ シンボル名(例: XAUUSD) │
│ 1008 │ Digits │ 小数点以下桁数 │
│ 320 │ SecurityReqID │ リクエストID │
└──────┴─────────────────┴─────────────────────────────┘

シンボル情報データクラス

Python
@dataclass
class 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が何の通貨ペアかを調べるスキャナー:

Python
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コントラクトサイズ
1EURUSD5100,000
2GBPUSD5100,000
3EURJPY3100,000
4USDJPY3100,000
41XAUUSD2100
42XAGUSD35,000
87BTCUSD21
88ETHUSD21

注意: コントラクトサイズはFIX APIでは取得できません!

上記の値は別途調査した結果です。

取得できる情報 vs 取得できない情報

情報FIX API備考
シンボルID✅Tag 55
シンボル名✅Tag 1007
桁数(Digits)✅Tag 1008
コントラクトサイズ❌Open APIでのみ取得可能
ロットステップ❌Open APIでのみ取得可能
スワップレート❌Open APIでのみ取得可能

完全動作するサンプルコード

Python
import time
import 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ライブ口座)

Code
✅ TRADEセッション ログオン成功
==================================================
📋 ポジション照会
==================================================
📊 ポジション: ID=123456789 Symbol=41 BUY Vol=1 Price=2645.50
📊 BUY 0.01ロット @ 2645.50
==================================================
📋 オーダー照会
==================================================
📭 待機中オーダーなし
FIX切断完了

よくあるエラーと対処法

1. Business Reject: Invalid tag

Code
原因: 標準FIXのタグを使っている
対処: RequestForPositionsはタグ710のみ使用

2. タイムアウト(レスポンスが来ない)

Python
# 確認事項:
# 1. TRADEセッションにログオンしているか?
# 2. 受信スレッドが動いているか?
# 3. メッセージタイプ(AP, AO, j)を正しく処理しているか?

3. ポジション数が合わない

Python
# TotalNumPosReports (タグ727) を確認
# 複数ポジションがある場合、複数のAPメッセージが返る
total = int(fields.get("727", "1"))
if len(positions) >= total:
# 全ポジション受信完了
position_received.set()

Volume ↔ ロット変換

PositionReportのLongQty/ShortQtyはVolume(基準単位数)で返ってきます。

Python
# ロット数に変換するには契約サイズで割る
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

まとめ

今日学んだこと

  1. cTrader FIX APIは標準FIXと大きく異なる
  2. RequestForPositionsはタグ710のみ(他のタグは全てエラー)
  3. ポジションなしはAO(RequestForPositionsAck)または728=2で判定
  4. オーダーなしはBusinessReject (35=j)で返る
  5. Volume→ロット変換には契約サイズが必要
  6. シンボル名はSecurityListRequest(35=x)で取得可能(コントラクトサイズは不可)

次回予告

次回「FIX API完全版④ エントリー・決済完全実装」では:

  • NewOrderSingle(新規注文)
  • 成行/指値/逆指値の違い
  • ポジション決済の方法
  • ロット数→Volume変換の落とし穴

を解説します!


関連記事

🎁おすすめツール・サービス

FX自動売買VPSなら50%OFF

キャンペーンで月1,790円〜。MT4/MT5プリインストール済み、自動復旧機能付き。14日間無料お試し。

シンクラウドデスクトップ for FX
ゴトー日EAを探しているならソースコード付

4年で資金4.7倍のバックテスト実績。ソースコード付きで自由にカスタマイズ可能。

ゴトー日EA Ultimate版(3,980円)

※上記リンクにはプロモーション(広告)が含まれています

🔮
MEMBERSHIP

量子相場謀略|ホソノP

ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。

✓EAソースコード公開
✓バックテスト生データ共有
✓リアルタイム相場分析
✓メンバー限定記事
メンバーシップに参加する※ noteアカウントで簡単登録

📚あわせて読みたい

📄

⚠️【非推奨】FIX API完全版⑤|全機能クラス→ Python cBotを使おう

【非推奨】FIX APIは17個のハマりポイントがあり初心者には推奨しません。代わりにPython cBotを使いましょう。この記事は参考資料として残しています。

ctrader2026/1/8
📄

⚠️【非推奨】FIX API完全版④|エントリー・決済→ Python cBotを使おう

【非推奨】FIX APIは17個のハマりポイントがあり初心者には推奨しません。代わりにPython cBotを使いましょう。この記事は参考資料として残しています。

ctrader2026/1/8
📄

⚠️【非推奨】FIX API完全版②|リアルタイム価格取得→ Python cBotを使おう

【非推奨】FIX APIは17個のハマりポイントがあり初心者には推奨しません。代わりにPython cBotを使いましょう。この記事は参考資料として残しています。

ctrader2026/1/8
📄

🚀【神機能】cTrader Open APIはプラットフォーム不要!MT5との決定的な違いとおすすめブローカー・プロップファーム

cTrader Open APIはMT5と違ってデスクトップアプリ不要!Pythonだけで直接サーバーに接続できる革命的なAPI。VPSでヘッドレス運用したいトレーダー必見。日本人におすすめのcTraderブローカーとプロップファームも徹底解説。

ctrader2026/1/6
📄

⚠️【非推奨】FIX API入門②|PythonでLogon→ Python cBotを使おう

【非推奨】FIX APIは17個のハマりポイントがあり初心者には推奨しません。代わりにPython cBotを使いましょう。この記事は参考資料として残しています。

fx2026/1/8
📄

🚨【結論】FIX APIは非推奨!Python cBotを使いましょう - 17個の地獄を経験した結論

FIX APIは17個のハマりポイントがあり非推奨です。cTrader 5.4以降のPython cBotなら1行で注文可能。FIX APIで813行書いた経験者が断言します。

fx2026/1/8