MetaTrader5の経済指標カレンダー関数を使ってみた

# はじめに
MetaTrader(MT4)からMetaTrader5(MT5)にバージョンアップして追加された機能のひとつに**経済指標カレンダー関数**があります。

バージョンアップした当初から気になってはいたのですが、経済指標でシグナルを出すのはちょっと違うなと思い、先送りしていました。

ただ、最近テクニカルだけのEAのネタも尽きたし、ニュースフィルタとして使うのはありかと思い、試してみることにしました。

# 経済指標カレンダーとは
経済指標カレンダーとは、各国の経済指標の発表や、要人のスピーチなど、相場に影響を与える可能性のあるイベント一覧のことです。

MT5では、ツールボックスの**指標カレンダー**のタブ画面に表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131169/a5338eb0-1756-bc33-aaf8-1baa589b4ced.png)

ツールボックスに指標カレンダーのタブが出てこない場合は、メニューの[ツール]－[オプション]－[コミュニティ]で、ターミナルで使用するサービスとして「**指標カレンダー**」にチェックを入れてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131169/bee6b025-a3c6-7e52-80bf-56c805cad0f9.png)

これらのイベントはチャートにも表示させられます。裁量トレードであれば、プログラムすることなく利用できるでしょう。

# 経済指標カレンダー関数でやりたいこと
商用EAのなかには、「**ニュースフィルタ**」という機能を搭載しているものがあります。

経済指標の発表直後は、相場が急変することも多いので、そういう値動きが苦手なEAでは、経済指標が発表される前後は取引を控えた方がよい場合もあります。シグナルが出てもニュースの前後のシグナルは無効化する機能がニュースフィルタです。

もちろん経済指標の発表スケジュールはネット上に公表されているので、そのページをスクレイピングしてEAに取り込むこともできます。

ただ、プログラミング初心者にとっては敷居の高いことなので、もっと簡単にできた方がいいでしょう。

MT5の**経済指標カレンダー関数**は、MQL5のライブラリ関数として使えるようになっているので、これを使って**ニュースフィルタを作る**のが今回の目的です。

# 経済指標カレンダー関数のデータ構造
MQL5の経済指標カレンダー関数のドキュメントは、以下のサイトに掲載されています。

https://www.mql5.com/ja/docs/calendar

経済指標カレンダー関数では、おもに以下の二つの構造体を使ってデータを管理しています。

* MqlCalendarEvent
* MqlCalendarValue

それぞれの構造体に含まれるメンバーはたくさんあるのですが、この記事で使うものだけ抜粋すると、以下のようになります。詳しくはMQL5のドキュメントをご参照ください。

```c++
struct MqlCalendarEvent
 {
  ulong id; //イベントID
  ulong country_id; //国ID
  ENUM_CALENDAR_EVENT_IMPORTANCE importance; //イベントの重要度
  string name; //イベント名
 };
 ```
 ```c++
 struct MqlCalendarValue
 {
  ulong id; //値ID
  ulong event_id; //イベントID
  datetime time; // イベントの日時
};
```
**MqlCalendarEvent**(イベント)は、「米国非農業部門雇用者数(NFP)」「消費者物価指数(CPI)」といった**経済指標の種類**を表す構造体で、**対象となる国**や**イベントの重要度**の情報も含みます。

**MqlCalendarValue**(値)は、個々の経済指標の**発表日時**や具体的な**指標値**を表す構造体です。前回の指標値とか予測値とかもあるのですが、ここではイベントの日時データだけを利用します。

個々のイベントの日時データを取得するには、次のように２段階で行います。

1. 通貨あるいは国を指定して、MqlCalendarEvent構造体の**イベントデータ**を取得します。イベントはイベントIDで区別しますが、日時データは含まないので、NFPとかCPIといった経済指標の種類が同じであれば同じイベントIDとなります。イベントデータにはそのイベントの重要度も含むので、必要に応じてイベントをフィルタリングします。

2. 取得したイベントIDと、いつからいつまでのイベントが必要かを指定して、各イベントの発表日時や指標値を含むMqlCalendarValue構造体の**値データ**を取得します。これがEAのニュースフィルタのためのイベント日時データリストになります。

具体的なコードのサンプルは、以下のセクションで紹介します。

# 経済指標イベントを取得する
関数の動作確認のため、MT5で動作するスクリプトのプログラムを作ってみます。

ここで利用する経済指標カレンダー関数は、`CalendarEventByCurrency()`と`CalendarValueHistoryByEvent()`です。

`CalendarEventByCurrency()`は、最初の引数に**通貨名**を指定し、その通貨名にマッチする**イベント一覧をEvent構造体の配列**として２番目の引数で返します。また関数の戻り値は、**取得したEventの総数**となります。

例えば、**EUR**という通貨に関連するイベントを`events`という配列に取得するコードは以下となります。
```c++
   MqlCalendarEvent events[];
   int count = CalendarEventByCurrency("EUR", events);
```

次に`CalendarValueHistoryByEvent()`は、最初の引数に**イベントID**を指定して、そのイベントに対する個々のデータを**Value構造体の配列**として２番目の引数で返します。なお、３番目の引数と４番目の引数では、**期間の範囲**を指定できます。戻り値は`bool`で、取得が成功したら`true`を返します。

なお、**イベントの重要度**は`events[]`の各要素の`importance`というメンバーに格納されています。重要度は**高**(`CALENDAR_IMPORTANCE_HIGH`)、**中**(`CALENDAR_IMPORTANCE_MODERATE`)、**低**(`CALENDAR_IMPORTANCE_LOW`)の３段階に分類されています。この情報を使って必要な重要度のイベントを抽出することができます。

例えば、上記のコードで取得した**EURに関するイベント**のうち、**重要度が高いもの**を抽出して、個々の**発表日時とイベント名**を`Print()`関数で表示させるコードは以下のようになります。
```c++
   MqlCalendarValue values[];
   datetime date_from = TimeCurrent();
   datetime date_to = date_from + 28*24*3600;
   for(int i=0; i<count; i++)
   {
      if(events[i].importance != CALENDAR_IMPORTANCE_HIGH) continue;
      if(CalendarValueHistoryByEvent(events[i].id, values, date_from, date_to))
      {
         for(int j=0; j<ArraySize(values); j++)
         {
            Print(values[j].time, " - ", events[i].name);
         }
      }
   }
```

ここで、このコードをスクリプトプログラムとして実行させると、以下のような表示となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131169/4520eca6-1ed4-01fd-b7fa-0823043c2676.png)

これを見ると、**発表時刻**が「**00:00:00**」になっているイベントがあります。これは、この時間に行われるというわけではなく、日中通して行われるなど発表時間が具体的に決まっていないイベントです。

そこで、このような発表時間が決まっていないイベントをカットするコードを追加すると、全体のスクリプトプログラムは以下のようになります。

```c++
void OnStart()
{
   MqlCalendarEvent events[];
   int count = CalendarEventByCurrency("EUR", events);

   MqlCalendarValue values[];
   datetime date_from = TimeCurrent();
   datetime date_to = date_from + 28*24*3600;
   for(int i=0; i<count; i++)
   {
      if(events[i].importance != CALENDAR_IMPORTANCE_HIGH) continue;
      if(CalendarValueHistoryByEvent(events[i].id, values, date_from, date_to))
      {
         for(int j=0; j<ArraySize(values); j++)
         {
            MqlDateTime tm;
            TimeToStruct(values[j].time, tm);
            if(tm.hour == 0 && tm.min == 0) continue;
            Print(values[j].time, " - ", events[i].name);
         }
      }
   }
}
```
なお、**MT5を起動して最初にこのスクリプトを実行**したときに、フリーズするというか、最初の関数の実行にやけに時間がかかってしまうことがあります。この現象は当方の環境によるものかと思っていたのですが、MQL5のフォーラムでも似たような書き込みを見かけたので、MT5のバグか何かかもしれません。

ちょっと解決策はわからなかったのですが、そういう場合、**スクリプトを強制終了させて再度実行**すれば大丈夫だと思います。

# EAのニュースフィルタとして使う
経済指標カレンダー関数を使って経済指標の発表される日時が取得できるようになったので、実際にEAのニュースフィルタとして使う方法を見てみましょう。

前述のコードを流用するなら、`Print()`のところで、`values[j].time`が現在時刻の前後の一定の時間帯に入っているかどうかをチェックすればOKです。EAに組み込みやすいように関数化してみます。

関数の一例として、ニュースの対象となる**通貨名**、シグナルをフィルタリングする**指標発表前の時間（分）**、**指標発表後の時間（分）** を引数として、現在の時刻がニュースの時間帯に該当していれば`true`、該当していなければ`false`を返す仕様にしてみます。

```c++
bool NewsFilter(string currency, int before_min, int after_min)
{
   MqlCalendarEvent events[];
   int count = CalendarEventByCurrency(currency, events);

   MqlCalendarValue values[];
   datetime date_from = TimeCurrent();
   datetime date_to = date_from + 28*24*3600;
   for(int i=0; i<count; i++)
   {
      if(events[i].importance != CALENDAR_IMPORTANCE_HIGH) continue;
      if(CalendarValueHistoryByEvent(events[i].id, values, date_from, date_to))
      {
         for(int j=0; j<ArraySize(values); j++)
         {
            MqlDateTime tm;
            TimeToStruct(values[j].time, tm);
            if(tm.hour == 0 && tm.min == 0) continue;
            if(TimeCurrent() >= values[j].time-before_min*60 && TimeCurrent() <= values[j].time+after_min*60) return true;
         }
      }
   }
   return false;
}
```
先ほどの`Print()`文が`if`文に変わっただけです。

FXの通貨ペアでは、二つの通貨が対象となっているので、最初の通貨名と２番目の通貨名は以下のように抽出することができます。
```c++
//EURに関するニュースの120分前から60分後
NewsFilter(StringSubstr(_Symbol, 0, 3), 120, 60)
//USDに関するニュースの120分前から60分後
NewsFilter(StringSubstr(_Symbol, 3, 3), 120, 60)
```
`_Symbol`が`EURUSD`であれば、それぞれの関数で、EURとUSDのニュースの時間帯かどうかの判別を行うことができます。各自のEAの仕様に合わせてシグナルの選別に利用してください。

# バックテストでもニュースフィルタが使えるようにする
単にMT5のチャート上でEAを動かすだけなら、前述のコードで問題ないのですが、そのEAをストラテジーテスターでバックテストする場合、ちょっと問題があります。

どうやら**バックテストの場合、経済指標カレンダー関数が機能しない**ようです。実際、MQL5のフォーラムでも同様の話題がありました。

そこでいい解決策があるのかと思いきや、結局は**指標の発表時間の一覧をファイルに保存しておいて、バックテスト時にそれを読み出して使え**、ということでした。

## 経済指標イベントをファイルに保存する

仕方ないので、**指標発表日時をファイルに保存すること**を考えてみます。

ファイルにはバイナリファイル、CSVファイル、テキストファイルなどありますが、定型のデータなので**CSVファイル**が扱いやすいでしょう。

ただ、**ファイルを作成する場所**に注意する必要があります。通常、EAを実行中にアクセスできるファイルの場所は、**データフォルダの下のMQL5\Files**フォルダです。

しかし、バックテストの際にアクセスできるファイルの場所は、そこではなく、バックテストの都度できる**testing_agent_directoryの下のMQL5\Files**フォルダとなります。

ただ、あらかじめデータファイルを作っておくのであれば、フォルダの場所が異なるのは都合が悪いです。そこで、別の方法として、**common**フォルダの下に作成することを考えます。

ここで、**common**フォルダとは、MT5のデータフォルダの上位の**terminal**というフォルダの下の**common**フォルダのことで、さらにその下の**Files**がEAの実行とバックテストの両方で同様に読み書きできるフォルダとなります。

ただし、このフォルダはMetaTraderアプリ共通で、同じPC上に複数のMT4やMT5をインストールしていると、お互いに影響を及ぼすことがあるので注意してください。

**common**フォルダのファイルを読み書きする場合、ファイルオープン関数のフラグに`FILE_COMMON`を追加しておく必要があります。

```c++
int file_handle = FileOpen("newstime.csv", FILE_WRITE|FILE_CSV|FILE_COMMON);
```
あとは、保存したいデータを`FileWrite()`関数で保存するだけです。

以下が引数に通貨名を指定して、**Common\Files\newstime.csv** というファイルにニュースの発表日時とイベント名を保存する関数の一例です。

```c++
void SaveNewstime(string currency)
{
   int file_handle = FileOpen("newstime.csv", FILE_WRITE|FILE_CSV|FILE_COMMON);
   if(file_handle == INVALID_HANDLE) return;

   MqlCalendarEvent events[];
   int count = CalendarEventByCurrency(currency, events);

   MqlCalendarValue values[];
   datetime date_from = TimeCurrent();
   datetime date_to = date_from + 28*24*3600;
   for(int i=0; i<count; i++)
   {
      if(events[i].importance != CALENDAR_IMPORTANCE_HIGH) continue;
      if(CalendarValueHistoryByEvent(events[i].id, values, date_from, date_to))
      {
         for(int j=0; j<ArraySize(values); j++)
         {
            MqlDateTime tm;
            TimeToStruct(values[j].time, tm);
            if(tm.hour == 0 && tm.min == 0) continue;
            FileWrite(file_handle, values[j].time, events[i].name);
         }
      }
   }
   FileClose(file_handle);
}
```

ただし、バックテスト用のデータの場合、バックテストの期間中の経済指標イベントを揃えないといけないので、`date_from`は過去にさかのぼって設定する必要があります。

この関数をスクリプトやEAで実行すると、**common**フォルダに経済指標カレンダーのデータファイルが作成されます。

## EAから経済指標ファイルを読み出す
次にEAからこのファイルを読み出して、バックテストでもニュースフィルタのテストができるようにしてみます。

ファイルから読み出したいデータは複数あるので、何からの配列に格納しておきます。
ここでは、イベントの日時データを`NewsTime[]`という配列に格納しておくことにします。

```c++
#define MaxSize 1000
datetime NewsTime[MaxSize];
```

ニュースの数は前もってわからないので、配列のサイズを後から動的に変えることもできますが、ここでは簡単のため最大のサイズを1000と固定しておきます。

**Common\Files\newtime.csv**から日時データを読み出し、配列に格納する関数は以下のように書けます。

```c++
int LoadNewsTime()
{
   int file_handle = FileOpen("newstime.csv", FILE_READ|FILE_CSV|FILE_COMMON);
   if(file_handle == INVALID_HANDLE) return 0;
   for(int i=0; i<MaxSize; i++)
   {
      datetime dt = FileReadDatetime(file_handle);
      if(dt == 0) return i;
      NewsTime[i] = dt;
      string name = FileReadString(file_handle);
      Print(NewsTime[i], ":", name);
   }
   FileClose(file_handle);
   return MaxSize;
}
```

１行に時間と名前の二つのデータが書かれているので、それぞれ`FileReadDatetime()`、`FileReadString()`で読み出します。

データがなくなったところで時間データが0となるので、その時点での`i`（読み込んだデータ数）を返します。

```c++
int NTsize = LoadNewsTime();
```
あとは、データ数`NTsize`と、データ配列`NewsTime[]`を使ってニュースフィルタの関数を作成します。

ニュースフィルタは、`NewsTime[]`のデータを順番にチェックするだけなので、以下のように書けます。

```c++
bool NewsFilter(int before_min, int after_min)
{
   for(int i=0; i<NTsize; i++)
   {
      if(TimeCurrent() >= NewsTime[i]-before_min*60 && TimeCurrent() <= NewsTime[i]+after_min*60) return true;
   }
   return false;
}
```
これを前述の`NewsFilter()`に置き換えればOKです。これで、バックテストでもニュースフィルタの機能を再現できるようになります。

# おわりに
当初は、経済指標カレンダー関数だけで何とかなるかと思っていたのですが、結局バックテストさせるにはファイルの入出力が必要ということで、それなりのコードになってしまいました。

まあ、自作のEAにニュースフィルタを入れてみたところ、バックテストでパフォーマンスが向上したので、今回の試行は無駄にはならなかったかなと思います。

ただ、どんなEAでもニュースフィルタが有効であるという保証はありません。新しい機能を追加する際には、バックテストやフォワードテストは欠かさず実施した方がよいでしょう。