PythonでMetaTrader5のデータを取得する

#はじめに
FXや株式、先物トレードのプラットフォームとして知られているMetaTrader5(MT5)には、Pythonから接続するためのモジュールが用意されています。

https://www.mql5.com/ja/docs/integration/python_metatrader5

関数リストやサンプルプログラムがあるので、何となくできることがわかるかと思います。

今回、MT5からFXヒストリカルデータを取得して簡単な分析をしてみたので、メモしておきます。

#動作環境
* Python 3.8.11
* OANDA MetaTrader5 build 2982

Python、MT5の導入については省略します。MT5は基本的にどこの業者でも同じはずですが、うまくいかない場合は、業者を変えるか、インストールし直してみてください。

必要となるモジュールMetaTrader5は、メジャーなものではないので、

```
pip install MetaTrader5
```
として、別途インストールしておいてください。

#MT5への接続
まず、正常にログインできるMT5を用意しておいてください。一番簡単な接続方法は、MetaTrader5モジュールの`initialize()`を実行することです。

```py
import MetaTrader5 as mt5
if mt5.initialize():
    print('Connected to MT5 Version', mt5.version())
else:
    print('initialize() failed, error code = ', mt5.last_error())
```

MT5が起動していればすぐに接続されます。起動していなければ最後に起動したMT5を自動的に起動して接続します。正常に接続できれば、`True`を返すので、MT5のバージョン情報が表示されます。

```
Connected to MT5 Version (500, 2982, '23 Jun 2021')
```

接続できない場合はエラーメッセージを表示します。

```
initialize() failed, error code =  (-10003, "IPC initialize failed, Pipe server didn't answer in 60 sec")
```

その場合、MT5の状態などを確認してみてください。

なお、起動するMT5を直接指定したい場合、以下のように、`initialize()`の引数にMT5の実行ファイルのフルパスを入れます。

```py
if mt5.initialize('C:\\Program Files\\OANDA MetaTrader 5\\terminal64.exe'):
```

#MT5のヒストリカルデータの取得
MT5のチャート上のデータを取得する関数は３つあります。

* `copy_rates_from()` - 開始時刻とバーの個数を指定 
* `copy_rates_from_pos()` - 開始するバーの位置とバーの個数を指定
* `copy_rates_range()` - 開始時刻と終了時刻を指定

データの範囲は日時で指定することが多いと思うので、ここでは、開始時刻と終了時刻を指定する`copy_rates_range()`を使ってみます。

```py
from datetime import datetime, timezone
symbol = "EURUSD"
tf = mt5.TIMEFRAME_H1
date_from = datetime(2020, 1, 1, tzinfo=timezone.utc)
date_to = datetime(2020, 12, 31, 23, 59, tzinfo=timezone.utc)
rates = mt5.copy_rates_range(symbol, tf, date_from, date_to)
rates
```
`copy_rates_range()`の引数は、シンボル、タイムフレーム、開始時刻、終了時刻です。

ここでは、シンボルを**EURUSD**、タイムフレームを**１時間足**とし、**2020年1月1日から2020年12月31日まで**のデータを取得します。

なお、`date_from`は、0時0分からなので、時刻の指定は省略していますが、`date_to`は、最も短いタイムフレームが１分足なので、**23:59**を指定しています。タイムゾーンは**utc**です。

すると、こんな感じでデータが取得できます。それぞれのバーの**開始時刻(time)**、**始値(open)**、**高値(high)**、**安値(low)**、**終値(close)**、**ティック数(tick_volume)**、**スプレッド(spread)**、**取引量(real_volume)**のデータです。

```
array([(1577923200, 1.12082, 1.12154, 1.12082, 1.12136,  321, 19, 0),
       (1577926800, 1.12139, 1.12215, 1.12135, 1.12187,  932,  5, 0),
       (1577930400, 1.12188, 1.1219 , 1.12155, 1.12182, 1626,  5, 0), ...,
       (1609448400, 1.22166, 1.22227, 1.2209 , 1.22096, 2469,  5, 0),
       (1609452000, 1.22097, 1.22192, 1.22095, 1.22161, 1725,  5, 0),
       (1609455600, 1.2216 , 1.22279, 1.22084, 1.22126, 2507,  5, 0)],
      dtype=[('time', '<i8'), ('open', '<f8'), ('high', '<f8'), ('low', '<f8'), ('close', '<f8'), ('tick_volume', '<u8'), ('spread', '<i4'), ('real_volume', '<u8')])
```

次にデータ処理しやすいように、pandasの`DataFrame`にしておきます。また、このままだと時刻がUNIX時刻でわかりにくいので、`datetime`に変換しておきます。

```py
import pandas as pd
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df
```

すると、こんなデータになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131169/d5294ad2-66d8-2b75-a27b-79538dbe585b.png)


#データ分析例

あとはご自由にデータを分析してください、ということですが、簡単な分析例を載せておきます。

その前に、データを取得してそのままデータ分析する場合、もうMT5とつながっている必要はないので、接続を切っても構いません。切断する関数は`shutdown()`です。

```py
mt5.shutdown()
```

では、データ分析例です。

いま、１時間足のデータがあるので、各時間で上がる日が多いとか、下がる日が多いとかの傾向があるのかを調べてみます。

各バーの始値で売買して終値で決済する場合、同じバーのデータだけ見ればいいので簡単です。ただ、２時間後に決済とか、３時間後に決済する場合はバーをずらさなくてはいけないので、各時間の始値から指定した時間後に決済したときの値動きの平均を計算する関数`AveDiff()`を定義しておきます。

```py
import numpy as np
def AveDiff(df, HoldHour):
    Bars = HoldHour-1
    Ave = np.zeros(24-Bars)
    for i in range(24-Bars):
       dfo = df[df['time'].dt.hour==i]
       dfc = df.iloc[dfo.index+Bars]
       dif = np.vstack(dfc['close'].values.T) - np.vstack(dfo['open'].values.T)
       Ave[i] = dif.mean()*10000
    return pd.Series(Ave)
```

この関数は、MT5で取得したデータ`df`と、何時間後に決済するかを表す`HoldHour`を引数とします。

同じバーの始値と終値で１時間の差があるので、始値と終値のバーのずれ`Bars`は`HoldHour-1`となります。ただし、終値のバーの時刻を見ているわけではなく、`Bars`だけバーをずらしているだけなので、正確に`HoldHour`だけずれているわけではありません。

そして、`i`を`0`から`24-Bars-1`まで変えて、`dfo`に`i`時のデータを、`dfc`に`i+Bars`時のデータを抽出し、終値から始値を引いた値を平均し、値をpipsとして返します。

結果は以下のようになりました。

* 1時間で決済する場合

```
AveDiff(df, 1)
0     1.006154
1     2.895385
2    -0.275385
3    -0.293462
4     1.401538
5     0.511154
6     0.151154
7     0.178846
8     0.680385
9    -1.212692
10    1.021923
11   -0.093077
12   -1.016923
13   -0.247308
14    0.250000
15    1.139231
16    0.460769
17   -1.473462
18   -0.782692
19   -0.562308
20    0.900769
21    0.069231
22    0.926538
23   -1.145769
```

* 2時間で決済する場合

```
AveDiff(df, 2)
0     3.998077
1     2.653846
2    -0.561923
3     1.105385
4     1.910385
5     0.663846
6     0.330000
7     0.856538
8    -0.531538
9    -0.203462
10    0.926154
11   -1.128077
12   -1.276538
13    0.001923
14    1.408462
15    1.605000
16   -1.017692
17   -2.193077
18   -1.346538
19    0.340000
20    0.952308
21    0.988077
22   -0.221923
```

* 3時間で決済する場合

```
AveDiff(df, 3)
0     3.756538
1     2.367308
2     0.836923
3     1.614231
4     2.063077
5     0.842692
6     1.007692
7    -0.355385
8     0.477692
9    -0.299231
10   -0.108846
11   -1.387692
12   -1.027308
13    1.160385
14    1.874231
15    0.126538
16   -1.737308
17   -2.756923
18   -0.444231
19    0.391538
20    1.871154
21   -0.160385
```

数時間の値動きも平均すれば大きくても数pips程度ですし、１年分のデータでサンプル数が少ないので、この結果からエッジは見つけにくいと思います。今回はデータ分析例ということで、ここまでにしておきます。

#おわりに

PythonからMT5のデータを取得する方法を紹介しました。MetaTrader5のモジュールには、ほかにMT5と接続した状態で実際にトレードを行う関数なども用意されています。

それを使えば、売買システムをPythonで記述することもできるようです。MT5では売買システムはEAを使うのが普通ですが、色々と選択肢が増えるのはいいことです。今後、MetaTrader5モジュールでできることが増えていくことを期待します。