FXなどの金融商品の取引プラットフォームMetaTrader5(MT5)では、システムトレードをMQL5という開発言語で実装(EA)するのですが、最近はPythonのAPIが正式に提供されています。
これでPythonからMT5を操作し、取引を自動化したり、時系列データを効率よく収集できるようになりました。
今回は MT5のPython APIを使ってレートやチャート(OHLC)を取得し、注文・クローズまでの一連の流れを、コードをみながら確認していきましょう。
検証環境
Windows 10
MetaTrader5 5.00
Python 3.9
MetaTrader5(Pythonモジュール) 5.0.34
numpy 1.20.2
事前準備
Python API モジュール
MT5(MetaTrader5)をPythonから操作するためにモジュールをインストールします。
pip なら以下のコマンドを実行します。
pip install MetaTrader5
基本的にMetaTrader5はWindows向けソフトなので、PythonモジュールもWindows環境でしかパッケージが提供されていません。
コードではMetaTrader5 をimportします。
import MetaTrader5 as mt5
検証用デモ口座
動作確認やテスト用に、デモ口座の情報も用意しておきましょう。
記事ではMetaQuotes-Demo口座を使っていますが、MT5に対応してさえいれば、どのブローカーのデモ口座でもOKです。
MT5のオプション設定
ツール → オプション からEAの動作許可をしておかないと、Pythonからも操作できません。
「アルゴリズム取引を許可」にチェックを入れておきます。
また「外部Python APIを介したアルゴリズム取引を無効にする」のチェックが外れているのを確認しましょう。
初期化と接続・認証
まずはMT5と接続し、MT5をPythonから操作できる状態にしてあげます。
# MT5と接続
if not mt5.initialize():
print(f"initialize() failed, error code = {mt5.last_error()}")
return
# 接続ができたらMT5のバージョンを表示する
print(f"MetaTrader5 package version {mt5.__version__}")
# MT5でブローカーにログイン
authorized = mt5.login(
12345678,
password="password",
server="Demo",
)
if not authorized:
print(f"User Authorization Failed")
return
MT5は、事前に起動させていてもいなくても、
どちらでも大丈夫です。
initialize
MT5と接続するのは mt5.initialize() 関数。
取引やレート参照など、いずれかの操作をする前にコードのどこかで呼べばOKです。
login
口座残高を参照したりはもちろん、取引を実行する場合にもログインが必要です。
口座の認証を行うのは mt5.login() 関数です。
渡すパラメータはMT5のログイン画面とまったく同じです。
mt5.login(
12345678,
password="password",
server="Demo",
)
- 第一引数に「Login」(数字のID)
※int型じゃないと通らない - password引数に取引Password
- server引数にServer名
を渡します。
口座情報の参照
残高を取得するときなどに使います。
# 口座情報を取得する
account_info = mt5.account_info()
if account_info is None:
print(f"Retreiving account information failed")
return
print(f"Balance: {account_info.balance}")
mt5.account_info()で口座情報をごっそり取れます。
戻り値のAccountInfoオブジェクトにいろいろなデータが格納されています。
例えば、残高は account_info().balance メンバです。
その他のメンバはDocを参照してください。
時系列(OHLC)の取得
# シンボルの情報を取得する
symbol_info = mt5.symbol_info(SYMBOL)
if symbol_info is None:
print("Symbol not found")
return
# 時系列データを取得する
df_rates = get_rates(SYMBOL, frame=mt5.TIMEFRAME_H1, count=100)
last_close_price = df_rates['Close'][-1]
print(f"Close: {last_close_price}")
def get_rates(symbol, frame, count):
""" 一定の期間の時系列データを取得
"""
rates = mt5.copy_rates_from_pos(symbol, frame, 0, count)
df_rates = pd.DataFrame(rates)
df_rates['time'] = pd.to_datetime(df_rates['time'], unit='s')
df_rates = df_rates.set_index('time')
df_rates.columns = [
'Open', 'High', 'Low', 'Close', 'tick_volume', 'spread', 'real_volume',
]
return df_rates
時系列データを取る方法は複数あります。
上記コードは copy_rates_from_pos() で最新時点(直近のバー)から過去100バー分のOHLCデータを取得するものです。
copy_rates_from_pos
おそらく一番使い勝手が良さそうなのが copy_rates_from_pos() 関数。
mt5.copy_rates_from_pos(
symbol='USDJPY', # 銘柄
frame=mt5.TIMEFRAME_H1, # 時間軸
start=0, # 開始バーの位置。0は現在を表す
count=100, # 取得するバーの数
)
frameはmt5モジュールで定義されている定数で指定します。
mt5.TIMEFRAME_M1 | 1分足 |
mt5.TIMEFRAME_M5 | 5分足 |
mt5.TIMEFRAME_M15 | 15分足 |
mt5.TIMEFRAME_H1 | 1時間足 |
… | … |
注意すべきなのは、startは新しい時点から始まって、過去へさかのぼってカウントしていくというところ。
1時間足なら、
start=0 は現在の足
start=1 は1時間前の足
start=2 は2時間前の足
…
を指します。
戻り値はnumpy形式なので、そのままでも使えますが、適当にDataFrameなど扱いやすい形式に変換してもいいでしょう。
上のコードでは取得したデータ(rates変数)をDataFrameに変換し、インデックスを付けた上で、カラム名を変更しています。
def get_rates(symbol, frame, count):
""" 一定の期間の時系列データを取得
"""
rates = mt5.copy_rates_from_pos(symbol, frame, 0, count)
df_rates = pd.DataFrame(rates)
df_rates['time'] = pd.to_datetime(df_rates['time'], unit='s')
df_rates = df_rates.set_index('time')
df_rates.columns = [
'Open', 'High', 'Low', 'Close', 'tick_volume', 'spread', 'real_volume',
]
timeはUNIXTIME(秒単位)なので、pd.to_datetime()により、DataFrameのDateTime型に変換しています。
また、列名はそのままでも何も問題ないのですが、後でTA-Libで分析するとき楽にするため少し変更を加えています。
他に、時系列データを取得する関数にはcopy_rates_from、copy_rates_rangeがあります。
これらは過去の一定期間の範囲のOHLCデータを取得できる関数です。
詳しくはそれぞれのDocを読んでみてください。
長期間の時系列データ取得
Python APIを経由してMT5から過去の時系列データ(ヒストリカルデータ)を取得できることを解説しました。
しかし1年~数年以上前のヒストリカルは取得できません(ブローカーによります)。
ブローカーによっては、特典として過去のTickデータを無料で配布している場合があります。OANDAでは5年くらい前からのTickデータを無料でダウンロードできます。ありがたいです…。
もし深層学習などで学習データを用意するなら、さらに前(5年~30年)のヒストリカルデータが必要かもしれません。
そんなときはForex Testerでデータ取得するのもオススメです。本来はシステムトレードのバックテスターですが、各種通貨ペア・株価指数・個別ティッカーの過去数十年の分足データなど、豊富なデータセットが用意されています。ちょっとした出費になりますが、リアルタイムでシミュレーションできるソフトの中では定番の商品となっています。
注文
# 注文を出す
point = symbol_info.point
result = post_market_order(
SYMBOL,
type=mt5.ORDER_TYPE_BUY,
vol=0.1,
price=mt5.symbol_info_tick(SYMBOL).ask,
dev=20,
sl=mt5.symbol_info_tick(SYMBOL).ask - point * 100,
tp=mt5.symbol_info_tick(SYMBOL).ask + point * 100,
)
# 決済する
position = result.order
result = post_market_order(
SYMBOL,
type=mt5.ORDER_TYPE_SELL,
vol=0.1,
price=mt5.symbol_info_tick(SYMBOL).bid,
dev=20,
position=position,
)
def post_market_order(symbol, type, vol, price, dev, sl=None, tp=None, position=None):
""" 注文を送信
"""
request = {
'action': mt5.TRADE_ACTION_DEAL,
'symbol': symbol,
'volume': vol,
'price': price,
'deviation': dev, # float型じゃだめ
'magic': 234000,
'comment': "python script open", # 何でもOK
'type_time': mt5.ORDER_TIME_GTC,
'type': type,
'type_filling': mt5.ORDER_FILLING_IOC, # ブローカーにより異なる
}
if sl is not None:
request.update({"sl": sl,})
if tp is not None:
request.update({"tp": tp,})
if position is not None:
request.update({"position": position})
result = mt5.order_send(request)
return result
order_send
新規注文を送信するにはmt5.order_send()関数を使います。
引数requestには、注文に必要な情報を入力した辞書(dict)を渡してあげます。基本的にドキュメントと同じように書けば正常に注文が完了します。
ただしorder_send()は、少しパラメータが間違っているだけでNoneが返ってくるなど、優しくない仕様なのでハマるときにはハマります。
特に上のサンプルではエラーハンドリングをまったくしていませんが、実践的な場面ではよーく検証して例外処理したほうがよいでしょう。
以下では、少し分かりにくかったフィールドを詳しく見ていきます。
action
取引操作の種類。値はTRADE_REQUEST_ACTIONS列挙体のうちの1つです。
注文に応じて変えます。
成行注文 | mt5. TRADE_ACTION_DEAL |
指値/逆指値注文 | mt5. TRADE_ACTION_PENDING |
SL・TPの変更 | mt5. TRADE_ACTION_SLTP |
指値注文の取消 | mt5. TRADE_ACTION_REMOVE |
詳しくはDoc
type
注文の種類。値はORDER_TYPE列挙体のうちの1つです。
こちらも注文に応じて変えます。actionパラメータとセットで変更するイメージです。
mt5.ORDER_TYPE_BUY | 成行買い注文 |
mt5.ORDER_TYPE_SELL | 成行売り注文 |
mt5.ORDER_TYPE_BUY_LIMIT | 買い指値注文 |
mt5.ORDER_TYPE_SELL_LIMIT | 売り指値注文 |
mt5.ORDER_TYPE_BUY_STOP | 買い逆指値注文 |
mt5.ORDER_TYPE_SELL_STOP | 売り逆指値注文 |
詳しくはDoc
type_filling
注文の種類。値はORDER_TYPE_FILLING値のうちの1つです。
ブローカー、または銘柄(通貨)によって対応しているパラメータが異なります。
ドキュメントのサンプルではORDER_FILLING_RETURNが指定されていますが、使っているブローカーに合わせて変更してください。
OANDAのUSDJPYではORDER_FILLING_IOCしか受け付けられませんでした。
mt5.ORDER_FILLING_FOK | 注文数量の全部が約定しない状況では、注文数量の全部をキャンセルする |
mt5.ORDER_FILLING_IOC | 注文数量の全部が約定しない状況では、約定できる分の数量だけ部分的に約定させ、残りはキャンセルする |
ORDER_FILLING_RETURN | 注文数量の全部が約定しない状況では、約定できる分の数量だけ部分的に約定させ、残りはリミット注文として残す |
詳しくはDoc
position
ポジションチケット。明確に識別するために、ポジションを変更および決済するときに入力します。通常は、ポジションを開いた注文のチケットと同じです。
クローズ(決済)や注文変更で指定することになります。
注文時のorder_send()の戻り値から以下のようにポジションチケット(int)が取得できます。
result = order_send(requests)
position = result.order
取得したポジションチケットを、決済注文や注文変更時にpositionフィールドに指定します。
requests = {
position: ポジションチケット
}
mt5.order_send(requests)
order_sendの結果・エラー処理
mt5.order_send()はOrderSendResultオブジェクトを返しますが、その中に注文の処理結果が格納されています。
例えば、注文がエラーとなった場合に、以下のようにエラーコードを確認できます。※例示のため一部コードのみです。
code = result.retcode
if code == 10009:
print("注文完了")
elif code == 10013:
print("無効なリクエスト")
elif code == 10018:
print("マーケットが休止中")
リターンコードの一覧はDocを参照してください。
リターンコード以外のフィールドは以下のようなものがあります(一部)
retcode | リターンコード |
deal | 約定チケット |
order | 注文チケット |
volume | (約定したら)約定ボリューム |
price | (約定したら) 約定価格 |
bid | 現在のマーケットBID |
ask | 現在のマーケットASK |
comment | 注文結果のコメント |
ポジションの確認
ticket = result.order
# ポジションを確認する
position = mt5.positions_get(ticket=ticket)
if position is None:
print("ポジションが存在しない")
return
pos = position[0]
print(f"Open: {pos.price_open}")
print(f"Current: {pos.price_current}")
print(f"Swap: {pos.swap}")
print(f"Profit: {pos.profit}")
mt5.positions_get() で現在のポジションの一覧とそれぞれの状態を取得できます。
positions_get()のパラメータにはシンボル(銘柄)かポジションチケットを指定し、見つかったポジションはタプルとして返ってきます。
返ってきたポジション情報の一覧は DataFrame などに変換すると扱いやすいでしょう。
DataFrameへの変換は公式APIドキュメントにサンプルがあります。
さいごに
記事ではMetaTrader5(MT5)をPythonで操作し、現在値取得から取引注文・決済まで一通りの動作を確認しました。
API自体はとてもシンプルで理解しやすいですが(所詮MQLのラッパーでしかないためか)、Pythonの世界では例外処理などが非常にやりにくいものになっています。
なのでシステムトレード(自動運用)の実運用では、素直にMQLで書いたほうがいいかもしれませんね…。
とはいえ、データ処理や分析面では、Pythonの既存資産を活用しやすいです。うまくすみ分けて活用していきたいですね。
今回の記事で使用したサンプルコードの全体を載せておきます。
import pandas as pd
import MetaTrader5 as mt5
def main():
# シンボル(銘柄)
SYMBOL = 'USDJPY'
# MT5と接続
if not mt5.initialize():
print(f"initialize() failed, error code = {mt5.last_error()}")
return
# 接続ができたらMT5のバージョンを表示する
print(f"MetaTrader5 package version {mt5.__version__}")
# MT5でブローカーにログイン
authorized = mt5.login(
12345678, # int 型
password="password",
server="Demo",
)
if not authorized:
print(f"User Authorization Failed")
return
# 口座情報を取得する
account_info = mt5.account_info()
if account_info is None:
print(f"Retreiving account information failed")
return
print(f"Balance: {account_info.balance}")
# シンボルの情報を取得する
symbol_info = mt5.symbol_info(SYMBOL)
if symbol_info is None:
print("Symbol not found")
return
# 時系列データを取得する
df_rates = get_rates(SYMBOL, frame=mt5.TIMEFRAME_H1, count=100)
last_close_price = df_rates['Close'][-1]
print(f"Close: {last_close_price}")
# 注文を出す
point = symbol_info.point
result = post_market_order(
SYMBOL,
type=mt5.ORDER_TYPE_BUY,
vol=0.1,
price=mt5.symbol_info_tick(SYMBOL).ask,
dev=20,
sl=mt5.symbol_info_tick(SYMBOL).ask - point * 100,
tp=mt5.symbol_info_tick(SYMBOL).ask + point * 100,
)
ticket = result.order
# ポジションを確認する
position = mt5.positions_get(ticket=ticket)
if position is None:
print("ポジションが存在しない")
return
pos = position[0]
print(f"Open: {pos.price_open}")
print(f"Current: {pos.price_current}")
print(f"Swap: {pos.swap}")
print(f"Profit: {pos.profit}")
# 決済する
ticket = result.order
result = post_market_order(
SYMBOL,
type=mt5.ORDER_TYPE_SELL,
vol=0.1,
price=mt5.symbol_info_tick(SYMBOL).bid,
dev=20,
position=ticket,
)
code = result.retcode
if code == 10009:
print("注文完了")
elif code == 10013:
print("無効なリクエスト")
elif code == 10018:
print("マーケットが休止中")
def get_rates(symbol, frame, count):
""" 一定の期間の時系列データを取得
"""
rates = mt5.copy_rates_from_pos(symbol, frame, 0, count)
df_rates = pd.DataFrame(rates)
df_rates['time'] = pd.to_datetime(df_rates['time'], unit='s')
df_rates = df_rates.set_index('time')
df_rates.columns = [
'Open', 'High', 'Low', 'Close', 'tick_volume', 'spread', 'real_volume',
]
return df_rates
def post_market_order(symbol, type, vol, price, dev, sl=None, tp=None, position=None):
""" 注文を送信
"""
request = {
'action': mt5.TRADE_ACTION_DEAL,
'symbol': symbol,
'volume': vol,
'price': price,
'deviation': dev, # float型じゃだめ
'magic': 234000,
'comment': "python script open", # 何でもOK
'type_time': mt5.ORDER_TIME_GTC,
'type': type,
'type_filling': mt5.ORDER_FILLING_IOC, # ブローカーにより異なる
}
if sl is not None:
request.update({"sl": sl,})
if tp is not None:
request.update({"tp": tp,})
if position is not None:
request.update({"position": position})
result = mt5.order_send(request)
return result
if __name__ == '__main__':
main()