データ・スヌーピング・バイアス

売買ルールを複雑にすればパフォーマンスは向上する?

ある売買ルールでパフォーマンスがいまいちな場合、何らかのルールを追加することでパフォーマンスを改善させようとする人が多いように思う。それが一概に悪いとは言えない。だが、例え無意味なルールでも、それを加えることでバックテスト上のパフォーマンスを向上させることはできてしまうのである。

例えば当日の終値が上昇するか下落するかは大体1:1の半々である。前日終値が上昇した場合に当日の終値が上昇するか下落するかも大体1:1の半々である。前日終値が下落した場合に当日の終値が上昇するか下落するかもやはり大体1:1の半々である。では前々日、3日前、4日前と条件を加えていくと、それでも大体1:1の半々となるだろうか。

データには偶然起きる偏りがあるものである。コインを投げて表が上になるか、裏が上になるかを何回も試した場合、「表、裏、表、裏、表、裏、...」のように交互に現れるとは限らない。表、あるいは裏が続くこともある。それでも全体として表と裏は大体1:1の半々で現れるのである。

したがって、条件をどんどん加えていって細分化すれば、必ず偏りが生じる。上昇、下落の比率が3:2以上、あるいは2:3以下となることも起きる。すると比率が3:2以上となった条件では買い、2:3以下となった条件では売りとすれば勝率60%以上の戦略の出来上がりである。FXなんて簡単だ!

だが、出来上がった戦略を別の期間のデータで試すと大抵はボロボロになる。これがつまりデータ・スヌーピング・バイアスだ。やたらとルールを加えてバックテスト上のパフォーマンスを向上させることは多くの場合、ただのお遊びで終わる。

このような過ちを避けるためには売買ルールは可能な限りシンプルにすることだ。そして、パラメータの最適化に使用しなかったデータでも売買ルールが有効か確認することである。

最近、機械学習が流行しているが…

最近、投資の世界では機械学習が流行しているようだ。機械学習の一種である決定木では説明変数で場合分けし、それを更に場合分けするということを繰り返し、複雑な条件の組み合わせによって目的変数を予測する。これもやりすぎるとデータ・スヌーピングになる。

例えば1本前の足のリターンを目的変数とし、2本前の足のリターンを説明変数とする。説明変数は1つだけだが、リターンなので、いくらでも細分化できる。「x <= 0.0」で場合分けし、yesなら更に「x <= -0.1」で、noなら更に「x <= 0.1」で場合分けするというようにである。

これでモデルを作成してから1本前の足のリターンを今度は説明変数としてモデルに与えれば、現在の足のリターンを予測してくれることになる。そして予測したリターンがプラスなら買い、マイナスなら売りとする。下のグラフは2011年1月1日から2013年12月31日までのドル円1時間足のデータを使ってモデルを作成し、同期間でバックテストしたものである。

シャープレシオは38.92で、すごいパフォーマンスである。

再現性がなければ意味はない

では、上で作ったモデルを使い、2014年1月1日から2016年12月31日までの期間でバックテストし、果たしてモデルに再現性があるかどうかを見てみる。

シャープレシオは-2.11となっており、ひどいパフォーマンスである。複雑な売買ルールでバックテスト上でのパフォーマンスを上げたところで、再現性があるかどうかはまた別の問題であるということが分かるだろう。

サンプルプログラム

①以下のプログラムをSpyderの「エディタ」にコピー&ペーストし、ファイル名を「data_snooping_bias.py」として「~/py」フォルダーに保存する。

# coding: utf-8

import forex_system as fs
import pandas as pd
from datetime import datetime
from sklearn import tree

# パラメータの設定
PARAMETER = None
# 最適化の設定
RRANGES = None

def strategy(parameter, symbol, timeframe):
    '''戦略を記述する。
    Args:
        parameter: パラメータ。
        symbol: 通貨ペア名。
        timeframe: 足の種類。
    Returns:
        シグナル。
    '''
    # 戦略を記述する。
    ret = fs.i_log_return(symbol, timeframe, 1, 1)
    index = ret.index
    start = datetime.strptime('2011.01.01', '%Y.%m.%d')
    end = datetime.strptime('2013.12.31', '%Y.%m.%d')
    clf = tree.DecisionTreeRegressor()
    y = ret[start:end]
    x = ret.shift(1)[start:end]
    x = x.values.reshape(len(x), 1)
    clf.fit(x, y)
    pred = clf.predict(ret.values.reshape(len(ret), 1))
    pred = pd.Series(pred, index=index)
    buy_entry = pred > 0.0
    buy_exit = buy_entry != 1
    sell_entry = pred < 0.0
    sell_exit = sell_entry != 1
    signal = fs.calc_signal(buy_entry, buy_exit, sell_entry, sell_exit)

    return signal

②以下のコマンドをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを押す。

%run -t ~/py/backtest.py --ea1 data_snooping_bias --symbol1 USDJPY --spread1 4 --timeframe 60 --start 2011.01.01 --end 2013.12.31

③以下のコマンドをSpyderの「IPython console」にコピー&ペーストして「Enter」キーを押す。

%run -t ~/py/backtest.py --ea1 data_snooping_bias --symbol1 USDJPY --spread1 4 --timeframe 60 --start 2014.01.01 --end 2016.12.31
(2017/02/10更新)

コメント

非公開コメント