暗号資産取引所 API は署名とレート制限で分かれる── 取引所を増やしても壊れない構成にする
REST と WebSocket という形は各社共通でも、署名の作り方とレート制限の数え方は揃っていません。差異をどこに閉じ込めるかを、bitFlyer と Binance の実装で具体的に見ます。

複数の取引所をつなぐとき、署名とレート制限の違いはアダプタで吸収できます。厄介なのはその先で、返ってくるデータの形が揃っていないことです。価格も数量も時刻も、名前が違い、型が違い、単位が違います。
ここを「とりあえず動く形」で個別に変換していくと、取引所が増えるたびに変換処理が散らばり、どこかで単位を取り違えます。データの正規化は、アダプタの中でも独立した層として設計するべき部分です。本稿では bitFlyer と Binance の実際のフィールドを並べ、正規化レイヤに何を持たせるかを具体的に決めていきます。仕様の出典は末尾に挙げています。
まず同じ情報を指しているフィールドを比べます。左が bitFlyer Lightning API、右が Binance Spot API です。
product_code(BTC_JPY、FX_BTC_JPY)/ symbol(BNBUSDT、BNBBTC)timestamp・exec_date(ISO 8601 の文字列)/ time・openTime・closeTime(ミリ秒の整数)size / qtybids / asks に price と size を持つ要素の配列 / bids / asks に価格と数量の 2 要素の組side("BUY" / "SELL" / 空文字列)/ isBuyerMaker(真偽値)volume・volume_by_product / volume(base 資産)・quoteVolume(quote 資産)同じ名前が別の意味を持つ例もあります。volume は両方に存在しますが、Binance では base 資産建ての出来高で、quote 資産建ては quoteVolume という別のフィールドです。名前が一致しているぶん、取り違えても気づきにくくなります。

Binance は価格も数量も文字列で返します。price も qty も volume も、型は string です。これは実装の都合ではなく、精度を落とさずに渡すための設計です。
受け取った側が float() で変換した時点で、その意図は失われます。二進浮動小数点では十進の小数を正確に表せないため、桁数の多い数量では丸めが入ります。少額の誤差でも、注文数量として送り返せば拒否される可能性があり、残高の突合では毎回わずかにずれ続けます。
正規化レイヤでは十進の固定小数点として扱います。Python なら Decimal、その際に文字列から直接構築するのが要点です。
from decimal import Decimal
# 正しい。文字列のまま Decimal へ渡す
qty = Decimal("0.00123456")
# 誤り。float を経由した時点で誤差が入る
qty = Decimal(float("0.00123456")) # Decimal('0.001234560000000000...')
bitFlyer のように数値型で返ってくる取引所でも、扱いは揃えます。JSON をパースした時点で float になっているため、文字列へ落としてから Decimal を作るか、パーサに parse_float=Decimal を渡して最初から Decimal で受けます。片方の取引所だけ float のまま扱うと、比較や集計で型が混ざります。
約定履歴の売買方向は、取引所によって表し方が根本的に違います。bitFlyer の side は "BUY" または "SELL" で、注文の向きをそのまま示します。ただし空文字列になる場合があります。
一方 Binance の isBuyerMaker は真偽値で、買い手がメイカーだったかどうかを示します。これは売買の向きそのものではありません。テイカー側の向きに直すと、isBuyerMaker が真のときテイカーは売り、偽のときテイカーは買いになります。
「どちらが仕掛けたか」を知りたい分析でこれを取り違えると、売り圧力と買い圧力が反転します。符号が逆になるだけの誤りは、動作が止まらないので発見が遅れます。正規化レイヤでは、向きを表す型を 1 つに決めて、取引所ごとの表現をそこへ寄せます。
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
class Side(Enum):
BUY = "buy"
SELL = "sell"
@dataclass(frozen=True)
class Trade:
"""業務ロジックが見るのはこの形だけ。生のフィールド名は外へ出さない。"""
symbol: str # 正規化した表記に統一する(例 "BTC/JPY")
price: Decimal
size: Decimal # base 資産建て。quote 建ては持たせない
side: Side | None # テイカー側の向き。判定できないときは None
at: datetime # tz 付き。naive な datetime は入れない
def from_binance(row: dict) -> Trade:
return Trade(
symbol="BTC/USDT",
price=Decimal(row["price"]), # 文字列のまま渡す
size=Decimal(row["qty"]),
# isBuyerMaker は「買い手がメイカーか」。テイカーの向きは逆になる。
side=Side.SELL if row["isBuyerMaker"] else Side.BUY,
at=datetime.fromtimestamp(row["time"] / 1000, tz=timezone.utc),
)
def from_bitflyer(row: dict) -> Trade:
raw = row.get("side") or "" # 空文字列で返ることがある
return Trade(
symbol="BTC/JPY",
price=Decimal(str(row["price"])),
size=Decimal(str(row["size"])),
side=Side(raw.lower()) if raw else None,
at=datetime.fromisoformat(row["exec_date"]).replace(tzinfo=timezone.utc),
)
時刻の表現も揃っていません。bitFlyer は ISO 8601 の文字列(2015-07-08T02:43:34.823 のような形式)で、Binance はミリ秒の整数です。
ここで注意が要るのは、ISO 8601 の文字列にタイムゾーンの指定が含まれていない場合です。パースするとタイムゾーンを持たない値になり、そのまま比較や差分の計算に使うと、実行環境のタイムゾーンに依存した結果になります。開発機と本番のタイムゾーンが違えば、そこで結果が変わります。
正規化レイヤを出る時点で、すべての時刻を UTC のタイムゾーン付きに揃えます。表示のときだけ現地時間へ変換します。ミリ秒とマイクロ秒の取り違えも同じ場所で吸収しておくと、後段で単位を意識せずに済みます。
銘柄の表記は取引所ごとにばらばらです。bitFlyer は BTC_JPY のようにアンダースコアで区切り、証拠金取引には FX_BTC_JPY という別の表記を使います。Binance は BNBUSDT のように区切りなしで連結します。
区切りがない表記は、機械的に分解できません。BNBUSDT を base と quote に割るには、扱う quote 資産の一覧を知っている必要があります。文字列の長さや位置で切る実装は、新しい銘柄が増えた時点で壊れます。
したがって、社内の正規表記を 1 つ決めたうえで、取引所ごとの変換表を明示的に持ちます。一覧は取引所の銘柄情報エンドポイントから取得して定期的に更新し、変換できない銘柄は握りつぶさずに例外として扱います。知らない銘柄を黙って無視する実装は、取扱いが増えたときに一部の銘柄だけ処理されない状態を作ります。
板の差分更新を扱う場合、版の管理方法が問題になります。Binance の板には lastUpdateId という更新 ID があり、差分がどこから続くのかを判定できます。bitFlyer の板には同種の ID がなく、判断の材料はスナップショットと更新の到着順になります。
正規化レイヤでは、この差を隠さずに持たせます。版を持つ取引所では ID の連続性で欠損を検知し、持たない取引所では定期的にスナップショットを取り直す。共通の型に押し込めて「どちらも同じ」と扱うと、欠損を検知できない取引所で板が静かにずれます。同じインタフェースで扱いたいのは業務ロジックから見た形であって、整合性の担保方法まで同じにする必要はありません。
版がある場合でも、スナップショットと差分をつなぐ順序には決まりがあります。差分の購読を先に始めてから、スナップショットを取得し、スナップショットの版より古い差分を捨てて、そこから積み上げる。逆にスナップショットを先に取ると、取得している間に届いた差分が抜けます。この手順は取引所のドキュメントに書かれていることが多いので、実装前に確認します。
板の状態を持つ側にも注意が要ります。価格を浮動小数点の連想配列の鍵にすると、同じ値段が別の鍵として登録されることがあります。鍵は文字列か十進の値で持ち、数量がゼロになった段階で削除します。ゼロのまま残す実装は、板の深さを数えるときに誤差を生みます。
取引所 API のデータは、銘柄・時刻・数量・売買方向のいずれも表現が揃っていません。文字列で返る数値は文字列のまま十進で受け、float を経由させない。売買方向は「向き」と「メイカーかどうか」が混ざるため、テイカー側の向きに寄せた型を 1 つ決める。時刻は正規化レイヤを出る時点で UTC のタイムゾーン付きに揃える。銘柄は社内の正規表記と変換表を 1 か所に置き、変換できないものは例外にする。板の版管理は取引所ごとに方法が違うため、共通化するのは業務ロジックから見た形だけにとどめる。この 5 点を最初に決めておけば、取引所を増やしたときに壊れるのは変換表だけで済みます。
金融テクノロジー総合研究所では、複数取引所のデータを扱う基盤の設計・実装を受託しています。既存システムのデータ不整合の切り分けや、正規化レイヤの再設計からもご相談いただけます。暗号資産取引所 API 開発の詳細もあわせてご覧ください。ご相談はお問い合わせよりご連絡ください。
FTL の技術スタックと受託開発の進め方は FTLの技術とは? にまとめています。