エンジニアリング

252ストラテジーが暴いたTradingViewブローカーの3つの未モデル化挙動

パリティ・コーパスが167件から252件に増える過程で見つかった、強制清算・FX対応affordability・トレーリングストップarmingという3つのバグの詳細。
読了 約9分#parity#corpus#engine#tradingview#broker-emulation

パリティ・コーパスを最後に公開した5月時点で、参照ストラテジーは167件、そのうちTradingView対比で厳密なトレード単位パリティに達していたのは165件だった。あれから増え続けている。コーパスは現在 252ストラテジー——251件が「excellent」パリティ(99.6%)、ドキュメント化済みアノマリーが1件、failはゼロになった。

見出しの数字自体は地味な話だ。面白いのはそこに至る過程で出てきたもの——既存167件では踏まれなかったコードパスを、実在または合成ストラテジーが踏んだことで初めて見つかった、TradingViewブローカーの未モデル化挙動が3つある。本稿はそのうち3件の詳細だ。

コーパスの現状

corpus/validation_report.md (2026-06-29)
  Total probes:    252
  Excellent:       251  (99.6%)
  Anomaly:           1
  Strong/Moderate/Weak/Minimal/Fail: 0
 
  TV trades:      389,590
  Engine trades:  389,688
  Matched:        389,468

コーパス全体で見ると、エンジンはTradingViewより98件多くトレードを出している——参照トレード389,590件に対し0.025%のずれだ。これは集計値であって、ストラテジー単位で見ればほぼ全てが後述の1件のアノマリーに集約される。残り251件に薄く広がったノイズではない。

最新6件のプローブは drawing カテゴリだ。Pineの描画オブジェクト(line.get_price、label/boxのジオメトリ)をチャート上の注釈としてだけでなく、バックテスト時データとして読み取るランタイムを実装した際に追加した。手動で固定したトレンドラインを動的サポートレベルとして使うような、描画した線を計算上の状態として使うストラテジーでは、スクリプトが既に削除したオブジェクトに対してもエンジンがジオメトリを返し続ける必要がある——TradingViewと同じ挙動だ。6件とも問題なくパスした。

唯一のアノマリー、隠さず明記する

一致しないストラテジーが1件あるが、それをフィルタして除外することはしない。anomaly-equity-mirror-strategy-equity-01 は、マージン許可判定における1倍equity境界ちょうどに位置する。TradingViewのブローカーエミュレータはこの境界上で非単調な挙動を示す——equity超過のエントリーを通すこともあれば、equity不足のエントリーを拒否しないこともあり、bar単位で一貫性がない。我々のエンジンは決定論的だ。同じequityなら、毎回同じ許可判定になる。これはPineのドキュメント化されたマージン仕様に照らせば正しい。だが同時に、TradingViewのブローカーがこの一点の境界で実際にやっていることとは違う、ということも証明できる。これに合わせるには我々の側で非決定性を持ち込む必要があり、それはしない。このギャップは隠さずドキュメント化されている。252件中、唯一の例外だ。

バグその1:TradingViewなら強制クローズしていたポジション

TradingViewのブローカーは、マージン要件を超えたポジションをいつ閉じるかをストラテジーロジックの判断に委ねない——清算する。レバレッジまたはショートポジションのequityが十分に下がり、必要マージンがアカウントequityを超えると、TradingViewは strategy.exit() の条件が満たされているかどうかに関わらず、次のbarでポジションを強制クローズする。

エンジンはこれをモデル化していなかった。ポジションを保持し続け、最終的にストラテジー自身のロジックがクローズするのを待っていた——タイトなストップを持たないストラテジーでは、TradingViewなら既に清算済みのポジションが何十barも残り続けることになる。レバレッジが大きいコーパスのストラテジーでは、エントリーとイグジットのロジックが他の全ての点で完全に一致していても、ストレス下のequityカーブが大きく異なる結果になっていた。

修正では、アカウントequityがマージン要件を割った時点で強制清算を追加した。清算価格の算出方法はTradingViewのブローカーと同一にし、その上で清算数量をシンボルのlot stepに合わせて量子化する——TradingViewは取引所が許可するより細かい分数ロットを清算しない。今のエンジンも同様だ。

これが最も影響するのは、無期限契約でレバレッジまたはショートのストラテジーをバックテストしているユーザーだ。ドローダウン中のequityカーブが妙に滑らかに見えていたなら、これが原因だった可能性が高い。

バグその2:FXを考慮していなかったaffordabilityチェック

Pineストラテジーは、シンボルのquote currencyとは異なるアカウントカレンシーを宣言できる——例えば currency.INR でUSDTペアを取引する場合だ。注文を実行できるか判定する前に、TradingViewのブローカーは注文のnotional valueをその時点のFXレートでアカウントカレンシーに変換し、同じカレンシーでequityと比較する。

エンジンのaffordabilityゲートはこの変換をスキップしていた。FXレートが1:1であるかのように、USDT notionalをINR建てのequityへそのまま比較していた——つまり currency.INR を宣言したストラテジーは、実際よりおよそ83倍affordableに見えていたことになる(1 USDTがおおよそその額のルピーに相当するため)。影響を受けるスクリプトでは、TradingViewのブローカーならunaffordableとして拒否していたはずのトレードを、エンジンがほぼ2倍の件数受け入れていた。

修正では、affordabilityチェックが走る前に、必要マージンへアカウントカレンシーのFXレートを適用するようにした。これはTradingViewが注文パイプライン上で適用する箇所と同じ位置だ。USDではない、あるいはUSDTではないアカウントカレンシーでUSDまたはUSDT建てのシンボルをバックテストしているなら、これが静かにトレード数を水増ししていたバグだ。

バグその3:一度もarmされなかったトレーリングストップ

これは我々より先にユーザーに見つけてほしかったケースだ。サイレントに失敗するからだ。strategy.exit() はトレーリングストップの指定方法を2つサポートしている。エントリー相対距離を指定する trail_points と、絶対的なアクティベーション価格を指定する trail_price だ。

// armed correctly today; trail_price-only calls used to never arm at all
strategy.exit("TS", from_entry = "Long", trail_price = close * 1.05, trail_offset = 10)

エンジンのarming条件は trail_points しか見ていなかった。trail_price のみで指定したトレーリングイグジットは、コンパイルエラーなし、実行時エラーなしで通り、そして一度もarmされなかった——ポジションはトレーリングストップが有効にならないまま、バックテスト全体を通してサイレントに放置される。例外もログ行もなく、トレードも発生しない。ストラテジーは保護されているように見えていた。実際には違った。

これを見つけたのは、trail_price 単独でのアクティベーションだけを切り出して検証するコーパスプローブがあったからだ——まさに validation カテゴリが存在する理由そのものの、狭く合成的なストラテジーだ(コーパス内訳の記事を参照)。儲かるかどうか、現実的かどうかは問題ではなく、Pineの一機能だけを単独で検証できるように作られている。だからこそ、このような回帰バグは「全体の数字を見れば問題なさそうだった」の裏に隠れられない。

修正では、trail_pointstrail_price のいずれかが存在すればトレーリングストップをarmするようにした。trail_price を使ったPineストラテジーを書いたことがあって、equityカーブがトレーリングストップなしのように見えて怪しいと感じたことがあるなら——この修正以前のどのエンジンバージョンでも、実際にトレーリングストップは存在していなかった。

これがコーパスを持つ意味そのものである理由

この3つのバグはいずれもコードレビューでは見つからなかった。形の合ったストラテジーがエンジンを通過し、TradingViewと一致しない数字を出したことで見つかった。167ストラテジー、そして252、さらに増え続けるというホールドアウトされたバージョン管理コーパスを、ひと握りのスモークテストの代わりに維持し続けている理由はそこにある。トレード件数やエントリー価格を変えないバグでも、最も重要な数字——そのストラテジーが結局いくら儲けた、あるいは失ったか——は変えてしまうことがある。それを捕まえる唯一の方法は、同じground truthに対する同じdiffに、より多くの形のストラテジーを通し続けることだ。

ギャップを見つけるたびにプローブを追加し続け、数字も——良い結果も、唯一のアノマリーも——そのまま公開し続ける。

ここから先へ