[第3回] Pythonを使ってMT5からトレード履歴をExcelファイルに出力し、トレード記録作業を簡略化してみた

# 本シリーズの目次
1. [第1回: 前提知識などの解説](https://qiita.com/gappac/items/969760de17ed20446148)
2. [第2回: コード前半部分の解説](https://qiita.com/gappac/items/a48176e2114164ac090d)
3. 第3回: コード後半部分の解説 ←本記事

# はじめに
本記事は前回からの続きとなっております。前回の記事はこちら

https://qiita.com/gappac/items/a48176e2114164ac090d

前回記事ではプログラムの前半部分を解説しました。本記事ではプログラムの後半部分を紹介します。

コード全文とトレードノートのテンプレファイルはgithubでダウンロードできます。

https://github.com/gappac/export_trade_history_with_python


# プログラム内容
それではコードの解説。いよいよ `main` 関数の定義に入ります。

## main関数その1
```Python
def main():
    # MT5への接続を初期化
    if not mt5.initialize():
        print("MT5の接続に失敗しました")
        mt5.shutdown()
        exit()
```
まずはMT5と接続します。プログラム実行の際は、MT5を起動させ、口座にログインしている状態にしてください。MT5を閉じた状態で実行すると、モジュールが自分自身でMT5の実行ファイルを見つけようとするらしく、待ち時間が発生します。もしMT5を起動していない状態でプログラムを実行したい場合は

```Python
mt5.initialize(
    path                    # MetaTrader 5ターミナルのEXEファイルへのパス
  )
```
あるいは
```Python
mt5.initialize(
  path                      # MetaTrader 5ターミナルのEXEファイルへのパス file
  timeout=TIMEOUT,          # timeout
  login=LOGIN,              # 口座番号
  password="PASSWORD",      # パスワード
  server="SERVER"           # ターミナルで指定されているサーバ名
  )
```
のようにしてください。

## main関数その2
```Python
    while(True):
        # エクセルのワークブック、ワークシート、テーブルを取得
        try:
            wb = openpyxl.load_workbook(FILE_PATH)
        except FileNotFoundError:
            print("ファイルパスが不適切です")
            break
        try:
            ws = wb[SHEET_NAME]
        except KeyError:
            print("ワークシート名が不適切です")
            break
        try:
            table = ws.tables[TABLE_NAME]
        except KeyError:
            print("テーブル名が不適切です")
            break
```
ここから長い `While` 文に突入します。`While` 文の外に出たらMT5との接続を切断し、プログラムが終了します。つまり、`While` 文を `break` するとそこで終了です。

上記のコードでは定数として宣言したファイルパス・ワークシート名・テーブル名が適切であるかをチェックしています。不適切であれば `break` して終了しています。


## main関数その3
```Python
        # input
        from_date_input = input("Enter from_date(e.g. 2024/1/14T14:30:00 or 2024/1/14): ")
        from_date = return_shifted_unix_time_from_string(from_date_input)
        if(from_date == None):
            print("入力がフォーマットに一致しません")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break
        to_date_input = input("Enter to_date(e.g. 2024/1/14T22:20:00 or 2024/1/14): ")
        to_date = return_shifted_unix_time_from_string(to_date_input)
        if(to_date == None):
            print("入力がフォーマットに一致しません")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break
            
        # 取得期間のエラー処理
        if from_date > to_date:
            print("Error: from_dateがto_dateよりも後になっています")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break

        # 時刻をTIME_SHIFTだけずらす
        from_date += TIME_SHIFT
        to_date += TIME_SHIFT
        
        symbol_input = input("Enter symbol(e.g. usdjpy): ")
        group_input = "*" + symbol_input.upper() + "*"
```
`from_date`、`to_date`、`symbol`の入力を受け取り、エラーチェックをしています。ここで`TIME_SHIFT` 分だけ時刻をずらしているので、入力する時刻はMT5の口座履歴の時刻にあわせます。

`symbol` は `usd` のように片方の通貨だけ指定することもできるし、実は指定しなくてもOK。

## main関数その4
```Python
        # 取引履歴を取得
        deal_history = mt5.history_deals_get(from_date, to_date, group=group_input)

        # 取引履歴が取得できているかチェック
        if deal_history is None:
            print("履歴の取得に失敗しました")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break
        elif len(deal_history) == 0:
            print("該当するデータがありませんでした")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break
```
取引履歴を取得します。うまく取得できなかった場合は最初に戻ることができます。

## main関数その5
```Python
        # テーブルの最終行の1行下の行に出力するようにする
        row_writing, _ = coordinate_to_tuple(table.ref.split(":")[1])
        row_writing += 1

        # テーブル範囲を変更
        expand_table_range_by_one_row(table)
        # 行の高さを変更
        ws.row_dimensions[row_writing].height = ROW_HEIGHT
```
テーブル範囲を取得して、テーブルの最終行の1行下にデータを書くように設定します。つまり、テーブル最終行が丸々1行空白だったとしても、その空白行にデータは出力されないので注意。

さらにテーブル範囲と行の高さを変更しています。


## main関数その6
```Python
        trade_date = ""
        trade_symbol = ""
        trade_type = 0
        trade_entry_price  = 0
        trade_stoploss_price = 0
        trade_close_price = 0
        trade_entry_volume = 0
        trade_close_volume = 0
        trade_profit = 0
        trade_commission = 0
        trade_swap = 0
        trade_total_profit = 0
        trade_riskreward = 0

        # トレード履歴をワークシートに追加
        for idx, deal in enumerate(deal_history):
            # 1個目の取引からトレード日、通貨ペア、エントリー方向を決定する
            if idx == 0:
                # TIME_SHIFTの分だけ時間をずらす
                tmp_date = datetime.fromtimestamp(deal.time - TIME_SHIFT)
                trade_date = tmp_date.strftime("%Y/%m/%d")
                trade_day_of_the_week = change_weekday_to_japanese(tmp_date.weekday())
                trade_symbol = deal.symbol
                trade_type = deal.type

            # 通貨ペアが異なる場合
            if deal.symbol != trade_symbol:
                print("Error: 複数の通貨ペアのトレード履歴が含まれます")
                if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                    continue
                else:
                    break
```
トレード結果のうち、出力したい値を宣言しておきます。そして、先ほど取得した取引履歴の中身を `deal` として1つずつ取り出していきます。取引履歴の中で最も古いものから、トレード日・通貨ペア・エントリー方向を決定します。

またすべての `deal` で通貨ペアのチェックを行い、もし通貨ペアが一致していなければやり直しとなります。

## main関数その7
```Python
            # エントリー/決済 共通
            trade_profit += deal.profit
            trade_commission += deal.commission + deal.fee
            trade_swap += deal.swap
            
            # エントリーの場合
            if (deal.entry == mt5.DEAL_ENTRY_IN and deal.type == trade_type):
                trade_entry_volume += deal.volume
                trade_entry_price += deal.price*deal.volume
                # 注文履歴からsl価格を取得
                order = mt5.history_orders_get(ticket=deal.order)
                trade_stoploss_price += order[0].sl*deal.volume
            # 決済の場合
            elif (deal.entry == mt5.DEAL_ENTRY_OUT and deal.type == 1-trade_type):
                trade_close_volume += deal.volume
                trade_close_price += deal.price*deal.volume

        # 追記
        #######################################################
        trade_entry_volume = round(trade_entry_volume,2)
        trade_close_volume = round(trade_close_volume,2)
        #######################################################
        
        if trade_entry_volume != trade_close_volume:
            print("Error: エントリーと決済のロット数が合致しません")
            if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
                continue
            else:
                break
```
`deal` をエントリーと決済に分類し、それぞれで処理を行います。
```Python:エントリーの判定
if (deal.entry == mt5.DEAL_ENTRY_IN and deal.type == trade_type):
```

```Python:決済の判定
elif (deal.entry == mt5.DEAL_ENTRY_OUT and deal.type == 1-trade_type):
```

`deal_type` （買い、売り）による判定も行っているので、例えばトレード中にリスクヘッジとして両建てをする場合でも、その分については無視することができます。

手数料を表す値が `commission` と `fee` で2種類あるのですが、違いがよくわからないので1つにまとめてます。すべての `deal` の処理が終了した後、エントリーと決済でロット数が一致しているかを確認します。

エントリーの場合は取引履歴 `deal` に対応する注文履歴 `order` を取得して、`order` からストップロス価格を取得しています。

```Python
order = mt5.history_orders_get(ticket=deal.order)
trade_stoploss_price += order[0].sl*deal.volume
```
`order` の中の最初の要素分しかストップロス価格を加算していませんが、1つの`deal` に対応する `order` は1つしか無いはずなのでおそらくこれで大丈夫だと思います。

※追記
ロット数の計算誤差のせいで、エントリーと決済のロット数が合致しないと判定されることがあるので、小数第２位で丸めることにしました。ロット数が小数第１位までの場合には対応できてないです。


## main関数その7
```Python   
        trade_type = change_type_to_string(trade_type)
        trade_entry_price = round_price(trade_entry_price/trade_entry_volume)
        trade_stoploss_price = round_price(trade_stoploss_price/trade_entry_volume)
        trade_close_price = round_price(trade_close_price/trade_entry_volume)
        trade_total_profit = trade_profit + trade_commission + trade_swap
        trade_riskreward = (trade_close_price - trade_entry_price)/(trade_entry_price - trade_stoploss_price)

        row = [
            trade_date,                   # 日付
            trade_day_of_the_week,        # 曜日
            trade_symbol,                 # シンボル
            trade_type,                   # エントリー方向
            trade_entry_price,            # エントリー価格
            trade_stoploss_price,         # ストップロス価格
            trade_close_price,            # 決済価格
            trade_entry_volume,           # ロット数
            trade_profit,                 # 利益
            trade_commission,             # 手数料
            trade_swap,                   # スワップ
            trade_total_profit,           # 総利益
            trade_riskreward              # リスクリワード
        ]
```
計算結果を成形し、出力データを`row` リストに入れます。 `row` 内の一番前の要素からテーブルの1列目、2列目...という風に書き込まれていくので、要素の順番は大事です。エントリー価格・ストップロス価格・決済価格は、ロット数を重みとした加重平均としています。


## main関数その8
```Python
        # データを出力
        for col_writing, value in enumerate(row, start=1):
            cell_writing = ws.cell(row=row_writing, column=col_writing)
            cell_above = ws.cell(row=row_writing-1, column=col_writing)
            copy_cell_style(cell_writing, cell_above)

            ws.cell(row=row_writing, column=col_writing, value=value)

        # Excelファイルを保存
        wb.save(FILE_PATH)
        print(f"トレード履歴が {FILE_PATH} に保存されました")
        if(ask_yes_no("トレード履歴の出力を続けますか？[y/n]: ")):
            continue
        else:
            break

    print("トレード履歴の出力を終了します")
    # MT5の終了
    mt5.shutdown()

if __name__ == "__main__":
    main()
```
データを出力し、ワークブックを保存します。データはデータ出力が終わったらMT5との接続を切断して終了。お疲れさまでした。


# 振り返り

## プログラムの改善アイデア
- 条件付き書式のコピーをできるようにしたい
- Excelファイルが無ければ作ってくれるようにしたい
- テーブルの項目名を照合して、「日付」なら日付データを入れる、みたいな感じにしたい

くそザコプログラミングなので改善アイデアはいくらでも出てくると思いますが、とりあえずはこれでいいのだ。

## 最後に
トレード記録を簡略化するためにPythonでプログラムを書いてみました。これをつくってから記録作業が結構楽になったので、よければみなさんも試してみてください。

コード全文とトレードノートのテンプレートはgithubからダウンロードできます。

https://github.com/gappac/export_trade_history_with_python

## 参考リンク
https://www.mql5.com/en/docs/python_metatrader5