APIのレートリミットを設計に織り込む方法|ボットが止まらない呼び出し設計


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

自動売買ボットを作っていると、必ずどこかで「レートリミットに引っかかって注文が遅れた」「一時的にAPIが止まって取引機会を逃した」という壁にぶつかります。エラーが出てから対処療法的にsleepを増やす、という進め方をしている人も多いのではないでしょうか。

本記事では、レートリミットをエラー処理の対象としてではなく、設計段階で織り込むべき制約として扱う考え方を整理します。個別のエラーへの対処法は ccxtのエラーが解決しない?実運用で踏んだ落とし穴と対処法まとめ で詳しく解説しているので、本記事は「そもそも引っかからない設計」に絞って解説します。

なぜ「あとから対処」では限界があるのか

レートリミットに引っかかるボットの多くは、次のような作りになっています。

  • 価格取得・残高確認・注文発注をすべて同じ優先度で呼んでいる
  • 複数の通貨ペアをループで順番に呼び、呼び出し回数が銘柄数に比例して増える
  • リトライ処理はあるが、リトライ自体が新たなリクエストとして制限を消費している

これらは「制限に当たってから直す」やり方の限界です。銘柄数を1つ増やしただけで急に制限に当たるようになった、という経験がある方も多いでしょう。設計段階でリクエスト量の見積もりと優先度づけをしておくことで、この手の問題は事前に防げます。

取引所ごとに制限方式が違うことを前提にする

レートリミットの方式は取引所によって異なります。代表的なのは次の2種類です。

方式 特徴
リクエスト数ベース 「1分間にN回まで」のようにシンプルに回数を数える
重み(weight)ベース エンドポイントごとに「コスト」が設定され、合計コストが上限を超えないようにする

重みベースの取引所では、板情報の全量取得のような重いエンドポイントは1回で大きなコストを消費し、単一銘柄の価格取得は軽いコストで済む、といった差があります。同じ「1回」のAPI呼び出しでも中身によって負荷が違うため、単純な呼び出し回数のカウントだけでは制限を予測できません。

※ 各取引所の制限値・重みの設定は変更されることがあるため、実装前に必ず公式ドキュメントの最新情報を確認してください。MEXCでのAPI運用で実際に踏んだ癖については MEXCでAPI自動売買を始める手順 にまとめています。

ccxtのenableRateLimitが防げること・防げないこと

ccxtにはenableRateLimit: trueを設定するだけで、取引所ごとに定義された最小呼び出し間隔を守ってくれる仕組みがあります。

import ccxt

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

これは「単一プロセスから同じインスタンスで順番に呼ぶ」ケースでは有効ですが、次のようなケースは守ってくれません。

  • 複数プロセス・複数サーバーから同じAPIキー(または同じIP)で呼んでいる場合
  • 重みベースの制限で、エンドポイントごとのコスト差までは考慮されない場合がある場合
  • そもそも呼び出し「回数」自体を減らす設計にはなっていない

つまりenableRateLimitは最低限の安全装置であり、それだけに頼った設計は不十分だということです。

設計パターン1: リクエストに優先度をつける

すべてのAPI呼び出しが同じ重要度ではありません。優先度を分けて、制限に近づいたときに何を諦めるかを決めておきます。

  1. 最優先: 注文の発注・キャンセル・約定確認(ボットの生死に直結)
  2. 中優先: 保有ポジション・残高の確認(頻度を落としても実害が小さい)
  3. 低優先: 市況把握のための価格・板情報のポーリング(WebSocketに置き換え可能なことが多い)

低優先のリクエストを間引く、あるいは呼び出し間隔を動的に伸ばすだけでも、注文系のリクエストに余裕を残せます。

設計パターン2: トークンバケットで自前のスロットリングを持つ

取引所側の制限を待つのではなく、自分のボット側にリクエスト予算(トークンバケット)を持たせる方法です。

import time
import threading

class TokenBucket:
    def __init__(self, capacity, refill_per_sec):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_per_sec = refill_per_sec
        self.last = time.monotonic()
        self.lock = threading.Lock()

    def consume(self, amount=1):
        with self.lock:
            now = time.monotonic()
            elapsed = now - self.last
            self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_sec)
            self.last = now
            if self.tokens < amount:
                wait = (amount - self.tokens) / self.refill_per_sec
                time.sleep(wait)
                self.tokens = 0
            else:
                self.tokens -= amount

# 例: 1秒あたり5トークンまで補充、バケット容量10
bucket = TokenBucket(capacity=10, refill_per_sec=5)

def call_api(weight=1):
    bucket.consume(weight)
    # ここで実際のccxt呼び出しを行う

重みベースの取引所であれば、consume(weight)のweightにエンドポイントごとのコストを渡すことで、取引所側の制限に近い形で自前の予算管理ができます。取引所の公称制限より少し厳しめの値をバケット容量に設定しておくと、複数プロセスからの同時アクセスや誤差にも余裕を持てます。

設計パターン3: 429エラーへの指数バックオフ+ジッター

自前のスロットリングをしていても、ネットワーク遅延や他プロセスからのアクセスで制限に当たることはあります。その際は一定間隔でのリトライではなく、指数バックオフとランダムなジッター(ゆらぎ)を組み合わせるのが定石です。

import random
import time

def call_with_backoff(func, max_retries=5):
    for attempt in range(max_retries):
        try:
            return func()
        except ccxt.RateLimitExceeded:
            base = 2 ** attempt
            jitter = random.uniform(0, 1)
            time.sleep(base + jitter)
    raise RuntimeError("リトライ上限に達しました")

ジッターを入れる理由は、複数のリトライが同じタイミングで一斉に再送されて再び制限に当たる「サンダリングハード問題」を避けるためです。リトライの発生自体も ボットのログ設計 で紹介している水準でログに残しておくと、どのエンドポイントがボトルネックになっているか後から分析できます。

ポーリングをやめてWebSocketに寄せるという選択肢

価格や板情報を一定間隔でREST APIに問い合わせる「ポーリング」は、銘柄数や間隔を増やすほどリクエスト消費が線形に増えていきます。多くの取引所はWebSocketでの購読(プッシュ配信)にも対応しており、これに切り替えることでREST APIのリクエスト予算をほぼ注文系だけに回すことができます。板情報の使い方自体は 板情報(オーダーブック)とは?ボットで見るべき数値と使い方 で解説しているので、あわせて参考にしてください。

すべてをWebSocketに置き換える必要はありません。「発注・確認は最小限のREST」「監視系はWebSocket」という役割分担にするだけでも、レートリミットに対する余裕は大きく変わります。

PR

本記事のコード例は MEXC のようなccxt対応取引所を想定しています。公開データ取得だけならAPIキーなしで試せるので、レートリミットの挙動を確認する練習にも向いています。

MEXCの口座を無料で開設する(紹介リンク)

まとめ

  • レートリミットは「エラーが出たら直す」ものではなく、設計段階で見積もっておく制約
  • 取引所によってリクエスト数ベース・重みベースなど方式が異なるため、公式ドキュメントの最新情報を必ず確認する
  • ccxtのenableRateLimitは最低限の安全装置であり、複数プロセスや重み差までは守ってくれない
  • リクエストに優先度をつけ、注文系を最優先・監視系を低優先にする
  • トークンバケットで自前の予算管理をし、429エラーには指数バックオフ+ジッターで対応する
  • ポーリングをWebSocketに置き換えることで、REST APIの予算を注文系に集中させられる

レートリミットの設計は地味な作業ですが、ここを丁寧にやっておくかどうかで、相場が動く一番大事な瞬間にボットが機能するかどうかが変わってきます。まずは ccxtとは?Pythonで仮想通貨ボットを自作する第一歩 で基本の呼び出し方を押さえたうえで、本記事の設計パターンを取り入れてみてください。