Zaif APIのorder_id=0で処理が落ちる件、原因と対処をまとめた
Zaif APIを使って自動売買や約定監視を実装している人向けの記事。
order_idが0で返ってくるケースを想定していなくて、処理が途中で落ちたり、注文の突合が狂ったりした経験がある人には特に読んでほしい。
自分はこれで半日溶かした。ログを追っても一見正常なレスポンスなのに、後続処理で辻褄が合わなくなる現象に最初は気づけなかった。
結論から書く。Zaifは成行注文が即座に約定した場合、order_idに0を返すことがある。これは「注文が存在しない」でも「エラー」でもなく、「注文板に残らず即約定した」という意味の0だ。この前提を知らずにorder_idをキーにして注文管理をすると、即約定した注文が管理テーブルから消えたり、二重発注のチェックが機能しなくなったりする。対処はシンプルで、order_id == 0のケースを専用の分岐で扱い、trade_idやfunds側の情報で約定を確認する設計にすればいい。
なぜこの問題が起きるか
Zaifのtrade_addは、指値注文と成行注文で戻り値の意味合いが微妙に違う。
指値注文で板に注文が残る場合は、通常のorder_id(1以上の整数)が返ってくる。ところが成行注文で即座に約定が成立した場合、板に注文自体が残らないため、Zaif側は「アクティブな注文として管理する対象がない」という扱いでorder_id: 0を返してくる。
これは他の取引所APIの感覚で実装していると想定外になりやすい。例えばbitFlyerだと成行注文でもchild_order_acceptance_idのような識別子は必ず発行される。Zaifは「注文管理する必要がないものにはIDを割り振らない」という設計思想らしく、この違いに気づかないままコードを書くと、成行注文の直後だけ処理が破綻するというバグを埋め込むことになる。
自分の場合、最初は指値注文だけでテストしていたので気づかなかった。本番に近い環境で成行の損切りロジックを動かした瞬間に、注文管理テーブルの主キー制約でエラーが出た。order_idをユニークキーにしていたので、0が複数回来た時点で衝突する。これはかなり地味に痛い不具合で、テスト環境では再現しづらい部類だと思う。
解決手順
ステップ1:レスポンスの構造を先に確認する
trade_addのレスポンスは以下のような形で返ってくる。
{
"success": 1,
"return": {
"received": 0.1,
"remains": 0,
"order_id": 0,
"funds": {
"jpy": 123456,
"btc": 0.5
}
}
}
ここで見るべきはorder_id単体ではなく、remainsとの組み合わせだ。remainsが0でorder_idも0なら「即時全約定」と判断できる。remainsが0でない場合にorder_idが0だと、それは異常系なのでエラーとして扱う必要がある。
ステップ2:即約定と通常注文で分岐する
自分は以下のような設計にした。C言語の組み込み屋的な発想で言うと、ステートマシンの状態を「発注中」「約定済み」「板残り」の3つに分けて、order_idだけに依存しない状態遷移にする。
def handle_trade_response(res: dict) -> OrderState:
order_id = res["return"]["order_id"]
remains = res["return"]["remains"]
if order_id == 0 and remains == 0:
# 即時全約定。板管理は不要、約定履歴側で追う
return OrderState.FILLED_IMMEDIATELY
if order_id == 0 and remains > 0:
# 想定外。Zaif側の一時的な不整合の可能性がある
raise UnexpectedZaifResponse(res)
# 通常のorder_idが払い出されたケース
return OrderState.PLACED(order_id=order_id, remains=remains)
ポイントはorder_id == 0を単なる異常値として弾かないこと。ここを弾いてしまうと、成行注文が正常に約定しているのにエラー扱いになって、二重発注のリトライロジックが誤作動する。自分は最初この分岐がなくて、成行注文のたびにリトライが走るという不具合を本番一歩手前で見つけた。
ステップ3:約定確認はtrade_history側でも二重チェックする
order_idが0のケースは、注文管理テーブルに登録せず、約定履歴(trade_history)のtimestampとfundsの差分で照合する設計にした。DB側ではorder_idをNULL許容のカラムにして、代わりに内部生成のUUIDを主キーにする。これで即約定の注文もちゃんとレコードとして残る。
自分のケースでは、1日平均で成行注文が20〜30件、そのうち即約定になるのは体感で7割前後だった。つまりほとんどの成行注文がorder_id=0を返してくる計算になる。これを異常系扱いにしていた最初の実装だと、7割の注文がまともに記録されないことになる。数字にすると結構怖い。
落とし穴と対処
一番はまったのは、Zaifのドキュメント上に「order_idが0になる場合がある」という説明が明確に書かれていない点だった。APIリファレンスを何周読んでも見つからず、結局はレスポンスを大量にログに吐かせて、成行注文のときだけ0が返る規則性に気づくまで時間がかかった。
もう一つの落とし穴は、テスト環境(Zaifにはtoken発行のテストモードがあるわけではないので、実際は少額の本番取引で検証することになる)だと成行注文を頻繁に試しにくいこと。板が薄い時間帯だと成行のつもりが部分約定してremainsが残ったりして、order_idが0でもremains > 0という一見矛盾したレスポンスに遭遇したこともある。この時は素直に「異常系」として例外を投げてSlack通知する実装にした。無理に自動リカバリさせようとすると、余計なバグを増やす気がしたので、ここは人間が確認する運用にしている。
この手の取引所ごとのクセは、1社対応して満足していると次の取引所でまた同じような沼にはまる。自分はZaif、bitFlyer、GMOコインと実装してきて、毎回「このAPI独自の変な仕様」に数時間から半日は溶かしている。この経験をまとめる形で、複数取引所のAPI連携パターンを実装サンプルとして整理したツールを作った(AutoTrader 実装学習キット)。Zaifのorder_id=0のような癖を吸収する設計パターンをコードごと載せている。
関連ツール
取引所APIのクセで実装が止まる問題は、Zaifに限らずどの取引所でも大なり小なり発生する。自分が実際に5取引所(Zaif/bitFlyer/GMO/Coincheck/Binance Japan)と連携する中で溜まった実装パターンを、FastAPI × React NativeのサンプルアプリとしてまとめたのがAutoTrader実装学習キット。今回書いたorder_id=0の分岐処理も、実装例の一部として含めている。
まとめ
Zaifのorder_id=0は異常値ではなく、成行注文が即約定した時の正常なレスポンスであることが多い。remainsとの組み合わせで判定し、注文管理の主キーをorder_idに依存させない設計にすれば、この問題自体は避けられる。自分は半日かけて原因を特定したが、この記事を読んだ人は同じ時間を溶かさずに済むはずだ。
著者:ぽん(@pon_freelance)
C言語実務23年、組み込み/制御系。
副業で技術記事販売と自作ツール販売をやっている。
書いているもの:
- AutoTrader 実装学習キット - FastAPI × React Native で作る外部 API 連携アプリ実装学習キット
(その他:(なし))
開発の裏側を購読できます — AutoTrader のリリースごとに「何を・なぜ・どう変えたか」を 2,000〜4,000 字で書き残しています。バグの原因、取引所 API 変更への追従、設計判断のトレードオフまで。
→ AutoTrader開発ログ(月500円・いつでも解約可)