English Deutsch
preview
MQL5経済指標カレンダーを使った取引(第8回):ニュース駆動型バックテストの最適化 - スマートなイベントフィルタリングと選択的ログ

MQL5経済指標カレンダーを使った取引(第8回):ニュース駆動型バックテストの最適化 - スマートなイベントフィルタリングと選択的ログ

MetaTrader 5トレーディング |
40 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

本記事では、連載「MQL5経済指標カレンダーを使った取引」をさらに前進させ、取引システムを超高速かつ視覚的に直感的なバックテストに最適化します。ライブおよびオフラインモードの双方でデータを可視化できるよう統合し、ニュース駆動型戦略の開発を支援します。第7回で構築したストラテジーテスター互換のリソースベースイベント分析の基盤を踏まえ、今回はスマートなイベントフィルタリングと選択的ログ出力を導入し、パフォーマンスを効率化します。これにより、リアルタイムおよび過去の環境での戦略可視化とテストを、最小限のノイズで実現できます。この記事は次のトピックで構成されています。

  1. ライブおよびオフライン領域をまたぐ、ニュース駆動型取引のための視覚的クロノグラフ
  2. MQL5での実装
  3. テストと検証
  4. 結論

これらの進歩を順に詳しく見ていきます。


ライブおよびオフライン領域をまたぐ、ニュース駆動型取引のための視覚的クロノグラフ

経済ニュースイベントをライブおよびオフライン環境の両方で可視化・分析できることは、私たちにとって画期的です。本連載のこの部分では、視覚的クロノグラフ(最適化されたイベント処理およびログ出力システムの比喩)を導入し、ニュース駆動型取引の時間的な流れを精密かつ効率的に把握できるようにします。

スマートなイベントフィルタリングを実装することで、ストラテジーテスターの計算負荷を大幅に削減し、ユーザーが指定した日付範囲内で最も関連性の高いニュースイベントのみを事前に選択できます。これにより、バックテストの速度と明瞭さが、実際の取引に近い状態で実現できます。このフィルタリング機構は、クロノグラフの正確な時間計測に似ており、不要なデータを除外して重要なイベントに集中できるため、過去のシミュレーションとリアルタイム市場分析の間でシームレスな移行が可能です。

さらに、選択的ログ出力により、クロノグラフのように重要情報のみが視覚的に表示されます。これにより、取引実行やダッシュボード更新などの重要情報が明確に把握でき、不要なログを抑制して、ライブモードとオフラインモードの両方で集中しやすいインターフェースを維持できます。このデュアルモード可視化機能により、ストラテジーテスターで過去データを用いて戦略をテストし、同じ直感的なダッシュボードを実際の取引に応用することが可能です。これにより、すべての市場環境で意思決定と戦略改善を統一されたワークフローで支援できます。以下は、目指す可視化のイメージです。

計画されたオフライン領域の可視化


MQL5での実装

MQL5でこれらの改善を実現するために、まずダウンロードしたイベントを追跡するためのいくつかの変数を宣言する必要があります。これらのイベントは、前回の記事で実際の取引時に使用したのと同様の形式で、ニュースダッシュボードにシームレスに表示されます。その前に、データを格納するリソースを以下のようにインクルードしてください。

//---- Include trading library
#include <Trade\Trade.mqh>
CTrade trade;

//---- Define resource for CSV
#resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData

まず、ライブおよびオフラインモードの両方でシームレスに取引を実行できる取引ライブラリを統合します。#include <Trade\Trade.mqh>ディレクティブを使用してMQL5取引ライブラリを取り込み、取引操作を管理するためのCTradeクラスを利用します。tradeという名前のCTradeオブジェクトを宣言することで、プログラムが売買注文を自動的に実行できるようになります。

次に#resourceディレクティブを使用して\Files\Database\EconomicCalendar.csvをEconomicCalendarDataという名前の文字列リソースとして定義します。このカンマ区切り値(CSV)は、LoadEventsFromResource関数を通じて読み込まれ、日付、時刻、通貨、予測などのイベント情報を提供します。これにより、ライブデータフィードに依存せず、統一されたデータ表示が可能になります。これで、残りの制御変数を定義する準備が整いました。

//---- Event name tracking
string current_eventNames_data[];
string previous_eventNames_data[];
string last_dashboard_eventNames[]; // Added: Cache for last dashboard event names in tester mode
datetime last_dashboard_update = 0; // Added: Track last dashboard update time in tester mode

//---- Filter flags
bool enableCurrencyFilter = true;
bool enableImportanceFilter = true;
bool enableTimeFilter = true;
bool isDashboardUpdate = true;
bool filters_changed = true;        // Added: Flag to detect filter changes in tester mode

//---- Event counters
int totalEvents_Considered = 0;
int totalEvents_Filtered = 0;
int totalEvents_Displayable = 0;

//---- Input parameters (PART 6)
sinput group "General Calendar Settings"
input ENUM_TIMEFRAMES start_time = PERIOD_H12;
input ENUM_TIMEFRAMES end_time = PERIOD_H12;
input ENUM_TIMEFRAMES range_time = PERIOD_H8;
input bool updateServerTime = true; // Enable/Disable Server Time Update in Panel
input bool debugLogging = false;    // Added: Enable debug logging in tester mode

//---- Input parameters for tester mode (from PART 7, minimal)
sinput group "Strategy Tester CSV Settings"
input datetime StartDate = D'2025.03.01'; // Download Start Date
input datetime EndDate = D'2025.03.21';   // Download End Date

//---- Structure for CSV events (from PART 7)
struct EconomicEvent {
   string eventDate;       // Date of the event
   string eventTime;       // Time of the event
   string currency;        // Currency affected
   string event;           // Event description
   string importance;      // Importance level
   double actual;          // Actual value
   double forecast;        // Forecast value
   double previous;        // Previous value
   datetime eventDateTime; // Added: Store precomputed datetime for efficiency
};

//---- Global array for tester mode events
EconomicEvent allEvents[];
EconomicEvent filteredEvents[]; // Added: Filtered events for tester mode optimization

//---- Trade settings
enum ETradeMode {
   TRADE_BEFORE,
   TRADE_AFTER,
   NO_TRADE,
   PAUSE_TRADING
};
input ETradeMode tradeMode = TRADE_BEFORE;
input int tradeOffsetHours = 12;
input int tradeOffsetMinutes = 5;
input int tradeOffsetSeconds = 0;
input double tradeLotSize = 0.01;

//---- Trade control
bool tradeExecuted = false;
datetime tradedNewsTime = 0;
int triggeredNewsEvents[];

ここでは、イベント名をcurrent_eventNames_data、previous_eventNames_data、last_dashboard_eventNamesに格納します。last_dashboard_eventNamesはテスターモードのダッシュボード更新をキャッシュするために使用し、last_dashboard_updateは必要なときにのみリフレッシュをスケジュールすることで、冗長な処理を削減します。

イベントフィルタリングは、enableCurrencyFilter、enableImportanceFilter、enableTimeFilter、filters_changedを使って切り替えます。filters_changedがtrueの場合にフィルタをリセットし、関連性のあるイベントのみを処理します。また、sinput group 'General Calendar Settings'内のdebugLoggingを使って、取引や更新のみをログ出力します。

バックテスト期間は、sinput group 'Strategy Tester CSV Settings'内のStartDateとEndDateで定義します。イベントはEconomicEvent構造体で管理し、eventDateTimeにより高速アクセスを可能にします。すべてのイベント(allEvents)は、処理を高速化するためにfilteredEventsにフィルタリングされます。同時にtradeModeおよび関連変数を設定し、取引を効率的に実行できるようにします。これにより、テスト期間を選択してデータをダウンロードし、同じ期間をバックテストに使用できるようになります。以下は、ユーザーインターフェースのイメージです。

ユーザー入力インターフェース

画像からわかるように、テスターモードでのイベント表示の制御や、パネル内の時刻更新およびログ出力の管理をおこなうための追加入力が用意されています。これは、バックテスト時に不要なリソースの使用を最適化するためにおこないました。次に、テスターイベントのフィルタリング処理を担当する関数を定義する必要があります。

//+------------------------------------------------------------------+
//| Filter events for tester mode                                    | // Added: Function to pre-filter events by date range
//+------------------------------------------------------------------+
void FilterEventsForTester() {
   ArrayResize(filteredEvents, 0);
   int eventIndex = 0;
   for (int i = 0; i < ArraySize(allEvents); i++) {
      datetime eventDateTime = allEvents[i].eventDateTime;
      if (eventDateTime < StartDate || eventDateTime > EndDate) {
         if (debugLogging) Print("Event ", allEvents[i].event, " skipped in filter due to date range: ", TimeToString(eventDateTime)); // Modified: Conditional logging
         continue;
      }
      ArrayResize(filteredEvents, eventIndex + 1);
      filteredEvents[eventIndex] = allEvents[i];
      eventIndex++;
   }
   if (debugLogging) Print("Tester mode: Filtered ", eventIndex, " events."); // Modified: Conditional logging
   filters_changed = false;
}

ここでは、スマートなイベントフィルタリングを実装し、ストラテジーテスターで処理されるニュースイベントの数を減らすことでバックテストを高速化します。FilterEventsForTester関数を使用し、まずArrayResize関数でfilteredEvents配列をクリアし、allEventsから関連するイベントを再構築します。各イベントについて、そのeventDateTimeがStartDateとEndDateの範囲内かどうかを確認し、範囲外のイベントはスキップします。スキップされたイベントは、debugLoggingがtrueの場合にのみPrint関数でログ出力し、ログの混乱を最小限に抑えます。

条件を満たしたイベントは、filteredEvents配列でインデックスeventIndexの位置にコピーされ、追加ごとにeventIndexをインクリメントします。ArrayResize関数を用いて動的に配列のスペースを確保します。debugLoggingが有効な場合のみ、Printで総eventIndex数をログに出力し、テスター出力を整理された状態に保ちます。最後にfilters_changedをfalseに設定し、フィルタリングが完了したことを示します。この集中型フィルタリングにより、イベントセットが縮小され、後続処理の速度が向上するとともに、オフラインモードでのニュースイベントの効率的な可視化が可能になります。最後に、この関数をOnInitイベントハンドラ内で呼び出し、ニュースデータを事前にフィルタリングします。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   //---- Create dashboard UI
   createRecLabel(MAIN_REC,50,50,740,410,clrSeaGreen,1);
   createRecLabel(SUB_REC1,50+3,50+30,740-3-3,410-30-3,clrWhite,1);
   createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10,clrGreen,1);
   createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15);

   //---- Create calendar buttons
   int startX = 59;
   for (int i = 0; i < ArraySize(array_calendar); i++) {
      createButton(ARRAY_CALENDAR+IntegerToString(i),startX,132,buttons[i],25,
                   array_calendar[i],clrWhite,13,clrGreen,clrNONE,"Calibri Bold");
      startX += buttons[i]+3;
   }

   //---- Initialize for live mode (unchanged)
   int totalNews = 0;
   bool isNews = false;
   MqlCalendarValue values[];
   datetime startTime = TimeTradeServer() - PeriodSeconds(start_time);
   datetime endTime = TimeTradeServer() + PeriodSeconds(end_time);
   string country_code = "US";
   string currency_base = SymbolInfoString(_Symbol,SYMBOL_CURRENCY_BASE);
   int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL);

   //---- Load CSV events for tester mode
   if (MQLInfoInteger(MQL_TESTER)) {
      if (!LoadEventsFromResource()) {
         Print("Failed to load events from CSV resource.");
         return(INIT_FAILED);
      }
      Print("Tester mode: Loaded ", ArraySize(allEvents), " events from CSV.");
      FilterEventsForTester(); // Added: Pre-filter events for tester mode
   }

   //---- Create UI elements
   createLabel(TIME_LABEL,70,85,"Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)+
               "   |||   Total News: "+IntegerToString(allValues),clrBlack,14,"Times new roman bold");
   createLabel(IMPACT_LABEL,70,105,"Impact: ",clrBlack,14,"Times new roman bold");
   createLabel(FILTER_LABEL,370,55,"Filters:",clrYellow,16,"Impact");

   //---- Create filter buttons
   string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
   color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
   bool filter_curr_state = enableCurrencyFilter;
   createButton(FILTER_CURR_BTN,430,55,110,26,filter_curr_text,filter_curr_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_STATE,filter_curr_state);

   string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
   color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
   bool filter_imp_state = enableImportanceFilter;
   createButton(FILTER_IMP_BTN,430+110,55,120,26,filter_imp_text,filter_imp_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_STATE,filter_imp_state);

   string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
   color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
   bool filter_time_state = enableTimeFilter;
   createButton(FILTER_TIME_BTN,430+110+120,55,70,26,filter_time_text,filter_time_txt_color,12,clrBlack);
   ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_STATE,filter_time_state);

   createButton(CANCEL_BTN,430+110+120+79,51,50,30,"X",clrWhite,17,clrRed,clrNONE);

   //---- Create impact buttons
   int impact_size = 100;
   for (int i = 0; i < ArraySize(impact_labels); i++) {
      color impact_color = clrBlack, label_color = clrBlack;
      if (impact_labels[i] == "None") label_color = clrWhite;
      else if (impact_labels[i] == "Low") impact_color = clrYellow;
      else if (impact_labels[i] == "Medium") impact_color = clrOrange;
      else if (impact_labels[i] == "High") impact_color = clrRed;
      createButton(IMPACT_LABEL+string(i),140+impact_size*i,105,impact_size,25,
                   impact_labels[i],label_color,12,impact_color,clrBlack);
   }

   //---- Create currency buttons
   int curr_size = 51, button_height = 22, spacing_x = 0, spacing_y = 3, max_columns = 4;
   for (int i = 0; i < ArraySize(curr_filter); i++) {
      int row = i / max_columns;
      int col = i % max_columns;
      int x_pos = 575 + col * (curr_size + spacing_x);
      int y_pos = 83 + row * (button_height + spacing_y);
      createButton(CURRENCY_BTNS+IntegerToString(i),x_pos,y_pos,curr_size,button_height,curr_filter[i],clrBlack);
   }

   //---- Initialize filters
   if (enableCurrencyFilter) {
      ArrayFree(curr_filter_selected);
      ArrayCopy(curr_filter_selected, curr_filter);
      Print("CURRENCY FILTER ENABLED");
      ArrayPrint(curr_filter_selected);
      for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
         ObjectSetInteger(0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_STATE, true);
      }
   }

   if (enableImportanceFilter) {
      ArrayFree(imp_filter_selected);
      ArrayCopy(imp_filter_selected, allowed_importance_levels);
      ArrayFree(impact_filter_selected);
      ArrayCopy(impact_filter_selected, impact_labels);
      Print("IMPORTANCE FILTER ENABLED");
      ArrayPrint(imp_filter_selected);
      ArrayPrint(impact_filter_selected);
      for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
         string btn_name = IMPACT_LABEL+string(i);
         ObjectSetInteger(0, btn_name, OBJPROP_STATE, true);
         ObjectSetInteger(0, btn_name, OBJPROP_BORDER_COLOR, clrNONE);
      }
   }

   //---- Update dashboard
   update_dashboard_values(curr_filter_selected, imp_filter_selected);
   ChartRedraw(0);
   return(INIT_SUCCEEDED);
}

createRecLabel関数を使用して、異なる色とサイズのダッシュボードパネルMAIN_REC、SUB_REC1、SUB_REC2を作成し、createLabel関数でHEADER_LABELを追加して「MQL5 Economic Calendar」と表示します。これは以前と同様の手順です。カレンダーボタンはarray_calendarからcreateButtonとArraySize関数を使って動的に作成し、startXとbuttonsで位置を調整してイベント表示をおこないます。

ライブモードの準備として、CalendarValueHistory関数でイベントをvaluesに取得し、TimeTradeServerPeriodSecondsを使って計算したstartTimeとendTimeを利用します。テスターモードでは、MQLInfoInteger関数でMQL_TESTERを確認し、LoadEventsFromResource関数でEconomicCalendarDataを読み込みallEventsに格納します。ここで最も重要なFilterEventsForTester関数を使用してfilteredEventsを生成し、イベント処理を最適化します。

UI要素としてTIME_LABEL、IMPACT_LABEL、FILTER_LABELをcreateLabelで追加し、フィルタボタンFILTER_CURR_BTN、FILTER_IMP_BTN、FILTER_TIME_BTN、CANCEL_BTNをcreateButtonとObjectSetIntegerで作成します。enableCurrencyFilterに基づきfilter_curr_stateなどの状態を設定します。[Importance]ボタンおよび[Currency]ボタンはimpact_labelsとcurr_filterからcreateButtonで作成し、フィルタcurr_filter_selectedとimp_filter_selectedはArrayFree関数とArrayCopy関数で初期化します。ダッシュボードはupdate_dashboard_valuesとChartRedraw関数で更新され、INIT_SUCCEEDEDを返すことでセットアップが完了したことを確認します。プログラムを初期化すると、次の結果が得られます。

テスターの初期結果

フィルタ後に関連データを読み込めるようになったので、OnTickイベントハンドラでは、指定された時間内の関連データのみを取得し、ダッシュボードに反映させる必要があります。単に全データを表示するのではなく、ライブモードと同様の方法で処理します。以下が採用しているロジックです。また、忘れないように、変更を加えた特定かつ重要な更新箇所には関連するコメントを追加しています。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   UpdateFilterInfo();
   CheckForNewsTrade();
   if (isDashboardUpdate) {
      if (MQLInfoInteger(MQL_TESTER)) {
         datetime currentTime = TimeTradeServer();
         datetime timeRange = PeriodSeconds(range_time);
         datetime timeAfter = currentTime + timeRange;
         if (filters_changed || last_dashboard_update < timeAfter) { // Modified: Update on filter change or time range shift
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            ArrayFree(last_dashboard_eventNames);
            ArrayCopy(last_dashboard_eventNames, current_eventNames_data);
            last_dashboard_update = currentTime;
         }
      } else {
         update_dashboard_values(curr_filter_selected, imp_filter_selected);
      }
   }
}

OnTickイベントハンドラでは、UpdateFilterInfo関数を使用してフィルタ設定を更新し、CheckForNewsTrade関数でニュースイベントに基づく取引を評価、実行します。isDashboardUpdateがtrueの場合、MQLInfoInteger関数でMQL_TESTERを確認し、テスターモード専用のロジックを適用します。currentTimeはTimeTradeServerで算出し、range_timeに対してPeriodSecondsを使ってtimeRangeを計算、さらにtimeAfterをcurrentTime+timeRangeとして求めます。

テスターモードでは、filters_changedがtrue、またはlast_dashboard_updateがtimeAfterより前の場合に、update_dashboard_values関数をcurr_filter_selectedとimp_filter_selectedで呼び出します。同時に、last_dashboard_eventNamesをArrayFreeでクリアし、current_eventNames_dataをArrayCopyでコピーし、last_dashboard_updateをcurrentTimeに更新して、不要なリフレッシュを最小化します。ライブモードでは、連続更新のためにupdate_dashboard_valuesを直接呼び出し、両モードで最適化された選択的ダッシュボード表示を実現します。これで、使用する関数を、特に時間区分を反映するように関連変更を組み込みながら修正できる状態になりました。

//+------------------------------------------------------------------+
//| Load events from CSV resource                                    |
//+------------------------------------------------------------------+
bool LoadEventsFromResource() {
   string fileData = EconomicCalendarData;
   Print("Raw resource content (size: ", StringLen(fileData), " bytes):\n", fileData);
   string lines[];
   int lineCount = StringSplit(fileData, '\n', lines);
   if (lineCount <= 1) {
      Print("Error: No data lines found in resource! Raw data: ", fileData);
      return false;
   }
   ArrayResize(allEvents, 0);
   int eventIndex = 0;
   for (int i = 1; i < lineCount; i++) {
      if (StringLen(lines[i]) == 0) {
         if (debugLogging) Print("Skipping empty line ", i); // Modified: Conditional logging
         continue;
      }
      string fields[];
      int fieldCount = StringSplit(lines[i], ',', fields);
      if (debugLogging) Print("Line ", i, ": ", lines[i], " (field count: ", fieldCount, ")"); // Modified: Conditional logging
      if (fieldCount < 8) {
         Print("Malformed line ", i, ": ", lines[i], " (field count: ", fieldCount, ")");
         continue;
      }
      string dateStr = fields[0];
      string timeStr = fields[1];
      string currency = fields[2];
      string event = fields[3];
      for (int j = 4; j < fieldCount - 4; j++) {
         event += "," + fields[j];
      }
      string importance = fields[fieldCount - 4];
      string actualStr = fields[fieldCount - 3];
      string forecastStr = fields[fieldCount - 2];
      string previousStr = fields[fieldCount - 1];
      datetime eventDateTime = StringToTime(dateStr + " " + timeStr);
      if (eventDateTime == 0) {
         Print("Error: Invalid datetime conversion for line ", i, ": ", dateStr, " ", timeStr);
         continue;
      }
      ArrayResize(allEvents, eventIndex + 1);
      allEvents[eventIndex].eventDate = dateStr;
      allEvents[eventIndex].eventTime = timeStr;
      allEvents[eventIndex].currency = currency;
      allEvents[eventIndex].event = event;
      allEvents[eventIndex].importance = importance;
      allEvents[eventIndex].actual = StringToDouble(actualStr);
      allEvents[eventIndex].forecast = StringToDouble(forecastStr);
      allEvents[eventIndex].previous = StringToDouble(previousStr);
      allEvents[eventIndex].eventDateTime = eventDateTime; // Added: Store precomputed datetime
      if (debugLogging) Print("Loaded event ", eventIndex, ": ", dateStr, " ", timeStr, ", ", currency, ", ", event); // Modified: Conditional logging
      eventIndex++;
   }
   Print("Loaded ", eventIndex, " events from resource into array.");
   return eventIndex > 0;
}

ここでは、CSVリソースから過去のニュースイベントを読み込み、オフラインバックテストでの最適化されたイベント処理と選択的ログ出力を可能にします。LoadEventsFromResource関数を使用してEconomicCalendarDataをfileDataに読み込み、PrintおよびStringLen関数でサイズをログ出力します。StringSplit関数でfileDataをlinesに分割し、lineCountを確認してデータの存在を検証し、ArrayResize関数でallEventsをクリアします。

linesを反復処理し、空行はStringLenでスキップし、スキップはdebugLoggingがtrueの場合のみログ出力します。各行をStringSplitでfieldsに分解し、fieldCountを確認した上で、dateStr、timeStr、currency、event、importance、actualStr、forecastStr、previousStrを抽出し、イベントフィールドを動的に結合します。

dateStrとtimeStrをStringToTimeでeventDateTimeに変換し、効率のためallEvents[eventIndex].eventDateTimeに格納します。ArrayResizeとStringToDoubleを用いてallEventsを構築し、読み込み成功は条件付きでログ出力します。eventIndexが正の場合にtrueを返すことで、バックテスト用の堅牢なイベントデータセットを確保します。次に、保存されたイベントデータを可視化するために重要な、ダッシュボード値を更新する関数の修正をおこないます。

//+------------------------------------------------------------------+
//| Update dashboard values                                          |
//+------------------------------------------------------------------+
void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) {
   totalEvents_Considered = 0;
   totalEvents_Filtered = 0;
   totalEvents_Displayable = 0;
   ArrayFree(current_eventNames_data);

   datetime timeRange = PeriodSeconds(range_time);
   datetime timeBefore = TimeTradeServer() - timeRange;
   datetime timeAfter = TimeTradeServer() + timeRange;

   int startY = 162;

   if (MQLInfoInteger(MQL_TESTER)) {
      if (filters_changed) FilterEventsForTester(); // Added: Re-filter events if filters changed
      //---- Tester mode: Process filtered events
      for (int i = 0; i < ArraySize(filteredEvents); i++) {
         totalEvents_Considered++;
         datetime eventDateTime = filteredEvents[i].eventDateTime;
         if (eventDateTime < StartDate || eventDateTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging
            continue;
         }

         bool timeMatch = !enableTimeFilter;
         if (enableTimeFilter) {
            if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true;
            else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true;
         }
         if (!timeMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter."); // Modified: Conditional logging
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (filteredEvents[i].currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
         }
         if (!currencyMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging
            continue;
         }

         bool importanceMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event_imp == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
         }
         if (!importanceMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter."); // Modified: Conditional logging
            continue;
         }

         totalEvents_Filtered++;
         if (totalEvents_Displayable >= 11) continue;
         totalEvents_Displayable++;

         color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite;
         createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),62,startY-1,716,26+1,holder_color,1,clrNONE);

         int startX = 65;
         string news_data[ArraySize(array_calendar)];
         news_data[0] = filteredEvents[i].eventDate;
         news_data[1] = filteredEvents[i].eventTime;
         news_data[2] = filteredEvents[i].currency;
         color importance_color = clrBlack;
         if (filteredEvents[i].importance == "Low") importance_color = clrYellow;
         else if (filteredEvents[i].importance == "Medium") importance_color = clrOrange;
         else if (filteredEvents[i].importance == "High") importance_color = clrRed;
         news_data[3] = ShortToString(0x25CF);
         news_data[4] = filteredEvents[i].event;
         news_data[5] = DoubleToString(filteredEvents[i].actual, 3);
         news_data[6] = DoubleToString(filteredEvents[i].forecast, 3);
         news_data[7] = DoubleToString(filteredEvents[i].previous, 3);

         for (int k = 0; k < ArraySize(array_calendar); k++) {
            if (k == 3) {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri");
            } else {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri");
            }
            startX += buttons[k]+3;
         }

         ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1);
         current_eventNames_data[ArraySize(current_eventNames_data)-1] = filteredEvents[i].event;
         startY += 25;
      }
   } else {

      //---- Live mode: Unchanged

   }
}

フィルタリングされたニュースイベントを効率的に表示するために、update_dashboard_values関数を使用して、totalEvents_Considered、totalEvents_Filtered、totalEvents_Displayableをリセットし、ArrayFreeでcurrent_eventNames_dataをクリアします。range_timeに対してPeriodSeconds関数でtimeRangeを設定し、TimeTradeServer関数でtimeBeforeとtimeAfterを計算します。MQLInfoInteger関数でMQL_TESTERを確認し、filters_changedがtrueの場合は、以前定義したFilterEventsForTester関数を使用してfilteredEventsを更新します。MQLInfoInteger関数を使用してMQL_TESTERをチェックし、filters_changedがtrueの場合は、以前に完全に定義しFilterEventsForTester関数を使用してfilteredEventsを更新します。

filteredEventsをArraySize関数でループ処理しながら、まずtotalEvents_Consideredをインクリメントします。次に、イベントがStartDateとEndDateの範囲外であったり、enableTimeFilter、enableCurrencyFilter、enableImportanceFilterのいずれかのチェックに不合格であった場合はスキップします。ただし、スキップのログ出力はdebugLoggingがtrueのときのみおこないます。

一致するイベントが最大11件までの場合、totalEvents_Displayableをインクリメントし、createRecLabel関数を使ってDATA_HOLDERS行を描画します。さらに、createLabel関数を使い、filteredEventsのeventDateやeventなどのフィールドからnews_dataを生成し、importance_colorやarray_calendarでスタイルを整えます。また、ArrayResizeを用いてcurrent_eventNames_dataをリサイズし、イベント名を格納します。これにより、素早く分かりやすいダッシュボード表示が実現されます。テスターモードで取引をおこなうために、取引チェックおよび発注をおこなう関数を以下のように修正します。

//+------------------------------------------------------------------+
//| Check for news trade (adapted for tester mode trading)           |
//+------------------------------------------------------------------+
void CheckForNewsTrade() {
   if (!MQLInfoInteger(MQL_TESTER) || debugLogging) Print("CheckForNewsTrade called at: ", TimeToString(TimeTradeServer(), TIME_SECONDS)); // Modified: Conditional logging
   if (tradeMode == NO_TRADE || tradeMode == PAUSE_TRADING) {
      if (ObjectFind(0, "NewsCountdown") >= 0) {
         ObjectDelete(0, "NewsCountdown");
         Print("Trading disabled. Countdown removed.");
      }
      return;
   }

   datetime currentTime = TimeTradeServer();
   int offsetSeconds = tradeOffsetHours * 3600 + tradeOffsetMinutes * 60 + tradeOffsetSeconds;

   if (tradeExecuted) {
      if (currentTime < tradedNewsTime) {
         int remainingSeconds = (int)(tradedNewsTime - currentTime);
         int hrs = remainingSeconds / 3600;
         int mins = (remainingSeconds % 3600) / 60;
         int secs = remainingSeconds % 60;
         string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                               IntegerToString(mins) + "m " + IntegerToString(secs) + "s";
         if (ObjectFind(0, "NewsCountdown") < 0) {
            createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
            Print("Post-trade countdown created: ", countdownText);
         } else {
            updateLabel1("NewsCountdown", countdownText);
            Print("Post-trade countdown updated: ", countdownText);
         }
      } else {
         int elapsed = (int)(currentTime - tradedNewsTime);
         if (elapsed < 15) {
            int remainingDelay = 15 - elapsed;
            string countdownText = "News Released, resetting in: " + IntegerToString(remainingDelay) + "s";
            if (ObjectFind(0, "NewsCountdown") < 0) {
               createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrRed, clrBlack);
               ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
               Print("Post-trade reset countdown created: ", countdownText);
            } else {
               updateLabel1("NewsCountdown", countdownText);
               ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
               Print("Post-trade reset countdown updated: ", countdownText);
            }
         } else {
            Print("News Released. Resetting trade status after 15 seconds.");
            if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown");
            tradeExecuted = false;
         }
      }
      return;
   }

   datetime lowerBound = currentTime - PeriodSeconds(start_time);
   datetime upperBound = currentTime + PeriodSeconds(end_time);
   if (debugLogging) Print("Event time range: ", TimeToString(lowerBound, TIME_SECONDS), " to ", TimeToString(upperBound, TIME_SECONDS)); // Modified: Conditional logging

   datetime candidateEventTime = 0;
   string candidateEventName = "";
   string candidateTradeSide = "";
   int candidateEventID = -1;

   if (MQLInfoInteger(MQL_TESTER)) {
      //---- Tester mode: Process filtered events
      int totalValues = ArraySize(filteredEvents);
      if (debugLogging) Print("Total events found: ", totalValues); // Modified: Conditional logging
      if (totalValues <= 0) {
         if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown");
         return;
      }

      for (int i = 0; i < totalValues; i++) {
         datetime eventTime = filteredEvents[i].eventDateTime;
         if (eventTime < lowerBound || eventTime > upperBound || eventTime < StartDate || eventTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int k = 0; k < ArraySize(curr_filter_selected); k++) {
               if (filteredEvents[i].currency == curr_filter_selected[k]) {
                  currencyMatch = true;
                  break;
               }
            }
            if (!currencyMatch) {
               if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging
               continue;
            }
         }

         bool impactMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_selected); k++) {
               if (event_imp == imp_filter_selected[k]) {
                  impactMatch = true;
                  break;
               }
            }
            if (!impactMatch) {
               if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to impact filter."); // Modified: Conditional logging
               continue;
            }
         }

         bool alreadyTriggered = false;
         for (int j = 0; j < ArraySize(triggeredNewsEvents); j++) {
            if (triggeredNewsEvents[j] == i) {
               alreadyTriggered = true;
               break;
            }
         }
         if (alreadyTriggered) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " already triggered a trade. Skipping."); // Modified: Conditional logging
            continue;
         }

         if (tradeMode == TRADE_BEFORE) {
            if (currentTime >= (eventTime - offsetSeconds) && currentTime < eventTime) {
               double forecast = filteredEvents[i].forecast;
               double previous = filteredEvents[i].previous;
               if (forecast == 0.0 || previous == 0.0) {
                  if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast or previous value is empty."); // Modified: Conditional logging
                  continue;
               }
               if (forecast == previous) {
                  if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast equals previous."); // Modified: Conditional logging
                  continue;
               }
               if (candidateEventTime == 0 || eventTime < candidateEventTime) {
                  candidateEventTime = eventTime;
                  candidateEventName = filteredEvents[i].event;
                  candidateEventID = i;
                  candidateTradeSide = (forecast > previous) ? "BUY" : "SELL";
                  if (debugLogging) Print("Candidate event: ", filteredEvents[i].event, " with event time: ", TimeToString(eventTime, TIME_SECONDS), " Side: ", candidateTradeSide); // Modified: Conditional logging
               }
            }
         }
      }
   } else {

      //---- Live mode: Unchanged

   }
}

テスターモードでニュース駆動の取引を評価・実行し、効率的なバックテストのためにイベントフィルタリングと選択的ログ出力を最適化する際、まずCheckForNewsTrade関数を起動します。この実行ログはdebugLoggingがtrueの場合のみ、Print関数とTimeToString、およびTimeTradeServerによる現在時刻を用いて出力し、テスターのログをクリーンに保ちます。tradeModeがNO_TRADEまたはPAUSE_TRADINGの場合は処理を終了し、ObjectFind関数でNewsCountdownを確認し、存在すればObjectDelete関数で削除し、Printでログを残します。その後、TimeTradeServer からcurrentTimeを算出し、tradeOffsetHours、tradeOffsetMinutes、tradeOffsetSecondsを組み合わせてoffsetSecondsを求め、取引後状態を管理します。

tradeExecutedがtrueの場合、tradedNewsTimeに基づいてカウントダウンタイマーを制御し、IntegerToString関数を用いて残り時間やリセット遅延を表すcountdownTextを整形します。 ObjectFindの結果に応じてcreateButton1またはupdateLabel1を呼び出し、ObjectSetInteger関数でスタイルを設定しつつ、Printでログ出力します。さらに、15秒後にObjectDeleteとPrintによってカウントダウンを削除しtradeExecutedをリセットします。

テスターモードはMQLInfoInteger関数でMQL_TESTERを確認して検出します。filteredEventsをArraySize関数で走査し、totalValuesを取得して条件付きでPrintします。空の場合はNewsCountdownをクリアして終了します。start_timeとend_timeをTimeTradeServerとPeriodSecondsから計算し、lowerBoundとupperBoundを設定し、debugLoggingが有効な場合は範囲をPrintで出力します。また、取引候補選択用にcandidateEventTime、candidateEventName、candidateEventID、candidateTradeSideを初期化します。

filteredEventsをループ処理し、lowerBoundとupperBoundの範囲外、StartDateとEndDateの範囲外、enableCurrencyFilterとcurr_filter_selectedの不一致、enableImportanceFilterとimp_filter_selectedの不一致といった条件でイベントをスキップします。スキップのログはdebugLoggingがtrueの場合のみPrintで残します。さらに、ArraySizeでtriggeredNewsEventsを確認し、すでに取引済みのイベントは除外し、これも条件付きでログを出力します。

TRADE_BEFOREモードでは、eventDateTimeのoffsetSeconds前の範囲内にあるイベントを対象とし、forecastとpreviousを検証します。最も早いイベントをcandidateEventTime、candidateEventName、candidateEventIDに格納し、forecastがpreviousを上回る場合はBUY、そうでなければSELLをcandidateTradeSideに設定します。この処理もdebugLoggingがtrueの場合のみPrintで出力します。これにより、最小限のログ出力で効率的な取引判断が可能となります。ライブモードの残りのロジックは変更せず、コンパイルすると、取引確認が可視化されます。

取引確認GIF

画像から分かるように、データの取得、フィルタリング、ダッシュボードへの反映、指定時間範囲に達した際のカウントダウン初期化、ニュースイベント取引の実行という一連の流れを再現できています。これにより、ライブモードの取引環境とまったく同じ挙動をシミュレーションでき、統合の目的が達成されました。残されているのは、システムを徹底的にバックテストすることです。その内容は次のセクションで扱います。


テストと検証

プログラムのテストは、まずライブ環境にロードし、必要なニュースイベントデータをダウンロードすることから始めます。その後、MetaTrader 5ストラテジーテスターを使い、StartDateを2025.03.01、EndDateを2025.03.21に設定し、debugLoggingを無効化します。さらに、EconomicCalendarData内のComma-Separated Values (CSV)ファイルを用いて、CheckForNewsTradeを介しfilteredEvents上での取引をシミュレートします。この後にあるGIFがダッシュボードを示しています。テストでは、filters_changedまたはlast_dashboard_updateがトリガーされたときのみupdate_dashboard_valuesによってダッシュボードが更新され、createLabelでフィルタリング済みイベントが描画されます。ログは取引と更新に関するものだけが出力され、クリーンな状態が保たれます。さらに、ライブモードでCalendarValueHistory関数を用いたテストをおこない、同じ可視化が確認されました。これにより、両モードでプログラムの高速かつ明確なパフォーマンスが検証されました。これが視覚化です。

最終GIF


結論

連載「MQL5経済指標カレンダーを使った取引」をさらに進化させ、スマートなイベントフィルタリングと効率的なログ出力によってバックテストを最適化しました。これにより、シームレスな実取引機能を維持しながら、戦略検証を迅速かつ明確におこなうことが可能になりました。.これにより、シームレスな実取引機能を維持しながら、戦略検証を迅速かつ明確におこなうことが可能になりました。この進歩は、効率的なオフラインテストとリアルタイムのイベント分析をつなぐものであり、テスト時の可視化でも示したように、ニュース駆動型戦略の洗練に役立つ強力なツールを提供します。さらに、この仕組みを基盤として、各自の取引ニーズに合わせて拡張することも可能です。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17999

添付されたファイル |
データサイエンスとML(第39回):ニュース × 人工知能、それに賭ける価値はあるか データサイエンスとML(第39回):ニュース × 人工知能、それに賭ける価値はあるか
ニュースは金融市場を動かす力を持っており、特に非農業部門雇用者数(NFP)のような主要指標の発表は大きな影響を与えます。私たちは、単一のヘッドラインが急激な価格変動を引き起こす様子を何度も目にしてきました。本記事では、ニュースデータと人工知能(AI)の強力な融合について探っていきます。
MQL5での取引戦略の自動化(第17回):ダイナミックダッシュボードで実践するグリッドマーチンゲールスキャルピング戦略 MQL5での取引戦略の自動化(第17回):ダイナミックダッシュボードで実践するグリッドマーチンゲールスキャルピング戦略
本記事では、グリッドマーチンゲールスキャルピング戦略(Grid-Mart Scalping Strategy)を探究し、MQL5による自動化と、リアルタイム取引インサイトを提供するダイナミックダッシュボードの構築をおこないます。本戦略のグリッド型マーチンゲールロジックとリスク管理機能を詳述し、さらに堅牢なパフォーマンスのためのバックテストおよび実運用展開についても案内します。
データサイエンスとML(第40回):機械学習データにおけるフィボナッチリトレースメントの利用 データサイエンスとML(第40回):機械学習データにおけるフィボナッチリトレースメントの利用
フィボナッチリトレースメントはテクニカル分析で人気のツールであり、トレーダーが潜在的な反転ゾーンを特定するのに役立ちます。本記事では、これらのリトレースメントレベルを機械学習モデルの目的変数に変換し、この強力なツールを使用して市場をより深く理解できるようにする方法について説明します。
データサイエンスとML(第38回):外国為替市場におけるAI転移学習 データサイエンスとML(第38回):外国為替市場におけるAI転移学習
AIの画期的な進歩、たとえばChatGPTや自動運転車などは、単独のモデルから生まれたわけではなく、複数のモデルや共通の分野から得られた累積的な知識を活用することで実現しています。この「一度学習した知識を他に応用する」というアプローチは、アルゴリズム取引におけるAIモデルの変革にも応用可能です。本記事では、異なる金融商品の情報を活用し、他の銘柄における予測精度向上に役立てる方法として、転移学習の活用方法について解説します。