Fxなんぞ、人工知能(AI)とかいう深層学習を使わなくとも戦える!
目次
深層学習など使う必要なくただの線形回帰の取引でもFxは戦える!
ずいぶん更新をさぼっていましたが、この前ふと思いついたトレーディングシステムを考案して、幾分パフォーマンスも良かったので共有しておきます。
深層学習とか使わない故にとても原理は簡単・簡潔なルールベースのアルゴリズム取引です。スペックの問題もあるため、モデルの構築に結構時間はかかりますが、ぜひお試しください。
解説:紹介するモデル
内容としてはとてもシンプルで、予測対象とする時系列に対し、過去データに関する特定の範囲で区切った収益率の時系列との類似度を評価し、各期間ごとのアンサンブル平均で取引の有無を判断するものです。
大変簡単なモデルでしょう。
では、まず初めに、アンサンブル平均について紹介します。
※注意※:意外と説明が長くなったので、そんなのさっさといいから、早くコード見せろやぁ!!って方は、次の節にお進みください。
アンサンブル平均とは
機械学習の分野でもアンサンブル学習という言葉があるように、数理最適化の分野ではよく用いられる手法です。何をするかというとイメージで言えば多数決に近いです。以下のような表を考えてみましょう。
大きいですね。
この表は、1期ごとにモニターであるA,B,C君たちが、あるレストランの評価をつけて、このお店のランク付けいわば食べログみたいなことをしようというわけです。
その時に、評価を行った全期間に対して、A,B,Cそれぞれが平均してどう評価したのかを表すのが、一番右の青で囲った列である期間平均の項目になり、これはいわゆる皆さんがイメージする平均だと思います。しかし、平均の評価には、もう一つ方法がありますよね。
先ほどは行に対する平均を取っていましたから、当然列に関する平均も取れるわけです。つまり一番下の行にある赤線で囲われてる欄が、各時期ごとによるA,B,C3人による評価の平均を表しています。
つまり、これがアンサンブル平均ということです。時点時点ごとに対して、それぞれ別々の特徴量(A,B,C)が持つ値に関して平均を取っているということです。
今回は凄く簡潔に説明したので、原理主義者からお叱りを受ける内容になっているかもしれませんが、アンサンブル平均に関する中心的な考え方を説明する目的で紹介しています。多少の説明不足ご容赦ください。
類似度評価とは
今回のモデルの肝となる部分に入ります。今回行う手法は、対象とするデータに対して、過去データとの類似度を評価するものです。何も情報が増えない説明の仕方になってますが、とりあえず例を上げて考えましょう。
類似度評価は主に二つあると思っていて、一つはユークリッド距離による評価、二つ目はコサイン類似度による評価です。それぞれについて
1ユークリッド距離による評価:比較したい特徴ベクトル同士の距離の近さで類似度を評価します。つまり評価基準が距離にあるということです。
2コサイン類似度による評価:比較したい特徴ベクトル同士の内積を考え、互いの長さの積で除した値で類似度を評価します。つまり、コサイン類似度は、ベクトル同士のなす角度で類似度を評価します。なす角の評価指標にコサインを使用しているので、1に近ければよく似ており、-1に近いと性質上まったく反対にあると考えられます。(やや語弊があるかもしれませんが、類似度という意味では、全く違うという風に捉えてください。)
言葉だけでは伝わりにくいかと思いますので、図にしてみました。
大きいですね。数式にLaTeX使えよって声が聞こえてきますが、最近耳が悪いのでよくわかりません。
要は、このような感じで二つのベクトルの類似度を評価しています。それぞれに対して、ユークリッドの方は単純に距離なので近ければより良いと考えられ、汎用性は高い反面、特徴量に関して線形性による影響がない、つまり、ベクトルaを、aと表現したとき、aと単純に2倍の長さである2aに関して大きな意味の違いはないということです。
これの何が行けないかというと、例えばaとbの類似度(距離)が1とほぼ同じときでも、2aとbの類似度(距離)は約2倍ぐらいになってしまい、2aとbは違うものと評価してしまいかねません。また、同様に少しベクトル同士のなす角が違うだけで、意味が大きく異なるような場合でも、ユークリッド距離による評価は、互いの距離が近いという理由で似ていると評価してしまいます。
そのような問題点を解決するのが、コサイン類似度です。とは言え、評価としてなす角を用いるため、距離に関する情報は含まれません。全ての特徴量が同じ長さに正規化されている場合にのみ使えるものなので注意してください。
以上が類似度に関する紹介でした。
今回のモデルで行う類似度の評価方法
今回のモデルでは、扱う特徴量を正規化させるのも面倒だし、距離を測って評価するのも使い勝手が悪いなぁと思ったので、今回はこの二つのハイブリッドにします。
説明も面倒なので、要点だけ言えば、対象とする特徴量のベクトルに、過去データの特徴量群を射影した際の対象とするベクトルとの一致度で似ているかどうかの評価を行います。
図で説明するのが一番手っ取り早いので、以下の図をどうぞ
ほぼ図に説明書いたので、詳細は省きます。とりあえずこんな感じで評価してます。あとはコードでも参照してください。
本題:モデルの中身
その前に
一応今回のモデルは、膨大な過去データを用いますので、以前紹介した記事
OANDA API V20で選択した期間の過去レートを取得してみる
こちらで過去データを取得するスクリプトを公開していますが、これをモジュール化して、関数としていつでも使えるようにしておきましょう。
以下がそのスクリプトです。
上記で挙げている記事を参照してもらえれば、何をしているかわかります。
このスクリプトを「getdata.py」という名前にして保存しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import configparser import pandas as pd import pytz import numpy as np import oandapyV20 import oandapyV20.endpoints.instruments as instruments import oandapyV20.endpoints.pricing as pricing from datetime import datetime, timedelta import datetime as dt import gc def get_data(asi,ex_pair="USD_JPY",end1="2016-01-01T10:00:00",start=False,step=5000): config = configparser.ConfigParser() config.read('./config/config_v1.txt') # パスの指定が必要です account_id = config['oanda']['account_id'] api_key = config['oanda']['api_key'] oanda = oandapyV20.API(access_token=api_key) ex_pair = ex_pair params1 = {"instruments": ex_pair} psnow = pricing.PricingInfo(accountID=account_id, params=params1) now = oanda.request(psnow) end = now['time'] asi = asi if start:end = start print('start date => ',end) # get_date = "2019-01-019T0:00:00" #get_date = "2009-12-12T10:00:00" get_date = end1 # "2005-01-02T19:12:00" からが良い i=0 while(end > get_date): params = {"count":step,"granularity":asi,"to":end} r = instruments.InstrumentsCandles(instrument=ex_pair, params=params,) apires = oanda.request(r) res = r.response['candles'] end = res[0]['time'] n = 0 if i == 0 : res1 = res.copy() else : for j in range(n,len(res1)):res.append(res1[j]) if end < get_date: for j in range(step): if res[j]['time'] > get_date: end = res[j-1]['time'] n = j break res1 = res[n:].copy() if i%10==0: print('res ok', i+1, 'and', 'time =', res1[0]['time']) gc.collect() i+=1 print('GET Finish!',I*step - n) data = [] for raw in res1: data.append([raw['time'], raw['mid']['o'], raw['mid']['h'], raw['mid']['l'], raw['mid']['c']]) df = pd.DataFrame(data) df.columns = ['date', 'open', 'high', 'low', 'close'] df["date"] = df["date"].astype("datetime64")+dt.timedelta(hours=9) df = df.set_index('date') return df.astype({'close':float,'open':float,'high':float,'low':float}) if __name__ == "__main__": asi = input('時間足') ex_pair = input('通貨ペア') df = get_data(asi,ex_pair) import os path = './data/' os.makedirs(path,exist_ok=True) df.to_csv(path+ex_pair[:3]+ex_pair[4:]+'_'+asi+'.csv', encoding='UTF8') |
この関数は、引数に(asi,ex_pair=”USD_JPY”,end1=”2016-01-01T10:00:00″,start=False,setp=5000)をとりますが、それぞれ
asi : 時間足
ex_pair : 通貨ペア
end1 : 持ってきたい過去データの期間
start : 持ってきたい過去データの最新の期間(初期値がFalseのときは、現在の価格からend1で指定したところまでを持ってくる)
step : 一度に取得するデータ数。初期値は5000。日足などを持ってきたい場合は、100とかにすると良い
となっています。
解析結果
今回、使う通貨と時間足は、ドル円と4時間足で、1日後を予測します。
特徴量データとして、過去[5,10,15,20,25,50,75,100,125,150,200,250]本分の収益データを使用し、各ベクトルを保存します。
期間は、類似度を評価するための基準のデータ群を2009年1月から2014年12月までのデータで、検証期間を2015年1月から現在までです。おおよそ8千データぐらいです。多い足りないの感覚はお任せします。
よって、検証期間では、上記の特徴量として得られた各時点に対するベクトルと基準データ群との類似度を評価し、直近で類似している6時点をピックアップし、それらのデータに対する1日後の価格差を確認し、類似度による重みづけ平均をとって、検証データにおける予測値を求めます。
また、予測値を出し終えたデータは、順次基準データに追加されていくようになっているので、最新のデータも考慮した類似度評価を行えるようにしています。
文章だけだと分かりにくいと思うので、詳細は最後にあるコードの中身を参照ください。(恐ろしく読みにくいので覚悟してください)
そんなこんなで、実際にコードを動かしてみました。
また、比較として、常に買い続けた場合とアンサンブル平均と線形回帰の3つを見てみます。また、線形回帰を用いるので、検証データの内、前半を訓練データに、後半をテストデータに用います。
それでは結果をご覧ください。
見た感じ、線形回帰がいい結果出てますね。あんなけ長々と説明したアンサンブル平均は惨敗です。線形回帰だと、2年半ほどで大体4000pipsは勝ち越してますね。
ちなみに、取引コストも考慮したものを見てみましょう。
単純に利益が減るだけなのですが、大体2500pipsほどは勝ててるようですね。
ですので、例えば、4時間ごとに10000通貨づつポジションを取得し続けて、各ポジションを1日経過するごとに下すと、2年半ほどで大体25万円は儲かるという算段ですね。これが100,000通貨であれば250万ほどですね。元手が大いに越したことはないですね。うらやましいです。
ちなみに、線形回帰が一番良くて、アンサンブル平均はやっぱりダメなの??という感じなのですが、少し扱うデータをいじってみても、やっぱり線形回帰が一番パフォーマンスが良かったですね。
ちなみに、以下は、ドル円の日足で6営業日後を予測しています。また、類似度の取得数として6時点から10時点に増やしました。その他は全て同じです。
1枚目が取引コストなし、2枚目が取引コストありです。
やはり、線形回帰がいいですね。というよりバカ勝ちですね。
まとめ
線形回帰こそ、全ての始まりであり至高のモデルであることが分かりました。実際に運用するには、(取引通貨量)の100倍程度の資金を用意しておくべきでしょう。(例えば、1万通貨づつ突っ込みたいなら、100万円ほどは最低でも用意しておくべき)
一応noteにて販売もしておきます
本モデルは明らかに日足を用いる方が利益を期待できます。なので、長期間プログラムを実行すると運用上のコストもあるので、使うときだけ実行して、ロングかショートのシグナルを出力してくれる程度のプログラムに留めておきます。
また、少しだけ改良も加えたものを販売いたします。値段もそんなに高くなく、1,000円程度で販売する予定です。
noteが出来次第公開します。お知らせ等は、Twitterを通して行うので、よければ@mathokaproで探してください。
あと、販売する以上コードの内容も詳しくコメントに残しているので、下記にある生のコードじゃわからんって人は、勉強用として、また、私へのサポートのお気持ちとして、買っていただけると大変励みになります。
どうぞよろしくお願いします。
公開しました!!:
多数決の原則でFxの取引シグナルを出力するPythonスクリプトを作ったので、よければ買ってってください。
付録:使用したコード
以下に使用したコードを残しておきます。一応コピペすれば使えるはずです。グラフの描画までしてくれます。
上記であげた「getdata.py」のスクリプトファイルと同じディレクトリに保存してください。
また、人に読んでもらえるような書き方をしていないので、根気がある方だけ読んでください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
import numpy as np import scipy.optimize as optimize import matplotlib.pyplot as plt import configparser import oandapyV20 import oandapyV20.endpoints.instruments as instruments import oandapyV20.endpoints.pricing as pricing from getdata import * import imageio import gc import random from tqdm import tqdm import statsmodels.api as sm from scipy import stats from sklearn.linear_model import Lasso, LinearRegression df = get_data('D',end1='2009-01-01',ex_pair='USD_JPY',step=100) Rt = (df.close.shift(-1)-df.close).dropna().copy() close = df.close.copy() param = [5,10,15,20,25,50,75,100,125,150,200,250] fc = 6 vectors = [] vectors2 = [] for num in param: vec = [] vec1 = [] for i in tqdm(range(max(param),len(Rt)-fc+1)): vec1.append(Rt[i:i+fc].values.sum()) vec.append(Rt[i-num:i].values) vectors.append(np.array(vec)) vectors2.append(np.array(vec1)) gc.collect() split = df[df.index<pd.Timestamp('2020-02-01 06:50')].shape[0]-max(param) x_box = [vectors[i][:split] for i in range(len(param))] t_box = [vectors2[i][:split] for i in range(len(param))] x_pre = [vectors[i][split:] for i in range(len(param))] t_pre = [vectors2[i][split:] for i in range(len(param))] x,t = [],[] for i in tqdm(range(x_pre[0].shape[0])): v1 = [x_pre[j][i] for j in range(len(param))] dots = [np.sort(((x_box[j]*v1[j]).sum(axis=1)/(v1[j]**2).sum())) for j in range(len(param))] dod = [((x_box[j]*v1[j]).sum(axis=1)/(v1[j]**2).sum()) for j in range(len(param))] pick = 5 up1 = [len(np.where(dots[j]-1>=0)[0][:pick]) for j in range(len(param))] down1 = [len(np.where(1-dots[j]>0)[0][-pick-(pick-up1[j]):]) for j in range(len(param))] up = [dots[j][np.where(dots[j]-1>=0)[0][:up1[j]]][-1:] for j in range(len(param))] down = [dots[j][np.where(dots[j]-1<0)[0][-down1[j]:]][-1:] for j in range(len(param))] up = [up[j] if len(up[j])!=0 else 1 for j in range(len(param))] down = [down[j] if len(down[j])!=0 else 1 for j in range(len(param))] cos = [(((x_box[j]*v1[j]).sum(axis=1)/(v1[j]**2).sum())>down[j])&(((x_box[j]*v1[j]).sum(axis=1)/(v1[j]**2).sum())<=up[j]) for j in range(len(param))] x_ = [(t_box[j][cos[j]]*dod[j][cos[j]]).sum()/dod[j][cos[j]].sum() for j in range(len(param))] x += [x_] t += [t_pre[0][i]] x_box = [vectors[j][:split+i+1] for j in range(len(param))] t_box = [vectors2[j][:split+i+1] for j in range(len(param))] if i%100==0:gc.collect() x = np.nan_to_num(x) sp = int(len(x)*0.5) sp2 = len(t) x_train,x_test = np.array(x)[:sp], np.array(x)[sp:sp2] t_train,t_test = np.array(t)[:sp], np.array(t)[sp:sp2] # reg = Lasso(alpha=0.2,normalize=True) # reg.fit(x_train,t_train) # y_pre = reg.predict(x_test) # y_acu = reg.predict(x_train) reg1 = LinearRegression(normalize=True) reg1.fit(x_train,t_train) y_pre = reg1.predict(x_test) y_acu = reg1.predict(x_train) Rt5 = np.log(df.close.shift(-fc)/df.close).copy().dropna() Rt5 = (df.close.shift(-fc)-df.close).copy().dropna() res = ((np.sign(y_pre)*Rt5[max(param)+split+sp:max(param)+split+sp+sp2])) res1 = ((Rt5[max(param)+split:max(param)+split+sp].values)) ac = (np.sign(x.mean(axis=1))*Rt5[max(param)+split:]) C = x.mean(axis=1)[:sp] cc = (x.mean(axis=1)>C.mean()+C.std()*1).astype(int) - (x.mean(axis=1)<C.mean()-C.std()*1) acc = (cc*Rt5[max(param)+split:]) (Rt5[max(param)+split+sp:].cumsum()).plot(label='ドル円') (ac[sp:].cumsum()).plot(label='アンサンブル平均') (res.cumsum()).plot(label='線形回帰') plt.ylabel('収益') plt.xlabel('期間') plt.legend() plt.tight_layout() plt.show() cost = 0.004 diff = (df.close.shift(-fc)-df.close).dropna() acc1 = (np.sign(y_pre)*diff[max(param)+split+sp:]) val_cost = acc1 - np.ones_like(acc1)*cost*np.abs(np.sign(y_pre)) val_cost2 = ac[sp:] - np.ones_like(np.sign(x.mean(axis=1))[sp:])*cost*np.abs(np.sign(np.sign(x.mean(axis=1)[sp:]))) val_cost3 = Rt5[max(param)+split+sp:] - np.ones_like(Rt5[max(param)+split+sp:].values)*cost*np.abs(np.sign(Rt5[max(param)+split+sp:].values)) val_cost3.cumsum().plot(label='ドル円買い') val_cost2.cumsum().plot(label='アンサンブル平均') val_cost.cumsum().plot(label='線形回帰') plt.ylabel('収益') plt.xlabel('期間') plt.legend() plt.tight_layout() plt.show() |
ディスカッション
コメント一覧
まだ、コメントがありません