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

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

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

https://qiita.com/gappac/items/969760de17ed20446148

本記事では実際にトレードノートへの記録作業を実行するプログラムを紹介します。コードが長いので本記事ではコードの前半部分を解説していきます。

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

https://github.com/gappac/export_trade_history_with_python

プログラムを動かすうえでいくつか前提があるので先にそれらを述べます。

### 注文形式の前提
1. 注文時にストップロス価格を設定している
1. 1回の注文でエントリー方向が反転するドテンをしていない（ `DEAL_ENTRY_INOUT` でない）

MT5ではワンクリックで売り買いできる機能がありますが、その機能を使ってエントリーをする場合はストップロス価格が正常に取得できません（ストップロス価格の取得が必要ないのであれば気にしなくてよいです）。

私はトレードをするときEAを使用していて、1回のエントリー注文でストップロス価格も設定しています。

ドテン買い、ドテン売りのトレード履歴は正常に取得できません。具体的に言うと、MT5の口座履歴から取引のエントリータイプを確認した時、`inout` と書かれている取引についてはダメです。ネッティング口座を利用している場合やドテンするEAを利用している場合は注意。

### トレードノート形式の前提
1. テーブル内に記録する
1. 条件付き書式などはあらかじめExcel側で設定しておく
1. 仮でもよいのでトレードデータがテーブルに存在している
1. 新規のExcelファイルを作成するのではなく、既存のExcelファイルを使用する

理由としては上から、

1. トレード結果を分析する上でフィルタリング機能が欲しかったから
1. 条件付き書式の範囲をいじるのが難しかったから（実力不足）
1. トレード履歴を入力するとき、入力セルの書式設定を1行上からコピーするから
1. 3つ目の前提より、新規で作成できないから

という感じです。プログラムがチャチい作りのために制約が多いですが、まあ使えるからこれでいいのだ。

## プログラムの動作
プログラムを実行すると `from_date`、`to_date`、`symbol` を入力するように言われます。
以下は入力例です。
```:input
Enter from_date(example 2024/1/14T14:30:00 or 2024/1/14): 2025/2/3
Enter to_date(example 2024/1/14T22:20:00 or 2024/1/14): 2025/2/4
Enter symbol(example usdjpy): gbpchf
```
上記の入力例の場合、2025/2/3の0時から2025/2/4の0時までに約定した取引のうち、通貨ペアがポンドフラン（GBPCHF）の取引を抽出して記録します。記録が完了したら
```
トレード履歴の出力を続けますか？[y/n]: 
```
と聞かれるので、y(yes)なら再び日付や通貨ペアの入力、n(no)ならプログラム終了となります。

なぜ記録のたびに毎回 `from_date`、`to_date`、`symbol` を入力しているかというと、私の場合分割エントリーを採用している都合上、複数のエントリー/決済をまとめて1回のトレード結果として記録したいのですが、どこからどこまでの取引を「1回のトレード」として記録するかあいまいだからです。（一括でしかエントリーをしないのであれば、いちいちこれらを入力せずにすべてのトレード履歴を一気にExcelファイルに出力することも可能だと思います）


## 実行結果
#### 実行前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4082076/29cc8453-3e3e-453d-b60c-a6104a2ab5b1.png)

#### 実行後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4082076/1271132e-50a2-49ea-bd7c-a76e1441ff0e.png)

プログラム実行によってA3からM3までのセルに入力することができました。
D列は条件付き書式を設定していますが、これはプログラムではなく、あらかじめExcel上で設定しています。

# プログラム内容
それではコードの解説。上から順に適当に分割して解説します。

## インポートと定数の宣言

```Python
import MetaTrader5 as mt5
import openpyxl
from openpyxl.utils import coordinate_to_tuple
from openpyxl.utils.cell import coordinate_from_string
from datetime import datetime
from copy import copy

# 定数を定義
TIME_SHIFT = 60 * 60 * 9                             # 時刻を9時間シフトするための定数
FILE_PATH = fr"C:\Users\username\tradenote.xlsx"     # トレードノートのパス
SHEET_NAME = "トレード履歴"                           # シート名
TABLE_NAME = "trade_history"                         # テーブル名
ROW_HEIGHT = 75                                      # 行の高さ
```
まずはライブラリのインポートと定数の定義ですね。

`FILE_PATH`、`SHEET_NAME`、`TABLE_NAME` はあらかじめ作ったトレードノートのExcelファイルと一致するようにします。`ROW_HEIGHT`はテーブルの行の高さを設定できます。お好みでどうぞ。

注目ポイントは `TIME_SHIFT` です。実は取得できるトレード履歴の時刻は、MT5の時刻と一致しているとは限りません。MT5は通常、サマータイム期間は `GMT+3` 、それ以外の期間は `GMT+2` の時刻が表示されるのですが、トレード履歴の時刻はMT5の時刻と関係なく記録されています。

私の場合はMT5に表示されている時刻（`GMT+3`）から9時間進んだ時刻（`GMT+12`）になっていました。 `TIME_SHIFT` はそのずれを解消するために定義しています。

この時刻のずれが何に起因するのかよくわかっていませんが、利用しているFX業者によってまちまちの可能性もあるので確かめた方が良いでしょう。

ということで時刻のずれを確認するためのコードが以下の `confirm_timeshift.py` です。このコードをMT5のアプリを開いて口座にログインした状態で実行してください。

```Python:confirm_timeshift.py
import MetaTrader5 as mt5
from datetime import datetime, timedelta

# MT5に接続
if not mt5.initialize():
    print("MT5の接続に失敗しました")
    mt5.shutdown()
    exit()

# 例：過去7日間を検索
to_time = datetime.now() + timedelta(days=1)
from_time = to_time - timedelta(days=7)

# 約定履歴を取得
deals = mt5.history_deals_get(from_time, to_time)

# 最新の約定を取得
if deals:
    latest_deal = deals[-1]  # 最後の要素が最新
    print("最新の約定時刻:", latest_deal.time)
else:
    print("約定が見つかりませんでした。")

mt5.shutdown()
```

過去7日間の取引履歴を検索し、最新の約定時刻を表示します。これとMT5の口座履歴を照らし合わせて、時刻がどれくらいずれているか確認してください。


:::note warn
サマータイム期間中しか確認していないため、それ以外の期間では時刻のずれが変化するかもしれません。ご了承ください。
:::

## 文字列をUNIX時間に変換する関数

```Python
# 関数を定義
def return_shifted_unix_time_from_string(date_string):
    if "T" in date_string:
        try:
            unix_time = datetime.strptime(date_string, "%Y/%m/%dT%H:%M:%S")
        except ValueError:
            return None
        
        unix_time = datetime.timestamp(unix_time)
    elif "t" in date_string:
        try:
            unix_time = datetime.strptime(date_string, "%Y/%m/%dt%H:%M:%S")
        except ValueError:
            return None
        
        unix_time = datetime.timestamp(unix_time)
    else:
        try:
            unix_time = datetime.strptime(date_string, "%Y/%m/%d")
        except ValueError:
            return None
        
        unix_time = datetime.timestamp(unix_time)
    return unix_time
```
プログラム実行直後に `from_date` と `to_date` を入力しますが、その文字列をUNIX時間に直す関数です。日付のみ指定する場合と、時分秒まで指定する場合の両方に対応しています。引数の文字列のフォーマットが想定と異なる場合は `None` を返します。


## 曜日の文字列を取得する関数
```Python
def change_weekday_to_japanese(weekday):
    match weekday:
        case 0:
            return "月"
        case 1:
            return "火"
        case 2:
            return "水"
        case 3:
            return "木"
        case 4:
            return "金"
        case 5:
            return "土"
        case 6:
            return "日"
        case _:
            return "
```
特になし。


## 取引のタイプを文字列に変換する関数
```Python
def change_type_to_string(type):
    match type:
        case mt5.DEAL_TYPE_BUY:
            return "buy"
        case mt5.DEAL_TYPE_SELL:
            return "sell"
        case mt5.DEAL_TYPE_BALANCE:
            return "balance"
        case _:
            return "else"
```
取引のタイプはたくさんあるっぽいですが、売り買いと入出金だけ区別できるようにしています。


## 価格を良き所で丸める関数
```Python
def round_price(price):
    if price < 10:
        return round(price,5)
    elif price < 1000:
        return round(price,3)
    else:
        return round(price,2)
```
通貨ペアによって小数第何位まで有効なのか変わってきます（USDJPYなら小数第３位まで、EURUSDなら小数第５位まで）。通貨ペアごとに場合分けしていくのが確実ですが、面倒だったので価格の大きさで場合分けしました。だいたいの通貨ペアはこれでいけると思いますが、CFDなどでは挙動がおかしくなること確実です。まあCFDは触らないからこれでいいのだ。

## セルのスタイルを1行上のセルからコピーする関数
```Python
def copy_cell_style(dst_cell, src_cell):
    dst_cell.font = copy(src_cell.font)
    dst_cell.fill = copy(src_cell.fill)
    dst_cell.border = copy(src_cell.border)
    dst_cell.alignment = copy(src_cell.alignment)
    dst_cell.number_format = copy(src_cell.number_format)
    return
```
フォント・塗りつぶし・枠線・テキストのそろえ方・数値の表示形式をコピーします。

## テーブル範囲を1行下に広げる関数
```Python
def expand_table_range_by_one_row(table):
    ref = table.ref
    start_cell, end_cell = ref.split(":")
    # 行・列番号に変換
    end_col_letter, end_row = coordinate_from_string(end_cell)
    table.ref = f"{start_cell}:{end_col_letter}{end_row+1}"
```
テーブル範囲を広げることでテーブルの一番下に空白行をつくり、そこに新たにデータを入力していきます。


## yes/noの質問をするための関数
```Python
def ask_yes_no(prompt="続けますか？ [y/n]: "):
    while True:
        answer = input(prompt).strip().lower()
        if answer in ("y", "yes"):
            return True
        elif answer in ("n", "no"):
            return False
        else:
            print("y または n で答えてください。")
            ask_yes_no(prompt)
```
特になし。


## まとめ
とりあえずコード前半部分まで解説しました。後半部分は以下の記事で解説しているのでぜひ。

https://qiita.com/gappac/items/45ff3bd7506511cb515e


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

https://github.com/gappac/export_trade_history_with_python

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