東大院生が考えたスマートフォンFXについて
この本は田畑昇人さんが書かれた本で、50万円の資金から9ヶ月で1000万円まで資産を増やしたという夢あふれる話を綴った本になります。
田畑さんはブログもされているので、そちらも参照ください。
注目すべき取引テクニック
こちらの本は、田畑さんが実践された具体的な取引ルールを多数書いてくれているので、すごく勉強になります。私が注目した取引テクニックが書かれた節をざっと以下に示します。
- 順バリ・逆バリは時間が決めてくれる
- ひとつめの武器「時間帯による値動きの特性」
- 為替市場の「特異」な傾向
- 月曜日の早起きは三文以上の得
- ゴトー日の「仲値のドル買い」を狙う
- 「スワップ3倍デー」の逆バリ取引
- 順バリにも逆バリにも使える「ボリン」
- 欧州順バリ時間のブレイク狙い
- エントリーの精度を上げる「MACD」
- 「急変後の朝8時台逆バリ」の公式を活用
時間帯によるFX市場の値動きの特性
今回は、以下の2点の内容をOANDAで取得したデータとPythonを用いて検証してみたいと思います。
- 順バリ・逆バリは時間が決めてくれる
- ひとつめの武器「時間帯による値動きの特性」
書籍にも書かれていますが、相場は以下の3種類しかありません。
- アップトレンド 上昇している相場
- ダウントレンド 下落している相場
- レンジ/もち合い トレンドがなく一定の幅で上下している相場
FXでの取引の仕方としては、アップトレンドにしたがって買い注文を入れる順バリ。アップトレンド中に下がるタイミングを予想して売り注文を入れる逆バリがあります。どちらが良いというのはないと思いますが、最初は順バリのほうが馴染みやすいかと思います。
本の中では、「いつ順バリで取引し、いつ逆バリで取引するべきか」を教えてくれるのが「時間」と言っています。
理由は金融市場を動かすメインプレーヤーの存在です。
ご存知の方も多いと思いますが、為替市場は24時間開いておりますが、市場を動かすメインプレイヤーはニューヨークのウォール街とロンドンのシティに住んでいます。あと東京市場も合わせて世界の三大金融市場となっています。なので、彼らが取引を行っているか行っていないかで相場の種類は変わるとのことです。
金融市場のメインタイムの図が以下になります。
東京 | ロンドン | ニューヨーク | |
株式市場の取引時間 | 9:00〜15:00 | 8:00〜16:30 | 9:30〜16:00 |
日本時間(冬時間) | 9:00〜15:00 | 17:00〜1:30 | 23:30〜6:00 |
日本時間(夏時間) | 9:00〜15:00 | 16:00〜0:30 | 22:30〜5:00 |
日本との時差 | – | マイナス9時間 | マイナス14時間 |
詳細は書籍を読んでいただければと思いますが、日本、ロンドン、ニューヨークのトレーダーの性格、心理から時間帯ごとに以下のような取引が良いと本の中で結論づけています。(時間帯は日本時間です。)

OANDAのデータを用いてデータ分析を実施
今回のデータ分析は、OANDAのデータを用いて実施します。OANDAデータの取得方法や分析前の前処理方法はこちらの投稿を御覧ください。
PythonとOANDA APIを使ってFXの過去データを取得
OANDAのFX時系列データを分析しやすいデータに変換
上記のページの処理を行うと以下のようなデータが得られます。
このデータを用いてデータ分析を行っていきます。

各時間帯の終値のトレンドをみる
今回は、ある時間帯にトレンドが発生しているかどうかをみたいので、指定した時間の最初の終値とその後の終値との比を取る処理をします。
もう少し具体的に言うと、2019年4月1日の9時から14時のデータのトレンドをみるとすると、2019年4月1日9時の終値が110.992円なので、2019年4月1日9時から14時までのデータを110.992円で除し、2019年4月1日9時の終値が「1」、それ以降の値は終値に対して、何倍になっているかを表すことになります。
以下のコードでは、時間帯を指定すると全データから指定の時間帯のデータを全てとってきて、dataリストに格納するようなコードにしています。
def specific_time(df,start_time,end_time):
return df[(df['hour'] >= start_time) & (df['hour'] <= end_time)]
def get_norm_close(df):
data = []
for y in range(2010,2020):
for m in range(1,13):
for d in range(1,32):
buf = df[(df['year'] == y)&(df['month'] == m)&(df['day'] == d)]
if buf.shape[0] != 0:
buf = buf.reset_index(drop=True)
buf['norm_close'] = buf['close']/buf.loc[0,'close']
buf = buf.loc[:,['time','norm_close']]
data.append(buf)
return data
df5 = specific_time(df4,9,14) #9時から14時のデータを取得
data = get_norm_close(df5)
specific_time関数は開始hourと終了hourを入れるとその時間帯だけのデータを抽出します。
get_norm_close関数はspecific_time関数で限定した時間帯のデータを年、月、日毎のデータに分割し、各年月日毎に最初のhourの値で除し、dataリストに格納します。
実行するとdataリストには3404個の9時〜14時の時間帯のデータが格納されました。
print(len(data))
>> 3403
そのうちの1つを見てみましょう。
おそらく下のような9時から14:55までのデータが72行分入っていると思います。
print(data[0])
>>
time norm_close
0 09:00:00 1.000000
1 09:05:00 0.999286
2 09:10:00 0.999481
3 09:15:00 0.999892
4 09:20:00 1.000195
5 09:25:00 1.000346
...
67 14:35:00 0.993251
68 14:40:00 0.993164
69 14:45:00 0.991715
70 14:50:00 0.991888
71 14:55:00 0.991650
[72 rows x 2 columns]
各時間帯の傾きの取得(トレンドの取得)
データにトレンドがあるかどうかというのを表現するのは非常に難しいのですが、今回はデータの1次回帰線の傾きの有無を見ていきたいと思います。(異論がある方は多いにいると思いますが、今回はとりあえずこれでいきます。)
先程のデータの1つをグラフ化して、1次回帰直線を書いてみたいと思います。
d = data[0]
x = d.index
y = d['norm_close']
res=np.polyfit(x,y,1)
y1 = np.poly1d(res)(x)
plt.scatter(x, y, label='元データ')
plt.plot(x, y1, label='1次')
plt.ylim(0.985,1.010)
plt.show()
print(res[0],res[1]) #res[0]が傾き、res[1]は切片
>> -0.00011096809465592363, 1.0005546473312508
実行すると以下のグラフが得られます。
この傾きが-0.00011と言っています。今回は5分足なので、5分毎に-0.00011円 = -0.11pips下がるような時間帯だといえます。これがトレンドと言うと怒られそうですが、今回はこの時間帯のトレンドとします。
各時間においてこの傾きがどれくらい発生しているかを見ることで、その時間帯がトレンドを持っているかどうか見ていきます。

傾きを取得しやすいようにget_slope関数を作っておきます。
傾きは1000倍してpipsの単位にしておきます。
def get_slope(data):
slopes = []
for i in range(len(data)):
d = data[i]
x = d.index
y = d['norm_close']
slope=np.polyfit(x,y,1)[0]
slopes.append(slope*1000) #pips単位に変換
return slopes
slopes = get_slope(data)
夏時間と冬時間の区別
時間帯ごとのデータの特性を確認する上で、夏時間と冬時間を区別することが重要になります。
本の中で述べている夏時間と冬時間の時間帯の違いを反映させるために辞書を作成します。
データフレームのEST_EDTの部分をもちいて、夏時間(EDT)と冬時間(EST)を区別し、取得するデータを変えます。
#夏時間と冬時間の時間帯の違いを辞書化
summer_time_range = {'start_time':[0,9,14,16,20,21],'end_time':[4,14,15,20,21,24]}
winter_time_range = {'start_time':[0,1,9,14,17,21,22],'end_time':[1,5,14,15,21,22,24]}
season = ['summer','winter']
#夏時間と冬時間でデータフレームから取得する時間帯を変える
if season == 'summer':
time_range = summer_time_range
df6 = df5[df5['EST_EDT']=='EDT']
elif season == 'winter':
time_range = winter_time_range
df6 = df5[df5['EST_EDT']=='EST']
時間帯毎の傾きの平均値と分布【結果】
ようやくすべての準備が整ったので、上で作成した関数を使いながら、傾きの分布と平均値を夏時間、冬時間および各時間帯ごとにみてみます。
summer_time_range = {'start_time':[0,9,14,16,20,21],'end_time':[4,14,15,20,21,24]}
winter_time_range = {'start_time':[0,1,9,14,17,21,22],'end_time':[1,5,14,15,21,22,24]}
season = ['summer','winter']
for s in season:
if s == 'summer':
time_range = summer_time_range
df6 = df5[df5['EST_EDT']=='EDT']
elif s == 'winter':
time_range = winter_time_range
df6 = df5[df5['EST_EDT']=='EST']
rep = len(time_range['start_time'])
for j in range(rep):
df6 = specific_time(df5,time_range['start_time'][j],time_range['end_time'][j])
data = get_norm_close(df6)
slopes = get_slope(data)
#傾きの平均、標準偏差を取得
mean = np.nanmean(np.array(slopes))
std = np.nanstd(np.array(slopes))
#ヒストグラムの和が1になるように処理(比較しやすいように)
weights = np.ones(len(slopes))/float(len(slopes))
#描画
plt.figure(figsize=(15,2))
plt.hist(slopes,ec='white',range=(-0.2, 0.2), bins = 40,weights=weights)
plt.title('{}_{}-{}oclock mean={:.4f} std={:.4f}'.format(\
s,time_range['start_time'][j],time_range['end_time'][j],mean,std))
plt.vlines(mean,0,0.25,color='red')
plt.vlines(mean+std,0,0.25,color='green')
plt.vlines(mean-std,0,0.25,color='green')
plt.ylim(0,0.25)
plt.grid()
plt.show()
時間帯ごとの平均をまとめた表が下になります。
夏時間9-14時を例に取ると仮に2010年1月10日〜2010年5月1日までの期間、朝9時に売り注文を入れて、14時に買い戻していれば-0.044pips/hour×5時間 = -0.22pips/日なので、-0.22pips/日×約2400日 = 528pipsを利益として得られた計算になります。でもこれはかなりイカサマ感があります。傾き使っているのがあまり良くないですね。ちゃんと9時と14時の終値の差分の統計をとった方が正確な利益の平均値が得られると思います。また、余裕があればそれも計算してみたいと思います。


傾きの分布をヒストグラムで表したものが下の図になります。
図の見方ですが、傾きが横軸で、縦軸が発生頻度を表しているので、0付近で棒グラフが大きい場合は、その時間帯はレンジ相場気味だということになります。傾きがプラス側に大きくなると上昇トレンドが発生している、逆に傾きがマイナス側に大きくなると下降トレンドが発生している考えます。


夏時間の14-15時、20-21時、21-24時、冬時間の0-1時、21-22時、22-24時は標準偏差が大きくなだらなかな山になっています。これは、レンジ相場も上昇トレンドも下降トレンドもほぼ同じように発生していて、分布も正規分布とは言いづらく、長期的な試行を繰り返しても先程の平均値が得られる可能性はかなり低そうです。
この不確実さの原因は、それぞれ時間レンジが短いというのがかなり影響している気がします。あとは、本の中で述べている「アメリカの経済指標の発表時間帯で市場が荒れやすい」というのも関係しているかもしれません。上のグラフは-0.1〜0.1でデータを切っていますが、実際はこの時間はすごく裾野が広く、変動幅が大きい点からもそれを示していると思います。
やはりこれらの時間帯で取引をするのはかなりリスクが高いので、「ニューヨーク市場が開く夏時間21-24時、冬時間の22-25時は取引を見送る」というのはデータの点からも合理的なのかなと思います。
夏時間の0-4時、9-14時、16-20時、冬時間の1-5時、9-14時、17-21時の時間帯はは割と正規分布になっているので、信頼性は割と高いと思います。ただ、よく考えたら上昇トレンドと下降トレンドのどちらもが同じ頻度で起こってしまうとピークが2つできる想定になるので、正規分布じゃない方がよいということになってしまいますね。
んートレンドの有無をみたいだけなら、傾きをすべてプラスにしてしまって、ヒストグラムをとった時にピークが存在するかみた方が良かったかもしれません。
あと、10年分のデータをまとめて統計とっているので、過去の環境と現在の環境などが違いすぎてなかなか自身が持てないですね。もう少し条件を絞ったり、最近のデータだけに絞るなどした方が良い気がしました。
まとめ
以上のような感じで以下2点確認してきましたが、かなり難しいですね。
まず順バリか逆バリかはどっちが良いのか今回の分析では全くわかりません。
時間帯による値動きの特性は、書籍の推奨する時間で実施すれば、確かにあると思いますが、上昇トレンドか下降トレンドのどちらになるかを別の方法で知る必要がある気がします。
- 順バリ・逆バリは時間が決めてくれる
- ひとつめの武器「時間帯による値動きの特性」
ただ、取引を避けた方が良い時間という点に関しては本の通りだったかなと思います。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import time
from datetime import datetime,timedelta
from pytz import timezone, utc
from tzlocal import get_localzone
import pickle
def prep(df):
df.iloc[:,1:] = df.iloc[:,1:].astype(float) #文字列からfloatへの変換
df = df.loc[:,['time','open','high','low','close','volume']] #ohlcへの変換
df['datetime_utc'] = [datetime.strptime(i,'%Y-%m-%dT%H:%M:%S') for i in df['time']] #時系列データの変換
df = df.drop('time',axis=1)
return df
def fill_index(df, gran):
#取得したデータの期間を確認
time_index = []
time = df['datetime_utc'][0]
time_range = int((df.iloc[-1,5] - df.iloc[0,5]).days * 24 * 60/gran) \
+ int((df.iloc[-1,5] - df.iloc[0,5]).seconds/60/gran)
#インデックスが正しく5分刻みになったデータフレームの作成
for i in range(time_range):
time_index.append(time)
time += timedelta(minutes=gran)
df_ = pd.DataFrame()
df_['datetime_utc'] = time_index
return pd.merge(df_,df, on='datetime_utc',how='left') #データフレームをマージ
def convert_datetime(buf):
#日本時間とニューヨーク時間の取得
utc = timezone('UTC')
tokyo = get_localzone()
ny = timezone('America/New_York')
jp_time = list(map(lambda x: utc.localize(x).astimezone(tokyo), buf['datetime_utc']))
ny_time = list(map(lambda x: utc.localize(x).astimezone(ny), buf['datetime_utc']))
df = pd.DataFrame()
for col in buf:
df[col] = buf[col]
df['datetime_jp'] = [v.strftime('%Y-%m-%d %H:%M:%S') for v in jp_time]
df['year'] = [t.year for t in jp_time]
df['month'] = [t.month for t in jp_time]
df['day'] = [t.day for t in jp_time]
df['time'] = [v.strftime('%H:%M:%S') for v in jp_time]
df['hour'] = [t.hour for t in jp_time]
df['weekday'] = [t.weekday() for t in jp_time]
df['EST_EDT'] = [v.strftime('%Y-%m-%d %H:%M:%S%Z')[-3:] for v in ny_time] #EDT = summer, EST = std
return df
def specific_time(df,start_time,end_time):
return df[(df['hour'] >= start_time) & (df['hour'] <= end_time)]
def get_norm_close(df):
data = []
for y in range(2010,2020):
for m in range(1,13):
for d in range(1,32):
buf = df[(df['year'] == y)&(df['month'] == m)&(df['day'] == d)]
if buf.shape[0] != 0:
buf = buf.reset_index(drop=True)
buf['norm_close'] = buf['close']/buf.loc[0,'close']
buf = buf.loc[:,['time','norm_close']]
data.append(buf)
return data
def get_slope(data):
slopes = []
for i in range(len(data)):
d = data[i]
x = d.index
y = d['norm_close']
slope=np.polyfit(x,y,1)[0]
slopes.append(slope*1000) #pips単位に変換
return slopes
with open("USD_JPY_M5_20100101-20190501.pickle", mode='rb') as f:
df = pickle.load(f)
gran = 5 #5分足を使用
df1 = prep(df)
df2 = fill_index(df1, gran)
#時系列データの変換
df3 = convert_datetime(df2)
#2010年1月10日〜2019年5月1日までにデータを限定
df4 = df3[(df3['datetime_jp'] > '2010-01-05')&(df3['datetime_jp'] < '2019-05-01')].reset_index(drop=True)
df5 = specific_time(df4,9,14) #9時から14時のデータを取得
data = get_norm_close(df5)
summer_time_range = {'start_time':[0,9,14,16,20,21],'end_time':[4,14,15,20,21,24]}
winter_time_range = {'start_time':[0,1,9,14,17,21,22],'end_time':[1,5,14,15,21,22,24]}
season = ['summer','winter']
for s in season:
if s == 'summer':
time_range = summer_time_range
df6 = df5[df5['EST_EDT']=='EDT']
elif s == 'winter':
time_range = winter_time_range
df6 = df5[df5['EST_EDT']=='EST']
rep = len(time_range['start_time'])
for j in range(rep):
df6 = specific_time(df5,time_range['start_time'][j],time_range['end_time'][j])
data = get_norm_close(df6)
slopes = get_slope(data)
#傾きの平均、標準偏差を取得
mean = np.nanmean(np.array(slopes))
std = np.nanstd(np.array(slopes))
#ヒストグラムの和が1になるように処理(比較しやすいように)
weights = np.ones(len(slopes))/float(len(slopes))
#描画
plt.figure(figsize=(15,2))
plt.hist(slopes,ec='white',range=(-0.2, 0.2), bins = 40,weights=weights)
plt.title('{}_{}-{}oclock mean={:.4f} std={:.4f}'.format(\
s,time_range['start_time'][j],time_range['end_time'][j],mean,std))
plt.vlines(mean,0,0.25,color='red')
plt.vlines(mean+std,0,0.25,color='green')
plt.vlines(mean-std,0,0.25,color='green')
plt.ylim(0,0.25)
plt.grid()
plt.show()
参考サイト
