Skip to content
Go back

【5日間の死闘】GMO Coin FX API ERR-5105の真犯人は二重JSON化

Edit page

【5日間の死闘】GMO Coin FX API ERR-5105の真犯人は二重JSON化

🔥 要約

GMO Coin FX APIを使ったFX自動売買システムで、2025年12月31日から2026年1月5日まで5日間連続でERR-5105エラーが発生。HTTP 404エラー、型不一致エラー、パラメータ削除、最小ロット修正…全ての対策を試しても解決しませんでした。

真犯人は「二重JSON化」だった。

place_order()json.dumps()_request()で再度json.dumps()を実行し、JSONがさらにJSON化されてエスケープされた文字列になっていた。GMOサポートの「正常なJSON形式でない」という曖昧な指摘から、isinstance(body, str)による型チェックを追加してERR-5105を完全解消。

この記事で学べること:


📅 5日間のエラー履歴タイムライン

Day 1: 2025-12-31 07:00 - 練習 mode初日の悪夢

背景: この日は待ちに待ったGMO Coin FX APIを使った練習 mode(最小ロット実運用)開始日だった。バックテストで破産確率0%を達成し、DryRunモードでも問題なし。満を持しての本番運用開始…のはずだった。

エラー内容:

2025-12-31 07:00:15 [ERROR] 注文実行エラー: HTTP 404
全7通貨ペアの注文が失敗

リクエスト:

{
  "symbol": "USD_JPY",
  "side": "SELL",
  "executionType": "MARKET",
  "size": "1000",
  "settleType": "OPEN",
  "timeInForce": "FAK"
}

初期仮説: MARKET注文でpriceパラメータが不要なのに指定していた?

対策: price: nullを削除 → 失敗(HTTP 404継続)

次の: 「DryRunで動いていたのになぜか動かない。APIドキュメントを読み直し」


Day 2: 2026-01-01 07:00 - HTTP 404の原因判明

WebFetch調査: GMO Coin FX API公式ドキュメントを改めて調査

発見: settleTypetimeInForceはGMO Coin Crypto API専用パラメータで、FX APIには存在しない

公式ドキュメントのサンプルコードが混在していたため、誤って使用していた。

対策: settleTypetimeInForceを削除

リクエスト(修正版):

{
  "symbol": "USD_JPY",
  "side": "SELL",
  "executionType": "MARKET",
  "size": "1000"
}

実行結果: HTTP 404は解消 → しかし新たなエラー「ERR-5105」が登場

{
  "status": 1,
  "messages": [{
    "message_code": "ERR-5105",
    "message_string": "Request parameter include mismatch type."
  }],
  "responsetime": "2026-01-01T22:00:38.495Z"
}

Day 3: 2026-01-02 07:00 - 型変更の迷走

仮説1: sizeパラメータは文字列型であるべき?

対策: sizeを数値型(1000)→ 文字列型("1000")に変更

結果: ERR-5105継続

# 修正コード
body_dict = {
    "symbol": symbol,
    "side": side.upper(),
    "executionType": execution_type.upper(),
    "size": str(size)  # 文字列型に変更
}

仮説2: losscutPrice(ストップロス価格)の型が不正?

対策: losscutPriceパラメータ削除(MARKET注文では使えない可能性)

結果: ERR-5105継続


Day 4: 2026-01-03 07:00 - 最小ロット問題の発覚

WebFetch調査: GMO Coin FX API取引ルール(GET /public/v1/rules)を確認

発見: 最小注文数量(minOpenOrderSize)は10,000通貨だった!

従来のOANDA API(最小1,000通貨)と混同していた。

対策: size100010000に修正

リクエスト(修正版):

{
  "symbol": "NZD_JPY",
  "side": "SELL",
  "executionType": "MARKET",
  "size": "10000"
}

実行結果: ERR-5105継続

{
  "status": 1,
  "messages": [{
    "message_code": "ERR-5105",
    "message_string": "Request parameter include mismatch type."
  }]
}

Day 5-1: 2026-01-04 09:00 - SELL方向制限仮説の検証

新仮説: GMO Coin FX APIではSELL方向(空売り)が制限されている?

検証: USD_JPYでBUY注文を実行

リクエスト:

{
  "symbol": "USD_JPY",
  "side": "BUY",
  "executionType": "MARKET",
  "size": "10000"
}

実行結果: BUYでもERR-5105発生

結論: SELL direction特有の問題ではない。より根本的なパラメータ問題が存在。


Day 5-2: 2026-01-04 14:00 - GMOサポート問い合わせ

問い合わせ内容:

MARKET注文でERR-5105(型不一致)エラーが継続的に発生しています。リクエストボディは公式ドキュメント通りで、settleType/timeInForce削除、size型修正、最小ロット10,000通貨に変更済みですが解決しません。

GMO回答(翌日):

お問い合わせいただいたエラー(ERR-5105)の場合、設定いただいているリクエストボディが正常なjson形式でないことが考えられます。


Day 5-3: 2026-01-05 19:00 - 真犯人発見!

GMOサポートの「正常なJSON形式でない」という曖昧な指摘を受け、JSON生成ロジックを再精査。

コードレビュー:

# place_order() メソッド (L1883)
body_dict = {
    "symbol": symbol,
    "side": side.upper(),
    "executionType": execution_type.upper(),
    "size": str(size)
}
body_json = json.dumps(body_dict)  # 🔴 1回目のJSON化
response = self._request("POST", "/v1/order", body=body_json)

# _request() メソッド (L1393) - 修正前
body_str = json.dumps(body) if body else ""  # 🔴 2回目のJSON化(誤り)

発見: place_order()で既にJSON化された文字列を、_request()で再度JSON化していた!

実際に送信されていたデータ:

"{\"symbol\": \"USD_JPY\", \"side\": \"BUY\", \"executionType\": \"MARKET\", \"size\": \"10000\"}"

→ JSON文字列がさらにJSON化され、ダブルクォートでエスケープされた文字列になっていた

GMO APIが期待していたのは:

{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}

しかし実際に受信したのは:

"{\"symbol\": \"USD_JPY\", \"side\": \"BUY\", \"executionType\": \"MARKET\", \"size\": \"10000\"}"

これでは文字列型の値として認識され、JSONオブジェクトとしてパースできない。


Day 5-4: 2026-01-05 19:30 - 修正と検証

修正内容execution/core/gmo_client.py:1393-1399):

# 修正前
body_str = json.dumps(body) if body else ""

# 修正後
# bodyが既にJSON文字列の場合はそのまま使用、dictの場合はJSON化
if isinstance(body, str):
    body_str = body  # ✅ 既にJSON化済みならそのまま使用
elif body:
    body_str = json.dumps(body)  # ✅ dictならJSON化
else:
    body_str = ""

検証:

PYTHONPATH=/Users/XXX/systemtrade/_fxTradingEngine:/Users/XXX/systemtrade \
  /opt/anaconda3/envs/st312/bin/python \
  execution/main_forward_test_gmo.py \
  --trade-type Swing --mode 練習 --once

実行結果: ✅ ERR-5105エラー完全解消!

新しいエラー: ERR-201: Trading margin is insufficient(証拠金不足)

{
  "status": 1,
  "messages": [{
    "message_code": "ERR-201",
    "message_string": "Trading margin is insufficient."
  }]
}

この新しいエラーは何を意味するか?

→ APIリクエストが正常に処理されている証明

ERR-5105(型不一致)ではなく、ERR-201(ビジネスロジックエラー)になったということは、GMO APIがリクエストを正しくパースし、注文処理まで進んだ証拠。


🔍 根本原因の詳細分析

二重JSON化とは?

正常なフロー(修正後):

# Step 1: Pythonオブジェクト(dict)作成
body_dict = {"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}

# Step 2: JSON文字列化(1回のみ)
body_json = json.dumps(body_dict)
# → '{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}'

# Step 3: HTTPリクエスト送信
requests.post(url, data=body_json, headers=headers)

異常なフロー(修正前):

# Step 1: Pythonオブジェクト(dict)作成
body_dict = {"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}

# Step 2: JSON文字列化(1回目)
body_json = json.dumps(body_dict)
# → '{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}'

# Step 3: 再度JSON文字列化(2回目)🔴 ここが問題!
body_str = json.dumps(body_json)
# → '"{\"symbol\": \"USD_JPY\", \"side\": \"BUY\", \"executionType\": \"MARKET\", \"size\": \"10000\"}"'

# Step 4: HTTPリクエスト送信
requests.post(url, data=body_str, headers=headers)

GMO APIサーバー側の処理

正常なリクエストを受信した場合:

# サーバー側でJSONパース
import json
request_body = '{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}'
data = json.loads(request_body)
# → {"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}

# パラメータ検証
assert isinstance(data, dict)  # ✅ Pass
assert "symbol" in data        # ✅ Pass
assert isinstance(data["size"], str)  # ✅ Pass

異常なリクエスト(二重JSON化)を受信した場合:

# サーバー側でJSONパース
import json
request_body = '"{\"symbol\": \"USD_JPY\", \"side\": \"BUY\", \"executionType\": \"MARKET\", \"size\": \"10000\"}"'
data = json.loads(request_body)
# → '{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}'
# 🔴 文字列型!dictではない!

# パラメータ検証
assert isinstance(data, dict)  # ❌ Fail!
# → ERR-5105: Request parameter include mismatch type.

💡 解決策の実装詳細

型チェックによる分岐処理

実装コードexecution/core/gmo_client.py:1393-1401):

def _request(
    self,
    method: str,
    path: str,
    body: Optional[Union[Dict[str, Any], str]] = None,
    max_retries: Optional[int] = None
) -> Dict[str, Any]:
    """
    GMO Coin FX APIへのHTTPリクエストを実行

    Args:
        method: HTTPメソッド(GET/POST/PUT/DELETE)
        path: APIパス(例: /v1/order)
        body: リクエストボディ(dictまたはJSON文字列)
        max_retries: 最大リトライ回数(Noneの場合はself.MAX_RETRIESを使用)

    Returns:
        APIレスポンス(JSON)
    """
    # URLとボディの準備
    url = self.base_url + path

    # 🔑 ここが重要!型チェックで二重JSON化を防止
    if isinstance(body, str):
        # 既にJSON文字列ならそのまま使用
        body_str = body
    elif body:
        # dictならJSON化
        body_str = json.dumps(body)
    else:
        # Noneまたは空ならば空文字列
        body_str = ""

    # HTTPヘッダー生成(HMAC-SHA256署名含む)
    headers = self._build_headers(method, path, body_str)

    # HTTPリクエスト実行
    response = requests.request(
        method=method,
        url=url,
        data=body_str,  # ✅ ここで送信されるのは1回だけJSON化された文字列
        headers=headers,
        timeout=self.TIMEOUT
    )

    return response.json()

型ヒントの活用

修正前はbodyパラメータの型が曖昧だった:

# 修正前
def _request(self, method: str, path: str, body=None):
    # bodyがdictかstrか不明
    body_str = json.dumps(body) if body else ""  # 🔴 常にjson.dumps()実行

修正後はUnion[Dict, str]で明示:

# 修正後
def _request(
    self,
    method: str,
    path: str,
    body: Optional[Union[Dict[str, Any], str]] = None  # ✅ 型ヒントで明示
):
    # bodyの型を判定して処理を分岐
    if isinstance(body, str):
        body_str = body
    elif body:
        body_str = json.dumps(body)
    else:
        body_str = ""

🎓 学んだ教訓

1. JSON生成の二重化に注意

問題の本質:

教訓:

防止策:

# ✅ 良い例: 型チェックで二重JSON化を防止
if isinstance(body, str):
    body_str = body
elif body:
    body_str = json.dumps(body)

# ❌ 悪い例: 常にjson.dumps()実行
body_str = json.dumps(body) if body else ""

2. サポート問い合わせの重要性

GMOサポートの指摘: 「正常なJSON形式でない」

この曖昧な表現が、二重JSON化という具体的な問題を発見する手がかりになった。

教訓:

効果的な問い合わせ例:

【問い合わせテンプレート】
- エラーコード: ERR-5105
- エラーメッセージ: Request parameter include mismatch type.
- リクエストボディ: {"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}
- 試した対策: settleType削除、size型変更、最小ロット修正
- 疑問点: どのパラメータの型が不一致なのか?

3. エラーメッセージの多義性

ERR-5105: Request parameter include mismatch type の意味:

  1. 型不一致(size: string vs number)
  2. 値の範囲違反(size: 1,000 < minOpenOrderSize: 10,000)
  3. JSON形式エラー(二重JSON化) ← 今回の原因

教訓:

ログ出力の改善例:

# 修正前
self.logger.error(f"API呼び出しエラー: {error_code}")

# 修正後
self.logger.error(f"API呼び出しエラー詳細:")
self.logger.error(f"  リクエストパス: {path}")
self.logger.error(f"  リクエストボディ: {body}")
self.logger.error(f"  レスポンス全体: {json.dumps(response_data, ensure_ascii=False, indent=2)}")

4. 段階的な検証の重要性

5日間で試した対策:

  1. price: null削除 → HTTP 404継続
  2. settleType/timeInForce削除 → ERR-5105登場
  3. size型変更(数値→文字列) → ERR-5105継続
  4. losscutPrice削除 → ERR-5105継続
  5. 最小ロット修正(1,000→10,000) → ERR-5105継続
  6. BUY方向テスト → ERR-5105継続(仮説否定)
  7. 二重JSON化修正 → ERR-5105解消

教訓:

5. 型システムの活用

型ヒントがあれば防げた:

# 型ヒントなし(修正前)
def _request(self, method, path, body=None):
    # bodyの型が不明確
    pass

# 型ヒント付き(修正後)
def _request(
    self,
    method: str,
    path: str,
    body: Optional[Union[Dict[str, Any], str]] = None  # ✅ 型を明示
) -> Dict[str, Any]:
    # bodyがdictかstrかを型ヒントで明示
    # isinstance()による型チェックが自然に導かれる
    pass

教訓:


🛠️ 実装コード全文

修正箇所1: _request()メソッド

ファイル: execution/core/gmo_client.py

def _request(
    self,
    method: str,
    path: str,
    body: Optional[Union[Dict[str, Any], str]] = None,
    max_retries: Optional[int] = None
) -> Dict[str, Any]:
    """
    GMO Coin FX APIへのHTTPリクエストを実行

    Args:
        method: HTTPメソッド(GET/POST/PUT/DELETE)
        path: APIパス(例: /v1/order)
        body: リクエストボディ(dictまたはJSON文字列)
        max_retries: 最大リトライ回数

    Returns:
        APIレスポンス(JSON)

    Raises:
        GmoApiError: API呼び出しエラー
    """
    # レート制限チェック
    self._check_rate_limit(method)

    # URLとボディの準備
    url = self.base_url + path

    # bodyが既にJSON文字列の場合はそのまま使用、dictの場合はJSON化
    if isinstance(body, str):
        body_str = body
    elif body:
        body_str = json.dumps(body)
    else:
        body_str = ""

    # HTTPヘッダー生成(署名含む)
    headers = self._build_headers(method, path, body_str)

    # リトライループ(指数バックオフ)
    for attempt in range(max_retries or self.MAX_RETRIES):
        try:
            self.logger.debug(
                f"APIリクエスト: {method} {path} "
                f"(attempt {attempt + 1}/{max_retries or self.MAX_RETRIES})"
            )

            # HTTPリクエスト実行
            response = requests.request(
                method=method,
                url=url,
                data=body_str,
                headers=headers,
                timeout=self.TIMEOUT
            )

            # レスポンス処理
            return self._handle_response(response, path, body_str)

        except requests.exceptions.Timeout:
            # タイムアウト時はリトライ
            if attempt < (max_retries or self.MAX_RETRIES) - 1:
                wait_time = 2 ** attempt  # 指数バックオフ
                self.logger.warning(
                    f"タイムアウト発生。{wait_time}秒後にリトライします"
                )
                time.sleep(wait_time)
                continue
            else:
                raise GmoApiError(
                    0,
                    "APIリクエストがタイムアウトしました",
                    error_code="TIMEOUT"
                )

修正箇所2: place_order()メソッド

ファイル: execution/core/gmo_client.py

def place_order(
    self,
    symbol: str,
    side: str,
    execution_type: str,
    size: int,
    price: Optional[float] = None,
    losscut_price: Optional[float] = None
) -> Dict[str, Any]:
    """
    新規注文を発行

    Args:
        symbol: 通貨ペア(例: USD_JPY)
        side: BUY/SELL
        execution_type: MARKET/LIMIT
        size: 注文数量(最小10,000通貨)
        price: 指値価格(LIMIT注文のみ)
        losscut_price: ストップロス価格(オプション、現在未使用)

    Returns:
        注文レスポンス
    """
    self.logger.info(
        f"注文発行: {symbol} {side} {execution_type} {size:,}通貨"
    )

    # リクエストボディ作成
    body_dict = {
        "symbol": symbol,
        "side": side.upper(),
        "executionType": execution_type.upper(),
        "size": str(size)  # ✅ 文字列型で送信(GMO API仕様)
    }

    # LIMIT注文の場合は指値価格を追加
    if execution_type.upper() == "LIMIT" and price is not None:
        body_dict["price"] = self._round_price(symbol, price)

    # JSON文字列化(1回のみ)
    body_json = json.dumps(body_dict)

    # APIリクエスト実行
    # _request()では二重JSON化を防止(isinstance(body, str)チェック)
    response = self._request("POST", "/v1/order", body=body_json)

    # レスポンスデータ取得
    data = response.get("data")
    if isinstance(data, list):
        # GMO APIはdataをlistで返すことがある
        data = data[0]

    self.logger.info(f"注文成功: order_id={data.get('orderId')}")
    return data

📊 検証結果

修正前(ERR-5105発生)

リクエスト(実際に送信されていたデータ):

"{\"symbol\": \"USD_JPY\", \"side\": \"BUY\", \"executionType\": \"MARKET\", \"size\": \"10000\"}"

レスポンス:

{
  "status": 1,
  "messages": [{
    "message_code": "ERR-5105",
    "message_string": "Request parameter include mismatch type."
  }],
  "responsetime": "2026-01-05T10:30:38.495Z"
}

修正後(ERR-5105解消)

リクエスト(正しいJSON):

{"symbol": "USD_JPY", "side": "BUY", "executionType": "MARKET", "size": "10000"}

レスポンス:

{
  "status": 1,
  "messages": [{
    "message_code": "ERR-201",
    "message_string": "Trading margin is insufficient."
  }],
  "responsetime": "2026-01-05T10:49:15.123Z"
}

ERR-201の意味: 証拠金不足(ビジネスロジックエラー)

APIリクエストが正常に処理されている証明


🎯 まとめ

この記事のポイント

  1. 二重JSON化問題: json.dumps()を2回実行すると、JSON文字列がエスケープされた文字列になる
  2. 型チェックの重要性: isinstance(body, str)で型判定し、二重JSON化を防止
  3. エラーメッセージの多義性: ERR-5105は型不一致、範囲違反、JSON形式エラーなど複数の意味を持つ
  4. サポート問い合わせの効果: 曖昧な指摘でも真の原因に近づく手がかりになる
  5. 段階的デバッグ: 5日間の試行錯誤が真の原因を絞り込んだ

GMO Coin FX API利用者へのアドバイス

チェックリスト:

避けるべきパターン:

# ❌ 悪い例
body_json = json.dumps(body_dict)
response = requests.post(url, data=json.dumps(body_json))  # 二重JSON化

# ✅ 良い例
body_json = json.dumps(body_dict)
response = requests.post(url, data=body_json)  # 1回のみJSON化

次のステップ

ERR-5105を解消した後は、証拠金管理、IFDOCO注文(Entry+SL+TP同時設定)、レート制限対策などの実装が待っています。

次回記事では**「GMO Coin FX API完全ガイド - 5つの落とし穴と正しい実装パターン」**をお届けします。


📚 参考リンク



Edit page
Share this post on:

Next Post
システムトレードの評価基準一覧