
MQL5でカレンダーベースのニュースイベントブレイクアウトエキスパートアドバイザーを開発する
はじめに
ボラティリティは、影響力の大きいニュースイベントの周辺でピークに達する傾向があり、大きなブレイクアウトの機会を生み出します。本記事では、MQL5でのカレンダーを基にしたブレイクアウト戦略の実装プロセスについて説明します。カレンダーデータを解釈・保存するためのクラスの作成、これを活用した現実的なバックテストの開発、そして最終的にライブ取引用の実行コードの実装までを一貫して解説します。
動機
MQL5コミュニティには、MetaTrader 5のカレンダーデータをバックテストで活用するための記事やコードベースが多数存在しますが、それらの多くは初心者にとっては難解で、単純なブレイクアウト戦略の開発を目指すには敷居が高いものです。本記事の目的は、MQL5のニュースカレンダーを活用した戦略構築のプロセスを簡素化し、明確かつ実用的なガイドを提供することにあります。
カレンダー型ニュースブレイクアウト戦略を構築するモチベーションは、経済指標の発表、企業の決算発表、地政学的イベントなど、事前に予定されているニュースイベントのタイミングを活かすことにあります。これらのイベントはしばしば、市場に大きなボラティリティと価格変動をもたらします。トレーダーは、この予測可能なイベントの発表を事前に意識し、ニュース後に価格が明確に既存のサポートまたはレジスタンスレベルを突破したタイミングでエントリーを狙うことで、ブレイクアウトによる利益を追求します。この戦略は、ニュース時に発生する流動性の急増とモメンタムを最大限に活用しつつ、同時に高まる不確実性をコントロールするための規律あるリスク管理を採用します。最終的には、重要なカレンダーイベントを中心に市場で繰り返し観察される特有のパターンや反応を利用するための、構造化されたアプローチを提供することを目的としています。
カレンダーニュースバックテスト
MQL5には、ブローカーが提供するニュースカレンダーデータを処理するための標準機能が用意されています。しかし、このデータは、既存のコードを使用してストラテジーテスター内で取得することはできません。そのため、Rene Balke氏の実装を元に適応したカレンダー履歴用のインクルードファイルを作成し、ニュース履歴データを処理してバイナリファイルとして保存する仕組みを構築します。このバイナリデータは、後のステップで利用することができます。
- CCalendarEntryクラスは、1つの経済カレンダーイベントを表現するクラスであり、国、イベントの詳細、関連する数値データ(予測値、実際値、前回値など)などの複数のプロパティを持ちます。
- Compare()メソッドは、2つのカレンダーイベントを「発表時刻」と「重要度」に基づいて比較し、どちらのイベントがより優先されるかを示す値を返します。
- ToString()メソッドは、イベントデータを人間が読みやすい形式の文字列に変換します。この文字列には、イベントの重要度やその他の関連プロパティが含まれ、ログやデバッグ時に便利です。
//+------------------------------------------------------------------+ //| A class to represent a single economic calendar event | //+------------------------------------------------------------------+ class CCalendarEntry :public CObject { public: ulong country_id; string country_name; string country_code; string country_currency; string country_currency_symbol; string country_url_name; ulong event_id; ENUM_CALENDAR_EVENT_TYPE event_type; ENUM_CALENDAR_EVENT_SECTOR event_sector; ENUM_CALENDAR_EVENT_FREQUENCY event_frequency; ENUM_CALENDAR_EVENT_TIMEMODE event_time_mode; ENUM_CALENDAR_EVENT_UNIT event_unit; ENUM_CALENDAR_EVENT_IMPORTANCE event_importance; ENUM_CALENDAR_EVENT_MULTIPLIER event_multiplier; uint event_digits; string event_source_url; string event_event_code; string event_name; ulong value_id; datetime value_time; datetime value_period; int value_revision; long value_actual_value; long value_prev_value; long value_revised_prev_value; long value_forecast_value; ENUM_CALENDAR_EVENT_IMPACT value_impact_type; //+------------------------------------------------------------------+ //| Compare news importance function | //+------------------------------------------------------------------+ int Compare(const CObject *node, const int mode = 0) const{ CCalendarEntry* other = (CCalendarEntry*)node; if (value_time==other.value_time){ return event_importance-other.event_importance; } return (int)(value_time -other.value_time); } //+------------------------------------------------------------------+ //| Convert data to string function | //+------------------------------------------------------------------+ string ToString(){ string txt; string importance = "None"; if(event_importance==CALENDAR_IMPORTANCE_HIGH)importance="High"; else if(event_importance==CALENDAR_IMPORTANCE_MODERATE) importance = "Moderate"; else if(event_importance==CALENDAR_IMPORTANCE_LOW)importance = "Low"; StringConcatenate(txt,value_time,">",event_name,"(",country_code,"|",country_currency,")",importance); return txt; } };
- CCalendarHistoryクラスは、CCalendarEntryクラスのオブジェクトをコレクションとして管理するクラスで、CArrayObjを拡張することで配列のような機能を提供します。また、カレンダーイベントデータへのアクセスおよび操作のための各種メソッドを備えています。
- operator[]メソッドはオーバーライドされており、指定したインデックスにあるCCalendarEntryオブジェクトを返します。これにより、配列形式でカレンダーエントリにアクセスすることができます。
- At()メソッドは、指定されたインデックスにあるCCalendarEntryへのポインタを返します。このメソッドは、配列にアクセスする前にインデックスが有効であることを確認します。
- LoadCalendarEntriesFromFile()メソッドは、バイナリファイルからカレンダーエントリを読み込みます。具体的には、国情報やイベントの詳細などの関連データを読み取り、それをCCalendarEntryオブジェクトに格納します。
//+------------------------------------------------------------------+ //| A class to manage a collection of CCalendarEntry objects | //+------------------------------------------------------------------+ class CCalendarHistory :public CArrayObj{ public: //overriding existing operators to better deal with calendar format data CCalendarEntry *operator[](const int index) const{return(CCalendarEntry*)At(index);} CCalendarEntry *At (const int index) const; bool LoadCalendarEntriesFromFile(string fileName); bool SaveCalendarValuesToFile(string filename); }; CCalendarEntry *CCalendarHistory::At(const int index)const{ if(index<0||index>=m_data_total)return(NULL); return (CCalendarEntry*)m_data[index]; } //+------------------------------------------------------------------+ //| A function to load calendar events from your saved binary file | //+------------------------------------------------------------------+ bool CCalendarHistory::LoadCalendarEntriesFromFile(string fileName){ CFileBin file; if(file.Open(fileName,FILE_READ|FILE_COMMON)>0){ while(!file.IsEnding()){ CCalendarEntry*entry = new CCalendarEntry(); int len; file.ReadLong(entry.country_id); file.ReadInteger(len); file.ReadString(entry.country_name,len); file.ReadInteger(len); file.ReadString(entry.country_code,len); file.ReadInteger(len); file.ReadString(entry.country_currency,len); file.ReadInteger(len); file.ReadString(entry.country_currency_symbol,len); file.ReadInteger(len); file.ReadString(entry.country_url_name,len); file.ReadLong(entry.event_id); file.ReadEnum(entry.event_type); file.ReadEnum(entry.event_sector); file.ReadEnum(entry.event_frequency); file.ReadEnum(entry.event_time_mode); file.ReadEnum(entry.event_unit); file.ReadEnum(entry.event_importance); file.ReadEnum(entry.event_multiplier); file.ReadInteger(entry.event_digits); file.ReadInteger(len); file.ReadString(entry.event_source_url,len); file.ReadInteger(len); file.ReadString(entry.event_event_code,len); file.ReadInteger(len); file.ReadString(entry.event_name,len); file.ReadLong(entry.value_id); file.ReadLong(entry.value_time); file.ReadLong(entry.value_period); file.ReadInteger(entry.value_revision); file.ReadLong(entry.value_actual_value); file.ReadLong(entry.value_prev_value); file.ReadLong(entry.value_revised_prev_value); file.ReadLong(entry.value_forecast_value); file.ReadEnum(entry.value_impact_type); CArrayObj::Add(entry); } Print(__FUNCTION__,">Loaded",CArrayObj::Total(),"Calendar Entries From",fileName,"..."); CArray::Sort(); file.Close(); return true; } return false; } //+------------------------------------------------------------------+ //| A function to save calendar values into a binary file | //+------------------------------------------------------------------+ bool CCalendarHistory::SaveCalendarValuesToFile(string fileName){ CFileBin file; if(file.Open(fileName,FILE_WRITE|FILE_COMMON)>0){ datetime chunk_end = TimeTradeServer(); // Let's do ~12 months (adjust as needed). int months_to_fetch = 12*25; while(months_to_fetch > 0) { // For each month, we go back ~30 days datetime chunk_start = chunk_end - 30*24*60*60; if(chunk_start < 1) // Just a safety check chunk_start = 1; MqlCalendarValue values[]; if(CalendarValueHistory(values, chunk_start, chunk_end)) { // Write to file for(uint i = 0; i < values.Size(); i++) { MqlCalendarEvent event; if(!CalendarEventById(values[i].event_id,event)) continue; // skip if not found MqlCalendarCountry country; if(!CalendarCountryById(event.country_id,country)) continue; // skip if not found file.WriteLong(country.id); file.WriteInteger(country.name.Length()); file.WriteString(country.name,country.name.Length()); file.WriteInteger(country.code.Length()); file.WriteString(country.code,country.code.Length()); file.WriteInteger(country.currency.Length()); file.WriteString(country.currency,country.currency.Length()); file.WriteInteger(country.currency_symbol.Length()); file.WriteString(country.currency_symbol, country.currency_symbol.Length()); file.WriteInteger(country.url_name.Length()); file.WriteString(country.url_name,country.url_name.Length()); file.WriteLong(event.id); file.WriteEnum(event.type); file.WriteEnum(event.sector); file.WriteEnum(event.frequency); file.WriteEnum(event.time_mode); file.WriteEnum(event.unit); file.WriteEnum(event.importance); file.WriteEnum(event.multiplier); file.WriteInteger(event.digits); file.WriteInteger(event.source_url.Length()); file.WriteString(event.source_url,event.source_url.Length()); file.WriteInteger(event.event_code.Length()); file.WriteString(event.event_code,event.event_code.Length()); file.WriteInteger(event.name.Length()); file.WriteString(event.name,event.name.Length()); file.WriteLong(values[i].id); file.WriteLong(values[i].time); file.WriteLong(values[i].period); file.WriteInteger(values[i].revision); file.WriteLong(values[i].actual_value); file.WriteLong(values[i].prev_value); file.WriteLong(values[i].revised_prev_value); file.WriteLong(values[i].forecast_value); file.WriteEnum(values[i].impact_type); } Print(__FUNCTION__, " >> chunk ", TimeToString(chunk_start), " - ", TimeToString(chunk_end), ": saved ", values.Size(), " events."); } // Move to the previous chunk chunk_end = chunk_start; months_to_fetch--; // short pause to avoid spamming server: Sleep(500); } file.Close(); return true; } return false; }
次に、バックテスト結果を取得するためのエキスパートアドバイザー(EA)を作成します。
このEAは5分足(M5)の時間枠で動作します。各クローズしたバーごとに、次の5分以内に高インパクトのニュースイベントが存在するかどうかを確認します。もし該当する場合は、現在のBid価格から一定の偏差内に買いストップ注文と売りストップ注文を出し、必要に応じてストップロスを設定します。さらに、指定した時刻に達した際には、すべてのポジションを決済します。
ニュースブレイクアウト取引戦略(高インパクトのニュースイベントの直前に、重要な価格レベルで買い/売りストップ注文を設定する)の開発は、いくつかの戦略的および戦術的な考慮事項によって動機づけられます。
- 爆発的な価格変動:高インパクトのニュースはしばしば大きな価格の急変を引き起こします。キーレベル付近にストップ注文を配置することで、ブレイクアウトが発生した瞬間にエントリーし、大きな値動きを捉えることが可能になります。
- リスクとリワードの強化:急激かつ変動の大きい値動きは、ブレイクアウトの方向にストップが執行されれば、有利なリスク・リワード比の取引機会を提供します。
- ニュース発表の予測可能性:高インパクトなニュースは事前に発表時刻が分かっているため、トレーダーはエントリーとエグジットをより正確に計画でき、市場タイミングに関する不確実性を減らすことができます。
- 流動性の急増を見越す:ニュースリリース時には市場参加者が増加し、流動性が高まり、重要な価格レベルのブレイクアウトがより信頼できるものになります。
- 事前に計画された執行:ニュース前にキーテクニカルレベルにストップ注文を置いておくことで、市場の急変時にも感情的な判断を避け、規律ある取引執行が可能になります。
- 自動化の可能性:事前に注文を設定しておくことで、ニュース発表と同時に自動的に実行され、手動操作を必要とせず市場変動に迅速に対応できます。
これらの動機を組み合わせることで、私たちは、高インパクトニュースイベントにおける予測可能性とボラティリティを活用しつつ、規律あるリスク管理と明確な実行ルールを持つ体系的なアプローチの構築を目指します。
EAは、必要なヘルパーファイルをインクルードし、関連するクラスのオブジェクトを作成することから始まります。また、後で使用する関連するグローバル変数も宣言します。
#define FILE_NAME "CalendarHistory.bin" #include <Trade/Trade.mqh> #include <CalendarHistory.mqh> #include <Arrays/ArrayString.mqh> CCalendarHistory calendar; CTrade trade; CArrayString curr; ulong poss, buypos = 0, sellpos=0; input int Magic = 0; int barsTotal = 0; int currentIndex = 0; datetime s_lastUpdate = 0; input int closeTime = 18; input int slp = 1000; input int Deviation = 1000; input string Currencies = "USD"; input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH; input bool saveFile = true;
OnInit()初期化関数では、以下の処理がおこなわれます。
- saveFileがtrueの場合、カレンダーエントリが「CalendarHistory.bin」という名前のファイルに保存されます。
- その後、カレンダーイベントはこのファイルから読み込まれます。ただし、保存処理の最後でファイルが閉じられるため、保存と読み込みは同時におこなうことはできません。
- 入力文字列Currenciesは個々の通貨に分割され、配列として格納されてソートされます。たとえば、USDとEURに関連するイベントの両方を対象としたい場合は、単に"USD";"EUR"と入力します。
- マジックナンバーがCTradeオブジェクトに割り当てられ、このEAによって開始された取引を識別できるようになります。
//+------------------------------------------------------------------+ //| Initializer function | //+------------------------------------------------------------------+ int OnInit() { if(saveFile==true)calendar.SaveCalendarValuesToFile(FILE_NAME); calendar.LoadCalendarEntriesFromFile(FILE_NAME); string arr[]; StringSplit(Currencies,StringGetCharacter(";",0),arr); curr.AddArray(arr); curr.Sort(); trade.SetExpertMagicNumber(Magic); return(INIT_SUCCEEDED); }
以下は、実行タスクを実行するために必要な関数です。
- OnTradeTransaction:受信した取引トランザクションを監視し、指定されたマジックナンバーを持つ買いまたは売り注文が追加された場合、その注文チケットをbuyposまたはsellposに記録します。
- executeBuy:指定された価格で買いストップ注文を発注し、ストップロスを計算して設定します。生成された注文チケットはbuyposに保存されます。
- executeSell:指定された価格で売りストップ注文を発注し、ストップロスを計算して設定します。生成された注文チケットはsellposに保存されます。
- IsCloseTime:現在のサーバー時間をチェックし、定義済みの終了時刻を過ぎているかどうかを判定します。
//+------------------------------------------------------------------+ //| A function for handling trade transaction | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy(double price) { double sl = price- slp*_Point; sl = NormalizeDouble(sl, _Digits); double lots=0.1; trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1); buypos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell(double price) { double sl = price + slp * _Point; sl = NormalizeDouble(sl, _Digits); double lots=0.1; trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Exit time boolean function | //+------------------------------------------------------------------+ bool IsCloseTime(){ datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime,timeStruct); int currentHour =timeStruct.hour; return(currentHour>closeTime); }
最後に、OnTick()関数に実行ロジックを実装します。
//+------------------------------------------------------------------+ //| OnTick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); datetime now = TimeTradeServer(); datetime horizon = now + 5*60; // 5 minutes from now while (currentIndex < calendar.Total()) { CCalendarEntry*entry=calendar.At(currentIndex); if (entry.value_time < now) { currentIndex++; continue; } // Now if the next event time is beyond horizon, break out if (entry.value_time > horizon) break; // If it is within the next 5 minutes, check other conditions: if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos ) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); executeBuy(bid + Deviation*_Point); executeSell(bid - Deviation*_Point); } currentIndex++; } if(IsCloseTime()){ for(int i = 0; i<PositionsTotal(); i++){ poss = PositionGetTicket(i); if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss); } } if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
これは、新しく確定したバーごとに条件をチェックすることを保証します。現在のバーが最後に保存されたバーと同じであれば、それはまだ新しいバーではないことを意味するため、残りの取引ロジックは実行せずに処理を終了します。
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
whileループのロジックは、データの先頭から処理を開始することで効率的なバックテストを実現します。現在の時刻が特定のイベントより後である場合、グローバル変数のインデックスをインクリメントすることで、再度先頭からループする必要をなくしています。これにより、計算時間やメモリ使用量が削減され、特に長期間のテストにおいて処理速度が大幅に向上し、多くの時間を節約できます。
while (currentIndex < calendar.Total()) { CCalendarEntry*entry=calendar.At(currentIndex); if (entry.value_time < now) { currentIndex++; continue; } // Now if the next event time is beyond horizon, break out if (entry.value_time > horizon) break; // If it is within the next 5 minutes, check other conditions: if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos ) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); executeBuy(bid + Deviation*_Point); executeSell(bid - Deviation*_Point); } currentIndex++; }
この部分では、現在の時刻がクローズ時間を過ぎているかどうかをチェックします。もし過ぎていれば、ポートフォリオ内をループして、EAのマジックナンバーに該当する未決済ポジションがあるかを確認し、それらをクローズします。ポジションがクローズされた後は、チケット番号をゼロにリセットします。
if(IsCloseTime()){ for(int i = 0; i<PositionsTotal(); i++){ poss = PositionGetTicket(i); if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss); } } if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
次に、プログラムをコンパイルし、MetaTrader 5端末に移動します。任意のチャートを開き、このEAを次のようにチャートにドラッグします。
saveFile設定が「true」に設定されていることを確認してください。
ブローカーから提供されるニュースイベントのデータはすべてのチャートで共通であるため、銘柄(通貨ペアなど)は関係ありません。EAはチャートにアタッチされた時点ですぐに初期化され、その際にファイルが保存されます。数秒待った後、EAをチャートから削除しても問題ありません。ファイルはお使いのコンピューター上の共通ファイルパスに保存されており、そこに移動すればバイナリファイルが正常に保存されたことを確認できます。
これで、ストラテジーテスターで戦略をテストできます。
バックテストのパラメータに関する重要な注意点
-
saveFile変数は「false」に設定してください。これにより、初期化時にバイナリデータファイルが閉じられるのを防ぎます。
-
適切な許容スリッページとストップロスを設定しましょう。許容スリッページが広すぎるとニュースイベントによる急激な値動きを捉えられず、逆に狭すぎるとニュース時のスパイクによってスリッページが発生しやすくなります。
-
ポジションのクローズ時間も適切に設定してください。市場の終了時間や取引日の終わりに近い時間を選ぶのが理想的です。そうすることで、ニュースによる値動きをすべて取り込むことができます。このときに設定する時間は、お使いのブローカーのサーバー時間に依存します。
以下は、2019年1月1日から2024年12月1日までの期間における、5分足のSPIUSDcでの私のバックテスト結果です。
主な結果
- プロフィットファクター:1.26
- シャープレシオ:2.66
- 取引数:1604
最後に、可能であれば実際のティックデータを使用することをおすすめします。影響の大きいニュースイベント時のスリッページや高スプレッドを考慮するために、高レイテンシーでのストレステストを選択してください。さらに、バックテスト結果の信頼性を検証するために、ライブ環境でデモ取引をおこなうことも重要です。ブローカーによってはスリッページが大きい場合もあれば、非常に小さい場合もあります。高ボラティリティに依存する戦略を運用する際は、ライブ取引でも確実に利益を出せるよう、必ず追加の対策を講じてください。
ライブ取引の実装
ライブ取引では、別のEAコードを使用します。これにより、MQL5のカレンダー操作機能を活用し、以前のロジックと同様の処理をおこなうことが可能です。主な違いは、今後発生するニュースイベントを更新するための関数を作成し、作成済みのカレンダーオブジェクトの配列にイベント情報を格納し、1時間ごとに更新する点です。
このコードのロジックは、最後の更新から1時間以上経過している場合にカレンダーAPIから新しいイベントデータを取得し、カレンダー履歴を更新します。取得したイベントの詳細(国、発表値、予測値など)を処理し、それぞれのイベントごとに新しいCCalendarEntryオブジェクトとして保存します。
//+------------------------------------------------------------------+ //| Update upcoming news events | //+------------------------------------------------------------------+ void UpdateCalendarHistory(CCalendarHistory &history) { //upcoming event in the next hour datetime fromTime = TimeTradeServer()+3600; // For example, if it's been > 1hr since last update: if(fromTime - s_lastUpdate > 3600) { // Determine the time range to fetch new events // For instance, from s_lastUpdate to 'now' MqlCalendarValue values[]; if(CalendarValueHistory(values, s_lastUpdate, fromTime)) { for(uint i = 0; i < values.Size(); i++) { MqlCalendarEvent event; if(!CalendarEventById(values[i].event_id,event)) continue; MqlCalendarCountry country; if(!CalendarCountryById(event.country_id, country)) continue; // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country' CCalendarEntry *entry = new CCalendarEntry(); entry.country_id = country.id; entry.value_time = values[i].time; entry.value_period = values[i].period; entry.value_revision = values[i].revision; entry.value_actual_value = values[i].actual_value; entry.value_prev_value = values[i].prev_value; entry.value_revised_prev_value = values[i].revised_prev_value; entry.value_forecast_value = values[i].forecast_value; entry.value_impact_type = values[i].impact_type; // event data entry.event_id = event.id; entry.event_type = event.type; entry.event_sector = event.sector; entry.event_frequency = event.frequency; entry.event_time_mode = event.time_mode; entry.event_unit = event.unit; entry.event_importance = event.importance; entry.event_multiplier = event.multiplier; entry.event_digits = event.digits; entry.event_source_url = event.source_url; entry.event_event_code = event.event_code; entry.event_name = event.name; // country data entry.country_name = country.name; entry.country_code = country.code; entry.country_currency = country.currency; entry.country_currency_symbol = country.currency_symbol; entry.country_url_name = country.url_name; // Add to your in-memory calendar history.Add(entry); } } // Sort to keep chronological order history.Sort(); // Mark the last update time s_lastUpdate = fromTime; } }
残りのコードはバックテストEAとほぼ同じです。このようにコードを実行EAに統合するだけで完了です。
#include <Trade/Trade.mqh> #include <CalendarHistory.mqh> #include <Arrays/ArrayString.mqh> CCalendarHistory calendar; CArrayString curr; CTrade trade; ulong poss, buypos = 0, sellpos=0; input int Magic = 0; int barsTotal = 0; datetime s_lastUpdate = 0; input int closeTime = 18; input int slp = 1000; input int Deviation = 1000; input string Currencies = "USD"; input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH; //+------------------------------------------------------------------+ //| Initializer function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); string arr[]; StringSplit(Currencies,StringGetCharacter(";",0),arr); curr.AddArray(arr); curr.Sort(); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Destructor function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| OnTick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; UpdateCalendarHistory(calendar); double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); datetime now = TimeTradeServer(); datetime horizon = now + 5*60; // 5 minutes from now // Loop over all loaded events for(int i = 0; i < calendar.Total(); i++) { CCalendarEntry *entry = calendar.At(i); // If event time is between 'now' and 'now+5min' if(entry.value_time > now && entry.value_time <= horizon&&buypos==sellpos&&entry.event_importance>=Importance&&curr.SearchFirst(entry.country_currency)>=0) { executeBuy(bid+Deviation*_Point); executeSell(bid-Deviation*_Point); } } if(IsCloseTime()){ for(int i = 0; i<PositionsTotal(); i++){ poss = PositionGetTicket(i); if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss); } } if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } } //+------------------------------------------------------------------+ //| A function for handling trade transaction | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy(double price) { double sl = price- slp*_Point; sl = NormalizeDouble(sl, _Digits); double lots=0.1; trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1); buypos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell(double price) { double sl = price + slp * _Point; sl = NormalizeDouble(sl, _Digits); double lots=0.1; trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Update upcoming news events | //+------------------------------------------------------------------+ void UpdateCalendarHistory(CCalendarHistory &history) { //upcoming event in the next hour datetime fromTime = TimeTradeServer()+3600; // For example, if it's been > 1hr since last update: if(fromTime - s_lastUpdate > 3600) { // Determine the time range to fetch new events // For instance, from s_lastUpdate to 'now' MqlCalendarValue values[]; if(CalendarValueHistory(values, s_lastUpdate, fromTime)) { for(uint i = 0; i < values.Size(); i++) { MqlCalendarEvent event; if(!CalendarEventById(values[i].event_id,event)) continue; MqlCalendarCountry country; if(!CalendarCountryById(event.country_id, country)) continue; // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country' CCalendarEntry *entry = new CCalendarEntry(); entry.country_id = country.id; entry.value_time = values[i].time; entry.value_period = values[i].period; entry.value_revision = values[i].revision; entry.value_actual_value = values[i].actual_value; entry.value_prev_value = values[i].prev_value; entry.value_revised_prev_value = values[i].revised_prev_value; entry.value_forecast_value = values[i].forecast_value; entry.value_impact_type = values[i].impact_type; // event data entry.event_id = event.id; entry.event_type = event.type; entry.event_sector = event.sector; entry.event_frequency = event.frequency; entry.event_time_mode = event.time_mode; entry.event_unit = event.unit; entry.event_importance = event.importance; entry.event_multiplier = event.multiplier; entry.event_digits = event.digits; entry.event_source_url = event.source_url; entry.event_event_code = event.event_code; entry.event_name = event.name; // country data entry.country_name = country.name; entry.country_code = country.code; entry.country_currency = country.currency; entry.country_currency_symbol = country.currency_symbol; entry.country_url_name = country.url_name; // Add to your in-memory calendar history.Add(entry); } } // Sort to keep chronological order history.Sort(); // Mark the last update time s_lastUpdate = fromTime; } } //+------------------------------------------------------------------+ //| Exit time boolean function | //+------------------------------------------------------------------+ bool IsCloseTime(){ datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime,timeStruct); int currentHour =timeStruct.hour; return(currentHour>closeTime); }
将来の戦略開発に向けて、この記事で紹介した基盤をもとに、ニュースを活用した取引アイデアを検討してみてください。以下はその代表例です。
主要レベルでのブレイクアウト取引
固定のデビエーションに頼るのではなく、重要なニュースによって主要なサポートやレジスタンスレベルを突破する価格変動に注目します。たとえば、大きな経済指標や企業発表が引き金となってブレイクアウトが起きた際、その方向にエントリーします。実装するには、ニューススケジュールを監視し、重要価格帯を事前に把握しておくことが必要です。
ニュースのフェードアウト
ニュース発表後の市場の最初の反応は過剰であると仮定し、その動きに逆らって取引をおこないます。急激な価格スパイクの後、市場が修正するタイミングを狙って逆方向にポジションを持つ戦略です。
ニュースイベントのフィルタリング
ボラティリティが低い市場で有効な戦略の場合は、影響力の大きいニュース発表時には取引を控えます。事前にニュースカレンダーを確認し、イベント前にポジションをクローズしたり、新規注文を停止したりする設定を行うことで、安定した相場環境で取引が可能になります。
ニューススカルピング
ニュースによる短期的な値動きを利用して小さな利益を積み重ねる戦略です。素早いエントリーとエグジット、タイトなストップロス設定、高速の利益確定がポイントです。特に価格変動が激しいイベント時に効果を発揮します。
経済指標カレンダー取引
このアプローチは、金利決定、GDP発表、雇用報告など、経済カレンダーに予定されているイベントを中心に展開されます。過去に市場が同様のニュースにどのように反応したかを分析し、現在の予想も考慮に入れることで、潜在的な価格変動を予測し、それに備えることができます。
これらの戦略はいずれも、関連ニュースデータを事前に収集・分析することが鍵となり、重要イベントによる市場のボラティリティを最大限に活かすことができます。
結論
この記事では、カレンダーニュースイベントデータを解釈・フォーマット・保存するためのヘルパーインクルードファイルを作成しました。次に、ブローカー提供のニュースイベントデータを取得し、戦略ロジックを実装して、このデータを使って取引戦略の結果をバックテストするEAを開発しました。この戦略は、5年間のティックデータにわたる1,600以上のサンプルを対象に、有望な収益性を示しました。最後に、ライブ取引用の実行EAコードを共有し、将来的な展望を述べるとともに、この記事で紹介したフレームワークを基盤にさらなる戦略開発を促しました。
ファイルの表
ファイル名 | ファイルの使用法 |
---|---|
CalendarHistory.mqh | カレンダーニュースイベントデータを処理するためのインクルードヘルパーファイル |
News Breakout Backtest.mq5 | ニュースイベントデータを保存し、バックテストを実行するためのEA |
News Breakout.mq5 | ライブ取引のためのEA |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16752
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
こんにちは!複数の通貨を入力するのに少し戸惑っています。試してみました:
「USD"; "GBP"
USD"; "GBP"。
「USD" "GBP";
最後のものだけがエラーを出しませんが、正しく動作しているかどうかはわかりません。もしかしたら、USDだけをピックアップしているのかもしれません。アドバイスいただけますか?
こんにちは!複数の通貨を入力するのに少し戸惑っています。試してみました:
「USD"; "GBP"
USD"; "GBP"。
「USD" "GBP";
最後のものだけはエラーが出ないが、正しく動作しているかどうかはわからない。もしかしたら、USDだけをピックアップしているのかもしれません。アドバイスいただけますか?
こんにちは、初期化関数のコードを見ると、コロンを分割し、異なる通貨をcurrオブジェクト属性に格納します。最初のものは動作するはずですが、クォーテーションを追加する必要はありません。保存処理では、属性に関係なくすべてのイベントがバイナリ・ファイルに保存されます。トレード・ロジックにおいてのみ、属性をフィルタリングします。以下は今私が実行したものです:
この実装では、ブローカー・サーバーのタイムゾーン切り替え(夏時間)を考慮しないため、バックテストや 最適化の際に不正確な結果が出るようです。
思い出させてくれてありがとう!私はデモのために夏時間を持たないブローカーを使用したため、記事の中でそれを考慮するのを忘れていました。
https://www.mql5.com/ja/book/advanced/calendar
このソースから、カレンダーデータはMQL5側から提供され、ブローカーの現在の Timetradeserver()のタイムゾーンに自動的に調整されることがわかります。
このソースから、カレンダーデータはMQL5側から提供され、ブローカーの現在の Timetradeserver()のタイムゾーンに自動的に調整されることがわかります。
書籍に掲載された実装は少し古いので、実際の(更新された)ストーリーは、ブログと コードベース(インジケータ)とコードベース(スクリプト)で見つけることができます。