取引所

現物・先物ウォレットは分かれている?資金移動忘れでボットが止まる理由


※本記事にはアフィリエイト広告(プロモーション)が含まれています。

自動売買ボットを新しい取引所で動かし始めたとき、「資金は入金したはずなのに、APIで残高を取得すると0になっている」「発注したら『残高不足』エラーが返ってくる」という経験をした方は少なくないと思います。この原因の多くは、取引所の内部で現物(スポット)ウォレットと先物(フューチャーズ)ウォレットが別々に管理されていることにあります。この記事では、この仕組みがなぜ存在するのか、ボット設計上どう注意すべきかを一般論として整理します。

なぜウォレットが分かれているのか

多くの海外取引所では、現物取引用の資産と、先物(無期限先物を含む)取引用の資産を、内部的に別の「口座」として管理しています。無期限先物とは?の記事で触れたとおり、先物取引は証拠金(マージン)を預け入れてレバレッジをかける仕組みであり、強制ロスカットの判定もこの証拠金の範囲内で行われます。

このため取引所側としては、

  • 現物資産と先物の証拠金を明確に分離し、片方のリスクがもう片方に直接波及しないようにする
  • 先物口座の証拠金計算(未実現損益・維持マージン率など)をシンプルに保つ
  • 規制上、資産の分別管理を求められる区分がある

といった理由から、ウォレットを分けて管理する設計を採用していることが一般的です。すべての取引所がこの構成というわけではなく、現物・先物の資産を一つの口座として扱う「ユニファイド口座」方式を採用している取引所もあります。どちらの方式かは取引所ごとに異なり、また同じ取引所でも将来的に方式が変更される可能性があります。具体的な口座構成は必ず利用する取引所の公式ドキュメントで確認してください。

ボットが踏みやすい落とし穴

落とし穴1: 残高取得APIが「見ているウォレット」を勘違いする

ccxtのような統一ライブラリを使っていても、fetch_balance()がデフォルトでどのウォレットを見るかは取引所によって異なります。先物取引をするつもりでAPIキーを発行し、資金を先物ウォレットに移したつもりでも、実際には現物ウォレットに資金が残っていて、先物側の残高取得では0が返ってくる、という状況が起こります。

import ccxt

exchange = ccxt.mexc({
    "apiKey": "YOUR_API_KEY",
    "secret": "YOUR_SECRET",
    "enableRateLimit": True,
})

# 現物ウォレットの残高
spot_balance = exchange.fetch_balance({"type": "spot"})

# 先物(無期限契約)ウォレットの残高
swap_balance = exchange.fetch_balance({"type": "swap"})

print(spot_balance["USDT"]["free"], swap_balance["USDT"]["free"])

typeパラメータの名称(spot / swap / future / marginなど)や、そもそもtype指定が必要かどうかは取引所ごとに異なります。「残高が0なのにAPIキーが間違っているわけでもない」という不具合に遭遇したら、まずは見ているウォレットが正しいかを疑ってみるとよいでしょう。

落とし穴2: 発注時の「残高不足」エラーが資金移動忘れだと気づかない

MEXCでAPI自動売買を始める手順でも触れているように、取引所のエラーメッセージは単なる失敗の通知ではなく、状態を知るための手がかりになります。先物口座への発注で残高不足エラーが出た場合、本当に資金が足りないのではなく、資金そのものは現物ウォレットにあって先物ウォレットには移されていない、というケースが実務上は多く見られます。両者を区別せずにリトライを繰り返す実装にしてしまうと、原因が分からないまま無駄なリトライログだけが積み重なることになります。

落とし穴3: 複数取引所対応ボットでウォレット構成の違いを見落とす

複数取引所対応ボットの設計で解説した共通インターフェースのように、取引所間の差異を1箇所に閉じ込める設計を採用している場合でも、「ウォレットが分かれているかどうか」「資金移動の方法」は取引所ごとに大きく異なるため、アダプター層で明示的に吸収する必要があります。ここを共通化しすぎると、片方の取引所では動くのにもう片方では残高が見えない、という不具合の原因になります。

資金移動(内部振替)の基本的な考え方

現物と先物でウォレットが分かれている取引所では、取引所の管理画面やAPIから「内部振替(トランスファー)」を行うことで、ウォレット間で資金を移動できるのが一般的です。ccxtでは一部の取引所に対して統一的なtransfer()メソッドが用意されています。

# 現物ウォレットから先物ウォレットへUSDTを移動するイメージ(対応取引所の例)
exchange.transfer(
    code="USDT",
    amount=100,
    fromAccount="spot",
    toAccount="swap",
)

内部振替は多くの場合、外部への送金(ブロックチェーン上の送金)とは異なり手数料がかからず、処理も数秒程度で完了することが一般的です。ただし対応状況や呼び出し方は取引所ごとに異なり、取引所のAPI仕様変更に備えるボット設計で触れたとおり、こうした仕様は将来変更される可能性もあります。本番コードに組み込む前には、必ず対象取引所の最新のAPIドキュメントで仕様を確認してください。

自動振替を組み込むかどうかは慎重に判断する

証拠金が不足しそうになったら現物ウォレットから自動で先物ウォレットに資金を移す、という仕組みを組みたくなるかもしれません。便利ではありますが、振替操作自体もAPIキーの権限範囲に含まれるため、APIキーの安全な管理の考え方に沿って、振替に必要な権限だけを絞って付与し、想定外の金額が動いていないかを監視・通知する仕組みとあわせて導入することをおすすめします。自動振替を入れる場合でも、最初は少額・低頻度での動作確認を十分に行ってから本格稼働させるのが安全です。

まとめ

  • 多くの海外取引所では現物ウォレットと先物ウォレットが内部的に分かれており、ユニファイド口座方式の取引所もある
  • 残高が意図せず0に見える、発注で残高不足エラーが出る不具合は、ウォレットの取り違えが原因であることが多い
  • ccxtで残高を取得する際はtypeパラメータでどのウォレットを見ているかを明示的に意識する
  • ウォレット間の資金移動は「内部振替」で行うのが一般的だが、対応状況や呼び出し方は取引所ごとに異なり変更もありうる
  • 自動振替を組む場合はAPIキーの権限を絞り、監視・通知とあわせて慎重に導入する

いずれも取引所固有の仕様が絡む部分なので、実装前には必ず公式ドキュメントで現在の仕様を確認してください。