⚠️【非推奨】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をどうしても使いたい方のみお読みください。
😤「0.01ロット注文したのに1ロット約定した!」
FIX APIで注文送信、ExecutionReportが返ってきた。
約定!...と思ったら100倍のポジションになってる!
「OrderQtyに0.01って入れたのに!」
これ、私がFundoraのライブ口座で実際にやらかした事件です 😭
OrderQtyはロット数じゃなくてVolumeです。
この記事では、この落とし穴と正しい注文実装を完全解説します!
この記事で学べること
- OrderQtyとVolumeの正体
- ロット数→Volume変換の正しい方法
- 成行/指値/逆指値注文の実装
- ポジション決済の実装
- ExecutionReportの処理
- 注文キャンセルの方法
最重要:OrderQty(タグ38)の真実
ロット数ではない!
標準的なMT4/MT5では「0.01」と指定すれば0.01ロット。
cTrader FIX APIは違います。
OrderQty = ロット数 × 契約サイズ契約サイズ一覧
| 商品 | 契約サイズ | 0.01ロットのOrderQty |
|---|---|---|
| FX通貨ペア | 100,000 | 1,000 |
| XAUUSD(ゴールド) | 100 | 1 |
| XAGUSD(シルバー) | 5,000 | 50 |
計算例
# EURUSD 0.01ロットorder_qty = 0.01 * 100000 # = 1000
# XAUUSD 0.01ロットorder_qty = 0.01 * 100 # = 1
# XAUUSD 0.1ロットorder_qty = 0.1 * 100 # = 10
# XAUUSD 1ロットorder_qty = 1.0 * 100 # = 100私の失敗談
# ❌ これをやってしまった"38=0.01" # ← 0.01 Volumeになる = ほぼ0
# ❌ これもダメ"38=1" # ← XAUUSD 0.01ロットのつもりが... # EURUSDだと 1/100000 = 0.00001ロット相当
# ✅ 正しい(XAUUSD 0.01ロット)"38=1" # Volume = 0.01 * 100 = 1変換関数の実装
class ContractSize: """契約サイズ定義""" FX = 100_000 XAUUSD = 100 XAGUSD = 5_000 XPTUSD = 100 XPDUSD = 100
def lot_to_volume(lot_size: float, contract_size: float) -> int: """ ロット数をVolumeに変換
Args: lot_size: ロット数 (例: 0.01) contract_size: 契約サイズ (例: 100 for XAUUSD)
Returns: int: Volume (OrderQtyに指定する値)
Examples: >>> lot_to_volume(0.01, 100000) # EURUSD 0.01ロット 1000 >>> lot_to_volume(0.01, 100) # XAUUSD 0.01ロット 1 >>> lot_to_volume(1.0, 100000) # EURUSD 1ロット 100000 """ return int(lot_size * contract_size)
def volume_to_lot(volume: int, contract_size: float) -> float: """ VolumeからロットNumに変換
Args: volume: Volume contract_size: 契約サイズ
Returns: float: ロット数 """ return volume / contract_size
# 使用例xauusd_volume = lot_to_volume(0.01, ContractSize.XAUUSD)print(f"XAUUSD 0.01ロット = Volume {xauusd_volume}") # 1
eurusd_volume = lot_to_volume(0.01, ContractSize.FX)print(f"EURUSD 0.01ロット = Volume {eurusd_volume}") # 1000NewOrderSingle(35=D)の実装
必須フィールド
┌─────┬──────────────────┬──────────────────────────────────┐│ Tag │ 名称 │ 説明 │├─────┼──────────────────┼──────────────────────────────────┤│ 11 │ ClOrdID │ クライアント注文ID(一意) ││ 55 │ Symbol │ シンボルID(数値!) ││ 54 │ Side │ 1=Buy, 2=Sell ││ 38 │ OrderQty │ Volume(ロット数ではない!) ││ 40 │ OrdType │ 1=成行, 2=指値, 3=逆指値 ││ 60 │ TransactTime │ 送信時刻(UTC) ││ 59 │ TimeInForce │ 1=GTC, 3=IOC │└─────┴──────────────────┴──────────────────────────────────┘追加フィールド(条件付き)
┌─────┬──────────────────┬──────────────────────────────────┐│ Tag │ 名称 │ 使用条件 │├─────┼──────────────────┼──────────────────────────────────┤│ 44 │ Price │ 指値注文時の価格 ││ 99 │ StopPx │ 逆指値注文時の価格 ││ 126 │ ExpireTime │ GTD注文時の有効期限 ││ 494 │ Designation │ カスタムラベル │└─────┴──────────────────┴──────────────────────────────────┘OrdType一覧
| 値 | 名称 | 説明 |
|---|---|---|
| 1 | MARKET | 成行注文(即時約定) |
| 2 | LIMIT | 指値注文 |
| 3 | STOP | 逆指値注文 |
| 4 | STOP_LIMIT | ストップリミット |
TimeInForce一覧
| 値 | 名称 | 説明 |
|---|---|---|
| 1 | GTC | Good Till Cancel(キャンセルまで有効) |
| 3 | IOC | Immediate Or Cancel(即時約定のみ) |
| 6 | GTD | Good Till Date(指定日時まで有効) |
成行注文の実装
基本実装
from enum import Enum
class Side(Enum): BUY = "1" SELL = "2"
class OrdType(Enum): MARKET = "1" LIMIT = "2" STOP = "3" STOP_LIMIT = "4"
class TimeInForce(Enum): GTC = "1" IOC = "3" GTD = "6"
def new_order( symbol_id: int, side: Side, lot_size: float, contract_size: float, ord_type: OrdType = OrdType.MARKET, price: float = 0, stop_price: float = 0, time_in_force: TimeInForce = TimeInForce.IOC, expire_time: str = "", label: str = "",) -> str: """ 新規注文を送信
Args: symbol_id: シンボルID(数値) side: 売買方向 (Side.BUY or Side.SELL) lot_size: ロット数 (例: 0.01) contract_size: 契約サイズ (XAUUSD=100, FX=100000) ord_type: 注文タイプ (デフォルト: 成行) price: 指値価格 (LIMIT注文時) stop_price: 逆指値価格 (STOP注文時) time_in_force: 有効期限タイプ expire_time: 有効期限 (GTD時、フォーマット: YYYYMMDD-HH:MM:SS) label: カスタムラベル
Returns: str: ClOrdID(注文追跡用) """ # Volume計算(超重要!) volume = lot_to_volume(lot_size, contract_size) if volume <= 0: raise ValueError(f"無効なVolume: {volume}")
# 一意のClOrdID生成 cl_ord_id = f"ORD_{int(time.time() * 1000)}"
fields = [ f"11={cl_ord_id}", # ClOrdID f"55={symbol_id}", # Symbol f"54={side.value}", # Side f"38={volume}", # OrderQty (Volume!) f"40={ord_type.value}", # OrdType f"60={get_utc_timestamp()}", # TransactTime f"59={time_in_force.value}", # TimeInForce ]
# 指値価格 if ord_type in (OrdType.LIMIT, OrdType.STOP_LIMIT) and price > 0: fields.append(f"44={price}")
# 逆指値価格 if ord_type in (OrdType.STOP, OrdType.STOP_LIMIT) and stop_price > 0: fields.append(f"99={stop_price}")
# 有効期限 if time_in_force == TimeInForce.GTD and expire_time: fields.append(f"126={expire_time}")
# カスタムラベル if label: fields.append(f"494={label}")
msg = build_fix_message("D", fields, "TRADE") send_to_trade_session(msg)
print(f"📤 新規注文: {side.name} {lot_size}ロット (Volume={volume}) Symbol={symbol_id}") return cl_ord_idショートカット関数
def buy(symbol_id: int, lot_size: float, contract_size: float, label: str = "") -> str: """成行買い""" return new_order( symbol_id=symbol_id, side=Side.BUY, lot_size=lot_size, contract_size=contract_size, label=label, )
def sell(symbol_id: int, lot_size: float, contract_size: float, label: str = "") -> str: """成行売り""" return new_order( symbol_id=symbol_id, side=Side.SELL, lot_size=lot_size, contract_size=contract_size, label=label, )
def buy_limit(symbol_id: int, lot_size: float, contract_size: float, price: float, label: str = "") -> str: """買い指値""" return new_order( symbol_id=symbol_id, side=Side.BUY, lot_size=lot_size, contract_size=contract_size, ord_type=OrdType.LIMIT, price=price, time_in_force=TimeInForce.GTC, label=label, )
def sell_limit(symbol_id: int, lot_size: float, contract_size: float, price: float, label: str = "") -> str: """売り指値""" return new_order( symbol_id=symbol_id, side=Side.SELL, lot_size=lot_size, contract_size=contract_size, ord_type=OrdType.LIMIT, price=price, time_in_force=TimeInForce.GTC, label=label, )
def buy_stop(symbol_id: int, lot_size: float, contract_size: float, stop_price: float, label: str = "") -> str: """買い逆指値""" return new_order( symbol_id=symbol_id, side=Side.BUY, lot_size=lot_size, contract_size=contract_size, ord_type=OrdType.STOP, stop_price=stop_price, time_in_force=TimeInForce.GTC, label=label, )
def sell_stop(symbol_id: int, lot_size: float, contract_size: float, stop_price: float, label: str = "") -> str: """売り逆指値""" return new_order( symbol_id=symbol_id, side=Side.SELL, lot_size=lot_size, contract_size=contract_size, ord_type=OrdType.STOP, stop_price=stop_price, time_in_force=TimeInForce.GTC, label=label, )使用例
# XAUUSD 0.01ロット成行買いcl_ord_id = buy( symbol_id=41, # XAUUSD lot_size=0.01, contract_size=100, # XAUUSDの契約サイズ label="TEST_BUY")print(f"注文送信完了: ClOrdID={cl_ord_id}")
# EURUSD 指値売りcl_ord_id = sell_limit( symbol_id=1, # EURUSD(仮のID) lot_size=0.1, contract_size=100000, # FXの契約サイズ price=1.1000, label="TEST_SELL_LIMIT")ExecutionReport(35=8)の処理
注文送信後、サーバーからExecutionReportが返ってきます。
ExecType一覧
| 値 | 名称 | 説明 |
|---|---|---|
| 0 | NEW | 注文受付 |
| 1 | PARTIAL_FILL | 部分約定 |
| 2 | FILL | 全量約定 |
| 4 | CANCELED | キャンセル |
| 8 | REJECTED | 拒否 |
| F | TRADE | 約定(詳細) |
| C | EXPIRED | 期限切れ |
パース実装
def handle_execution_report(fields: dict): """ExecutionReport処理""" 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", "") pos_id = fields.get("721", "") # 約定時のポジションID
if exec_type == "F": # Trade (約定) price = fields.get("31", fields.get("6", "")) qty = fields.get("32", "") print(f"✅ 約定: ClOrdID={cl_ord_id} {qty}@{price} PosID={pos_id}")
# コールバック実行 if on_fill_callback: on_fill_callback(fields)
elif exec_type == "0": # New print(f"📝 注文受付: ClOrdID={cl_ord_id}")
elif exec_type == "8": # Rejected reason = fields.get("58", "Unknown") print(f"❌ 注文拒否: ClOrdID={cl_ord_id} 理由={reason}")
elif exec_type == "4": # Canceled print(f"🗑️ 注文キャンセル: ClOrdID={cl_ord_id}")
elif exec_type == "C": # Expired print(f"⏰ 注文期限切れ: ClOrdID={cl_ord_id}")ポジション決済の実装
決済の仕組み
cTrader FIX APIでの決済は「反対売買」で行います。
BUYポジションを決済 → SELL注文(同じVolume)SELLポジションを決済 → BUY注文(同じVolume)close_position関数
def close_position(position: Position, contract_size: float) -> str: """ ポジションを決済
Args: position: 決済するポジション contract_size: 契約サイズ
Returns: str: ClOrdID """ # 反対売買 if position.side == "BUY": close_side = Side.SELL else: close_side = Side.BUY
# Volumeをロットに変換 lot_size = volume_to_lot(position.volume, contract_size)
# 決済注文送信 cl_ord_id = new_order( symbol_id=position.symbol_id, side=close_side, lot_size=lot_size, contract_size=contract_size, label=f"CLOSE_{position.position_id}", )
print(f"🔄 決済注文: Position={position.position_id} {close_side.name} {lot_size}ロット") return cl_ord_id全ポジション決済
def close_all_positions(symbol_id: int = 0, contract_size: float = 100) -> list: """ 全ポジションを決済
Args: symbol_id: 対象シンボル(0=全シンボル) contract_size: 契約サイズ
Returns: list: 送信したClOrdIDリスト """ # ポジション取得 all_positions = get_all_positions()
if not all_positions: print("📭 決済するポジションなし") return []
cl_ord_ids = [] for pos in all_positions: # シンボルフィルター if symbol_id > 0 and pos.symbol_id != symbol_id: continue
cl_ord_id = close_position(pos, contract_size) cl_ord_ids.append(cl_ord_id)
print(f"🔄 {len(cl_ord_ids)}件の決済注文を送信") return cl_ord_ids注文キャンセルの実装
OrderCancelRequest(35=F)
待機中の指値/逆指値注文をキャンセルします。
def cancel_order(original_cl_ord_id: str, symbol_id: int, side: str) -> str: """ 注文をキャンセル
Args: original_cl_ord_id: 元の注文のClOrdID symbol_id: シンボルID side: "1"=Buy, "2"=Sell
Returns: str: 新しいClOrdID """ new_cl_ord_id = f"CXL_{int(time.time() * 1000)}"
fields = [ f"11={new_cl_ord_id}", # 新しいClOrdID f"41={original_cl_ord_id}", # 元のClOrdID f"55={symbol_id}", # Symbol f"54={side}", # Side f"60={get_utc_timestamp()}", # TransactTime ]
msg = build_fix_message("F", fields, "TRADE") send_to_trade_session(msg)
print(f"🗑️ キャンセル要求: OrigClOrdID={original_cl_ord_id}") return new_cl_ord_idキャンセル拒否(35=9)
キャンセルが失敗した場合はOrderCancelRejectが返ります:
def handle_cancel_reject(fields: dict): """OrderCancelReject処理""" orig_cl_ord_id = fields.get("41", "") reason = fields.get("58", "Unknown") cxl_rej_reason = fields.get("102", "")
print(f"❌ キャンセル拒否: OrigClOrdID={orig_cl_ord_id}") print(f" 理由: {reason}") print(f" コード: {cxl_rej_reason}")完全動作サンプル
def trading_demo(): """トレーディングデモ"""
# 接続 connect() send_logon("TRADE") wait_for_logon("TRADE") start_receiver_thread()
# 価格取得用にQUOTE接続 connect_quote() send_logon("QUOTE") wait_for_logon("QUOTE") start_quote_receiver_thread()
# XAUUSD購読 symbol_id = 41 contract_size = 100 subscribe_market_data(symbol_id) time.sleep(2)
# 現在価格確認 quote = get_quote(symbol_id) if quote: print(f"\n💰 現在価格: Bid={quote.bid} Ask={quote.ask}")
# 成行買い 0.01ロット print("\n📊 成行買い注文...") cl_ord_id = buy( symbol_id=symbol_id, lot_size=0.01, contract_size=contract_size, label="DEMO_BUY" )
# 約定待ち time.sleep(3)
# ポジション確認 print("\n📋 ポジション確認...") all_positions = get_all_positions() for pos in all_positions: lot = volume_to_lot(pos.volume, contract_size) print(f" {pos.side} {lot:.2f}ロット @ {pos.entry_price}")
# 決済 print("\n🔄 全ポジション決済...") close_all_positions(symbol_id, contract_size)
time.sleep(3)
# 最終確認 print("\n📋 決済後確認...") final_positions = get_all_positions() if not final_positions: print(" ✅ 全ポジション決済完了") else: for pos in final_positions: print(f" ⚠️ 残: {pos.side} @ {pos.entry_price}")
# 切断 disconnect()
if __name__ == "__main__": trading_demo()実行結果(Fundoraライブ口座)
✅ TRADEセッション ログオン成功✅ QUOTEセッション ログオン成功✅ マーケットデータ購読: Symbol ID=41
💰 現在価格: Bid=2648.31 Ask=2648.61
📊 成行買い注文...📤 新規注文: BUY 0.01ロット (Volume=1) Symbol=41📝 注文受付: ClOrdID=ORD_1736222712345✅ 約定: ClOrdID=ORD_1736222712345 1@2648.61 PosID=987654321
📋 ポジション確認...📊 ポジション: ID=987654321 Symbol=41 BUY Vol=1 Price=2648.61 BUY 0.01ロット @ 2648.61
🔄 全ポジション決済...🔄 決済注文: Position=987654321 SELL 0.01ロット📤 新規注文: SELL 0.01ロット (Volume=1) Symbol=41✅ 約定: 1@2648.31 PosID=987654321
📋 決済後確認... ✅ 全ポジション決済完了
FIX切断完了よくあるエラーと対処法
1. 「Volume=0」で注文拒否
原因: lot_size * contract_size の結果が1未満対処: 最小ロット数を確認(XAUUSDは0.01ロット=Volume 1)2. 「Invalid Symbol」
原因: シンボルIDが文字列対処: 数値IDを使用(55=41)3. 「NOT_ENOUGH_MONEY」エラー
最重要:このエラーは本当に証拠金不足とは限らない!
原因の90%: OrderQtyの計算ミス
例: XAUUSD 0.01ロットを注文したつもりが...❌ 38=1000 (FXの計算式を使ってしまった)→ 1000オンス = 10ロット相当 = $4,450,000の証拠金が必要→ NOT_ENOUGH_MONEY
✅ 38=1 (正しい計算)→ 1オンス = 0.01ロット相当 = 約$44の証拠金デバッグ方法:
# 送信前にログで確認volume = lot_to_volume(lot_size, contract_size)print(f"DEBUG: lot_size={lot_size}, contract_size={contract_size}, volume={volume}")# 期待値:# XAUUSD 0.01ロット → volume=1# EURUSD 0.01ロット → volume=10004. 「MARKET_CLOSED」エラー(2026-01-08追加)
市場クローズ中に注文を送るとBusinessReject (35=j)が返ります:
58=MARKET_CLOSED: Trading is not available: Market is closed.重要:商品によってメンテナンス時間が違う!
| 商品 | メンテナンス時間(JST) | 備考 |
|---|---|---|
| XAUUSD | 06:00-07:00 | 貴金属全般 |
| XAGUSD | 06:00-07:00 | 貴金属全般 |
| USDJPY | なし | FX通貨ペアは取引可能な場合が多い |
| EURUSD | なし | FX通貨ペアは取引可能な場合が多い |
検証結果(2026-01-08 07:03 JST):
XAUUSD: MARKET_CLOSED エラー ❌USDJPY: 正常に約定 @ 156.773 ✅対処法:
def is_market_open(symbol_name: str) -> bool: """市場オープン確認(簡易版)""" now_jst = datetime.now(timezone(timedelta(hours=9))) hour = now_jst.hour
# 貴金属: 06:00-07:00 JST はメンテナンス if "XAU" in symbol_name or "XAG" in symbol_name: if 6 <= hour < 7: return False
# 週末チェック if now_jst.weekday() >= 5: # 土日 return False
return True5. 「Order rejected by server」
よくある原因:├── 市場が閉まっている(MARKET_CLOSEDと同じ)├── 証拠金不足(本当の証拠金不足)├── 最大ポジション数超過└── スプレッドが広すぎる(指値)6. 約定したけどポジションが見つからない
# ExecutionReport(150=F)のタグ721でポジションIDを取得pos_id = fields.get("721", "")# このIDでポジション照会約定確認とリトライロジック
なぜリトライが必要か
注文を送信しても、ネットワーク遅延や一時的なエラーで約定しないことがあります。 「送信したら終わり」ではダメです。
推奨実装(2026-01-08検証済み)
def entry_with_retry(client, symbol_id: int, side: Side, lot_size: float, contract_size: float): """ 約定確認付きエントリー
重要: 重複注文を防ぐため、再注文間隔は30秒以上に設定 """ REORDER_INTERVAL = 30 # 秒
# 最初の注文 cl_ord_id = client.new_order(symbol_id, side, lot_size, contract_size) last_order_time = time.time()
# 約定確認ループ retry_count = 0 while True: time.sleep(2) # 2秒間隔でチェック retry_count += 1
positions = client.request_positions() log.info(f"ポジション照会結果: {len(positions)}件")
# ポジション確認OK if positions: for pos in positions: if pos.symbol_id == symbol_id: log.info(f"✅ ポジション確認OK: ID={pos.position_id} @{pos.entry_price} (リトライ{retry_count}回)") return pos
log.warning(f"ポジション未確認 - リトライ {retry_count}")
# 一定時間経過後に再注文(重複防止) elapsed = time.time() - last_order_time if elapsed >= REORDER_INTERVAL: # スプレッドと市場状態を確認してから再注文 quote = client.get_quote(symbol_id) if quote and quote.spread and quote.spread < 50: # スプレッド50pt以下 log.info(f"再注文実行 ({elapsed:.0f}秒経過)") cl_ord_id = client.new_order(symbol_id, side, lot_size, contract_size) last_order_time = time.time() else: log.warning("スプレッド広いため再注文スキップ")
# 安全弁: 3分以上経過したら諦める if retry_count > 90: log.error("約定確認タイムアウト") return Noneポイント
- 再注文間隔は30秒以上: 短すぎると重複注文のリスク
- スプレッド確認: 再注文前にスプレッドをチェック
- 安全弁: 無限ループを防ぐためのタイムアウト
まとめ
今日学んだこと
- OrderQty = ロット数 × 契約サイズ(超重要!)
- XAUUSD契約サイズ = 100、FX = 100,000
- 決済は反対売買(BUY→SELL、SELL→BUY)
- ExecutionReportで約定確認
- OrderCancelRequestでキャンセル
次回予告
次回「FIX API完全版⑤ 全機能クラスの実装」では:
- 今までの全機能を統合したクラス
- スレッドセーフな設計
- コールバック処理
- エラーハンドリング
を解説します!
関連記事
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。