複数取引所対応ボットの設計とは?共通インターフェースで拡張しやすくする方法


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

1つの取引所向けに書いたボットを、あとから別の取引所にも対応させようとして「取引所ごとにコードが分岐しまくって収拾がつかない」となった経験はないでしょうか。取引所を増やすこと自体は難しくなくても、増やし方を間違えると保守がどんどん重くなります。

本記事では、複数取引所に対応するボットを設計するときに押さえておきたい考え方と、共通インターフェースで差異を吸収するアーキテクチャパターンをコード付きで整理します。ccxtの基本的な使い方は ccxtとは?Pythonで仮想通貨ボットを自作する第一歩 で解説しているので、まだの方はあわせて参考にしてください。

なぜ複数取引所に対応させるのか

複数取引所への対応を検討する動機は、一般的に次のようなものです。

  • 単一取引所の障害・メンテナンスへの耐性: 1つの取引所がメンテナンスやAPI障害で止まっても、ボット全体が止まらない
  • 上場銘柄の違いへの対応: 取引したい銘柄がすべての取引所に上場しているとは限らない
  • 手数料・流動性の比較: 取引所によって手数料体系や板の厚みが異なる
  • 将来の拡張性: 最初から1取引所専用に密結合したコードを書くと、あとから増やすときの改修コストが大きくなる

一方で、複数取引所対応は「対応取引所を増やせば増やすほど良い」というものでもありません。次の章で触れるコストを踏まえたうえで、必要な範囲にとどめる判断も重要です。

対応させる前に考えるべきコスト

複数取引所対応には、見落とされがちなコストがあります。

  • 監視対象が増える: 取引所の数だけ死活監視・残高確認・障害切り分けの対象が増えます
  • APIキー・資金管理が煩雑になる: 取引所ごとにAPIキーと入金資金を分けて管理する必要があります。APIキーの安全な管理については 仮想通貨取引所のAPIキーは危険?安全な発行・管理の5つの鉄則 にまとめています
  • 取引所ごとの癖を吸収するコストがかかる: シンボル表記、最小注文単位、価格の刻み幅(tick size)、レートリミットの方式などが取引所ごとに異なります
  • 規約・KYC要件の違い: 取引所によって利用規約や本人確認の要件が異なり、想定していた使い方ができないこともあります

「対応取引所を増やす」という判断は、開発コストだけでなく運用コストも増えることを前提に検討するとよいでしょう。

設計の基本方針: 差異を吸収する層を作る

複数取引所対応で最も避けたいのは、ボットの戦略ロジック(いつ買うか・いつ売るか)の中に、取引所固有の処理が直接書かれてしまうことです。

# 避けたいパターン: 戦略ロジックに取引所判定が混ざる
def place_order(exchange_name, symbol, side, amount):
    if exchange_name == "mexc":
        # MEXC固有の処理...
        pass
    elif exchange_name == "bitget":
        # Bitget固有の処理...
        pass

このような分岐が戦略コードのあちこちに現れると、取引所を1つ追加するたびに全箇所を洗い出して修正する必要が出てきます。代わりに、**取引所ごとの差異を1箇所に閉じ込める「共通インターフェース層」**を用意するのが基本方針です。

幸い、ccxtはすでに取引所ごとのAPI差異(エンドポイントの形式・認証方式など)をかなりの部分吸収してくれています。複数取引所対応の設計は、「ccxtが吸収してくれない部分」をどう扱うかがポイントになります。ccxtがどこまで面倒を見てくれるかは ccxtの使い方まとめ|主要メソッド早見表 も参考になります。

アーキテクチャ例: 共通インターフェース + アダプター

具体的には、次のような構成にします。

  1. 戦略ロジックが呼び出す共通インターフェース(抽象クラスやプロトコル)を定義する
  2. 取引所ごとに、そのインターフェースを実装するアダプタークラスを用意する
  3. 戦略ロジックは共通インターフェースだけを見て動き、取引所固有の処理はアダプター内に閉じ込める
from abc import ABC, abstractmethod

class ExchangeClient(ABC):
    """戦略ロジックが依存する共通インターフェース"""

    @abstractmethod
    def fetch_balance(self, currency: str) -> float:
        ...

    @abstractmethod
    def fetch_price(self, symbol: str) -> float:
        ...

    @abstractmethod
    def create_order(self, symbol: str, side: str, amount: float) -> dict:
        ...


class CcxtExchangeClient(ExchangeClient):
    """ccxt対応取引所向けの共通実装。取引所固有の癖はここで吸収する"""

    def __init__(self, ccxt_exchange, min_order_amounts: dict):
        self.exchange = ccxt_exchange
        self.min_order_amounts = min_order_amounts

    def fetch_balance(self, currency: str) -> float:
        balance = self.exchange.fetch_balance()
        return balance["free"].get(currency, 0.0)

    def fetch_price(self, symbol: str) -> float:
        ticker = self.exchange.fetch_ticker(symbol)
        return ticker["last"]

    def create_order(self, symbol: str, side: str, amount: float) -> dict:
        min_amount = self.min_order_amounts.get(symbol, 0)
        if amount < min_amount:
            raise ValueError(f"{symbol}の最小注文数量({min_amount})を下回っています")
        return self.exchange.create_order(symbol, "market", side, amount)

呼び出し側(戦略ロジック)は、実際にどの取引所を使っているかを意識しません。

import ccxt

def build_client(exchange_id: str, api_key: str, secret: str) -> ExchangeClient:
    exchange_class = getattr(ccxt, exchange_id)
    exchange = exchange_class({
        "apiKey": api_key,
        "secret": secret,
        "enableRateLimit": True,
    })
    # 取引所ごとの最小注文数量は設定ファイルなどで管理する
    min_amounts = {"mexc": {"BTC/USDT": 0.0001}, "bitget": {"BTC/USDT": 0.0001}}
    return CcxtExchangeClient(exchange, min_amounts.get(exchange_id, {}))

def run_strategy(client: ExchangeClient):
    price = client.fetch_price("BTC/USDT")
    balance = client.fetch_balance("USDT")
    if balance > 100:
        client.create_order("BTC/USDT", "buy", 0.001)

この形にしておくと、新しい取引所を追加するときはbuild_clientにケースを増やすだけで済み、run_strategy側の戦略ロジックには一切手を入れずに済みます。テストの書き方については ボットのテストコードはどう書く?pytestとモックで検証する基本 で解説している通り、ExchangeClientをモックに差し替えれば戦略ロジックだけを単体テストできるのも、この構成のメリットです。

実装の落とし穴

共通インターフェースを作っても、次のような点は取引所ごとに個別に注意する必要があります。

シンボル表記の違い

ccxtはBTC/USDTのような統一シンボル表記を基本的に使えますが、取引所によっては先物・現物で微妙にシンボルの扱いが異なることがあります。設定ファイルなどでシンボルを一元管理し、取引所固有の変換が必要な場合はアダプター内で吸収します。

レートリミットは取引所ごとに別管理にする

複数取引所を同時に叩く場合、レートリミットの管理も取引所ごとに独立させる必要があります。1つのグローバルなスロットリングで済ませてしまうと、片方の取引所の制限がもう片方の取引所への呼び出しまで不必要に遅らせてしまいます。取引所単位でリクエスト予算を持たせる考え方は APIのレートリミットを設計に織り込む方法 で詳しく解説しています。

ログに「どの取引所か」を必ず残す

複数取引所対応で障害調査が一気に難しくなる理由の1つが、「どの取引所で何が起きたか」がログから分からないケースです。ログの各行に取引所IDを含めるのを徹底するだけで、調査時間は大きく変わります。ログ設計の基本方針は 自動売買ボットのログ設計とは? にまとめています。

APIキーと資金は取引所ごとに分離する

環境変数のキー名に取引所IDを含める(MEXC_API_KEYBITGET_API_KEYなど)、資金管理を取引所単位で分けるなど、設定・秘密情報の管理も取引所ごとに独立させておくと、片方の取引所でのトラブルがもう片方に波及しにくくなります。

運用面での注意

設計をきれいにしても、運用面での複雑さは別に残ります。

  • 資金分散のリスク管理: 複数取引所に資金を分散すると、1箇所あたりの資金効率は下がります。全体としてのポジションサイズ管理は 自動売買ボットの資金管理術 の考え方を取引所横断で適用する必要があります
  • 障害切り分けの複雑化: あるシグナルが実行されなかったとき、「戦略の判断ミスか」「特定の取引所のAPI障害か」を切り分けるための仕組み(取引所ごとのヘルスチェックなど)が必要になります
  • 監視の負荷増加: 稼働させる取引所が増えるほど、ボットを動かすサーバー側の監視項目やリソースも増えます。VPSでの24時間運用の考え方は 自動売買ボットをVPSで24時間動かす方法 を参照してください
PR

複数取引所を同時に監視・運用するボットは、単一取引所向けよりCPU・メモリの余裕を持たせておきたいところです。開発・検証環境を柔軟にスケールできるVPSとしてConoHaがあります。

サービス開発やテスト環境に便利な【ConoHa】

まとめ

  • 複数取引所対応は「単一障害点の解消」「銘柄カバレッジの拡大」などのメリットがある一方、監視・資金管理・規約対応のコストも増える
  • 戦略ロジックに取引所固有の分岐を直接書かず、共通インターフェース+アダプターで差異を1箇所に閉じ込めるのが基本方針
  • ccxtの統一APIは差異の多くを吸収してくれるが、シンボル表記・最小注文単位・レートリミットなどは個別に注意が必要
  • レートリミットとAPIキー・資金管理は取引所ごとに独立させ、ログには必ず取引所IDを残す
  • 共通インターフェースにしておくと、モックを使った戦略ロジックの単体テストもしやすくなる

複数取引所への対応は、必要になってから慌てて継ぎ足すより、最初の設計段階で「取引所固有の処理をどこに閉じ込めるか」を決めておくほうが、あとの拡張がずっと楽になります。まずは1取引所で共通インターフェースの形を作り、動作を確認してから2つ目の取引所を追加してみることをおすすめします。