メインコンテンツにスキップ
エンジニアリング浮動小数点API統合
浮動小数点はあなたのお金を奪う:取引システムにおける精度

浮動小数点はあなたのお金を奪う:取引システムにおける精度

丸め処理は、すでに解決済みの問題のように聞こえます。数値があって、ステップサイズがあって、最も近い有効な値に丸めるだけ。ユーティリティ関数を1つ、20行書けば、あとは永久に使い回せる。そう思えます。

しかし、それは1つの関数では済みません。複数の取引所に注文を送るシステムでは、精度は1つの領域であり、そこから生まれるバグには厄介な共通点があります:例外を投げないのです。少しだけ間違った注文を出すか、ユーザーには見えない理由で拒否される注文を出すのです。

私たちの注文パスから見つかった3つの実際の問題を使って、この作業が実際にどのようなものかをお見せします。

要点

  • IEEE-754はステップの整合を信頼できないものにします:0.29を100倍すると28.999999999999996になり、切り捨てると1ステップ分小さすぎる値になります。
  • 3つの取引所は互換性のない3つの最小注文ルールを課します。1つはUSDT価値を、1つは契約数を、1つは両方をチェックします。
  • この領域で最悪のバグは、ある取引所には存在しないルールを勝手に作り出し、それに照らして有効な注文を拒否したことから生まれました。
  • APIが与えていない制限を合成してはいけません。取引所に想定元本の最小値がないなら、他のフィールドから計算するのは防御的な姿勢ではなく、捏造です。
  • 取引所のメタデータが欠けている場合は、デフォルト値にフォールバックするのではなく、エラーを発生させるべきです。誤ったデフォルト値は、静かに誤った注文を生み出します。

1. 算術の問題:丸めが数値を動かす

最もシンプルなケースから始めましょう。なぜなら、それは人々が安全だと思い込んでいるケースだからです。

取引所は、ステップサイズの倍数で数量を受け付けます。手元に 0.29 があり、ステップは 0.01 です。この値はすでに有効なので、切り捨てても何も起きないはずです。標準的な実装では、スケールアップして、切り捨てて、またスケールダウンします:

0.29 / 0.01 = 28.999999999999996   ← IEEE-754, not 29
floor(...)  = 28
28 × 0.01   = 0.28

すでに整合していた値が、静かに1ステップ分下に動き、より小さな注文の方向にずれました。例外もなく、警告もなく、しかも値とステップの特定の組み合わせでしか起きません。これこそが、気づきにくさの原因です。

修正方法は、スケーリング中にイプシロンを適用することです。表現上のノイズを吸収できるほど大きく、しかし 0.289999 のように本当に下回っている値は正しく切り捨てられるほど小さい値です。これを発見した経緯についてはこのプラットフォームを作る上で私が過小評価していたことに書きました。ここで重要なのは、それがどのカテゴリーに属するかということです。

なぜなら、数量に対する算術が信頼できないと認めた瞬間から、数量に触れる他のすべてを見直し始めるからです。本当の驚きは、そこにあります。

2. 3つの取引所、3つの互換性のない最小値

どの取引所も最小注文を課します。ルールが異なるのはその値であって、その形ではないと思うでしょう。ところが、形が異なるのです:

取引所ルール
Binanceqty × price >= minNotional(USDT価値のチェック)
Bybitqty >= minOrderQty かつ notional >= minNotional(2つのチェック)
OKX無期限契約qty >= minSz(契約数、想定元本のチェックは一切なし)

この表は、同じ注文について3つの異なる質問がなされていると読んでください。Binanceはそれがいくらの価値かを問います。OKXはそれが何契約かを問います。Bybitは両方を問い、注文はどちらか一方だけを満たしていても拒否されることがあります。

OKXにはさらにややこしい点があります:数量は通貨ではなく契約単位なので、この話が意味を持つ前に、契約サイズが両者を変換しなければなりません。ある取引所で「0.5 BTC」を意味する数量が、別の取引所では「50契約」を意味するのです。

私たちが行き着いたアーキテクチャ上の結論はこうです:各取引所は自分自身の注文を検証する。 戦略エンジンには、どの取引所と通信しているかについての条件分岐が1つもありません。注文をアダプターに渡し、アダプターが自分自身のルールを適用します。これらのルールを1つの抽象化の背後に統一しようとするたびに、4つ目のケースが現れた瞬間に、その抽象化は嘘になりました。

3. 高くついたもの:存在しないルールを作り出す

これは、他のエンジニアに最も避けてほしいバグです。なぜなら、その間違いは勤勉さのように見えるからです。

OKXの無期限契約には想定元本の最小値がありません。私たちはそれを知りませんでした。他の取引所からの類推で推論し、私たちはそれを計算してしまいました:最小契約数×契約サイズ×価格です。それは合理的に見えました。もっともらしいUSDTの数字を生み出しました。そして、それは作り話でした。

どこで破綻したのかは具体的で、たどってみる価値があります。DCA戦略は、セーフティオーダーをエントリー価格の下に配置します。私たちの合成した最小値はある1つの価格で計算され、それより低い価格の注文と比較されました。そのため、はしごの一番上では通った同じ数量が、下の方では通らなくなったのです。

結果として:ちょうど 0.1 契約の注文は、取引所の最小値そのものであり、受け付けられたはずなのに、システムから出る前に自分自身のバリデーターに拒否されました。取引所はそれらを見ることさえありませんでした。ユーザー側から見ると、戦略は設定されたとおりに動かなかっただけでした。

この教訓は、取引をはるかに超えて一般化できます:

APIが与えていない制約を合成してはいけません。 取引所が契約単位で最小値を公開しているなら、最小値は契約単位です。価値ベースの等価物を導き出すのは慎重さではなく、ルールを作り出してそれをユーザーに押し付けることです。

後から振り返ると、その兆候は、私たちが計算した値が価格に依存していたのに対し、取引所の実際のルールはそうではなかったことでした。あなたが作った制約のバージョンが、本物には存在しない入力を持っているなら、いつであれ、あなたはモデル化しようとしたものとは別物を作ってしまっています。

4. メタデータの欠落はエラーであるべきで、デフォルトではない

最後の項目はバグというよりは方針であり、これが最初の3つが静かに再発するのを防いでいます。

どの取引所も、シンボルごとのメタデータを提供します:ステップサイズ、ティックサイズ、最小数量、契約サイズです。そのどれもが欠けている可能性があり、誘惑に駆られてやりがちなのがフォールバックです。ステップサイズがない? 0.001 と仮定しよう。最小数量がない? ゼロと仮定して、取引所に判断させよう。

私たちはその逆をします:必須フィールドが欠けていれば、注文が構築される前に、即座に例外を投げます。下流のどのコードも、推測の上に何かを組み立てる機会を得ることはありません。

その理由は、結果の非対称性にあります。投げられたエラーは、修正できる人の目の前に、すぐに現れる大きな失敗です。デフォルト値は、間違ったサイズの注文を出し、後になってお金という形で正体を現す、静かな誤答です。無視できないエラーと、検証できない数字のどちらを取るかなら、エラーの方が毎回コストが低いのです。

これはまた、メタデータ層が自分の知っていることについて正直であることを意味します。必須フィールドを読むコードは、本物の値か例外を受け取ります。本物のデータの形を装ったプレースホルダーを受け取ることは決してありません。

持ち帰るべきこと

  • ステップとティックの整合は、書式設定ではなく、値を動かしうる算術として扱いましょう。
  • 本当に異なるルールを統一してはいけません。 取引所ごとの検証は取引所が追加されても正しいままですが、互換性のないルールをまたぐ共有の抽象化はそうではありません。
  • APIが明示していない制限を導き出してはいけません。 あなたが作った制約のバージョンが、本物には存在しない入力を取るなら、それは別の制約です。
  • メタデータが欠けていたら、大きな声で失敗しましょう。 デフォルト値は、目に見えるエラーを、目に見えない誤った価格の注文に変えてしまいます。

精度対応は地味な作業で、機能一覧に載ることは決してありません。しかし、これらのバグはどれも、間違った注文か、そもそも存在しなかった注文を生み出しました。誰かに代わって取引するシステムにおいて、それらは、その人のお金を丸ごと失うのと同じカテゴリーの失敗なのです。


関連記事

学んだことを実践しよう。

無料アカウントを作成し、戦略をテストして、最初のボットを起動しましょう。

無料で始める
このコンテンツは教育目的のみであり、金融アドバイスではありません。仮想通貨取引には重大なリスクが伴います。失っても許容できる金額以上の投資はしないでください。