⚠️【非推奨】FIX API入門②|PythonでLogon→ 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プロトコルの基礎を学んだ。
「じゃあPythonでログオンしてみよう」
…3時間かかった。
❌ ログオン失敗❌ 接続切断❌ チェックサム不正❌ ボディ長が違う❌ パスワードが違う地獄だった。
🎯 この記事でできるようになること
- PythonでSSL接続を確立
- FIXメッセージを正しく組み立て
- チェックサムを計算
- ログオンが通る
サンプルコード全文付き。
📋 必要な情報
ログオン前に、以下の情報を準備:
# 接続先TRADE_HOST = "h52.p.ctrader.com" # ブローカーによるTRADE_PORT = 5212 # 固定
# 認証情報SENDER_COMP_ID = "your_login_id" # cTraderのログインIDTARGET_COMP_ID = "cServer" # 通常はcServerFIX_PASSWORD = "your_fix_password" # FIX専用パスワード
# プロトコルFIX_VERSION = "FIX.4.4"HEARTBEAT_INTERVAL = 30⚠️ 重要な注意
| 情報 | 取得場所 |
|---|---|
| TRADE_HOST | ブローカーのFIX API設定ページ |
| SENDER_COMP_ID | cTraderのログインID(メアドじゃない) |
| TARGET_COMP_ID | 通常は "cServer" |
| FIX_PASSWORD | cTrader Web → 設定 → FIX API |
CTIDパスワード(普段使うやつ)は使えない!
🔌 Step 1: SSL接続
コード
import socketimport ssl
def connect(host: str, port: int): """SSL接続を確立""" # 生ソケット作成 raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SSL/TLSでラップ context = ssl.create_default_context() wrapped_sock = context.wrap_socket(raw_sock, server_hostname=host)
# 接続 wrapped_sock.connect((host, port)) print(f"✅ 接続成功: {host}:{port}")
return wrapped_sock使い方
sock = connect(TRADE_HOST, TRADE_PORT)ハマりポイント①: SSL証明書検証
# ❌ 検証を無効にしたくなる誘惑context.check_hostname = Falsecontext.verify_mode = ssl.CERT_NONE
# ✅ デフォルト設定で十分(cTraderは正規証明書)context = ssl.create_default_context()cTraderサーバーは正規SSL証明書を使用。検証無効化は不要。
📝 Step 2: FIXメッセージの構造
基本構造
ヘッダー | ボディ | トレーラー
ヘッダー: 8=FIX.4.4 (BeginString) 9=123 (BodyLength) 35=A (MsgType) 49=SENDER (SenderCompID) 56=TARGET (TargetCompID) 57=TRADE (TargetSubID) ← 重要! 34=1 (MsgSeqNum) 52=20260106-12:00:00 (SendingTime)
ボディ: (メッセージ固有のフィールド)
トレーラー: 10=234 (CheckSum)区切り文字: SOH(\x01)
見た目は「|」だけど、実際は制御文字。
SOH = "\x01" # ASCII 1 = Start of Header
# メッセージ例msg = f"8=FIX.4.4{SOH}9=123{SOH}35=A{SOH}..."ハマりポイント②: SOHを忘れる
# ❌ パイプで区切る(動かない)msg = "8=FIX.4.4|9=123|35=A"
# ✅ SOH(\x01)で区切るmsg = f"8=FIX.4.4{SOH}9=123{SOH}35=A{SOH}"🔢 Step 3: ボディ長の計算
ルール
BodyLength = タグ35から最後のSOH(タグ10の前)までのバイト数タグ8、タグ9は含まない。タグ10も含まない。
コード
def calc_body_length(body_with_header: str) -> int: """ ボディ長を計算 35=... から最後のSOHまでのバイト数 """ # 8=FIX.4.4\x01 と 9=XXX\x01 を除いた部分 # つまり35=から始まる部分 return len(body_with_header.encode('ascii'))使い方
# 35=から10=の前までbody_part = f"35=A{SOH}49={SENDER}{SOH}56={TARGET}{SOH}..."body_length = calc_body_length(body_part)ハマりポイント③: ボディ長計算ミス
# ❌ 全体の長さを計算(間違い)body_length = len(f"8=FIX.4.4{SOH}9=XXX{SOH}35=A{SOH}...")
# ❌ SOHを数えない(間違い)body_length = len("35=A49=SENDER56=TARGET...")
# ✅ 35=から10=の前までのバイト数(SOH含む)body_part = f"35=A{SOH}49={SENDER}{SOH}56={TARGET}{SOH}..."body_length = len(body_part.encode('ascii'))✅ Step 4: チェックサム計算
ルール
CheckSum = (全バイトの合計) mod 256タグ10=より前の全バイト(8=から始まる)の合計。
コード
def calc_checksum(msg_without_checksum: str) -> str: """ チェックサムを計算 全バイトの合計 mod 256 を3桁ゼロ埋めで返す """ checksum = sum(ord(c) for c in msg_without_checksum) % 256 return f"10={checksum:03d}{SOH}"使い方
# チェックサム以外を組み立てmsg_without_checksum = f"8=FIX.4.4{SOH}9={body_length}{SOH}{body_part}"
# チェックサムを計算して追加checksum = calc_checksum(msg_without_checksum)full_msg = msg_without_checksum + checksumハマりポイント④: チェックサム形式
# ❌ ゼロ埋めなし(間違い)f"10={checksum}{SOH}" # → "10=23" (2桁)
# ✅ 3桁ゼロ埋めf"10={checksum:03d}{SOH}" # → "10=023"チェックサムは必ず3桁。
🔑 Step 5: Logonメッセージ組み立て
Logon(35=A)の必須フィールド
| タグ | 名前 | 値 |
|---|---|---|
| 35 | MsgType | A (Logon) |
| 49 | SenderCompID | ログインID |
| 56 | TargetCompID | cServer |
| 57 | TargetSubID | TRADE |
| 34 | MsgSeqNum | 1(最初は1) |
| 52 | SendingTime | UTC時刻 |
| 98 | EncryptMethod | 0(暗号化なし) |
| 108 | HeartBtInt | 30(秒) |
| 141 | ResetSeqNumFlag | Y(推奨) |
| 553 | Username | ログインID |
| 554 | Password | FIXパスワード |
ハマりポイント⑤: TargetSubIDを忘れる
# ❌ TargetSubIDなし(ログオン無視される)f"49={SENDER}{SOH}56={TARGET}{SOH}34=1{SOH}..."
# ✅ TargetSubID必須f"49={SENDER}{SOH}56={TARGET}{SOH}57=TRADE{SOH}34=1{SOH}..."タグ57(TargetSubID)がないとログオンが無視される!
ハマりポイント⑥: UTC時刻フォーマット
from datetime import datetime, timezone
# ❌ ローカル時刻(間違い)timestamp = datetime.now().strftime("%Y%m%d-%H:%M:%S")
# ❌ フォーマット間違いtimestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
# ✅ UTC時刻・正しいフォーマットtimestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S")# 例: "20260106-03:00:00"🎯 Step 6: 完全なLogon送信コード
import socketimport sslfrom datetime import datetime, timezone
SOH = "\x01"
# 設定TRADE_HOST = "h52.p.ctrader.com" # ブローカーによるTRADE_PORT = 5212SENDER_COMP_ID = "your_login_id"TARGET_COMP_ID = "cServer"FIX_PASSWORD = "your_fix_password"
# シーケンス番号(セッション内でインクリメント)seq_num = 1
def get_utc_timestamp(): """UTC時刻を取得""" return datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S")
def calc_checksum(msg: str) -> str: """チェックサムを計算""" checksum = sum(ord(c) for c in msg) % 256 return f"10={checksum:03d}{SOH}"
def build_logon_message(): """Logonメッセージを組み立て""" global seq_num
# ボディ部分(35=から) body = ( f"35=A{SOH}" f"49={SENDER_COMP_ID}{SOH}" f"56={TARGET_COMP_ID}{SOH}" f"57=TRADE{SOH}" # ← 重要! f"34={seq_num}{SOH}" f"52={get_utc_timestamp()}{SOH}" f"98=0{SOH}" # 暗号化なし f"108=30{SOH}" # ハートビート30秒 f"141=Y{SOH}" # シーケンスリセット f"553={SENDER_COMP_ID}{SOH}" # Username f"554={FIX_PASSWORD}{SOH}" # Password )
# ヘッダー(8=, 9=) body_length = len(body.encode('ascii')) header = f"8=FIX.4.4{SOH}9={body_length}{SOH}"
# チェックサム計算 msg_without_checksum = header + body checksum = calc_checksum(msg_without_checksum)
# 完成 seq_num += 1 return msg_without_checksum + checksum
def send_message(sock, msg: str): """メッセージを送信""" sock.send(msg.encode('ascii')) print(f"→ 送信: {msg.replace(SOH, '|')}")
def receive_message(sock) -> str: """メッセージを受信""" data = sock.recv(4096) msg = data.decode('ascii') print(f"← 受信: {msg.replace(SOH, '|')}") return msg
def main(): # 接続 raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) context = ssl.create_default_context() sock = context.wrap_socket(raw_sock, server_hostname=TRADE_HOST) sock.connect((TRADE_HOST, TRADE_PORT)) print(f"✅ 接続成功: {TRADE_HOST}:{TRADE_PORT}")
# Logon送信 logon_msg = build_logon_message() send_message(sock, logon_msg)
# 応答受信 response = receive_message(sock)
# 結果判定 if "35=A" in response: print("🎉 ログオン成功!") elif "35=3" in response: print("❌ Reject: メッセージ形式エラー") elif "35=j" in response: print("❌ BusinessReject: 認証エラーなど") elif "35=5" in response: print("❌ Logout: サーバーから切断")
# 切断 sock.close()
if __name__ == "__main__": main()✅ 成功したときの応答
→ 送信: 8=FIX.4.4|9=156|35=A|49=12345678|56=cServer|57=TRADE|34=1|52=20260106-03:00:00|98=0|108=30|141=Y|553=12345678|554=MyPassword123|10=234|
← 受信: 8=FIX.4.4|9=98|35=A|49=cServer|56=12345678|34=1|52=20260106-03:00:00.123|98=0|108=30|10=187|
🎉 ログオン成功!35=A が返ってきたら成功!
❌ 失敗パターンと対処
パターン1: RET_INVALID_DATA
← 受信: 35=j|58=RET_INVALID_DATA|...原因: CTIDパスワードを使用
対処: cTrader Web → FIX API設定でFIX専用パスワードを設定
パターン2: Reject(35=3)
← 受信: 35=3|58=Required tag missing|...原因: 必須タグの欠落
対処: タグ57(TargetSubID)を追加
パターン3: 無応答
原因: ボディ長またはチェックサムの計算ミス
対処: 計算ロジックを確認
パターン4: 接続切断
原因: ホスト名/ポート番号が違う
対処: ブローカーのFIX API設定ページで確認
🔄 ハートビート対応
ログオン成功後、30秒ごとにハートビートを送る必要がある。
def build_heartbeat(test_req_id: str = None): """ハートビートメッセージ""" global seq_num
body = ( f"35=0{SOH}" f"49={SENDER_COMP_ID}{SOH}" f"56={TARGET_COMP_ID}{SOH}" f"57=TRADE{SOH}" f"34={seq_num}{SOH}" f"52={get_utc_timestamp()}{SOH}" )
# TestReqIDがあれば追加(TestRequest応答時) if test_req_id: body += f"112={test_req_id}{SOH}"
body_length = len(body.encode('ascii')) header = f"8=FIX.4.4{SOH}9={body_length}{SOH}" msg_without_checksum = header + body checksum = calc_checksum(msg_without_checksum)
seq_num += 1 return msg_without_checksum + checksumTestRequest(35=1)への応答
サーバーからTestRequestが来たら、即座にハートビートで応答:
def handle_message(msg: str, sock): """受信メッセージを処理""" if "35=1" in msg: # TestRequest # タグ112(TestReqID)を取得 test_req_id = extract_tag(msg, "112") # ハートビートで応答 heartbeat = build_heartbeat(test_req_id) send_message(sock, heartbeat)📊 Logonで踏むハマりポイントまとめ
| # | ハマりポイント | 症状 |
|---|---|---|
| 1 | CTIDパスワード使用 | RET_INVALID_DATA |
| 2 | SOHの代わりに|使用 | 接続切断 |
| 3 | ボディ長計算ミス | 無応答 |
| 4 | チェックサム計算ミス | 無応答 |
| 5 | チェックサム3桁ゼロ埋め忘れ | Reject |
| 6 | TargetSubID(57)欠落 | 無応答 |
| 7 | UTC時刻フォーマット間違い | Reject |
| 8 | シーケンス番号の管理ミス | 接続切断 |
8個中3個はLogonで踏む。
📝 まとめ
必須知識
- SOH(\x01) で区切る(
|じゃない) - ボディ長 は35=から10=の前まで
- チェックサム は全バイトmod256の3桁
- TargetSubID は必須
- UTC時刻 をYYYYMMDD-HH:MM:SS形式で
やっとスタートライン
ログオンが通っただけ。
これで「注文が送れる状態」になった。
次は実際に注文を送る。
🔥 次回予告
【FIX API入門③】Pythonで注文送信!0.01ロットが10ロットになった事件
- NewOrderSingle(35=D)の組み立て
- OrderQtyの単位の罠
- ExecutionReportの解析
- ポジションの確認方法
15個のハマりポイントの大半はここで踏む。
📚 関連記事
FIX API完全版シリーズ(2026年版・Fundora検証済み)
- FIX API完全版① 2セッション構成とパスワードの真実
- FIX API完全版② リアルタイム価格取得編
- FIX API完全版③ ポジション・オーダー照会編
- FIX API完全版④ エントリー・決済完全実装
- FIX API完全版⑤ 全機能クラスの実装
その他
📖 参考リンク
ログオンが通った瞬間の喜びは格別。 🎉
だがこれはまだ序章に過ぎない。
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。