
Handel mit dem MQL5 Wirtschaftskalender (Teil 8): Optimierung des nachrichtengesteuerten Backtests mit intelligenter Ereignisfilterung und gezielten Protokollen
Einführung
In diesem Artikel treiben wir die Serie über den MQL5-Wirtschaftskalender voran, indem wir unser Handelssystem für blitzschnelles, visuell intuitive Backtests optimieren und die Datenvisualisierung sowohl für den Live- als auch für den Offline-Modus nahtlos integrieren, um die Entwicklung nachrichtenorientierter Strategien zu verbessern. Aufbauend auf der Grundlage der ressourcenbasierten Ereignisanalyse von Teil 7 für die Kompatibilität mit Strategy Tester führen wir jetzt intelligente Ereignisfilter und gezielte Protokollierung ein, um die Leistung zu optimieren und sicherzustellen, dass wir Strategien in Echtzeit- und historischen Umgebungen mit minimaler Unordnung effizient visualisieren und testen können. Wir gliedern den Artikel in die folgenden Themen:
- Ein visueller Chronograph für reibungslosen, nachrichtengesteuerten Handel über Live- und Offline-Bereiche hinweg
- Implementierung in MQL5
- Tests und Validierung
- Schlussfolgerung
Lassen Sie uns diese Fortschritte erkunden!
Ein visueller Chronograph für reibungslosen, nachrichtengesteuerten Handel über Live- und Offline-Bereiche hinweg
In diesem Teil der Serie stellen wir einen visuellen Chronographen vor - eine Metapher für unser optimiertes System zur Verarbeitung und Protokollierung von Ereignissen - der es uns ermöglichen wird, die zeitliche Landschaft des nachrichtengesteuerten Handels mit Präzision und Effizienz zu navigieren.
Durch die Implementierung einer intelligenten Ereignisfilterung werden wir den Rechenaufwand im Strategy Tester drastisch reduzieren, indem wir nur die relevantesten Nachrichtenereignisse innerhalb eines nutzerdefinierten Datumsbereichs vorselektieren, was gewährleistet, dass die Backtests die Geschwindigkeit und Klarheit des Live-Handels widerspiegelt. Dieser Filtermechanismus, der der präzisen Zeitmessung eines Chronographen ähnelt, wird es uns ermöglichen, uns auf kritische Ereignisse zu konzentrieren, ohne irrelevante Daten zu durchsuchen, und ermöglicht einen nahtlosen Übergang zwischen historischen Simulationen und Echtzeit-Marktanalysen.
Ergänzend dazu wird unser zielgerichtetes Protokollierungssystem als Anzeige des Chronographen fungieren und nur wesentliche Informationen - wie Handelsausführungen und Dashboard-Updates - anzeigen, während überflüssige Protokolle ausgeblendet werden, sodass sowohl im Live- als auch im Offline-Modus eine übersichtliche, ablenkungsfreie Oberfläche erhalten bleibt. Diese duale Visualisierungsfähigkeit stellt sicher, dass wir Strategien mit historischen Daten im Strategy Tester testen und dasselbe intuitive Dashboard im Live-Handel anwenden können. Dadurch wird ein einheitlicher Arbeitsablauf gefördert, der die Entscheidungsfindung und die Verfeinerung von Strategien unter allen Marktbedingungen verbessert. Hier ist eine Visualisierung dessen, was wir erreichen wollen.
Implementierung in MQL5
Um die Verbesserungen in MQL5 vorzunehmen, müssen wir zunächst einige Variablen deklarieren, die wir verwenden werden, um die heruntergeladenen Ereignisse zu verfolgen, die wir dann problemlos im Nachrichten-Dashboard anzeigen werden, wobei wir ein ähnliches Format verwenden, wie wir es in den vorherigen Artikeln beim Live-Handel getan haben, aber zunächst die Ressource einschließen, in der wir die Daten wie untenstehend speichern.
//---- Include trading library #include <Trade\Trade.mqh> CTrade trade; //---- Define resource for CSV #resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData
Wir beginnen mit der Integration einer Handelsbibliothek, die eine nahtlose Handelsausführung im Live- und Offline-Modus ermöglicht. Wir verwenden die Direktive „#include <Trade\Trade.mqh>“, um die MQL5-Handelsbibliothek einzubinden, die die Klasse „CTrade“ für die Verwaltung von Handelsoperationen bereitstellt. Indem wir ein Objekt vom Typ „CTrade“ mit dem Namen „trade“ deklarieren, ermöglichen wir dem Programm, Kauf- und Verkaufsaufträge programmatisch auszuführen.
Anschließend verwenden wir die Richtlinie „#resource“, um „\Files\Database\EconomicCalendar.csv“ als String-Ressource mit dem Namen „EconomicCalendarData“ zu definieren. Diese durch Kommas getrennten Werte (CSV), die über die Funktion „LoadEventsFromResource“ geladen werden, liefern Ereignisdetails wie Datum, Uhrzeit, Währung und Prognose und bieten eine einheitliche Datendarstellung ohne Abhängigkeit von Live-Datenfeeds. Nun können wir die restlichen Kontrollvariablen definieren.
//---- 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[];
Hier werden die Ereignisnamen in „current_eventNames_data“, „previous_eventNames_data“ und „last_dashboard_eventNames“ gespeichert, wobei „last_dashboard_eventNames“ zur Zwischenspeicherung von Dashboard-Aktualisierungen im Tester-Modus und „last_dashboard_update“ zur Planung von Aktualisierungen nur bei Bedarf verwendet wird, um redundante Verarbeitung zu vermeiden.
Wir schalten die Ereignisfilterung mit „enableCurrencyFilter“, „enableImportanceFilter“, „enableTimeFilter“ und „filters_changed“ ein und setzen die Filter zurück, wenn „filters_changed“ wahr ist, um nur relevante Ereignisse zu verarbeiten und „debugLogging“ unter „sinput group ‚General Calendar Settings‘“ zu verwenden, um nur Trades und Updates zu protokollieren.
Wir definieren den Backtests-Zeitraum mit „StartDate“ und „EndDate“ unter „sinput group ‚Strategy Tester CSV Settings‘“, strukturieren Ereignisse in „EconomicEvent“ mit „eventDateTime“ für einen schnellen Zugriff, und filtern „allEvents“ in „filteredEvents“ für eine schnellere Bearbeitung, während wir „tradeMode“ und die zugehörigen Variablen für eine effiziente Ausführung von Handelsgeschäfte setzen. Jetzt können wir den Testzeitraum wählen, von dem wir die Daten herunterladen und denselben Zeitbereich für den Test verwenden wollen. Dies ist die Nutzeroberfläche, die wir haben.
Aus dem Bild ist ersichtlich, dass wir über zusätzliche Eingänge verfügen, um die Anzeige der Ereignisse im Testermodus zu steuern, sowie über kontrollierte Aktualisierungen der Uhrzeit im Panel und der Protokollierung. Wir haben dies getan, um unnötige Ressourcen beim Backtests zu optimieren. Nun müssen wir eine Funktion definieren, die die Filterung der Testereignisse vornimmt.
//+------------------------------------------------------------------+ //| 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; }
Hier implementieren wir eine intelligente Ereignisfilterung, um die Backtests zu beschleunigen, indem wir die Anzahl der im Strategy Tester verarbeiteten Nachrichtenereignisse reduzieren. Wir verwenden die Funktion „FilterEventsForTester“, um das Array „filteredEvents“ mit der Funktion ArrayResize zu leeren und es mit den relevanten Ereignissen aus „allEvents“ neu zu füllen. Für jedes Ereignis wird die „eventDateTime“ mit „StartDate“ und „EndDate“ verglichen, wobei diejenigen, die außerhalb des Bereichs liegen, übersprungen werden, und nur dann mit Print protokolliert, wenn „debugLogging“ wahr ist.
Wir kopieren qualifizierte Ereignisse in „filteredEvents“ mit dem Index „eventIndex“, wobei wir diesen bei jeder Hinzufügung erhöhen, und verwenden die Funktion „ArrayResize“, um dynamisch Platz zuzuweisen. Wir protokollieren die Gesamtzahl der „eventIndex“-Ereignisse über „Print“ nur, wenn „debugLogging“ aktiviert ist, um die Ausgabe des Testers sauber zu halten, und setzen „filters_changed“ auf false, um zu signalisieren, dass die Filterung abgeschlossen ist. Durch diese gezielte Filterung wird der Ereignissatz verkleinert, was die nachfolgende Verarbeitung beschleunigt und eine effiziente Visualisierung von Nachrichtenereignissen im Offline-Modus ermöglicht. Wir rufen diese Funktion dann in OnInit auf, um die Nachrichtendaten vorzufiltern.
//+------------------------------------------------------------------+ //| 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); }
Wir verwenden die Funktion „createRecLabel“, um die Dashboard-Panels „MAIN_REC“, „SUB_REC1“ und „SUB_REC2“ mit unterschiedlichen Farben und Größen zu erstellen, und die Funktion „createLabel“, um ein „HEADER_LABEL“ hinzuzufügen, das „MQL5 Economic Calendar“ anzeigt, wie wir es zuvor getan haben. Wir erstellen Kalenderschaltflächen dynamisch aus „array_calendar“ mit den Funktionen „createButton“ und ArraySize und positionieren sie mit „startX“ und „buttons“ für die Ereignisanzeige.
Wir bereiten den Live-Modus vor, indem wir Ereignisse mit der Funktion CalendarValueHistory in „values“ abrufen, wobei „startTime“ und „endTime“ über TimeTradeServer und PeriodSeconds berechnet werden, und für den Testermodus verwenden wir die Funktion MQLInfoInteger, um MQL_TESTER zu prüfen, und laden „EconomicCalendarData“ mit der Funktion „LoadEventsFromResource“ in „allEvents“. Wir verwenden die Funktion „FilterEventsForTester“, die hier am wichtigsten ist, um „filteredEvents“ zu füllen und die Ereignisverarbeitung zu optimieren.
Wir fügen UI-Elemente wie „TIME_LABEL“, „IMPACT_LABEL“ und „FILTER_LABEL“ mit „createLabel“ und Filterschaltflächen „FILTER_CURR_BTN“, „FILTER_IMP_BTN“, „FILTER_TIME_BTN“ und „CANCEL_BTN“ mit „createButton“ und ObjectSetInteger, wobei Zustände wie „filter_curr_state“ auf der Grundlage von „enableCurrencyFilter“ gesetzt werden. Wir erstellen Schaltflächen für Auswirkungen und Währungen aus „impact_labels“ und „curr_filter“ mit „createButton“, initialisieren die Filter „curr_filter_selected“ und „imp_filter_selected“ mit ArrayFree und ArrayCopy, und aktualisieren das Dashboard mit „update_dashboard_values“ und ChartRedraw, wobei „INIT_SUCCEEDED“ zurückgegeben wird, um die Einrichtung zu bestätigen. Wenn wir nun das Programm initialisieren, erhalten wir folgendes Ergebnis.
Da wir nun die relevanten Daten nach der Filterung in OnTick laden können, müssen wir sicherstellen, dass wir die relevanten Daten innerhalb eines bestimmten Zeitraums erhalten und sie in das Dashboard einfügen, anstatt nur alle Daten, so wie wir es im Live-Modus tun. Hier ist die Logik, die wir anwenden, und bevor wir es vergessen, haben wir den spezifischen und wichtigen Aktualisierungsabschnitten, in denen wir die Änderungen vorgenommen haben, entsprechende Kommentare hinzugefügt.
//+------------------------------------------------------------------+ //| 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); } } }
In OnTick verwenden wir die Funktion „UpdateFilterInfo“, um die Filtereinstellungen zu aktualisieren, und die Funktion „CheckForNewsTrade“, um Trades auf der Grundlage von Nachrichtenereignissen zu bewerten und auszuführen. Wenn „isDashboardUpdate“ wahr ist, prüfen wir MQL_TESTER mit der Funktion MQLInfoInteger, um testerspezifische Logik anzuwenden, wobei „currentTime“ mit TimeTradeServer, „timeRange“ mit PeriodSeconds auf „range_time“ und „timeAfter“ als „currentTime“ plus „timeRange“ berechnet wird.
Im Testmodus verwenden wir die Bedingung „filters_changed“ oder „last_dashboard_update“ kleiner als „timeAfter“, um die „update_dashboard_values“ Funktion mit „curr_filter_selected“ und „imp_filter_selected“ auszulösen, „last_dashboard_eventNames“ mit der Funktion ArrayFree zu löschen, „current_eventNames_data“ mit ArrayCopy dorthin zu kopieren und „last_dashboard_update“ auf „currentTime“ zu aktualisieren, wodurch Auffrischungen minimiert werden. Im Live-Modus rufen wir direkt „update_dashboard_values“ für kontinuierliche Aktualisierungen auf und gewährleisten so eine optimierte, zielgerichtete Dashboard-Visualisierung in beiden Modi. Wir können nun die Funktionen, die wir verwenden, wie folgt ändern und sicherstellen, dass sie die entsprechenden Änderungen, insbesondere die Zeiteinteilung, enthalten.
//+------------------------------------------------------------------+ //| 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; }
Hier laden wir historische Nachrichtenereignisse aus einer CSV-Ressource, um Offline-Backtests mit optimierter Ereignisbehandlung und gezielter Protokollierung zu ermöglichen. Wir verwenden die Funktion „LoadEventsFromResource“, um „EconomicCalendarData“ in „fileData“ einzulesen, und protokollieren seine Größe mit den Funktionen Print und StringLen. Wir teilen „fileData“ mit der Funktion StringSplit in „lines“ auf, überprüfen „lineCount“, um sicherzustellen, dass Daten vorhanden sind, und löschen „allEvents“ mit der Funktion ArrayResize.
Wir iterieren durch „Zeilen“, überspringen leere Zeilen mit der Funktion „StringLen“ und protokollieren nur, wenn „debugLogging“ wahr ist. Wir verwenden „StringSplit“, um jede Zeile in „fields“ zu zerlegen, „fieldCount“ zu überprüfen und „dateStr“, „timeStr“, „currency“, „event“, „importance“, „actualStr“, „forecastStr“ und „previousStr“, wobei die Ereignisfelder dynamisch kombiniert werden.
Wir konvertieren „dateStr“ und „timeStr“ in „eventDateTime“ mit der Funktion StringToTime und speichern sie in „allEvents[eventIndex].eventDateTime“, füllen „allEvents“ mit „ArrayResize“ und StringToDouble auf, protokollieren erfolgreiche Ladevorgänge bedingt und geben true zurück, wenn „eventIndex“ positiv ist, um einen robusten Ereignisdatensatz für Backtests zu gewährleisten. Jetzt aktualisieren wir noch die Funktion, die für die Aktualisierung der Dashboard-Werte verantwortlich ist, die für die Visualisierung der gespeicherten Ereignisdaten entscheidend ist (siehe unten).
//+------------------------------------------------------------------+ //| 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 } }
Um gefilterte Nachrichtenereignisse effizient anzuzeigen, verwenden wir die Funktion „update_dashboard_values“, um „totalEvents_Considered“, „totalEvents_Filtered“, „totalEvents_Displayable“ zurückzusetzen und „current_eventNames_data“ mit der Funktion ArrayFree zurückzusetzen, „timeRange“ über die Funktion PeriodSeconds auf „range_time“ zu setzen und „timeBefore“ und „timeAfter“ mit TimeTradeServer zu berechnen. Wir prüfen „MQL_TESTER“ mit der Funktion MQLInfoInteger und verwenden, wenn „filters_changed“ wahr ist, die Funktion „FilterEventsForTester“, die wir zuvor vollständig definiert hatten, um „filteredEvents“ zu aktualisieren.
Wir iterieren durch „filteredEvents“ mit der Funktion ArraySize, erhöhen „totalEvents_Considered“ und überspringen Ereignisse außerhalb von „StartDate“ oder „EndDate“ liegen oder die Prüfungen „enableTimeFilter“, „enableCurrencyFilter“ oder „enableImportanceFilter“ nicht bestanden haben, wird die Protokollierung nur übersprungen, wenn „debugLogging“ wahr ist.
Für bis zu 11 übereinstimmende Ereignisse erhöhen wir „totalEvents_Displayable“, verwenden die „createRecLabel“-Funktion, um „DATA_HOLDERS“-Zeilen zu zeichnen, und verwenden die „createLabel“-Funktion, um „news_data“ aus „filteredEvents“ aufzufüllen Felder wie „eventDate“ und „event“, gestylt mit „importance_color“ und „array_calendar“, Größenänderung von „current_eventNames_data“ mit ArrayResize zum Speichern von Ereignisnamen, um eine schnelle, klare Dashboard-Visualisierung zu gewährleisten. Um im Testmodus zu handeln, ändern wir die Funktion, die für die Prüfung und die Eröffnung von Handelsgeschäften verantwortlich ist, wie folgt.
//+------------------------------------------------------------------+ //| 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 } }
Um nachrichtengesteuerte Handelsgeschäfte im Testermodus mit optimierter Ereignisfilterung und gezielter Protokollierung für effiziente Backtests zu bewerten und auszulösen, verwenden wir die Funktion „CheckForNewsTrade“ zum Starten und protokollieren ihre Ausführung nur, wenn „debugLogging“ mit der Funktion Print, TimeToString und „TimeTradeServer“ für den aktuellen Zeitstempel wahr ist, um die Testerprotokolle sauber zu halten. Wir beenden den Handel, wenn „tradeMode“ „NO_TRADE“ oder „PAUSE_TRADING“ ist, suchen mit der Funktion ObjectFind nach „NewsCountdown“ und entfernen es mit ObjectDelete, während wir über „Print“ protokollieren und die Verwaltung der Zustände nach einem Handel durch Berechnung von „currentTime“ mit TimeTradeServer und „offsetSeconds“ aus „tradeOffsetHours“, „tradeOffsetMinutes“ und „tradeOffsetSeconds“.
Wenn „tradeExecuted“ wahr ist, verarbeiten wir Countdown-Timer für „tradedNewsTime“, formatieren „countdownText“ mit IntegerToString, um die verbleibende Zeit anzuzeigen oder die Verzögerung zurückzusetzen, erstellen oder aktualisieren „NewsCountdown“ mit „createButton1“ oder „updateLabel1“ auf der Grundlage von „ObjectFind“, die Formatierung mit ObjectSetInteger und die Protokollierung über „Print“, das Zurücksetzen von „tradeExecuted“ nach 15 Sekunden mit „ObjectDelete“ und „Print“.
Im Testermodus, bestätigt durch MQLInfoInteger, das MQL_TESTER prüft, verarbeiten wir „filteredEvents“ mit ArraySize, um „totalValues“ zu erhalten, protokollieren es bedingt mit „Print“ und beenden, wenn leer, nachdem wir „NewsCountdown“ gelöscht haben. Wir setzen „lowerBound“ und „upperBound“ mit „TimeTradeServer“ und PeriodSeconds auf „start_time“ und „end_time“, protokollieren den Bereich mit „Print“ wenn „debugLogging“ wahr ist, und initialisieren „candidateEventTime“, „candidateEventName“, „candidateEventID“ und „candidateTradeSide“ für die Handelsauswahl.
Wir iterieren über „filteredEvents“, wobei wir Ereignisse außerhalb von „lowerBound“, „upperBound“, „StartDate“ oder „EndDate“ überspringen, oder wenn „enableCurrencyFilter“ gegen „curr_filter_selected“ oder „enableImportanceFilter“ gegen „imp_filter_selected“ unter Verwendung von „ArraySize“, wobei Auslassungen nur dann über Print protokolliert werden, wenn „debugLogging“ aktiviert ist. Wir verwenden ArraySize auf „triggeredNewsEvents“, um gehandelte Ereignisse auszuschließen und bedingt zu protokollieren.
Für den Modus „TRADE_BEFORE“ suchen wir nach Ereignissen innerhalb von „offsetSeconds“ vor „eventDateTime“, validieren „forecast“ und „previous“ und wählen das früheste Ereignis in „candidateEventTime“, „candidateEventName“, „candidateEventID“ und „candidateTradeSide“ („BUY“, wenn „forecast“ „previous“ übersteigt, sonst „SELL“), wobei die Protokollierung mit „Print“ erfolgt, wenn „debugLogging“ wahr ist, was effiziente Handelsentscheidungen mit minimaler Protokollierung gewährleistet. Der Rest der Logik des Live-Modus bleibt unverändert. Nach der Zusammenstellung erhalten wir die folgende Visualisierung der Handelsbestätigung.
Aus dem Bild können wir ersehen, dass wir die Daten abrufen, sie filtern und in das Dashboard einfügen können, Countdowns initialisieren können, wenn der entsprechende Zeitbereich bestimmter Daten erreicht wird, und das Nachrichtenereignis handeln können, wodurch wir genau das simulieren, was wir in der Handelsumgebung im Live-Modus haben, und somit unser Integrationsziel erreichen. Was jetzt noch bleibt, sind gründliche Backtests des Systems, das im nächsten Abschnitt behandelt wird.
Tests und Validierung
Wir testen das Programm, indem wir es zunächst in einer Live-Umgebung laden, die gewünschten Daten zu den Nachrichtenereignissen herunterladen und es im MetaTrader 5 Strategy Tester ausführen, wobei „StartDate“ auf „2025.03.01', „EndDate“ auf '2025.03.21', und „debugLogging“ deaktiviert, unter Verwendung einer Datei mit durch Kommas getrennten Werte (CSV) in „EconomicCalendarData“, um Trades über „CheckForNewsTrade“ auf „filteredEvents“ zu simulieren. Ein GIF zeigt das Dashboard, das durch „update_dashboard_values“ nur dann aktualisiert wird, wenn „filters_changed“ oder „last_dashboard_update“ ausgelöst wird, und das gefilterte Ereignisse mit „createLabel“ und saubere Protokolle von Trades und Updates anzeigt. Tests im Live-Modus mit der Funktion CalendarValueHistory bestätigen die identische Darstellung und bestätigen die schnelle und klare Leistung des Programms in beiden Modi. Hier ist die Visualisierung.
Schlussfolgerung
Zusammenfassend lässt sich sagen, dass wir die Serie mit dem MQL5-Wirtschaftskalender durch die Optimierung des Backtests mit intelligenter Ereignisfilterung und optimierter Protokollierung verbessert haben, was eine schnelle und klare Strategievalidierung ermöglicht, während die nahtlosen Live-Handelsfunktionen erhalten bleiben. Dieser Fortschritt verbindet effiziente Offline-Tests mit der Echtzeit-Ereignisanalyse und bietet uns ein robustes Tool zur Verfeinerung von nachrichtengesteuerten Strategien, wie in unserer Testvisualisierung zu sehen ist. Sie können es als Grundgerüst verwenden und es weiter ausbauen, um Ihre spezifischen Handelsanforderungen zu erfüllen.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17999
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.