
[Forex]MachineLearning x Heroku(Python) x MT4/5
はじめまして、ナイナイです。今までエンジニアとして活躍してきて、直近はプロデューサー職を経て、現在はデータサイエンティストのヒヨコとして活躍しております。そんな中で、今までMTには興味があり、独自のEAなども作ったことはあり、将来的にMachine Learningを使ってトレードできたらいいなー。とよくある安直な考えのもと勉強し、OANDA APIって便利なものがあるー、が最近は利用するための敷居が高くなった!などもあり、今回の環境で構築してみようと思いました。興味がある方は、ぜひ参考になればと思います。
今回のnoteを読んでいただきできるようになること
1,Google Colaboratoryを使った予測モデルの作成(Regressionモデル)ができるようになります(要望があれば他の予測モデルも公開予定)
2,Herokuを使ったPython+Flask+PostgreSQLのシステム開発(拡張性を考慮した実装にします)ができるようになります
3,MT4/5からWEBサーバを呼び出しトレードを行うExpeartAdvisorを作れるようになります(MT4/5では決済のメソッドが違う)
逆に今回やらないこと
1,テストなどの類は一切やりません。基本、デモ口座でフォワードテストをします(MTの都合もあり)
では、早速Machine Learningパートから。
まずはMachine Learningの話をしたいのですが、その前に、そもそも今回どんなシステム構成となるのかを先に整理しておきます。
基本的にはGoogle Colaboratoryで予測モデルを作成し、Python+Flask+PostgreSQL環境をHeroku上に構築し、MT4/5から、そのHerokuの環境にアクセスし、作った予測モデルを呼び出してトレーディングのエントリーに使う。という仕組みになります。(絵が下手なので言葉のみですみません)
予測モデルを作る上で大事なこと
予測モデルを作る上で、まずは1.どういった情報があるのか?2.何を結果として出したいのか。の2つの確認が必要となります。
「1.どういった情報があるのか」については、今回はMT4/5から取得できる情報としています。本来であれば、株価などの動きも考慮したほうが良いと思いますが、今回はショートカットで今回の一連の環境を構築することを目的としています。
「2.何を結果として出したいか」についても、様々な議論があると思います、Regression系で言うなら、Close(終値)を予測するとか、Classification系なら次の1時間後に上がっているか、下がっているかを予測するとか。今回は15分足の情報を利用し、次の15分後の高値を予測するRegressionモデルを採用します。
もう1つ、とても大事な話をしますが、通常のRegressionモデルでは過去の様々な特徴量から予測値を導き出しますが、Forexは時系列で動く情報であるため時間の流れが重要です。そういう意味で、今回は時系列も考慮した実装にする予定です。(これらの基礎知識については申し訳ありませんが、触れません。触れると長くなるから)
今回採用する予測モデルの構成は、LightGBMを基本として、時系列データを擬似的に生成して予測するスタイルにします(一部、実は不要なコードがあります)
これからコードを記載します。これらはGoogleColaboratory上に、inputsという名前のフォルダ作成し、MT5で、USDJPYの15分足のHistoricalDataをUSDJPY_M15.csvと言うファイル名で用意してください
<DATE> <TIME> <OPEN> <HIGH> <LOW> <CLOSE> <TICKVOL> <VOL> <SPREAD>
2020.04.01 00:00:00 107.525 107.552 107.470 107.537 30 0 61
2020.04.01 00:15:00 107.537 107.555 107.514 107.540 83 0 57
data = pd.read_csv('inputs/USDJPY_M15.csv', sep='\t', names=('date', 'time', 'open', 'high', 'low', 'close'), usecols=[0, 1, 2, 3, 4, 5], skiprows=1)
data['datetime'] = pd.to_datetime(data['date'] + ' ' + data['time'])
data.drop(['date', 'time'], axis=1, inplace=True)
data
これでMT5から取得したHistoricalDataの読み込みは終わりました。次にデータ加工です。
データ加工術その1
# extract features from date
all_data['day'] = [i.day for i in all_data['datetime']]
all_data['month'] = [i.month for i in all_data['datetime']]
all_data['year'] = [i.year for i in all_data['datetime']]
all_data['day_of_week'] = [i.dayofweek for i in all_data['datetime']]
all_data['day_of_year'] = [i.dayofyear for i in all_data['datetime']]
all_data['hour'] = [i.hour for i in all_data['datetime']]
all_data['minute'] = [i.minute for i in all_data['datetime']]
all_data
日付になっている項目から、年月日や曜日などそれぞれの特徴量に分けます。こうすることで毎週x曜日や、毎日x時ぐらいという特徴量を作り出すことができます
データ化後述その2
dataset2 = dataset.copy()
for i in range(1, 13):
dataset2['shift%s'%i] = dataset2['high'].shift(i)
dataset2['sma5'] = dataset2['high'].rolling(5).mean()
dataset2['sma15'] = dataset2['high'].rolling(15).mean()
この辺は見る方が見れば分かると思いますが、SMA(Simple Moving Average)と言うテクニカル指標を使っていくつかの特徴量を作っています(単に過去数日間の平均値を計算しているだけです)
予測したい次の15分の高値をy(目的変数)に設定する
dataset['y'] = dataset['high'].shift(-1)
原理は、上から下に時系列にデータが並んでいますので、上のshift関数を使って、1つ下のhigh(高値)を取得して、自分の目的変数とします。これでこの1行には、目的変数(次の15分後の高値)が入った状態で、説明変数(取得時の情報)を使うというデータを作り出せたわけです。
予測モデルの作成
import lightgbm as lgb
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold, train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, shuffle=False)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2, shuffle=False)
lgb_train = lgb.Dataset(X_train, y_train)
lgb_eval = lgb.Dataset(X_val, y_val, reference=lgb_train)
# LightGBM parameters
params = {
'task' : 'train',
'boosting':'gbdt',
'objective' : 'regression',
'metric' : {'rmse'},
'num_leaves':78,
'drop_rate':0.05,
'learning_rate':0.005,
'seed':0,
'verbose':0,
'device': 'cpu',
'max_depth': -1,
'random_state': 0
}
evaluation_results = {}
gbm = lgb.train(params,
lgb_train,
num_boost_round=100000,
valid_sets=[lgb_train, lgb_eval],
valid_names=['Train', 'Valid'],
evals_result=evaluation_results,
early_stopping_rounds=1000,
verbose_eval=100)
y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)
y_ = np.concatenate([np.array([None for i in range(len(y_train)+len(y_val))]) , y_pred])
y_ = pd.DataFrame(y_, index=X.index)
plt.figure(figsize=(16,5))
plt.plot(y, label='original')
plt.plot(y_, '--', label='predict')
plt.legend()
そしてこれが、心臓部、LightGBMでRegression予測した結果です。実際の値と予測した結果を重ね合わせるとこんな感じになりました。
オレンジが予測した結果ですが、これでは少し分かりづらいですね
これは直近10本の結果になります。青がoriginalなので実際のHighの値で、オレンジがpredictなので予測した結果です。多少乖離はあるものの今回はこれは許容します
今回はこの設定の予測モデルを採用して、次に進みます。
ちょっとペースが早く感じられている方もいるかと思いますが、まだ1/3なのでこのあともこのペースで行きますwwソースコードがほしい方は有料コンテンツで公開予定ですので、そちらでじっくり学習いただければと思います。
HEROKUのためのPythonとFlask
ここでは、主にPythonを使った説明をしていきます。HEROKUのAddonや、どうやってPostgreSQLに初期データを突っ込むなどは触れません
さてPythonを書いてみる
まず、Heroku上で、PostgreSQLをAddonで追加しておきます。
今回の予測結果を応答するPython側の実装としては、
1,インタフェースはAPI方式を採用し、MTからTICK情報をPOSTで送信してもらう
2,そのデータはPostgreSQLのレコードに追加
3,そのPostgreSQLのデータを使って先程のMachine Learingの予測モデル用に前処理を行い、予測を行う。その結果を応答する
では、1つずつ必要なコードを書いていきます。の前に、ディレクトリ構成は参考までにこんな感じです。
いくつか不要なものもありますが、だいたいこんな感じです。requirements.txt, Procfile, runtime.txtはHerokuに上げる都合上必要なもので、普段のPython+Herokuの実装では不要です。
予測モデルについては、「model.pkl」ということで、予測したモデルをPythonのPickleを利用してモジュール化しています
実際に動かすrun.py
import os
from app import app
if __name__ == '__main__':
port = int(os.getenv("PORT", 5000))
app.run(host="0.0.0.0", port=port)
はい、シンプルです。Herokuではこいつを呼び出すように定義してください。
次は実装の中核app.py
from flask import Flask
from database import init_db
from v1.views import v1 as v1_app
from v1_1.views import v1_1 as v1_1_app
def create_app():
app = Flask(__name__)
app.secret_key = 'secret'
app.config.from_object('config.Config')
app.register_blueprint(v1_app)
app.register_blueprint(v1_1_app)
init_db(app)
return app
app = create_app()
こいつは記載行数は少ないものの結構重要な機能を担っています。APIをバージョン管理できるようにblueprintを採用し、SQLAlchemyなどのための初期設定もこいつから呼び出しています。
そして本丸、views.py
import os
import pandas as pd
import numpy as np
import pickle
from flask import Flask, request, session, Blueprint, current_app
from sqlalchemy import and_, asc, desc
from models.models import TICK
v1 = Blueprint('v1', __name__, url_prefix='/')
@v1.route('/')
def index():
return 'v1'
def ValuePredictor_v1():
tick_data = TICK.query.order_by(desc(TICK.id)).limit(200)
# print(type(tick_data))
data = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close'])
for tick_line in tick_data:
data = data.append({'time': tick_line.time, 'open': tick_line.open, 'high': tick_line.high, 'low': tick_line.low, 'close': tick_line.close}, ignore_index=True)
data = data.sort_values('time', ascending=True)
# print(data)
data['open'] = data['open'].astype('float64')
data['high'] = data['high'].astype('float64')
data['low'] = data['low'].astype('float64')
data['close'] = data['close'].astype('float64')
data['time'] = pd.to_datetime(data['time'], unit='s')
# extract features from date
data['day'] = [i.day for i in data['time']]
data['month'] = [i.month for i in data['time']]
data['year'] = [i.year for i in data['time']]
data['day_of_week'] = [i.dayofweek for i in data['time']]
data['day_of_year'] = [i.dayofyear for i in data['time']]
data['hour'] = [i.hour for i in data['time']]
data['minute'] = [i.minute for i in data['time']]
# for Buying trade
dataset = data[['open', 'high', 'low', 'close', 'day', 'month', 'year', 'day_of_week', 'day_of_year', 'hour', 'minute']].copy()
dataset['y'] = data['high'].shift(-1)
for i in range(1, 13):
dataset['shift%s'%i] = data['high'].shift(i)
dataset['sma5'] = data['high'].rolling(5).mean()
dataset['sma15'] = data['high'].rolling(15).mean()
dataset = dataset[100:-1]
to_predict = dataset[-1:].drop('y', axis=1)
gbm_buy = pickle.load(open('model.pkl','rb'))
result_buy = gbm_buy.predict(to_predict)
return str(result_buy[0]))
@v1.route('/predict',methods=['POST'])
def v1_predict():
if request.method == 'POST':
if current_app.config['W_UPDATE'] == 'True':
curr = []
curr = TICK.query.filter(TICK.time==request.json['time']).all()
if(len(curr) == 0):
items_idx = list()
item = dict()
item['currency'] = request.json['curr']
item['period'] = request.json['perd']
item['time'] = request.json['time']
item['open'] = request.json['open']
item['high'] = request.json['high']
item['low'] = request.json['low']
item['close'] = request.json['close']
items_idx.append(item)
db.session.execute(TICK.__table__.insert(), items_idx)
db.session.commit()
result_predict = ValuePredictor_v1()
return result_predict
こいつが一番の肝です。URL上、http://localhost:5000/と呼び出されると"v1"という文字列を返します。実際に予測で使う場合は、http://localhost:5000/predictをPOSTで呼び出す処理と、その後、前処理を行って予測するValuPredictor_v1と言う2大構成です。この応答結果は、15分後のHigh(高値)が応答されます
ちなみに、省略していますがこのプログラムを動かす前に、30TICK程度のデータをPostgreSQLに予め登録しておく必要があります
ここまでかなり駆け足でやってきました。この記事を読んでいただいているスキルは様々なのは理解しておりますので、都度、関連記事を増やしていければと思います。モチベーションを保つため、応援のほどよろしくお願いします。
いよいよ来ました、最終章、MetaTraderです。今回はそれぞれの実装の違いから完全に汎用化することはできませんでしたが、9割同じで、決済時の関数呼び出しのみ切り替えることで、MT4/MT5で利用できるようにしています。
相変わらず、MTに関する、EAの意味や使い方などは省略しサンプルコードを記載していきます。
EA内の構成としては単純で、先程実装したPythonをHerokuに上げ、それをMTから呼び出して、条件に従い決済を入れるという作りです。
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//---
int currentBars = iBars(NULL,0);
int trade_flg = 0;
// 新しい足を生成した時ではない場合は、スキップ
if(currentBars == gPrvBars){
gPrvBars = currentBars;
return;
}
trade = GetWebData();
int ticket_id = 0;
if ※なんかの条件を入れる※:
//--- リクエストの送信
ticket_id = OrderSend(_Symbol, OP_BUY, Lot, Ask, 20, NormalizeDouble(Ask - STP*_Point,_Digits), NormalizeDouble(Ask + TKP*_Point,_Digits), NULL, EA_Magic, 0, clrRed);
if(ticket_id == -1){
int errorcode = GetLastError();
printf("エラーコード:%d , 詳細:%s ",errorcode , ErrorDescription(errorcode));
}
printf("ask: %f, stp: %d, point: %f, digit: %d", Ask, STP, _Point, _Digits);
printf("sl: %f, tp: %f", NormalizeDouble(Ask - STP*_Point,_Digits), NormalizeDouble(Ask + TKP*_Point,_Digits));
}
gPrvBars = currentBars;
}
これがOnTick内で呼ばれる内容となります。※なんかの条件を入れる※は適当に考えてください。こちらはMT4で買いを入れるコードです
//--- リクエストと結果の宣言と初期化
MqlTick latest_price;
MqlTradeRequest request={0};
MqlTradeResult result={0};
//--- Get the last price quote using the MQL5 MqlTick Structure
if(!SymbolInfoTick(_Symbol,latest_price))
{
Alert("Error getting the latest price quote - error:",GetLastError(),"!!");
return;
}
if ※なんかの条件を入れる※:
//--- リクエストのパラメータ
// price = SymbolInfoDouble(Symbol(),SYMBOL_ASK)*point;
request.action = TRADE_ACTION_DEAL; // 取引操作タイプ
request.symbol = _Symbol; // シンボル
request.volume = Lot; // 0.1ロットのボリューム
request.type = ORDER_TYPE_BUY; // 注文タイプ
request.price = NormalizeDouble(latest_price.ask, _Digits); // 発注価格
request.magic = EA_Magic; // 注文のMagicNumber
request.sl = NormalizeDouble(latest_price.ask - STP*_Point,_Digits);
request.tp = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits);
request.type_filling = ORDER_FILLING_FOK;
request.deviation = 100;
//--- リクエストの送信
if(!OrderSend(request,result))
PrintFormat("OrderSend error %d",GetLastError()); // リクエストの送信が失敗した場合、エラーコードを出力する
//--- 操作に関する情報
PrintFormat("retcode=%u deal=%I64u order=%I64u",result.retcode,result.deal,result.order);
}
これがMT5の場合の買いを入れるロジックになります。
int GetWebData()
{
CJAVal jv;
int WebR;
int timeout = 5000;
string cookie = NULL, headers;
char data[], ReceivedData[];
int shift = 1;
int time = iTime(Symbol(), Period(), shift);
double open = iOpen(Symbol(), Period(), shift);
double high = iHigh(Symbol(), Period(), shift);
double low = iLow(Symbol(), Period(), shift);
double close = iClose(Symbol(), Period(), shift);
jv["curr"]=Symbol();
jv["perd"]="15";
jv["time"]=time;
jv["open"]=open;
jv["high"]=high;
jv["low"]=low;
jv["close"]=close;
ArrayResize(data, StringToCharArray(jv.Serialize(), data, 0, WHOLE_ARRAY)-1);
WebR = WebRequest("POST", PREDICT_URL, "Content-Type: application/json\r\n", timeout, data, ReceivedData, headers );
if(!WebR) Print("Web request failed");
string ReceivedText = CharArrayToString(ReceivedData);
Print(ReceivedText);
Print("Updated!");
return ReceivedText;
}
そしてこれが、実際にHerokuサーバにアクセスして、予測結果を取得する処理になります。
これで全てになります。一通りこれを実装できれば、OANDA APIがなくてもMTと、Machine Learningを利用してトレードを行うことができます。
完全ソースコードを載せた有料版は後日出しますので、乞うご期待ください