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

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

スイスフランショック

政府の市場介入として、ここではスイス中銀が2011年9月から2015年1月まで実施したユーロスイスに対する無制限介入を取り上げる。スイス中銀は2011年9月、スイスフラン高を嫌い、ユーロスイスの下限を1.2スイスフランに設定して無制限介入を開始した。だが、スイス中銀は2015年1月15日、突如として下限を撤廃し、市場は大混乱した。スイスフランショックである。

下限が設定されるということは、ユーロスイスが1.2スイスフランを割ることはないとスイス中銀が保証してくれたわけである。これを当てにしてユーロスイスを買うというトレード戦略も考えられる。

バイアスを含んだバックテスト

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

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

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

では先ず、データとして2012年1月1日から2015年1月1日までEURCHFの5分足を使用し、コストとしてスプレッド2.0pipsを適用して最適化を行ってしてみる。つまり、スイスフランショック直前までのデータを使ってバックテストするわけである。

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

import forex_system as fs

PERIOD = 1500
ENTRY_THRESHOLD = 0.002
EXIT_THRESHOLD = 0.08
PARAMETER = [PERIOD, ENTRY_THRESHOLD, EXIT_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_EXIT_THRESHOLD = 0.01
END_EXIT_THRESHOLD = 0.1
STEP_EXIT_THRESHOLD = 0.01
RRANGES = (
    slice(START_PERIOD, END_PERIOD, STEP_PERIOD),
    slice(START_ENTRY_THRESHOLD, END_ENTRY_THRESHOLD, STEP_ENTRY_THRESHOLD),
    slice(START_EXIT_THRESHOLD, END_EXIT_THRESHOLD, STEP_EXIT_THRESHOLD),
)

get_model = None

def strategy(parameter, symbol, timeframe):
    period = int(parameter[0])
    entry_threshold = float(parameter[1])
    exit_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 + exit_threshold) / 100))
    sell_entry = close1 != close1
    sell_exit = close1 != close1
    return buy_entry, buy_exit, sell_entry, sell_exit

if __name__ == '__main__':
    pnl = fs.forex_system()

①以下のコマンドをIPythonに入力して「Enter」キーを押す。

%run ~/py/swiss_franc_shock.py --mode backtest_opt --symbol EURCHF --timeframe 5 --spread 2.0 --start 2012.01.01 --end 2015.01.01 --min_trade 0

     start         end trades    apr sharpe drawdown              parameter
2012.01.01  2014.12.31     94  0.012  1.797    0.005  [1500.0, 0.002, 0.08]

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

最適化の結果、最適パラメータは

計算期間:1500
エントリー閾値:0.002
エグジット閾値:0.08

となった。

バイアスを除いたバックテスト

次に、バックテスト期間を2016年1月1日まで延長し、スイスフランショック後も含むとどうなるかを見てみる。最適パラメータは予め上のコードに入力してある。

○以下のコマンドをIPythonに入力して「Enter」キーを押す。

%run ~/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 drawdown              parameter
2012.01.01  2015.12.31    134  -0.043  -0.494    0.161  [1500.0, 0.002, 0.08]

3年もかけてコツコツと積み上げた利益は一気に消えてしまい、約10%以上の負けとなっている。

政府の介入によって歪められた市場のデータを使ってこのようなトレード戦略をバックテストすると、それなりのパフォーマンスを得られる。だが、その寿命は介入が継続している間だけに限られる。そして、介入は何のアナウンスもなく突如撤廃され、後は阿鼻叫喚の地獄がやってくる。

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

データ・スヌーピング・バイアスとは要するに過剰最適化、オーバーフィッティング、カーブフィッティングなどと呼ばれているものと同じと考えていい。

時系列データはノイズだらけである。多くの変数を用いて複雑なトレード戦略を構築し、バックテストで最適化すると、非常に素晴らしいパフォーマンスを生み出す。だが、それはノイズに最適化しただけなので、最適化に用いた期間とは別の期間のデータでバックテストすると、惨憺たる結果になる。

データ・スヌーピング・バイアスを含んだバックテスト

機械学習の一種である決定木を用いてデータ・スヌーピング・バイアスを含んだバックテストをやってみる。

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

これでモデルを作成して、1本前の足の変化率を今度は説明変数としてモデルに与えれば、現在の足の変化率を予測してくれることになる。そして予測した変化率がプラスなら買い、マイナスなら売りとする。

先ず、データとして2014年1月1日から2016年1月1日までUSDJPYの5分足を使用してモデルを作成し、コストとしてスプレッド0.4pipsを適用して同期間でバックテストしてみる。

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

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):
    ret = fs.i_roc(symbol, timeframe, 1, 1)
    index = ret.index
    start = datetime.strptime('2014.01.01', '%Y.%m.%d')
    end = datetime.strptime('2016.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
    return buy_entry, buy_exit, sell_entry, sell_exit

if __name__ == '__main__':
    pnl = fs.forex_system()

②以下のコマンドをIPythonに入力して「Enter」キーを押す。

%run ~/py/data_snooping_bias.py --mode backtest --symbol USDJPY --timeframe 5 --spread 0.4 --start 2014.01.01 --end 2016.01.01

     start         end trades     apr   sharpe    drawdown
2014.01.01  2015.12.31  38320  10.154  142.521  821551.981

資産曲線は右肩上がりの曲線を描いている。複利で計算しているので、最後は爆発的に利益が増えている。シャープレシオは142.521で、すごいパフォーマンスである。

先読みバイアスとは違って、通常はここまですごいパフォーマンスにはならないが、さすが機械学習、恐るべしといったところか。

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

次に、上で作ったモデルを使い、2016年1月1日から2018年1月1日までの期間でバックテストしてどうなるかを見てみる。

○以下のコマンドをIPythonに入力して「Enter」キーを押す。

%run ~/py/data_snooping_bias.py --mode backtest --symbol USDJPY --timeframe 5 --spread 0.4 --start 2016.01.01 --end 2018.01.01

     start         end trades     apr  sharpe drawdown
2016.01.01  2017.12.31  38342  -1.375  -12.65    0.944

シャープレシオは-12.65となっており、ひどいパフォーマンスである。

これを見れば、多くの変数を用いて複雑なトレード戦略を構築し、見せかけのパフォーマンスを上げたところで再現性がなければ意味がない、ということが分かるだろう。

先読みバイアス (2018/05/25)

先読みバイアスとは、ここではバックテストにおいて未来のデータを使うことにより、過剰なパフォーマンスを得られることとする。もしタイムマシーンがあって、未来に行くことができたとする。未来に行って、ある競馬の結果を知り、それから現在に戻って馬券を買えば大儲け間違いなしである。過剰なパフォーマンスとはこのようなものである。

先読みバイアスを含んだバックテスト

ここで、現在の足の終値が1本前の足の終値より上であれば買い、下であれば売りというトレード戦略をバックテストしてみる。バックテストの時点は現在の足の始値なので、現在の足の終値はまだ存在せず、未来のデータである。データとして2015年1月1日から2018年1月1日までUSDJPYの5分足を使用し、コストとしてスプレッド0.4pipsを適用した。

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

import forex_system as fs
import pandas as pd

PARAMETER = None
RRANGES = None

get_model = None

def strategy(parameter, symbol, timeframe):
    close1 = fs.i_close(symbol, timeframe, 0)
    close2 = fs.i_close(symbol, timeframe, 1)
    buy_entry = close2 < close1
    buy_exit = close2 > close1
    sell_entry = close2 > close1
    sell_exit = close2 < close1
    return buy_entry, buy_exit, sell_entry, sell_exit

if __name__ == '__main__':
    pnl = fs.forex_system()

②以下のコマンドをIPythonに入力し、「Enter」キーを押す。

%run ~/py/look_ahead_bias.py --mode backtest --symbol USDJPY --timeframe 5 --spread 0.4 --start 2015.01.01 --end 2018.01.01

     start         end trades     apr   sharpe          drawdown
2015.01.01  2017.12.31  56313  15.706  192.493  3.2505940726e+17

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

先読みバイアスを排除したバックテスト

次に、1本前の足の終値が2本前の足の終値より上であれば買い、下であれば売りというトレード戦略をバックテストしてみる。バックテストの時点は現在の足の始値なので、1本前の足の終値はすでに存在しており、未来のデータではない。上と同じく、データとして2015年1月1日から2018年1月1日までUSDJPYの5分足を使用し、コストとしてスプレッド0.4pipsを適用した。

①以下のコードで「look_ahead_bias.py」ファイルを上書きし、「~/py」フォルダーに保存する。

import forex_system as fs
import pandas as pd

PARAMETER = None
RRANGES = None

get_model = None

def strategy(parameter, symbol, timeframe):
    close1 = fs.i_close(symbol, timeframe, 1)
    close2 = fs.i_close(symbol, timeframe, 2)
    buy_entry = close2 < close1
    buy_exit = close2 > close1
    sell_entry = close2 > close1
    sell_exit = close2 < close1
    return buy_entry, buy_exit, sell_entry, sell_exit

if __name__ == '__main__':
    pnl = fs.forex_system()

②以下のコマンドをIPythonに入力し、「Enter」キーを押す。

%run ~/py/look_ahead_bias.py --mode backtest --symbol USDJPY --timeframe 5 --spread 0.4 --start 2015.01.01 --end 2018.01.01

     start         end trades     apr   sharpe drawdown
2015.01.01  2017.12.31  56314  -1.639  -15.512    0.999

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

為替・株価の予測で注意すべきこと (2017/09/16)

為替・株価の予測で注意すべきことについて説明する。

予測時点で存在しないデータを使わない

それは「予測時点で存在しないデータを使わない」ということである。「当たり前だ」と思うかもしれないが、これは分かっていても、うっかり間違えることがあるので要注意だ。

FXの場合、日足始値は業者によってGMTの設定時間が違うことがある。これをごちゃまぜにすると間違える。

例えば、ニューヨーク・クロージングとGMTの0時は冬時間の場合、日本時間でそれぞれ午前7時と午前9時である。するとGMT0時での前日終値はニューヨーク・クロージングの当日始値より未来のデータになる。「前日のデータだから使っても大丈夫」というわけにはいかない。

これに夏時間の問題が加わる。欧米には夏時間があるが、日本にはない。欧州と米国は夏時間があるが、夏時間を適用する期間にずれがある。こういう点にも注意しなければならない。

株式市場の場合、また別の問題がある。国によって取引時間が違うし、同じ国でも取引所によって違う場合がある。

それにFXは24時間取引だから前日終値と当日始値はほとんど同時である。したがって、前日終値の時点で予測するのも当日始値の時点で予測するのも週明けを除けば同じと考えていい。

だが、株式市場の取引時間は1/3日程度であり、前日終値と当日始値との間には2/3日程度のずれがある。したがって、前日終値の時点で予測するのと当日始値の時点で予測するのとでは大変な違いがある。つまり、当日終値が当日始値より上か下かを予測するのと、当日終値が前日終値より上か下かを予測するのとは全くの別物なのである。

失敗例

予測時点で存在しないデータを使った、しかも、その影響力の大きさからさっさと修正なり削除なりするべきだと私が考えるのはGoogleの以下のレポートである。

https://github.com/corrieelston/datalab/blob/master/FinancialTimeSeriesTensorFlow.ipynb

これによると、ディープラーニングの手法を用いてS&P500の騰落を予測し、的中率が70%を超えたらしい。しかし、この的中率は予測時点で存在しないデータを使うという過ちによってもたらされたものなのである。

詳しく見ると、当日のS&P500の騰落を予測するのにS&P500、ダウ、ナスダック、NYSEの1-3日前のデータを使っていた。これらは取引時間が同じであり、1日以上空けてデータを利用するのだから問題ない。

問題は米国より先に始まるアジアや欧州の当日の株価指数を使っていることである。アジアや欧州の取引終了時間は米国より早い。だから使ってもいいと考えては間違える。

注意しなければならないことはレポートにおける騰落とは当日終値が当日始値より上か下かではなくて、当日終値が前日終値より上か下かだということだ。つまり、予測時点は当日始値ではなくて、前日終値である。

日経平均株価はS&P500が終わってから始まり、S&P500は日経平均株価が終わってから始まるので、取引時間が重なることはない。このため、当日の日経平均株価が終わった後なら、そのデータを使ってもいいように勘違いしやすい。

だが、予測時点はS&P500の前日終値なのであり、当日の日経平均株価のデータはその時点では存在しないのだから、使ってはいけないのである。まして取引時間が近く、重なってもいる欧州の株価指数を使えばさらにありえない予測力を持つことになる。

未来のデータなし+ディープラーニング

レポートによると、2回予測が行われており、1回目は二項分類を用いたモデル、2回目は隠れ層を加えたモデルによっている。そして1回目の的中率は60.1%、2回目は72.2%という結果になっている。

そこで、私はレポートのプログラムの予測部分をほぼそのまま利用、しかし説明変数として0-2日前のデータが使われていた株価指数については1-3日前に変更して検証しなおしてみた。

○以下のプログラムを実行する。

# coding: utf-8
import numpy as np
import pandas as pd
import tensorflow as tf
import time
from datetime import datetime
from pandas_datareader import data as web

# ヒストリカルデータの開始日と終了日を設定する。
start = datetime(2010, 1, 1)
end = datetime(2015, 10, 1)
# ヒストリカルデータをダウンロードする(間隔を空けないとエラーになる)。
second = 10
snp = web.DataReader('^GSPC', 'yahoo', start, end)
time.sleep(second)
nyse = web.DataReader('^NYA', 'yahoo', start, end)
time.sleep(second)
djia = web.DataReader('^DJI', 'yahoo', start, end)
time.sleep(second)
nikkei = web.DataReader('^N225', 'yahoo', start, end)
time.sleep(second)
hangseng = web.DataReader('000001.SS', 'yahoo', start, end)
time.sleep(second)
ftse = web.DataReader('^FTSE', 'yahoo', start, end)
time.sleep(second)
dax = web.DataReader('^GDAXI', 'yahoo', start, end)
time.sleep(second)
aord = web.DataReader('^AORD', 'yahoo', start, end)
# 終値を格納する。
closing_data = pd.DataFrame()
closing_data['snp_close'] = snp['Close']
closing_data['nyse_close'] = nyse['Close']
closing_data['djia_close'] = djia['Close']
closing_data['nikkei_close'] = nikkei['Close']
closing_data['hangseng_close'] = hangseng['Close']
closing_data['ftse_close'] = ftse['Close']
closing_data['dax_close'] = dax['Close']
closing_data['aord_close'] = aord['Close']
# 終値の欠損値を前のデータで補間する。
closing_data = closing_data.fillna(method='ffill')
# 終値の対数変化率を格納する。
log_return_data = pd.DataFrame()
log_return_data['snp_log_return'] = (np.log(closing_data['snp_close'] /
    closing_data['snp_close'].shift()))
log_return_data['nyse_log_return'] = (np.log(closing_data['nyse_close'] /
    closing_data['nyse_close'].shift()))
log_return_data['djia_log_return'] = (np.log(closing_data['djia_close'] /
    closing_data['djia_close'].shift()))
log_return_data['nikkei_log_return'] = (np.log(closing_data['nikkei_close']
    / closing_data['nikkei_close'].shift()))
log_return_data['hangseng_log_return'] = (
    np.log(closing_data['hangseng_close'] /
    closing_data['hangseng_close'].shift()))
log_return_data['ftse_log_return'] = (np.log(closing_data['ftse_close'] /
    closing_data['ftse_close'].shift()))
log_return_data['dax_log_return'] = (np.log(closing_data['dax_close'] /
    closing_data['dax_close'].shift()))
log_return_data['aord_log_return'] = (np.log(closing_data['aord_close'] /
    closing_data['aord_close'].shift()))
# S&P500の対数変化率が0以上なら1、さもなければ0を格納した列を加える。
log_return_data['snp_log_return_positive'] = 0
log_return_data.loc[log_return_data['snp_log_return'] >= 0,
                   'snp_log_return_positive'] = 1
# S&P500の対数変化率が0未満なら1、さもなければ0を格納した列を加える。
log_return_data['snp_log_return_negative'] = 0
log_return_data.loc[log_return_data['snp_log_return'] < 0,
                   'snp_log_return_negative'] = 1
# 学習・テスト用データを作成する。
training_test_data = pd.DataFrame(
    columns=[
        'snp_log_return_positive', 'snp_log_return_negative',
        'snp_log_return_1', 'snp_log_return_2', 'snp_log_return_3',
        'nyse_log_return_1', 'nyse_log_return_2', 'nyse_log_return_3',
        'djia_log_return_1', 'djia_log_return_2', 'djia_log_return_3',
        'nikkei_log_return_1', 'nikkei_log_return_2', 'nikkei_log_return_3',
        'hangseng_log_return_1', 'hangseng_log_return_2',
            'hangseng_log_return_3',
        'ftse_log_return_1', 'ftse_log_return_2', 'ftse_log_return_3',
        'dax_log_return_1', 'dax_log_return_2', 'dax_log_return_3',
        'aord_log_return_1', 'aord_log_return_2', 'aord_log_return_3'])
for i in range(7, len(log_return_data)):
    snp_log_return_positive = (
        log_return_data['snp_log_return_positive'].iloc[i])
    snp_log_return_negative = (
        log_return_data['snp_log_return_negative'].iloc[i])
    # 先読みバイアスを排除するため、当日のデータを使わない。
    snp_log_return_1 = log_return_data['snp_log_return'].iloc[i-1]
    snp_log_return_2 = log_return_data['snp_log_return'].iloc[i-2]
    snp_log_return_3 = log_return_data['snp_log_return'].iloc[i-3]
    nyse_log_return_1 = log_return_data['nyse_log_return'].iloc[i-1]
    nyse_log_return_2 = log_return_data['nyse_log_return'].iloc[i-2]
    nyse_log_return_3 = log_return_data['nyse_log_return'].iloc[i-3]
    djia_log_return_1 = log_return_data['djia_log_return'].iloc[i-1]
    djia_log_return_2 = log_return_data['djia_log_return'].iloc[i-2]
    djia_log_return_3 = log_return_data['djia_log_return'].iloc[i-3]
    nikkei_log_return_1 = log_return_data['nikkei_log_return'].iloc[i-1]
    nikkei_log_return_2 = log_return_data['nikkei_log_return'].iloc[i-2]
    nikkei_log_return_3 = log_return_data['nikkei_log_return'].iloc[i-3]
    hangseng_log_return_1 = log_return_data['hangseng_log_return'].iloc[i-1]
    hangseng_log_return_2 = log_return_data['hangseng_log_return'].iloc[i-2]
    hangseng_log_return_3 = log_return_data['hangseng_log_return'].iloc[i-3]
    ftse_log_return_1 = log_return_data['ftse_log_return'].iloc[i-1]
    ftse_log_return_2 = log_return_data['ftse_log_return'].iloc[i-2]
    ftse_log_return_3 = log_return_data['ftse_log_return'].iloc[i-3]
    dax_log_return_1 = log_return_data['dax_log_return'].iloc[i-1]
    dax_log_return_2 = log_return_data['dax_log_return'].iloc[i-2]
    dax_log_return_3 = log_return_data['dax_log_return'].iloc[i-3]
    aord_log_return_1 = log_return_data['aord_log_return'].iloc[i-1]
    aord_log_return_2 = log_return_data['aord_log_return'].iloc[i-2]
    aord_log_return_3 = log_return_data['aord_log_return'].iloc[i-3]
    # 各データをインデックスのラベルを使用しないで結合する。
    training_test_data = training_test_data.append(
        {'snp_log_return_positive':snp_log_return_positive,
        'snp_log_return_negative':snp_log_return_negative,
        'snp_log_return_1':snp_log_return_1,
        'snp_log_return_2':snp_log_return_2,
        'snp_log_return_3':snp_log_return_3,
        'nyse_log_return_1':nyse_log_return_1,
        'nyse_log_return_2':nyse_log_return_2,
        'nyse_log_return_3':nyse_log_return_3,
        'djia_log_return_1':djia_log_return_1,
        'djia_log_return_2':djia_log_return_2,
        'djia_log_return_3':djia_log_return_3,
        'nikkei_log_return_1':nikkei_log_return_1,
        'nikkei_log_return_2':nikkei_log_return_2,
        'nikkei_log_return_3':nikkei_log_return_3,
        'hangseng_log_return_1':hangseng_log_return_1,
        'hangseng_log_return_2':hangseng_log_return_2,
        'hangseng_log_return_3':hangseng_log_return_3,
        'ftse_log_return_1':ftse_log_return_1,
        'ftse_log_return_2':ftse_log_return_2,
        'ftse_log_return_3':ftse_log_return_3,
        'dax_log_return_1':dax_log_return_1,
        'dax_log_return_2':dax_log_return_2,
        'dax_log_return_3':dax_log_return_3,
        'aord_log_return_1':aord_log_return_1,
        'aord_log_return_2':aord_log_return_2,
        'aord_log_return_3':aord_log_return_3},
        ignore_index=True)
# 3列目以降を説明変数として格納する。
predictors_tf = training_test_data[training_test_data.columns[2:]]
# 1、2列目を目的変数として格納する。
classes_tf = training_test_data[training_test_data.columns[:2]]
# 学習用セットのサイズを学習・テスト用データの80%に設定する。
training_set_size = int(len(training_test_data) * 0.8)
# 説明変数の初めの80%を学習用データにする。
training_predictors_tf = predictors_tf[:training_set_size]
# 目的変数の初めの80%を学習用データにする。
training_classes_tf = classes_tf[:training_set_size]
# 説明変数の残りの20%をテスト用データにする。
test_predictors_tf = predictors_tf[training_set_size:]
# 目的変数の残りの20%をテスト用データにする。
test_classes_tf = classes_tf[training_set_size:]

def tf_confusion_metrics(model, actual_classes, session, feed_dict):
    #
    predictions = tf.argmax(model, 1)
    #
    actuals = tf.argmax(actual_classes, 1)
    #
    ones_like_actuals = tf.ones_like(actuals)
    zeros_like_actuals = tf.zeros_like(actuals)
    ones_like_predictions = tf.ones_like(predictions)
    zeros_like_predictions = tf.zeros_like(predictions)
    tp_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
            tf.equal(actuals, ones_like_actuals), 
            tf.equal(predictions, ones_like_predictions)
            ), 
            "float"
        )
    )
    tn_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
            tf.equal(actuals, zeros_like_actuals), 
            tf.equal(predictions, zeros_like_predictions)
            ), 
            "float"
         )
    )    
    fp_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
            tf.equal(actuals, zeros_like_actuals), 
            tf.equal(predictions, ones_like_predictions)
            ), 
            "float"
        )
    )    
    fn_op = tf.reduce_sum(
        tf.cast(
            tf.logical_and(
            tf.equal(actuals, ones_like_actuals), 
            tf.equal(predictions, zeros_like_predictions)
            ), 
            "float"
        )
    )
    tp, tn, fp, fn = \
        session.run(
            [tp_op, tn_op, fp_op, fn_op], 
            feed_dict
        )
    tpr = float(tp)/(float(tp) + float(fn))
    accuracy = ((float(tp) + float(tn))/(float(tp) + float(fp) + float(fn) +
        float(tn)))
    recall = tpr
    precision = float(tp)/(float(tp) + float(fp))  
    f1_score = (2 * (precision * recall)) / (precision + recall)  
    print('Precision = ', precision)
    print('Recall = ', recall)
    print('F1 Score = ', f1_score)
    print('Accuracy = ', accuracy)
#
sess = tf.Session()
# 説明変数と目的変数の数を格納する。
num_predictors = len(training_predictors_tf.columns)
num_classes = len(training_classes_tf.columns)
# 
feature_data = tf.placeholder("float", [None, num_predictors])
actual_classes = tf.placeholder("float", [None, num_classes])
# 重みとバイアスを格納する変数を生成する。
weights = tf.Variable(
    tf.truncated_normal([num_predictors, num_classes], stddev=0.0001))
biases = tf.Variable(tf.ones([num_classes]))
# モデルを定義する。
model = tf.nn.softmax(tf.matmul(feature_data, weights) + biases)
# コスト関数を定義する。
cost = -tf.reduce_sum(actual_classes*tf.log(model))
# 学習率を設定する。
training_step = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)
# 
init = tf.global_variables_initializer()
sess.run(init)
# 正解した予測を格納する。
correct_prediction = tf.equal(tf.argmax(model, 1),
                              tf.argmax(actual_classes, 1))
# 正解率を格納する。
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
# 学習を実行する。
print('***二項分類を用いたモデル***')
print('学習用データを用いた正解率')
for i in range(1, 30001):
    sess.run(
        training_step, 
        feed_dict={
            feature_data: training_predictors_tf.values, 
            actual_classes: training_classes_tf.values.reshape(
                len(training_classes_tf.values), 2)
        }
    )
    if i%5000 == 0:
        print(i, sess.run(
            accuracy,
            feed_dict={
                feature_data: training_predictors_tf.values, 
                actual_classes: training_classes_tf.values.reshape(
                    len(training_classes_tf.values), 2)
            }
        ))
# 
feed_dict= {
    feature_data: test_predictors_tf.values,
    actual_classes: test_classes_tf.values.reshape(
        len(test_classes_tf.values), 2)
}
#
print('テスト用データを用いた検証結果')
tf_confusion_metrics(model, actual_classes, sess, feed_dict)
print('\n')
#
sess1 = tf.Session()
# 説明変数と目的変数の数を格納する。
num_predictors = len(training_predictors_tf.columns)
num_classes = len(training_classes_tf.columns)
# 
feature_data = tf.placeholder("float", [None, num_predictors])
actual_classes = tf.placeholder("float", [None, 2])
# 重み1-3、バイアス1-3を格納する変数を生成する。
weights1 = tf.Variable(tf.truncated_normal([24, 50], stddev=0.0001))
biases1 = tf.Variable(tf.ones([50]))
weights2 = tf.Variable(tf.truncated_normal([50, 25], stddev=0.0001))
biases2 = tf.Variable(tf.ones([25]))
weights3 = tf.Variable(tf.truncated_normal([25, 2], stddev=0.0001))
biases3 = tf.Variable(tf.ones([2]))
# 隠れ層を生成する。
hidden_layer_1 = tf.nn.relu(tf.matmul(feature_data, weights1) + biases1)
hidden_layer_2 = tf.nn.relu(tf.matmul(hidden_layer_1, weights2) + biases2)
# モデルを定義する。
model = tf.nn.softmax(tf.matmul(hidden_layer_2, weights3) + biases3)
# コスト関数を定義する。
cost = -tf.reduce_sum(actual_classes*tf.log(model))
# 学習率を設定する。
train_op1 = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)
# 
init = tf.global_variables_initializer()
sess1.run(init)
# 正解した予測を格納する。
correct_prediction = tf.equal(tf.argmax(model, 1),
                              tf.argmax(actual_classes, 1))
# 正解率を格納する。
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
# 学習を実行する。
print('***隠れ層を加えたモデル***')
print('学習用データを用いた正解率')
for i in range(1, 30001):
    sess1.run(
        train_op1, 
        feed_dict={
            feature_data: training_predictors_tf.values, 
            actual_classes: training_classes_tf.values.reshape(
                len(training_classes_tf.values), 2)
        }
    )
    if i%5000 == 0:
        print(i, sess1.run(
            accuracy,
            feed_dict = {
                feature_data: training_predictors_tf.values, 
                actual_classes: training_classes_tf.values.reshape(
                    len(training_classes_tf.values), 2)
            }
        ))
#
feed_dict= {
    feature_data: test_predictors_tf.values,
    actual_classes: test_classes_tf.values.reshape(
        len(test_classes_tf.values), 2)
}
#
print('テスト用データを用いた検証結果')
tf_confusion_metrics(model, actual_classes, sess1, feed_dict)

***二項分類を用いたモデル***
学習用データを用いた正解率
5000 0.559028
10000 0.559896
15000 0.5625
20000 0.567708
25000 0.567708
30000 0.565104
テスト用データを用いた検証結果
Precision =  0.39285714285714285
Recall =  0.0763888888888889
F1 Score =  0.12790697674418605
Accuracy =  0.4809688581314879


***隠れ層を加えたモデル***
学習用データを用いた正解率
5000 0.55816
10000 0.565972
15000 0.563368
20000 0.563368
25000 0.563368
30000 0.563368
テスト用データを用いた検証結果
Precision =  0.5102040816326531
Recall =  0.1736111111111111
F1 Score =  0.2590673575129534
Accuracy =  0.5051903114186851

「Accuracy」がテスト用データにおける正解率、つまり的中率である。二項分類を用いたモデルでは48.1%、隠れ層を加えたモデルでは50.5%で、いずれもほぼ50%となっている。これはコイントスと同じで、予測力はまったくないと言っていいだろう。

未来のデータあり+超単純モデル

今度は逆に、未来のデータを使えば、ディープラーニングでなくてもほぼ同等のパフォーマンスを叩き出すことができることを示そうと思う。

Googleのレポートで行われたことを簡単に説明すると、当日のS&P500が上がるか下がるかを、S&P500、NYSE総合、NYダウの1-3日前の対数変化率、日経平均株価、香港ハンセン、FTSE100、DAX、オーストラリアASXの0-2日前の対数変化率、合計8*3=24個のデータを説明変数としてディープラーニングで予測するというものである。

その結果、実に72.2%という的中率を実現した、と主張する。私は未来のデータを使うことが許されるなら、もっと少ない説明変数で、もっと単純な手法で同等の的中率を実現できることをこれから示す。

先ず、説明変数には0日前、つまり当日のFTSE100が上がったか下がったかの1個だけを使用する。次に学習は行わず、単にFTSE100が上がればS&P500も上がる、FTSE100が下がればS&P500も下がると予測することにする。およそディープラーニングとはほど遠い超単純なモデルである。

○以下のプログラムを実行する。

# coding: utf-8
import numpy as np
import pandas as pd
import time
from datetime import datetime
from pandas_datareader import data as web

# ヒストリカルデータの開始日と終了日を設定する。
start = datetime(2010, 1, 1)
end = datetime(2015, 10, 1)
# ヒストリカルデータをダウンロードする。
snp = web.DataReader('^GSPC', 'yahoo', start, end)
time.sleep(10)
ftse = web.DataReader('^FTSE', 'yahoo', start, end)
# 終値を格納する。
closing_data = pd.DataFrame()
closing_data['snp_close'] = snp['Close']
closing_data['ftse_close'] = ftse['Close']
# 終値の欠損値を補間する。
closing_data = closing_data.fillna(method='ffill')
# 終値の対数変化率を格納する。
log_return_data = pd.DataFrame()
log_return_data['snp_log_return'] = (
np.log(closing_data['snp_close'] /
    closing_data['snp_close'].shift()))
log_return_data['ftse_log_return'] = (np.log(closing_data['ftse_close'] /
    closing_data['ftse_close'].shift()))
# 最初の行はNaNなので削除する。
log_return_data = log_return_data[1:]
correct_prediction = (log_return_data['snp_log_return'] *
    log_return_data['ftse_log_return'])
correct_prediction[correct_prediction>=0] = 1
correct_prediction[correct_prediction<0] = 0
accuracy = correct_prediction.sum() / len(correct_prediction)
print('***超単純モデル***')
print('Accuracy = ', accuracy)

***超単純モデル***
Accuracy =  0.7159640635798203

的中率は71.6%でGoogleレポートの72.2%とほぼ同じである。つまり的中率70%はディープラーニングのおかげではなくて、未来のデータを使ったことによるのである。

日本株の予測でさらに注意すべきこと

予測をするのに当日のデータを使ってはいけない、ということが分かった。だが、前日のデータなら使って安心、とはならない。これは日本などのアジア株特有の問題である。

もしS&P500の前日終値が前日始値(または前々日終値)より上か下かで日経平均株価の当日終値が当日始値より上か下かを予測するのであれば問題ない。予測時点は日経平均株価の当日始値であり、S&P500の前日終値のデータはすでに存在するからである。

だが、S&P500の前日終値が前日始値(または前々日終値)より上か下かで日経平均株価の当日終値が前日終値より上か下かを予測するのであればこれは大問題である。予測時点は日経平均株価の前日終値であり、S&P500の前日終値のデータはまだ存在しないからである。

つまり、当日の日本の株価を予測する場合、予測時点が前日終値であれば、前日の欧米の株価を使ってはならないのであり、前々日以前のものを使わなければならないのである。