Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
Введение
Обработка экономических новостей в MQL5 обычно выполняется одним из двух способов: новости либо полностью игнорируются, либо применяются фиксированные торговые ограничения. Хотя ручной временной фильтр может учитывать запланированную публикацию, такую как Non-Farm Payrolls, он не способен справиться с внеплановой волатильностью, экстренными заседаниями центральных банков или неожиданными пересмотрами данных. Такой подход делает советник уязвимым к расширению спреда и проскальзыванию, которые возникают за пределами заранее заданных временных окон. В результате показатели стратегии в реальной торговле зачастую страдают из-за проблем с исполнением, которых не было во время первоначального бэктеста.
В данной статье представлен модуль фильтрации новостей, разработанный с использованием только встроенных функций календаря. Модуль приостанавливает торговлю перед событиями высокой значимости, автоматически возобновляет её по истечении настраиваемой задержки после публикации данных и предоставляет дополнительную функцию, которая уменьшает объём позиции вместо полной блокировки торговли в дни с несколькими запланированными событиями. Сопутствующий скрипт-логгер генерирует новостной календарь на основе CSV, который фильтр может считывать в Тестере стратегий, где API календаря в реальном времени недоступен.
Раздел 1: API календаря MetaTrader 5
1.1 Что доступно и откуда поступают данные
Компания MetaQuotes предоставляет встроенный экономический календарь в режиме реального времени для MetaTrader 5. Он охватывает запланированные экономические события более чем в 60 странах, включая важные заявления центральных банков, отчёты по занятости, данные по инфляции, публикации показателей ВВП, публикации индексов деловой активности (PMI) и данные по торговому балансу. Данные обновляются в режиме реального времени по мере планирования, пересмотра или выхода событий.
В коде MQL5 доступ к этим данным обеспечивают шесть функций. В таблице 1 ниже приведено их краткое описание с указанием типов возвращаемых значений и основных сценариев использования. На практике модуль использует функцию CalendarEventByCountry() для получения событий по коду страны. Затем с помощью функции CalendarValueHistoryByEvent() загружаются метки времени запланированных событий. Эта логика используется как для API в реальном времени, так и для сопутствующего скрипта-логгера.
| Функция | Возвращает | Вариант использования |
|---|---|---|
| CalendarCountries() | Массив стран | Получить список всех стран, экономические события которых включены в календарь |
| CalendarEventByCountry() | Массив событий | Получить все запланированные события для определённого кода страны |
| CalendarEventById() | Одно событие | Получить полное описание одного события по его уникальному идентификатору |
| CalendarValueById() | Одно значение | Получить последнее значение опубликованного показателя для конкретного события |
| CalendarValueHistoryByEvent() | Массив значений | Получить все исторические значения для одного события за указанный диапазон дат |
| CalendarValueLast() | Массив значений | Получить новые события, начиная с указанного идентификатора изменения — идеально подходит для опроса в режиме реального времени |
Таблица 1 — шесть функций API календаря MQL5, доступных в MetaTrader 5 сборки 2265 и более поздних. Все шесть функций бесплатны, не требуют внешнего подключения, кроме стандартного сервера MetaTrader 5, и работают на любом демо- или реальном счёте. Ни одна из них не работает в Тестере стратегий, поскольку во время воспроизведения исторических данных у тестера отсутствует подключение к серверу.
Эти функции работают с использованием двух основных структур данных:
- MqlCalendarEvent: Описывает тип запланированного события — его название, страну, валюту, уровень важности и уникальный идентификатор.
- MqlCalendarValue: Описывает один конкретный экземпляр данного события — временную метку, прогнозное значение, предыдущее значение и фактическое значение после публикации.
С одной записью MqlCalendarEvent может быть связано несколько десятков записей MqlCalendarValue — по одной для каждой исторической публикации показателя.
1.2 Структура MqlCalendarEvent
Для создания эффективного фильтра важно понимать, какие поля доступны в MqlCalendarEvent. Наиболее важными полями для фильтрации новостей являются importance, country_code, event_currency и type. Поле importance принимает одно из трёх значений, соответствующих низкому, среднему и высокому влиянию. Именно на этом фильтр основывается при принятии решения о том, приостанавливать торговлю или нет.
//+------------------------------------------------------------------+ //| MqlCalendarEvent key fields used by the news filter | //+------------------------------------------------------------------+ struct MqlCalendarEvent { ulong id; ENUM_CALENDAR_EVENT_TYPE type; // Event type ENUM_CALENDAR_EVENT_SECTOR sector; // Event sector ENUM_CALENDAR_EVENT_FREQUENCY frequency; // Event frequency ENUM_CALENDAR_EVENT_TIMEMODE time_mode; // Event time mode ulong country_id; // Country ID ENUM_CALENDAR_EVENT_UNIT unit; // Event unit ENUM_CALENDAR_EVENT_IMPORTANCE importance; // Event importance ENUM_CALENDAR_EVENT_MULTIPLIER multiplier; // Event multiplier uint digits; // Event digits string country_code; // Country code string name; // Event name string event_code; // Event code string event_currency;// Event currency string event_source; // Event source string event_url; // Event URL };1.3 Уровни влияния и их значение
Поле importance является основным критерием отбора в любом новостном фильтре. В таблице 2 три константы важности MQL5 сопоставлены с их целочисленными значениями, типами событий, которые обычно имеют каждый уровень, а также рекомендуемым действием фильтра для каждого из них. Большинство стратегий фильтрации новостей сосредоточены исключительно на событиях с высоким влиянием, которые составляют лишь небольшую долю всех запланированных публикаций, но являются причиной большинства внутридневных всплесков волатильности.
| Уровень влияния | Константа MQL5 | Целое число | Типичные события | Действие фильтра |
|---|---|---|---|---|
| Низкий | CALENDAR_IMPORTANCE_LOW | 1 | Незначительные опросы, вторичные индексы | Игнорировать — приостановка торговли не требуется |
| Средний | CALENDAR_IMPORTANCE_MODERATE | 2 | ИПЦ, ИЦП, розничные продажи, PMI | Необязательная пауза — при необходимости уменьшить размер позиции |
| Высокий | CALENDAR_IMPORTANCE_HIGH | 3 | NFP, FOMC, ставки центральных банков, ВВП | Приостанавливать входы в рынок за 30 минут до и на 30 минут после |
Таблица 2 — Три уровня важности календарных событий с соответствующими константами MQL5, типичными типами событий и рекомендуемыми действиями фильтра. Фильтр NewsFilter.mqh, созданный в этой статье, по умолчанию фильтрует только события с важностью CALENDAR_IMPORTANCE_HIGH, при этом предусмотрен дополнительный параметр, позволяющий также блокировать события со средним уровнем влияния.
Примечание: Компания MetaQuotes присваивает уровень важности на основе собственной оценки типичного рыночного влияния каждого события. Эта классификация иногда может отличаться от ожиданий трейдеров. Некоторые решения региональных центральных банков помечаются как события среднего уровня, хотя для отдельных валютных пар они могут иметь высокий уровень влияния. Фильтр позволяет вручную переопределять минимальный порог значимости с помощью входного параметра, поэтому трейдеры могут задать для него CALENDAR_IMPORTANCE_MODERATE для более консервативной работы или HIGH для минимального количества прерываний.
Раздел 2: Сопоставление символов с релевантными валютами
2.1 Сопоставление символов со странами
API календаря группирует события по кодам стран, а не по торговым символам. Чтобы определить, какие события относятся к EURUSD, фильтр должен проверять события как по EU (влияющие на EUR), так и по US (влияющие на USD). Для USDJPY он отслеживает США и Японию. Для XAUUSD основной страной для мониторинга является US, поскольку золото номинировано в USD, а большинство ключевых событий, влияющих на золото, — это публикации данных из США, хотя глобальное событие в режиме ухода от риска, затрагивающее несколько валют, также может резко сдвинуть цену золота.
В таблице 3 ниже показано сопоставление для наиболее распространенных инструментов. Для символов, отсутствующих в этой таблице, сопоставление можно вывести, извлекая первые три символа строки как базовую валюту, а последние три — как валюту котировки, а затем находя соответствующие коды стран. Вспомогательная функция GetCurrenciesForSymbol() в NewsFilter.mqh выполняет это автоматически для стандартных валютных пар Forex.
| Символ / торговый инструмент | Базовая валюта | Валюта котировки | Коды стран для мониторинга |
|---|---|---|---|
| EURUSD | EUR | USD | EU, DE, FR — плюс US |
| GBPUSD | GBP | USD | GB — плюс US |
| USDJPY | USD | JPY | US — плюс JP |
| AUDUSD | AUD | USD | AU — плюс US |
| USDCAD | USD | CAD | US — плюс CA |
| XAUUSD | XAU (золото) | USD | US — основной; также EU/JP для контекста режима ухода от риска |
| GER40 / DAX | EUR (индекс) | Н/Д | DE, EU — ЕЦБ, индекс потребительских цен Германии, индекс делового климата Ifo |
| US30 / DJIA | USD (индекс) | Н/Д | Только US — FOMC, NFP, ВВП, ISM |
Таблица 3 — Сопоставление символов/торговых инструментов со странами. В случае стандартных валютных пар базовая и котируемая валюты определяют, за событиями каких стран следует следить. Для индексов и металлов используйте страну основной валюты номинации, а также любые крупные экономики, связанные с настроениями относительно риска и значимые для факторов, определяющих цену инструмента.
Метод извлечения валют корректно обрабатывает большинство стандартных валютных пар, однако имеет особые случаи для экзотических пар, CFD на отдельные акции и инструментов с нестандартными именами символов (например, с брокерскими суффиксами .a, .c, .m, добавляемыми к символам). Фильтр включает входной параметр для ручного переопределения валют, чтобы трейдер мог напрямую указать валюты для мониторинга, когда автоматическое извлечение не дает правильного результата.
2.2 Функция CalendarEvents() и коды стран
Функция CalendarEventByCountry() принимает строку с кодом страны и заполняет массив структур MqlCalendarEvent всеми событиями, связанными с этой страной. Формат кода страны соответствует кодам ISO 3166-1 alpha-2: «US» для Соединённых Штатов, «EU» для Европейского союза, «GB» для Соединённого Королевства, «JP» для Японии, «AU» для Австралии и так далее. Полный список доступных кодов стран можно получить с помощью функции CalendarCountries() и просмотреть на вкладке «Журнал» терминала в процессе разработки.
//+------------------------------------------------------------------+ //| Example: Retrieve all high-impact USD events | //+------------------------------------------------------------------+ MqlCalendarEvent events[]; int count = CalendarEventByCountry("US", events); PrintFormat("Found %d events for country code US", count); for(int i = 0; i < count; i++) { if(events[i].importance == CALENDAR_IMPORTANCE_HIGH) { PrintFormat("High-impact: [%I64u] %s (%s)", events[i].id, events[i].name, events[i].event_currency); } } //+------------------------------------------------------------------+
Эта схема — сначала извлекать все события по стране, а затем фильтровать их по степени важности — лежит в основе цикла опроса в режиме реального времени внутри новостного фильтра. Идентификаторы событий, полученные таким образом, передаются в функцию CalendarValueHistoryByEvent(), чтобы извлечь запланированные временные метки для каждой предстоящей публикации показателя и заполнить внутренний кэш.
Раздел 3: Создание файла NewsFilter.mqh
3.1 Архитектура фильтра
Файл NewsFilter.mqh предоставляет вызывающему советнику три функции:
- IsNewsWindow(): Возвращает true, если текущее время попадает в окно паузы перед событием для любого релевантного события высокой значимости.
- IsPostNewsWindow(): Возвращает true, если текущее время попадает в окно волатильности после публикации, связанное с недавним событием.
- IsHighImpactNewsToday(): Возвращает значение true, если в течение текущего торгового дня запланировано какое-либо событие высокой значимости, затрагивающее торгуемые валюты.
Вызывающий советник использует эти три функции как условия допуска в своей логике OnTick().
Внутри фильтр ведёт кэшированный список предстоящих релевантных событий, хранящийся в массиве фиксированного размера на 3 000 слотов (NF_MAX_EVENTS). В режиме реального времени этот кэш автоматически обновляется в начале каждого нового часа посредством вызова функции NF_LoadEvents(), которая использует CalendarEventByCountry() для получения всех событий по соответствующим кодам стран и CalendarValueHistoryByEvent() для извлечения их временных меток. Проверки временного окна в IsNewsWindow() затем сравнивают TimeCurrent() с этим кэшированным списком. Функция IsPostNewsWindow() при каждом вызове пересчитывает время последнего прошедшего события, просматривая кэш. Она не полагается на значение, заданное только при инициализации или во время последнего ежечасного обновления. Благодаря этому точность окна после события сохраняется по мере течения времени. Это работает как при реальной торговле между ежечасными обновлениями, так и при полном воспроизведении бэктеста.
В Тестере стратегий кэш заполняется один раз при инициализации из CSV-файла, сгенерированного сопутствующим скриптом-логгером NewsEventLogger. Функция NF_LoadCsv() включает четыре автоматические проверки корректности, которые выводят диагностические предупреждения, если покрытие CSV-файла не совпадает с диапазоном дат бэктеста — например, при полном превышении вместимости, когда файл начинается после начала бэктеста или заканчивается до его начала.
3.2 Полный файл NewsFilter.mqh
//+------------------------------------------------------------------+ //| NewsFilter.mqh | //+------------------------------------------------------------------+ #property strict //--- Input parameters input int InpPreEventMins = 30; input int InpPostEventMins = 30; input bool InpFilterHigh = true; input bool InpFilterMedium = false; input string InpManualCurrencies = ""; input bool InpUseCsvFallback = false; input string InpCsvFileName = "NewsCalendarLog_EURUSD.csv"; //--- Internal cache #define NF_MAX_EVENTS 3000 datetime g_nfEventTimes[NF_MAX_EVENTS]; int g_nfEventCount = 0; datetime g_nfLastRefresh = 0; datetime g_nfLastRelease = 0; bool g_nfHighToday = false; bool g_nfCsvLoaded = false; //+------------------------------------------------------------------+ //| Helper: Get Currencies | //+------------------------------------------------------------------+ void NF_GetCurrencies(string &cur1, string &cur2) { if(StringLen(InpManualCurrencies) >= 3) { string parts[]; StringSplit(InpManualCurrencies, ',', parts); cur1 = (ArraySize(parts) > 0) ? parts[0] : ""; cur2 = (ArraySize(parts) > 1) ? parts[1] : ""; StringTrimLeft(cur1); StringTrimRight(cur1); StringTrimLeft(cur2); StringTrimRight(cur2); return; } string sym = _Symbol; int dotPos = StringFind(sym, "."); if(dotPos > 0) sym = StringSubstr(sym, 0, dotPos); int usPos = StringFind(sym, "_"); if(usPos > 0) sym = StringSubstr(sym, 0, usPos); if(StringLen(sym) == 6) { cur1 = StringSubstr(sym, 0, 3); cur2 = StringSubstr(sym, 3, 3); } else { cur1 = "USD"; cur2 = ""; } } //+------------------------------------------------------------------+ //| Helper: Country Codes | //+------------------------------------------------------------------+ string NF_CountryForCurrency(string currency) { StringToUpper(currency); if(currency == "USD") return("US"); if(currency == "EUR") return("EU"); if(currency == "GBP") return("GB"); if(currency == "JPY") return("JP"); if(currency == "AUD") return("AU"); if(currency == "CAD") return("CA"); if(currency == "CHF") return("CH"); if(currency == "NZD") return("NZ"); return(""); } //+------------------------------------------------------------------+ //| Load from Live API | //+------------------------------------------------------------------+ void NF_LoadEvents() { g_nfEventCount = 0; g_nfLastRelease = 0; g_nfHighToday = false; datetime now = TimeCurrent(); datetime window = now + 86400; int tzOffset = (int)(TimeCurrent() - TimeGMT()); string cur1, cur2; NF_GetCurrencies(cur1, cur2); string countries[2]; countries[0] = NF_CountryForCurrency(cur1); countries[1] = NF_CountryForCurrency(cur2); for(int c = 0; c < 2; c++) { if(StringLen(countries[c]) == 0) continue; MqlCalendarEvent events[]; int evCount = CalendarEventByCountry(countries[c], events); if(evCount <= 0) continue; for(int i = 0; i < evCount; i++) { bool isHigh = (events[i].importance == CALENDAR_IMPORTANCE_HIGH); bool isMed = (events[i].importance == CALENDAR_IMPORTANCE_MODERATE); if(!isHigh && !(InpFilterMedium && isMed)) continue; MqlCalendarValue values[]; int valCount = CalendarValueHistoryByEvent(events[i].id, values, now - 86400, window); if(valCount <= 0) continue; for(int v = 0; v < valCount; v++) { datetime evTime = values[v].time + tzOffset; if(evTime > now && evTime <= window) { if(g_nfEventCount < NF_MAX_EVENTS) { g_nfEventTimes[g_nfEventCount] = evTime; g_nfEventCount++; if(isHigh) g_nfHighToday = true; } } if(evTime <= now && evTime > g_nfLastRelease) g_nfLastRelease = evTime; } } } g_nfLastRefresh = now; PrintFormat("NewsFilter: Live API refreshed. %d events loaded.", g_nfEventCount); } //+------------------------------------------------------------------+ //| Load from CSV | //+------------------------------------------------------------------+ void NF_LoadCsv(string fileName) { if(g_nfCsvLoaded) return; g_nfEventCount = 0; g_nfHighToday = false; g_nfLastRelease = 0; int handle = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(handle == INVALID_HANDLE) handle = FileOpen(fileName, FILE_READ | FILE_CSV | FILE_ANSI | FILE_COMMON, ';'); if(handle == INVALID_HANDLE) { PrintFormat("NewsFilter ERROR: Could not open file %s", fileName); return; } string cur1, cur2; NF_GetCurrencies(cur1, cur2); StringToUpper(cur1); StringToUpper(cur2); //--- Skip 2 header lines for(int i = 0; i < 2; i++) { while(!FileIsLineEnding(handle) && !FileIsEnding(handle)) FileReadString(handle); } int loadedCount = 0; bool arrayFullWarning = false; while(!FileIsEnding(handle)) { string dtStr = FileReadString(handle); string name = FileReadString(handle); string impact = FileReadString(handle); string curr = FileReadString(handle); while(!FileIsLineEnding(handle) && !FileIsEnding(handle)) FileReadString(handle); if(dtStr == "" || dtStr == NULL) continue; StringTrimLeft(curr); StringTrimRight(curr); StringToUpper(curr); if(curr != cur1 && curr != cur2) continue; StringTrimLeft(impact); StringTrimRight(impact); StringToUpper(impact); if(!(impact == "HIGH" || (InpFilterMedium && impact == "MEDIUM"))) continue; string cleanDate = dtStr; StringReplace(cleanDate, ".", "-"); datetime evTime = StringToTime(cleanDate); if(evTime == 0) continue; //--- Check for array capacity if(g_nfEventCount < NF_MAX_EVENTS) { g_nfEventTimes[g_nfEventCount] = evTime; g_nfEventCount++; if(impact == "HIGH") g_nfHighToday = true; loadedCount++; } else { arrayFullWarning = true; } } FileClose(handle); g_nfCsvLoaded = true; //--- MQL5 BACKTEST HEALTH CHECKS if(MQLInfoInteger(MQL_TESTER)) { datetime testStart = TimeCurrent(); if(g_nfEventCount > 0) { datetime firstEvent = g_nfEventTimes[0]; datetime lastEvent = g_nfEventTimes[g_nfEventCount - 1]; //--- 1. Start Date Gap check if(testStart > 0 && firstEvent > testStart) { PrintFormat("NewsFilter WARNING: Backtest starts at %s, but CSV begins at %s.", TimeToString(testStart), TimeToString(firstEvent)); } //--- 2. Complete Mismatch Check (CSV is entirely too old) if(testStart > 0 && lastEvent < testStart) { PrintFormat("NewsFilter CRITICAL ERROR: CSV ended on %s, before backtest started on %s!", TimeToString(lastEvent), TimeToString(testStart)); } //--- 3. Early Finish / End Date Notice PrintFormat("NewsFilter INFO: CSV coverage spans from %s to %s.", TimeToString(firstEvent), TimeToString(lastEvent)); PrintFormat("NewsFilter NOTICE: If your backtest runs past %s, news filtering will stop working.", TimeToString(lastEvent)); } else { Print("NewsFilter WARNING: No matching events found in CSV."); } } PrintFormat("NewsFilter: SUCCESS! Loaded %d events from %s.", g_nfEventCount, fileName); } //+------------------------------------------------------------------+ //| Initialization (Universal Priority Strategy) | //+------------------------------------------------------------------+ void NewsFilterInit(bool forceCsv = false) { string targetFile = ""; //--- 1. Priority: Exact User Input if(InpCsvFileName != "" && FileIsExist(InpCsvFileName, FILE_COMMON)) targetFile = InpCsvFileName; //--- 2. Priority: Standard Script Format else if(FileIsExist("NewsCalendarLog_" + _Symbol + ".csv", FILE_COMMON)) targetFile = "NewsCalendarLog_" + _Symbol + ".csv"; //--- 3. Priority: Pattern Match (Auto-detect files with date stamps) else { string searchPattern = "NewsCalendarLog_" + _Symbol + "_*.csv"; long searchHandle; string foundFile; searchHandle = FileFindFirst(searchPattern, foundFile, FILE_COMMON); if(searchHandle != INVALID_HANDLE) { targetFile = foundFile; FileFindClose(searchHandle); PrintFormat("NewsFilter: Auto-detected date-stamped file: %s", targetFile); } } //--- Execution Logic if((bool)MQLInfoInteger(MQL_TESTER) || forceCsv || InpUseCsvFallback) { if(targetFile != "") { if(!g_nfCsvLoaded) NF_LoadCsv(targetFile); } else { Print("NewsFilter ERROR: No matching CSV found! Please provide the full filename in inputs."); } } else { NF_LoadEvents(); } } //+------------------------------------------------------------------+ //| Pre-News Window Check | //+------------------------------------------------------------------+ bool IsNewsWindow() { datetime now = TimeCurrent(); if(!(bool)MQLInfoInteger(MQL_TESTER) && !InpUseCsvFallback) { if(now - g_nfLastRefresh >= 3600) NF_LoadEvents(); } long preSec = (long)InpPreEventMins * 60; for(int i = 0; i < g_nfEventCount; i++) { long diff = (long)g_nfEventTimes[i] - (long)now; if(diff >= 0 && diff <= preSec) return(true); } return(false); } //+------------------------------------------------------------------+ //| Post-News Window Check | //+------------------------------------------------------------------+ bool IsPostNewsWindow() { datetime now = TimeCurrent(); g_nfLastRelease = 0; for(int i = 0; i < g_nfEventCount; i++) { if(g_nfEventTimes[i] <= now) { if(g_nfEventTimes[i] > g_nfLastRelease) g_nfLastRelease = g_nfEventTimes[i]; } } if(g_nfLastRelease == 0) return(false); long elapsed = (long)now - (long)g_nfLastRelease; return(elapsed >= 0 && elapsed <= (long)InpPostEventMins * 60); } //+------------------------------------------------------------------+ //| High Impact Checker | //+------------------------------------------------------------------+ bool IsHighImpactNewsToday() { return(g_nfHighToday); } //+------------------------------------------------------------------+
Раздел 4: Интеграция и тестирование в реальном времени
4.1 Подключение фильтра к советнику
Чтобы добавить новостной фильтр в любой существующий советник, необходимо внести четыре изменения: одну директиву #include в начале кода, один вызов NewsFilterInit() в OnInit() и две условные проверки в начале OnTick(). Вызовы IsNewsWindow() и IsPostNewsWindow() размещаются перед любой логикой входа в рынок и при активном новостном окне приводят к досрочному выходу. Сама логика входа в рынок не изменяется.
Функция NewsFilterInit() принимает необязательный логический параметр forceCsv. При значении true этот параметр переопределяет путь API в реальном времени и принудительно использует загрузчик CSV независимо от того, обнаружен ли MQL_TESTER. Рекомендуемый подход в OnInit() — явно считывать флаг MQL_TESTER и передавать его при вызове, чтобы одна и та же OnInit() работала одинаково в режиме реального времени и в Тестере стратегий без изменений кода между запусками.
//+------------------------------------------------------------------+ //| Integration: NewsFilter Link | //+------------------------------------------------------------------+ #include <NewsFilter.mqh> //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { bool isTesting = (bool)MQLInfoInteger(MQL_TESTER); //--- Initialize filter based on environment NewsFilterInit(isTesting); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- 1. News filter gates: check before entry logic if(IsNewsWindow()) { Comment("NewsFilter: Pre-event window active — entries paused."); return; } if(IsPostNewsWindow()) { Comment("NewsFilter: Post-release window active — entries paused."); return; } //--- 2. Risk Management: Handle high-impact news days double lots = InpBaseLots; if(IsHighImpactNewsToday()) { lots = NormalizeDouble(InpBaseLots * 0.5, 2); } //--- 3. Reset dashboard and proceed Comment(""); //--- Your existing entry logic runs here }
Использование функции IsHighImpactNewsToday() в конце примера не является обязательным. Некоторые трейдеры предпочитают торговать полным объёмом позиции и просто приостанавливать торговлю вокруг временных окон конкретных событий, тогда как другие предпочитают сокращать экспозицию в любой день с запланированной публикацией показателя высокой значимости, независимо от того, блокирует ли фильтр торговлю в данный момент. Функция поддерживает оба подхода и не создает дополнительных накладных расходов помимо первоначальной проверки кэша.
4.2 Ограничение Тестера стратегий
Для выполнения всех функций API календаря — CalendarEventByCountry(), CalendarValueHistoryByEvent(), CalendarValueLast() — требуется действующее подключение к серверу MetaQuotes. Во время запуска в Тестере стратегий такое соединение отсутствует. Тестер воспроизводит исторические данные баров и тиков в изолированном режиме, поэтому любой вызов этих функций внутри тестера возвращает нулевой результат.
Функция NewsFilterInit() определяет, запущена ли она в тестере, проверяя значение MQLInfoInteger(MQL_TESTER). Если флаг тестера равен true — или если параметр forceCsv передан со значением true — функция переходит к загрузчику CSV вместо API в реальном времени. Для автоматического поиска нужного файла используется алгоритм поиска с тремя уровнями приоритета:
- Приоритет 1 — если входной параметр InpCsvFileName содержит допустимое имя файла и этот файл существует в общей папке файлов MetaTrader 5, он используется напрямую. Это правильный выбор, если вам нужно указать конкретный файл по имени.
- Приоритет 2 — если существует файл с именем NewsCalendarLog_, за которым следует текущий символ (торговый инструмент) и расширение .csv (например, NewsCalendarLog_EURUSD.csv), используется именно он. Это стандартное имя выходного файла, создаваемого при запуске скрипта-логгера без дат в имени.
- Приоритет 3 — если ни один из указанных выше вариантов не совпадает, фильтр ищет любой файл, соответствующий шаблону NewsCalendarLog_, за которым следуют символ и шаблон даты с подстановочными символами (например, NewsCalendarLog_EURUSD_20230101_20240101.csv). Если найден соответствующий файл, он используется автоматически, а его имя выводится в журнал «Эксперты».
Если ни один из трех вариантов поиска не срабатывает, выводится сообщение об ошибке, и события не загружаются; это означает, что фильтр не будет блокировать сделки во время бэктеста.
После загрузки функция NF_LoadCsv() выполняет четыре проверки работоспособности и выводит диагностическую информацию. Она выводит предупреждение, если CSV содержит больше NF_MAX_EVENTS событий, если данные в файле начинаются слишком поздно, а также выводит первую и последнюю временные метки. Кроме того, выдается критическая ошибка, если CSV заканчивается до даты начала бэктеста (фильтрация выполняться не будет).
Важно: API в реальном времени работает только на подключенных счетах. Для работы функций API календаря MetaTrader 5 необходимо, чтобы терминал был подключен к серверу брокера через реальный или демо-счет. Они не работают, когда терминал находится в автономном режиме или при запуске изолированного исторического бэктеста без подключения к серверу. Перед использованием на реальном счете всегда тестируйте фильтр на подключенном демо-счете и с помощью вызовов Print() убедитесь, что функция CalendarEventByCountry() возвращает ненулевые значения для ожидаемых кодов стран.
4.3 Демо-советник «News Filter»
NewsFilterDemo.mq5 — это автономный советник, который демонстрирует все описанные в данной статье схемы интеграции на примере работающей торговой стратегии. В качестве сигнала входа используется простое пересечение двух EMA (быстрая с периодом 20, медленная с периодом 50), благодаря чему влияние фильтра изолировано и отчетливо видно в результатах бэктеста. Этот советник не предназначен для использования в качестве рабочей стратегии для реальной торговли; его цель — предоставить вам полноценный, готовый к запуску пример, который можно изучить, изменить и использовать для проверки корректности работы фильтра, прежде чем подключать его к собственному советнику.
Советник добавляет один элемент, которого нет в примере минимальной схемы подключения из раздела 4.1: панель мониторинга на графике в реальном времени. Цветная текстовая надпись в левом верхнем углу графика обновляется при каждом тике, отображая текущее состояние фильтра. Когда ни одно новостное окно не активно, метка зелёным цветом показывает "NEWS FILTER: ACTIVE". Когда окно перед событием активно, метка переключается на "NEWS BLOCK: PRE-EVENT BLOCK ACTIVE" красным цветом. Во время окна после события метка показывает "NEWS BLOCK: POST-EVENT BLOCK ACTIVE" оранжево-красным цветом. Если для параметра InpNewsFilterEnabled установлено значение false, метка показывает "NEWS FILTER: DISABLED" серым цветом. Благодаря этому можно легко отслеживать поведение фильтра в режиме реального времени на демонстрационном графике.
При запуске в Тестере стратегий советник выводит в OnDeinit() структурированную сводку, где указано общее количество сигналов, обнаруженных по пересечению, сколько из них было заблокировано окном перед событием, сколько — окном после события и сколько сделок было фактически открыто. Эта сводка — самый быстрый способ убедиться, что фильтр работает: если оба счетчика блокировок равны нулю, CSV-файл либо не найден, либо не охватывает период бэктеста.
//+------------------------------------------------------------------+ //| NewsFilterDemo.mq5 | //| Optimized with Live Dashboard | //+------------------------------------------------------------------+ #property strict #property description "News Filter Demo — Smart Logs and Dashboard" #include <Trade\Trade.mqh> #include "NewsFilter.mqh" //--- INPUTS input group "=== News Filter Options ===" input bool InpNewsFilterEnabled = true; // Enable News Filter input bool InpReduceSizeOnNews = true; // Reduce Lot on News Days input double InpNewsDayLotFactor = 0.5; // Risk Reduction Factor input group "=== MA Strategy ===" input int InpFastPeriod = 20; // EMA Fast Period input int InpSlowPeriod = 50; // EMA Slow Period input double InpBaseLotSize = 0.1; // Base Lot Size input int InpStopLossPips = 30; // Stop Loss (Pips) input int InpTakeProfitPips = 60; // Take Profit (Pips) //--- GLOBALS CTrade g_Trade; int g_FastHandle = INVALID_HANDLE; int g_SlowHandle = INVALID_HANDLE; datetime g_LastBarTime = 0; //--- Summary Counters int g_TotalSignals = 0; int g_PreEventBlocks = 0; int g_PostEventBlocks = 0; int g_TradesTotal = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { g_FastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE); g_SlowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE); if(g_FastHandle == INVALID_HANDLE || g_SlowHandle == INVALID_HANDLE) return(INIT_FAILED); g_Trade.SetExpertMagicNumber(20240801); g_Trade.SetDeviationInPoints(20); //--- Initialize news filter based on environment if(InpNewsFilterEnabled) { bool isTesting = (bool)MQLInfoInteger(MQL_TESTER); NewsFilterInit(isTesting); } //--- Create Dashboard Label ObjectCreate(0, "NewsStatus", OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, "NewsStatus", OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, "NewsStatus", OBJPROP_XDISTANCE, 20); ObjectSetInteger(0, "NewsStatus", OBJPROP_YDISTANCE, 20); ObjectSetString(0, "NewsStatus", OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, "NewsStatus", OBJPROP_FONTSIZE, 12); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Print backtest summary for analysis if(MQLInfoInteger(MQL_TESTER)) { Print("=== News Filter Backtest Summary ==="); PrintFormat("Total Signals Detected: %d", g_TotalSignals); PrintFormat("Blocked by Pre-News Window: %d", g_PreEventBlocks); PrintFormat("Blocked by Post-News Window: %d", g_PostEventBlocks); PrintFormat("Trades Opened: %d", g_TradesTotal); Print("===================================="); } ObjectDelete(0, "NewsStatus"); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { string statusText = "NEWS FILTER: ACTIVE"; color statusColor = clrLimeGreen; //--- 1. Update Dashboard State if(InpNewsFilterEnabled) { if(IsNewsWindow()) { statusText = "NEWS BLOCK: PRE-EVENT BLOCK ACTIVE"; statusColor = clrRed; } else if(IsPostNewsWindow()) { statusText = "NEWS BLOCK: POST-EVENT BLOCK ACTIVE"; statusColor = clrOrangeRed; } } else { statusText = "NEWS FILTER: DISABLED"; statusColor = clrGray; } ObjectSetString(0, "NewsStatus", OBJPROP_TEXT, statusText); ObjectSetInteger(0, "NewsStatus", OBJPROP_COLOR, statusColor); //--- 2. Strategy Logic if(Bars(_Symbol, PERIOD_CURRENT) < InpSlowPeriod) return; datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0); if(barTime == g_LastBarTime) return; g_LastBarTime = barTime; double fast[2], slow[2]; if(CopyBuffer(g_FastHandle, 0, 1, 2, fast) < 2) return; if(CopyBuffer(g_SlowHandle, 0, 1, 2, slow) < 2) return; bool bullCross = (fast[1] <= slow[1] && fast[0] > slow[0]); bool bearCross = (fast[1] >= slow[1] && fast[0] < slow[0]); if(!bullCross && !bearCross) return; if(PositionSelect(_Symbol)) return; g_TotalSignals++; //--- 3. Blocking Logic if(InpNewsFilterEnabled) { if(IsNewsWindow()) { g_PreEventBlocks++; return; } if(IsPostNewsWindow()) { g_PostEventBlocks++; return; } } //--- 4. Risk Reduction Calculation double lotSize = InpBaseLotSize; if(InpNewsFilterEnabled && InpReduceSizeOnNews && IsHighImpactNewsToday()) { lotSize = NormalizeDouble(InpBaseLotSize * InpNewsDayLotFactor, 2); } //--- 5. Trade Execution double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); double pipSize = (digits == 5 || digits == 3) ? point * 10.0 : point; if(bullCross) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = ask - InpStopLossPips * pipSize; double tp = ask + InpTakeProfitPips * pipSize; if(g_Trade.Buy(lotSize, _Symbol, ask, sl, tp)) g_TradesTotal++; } else if(bearCross) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bid + InpStopLossPips * pipSize; double tp = bid - InpTakeProfitPips * pipSize; if(g_Trade.Sell(lotSize, _Symbol, bid, sl, tp)) g_TradesTotal++; } } //+------------------------------------------------------------------+
4.4 Проверка демо-советника «News Filter» в Тестере стратегий
В этом разделе подробно описан полный сквозной проверочный прогон с использованием NewsFilterDemo.mq5. Выполняйте каждый шаг по порядку. К концу этого процесса вы сможете, опираясь только на журнал «Эксперты», убедиться, что фильтр загрузился правильно, обнаружил события и заблокировал сделки в нужные моменты.
Шаг 1 — Сгенерируйте CSV-файл
В реальном или демо-терминале (не в Тестере стратегий) запустите NewsEventLogger.mq5 на любом графике как скрипт. Для проверочного прогона на EURUSD используйте следующие рекомендуемые входные параметры:
- InpStartDate: D'2023.01.01'
- InpEndDate: D'2024.12.31'
- InpCurrencies: оставить пустым (скрипт автоматически определит USD и EUR по символу (торговому инструменту) графика)
- InpLogPrefix: NewsCalendarLog (по умолчанию)
- InpLogMedium: false
Скрипт создает файл с датой в имени в папке общих файлов MetaTrader 5, например NewsCalendarLog_EURUSD_20230101_20241231.csv. Убедитесь, что файл был создан: проверьте вкладку «Эксперты» — там должна появиться строка вида «NewsLogger SUCCESS: 412 events written.» Откройте файл в табличном редакторе, чтобы выборочно проверить правильность дат, кодов валют и уровней влияния.

Рис. 1: Пример CSV-файла NewsCalendarLog_EURUSD, используемого как резервный источник исторических данных для бэктеста.
Шаг 2 — Настройка входных параметров советника
Откройте «Тестер стратегий» и загрузите файл loadNewsFilterDemo.mq5. На вкладке «Входные параметры» задайте следующее:
- InpNewsFilterEnabled: true
- InpReduceSizeOnNews: true
- InpFilterHigh: true
- InpFilterMedium: true
- InpUseCsvFallback: false — оставьте значение false; советник автоматически передает isTesting=true в функцию NewsFilterInit() при запуске в Тестере стратегий, поэтому ветка с CSV используется независимо от значения этого входного параметра
- InpCsvFileName: оставьте пустым, чтобы использовать автоматическое определение, или введите точное имя файла из шага 1, если хотите жестко указать его
Шаг 3 — Настройка Тестера стратегий
- Символ: EURUSD (или тот торговый символ, который соответствует вашему CSV-файлу)
- Таймфрейм: H1
- Диапазон дат: с 01.01.2023 по 31.12.2024 (должен покрываться CSV-файлом)
- Моделирование: для проверки достаточно режима «Только цены открытия»; для полной проверки качества используйте режим «Каждый тик»
- Спред: текущий спред или фиксированное значение, например 15
Шаг 4 — Запустите тест и просмотрите журнал Experts
Нажмите «Старт». По завершении бэктеста откройте вкладку «Журнал» в Тестере стратегий. Найдите следующие строки в указанном порядке:
Сначала — вывод с результатом обнаружения файла. Вы должны увидеть либо строку «NewsFilter: Auto-detected date-stamped file: NewsCalendarLog_EURUSD_20230101_20241231.csv» (совпадение с приоритетом 3), либо сообщение о совпадении с приоритетом 1 или 2, если вы назвали файл соответствующим образом.
Во-вторых, результаты проверки работоспособности. Вы должны увидеть строку «NewsFilter INFO: Coverage from 2023.01.02 to 2024.12.27» (или аналогичные даты, соответствующие вашему CSV-файлу). Если вы увидите какие-либо строки с «CRITICAL WARNING», устраните указанные проблемы, прежде чем делать выводы по результатам бэктеста. Предупреждение о том, что CSV-файл заканчивается до даты начала бэктеста, означает, что ни одна сделка не будет заблокирована из-за новостей.
В-третьих, подтверждение загрузки: «NewsFilter: SUCCESS! Loaded 412 events from NewsCalendarLog_EURUSD_20230101_20241231.csv» (количество событий будет совпадать с тем, которое сообщил логгер на шаге 1).
Шаг 5 — Ознакомьтесь со сводкой OnDeinit
В самом конце вывода журнала, после завершения бэктеста, советник выводит сводный блок:
| News Filter Summary |
|---|
| Всего обнаружено сигналов: 82 |
| Заблокировано окном перед событием: 4 |
| Заблокировано окном после события: 2 |
| Открыто сделок: 76 |
Точные цифры будут зависеть от выбранного вами периода и рыночной конъюнктуры. Для проверки важно, чтобы число блокировок в окне перед событием и в окне после события было больше нуля. Если любое из этих значений равно нулю, а вы проводили многолетний бэктест, охватывающий события USD и EUR с высоким влиянием, значит, фильтр некорректно подключается к данным CSV — вернитесь и проверьте сообщения проверки работоспособности на этапе 4.

Рис. 2: Журнал Тестера стратегий со сводкой бэктеста новостного фильтра, где подробно показано количество обнаруженных сигналов и количество сигналов, заблокированных окнами перед событием и после события.
Шаг 6 — Сверьте конкретную блокировку
Чтобы убедиться, что блокировка происходила в нужные моменты календарного времени, экспортируйте полный список сделок из результата бэктеста (щелкните правой кнопкой мыши вкладку «Сделки» и сохраните файл в формате CSV). Затем параллельно откройте CSV-файл NewsCalendarLog. Выберите любую дату события с высоким влиянием — например, публикацию NFP 2023.02.03 в 13:30 UTC. Убедитесь, что в этот день с 13:00 до 14:00 UTC сделки не открывались (по 30 минут с каждой стороны). Если сделки появляются внутри этого временного окна, убедитесь, что валюта события (USD или EUR) соответствует тестируемой валютной паре, а временная метка в CSV-файле совпадает с ожидаемым временем публикации.
Шаг 7 — Сравните с базовым прогоном
Запустите бэктест во второй раз, установив для параметра InpNewsFilterEnabled значение false. В сводке OnDeinit будет показано то же значение «Всего обнаруженных сигналов», но число блокировок будет равно нулю, а количество открытых сделок — выше. Сравнение двух кривых капитала на вкладке «График» позволяет увидеть чистый эффект применения фильтра к данной стратегии и выбранному диапазону дат.
4.5 Проверка фильтра в режиме реального времени
Прикрепите любой советник с фильтром новостей к графику демо-счёта в течение обычной торговой недели. Добавьте вызов Print() непосредственно внутри блока IsNewsWindow(), в котором выполняется return, — например, Print("Blocked: pre-event window") — и проверьте вкладку «Эксперты» примерно во время известного предстоящего события. Примерно за 30 минут до запланированного времени в журнале должно начать выводиться сообщение о блокировке. Вывод должен прекратиться по истечении 30-минутного окна после события.
Сверьте периоды блокировки с экономическим календарем, отображаемым в собственном представлении календаря MetaTrader 5 (доступно в верхнем меню в разделе «Вид»). Если фильтр срабатывает в нужное время относительно запланированного события с высоким влиянием для отслеживаемых валют, значит API работает корректно. Если вокруг событий, которые, как вы ожидали, должны были фильтроваться, не появляются блокировки, проверьте, соответствуют ли коды стран из NF_CountryForCurrency() фактическому коду страны, используемому в календаре для этой валюты.

Рис. 3: График EURUSD с демонстрацией панели мониторинга на графике в реальном времени. Зеленая метка статуса подтверждает, что фильтр активен и отслеживает новости высокой значимости в режиме реального времени без ручной настройки.
Раздел 5: Скрипт-логгер новостных событий и резервный механизм для Тестера стратегий
5.1 Почему важен CSV-резерв
Бэктестинг советника, учитывающего новости, необходим для оценки того, улучшает ли фильтр результативность, однако API календаря в реальном времени не позволяет сделать это напрямую. Без резервного механизма бэктесты советников, использующих NewsFilter.mqh, просто игнорировали бы все новостные события — фильтр никогда бы не срабатывал, а бэктест не отражал бы поведение советника в реальном времени вокруг публикаций важных показателей.
Запустите скрипт NewsEventLogger на реальном или демо-терминале на целевой период и сохраните временные метки в CSV. Используйте этот CSV-файл в качестве источника новостей во время бэктестов. NewsEventLogger автоматически генерирует имена файлов с датой в имени — например, NewsCalendarLog_EURUSD_20230101_20241231.csv — поэтому файлы для разных диапазонов дат не перезаписывают друг друга и могут сосуществовать в папке общих файлов. Поиск файла по трем приоритетам в фильтре (описанный в разделе 4.2) автоматически находит нужный файл на основе текущего торгового инструмента, поэтому в большинстве случаев ручная настройка имени файла во входных параметрах советника не требуется.
Фильтр считывает CSV-файл и применяет ту же логику окон перед событием и после события, что и при работе с API в реальном времени. Затем в ходе бэктеста открытие позиций корректно приостанавливается вблизи событий, которые действительно произошли в течение этого исторического периода.
5.2 Скрипт NewsEventLogger//+------------------------------------------------------------------+ //| NewsEventLogger.mq5 | //+------------------------------------------------------------------+ #property strict #property script_show_inputs //--- INPUTS input group "=== Date Range Settings ===" input datetime InpStartDate = D'2023.01.01'; // Start Date input datetime InpEndDate = D'2024.01.01'; // End Date input group "=== Filter & File Options ===" input string InpCurrencies = ""; // Manual Currencies (Auto if empty) input string InpLogPrefix = "NewsCalendarLog"; input bool InpLogMedium = false; // Log medium-impact events //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- 1. Currency Detection string targetCurrencies = InpCurrencies; if(StringLen(targetCurrencies) == 0) { string base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE); string profit = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT); targetCurrencies = base + "," + profit; } //--- 2. Generate Date-Stamped Filename string startStr = TimeToString(InpStartDate, TIME_DATE); string endStr = TimeToString(InpEndDate, TIME_DATE); StringReplace(startStr, ".", ""); StringReplace(endStr, ".", ""); string filename = InpLogPrefix + "_" + _Symbol + "_" + startStr + "_" + endStr + ".csv"; //--- 3. File Creation int fileHandle = FileOpen(filename, FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_COMMON, ','); if(fileHandle == INVALID_HANDLE) { Print("NewsLogger ERROR: Cannot create file: ", GetLastError()); return; } //--- Write CSV headers FileWrite(fileHandle, "sep=,"); FileWrite(fileHandle, "DateTime", "EventName", "Impact", "Currency", "CountryCode", "Forecast", "Previous", "Actual"); string currencies[]; int numCurrencies = StringSplit(targetCurrencies, ',', currencies); string codes[][2] = { {"USD", "US"}, {"EUR", "EU"}, {"GBP", "GB"}, {"JPY", "JP"}, {"AUD", "AU"}, {"CAD", "CA"}, {"CHF", "CH"}, {"NZD", "NZ"}, {"XAU", "US"}, {"XAG", "US"}, {"BTC", "US"} }; int totalWritten = 0; int tzOffset = (int)(TimeCurrent() - TimeGMT()); //--- Process news data per currency for(int ci = 0; ci < numCurrencies; ci++) { string currency = currencies[ci]; StringTrimLeft(currency); StringTrimRight(currency); StringToUpper(currency); string countryCode = ""; for(int m = 0; m < ArrayRange(codes, 0); m++) { if(codes[m][0] == currency) { countryCode = codes[m][1]; break; } } if(StringLen(countryCode) == 0) continue; MqlCalendarEvent events[]; int evCount = CalendarEventByCountry(countryCode, events); //--- Iterate through events for(int i = 0; i < evCount; i++) { bool isHigh = (events[i].importance == CALENDAR_IMPORTANCE_HIGH); bool isMed = (events[i].importance == CALENDAR_IMPORTANCE_MODERATE); if(!isHigh && !(InpLogMedium && isMed)) continue; MqlCalendarValue values[]; int vCount = CalendarValueHistoryByEvent(events[i].id, values, InpStartDate, InpEndDate); //--- Log specific values for the date range for(int v = 0; v < vCount; v++) { datetime adjustedTime = values[v].time + tzOffset; FileWrite(fileHandle, TimeToString(adjustedTime, TIME_DATE | TIME_SECONDS), events[i].name, (isHigh ? "HIGH" : "MEDIUM"), currency, countryCode, (values[v].forecast_value == DBL_MAX) ? "" : DoubleToString(values[v].forecast_value, 2), (values[v].prev_value == DBL_MAX) ? "" : DoubleToString(values[v].prev_value, 2), (values[v].actual_value == DBL_MAX) ? "" : DoubleToString(values[v].actual_value, 2)); totalWritten++; } } } //--- Close file and report status FileClose(fileHandle); PrintFormat("NewsLogger SUCCESS: %d events written.", totalWritten); PrintFormat("Filename: %s", filename); } //+------------------------------------------------------------------+
Запускайте этот скрипт один раз для каждого периода бэктеста. Для двухлетнего бэктеста пары EURUSD установите для параметра InpStartDate первую дату бэктеста, а для параметра InpEndDate — последнюю. Оставьте InpCurrencies пустым — скрипт автоматически определит валюты USD и EUR по торговому инструменту графика. Скрипт извлекает из календаря MetaQuotes все события с высоким влиянием для указанных валют за заданный диапазон дат, применяет смещение часового пояса, чтобы временные метки соответствовали времени сервера брокера, и записывает их в CSV-файл с датой в имени в папку общих файлов MetaTrader 5. Весь процесс занимает несколько секунд.
После создания файла CSV советник не требует дополнительной настройки. Функция NewsFilterInit() автоматически найдет файл с помощью сопоставления с шаблоном приоритета 3 (раздел 4.2) на основе имени текущего торгового инструмента. Если вы предпочитаете указать имя файла явно, введите точное имя файла во входной параметр InpCsvFileName. Фильтр загружает CSV-файл во время выполнения OnInit() и применяет к каждому записанному событию одинаковые 30-минутные окна перед событием и после события. Бэктест будет корректно приостанавливать открытие позиций вблизи фактических исторических публикаций данных за рассматриваемый период.
Раздел 6: Практические аспекты
6.1 Предварительное и плавающее время событий
У некоторых событий календаря поле time_mode имеет значение CALENDAR_TIMEMODE_TENTATIVE, что означает, что время публикации является приблизительным и может сдвинуться на час или более относительно запланированного времени. Типичные примеры — пресс-конференции центральных банков и выступления их представителей. Фильтр обрабатывает это консервативно: если фактическое время публикации события отличается от первоначально запланированного, обновление кэша, выполняемое в следующий полный час, обновит временную метку события. Тем временем окно перед событием применяется к первоначально запланированному времени, что может привести к немного более ранней или более поздней блокировке в зависимости от направления корректировки.
В фильтре по умолчанию установлены консервативные значения: 30-минутное окно перед событием и 30-минутное окно после события, чтобы учесть неопределенность времени и дать спредам достаточно времени для нормализации после публикации данных. Для событий высокой значимости, таких как объявления FOMC и пресс-конференции крупных центральных банков, эти значения по умолчанию обеспечивают надежный буфер. Если в результате частота совершения сделок снизится слишком сильно для вашей стратегии, значение параметра InpPreEventMins можно уменьшить до 20 или 15 минут.
Для событий с исторически сильной, но кратковременной реакцией — таких как публикация данных по занятости в несельскохозяйственном секторе США (NFP) или неожиданное решение по ставке — увеличение значения параметра InpPostEventMins свыше 30 дает рынку дополнительное время, чтобы усвоить новость, прежде чем будут предприниматься попытки новых входов в рынок.
6.2 События с указанием только даты
У небольшого числа событий календаря параметр time_mode имеет значение CALENDAR_TIMEMODE_RELEASED: точное время публикации заранее неизвестно — известна только календарная дата. К этой категории относятся объявления о бюджете, публикации протоколов заседаний некоторых центральных банков, а также определенные правительственные отчеты. Для таких событий временная метка, записанная в MqlCalendarValue, как правило, будет соответствовать полуночи даты публикации.
Когда фильтр загружает одно из таких событий, окно перед событием активируется около полуночи в день публикации, а окно после события вскоре после этого завершается. Это не идеальный вариант — фактическая публикация события может произойти в любой момент в течение этого дня. На практике рекомендуется рассматривать события только с датой как периоды повышенной осторожности на весь день и использовать для них IsHighImpactNewsToday() вместо IsNewsWindow(): уменьшать размер позиций на всю торговую сессию в эту дату, а не применять короткое временное окно вокруг неопределённой временной метки.
6.3 Ограничения покрытия данных календаря
Функция CalendarValueHistoryByEvent() позволяет получать исторические данные о событиях за несколько лет для большинства крупнейших экономик. Однако глубина покрытия зависит от конкретного события. События, совсем недавно добавленные в календарь, могут иметь более короткую историю, а в исторических данных по некоторым событиям, относящимся к отдельным странам, могут быть пробелы. При запуске NewsEventLogger для длительного периода бэктеста сравните общее количество событий, выведенное в журнал Experts, с тем, что вы ожидали бы увидеть при ручном анализе этого периода. Если количество событий кажется заниженным, запустите скрипт для более короткого диапазона дат и объедините полученные результаты.
Календарь MetaQuotes в сравнении с Forex Factory: Экономический календарь MetaQuotes охватывает тот же набор событий высокой значимости для всех крупнейших экономик, что и календари Forex Factory и DailyFX. По охвату он в целом эквивалентен им для целей фильтрации новостей. Преимущество по сравнению с ручным сбором данных с этих сайтов заключается в том, что календарь MetaTrader 5 доступен из кода MQL5 без каких-либо веб-запросов, не имеет ограничений по количеству запросов и поддерживается компанией MetaQuotes в рамках платформы. Недостаток заключается в том, что очень незначительные региональные события и некоторые второстепенные показатели по отдельным странам могут не включаться, если MetaQuotes не добавила их в поток данных.
6.4 Совместное использование с другими фильтрами
Фильтр новостей работает независимо от любых других фильтров в советнике. Проверки IsNewsWindow() и IsPostNewsWindow() находятся в самом начале OnTick() и при срабатывании досрочно возвращают управление, поэтому они выполняются до запуска любой другой логики. Их можно сочетать с фильтрами по времени сессии, защитой по спреду, дневными лимитами торговли или любыми другими механизмами ограничения без какого-либо взаимодействия между ними.
Для советников, в которых уже используется фильтр сессии (например, SessionClock.mqh из статьи 1), рекомендуется сначала проверять фильтр сессии, затем — защиту по спреду, а затем — новостной фильтр. Проверки сессии и спреда требуют меньших вычислительных затрат, поскольку не предполагают перебора массива событий, хранящихся в кэше. Такой порядок обеспечивает максимально быстрое выполнение функции OnTick() в типичном случае, когда все фильтры пропускают выполнение дальше.
Заключение
Экономический календарь MetaTrader 5 доступен программистам MQL5 уже несколько лет и по-прежнему остается одной из наименее используемых функций платформы. Шесть функций календаря предоставляют каждому пользователю MetaTrader 5 бесплатный доступ к тому же расписанию новостей, с которым ручные трейдеры сверяются на Forex Factory и DailyFX, но прямо из кода, без веб-скрейпинга, API-ключей или подписок.
Фильтр, созданный в этой статье, использует эти данные для решения проблемы, с которой сталкивается каждый постоянно работающий советник: при открытии позиций во время публикации важных новостей исполнение хуже, спреды шире, а проскальзывание выше, чем при входах в рынок в обычных условиях. Это влияние зачастую остается незаметным в стандартном бэктесте, поскольку тестер применяет фиксированные или типичные спреды независимо от времени суток. Фильтр делает это влияние видимым, сравнивая результаты с фильтром и без него по конкретным сделкам, которые приходятся на период вокруг запланированных событий.
Механизм резервного перехода на CSV обеспечивает правильную работу фильтра в Тестере стратегий без необходимости вносить изменения в основную логику. Один и тот же файл NewsFilter.mqh прозрачно обрабатывает как работу в реальном времени, так и бэктест — советнику не нужно знать, в каком режиме он работает. Один вызов NewsFilterInit() в OnInit() выполняет маршрутизацию, а две функции-фильтра работают одинаково независимо от того, читают ли они данные из календаря в реальном времени или из сохраненного CSV-файла.
Программы, используемые в этой статье
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | NewsFilter.mqh | Подключаемый файл | Основной подключаемый файл, содержащий всю логику фильтрации, интеграцию с API в реальном времени и автоматические проверки работоспособности CSV-файлов. Он автоматически переключается между режимом реального времени и режимом тестера для управления торговыми ограничениями, связанными с новостями. |
| 2 | NewsEventLogger.mq5 | скрипт | Вспомогательный скрипт, который генерирует CSV-файлы с датой в имени из календаря MetaTrader 5 для бэктестинга. Он сохраняет эти файлы в общую папку Common, обеспечивая фильтр историческими данными во время моделирования стратегий. |
| 3 | NewsFilterDemo.mq5 | Демо-советник | Пример советника на основе пересечения EMA, демонстрирующий эту интеграцию. Он оснащён панелью мониторинга в реальном времени для реальной торговли и подробной сводкой по блокировкам торговли в журнале для проверки бэктеста. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22580
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.