未来を参照することで非現実的な良い結果を生み出すストラテジー

Pine言語を開発する上での主な目標の一つは、できるだけ多くの便利なツールをユーザーの方にご提供することです。これらのツールには様々な用途があり、一部のインジケーターやチャートタイプでは特定の操作で(現在処理されているバーからみて相対的に)未来のバーやトレードからデータを引き出すことが可能となります。実際のトレードではトレーダーはこうした未来のデータを受け取ることができませんので、それらを元に構築されたストラテジーは、バックテストでは非現実的な利益をもたらすことができますが、リアルタイムのトレードでは損失となります。ストラテジーで「未来」の情報を使用するミスは、先読みバイアスとも呼ばれます。

一部のTradingViewユーザは、無知であったりまたは悪意を持って、この機能を悪用したアイデアやスクリプトを作成したり公開する場合があります。これは場合によっては役立つ可能性がある為、TradingViewがこの機能自体を削除することはできませんが、この動作についてユーザーの方に警告を行うことをお約束致します。

日本式のローソク足を利用したストラテジー

このような挙動の原因として非常によくあるケースが、日本式のチャート(練行足やカギ足など)でのストラテジー・バックテストです。この問題は、ストラテジーのバックテストエンジンでは、それらの各バーを(通常のローソク足チャートのように)始値/安値/高値/終値の4つのトランザクションとみなす事に起因します。その為、例えば練行足のチャートでは、現実には存在しない価格でストラテジーのバックテストエンジンがポジションをエントリー/エグジットすることが可能となります。またボックスサイズの値を最小ティックよりも小さい値に設定すると、バックテストエンジンが実際の価格を処理する前に、次の価格が現在の価格よりも高いか低いかをチェックして、事前にポジションのエントリー/エグジットを行うといった事が可能になります。

//@version=4
strategy("My Strategy", overlay=true)
if close < close[1]
    strategy.entry("ShortEntryId", strategy.short)
strategy.close("ShortEntryId", when = close > close[1])

if close > close[1]
    strategy.entry("LongEntryId", strategy.long)
strategy.close("LongEntryId", when = close < close[1])

スクリーンショットでご確認頂けますように、このシンプルなストラテジーでは、最大/最小に非常に近い価格で取引を行う事ができます。

calc_on_order_fills = true のパラメーターを使用したストラテジー

ストラテジー関数で calc_on_order_fills = true のパラメータが指定されている場合、(バーの終値でのみストラテジーが計算が行われる通常のケースとは対照的に)バックテストエンジンは注文が約定された後、そのバー内で追加計算を実行します。同時にストラテジーは計算中に、多くの追加のバーのパラメーター(例えば、高値と安値)にもアクセスできます。これにより、バックテストで素晴しいパフォーマンスを発揮するストラテジーを記述できます:

//@version=4
strategy("CalcOnOrderFillsStrategy", overlay=true, calc_on_order_fills=true)

// a variable is used to prevent double entry on the same bar
var lastTimeEntry = 0

longCondition = close > sma(close, 14)  and lastTimeEntry != time
if longCondition
    strategy.entry("LongEntryId", strategy.long)

strategy.exit("exitId", "LongEntryId", limit=high)
lastTimeEntry := time

スクリーンショットを見ると、エントリーはバーの始値で、エグジットは同じバーの高値で行われている事が確認できます。つまり計算では、注文が約定された後、strategy.exit の指値価格を現在のバーの高値と同じに設定していますが、これは実際の取引では行う事ができません。

security関数で lookahead = barmerge.lookahead_on のパラメーターを指定、またはPine v3以前のsecurity関数

Pineのsecurity関数では、他のシンボルや時間足のデータを要求できます。実装にも依りますが、これによりストラテジーが未来のデータを受信できるようになります。例えば日足のバーの終値や高値を要求する場合、バックテストの間、ストラテジーはその日の開始時にこれらの値を知ることができます。

バージョン3より前のバージョンでは、security関数は、アクセスが可能になる前であっても上位足の値を返していました。バージョン3ではこの動作は修正されましたが、互換性を保つ為、security関数にlookaheadパラメータが追加されました。このパラメーターはデフォルトでは false(つまり「未来を見ること」はオフ)になっていますが、lookaheadパラメータの値を barmerge.lookahead_on に設定することで有効にすることができます。

この機能を利用して構築された収益性の高いストラテジーの例:

//@version=4
strategy("My Strategy", overlay=true)
dayStart = security(syminfo.tickerid, "1D", time, lookahead=barmerge.lookahead_on)
dayHigh = security(syminfo.tickerid, "1D", high, lookahead=barmerge.lookahead_on)
dayLow = security(syminfo.tickerid, "1D", low, lookahead=barmerge.lookahead_on)

// entry at first bar of a day
if time == dayStart
    // distance to daily high is further, so we can earn more
    if abs(open - dayHigh) > abs(open - dayLow)
        strategy.entry("LongEntryId", strategy.long)
        strategy.exit("exitLongId", "LongEntryId", limit=dayHigh)
    else
        strategy.entry("ShortEntryId", strategy.short)
        strategy.exit("exitShortId", "ShortEntryId", limit=dayLow)
        
plot(dayHigh)
plot(dayLow)

この例では最初のバーで始値と比較して価格が上下にどちらに動くかを分析し、その結果に基づいてロングまたはショートポジションをエントリーして、その日の最高価格または最低価格でエグジットしています。

security() で barmerge.lookahead_on 引数があるすべてのケースが未来のデータを参照するわけではない事にご注意ください。例えば上記のコードで security() 内の time/high/low をそれぞれ time[1]/high[1]/low[1] に置き換えた場合、すでにクローズしたバーの値を受け取ることになります。これは経験豊富なプログラマーが lookahead のリスクなしに security() からデータを取得する為によく使われます。

これらは現時点でのすべての未来を見通すストラテジーの既知の手法です。この解説により、こうした欠点を持たないストラテジーを作成される一助となります事と、作者がこれらの特徴を悪用している公開ストラテジーを避けることができるようになります事を願っております。