MT5でバックテストと同時に結果をCSV出力する方法


　個人利用するために書いた、とりあえず動作する程度のプログラミングコードなので、「無駄がある」「汎用的でない」などあるかもしれませんが予めご了承ください。もっとこういう書き方すればいいよというアドバイスがある方は是非ご教授ください。

# 本題
　MT5のストラテジーテスターでEAをバックテストするときその結果をバックテストと同時にCSVファイルに出力する仕組みを作っておけば、その後の分析をスムーズに行うことができるので、この方法をコピペしてすぐに使える形で共有しておきます。

　↓こんな感じのバックテスト情報CSVファイルを出力します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1882169/a959fa4d-7ca9-c5a2-4b2b-bc58d0ce99a0.png)




### 分析に必要な8つのデータを取得しよう
　今回は以下の8つのデータを取得するように作ります。必要に応じて取得するデータは変更してください。
||取得したい情報|MQL5でのデータ型|
|--:|:-:|:-:|
|1|in日時|datetime(long)|
|2|out日時|datetime(long)|
|3|通貨ペア|string|
|4|Buy/Sell|ENUM_DEAL_TYPE(long)|
|5|in約定価格|double|
|6|out約定価格|double|
|7|ロット数|double|
|8|損益|double|

　この8つのデータを全てのトレード分取得するために約定情報を配列に書き込む関数を用意します。関数は取得したいデータの型に合わせて作る必要があるのでlong型、double型、string型で合計3つ作成します。

```1個目.cpp
//+------------------------------------------------------------------+
//| long型の約定情報を配列に書き込む
//+------------------------------------------------------------------+
bool GetTradeResults(long &results[], const ENUM_DEAL_PROPERTY_INTEGER EDPI, const ENUM_DEAL_ENTRY EDE)
{
//--- 全期間の取引履歴をリクエストする
  if(!HistorySelect(0, TimeCurrent()))
    return (false);

  uint total_deals = HistoryDealsTotal();

//--- 配列を、履歴の取引数にリサイズ
  ArrayResize(results, total_deals);

  int   counter = 0;             // 取引回数カウンター
  ulong ticket_history_deal = 0; // ディールチケット

//--- 全ての取引を配列に取得
  for(uint i = 0; i < total_deals; i++) {
    //--- 取引を選択する
    if((ticket_history_deal = HistoryDealGetTicket(i)) > 0) {
      ENUM_DEAL_ENTRY deal_entry  = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_history_deal, DEAL_ENTRY);
      long            deal_type   = HistoryDealGetInteger(ticket_history_deal, DEAL_TYPE);
      long            deal_info   = HistoryDealGetInteger(ticket_history_deal, EDPI);

      //--- 取引操作以外はスルー
      if((deal_type != DEAL_TYPE_BUY) && (deal_type != DEAL_TYPE_SELL))
        continue;

      //--- ENUM_DEAL_ENTRYで指定した情報のみ
      if(deal_entry == EDE) {
        //--- 取引結果を配列に書き込み、取引のカウントアップ
        results[counter] = deal_info;
        counter++;
      }
    }
  }
//--- 配列の最終サイズを設定する
  ArrayResize(results, counter);
  return (true);
}
```

```2個目.cpp
//+------------------------------------------------------------------+
//| Double型の約定情報を配列に書き込む
//+------------------------------------------------------------------+
bool GetTradeResults(double &results[], const ENUM_DEAL_PROPERTY_DOUBLE EDPD, const ENUM_DEAL_ENTRY EDE)
{
//--- 全期間の取引履歴をリクエストする
  if(!HistorySelect(0, TimeCurrent()))
    return (false);

  uint total_deals = HistoryDealsTotal();

//--- 配列を、履歴の取引数にリサイズ
  ArrayResize(results, total_deals);

  int   counter = 0;             // 取引回数カウンター
  ulong ticket_history_deal = 0; // ディールチケット

//--- 全ての取引を配列に取得
  for(uint i = 0; i < total_deals; i++) {
    //--- 取引を選択する
    if((ticket_history_deal = HistoryDealGetTicket(i)) > 0) {
      ENUM_DEAL_ENTRY deal_entry  = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_history_deal, DEAL_ENTRY);
      long            deal_type   = HistoryDealGetInteger(ticket_history_deal, DEAL_TYPE);
      double          deal_info   = HistoryDealGetDouble(ticket_history_deal, EDPD);

      //--- 取引操作以外はスルー
      if((deal_type != DEAL_TYPE_BUY) && (deal_type != DEAL_TYPE_SELL))
        continue;

      //--- ENUM_DEAL_ENTRYで指定した情報のみ
      if(deal_entry == EDE) {
        //--- 取引結果を配列に書き込み、取引のカウントアップ
        results[counter] = deal_info;
        counter++;
      }
    }
  }
//--- 配列の最終サイズを設定する
  ArrayResize(results, counter);
  return (true);
}
```

```3個目.cpp
//+------------------------------------------------------------------+
//| string型の約定情報を配列に書き込む
//+------------------------------------------------------------------+
bool GetTradeResults(string &results[], const ENUM_DEAL_PROPERTY_STRING EDPS, const ENUM_DEAL_ENTRY EDE)
{
//--- 全期間の取引履歴をリクエストする
  if(!HistorySelect(0, TimeCurrent()))
    return (false);

  uint total_deals = HistoryDealsTotal();

//--- 配列を、履歴の取引数にリサイズ
  ArrayResize(results, total_deals);

  int   counter = 0;             // 取引回数カウンター
  ulong ticket_history_deal = 0; // ディールチケット

//--- 全ての取引を配列に取得
  for(uint i = 0; i < total_deals; i++) {
    //--- 取引を選択する
    if((ticket_history_deal = HistoryDealGetTicket(i)) > 0) {
      ENUM_DEAL_ENTRY deal_entry  = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_history_deal, DEAL_ENTRY);
      long            deal_type   = HistoryDealGetInteger(ticket_history_deal, DEAL_TYPE);
      string          deal_info   = HistoryDealGetString(ticket_history_deal, EDPS);

      //--- 取引操作以外はスルー
      if((deal_type != DEAL_TYPE_BUY) && (deal_type != DEAL_TYPE_SELL))
        continue;

      //--- ENUM_DEAL_ENTRYで指定した情報のみ
      if(deal_entry == EDE) {
        //--- 取引結果を配列に書き込み、取引のカウントアップ
        results[counter] = deal_info;
        counter++;
      }
    }
  }
//--- 配列の最終サイズを設定する
  ArrayResize(results, counter);
  return (true);
}
```

　次に「バックテスト結果をCSVファイルに出力する関数」を作っていきます。すでに同名CSVファイルが存在するときは上書き保存するようにします。また、単一バックテストのみで動作するようにしています。最適化で動作しないように注意しましょう。

　もっと汎用的なコードを書きたい人はご自身でいろいろ工夫してみて下さい。

```.cpp
//+------------------------------------------------------------------+
//| バックテスト結果をCSV出力する関数
//+------------------------------------------------------------------+
bool OutputResultsToCSV(void)
{
//--- 単一バックテストのみ
  if(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
    return false;

//--- ファイル名(なんでもOK)
  string filename = __FILE__+"_BackTest";

//--- ファイルハンドルの取得
  int handle = FileOpen(filename + ".csv", FILE_CSV | FILE_WRITE);
  if( handle == INVALID_HANDLE ) {
    Print("ハンドル取得に失敗：", GetLastError());
    return false;
  }

  long   DealTimeIn[], DealTimeOut[], DealType[];
  double DealPriceIn[], DealPriceOut[], DealVolume[], DealProfit[];
  string DealSymbol[];
  int    total_deals  = 0;

//--- 取引損益を配列に書き込み
  if(!GetTradeResults(DealTimeIn, DEAL_TIME, DEAL_ENTRY_IN)
      || !GetTradeResults(DealTimeOut, DEAL_TIME, DEAL_ENTRY_OUT)
      || !GetTradeResults(DealType, DEAL_TYPE, DEAL_ENTRY_IN)
      || !GetTradeResults(DealPriceIn, DEAL_PRICE, DEAL_ENTRY_IN)
      || !GetTradeResults(DealPriceOut, DEAL_PRICE, DEAL_ENTRY_OUT)
      || !GetTradeResults(DealVolume, DEAL_VOLUME, DEAL_ENTRY_OUT)
      || !GetTradeResults(DealProfit, DEAL_PROFIT, DEAL_ENTRY_OUT)
      || !GetTradeResults(DealSymbol, DEAL_SYMBOL, DEAL_ENTRY_OUT) ){
    FileClose(handle);
    return false;
  }

  total_deals = ArraySize(DealProfit);

//--- カラム名を書き込む
  FileWrite(handle, "in日時", "out日時", "通貨ペア", "Buy/Sell", "in約定価格", "out約定価格", "ロット数", "損益");

//--- CVSにデータを追加する
  for(int i = 0; i < total_deals; i++) {
    //--- ENUM_DEAL_TYPEをstring型の"Buy/Sell"で表現する
    string deal_type = "Buy";
    if(DEAL_TYPE_SELL == (ENUM_DEAL_TYPE)DealType[i])
      deal_type = "Sell";

    //--- 書き込み
    FileWrite(handle, (datetime)DealTimeIn[i],
              (datetime)DealTimeOut[i],
              DealSymbol[i],
              deal_type,
              DealPriceIn[i],
              DealPriceOut[i],
              DealVolume[i],
              DealProfit[i]);
  }

  FileClose(handle);

  return true;
}
```

　ここまでのプログラムは「__インクルードファイル(○○.mqh)__」にまとめておくと便利だと思います。

　あとはバックテストしたいEAのOnDeinit関数内で呼び出すだけです
```これはEAに書き足す.cpp
void OnDeinit(const int reason)
{
  if(OutputResultsToCSV())
    Print("CSV出力が完了しました");
  else
    Print("CSV出力が失敗しました(ファイルを開いてないか確認!!)");
}
```
　念のため、CSV出力が失敗していないか確認できるようにPrint()関数を書いておくと良いかと思います。失敗例でよくあるのが上書き保存したいCSVファイルをExcelなどで開いていて上書きできなくて失敗するパターンだと思います。

### 保存場所について
　最後に保存場所ですがMT5の仕様で、バックテスト中のEAからCSV出力するときは
__ファイルパス： \MetaQuotes\Tester\　(MT5アカウントディレクトリ) \Agent-○○(たぶんPCによって違う)\MQL5\Files__
ここに保存されるようになっています。