政府の市場介入によるバイアス (2017/05/27)

政府の市場介入によるバイアスによってパフォーマンスがどうなるか、そして、バイアスを除いたあとのパフォーマンスがどうなるかを見てみる。

スイスフランショック

政府の市場介入は価格形成に歪みを与える。その歪みを利用して利益を上げることも可能ではある。だが、恐ろしいのは政府が方針を変更したときである。2015年1月15日に起きたスイスフランショックは記憶に新しい。

スイスフラン高を嫌ったスイス中銀は2011年9月、ユーロスイスの下限を1.2スイスフランに設定して無制限介入を開始した。ユーロスイスが1.2スイスフランを割ることはないとスイス中銀が保証してくれたわけだ。これをバックにして買えば低リスクで利益を上げられると考えたトレーダーも少なくなかっただろう。

かく言う私もスイス中銀の介入をバックにして下がったら買う逆張り戦略をブログで記事にした。ただ、スイス中銀が無制限介入を行ったときにユーロスイスが暴騰したことを挙げて、スイス中銀が方針を変換したら逆のことが起きると警告した。また、1.2スイスフランを割ったら損切りすべきだとも書いた。だが、私の認識は甘すぎた。

スイス中銀が下限を撤廃したことが伝わると、スプレッドが大きく広がり、そしてレート配信が止まった。運よく損切りできても想定以上の損失を受けただろうし、損切りできず、ロスカットすらできずに全資金を失い、さらにはFX会社に対して負債すら抱えてしまったトレーダーも少なくなかった。FX会社が負債を回収できずに倒産するというケースもあった。幸い、私は記事にしただけで実際には運用しなかった。だが、いい加減な記事を書いてはいけないと痛感した。

一時的には勝てるが…

政府の介入によって歪められた市場のデータを使ってバックテストしても、その寿命はその介入が継続している間だけに限られる。そして、介入は何のアナウンスもなく突然撤廃され、後は阿鼻叫喚の地獄がやってくるのである。

例として直近安値をバックにしてユーロスイスを買う戦略を考えてみる。具体的には

  • 終値が直近安値*(1+エントリー閾値/100)以下になったら買いエントリー、
  • 終値が直近安値*(1+(エントリー閾値+エグジット閾値)/100)以上になったら買いエグジット、

という売買ルールである。

では、上記の売買ルールに基づき、2012年1月1日から2015年1月1日までの期間でバックテストしてみる。

以下のコードを「swiss_franc_shock.py」ファイルとして「~/py」フォルダーに保存する。

# coding: utf-8

import forex_system as fs
import pandas as pd

# パラメータの設定
PERIOD = 1500
ENTRY_THRESHOLD = 0.002
PROFIT_THRESHOLD = 0.08
PARAMETER = [PERIOD, ENTRY_THRESHOLD, PROFIT_THRESHOLD]

# 最適化の設定
START_PERIOD = 250
END_PERIOD = 2500
STEP_PERIOD = 250
START_ENTRY_THRESHOLD = 0.001
END_ENTRY_THRESHOLD = 0.01
STEP_ENTRY_THRESHOLD = 0.001
START_PROFIT_THRESHOLD = 0.01
END_PROFIT_THRESHOLD = 0.1
STEP_PROFIT_THRESHOLD = 0.01
RRANGES = (
    slice(START_PERIOD, END_PERIOD, STEP_PERIOD),
    slice(START_ENTRY_THRESHOLD, END_ENTRY_THRESHOLD, STEP_ENTRY_THRESHOLD),
    slice(START_PROFIT_THRESHOLD, END_PROFIT_THRESHOLD, STEP_PROFIT_THRESHOLD),
)

get_model = None

def strategy(parameter, symbol, timeframe):
    '''戦略を記述する。
    Args:
        parameter: パラメーター。
        symbol: 通貨ペア。
        timeframe: 期間。
    Returns:
        買いエントリー、買いエグジット、売りエントリー、売りエグジット、ロット数。
    '''
    # パラメータを格納する。
    period = int(parameter[0])
    entry_threshold = float(parameter[1])
    profit_threshold = float(parameter[2])
    # 戦略を記述する。
    close1 = fs.i_close(symbol, timeframe, 1)
    hl_band1 = fs.i_hl_band(symbol, timeframe, period, 1)
    buy_entry = close1 <= hl_band1['low'] * (1 + entry_threshold / 100)
    buy_exit = (close1 >= hl_band1['low'] *
                (1 + (entry_threshold + profit_threshold) / 100))
    sell_entry = close1 != close1  # 売りはやらないので無意味な条件を与える。
    sell_exit = close1 != close1  # 同上。
    index = close1.index
    lots = pd.Series(1.0, index=index)
    return buy_entry, buy_exit, sell_entry, sell_exit, lots

if __name__ == '__main__':
    fs.platform()

以下のコマンドを実行してバックテストする。

%run -t ~/py/swiss_franc_shock.py --mode backtest --symbol EURCHF --timeframe 5 --spread 2.0 --start 2012.01.01 --end 2015.01.01

       start         end trades   apr sharpe    kelly drawdowns durations              parameter
  2012.01.01  2014.12.31    115  0.01  1.661  269.309     0.005        55  [1500.0, 0.002, 0.08]

シャープレシオはそれほど高くはないが、資産曲線は比較的きれいな右肩上がりとなっている。

地獄は突然やってくる

2015年でも同様のパフォーマンスを期待したいが、残念ながらスイス中銀は1月15日、突如としてスイスフランの下限を撤廃する。では、すでに最適化されたパラメータを使い、期間を2016年1月1日まで延長したデータでどうなるかを見てみる。

以下のコマンドを実行してバックテストする。

%run -t ~/py/swiss_franc_shock.py --mode backtest --symbol EURCHF --timeframe 5 --spread 2.0 --start 2012.01.01 --end 2016.01.01

      start         end trades    apr sharpe  kelly drawdowns durations              parameter
  2012.01.01  2015.12.31    155 -0.032  -0.41 -5.408     0.187       258  [1500.0, 0.002, 0.08]

一体何が起きたのか、という感じである。3年もかけてコツコツと積み上げた利益など一瞬で消えてしまい、約10%以上の負けとなっている。

しかも、これはレバレッジが1倍ならの話である。もしレバレッジが10倍だったら、全資金を失うばかりか、負債まで抱えることとなっていただろう。実際にはスプレッドは広がるし、レート配信は停止するしで、バックテストよりもっとひどいことになっていたかもしれない。

トレードで利益を上げるには単に市場の歪みを捉えるだけではなく、それが継続性のあるものでなければならない。政府によって一時的に作られた歪みによって利益を得ようとすることは後々とんでもないしっぺ返しを受けることになる。

データ・スヌーピング・バイアス (2017/05/27)

データ・スヌーピング・バイアスによってパフォーマンスがどうなるか、そして、バイアスを除いたあとのパフォーマンスがどうなるかを見てみる。

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

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

例えば当日の終値が上昇するか下落するかは大体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日から2014年1月1日までのドル円1時間足のデータを使ってモデルを作成し、同期間でバックテストしてみる。

以下のコードを「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

get_model = None

def strategy(parameter, symbol, timeframe):
    '''戦略を記述する。
    Args:
        parameter: パラメーター。
        symbol: 通貨ペア。
        timeframe: 期間。
    Returns:
        買いエントリー、買いエグジット、売りエントリー、売りエグジット、ロット数。
    '''
    ret = fs.i_roc(symbol, timeframe, 1, 1)
    index = ret.index
    start = datetime.strptime('2011.01.01', '%Y.%m.%d')
    end = datetime.strptime('2014.01.01', '%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
    lots = pd.Series(1.0, index=index)
    return buy_entry, buy_exit, sell_entry, sell_exit, lots

if __name__ == '__main__':
    fs.platform()

以下のコマンドを実行してバックテストする。

%run -t ~/py/data_snooping_bias.py --mode backtest --symbol USDJPY --timeframe 60 --spread 0.4 --start 2011.01.01 --end 2014.01.01

       start         end trades     apr  sharpe    kelly drawdowns durations
  2011.01.01  2013.12.31   9768  424615  58.548  731.352     0.015         0

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

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

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

以下のコマンドを実行してバックテストする。

%run -t ~/py/data_snooping_bias.py --mode backtest --symbol USDJPY --timeframe 60 --spread 0.4 --start 2014.01.01 --end 2017.01.01

       start         end trades    apr sharpe  kelly drawdowns durations
  2014.01.01  2016.12.31   9363 -0.095 -1.121 -11.76     0.418       781

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

先読みバイアス (2017/05/27)

先読みバイアスによってパフォーマンスがどうなるか、そして、バイアスを除いたあとのパフォーマンスがどうなるかを見てみる。

先読みバイアスは不注意から生まれる

先読みバイアスはありえないパフォーマンスを生む。もしタイムマシーンがあって未来の情報を手に入れることができるなら、投資もギャンブルも百戦百勝である。だがもちろん、そのようなことはできない話である。

先読みバイアスはバックテストのときに誤って未来のデータを使ってしまった場合に起きる。MT4では日付をずらしたデータを作成して使ったりしないかぎり、未来のデータを使ってバックテストはできない仕組みになっている。しかし、自分でバックテストシステムを作った場合、先読みバイアスの過ちを犯してしまうリスクがある。

先読みバイアスはプログラミング上の誤りではないので、エラーが発生しない。このため、問題があることに気づきにくいのでやっかいである。

先読みバイアスを避けるにはトレードの時点で存在しないデータを使わないよう注意するしかない。だが、もしありえないようなパフォーマンスが得られたなら、先ず先読みバイアスを疑うべきである。

先読みバイアスが生む異常なパフォーマンス

例えば1本前の足の終値が2本前の足の終値より上であれば買い、下であれば売りという戦略を考えたとする。このような単純な戦略では取引コストを無視すれば勝つということもないが負けることもない。だが取引コストを考慮するなら、右肩下がりの資産曲線となる。

ところが間違えて現在の足の終値が1本前の足の終値より上であれば買い、下であれば売り、とプログラムしたとする。現在の足の終値が上がるか下がるか、始値の時点では分からないのだから、このようなことは現実には実行できない。だがバックテストではできてしまう。

これはつまり終値が上がっていたら始値の時点に戻って買い、終値が下がっていたら始値の時点に戻って売るということである。あたかもタイムマシンで未来に行って結果を見てきてから現在に戻って売買するのと同じである。これでは上がるか下がるの予測は百発百中となるに決まっている。

もし、ある戦略のシャープレシオが3.0くらいあるとしたら、それは優秀な戦略と言っていいと思う。このような戦略を作ることは不可能ではないが、決して簡単なことではない。もし、シャープレシオが3.0を超えるような戦略がふと出来上がってしまったとしたら、先ず先読みバイアスを疑うべきである。

では、上で説明した間違ったコードを2014年1月1日から2017年1月1日までUSDJPYの5分足に適用して検証してみる。

以下のコードを「look_ahead_bias.py」ファイルとして「~/py」フォルダーに保存する。

# coding: utf-8

import forex_system as fs
import pandas as pd

# パラメータの設定
PARAMETER = None

# 最適化の設定
RRANGES = None

get_model = None

def strategy(parameter, symbol, timeframe):
    '''戦略を記述する。
    Args:
        parameter: パラメーター。
        symbol: 通貨ペア。
        timeframe: 期間。
    Returns:
        買いエントリー、買いエグジット、売りエントリー、売りエグジット、ロット数。
    '''
    close1 = fs.i_close(symbol, timeframe, 0)  # 「1」を誤って「0」にしている。
    close2 = fs.i_close(symbol, timeframe, 1)  # 「2」を誤って「1」にしている。
    buy_entry = close2 < close1
    buy_exit = close2 > close1
    sell_entry = close2 > close1
    sell_exit = close2 < close1
    index = close1.index
    lots = pd.Series(1.0, index=index)
    return buy_entry, buy_exit, sell_entry, sell_exit, lots

if __name__ == '__main__':
    fs.platform()

以下のコマンドを実行してバックテストする。

%run -t ~/py/look_ahead_bias.py --mode backtest --symbol USDJPY --timeframe 5 --spread 0.4 --start 2014.01.01 --end 2017.01.01

       start         end  trades          apr   sharpe    kelly drawdowns durations
  2014.01.01  2016.12.31  112760  9.04702e+18  185.484  2307.89     0.009         0

資産曲線は右肩上がりの曲線を描いている。複利で計算しているので、最後は爆発的に利益が増えている。シャープレシオは185.48もあるが、これは明らかに異常なパフォーマンスである。このような結果になったら先ず、先読みバイアスを疑って、コードを確認することである。

バイアスを排除した後の惨めなパフォーマンス

実際にコードを確認すると、やはり間違いがあった。そこで間違いを訂正して実行したところ、以下のようになった。

       start         end  trades    apr  sharpe    kelly drawdowns durations
  2014.01.01  2016.12.31  112759 -0.331 -16.286 -163.036   133.332       782

資産曲線は右肩下がりの曲線を描いている。シャープレシオは-16.29で、実際には全く使い物にならない戦略だということが分かる。