OptiNod アカデミー

ルックアヘッドとリペイント — チャートでは完璧だったシグナルが実際の取引では存在しなかった理由

チャート上では完璧に見えるシグナルが、実戦ではその時点に存在していなかった理由を、request.security と足の確定メカニズムから解き明かします。

リペイント(Repainting)とは、インジケーターが足の確定後に過去のシグナルを描き直す現象です。ルックアヘッド・バイアス(Look-ahead Bias)とは、バックテストが未来のデータを先に見た状態でシグナルを計算してしまう誤りです。原因は異なりますが、結果は同じです。過去チャートではシグナルが完璧な位置に表示されているのに、その時点でリアルタイムにチャートを見ていたなら、シグナルはそもそも存在しなかったか、別の位置に出ていたはずです。

一般的な解釈は単純です。インジケーターがシグナルを出したら、その足で入ればよい。矢印は底値に正確に表示されており、バックテストの勝率も70%を超えている。だからそのまま信じてしまいます。この信頼が崩れるのは、リアルタイム取引の場面です。同じインジケーターをライブチャートに表示して待っていると、足が確定する前に出ていた矢印が、足の確定と同時に消えたり、1本隣へ移動したりします。バックテストに表示されていたそのシグナルは、事後的に描かれたものだったのです。

押さえるべき要点はこれです。TradingViewでバックテスト成績が良く見える戦略の多くは、未来を先に見ているコードによって良く見えているだけで、戦略そのものに優位性はありません。request.security()のルックアヘッド設定1行、未確定足で計算するオプション1つ、本質的に描き直されるツール1つが、その差を生みます。メカニズムを理解すれば、どのバックテストを除外すべきか、ライブではどのシグナルを1本遅れで受け取るべきかを判断できます。

同じ区間で事後チャートとリアルタイム時点のシグナル位置が食い違う

シグナルの確定時点とエントリー可能時点は同じではない

リペイントとルックアヘッドを理解する出発点は、足に2つの状態があることです。確定済みの過去足では、始値・高値・安値・終値が固定され、再び変わることはありません。一方、進行中のリアルタイム足では、高値・安値・終値が確定前まで何度も変化します。TradingViewのコードはこの2つの状態で異なる挙動をしますが、多くのユーザーはその違いを意識していません。

インジケーターが「この足で買い」という矢印を表示したとします。その足がまだ進行中であれば、矢印は現時点までの終値を基準に計算された暫定値です。確定前に価格の方向が変われば条件は崩れ、矢印は消えます。そのため、進行中の足に出たシグナルを見てエントリーすると、足の確定時点には存在していなかったシグナルを根拠にポジションを持つことになります。

実戦で使えるシグナルは、足が確定し、終値が決まったシグナルだけです。シグナルが確定するのは足が閉じる瞬間であり、そのシグナルを見てエントリーできる最も早いタイミングは次の足が開く瞬間です。バックテストがシグナル足の終値で約定したと仮定しているなら、その時点でわずかなルックアヘッドが含まれます。足が閉じる正確な瞬間に終値で約定することは、実戦ではほぼ不可能だからです。シグナル確定とエントリー可能時点のこの1本分の差を無視すると、バックテストは実戦より良く見えます。

request.security の lookahead_on は未来の確定値を引き込む

上位時間足(HTF)のデータを取得するときに使う関数がrequest.security()です。日足チャートで週足終値を参照したり、5分足で1時間足のトレンドを取り込んだりするときに使います。ルックアヘッド・バイアスが最も入り込みやすいのが、この関数のlookaheadパラメーターです。

request.security(syminfo.tickerid, "1W", high, lookahead=barmerge.lookahead_on)を日足チャートで呼び出すと、まだ進行中の週足の最終高値を、その週の初日から先取りして取得します。公式ドキュメントでも、この呼び出しは過去足で未来のデータを取得するため、危険なほど誤った結果を表示すると説明されています。チャート上では完璧に機能しているように見えますが、その完璧さは未来を見た結果です。

2024年3月11日の週に始まったBTC週足を例にします。この週足は週中の3月14日に73,777ドルの高値を付け、68,393ドルで引けました。lookahead_onでこの週足の高値を日足から参照すると、3月11日月曜日の時点ですでに73,777という値がチャートに入ります。実際に73,777を付ける3日前です。この値を使って「週足高値付近でショート」というバックテストを回すと、3月11日の時点で73,777を知っていたことになり、勝率は非現実的に高くなります。実戦で3月11日にその値を知る方法はありませんでした。チャートで完璧に見えたそのショート位置は、その時点では存在しなかった情報によって作られた位置にすぎません。

lookahead_onが上位足の最終高値を確定前の足に先取りする仕組み

lookahead_off が安全なデフォルトであり、未来参照は計算式そのものにリークがある

正しいパターンはbarmerge.lookahead_offです。この設定では、上位時間足のデータはその足が実際に確定した後で初めて下位時間足へ渡されます。週足が日曜日に閉じてから、その週足の高値・終値が日足に反映されるため、過去足が未来を見ることはありません。設定を省略した場合のデフォルトはlookahead_offですが、インターネットからコピーしたコードにはlookahead_onが明示的に入っていることが多いため、自分で確認する必要があります。

確定済みの上位時間足の値を安全に使いつつ、できるだけ早くシグナルを受け取りたい場合、公式ドキュメントが推奨する形があります。request.security(syminfo.tickerid, "1W", expression[1], lookahead=barmerge.lookahead_on)のように、式に[1]のオフセットを付け、直前に確定した上位時間足の値を参照する方法です。これなら過去でもリアルタイムでも同じように動作します。シグナルが1本遅れる代償はありますが、その遅れは実戦ではいずれにせよ受け入れるべき遅れです。

ルックアヘッドの別の形が、未来参照と負のオフセットです。close[-1]のように負のインデックスで次の足の終値を参照すれば、明白な未来データのリークです。security()内でオフセットなしのlookahead_onを使うのと同じ結果を、より直接的に作ります。バックテスト結果が異常に良い場合は、まずコード内の負のインデックス指定とlookahead_onの2つを疑うべきです。

足の確定前に計算するインジケーターは、出ては消えるシグナルを作る

インジケーターや戦略にcalc_on_every_tick=trueが設定されていると、足が進行している間に入ってくるすべてのティックごとに計算が走り直します。ライブチャートでは、この設定のせいでシグナルが出たり消えたりを繰り返します。終値が条件を満たした瞬間に矢印が出ても、価格が再び動いて終値の前提が変わると、矢印は消えます。

問題は、バックテストとリアルタイムの不一致です。公式ドキュメントが指摘しているように、戦略は過去足では足の確定基準で実行されますが、calc_on_every_tick=trueの場合、リアルタイムでは毎ティック実行されます。実行方法が異なるため、過去成績とリアルタイムの挙動がずれます。チャートの過去区間ではすべての足がすでに確定しているため、シグナルはきれいに表示されていますが、そのきれいさは足が閉じた後に見た結果にすぎません。

この問題を防ぐ標準的な手段がbarstate.isconfirmedです。エントリー条件にand barstate.isconfirmedを加えると、足の最後の瞬間、つまり終値が確定する時点でのみシグナルが発生します。過去足は常に確定済みとして扱われるため、この条件は過去とリアルタイムで同じように再現されます。ライブで出たり消えたりするシグナルをなくすには、この1行が最もシンプルな方法です。進行中の足に出た矢印を見てエントリーを急ぐ前に、そのシグナルがbarstate.isconfirmedのゲートを通過したものかどうかを先に確認する必要があります。

本質的に描き直されるツールは、最後のスイングを事後的に移動させる

設定を変えても解決できない、計算式そのものがリペイントするツールがあります。代表例がZigZagです。ZigZagは一定パーセント以上の反転が確認されて初めてスイング高値・安値を確定しますが、最後のスイングはその反転が確認されるまで価格に追随して動き続けます。価格がさらに上がれば最後の高値は上へ移動し、反転が確認された後でようやくその位置に固定されます。

3月14日にBTCが73,777ドルを付けた例に戻ります。ZigZagがこの73,777をスイング高値として確定したのは、3月14日当日ではありません。その後、価格が十分に下落した3月16日または19日ごろになって初めて確定します。3月19日には、BTCは61,555ドルの安値まで下落しました。ZigZagは反転閾値が満たされたその時点でようやく73,777に高値を固定し、それまでは最後の線分が価格に合わせて揺れ動いていました。過去チャートでは73,777にきれいな高値が表示され、誰でもそこでショートできたかのように見えます。しかしその高値が確定した時点では、価格はすでに12,000ドル以上下落した後でした。

同じ落とし穴は、一部のSupertrend派生型やフィボナッチ自動描画ツールにもあります。最後のスイングを基準にリトレースメント水準を自動で引くツールは、そのスイングが確定するまで水準全体が移動します。自動で引かれた0.618リトレースメントラインが、チャート上では完璧なサポートとして機能したように見えても、そのラインがその位置に描かれたのは、サポートが確認された後だった可能性があります。自動ツールの過去表示をエントリー根拠として信頼してはいけません。

ZigZagの最後の線分が価格に追随し、反転確定後に高値を固定する

HTFシグナルをリアルタイムでそのまま追うと、足の途中で反転する

リペイントするツールの矢印だけを見てバックテスト勝率を信じることが、1つ目の誤用です。ZigZagベースの戦略のバックテストが80%の勝率を示しているなら、その勝率はスイングが事後的に確定した位置で計算された結果である可能性が高いです。リアルタイムでは、そのスイングが確定する前にエントリー判断を下さなければならないため、バックテストが想定したエントリー位置には入れません。勝率の数字そのものが、未来を見た計算です。

2つ目の誤用は、上位時間足のシグナルをリアルタイムでそのまま追うことです。4時間足や日足のシグナルを5分足チャートでリアルタイムに見ると、上位時間足が確定するまでそのシグナルは変わり続けます。日足が陽線で引けそうに見えて買いシグナルが出たものの、日足が閉じるころに陰線へ反転してシグナルが消えることはよくあります。上位時間足のシグナルは、その時間足の足が確定した後にのみ確定したものとして見るべきです。進行中の日足のトレンド色を5分ごとに確認しながら売買すると、同じシグナルで出入りを繰り返し、手数料で口座残高が少しずつ削られていきます。

3つ目はアラートです。TradingViewのアラートをcalc_on_every_tickベースの条件に設定していると、足が進行している間に条件に一瞬でも触れたタイミングでアラートが鳴ります。足の確定時点では条件が外れていても、アラートはすでに送信された後です。アラートオプションを「Once Per Bar Close」に設定していなければ、足の途中で鳴ったアラートを見てエントリーした位置が、足の確定時点ではシグナルではなかった位置になります。

インジケーターがリペイントするかを検証する手順

リペイントの有無は、コードを読まなくてもチャート上で直接確認できます。疑わしいインジケーターを検証するときは、次の手順を踏みます。

  • [ ] バーリプレイで再生する: TradingViewのバーリプレイ(Bar Replay)機能で過去の任意の時点を選び、足を1本ずつ進めながらシグナル矢印の位置を記録します。足が確定するときに矢印が移動したり消えたりするなら、リペイントするツールです。
  • [ ] lookahead設定を確認する: インジケーターのソースコードでrequest.securityを検索し、lookahead=barmerge.lookahead_onがオフセット([1])なしで使われていないか確認します。オフセットなしのlookahead_onが1つでもあれば、未来データの漏洩が入っています。
  • [ ] 負のインデックスを検索する: ソース内で[-を検索し、close[-1]のような負のオフセットがないか確認します。負のインデックス指定は、次の足を先に見る直接的な漏洩です。
  • [ ] ティック計算オプションを確認する: 戦略であればcalc_on_every_tickの値を確認し、trueならエントリー条件にbarstate.isconfirmedも一緒に入っているかを確認します。ゲートがなければ、リアルタイムのシグナルは足の中で揺れます。
  • [ ] 過去とリアルタイムの一致を検証する: 同じインジケーターをライブチャートに表示したまま実時間で数日観察し、その間に発生したシグナル位置を後から過去チャートで見直します。2つの位置が異なるなら、そのインジケーターのバックテストは信頼できません。
実時間で観察したシグナルと過去チャートを照合してリペイントを見抜く検証

リペイントを見抜く補助チェック

検証手順をすべて行う時間がないときに、素早く見分ける方法があります。第一に、バックテスト勝率が異常に高い戦略はまず疑います。単純なトレンドフォロー戦略の現実的な勝率は40〜55%の範囲です。70%を超える勝率が出ているなら、損益比が極端に低いマーチンゲール系か、未来を見たルックアヘッドが入っている可能性が高いです。数字が良すぎるときは、まずコードを疑うことが損失を減らす習慣になります。

第二に、インジケーターの説明に「non-repainting」と書かれていても、そのまま信じないことです。作成者が意図せずlookahead_onを残していたり、マルチタイムフレーム部分だけ処理してZigZag計算はリペイントするままにしていたりするケースは多くあります。説明文よりも、バーリプレイで自分が確認した結果を優先すべきです。

第三に、シグナルは常に1本遅れで受け取る習慣を基本にします。どのインジケーターであっても、シグナル足の終値は使わず、次の足の始値でエントリーすると仮定してセットアップを組みます。この1本分の余裕が、足の中で反転するシグナルと確定済みシグナルを分ける境界であり、バックテストと実戦の差を最も大きく縮めます。チャート上で完璧に見えるシグナルほど、その完璧さが足の確定後に見えているだけではないかを、まず問う必要があります。