Tech

MT5をPythonで操作してレート取得・注文する方法まとめ

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_M11分足
mt5.TIMEFRAME_M55分足
mt5.TIMEFRAME_M1515分足
mt5.TIMEFRAME_H11時間足

 

注意すべきなのは、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_fromcopy_rates_rangeがあります。
これらは過去の一定期間の範囲のOHLCデータを取得できる関数です。

詳しくはそれぞれのDocを読んでみてください。

copy_rates_from

copy_rates_range

 

注文

# 注文を出す
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()
  • この記事を書いた人

nextpenguin

システム開発・プログラミングのしごとやっています。甘味とコーヒーは生命線。日常での学びを記事にしています。

人気の記事

1

先日の記事では、初心者からフリーランスプログラマーになる難しさと、それでもなんとか稼げるようになるにはどうしたらいいか解説しました。 今回の記事では、稼げるフリーランサーになるために必要な要素を、もう ...

2

気休めだけの甘い言葉は書きません。 最近は多くの企業やサイトで、使い捨てられるプログラマーが欲しいがために、甘い言葉で初心者プログラマを誘い出しています。 この記事では、まずは「現実」をちゃんと理解し ...

3

Vuetifyの v-progress-circular コンポーネントは、数値データや処理状況を環状(円状)のデザインで教えてくれるUIデザインです。 ローディングのスピナー(処理中のマーク)として ...

4

※画像はずとまよの新曲とは一切関係ありません   ずっと真夜中でいいのに。(以下、ずとまよ)の新曲『勘ぐれい』のMVが12月1日に公開されましたね。 MV前に公開されていた原曲を聴き、神曲な ...

5

Vuexのstore(ストア)を使うと、各コンポーネント間で個別にデータのやり取りすることなく、データを一元的に管理できます。Vueでは欠かせない機能といえるでしょう。 state(変数、コンポーネン ...

-Tech

© 2021 スターレイヴ