はじめに
MetaTrader(MT4)からMetaTrader5(MT5)にバージョンアップして追加された機能のひとつに経済指標カレンダー関数があります。
バージョンアップした当初から気になってはいたのですが、経済指標でシグナルを出すのはちょっと違うなと思い、先送りしていました。
ただ、最近テクニカルだけのEAのネタも尽きたし、ニュースフィルタとして使うのはありかと思い、試してみることにしました。
経済指標カレンダーとは
経済指標カレンダーとは、各国の経済指標の発表や、要人のスピーチなど、相場に影響を与える可能性のあるイベント一覧のことです。
MT5では、ツールボックスの指標カレンダーのタブ画面に表示されます。
ツールボックスに指標カレンダーのタブが出てこない場合は、メニューの[ツール]-[オプション]-[コミュニティ]で、ターミナルで使用するサービスとして「指標カレンダー」にチェックを入れてみてください。
これらのイベントはチャートにも表示させられます。裁量トレードであれば、プログラムすることなく利用できるでしょう。
経済指標カレンダー関数でやりたいこと
商用EAのなかには、「ニュースフィルタ」という機能を搭載しているものがあります。
経済指標の発表直後は、相場が急変することも多いので、そういう値動きが苦手なEAでは、経済指標が発表される前後は取引を控えた方がよい場合もあります。シグナルが出てもニュースの前後のシグナルは無効化する機能がニュースフィルタです。
もちろん経済指標の発表スケジュールはネット上に公表されているので、そのページをスクレイピングしてEAに取り込むこともできます。
ただ、プログラミング初心者にとっては敷居の高いことなので、もっと簡単にできた方がいいでしょう。
MT5の経済指標カレンダー関数は、MQL5のライブラリ関数として使えるようになっているので、これを使ってニュースフィルタを作るのが今回の目的です。
経済指標カレンダー関数のデータ構造
MQL5の経済指標カレンダー関数のドキュメントは、以下のサイトに掲載されています。
経済指標カレンダー関数では、おもに以下の二つの構造体を使ってデータを管理しています。
- MqlCalendarEvent
- MqlCalendarValue
それぞれの構造体に含まれるメンバーはたくさんあるのですが、この記事で使うものだけ抜粋すると、以下のようになります。詳しくはMQL5のドキュメントをご参照ください。
struct MqlCalendarEvent
{
ulong id; //イベントID
ulong country_id; //国ID
ENUM_CALENDAR_EVENT_IMPORTANCE importance; //イベントの重要度
string name; //イベント名
};
struct MqlCalendarValue
{
ulong id; //値ID
ulong event_id; //イベントID
datetime time; // イベントの日時
};
MqlCalendarEvent(イベント)は、「米国非農業部門雇用者数(NFP)」「消費者物価指数(CPI)」といった経済指標の種類を表す構造体で、対象となる国やイベントの重要度の情報も含みます。
MqlCalendarValue(値)は、個々の経済指標の発表日時や具体的な指標値を表す構造体です。前回の指標値とか予測値とかもあるのですが、ここではイベントの日時データだけを利用します。
個々のイベントの日時データを取得するには、次のように2段階で行います。
-
通貨あるいは国を指定して、MqlCalendarEvent構造体のイベントデータを取得します。イベントはイベントIDで区別しますが、日時データは含まないので、NFPとかCPIといった経済指標の種類が同じであれば同じイベントIDとなります。イベントデータにはそのイベントの重要度も含むので、必要に応じてイベントをフィルタリングします。
-
取得したイベントIDと、いつからいつまでのイベントが必要かを指定して、各イベントの発表日時や指標値を含むMqlCalendarValue構造体の値データを取得します。これがEAのニュースフィルタのためのイベント日時データリストになります。
具体的なコードのサンプルは、以下のセクションで紹介します。
経済指標イベントを取得する
関数の動作確認のため、MT5で動作するスクリプトのプログラムを作ってみます。
ここで利用する経済指標カレンダー関数は、CalendarEventByCurrency()
とCalendarValueHistoryByEvent()
です。
CalendarEventByCurrency()
は、最初の引数に通貨名を指定し、その通貨名にマッチするイベント一覧をEvent構造体の配列として2番目の引数で返します。また関数の戻り値は、取得したEventの総数となります。
例えば、EURという通貨に関連するイベントをevents
という配列に取得するコードは以下となります。
MqlCalendarEvent events[];
int count = CalendarEventByCurrency("EUR", events);
次にCalendarValueHistoryByEvent()
は、最初の引数にイベントIDを指定して、そのイベントに対する個々のデータをValue構造体の配列として2番目の引数で返します。なお、3番目の引数と4番目の引数では、期間の範囲を指定できます。戻り値はbool
で、取得が成功したらtrue
を返します。
なお、イベントの重要度はevents[]
の各要素のimportance
というメンバーに格納されています。重要度は高(CALENDAR_IMPORTANCE_HIGH
)、中(CALENDAR_IMPORTANCE_MODERATE
)、低(CALENDAR_IMPORTANCE_LOW
)の3段階に分類されています。この情報を使って必要な重要度のイベントを抽出することができます。
例えば、上記のコードで取得したEURに関するイベントのうち、重要度が高いものを抽出して、個々の発表日時とイベント名をPrint()
関数で表示させるコードは以下のようになります。
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);
}
}
}
ここで、このコードをスクリプトプログラムとして実行させると、以下のような表示となります。
これを見ると、発表時刻が「00:00:00」になっているイベントがあります。これは、この時間に行われるというわけではなく、日中通して行われるなど発表時間が具体的に決まっていないイベントです。
そこで、このような発表時間が決まっていないイベントをカットするコードを追加すると、全体のスクリプトプログラムは以下のようになります。
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
を返す仕様にしてみます。
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の通貨ペアでは、二つの通貨が対象となっているので、最初の通貨名と2番目の通貨名は以下のように抽出することができます。
//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
を追加しておく必要があります。
int file_handle = FileOpen("newstime.csv", FILE_WRITE|FILE_CSV|FILE_COMMON);
あとは、保存したいデータをFileWrite()
関数で保存するだけです。
以下が引数に通貨名を指定して、Common\Files\newstime.csv というファイルにニュースの発表日時とイベント名を保存する関数の一例です。
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[]
という配列に格納しておくことにします。
#define MaxSize 1000
datetime NewsTime[MaxSize];
ニュースの数は前もってわからないので、配列のサイズを後から動的に変えることもできますが、ここでは簡単のため最大のサイズを1000と固定しておきます。
Common\Files\newtime.csvから日時データを読み出し、配列に格納する関数は以下のように書けます。
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;
}
1行に時間と名前の二つのデータが書かれているので、それぞれFileReadDatetime()
、FileReadString()
で読み出します。
データがなくなったところで時間データが0となるので、その時点でのi
(読み込んだデータ数)を返します。
int NTsize = LoadNewsTime();
あとは、データ数NTsize
と、データ配列NewsTime[]
を使ってニュースフィルタの関数を作成します。
ニュースフィルタは、NewsTime[]
のデータを順番にチェックするだけなので、以下のように書けます。
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でもニュースフィルタが有効であるという保証はありません。新しい機能を追加する際には、バックテストやフォワードテストは欠かさず実施した方がよいでしょう。