English
preview
Инжиниринг признаков для машинного обучения (Часть 4): Реализация временных признаков в MQL5

Инжиниринг признаков для машинного обучения (Часть 4): Реализация временных признаков в MQL5

MetaTrader 5 — Торговые системы |
27 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Содержание

  1. Введение
  2. Проблема UTC в MQL5
  3. Архитектура: CTimeFeatures и CRingBuffer
  4. Циклическое кодирование в MQL5
  5. Определение торговых сессий: пограничный случай с переходом через полночь
  6. Волатильность сессии: кольцевой буфер
  7. Частотный фильтр
  8. Интеграция с советником
  9. Справочник по количеству признаков
  10. Заключение
  11. Список литературы


Введение

В части 3 этой серии была создана Python-функция get_time_features, которая преобразует объект DatetimeIndex в структурированную матрицу признаков: циклические переменные, закодированные по Фурье, для часа, дня недели и дня года; флаги принадлежности к четырём основным торговым сессиям на рынке Форекс; индикатор перекрытия сессий; сессионно-зависимую скользящую волатильность; а также календарные эффекты на границах периодов. Реализация на языке Python обрабатывает пакетные данные с полной векторизацией. MQL5 обрабатывает данные бар за баром в режиме реального времени.

В этой статье тот же набор признаков реализован на MQL5 в виде многократно используемого файла включения CTimeFeatures.mqh. Любой советник может подключить его без изменений. Три проблемы требуют решений, выходящих за рамки простого механического перевода кода. Во-первых, MQL5 использует время брокера, а не UTC — при этом большинство розничных брокеров смещают время на своих серверах на два–три часа, что незаметно искажает определение границ торговых сессий для каждого бара в бэктесте. Во-вторых, Python вычисляет скользящую волатильность пакетно с помощью pandas. В MQL5 волатильность необходимо поддерживать инкрементально для каждой торговой сессии с помощью кольцевого буфера. В-третьих, Python применяет частотный фильтр с помощью вызова DataFrame.drop. В MQL5 частотный фильтр необходимо задать при инициализации, а его настройки должны быть отражены в реестре имён признаков, используемом последующими моделями ONNX.

Полученный класс предоставляет три метода, доступные советнику, — Initialize, Update и Calculate — и формирует плоский массив типа double, имена признаков и порядок столбцов в котором идентичны соответствующим значениям функции get_time_features. Модель, обученную в Python на признаках Python, можно развернуть в MQL5 с помощью OnnxRun, передав массив из Calculate без какого-либо изменения порядка элементов.


Проблема UTC в MQL5

Самое существенное различие между реализациями на Python и MQL5 заключается в системе отсчёта времени. DatetimeIndex в Python содержит явный часовой пояс. Вызов tz_convert("UTC") для любого индекса с заданным часовым поясом обеспечивает правильную привязку к UTC до применения границ торговых сессий. В MQL5 нет аналога: iTime(), TimeCurrent() и все функции временных меток баров возвращают время сервера брокера с тем смещением, которое настроил брокер.

Наиболее распространенная конфигурация среди розничных брокеров MetaTrader 5 — UTC+2 зимой (EET) и UTC+3 летом после перехода на европейское летнее время. Применительно к лондонской торговой сессии (UTC 07:00–15:59) смещение по часовому поясу UTC+2 сдвигает временной интервал на 09:00–17:59 по времени брокера. Советник, проверяющий условие dt.hour >= 7 && dt.hour < 16 по времени брокера, будет классифицировать каждый бар с 09:00 до 15:59 как относящийся к Лондону, а не с 07:00 до 15:59 — это двухчасовая ошибка, которая систематически приводит к неверной классификации открытия лондонской сессии и перекрытия токийской и лондонской сессий на протяжении всей истории бэктеста.

Неверная классификация торговой сессии при замене UTC временем брокера

Рисунок 1. Двухпанельная иллюстрация ошибочной классификации торговых сессий, когда вместо UTC используется время брокера

  • Панель (a): брокер с UTC+2 сдвигает полосу каждой торговой сессии на два часа вправо. Зелёные бары обозначают правильные границы UTC; красные бары показывают, где эти границы оказываются во времени брокера. Открытие лондонской сессии смещается с 07:00 на 09:00, и окно перекрытия сессий Лондона и Нью-Йорка сдвигается на ту же величину.
  • Панель (b): брокер с UTC+3 (EET летом, после перехода на летнее время) создаёт трёхчасовой сдвиг. Модель, обученная на признаках, рассчитанных в Python по данным UTC, будет получать несогласованные флаги торговых сессий от советника, использующего время брокера.

Исправление заключается в использовании единственного целочисленного смещения, вычисляемого при инициализации:

//--- Capture broker-to-UTC offset once at OnInit() time
m_gmt_offset = (int)(TimeGMT() - TimeCurrent());

TimeGMT() возвращает текущее время UTC. TimeCurrent() возвращает последнее известное серверное время тика в локальном времени брокера. Их разница представляет собой смещение со знаком в секундах, которое необходимо прибавить к любой временной метке брокера, чтобы получить UTC. Для брокера в часовом поясе UTC+2 значение m_gmt_offset равно −7200 (минус два часа); для брокера в часовом поясе UTC оно равно нулю.

Это смещение определяется однократно при выполнении OnInit() и остаётся неизменным на протяжении всего времени работы советника. Ограничение заключается в том, что это смещение не может отслеживать переходы на летнее время, происходящие внутри торговой сессии: если брокер переводит часы на один час вперёд между моментом выполнения OnInit() и границей сессии, сохранённое смещение будет ошибаться на один час до конца этой сессии. Это допустимое приближение для признаков, цель которых — помечать статистические режимы сессий, а не срабатывать точно в моменты открытия институциональных сессий. Пользователям, которым требуется точность сессий до минуты (например, чтобы зафиксировать всплеск на открытии Лондона в 07:00 UTC), следует использовать брокера, настроенного на UTC. Это полностью устраняет проблему.


Архитектура: CTimeFeatures и CRingBuffer

Реализация состоит из двух классов, содержащихся в одном заголовочном файле — CTimeFeatures.mqh.

CRingBuffer — это кольцевой буфер фиксированной ёмкости для скалярных значений. Он поддерживает Push для добавления значения и Std для вычисления стандартного отклонения генеральной совокупности по всем значениям, находящимся в буфере в данный момент. Вместо выборочного стандартного отклонения используется стандартное отклонение генеральной совокупности (ddof=0), чтобы соответствовать поведению NumPy по умолчанию, которое используется в моделировании кольцевого буфера в Python при воспроизведении расчёта скользящей волатильности. Каждому сессионному и календарному столбцу соответствует один экземпляр CRingBuffer; всего поддерживается девять буферов.

CTimeFeatures содержит настройки, девять кольцевых буферов и реестр имён признаков. Класс предоставляет советнику три открытых метода:

  • Initialize(timeframe, n_terms, forex, is_time_bar): фиксирует смещение относительно UTC, устанавливает флаги частотного фильтра, задаёт размеры и сбрасывает все кольцевые буферы, а также формирует массив имён признаков. Если is_time_bar равно false, Initialize добавляет два столбца — bar_duration и bar_duration_accel — в начало реестра имён признаков; эти столбцы отсутствуют, если is_time_bar равно true. Необходимо вызвать один раз из OnInit().
  • Update(close_price, bar_broker_time): вычисляет логарифмическую доходность для закрытого бара, определяет, какие буферы волатильности торговых сессий и календаря активны в момент времени этого бара по UTC, и записывает доходность в каждый активный буфер. Если is_time_bar имеет значение false, Update также вычисляет количество секунд, прошедших с предыдущего бара, и сохраняет результат для следующего вызова Calculate. Необходимо вызывать на каждом закрытом баре перед соответствующим вызовом Calculate.
  • Calculate(bar_broker_time, features[]): заполняет выходной массив всеми циклическими признаками, признаками сессий, календарными признаками и признаками волатильности сессий для заданного времени бара. Возвращает false, если не удалось выделить массив.

Когда is_time_bar=false, Update отслеживает брокерскую временную метку каждого закрытого бара. При втором и последующих вызовах функция вычисляет bar_duration как количество секунд, прошедших между временной меткой текущего бара и временной меткой предыдущего бара, а bar_duration_accel — как первую разность этого интервала; это та же арифметика, что и в выражении Python df.index.diff().dt.total_seconds() и последующем вызове .diff(). Оба значения сохраняются в Update и считываются Calculate на следующем баре. Таким образом сохраняется задержка в один бар, применяемая ко всем признакам. Для стандартных временных OHLCV-баров передайте is_time_bar=true; два столбца продолжительности отсутствуют в выходных данных и не отображаются в FeatureNames().

CTimeFeatures — структура класса и интеграция с советником

Рисунок 2. Архитектурная схема CTimeFeatures и паттерн его интеграции с советником

  • Левый блок: интерфейс советника — четыре стандартных обработчика событий MQL5. OnInit вызывает Initialize, а затем запускает цикл прогрева, в ходе которого исторические бары подаются в Update. OnTick вызывает Update при закрытии бара и Calculate при открытии каждого нового бара.
  • Центральный блок: Пять общедоступных методов класса CTimeFeatures. Эти три функциональных метода делегируют работу трём внутренним подсистемам.
  • Правая колонка: три внутренние подсистемы — циклический кодировщик (вычисляет гармоники sin/cos), детектор сессий (сопоставляет час UTC с логическими флагами) и девять кольцевых буферов (поддерживают скользящую историю логарифмической доходности для каждой сессии и календарного столбца).
  • Нижняя панель: выходной массив для H1/forex при n_terms=3: шесть циклических признаков, пять флагов торговых сессий и девять столбцов волатильности торговых сессий, всего 20 признаков.


Циклическое кодирование в MQL5

Кодирование Фурье в MQL5 воспроизводит ту же арифметику, что и в Python, с двумя преобразованиями координат, необходимыми для того, чтобы значения признаков совпадали.

Выравнивание по дню недели. В MQL5 MqlDateTime.day_of_week принимает значения от 0 (воскресенье) до 6 (суббота). В Python pd.DatetimeIndex.dayofweek принимает значения от 0 (понедельник) до 6 (воскресенье). Если напрямую подать целое число MQL5 в формулу Фурье, воскресенье будет закодировано в позиции 0, а суббота — в позиции 6, что поместит их на одинаковое угловое расстояние от понедельника и от пятницы — это неверное геометрическое соотношение для недельного цикла, в котором понедельник является первым активным торговым днём. Преобразование выглядит следующим образом:

//--- MQL5 day_of_week: 0=Sunday, 1=Monday ... 6=Saturday
//--- Python dayofweek: 0=Monday ... 6=Sunday
int py_dow = (dt.day_of_week == 0) ? 6 : dt.day_of_week - 1;

Выравнивание по дню года. В MQL5 MqlDateTime.day_of_year индексируется с нуля: 1 января соответствует значению 0. В Python DatetimeIndex.dayofyear индексируется с единицы: 1 января соответствует значению 1. Продолжительность цикла в обоих случаях составляет 366 (с учётом високосных лет). Добавление 1 к значению MQL5 перед вычислением Фурье устраняет смещение:

int py_doy = dt.day_of_year + 1;   // MQL5: 0-based; Python: 1-based

После внесения этих двух корректировок само вычисление Фурье одинаково в обоих языках:

//--- Hour: extra harmonics for sub-hourly bars; base harmonic only for H1+
int hour_harms = m_extra_hour_harms ? m_n_terms : 1;
for(int k = 1; k <= hour_harms; k++)
  {
   double ang = 2.0 * M_PI * k * hour / 24.0;
   features[idx++] = MathSin(ang);
   features[idx++] = MathCos(ang);
  }

//--- Day-of-week (base harmonic, cycle 7)
double dow_ang = 2.0 * M_PI * py_dow / 7.0;
features[idx++] = MathSin(dow_ang);
features[idx++] = MathCos(dow_ang);

//--- Day-of-year (base harmonic, cycle 366)
double doy_ang = 2.0 * M_PI * py_doy / 366.0;
features[idx++] = MathSin(doy_ang);
features[idx++] = MathCos(doy_ang);

Переменная частотного фильтра m_extra_hour_harms устанавливается в true, когда таймфрейм меньше часа (от M1 до M30). Это воспроизводит поведение Python: для минутных баров аргумент extra_fourier_features=["hour"] активирует несколько гармоник для часа и добавляет к каждой суффикс _h{k}; для часовых и старших баров extra_fourier_features=[] отключает это, и генерируется только базовая гармоника (без суффикса). Реестр имён признаков в _BuildNames точно отражает это, поэтому FeatureNames() всегда возвращает корректные имена столбцов, совместимые с Python.


Определение торговых сессий: пограничный случай с переходом через полночь

Принадлежность к торговой сессии определяется часом по UTC, извлечённым из преобразованной временной метки бара. Три из четырёх торговых сессий имеют непрерывные временные интервалы, не пересекающие полночь: Токио (00:00–08:59), Лондон (07:00–15:59) и Нью-Йорк (13:00–21:59). Проверка их принадлежности представляет собой простое условие «И». Сиднейская торговая сессия (21:00–05:59) пересекает полночь и требует условия «ИЛИ» — той же логики, что используется в реализации на Python:

//+------------------------------------------------------------------+
//| InSession: true when UTC hour h falls inside session s           |
//+------------------------------------------------------------------+
bool CTimeFeatures::_InSession(const int h, const int s) const
  {
   if(m_s_xmid[s])
      return(h >= m_s_start[s] || h < m_s_end[s]);
   return(h >= m_s_start[s] && h < m_s_end[s]);
  }

Границы торговых сессий хранятся в виде массивов экземпляра m_s_start[], m_s_end[] и m_s_xmid[], инициализированных в конструкторе. Индикатор перекрытия и четыре условия календарных эффектов вычисляются в приватном методе _Activity, который определяет, в какой из девяти кольцевых буферов следует записать данные для данного бара:

//+------------------------------------------------------------------+
//| Activity: populate the 9 active-buffer flags for a given bar     |
//+------------------------------------------------------------------+
void CTimeFeatures::_Activity(
   const int         hour,
   const MqlDateTime &dt,
   bool              &active[]
) const
  {
//--- MQL5 day_of_week: 0=Sunday; Python dayofweek: 0=Monday
   int py_dow = (dt.day_of_week == 0) ? 6 : dt.day_of_week - 1;

//--- Four main sessions
   int n_active = 0;
   for(int i = VB_SYDNEY; i <= VB_NY; i++)
     {
      active[i] = _InSession(hour, i);
      if(active[i])
         n_active++;
     }

//--- Overlap: two or more sessions simultaneously active
   active[VB_OVERLAP] = (n_active > 1);

//--- Calendar effects
//--- friday_ny_close: Python day_of_week==4 (Fri) and hour>=21
//--- sunday_open:     Python day_of_week==6 (Sun) and hour<=2
//--- month_end:       Python day >= 28
//--- quarter_end:     Python month%3==0 and day>=28
   active[VB_FRIDAY]      = (py_dow == 4 && hour >= 21);
   active[VB_SUNDAY_OPEN] = (dt.day_of_week == 0 && hour <= 2);
   active[VB_MONTH_END]   = (dt.day >= 28);
   active[VB_QUARTER_END] = (dt.mon % 3 == 0 && dt.day >= 28);
  }

Стоит отметить одну асимметрию в календарных условиях: условие sunday_open использует собственное для MQL5 значение dt.day_of_week == 0 (воскресенье в кодировке MQL5), поскольку оно проверяет первый день недели, а не день относительно понедельника. В условии friday_ny_close используется преобразованное значение py_dow == 4, поскольку оно проверяет пятый рабочий день, где важно принятое в Python соглашение с нулевым номером для понедельника. Использование неверного соглашения для любого из этих условий привело бы к незаметному срабатыванию не в тот день недели.


Волатильность сессии: кольцевой буфер

В Python волатильность торговой сессии вычисляется одной операцией pandas: ряд логарифмической доходности маскируется по барам внутри сессии, вызывается .rolling(20).std(ddof=0) (с указанием ddof=0 для соответствия стандартному отклонению генеральной совокупности NumPy, которое отличается от используемого в pandas по умолчанию ddof=1), затем выполняется прямое заполнение для всех баров и сдвиг на один бар с помощью .shift(1). Эта операция имеет доступ ко всей истории сразу. В MQL5 эквивалентную логику необходимо поддерживать инкрементально: каждый новый закрытый бар даёт одну новую логарифмическую доходность, которая записывается в кольцевой буфер каждой торговой сессии, к которой относится этот бар.

Класс CRingBuffer содержит массив фиксированного размера из значений типа double в кольцевом порядке. Push записывает новое значение в текущую позицию головки и сдвигает головку вперёд, перезаписывая самое старое значение после заполнения буфера. Std проходит по занятой части массива для вычисления стандартного отклонения генеральной совокупности:

   //+------------------------------------------------------------------+
   //| Push: append value at head position and advance the head index   |
   //+------------------------------------------------------------------+
   void CRingBuffer::Push(const double value)
     {
      m_data[m_head] = value;
      m_head         = (m_head + 1) % m_size;
      if(m_count < m_size)
         m_count++;
     }

   //+------------------------------------------------------------------+
   //| Std: population standard deviation (ddof=0) of buffered values   |
   //| Matches NumPy default used in Python's rolling volatility calc   |
   //+------------------------------------------------------------------+
   double CRingBuffer::Std(void) const
     {
      if(m_count < 2)
         return(0.0);
      double sum = 0.0, ssq = 0.0;
      for(int i = 0; i < m_count; i++)
        {
         sum += m_data[i];
         ssq += m_data[i] * m_data[i];
        }
      double mean = sum / m_count;
      double var  = ssq / m_count - mean * mean;
      return(var > 0.0 ? MathSqrt(var) : 0.0);
     }

Метод Update класса CTimeFeatures записывает логарифмическую доходность в каждый активный буфер и сразу вычисляет и сохраняет новое стандартное отклонение в m_last_vol[i]. Для неактивных торговых сессий значение m_last_vol[i] остаётся неизменным; это и есть прямое заполнение, которое Python реализует с помощью .reindex(method="ffill"). Лаг в один бар достигается структурно, а не явно: метод Update вызывается для закрытого бара (индекс бара 1) до того, как метод Calculate вызывается для только что открытого бара (индекс бара 0). Когда метод Calculate считывает m_last_vol, найденные значения в последний раз были обновлены на основе доходности бара 1, а не бара 0. Явный вызов shift(1) не требуется.

Одно отличие от Python сделано намеренно. Python вычисляет волатильность для всех девяти сессионных и календарных столбцов. Таким образом, в выходные данные включаются friday_ny_close_vol, sunday_open_vol, month_end_vol и quarter_end_vol даже в тех случаях, когда частотный фильтр исключает соответствующие столбцы-флаги на внутридневных таймфреймах. Данная реализация на MQL5 сохраняет это поведение: все девять столбцов волатильности (столбцы с суффиксом _vol) присутствуют в массиве features[] независимо от таймфрейма. Их удаление нарушило бы совместимость с ONNX-моделями, обученными на выходных данных Python.

Пример выходных значений признаков: M15 EURUSD, n_terms=3, 48-часовое окно

Рисунок 3. Трёхпанельная иллюстрация выходных значений признаков за 48-часовое окно M15 (n_terms=3)

  • Панель (a): циклическое кодирование часа с n_terms=3 на таймфрейме младше H1. Пара k=1 (сплошные линии) кодирует доминирующий 24-часовой цикл; пара k=2 (пунктирные линии) добавляет второе колебание в сутки, повышая разрешение для бимодальной структуры внутридневной волатильности. На H1 и старших таймфреймах генерируется только k=1; для k=2 и k=3 требуются и таймфрейм младше H1, и n_terms=3.
  • Панель (b): Флаги торговых сессий для обоих торговых дней. Сиднейская сессия (жёлтая) становится активной примерно в 21:00 UTC и отключается в 06:00. Перекрытия сессий Токио–Лондон и Лондон–Нью-Йорк (фиолетовым) видны, поскольку индикатор перекрытия срабатывает в течение двух ежедневных окон перекрытия.
  • Панель (c): скользящая волатильность для лондонской сессии, нью-йоркской сессии и перекрытия сессий. Каждая серия обновляется только тогда, когда соответствующая торговая сессия активна; в остальных случаях сохраняется предыдущая оценка. Ряд перекрытия имеет повышенные значения по сравнению с отдельными сессиями, поскольку он накапливает значения доходности только из окна дня с наибольшей активностью.


Частотный фильтр

Частотный фильтр определяет, какие признаки присутствуют в выходном массиве в зависимости от таймфрейма, переданного в метод Initialize. Он воспроизводит двухэтапную логику функции Python get_time_features:

  1. Таймфреймы короче часа (M1–M30): признак часа получает несколько гармоник вплоть до n_terms с суффиксом _h{k}. Для «дня недели» и «дня года» используется только базовая гармоника без суффикса. Столбцы флагов календарных эффектов удаляются.
  2. Часовые таймфреймы (H1–H12): для признака часа используется только базовая гармоника без суффикса. Для дня недели и дня года правила такие же, как для таймфреймов короче часа. Столбцы флагов календарных эффектов удаляются.
  3. Дневные и более высокие таймфреймы (D1, W1, MN1): признак часа получает только базовую гармонику. В выходные данные включены четыре столбца с флагами календарных эффектов: friday_ny_close, sunday_open, month_end, quarter_end.

Два логических флага — m_extra_hour_harms и m_include_calendar — устанавливаются в Initialize с помощью двух закрытых вспомогательных методов и проверяются как в Calculate, так и в _BuildNames:

m_extra_hour_harms = _IsSubHourly();
m_include_calendar = _IsDailyPlus();

Оба вспомогательных метода используют сравнения через PeriodSeconds вместо явных операторов switch по перечислению, поэтому фильтр обрабатывает пользовательские таймфреймы — PERIOD_H2, PERIOD_H3, PERIOD_H6 — без изменений. Любой период короче одного часа активирует дополнительные гармоники часа; любой период продолжительностью не менее одного дня активирует календарные эффекты.

Массив имён признаков формируется однократно при инициализации, поэтому FeatureCount() и FeatureNames() всегда отражают фактическое количество столбцов для настроенного таймфрейма. Модели ONNX, которые загружают спецификацию признаков из файла, могут вызывать FeatureNames, чтобы во время выполнения проверить, соответствует ли текущий набор признаков спецификации, использовавшейся при обучении:

string names[];
g_tf.FeatureNames(names);
if(ArraySize(names) != expected_feature_count)
  {
   Print("Feature count mismatch — check timeframe and n_terms");
   return(INIT_FAILED);
  }


Интеграция с советником

Поместите файл CTimeFeatures.mqh в каталог MQL5\Include\Features . Используйте синтаксис с угловыми скобками во всех файлах советников и скриптов: #include <CTimeFeatures.mqh>. Синтаксис с двойными кавычками предназначен для относительных подключений из того же каталога и не будет работать, когда советник компилируется из MQL5\Experts\ или MQL5\Scripts\.

Канонический шаблон интеграции включает четыре строки в OnInit и четыре — в OnTick. OnInit инициализирует класс, запускает цикл прогрева для заполнения кольцевых буферов историческими значениями доходности баров и записывает в журнал количество признаков. OnTick обнаруживает новые бары, вызывает Update для только что закрывшегося бара и Calculate для только что открывшегося бара:

//--- Global scope
#include <Features\CTimeFeatures.mqh>
CTimeFeatures g_tf;
input int InpNTerms = 3;    // Fourier harmonics
input int InpWarmup = 100;  // Historical bars for vol warmup

//+------------------------------------------------------------------+
//| OnInit: initialize CTimeFeatures and warm up ring buffers        |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   if(!g_tf.Initialize(_Period, InpNTerms, true, true))
      return(INIT_FAILED);

   int total        = iBars(_Symbol, _Period);
   int warmup_start = MathMin(InpWarmup, total - 2);
   for(int i = warmup_start; i >= 1; i--)
      g_tf.Update(iClose(_Symbol, _Period, i),
                  iTime(_Symbol, _Period, i));

   PrintFormat("CTimeFeatures ready: %d features", g_tf.FeatureCount());
   return(INIT_SUCCEEDED);
  }


//+------------------------------------------------------------------+
//| OnTick: update buffers on bar close and compute features         |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   static datetime s_last_bar = 0;
   datetime cur_bar = iTime(_Symbol, _Period, 0);
   if(cur_bar == s_last_bar)
      return;
   s_last_bar = cur_bar;

   //--- Update vol buffers with the just-closed bar (index 1)
   g_tf.Update(iClose(_Symbol, _Period, 1),
               iTime(_Symbol, _Period, 1));

   //--- Compute features for the newly opened bar (index 0)
   double features[];
   if(!g_tf.Calculate(cur_bar, features))
      return;

   //--- features[] is ready: pass to OnnxRun(), write to CSV, etc.
  }

Следует обратить внимание на две детали в цикле прогрева. Цикл выполняется от warmup_start вниз до 1 — а не до 0, — поскольку бар 0 является текущим баром, который пока остаётся открытым. Его цена закрытия постоянно меняется и ни в коем случае не должна попадать в буфер волатильности. Направление цикла — от самого старого к самому новому (по убыванию индекса баров), чтобы сохранить хронологический порядок, которого ожидает Update; изменение направления цикла привело бы к загрузке значений доходности в обратном хронологическом порядке, что вызвало бы некорректное состояние кольцевого буфера.

Глубина прогрева в 100 баров является практическим минимумом. CRingBuffer хранит 20 значений в каждом буфере. Бары сиднейской сессии на H1 встречаются примерно 9 часов в сутки; за 100 баров H1 (примерно четыре торговых дня) буфер Сиднея накапливает около 37 записей — этого достаточно, чтобы достичь полной ёмкости. Буферы лондонской и нью-йоркской сессий прогреваются быстрее. Буферы календарных эффектов — особенно quarter_end, который срабатывает лишь четыре раза в год, — почти наверняка не достигнут полной ёмкости после прогрева на 100 барах; их оценки волатильности следует считать ненадёжными до тех пор, пока не будет наблюдаться полный календарный квартал. Для моделей ONNX, использующих все 20 признаков, более консервативным подходом будет обеспечить как минимум 200–300 баров прогрева.


Справочник по количеству признаков

В приведённой ниже таблице указано полное количество признаков для каждого класса таймфреймов. Все значения количества предполагают forex=true, is_time_bar=true и указанное значение n_terms. Девять столбцов волатильности (столбцы с суффиксом _vol) всегда присутствуют независимо от таймфрейма, поскольку частотный фильтр в Python отбрасывает только столбцы календарных флагов, но не соответствующие им столбцы волатильности. Когда is_time_bar=false, перед всеми остальными признаками добавляются в начало два дополнительных столбца — bar_duration и bar_duration_accel, что увеличивает каждое итоговое количество на 2.

Таймфрейм Циклические Флаги Волатильность Всего (n_terms=3)
M1–M30 час × (2·n_terms) + день недели×2 + день года×2 5 9 10 + 5 + 9 = 24
H1–H12 час×2 + день недели×2 + день года×2 5 9 6 + 5 + 9 = 20
D1, W1, MN1 час×2 + день недели×2 + день года×2 5 + 4 = 9 9 6 + 9 + 9 = 24

Для проверки запустите скрипт TimeFeaturesVerify.mq5 на графике EURUSD с периодом H1. Он выводит в журнал Experts строку заголовка, разделённую табуляцией, а затем по одной строке для каждого бара. Скопируйте эти строки в CSV-файл и сравните их с выводом Python-функции get_time_features(df, timeframe="H1", forex=True) для того же диапазона дат. При работе с брокером в UTC все значения циклических признаков и флагов торговых сессий должны совпадать с точностью до шести знаков после запятой. Девять столбцов волатильности в выводе скрипта не будут точно совпадать со сдвинутым выводом Python: скрипт вызывает Update непосредственно перед Calculate для каждого выводимого бара, поэтому значения волатильности отражают доходность текущего бара, а не предыдущего. Это намеренное упрощение в проверочном скрипте, призванное сократить количество строк; шаблон рабочей интеграции, приведённый в разделе 8, сохраняет правильную задержку на один бар. У брокера с часовым поясом UTC+2 столбцы с флагами торговых сессий будут дополнительно отличаться на два часа для всех баров, расположенных вблизи границ торговых сессий.


Заключение

Каждая из трёх технических проблем, отличающих реализацию MQL5 от механического перевода кода, имеет единственное правильное решение. Проблема смещения относительно UTC решается так: при инициализации фиксируется TimeGMT() − TimeCurrent(), и это значение добавляется к каждой временной метке брокера перед определением торговой сессии; это устраняет ошибку классификации на 2–3 часа, которая иначе искажала бы флаги торговых сессий на протяжении всего бэктеста у любого брокера не в UTC. Проблема инкрементальной волатильности решается с помощью CRingBuffer: добавлять значение на барах активной сессии, сохранять прежнее значение на неактивных барах и считывать сохранённое значение в Calculate — такая структура воспроизводит операции pandas по прямому заполнению и сдвигу без доступа ко всей истории. Проблема частотного фильтра решается вычислением двух логических флагов в Initialize и использованием их как условия как в Calculate, так и в _BuildNames, благодаря чему FeatureCount() и массив признаков всегда согласованы.

Вывод Calculate напрямую совместим с выводом Python-функции get_time_features для того же таймфрейма и того же значения n_terms. Модель ONNX, обученная на признаках, рассчитанных в Python для EURUSD H1, получает через OnnxRun массив, численно и структурно эквивалентный тому, на чём она обучалась; переиндексация, изменение порядка или переименование признаков не требуются. Вместе с ценовыми признаками с дробным дифференцированием из части 2 это завершает конвейер признаков MQL5 для первых двух слоёв признаков в схеме: ценовых признаков и признаков временного контекста.


Список литературы

  1. Лопес де Прадо, М. (2018). Достижения в области финансового машинного обучения. Wiley. Ссылка на страницу издателя
  2. Броквелл, П. Дж. и Дэвис, Р. А. (2002). Введение в анализ временных рядов и прогнозирование (2-е изд.). Springer. SpringerLink
  3. Дакорогна, М., Генчай, Р., Мюллер, У. А., Пикте, О. В. и Олсен, Р. Б. (2001). Введение в высокочастотные финансы. Academic Press. ScienceDirect
  4. MetaQuotes Ltd. (2024). Справочник MQL5: функции даты и времени. Ссылка на справочник MQL5


Прикрепленные файлы

Файл Папка Описание
CTimeFeatures.mqh MQL5\Include\Features Единый include-файл: CRingBuffer и CTimeFeatures с методами Initialize, Update, Calculate, FeatureCount, FeatureNames
TimeFeaturesVerify.mq5 MQL5\Scripts Скрипт: выводит в журнал Experts значения признаков, разделенные табуляцией, для сравнения с выводом Python
TimeFeaturesDemoEA.mq5 MQL5\Experts Минимальный советник, демонстрирующий шаблон прогрева в OnInit и интеграцию по закрытию бара в OnTick

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22517

Прикрепленные файлы |
CTimeFeatures.mqh (19.17 KB)
Индикатор средней цены входа маркет-мейкера Индикатор средней цены входа маркет-мейкера
В статье исследуется, можно ли по обычным данным MetaTrader 5 оценить среднюю стоимость позиции пассивной стороны рынка, если реальные сделки и позиции маркет-мейкеров недоступны. Пошагово строится модель: от оценки направленного потока и виртуальной позиции до Inventory Basis, Reservation Price по Avellaneda–Stoikov и адаптивных Bid/Ask с учётом токсичности потока.
От начального к среднему уровню: Файл шаблона графика (II) От начального к среднему уровню: Файл шаблона графика (II)
В статье показано, как применять индикаторы к графикам, созданным с помощью объекта OBJ_CHART, используя шаблоны, когда ChartIndicatorAdd в этом контексте не работает. Мы объясняем, как сохранить шаблон с индикатором, получить идентификатор графика и применить ChartApplyTemplate к нужной цели. Вы научитесь интегрировать индикаторы как ресурс, автоматизировать настройку встроенного графика и поймёте некоторые практические ограничения, например то, что индикаторы не появляются в стандартном окне.
От начального к среднему уровню: Файл шаблона графика (III) От начального к среднему уровню: Файл шаблона графика (III)
Некоторое время назад я опубликовал статью, которая вызвала большую путаницу, и многие из тех, кто её прочитал, плохо её поняли. Итак, в этой статье мы как раз вернёмся к тому понятию, которое в своё время, казалось, не имело никакого смысла, и объясним его гораздо лучше. То есть мы рассмотрим, как разместить несколько индикаторов в одном подокне так, чтобы их можно было легко читать и понимать.
Альтернатива временным барам (Часть 3): Индикатор в подокне для отображения альтернативных баров в MQL5 Альтернатива временным барам (Часть 3): Индикатор в подокне для отображения альтернативных баров в MQL5
AlternativeBarsViewer — индикатор в подокне, отрисовывающий все десять типов альтернативных баров как свечи с цветовой маркировкой. Он использует ту же иерархию CBarConstructor, что и BarBuilderEA, обеспечивая идентичность баров. Он поддерживает три источника данных (реальные тики, синтетические тики на основе OHLC или CSV-файл советника) и два режима отображения (TIME и INDEX), между которыми можно переключаться во время работы. Вырожденные бары выделяются, а сводная информация о них выводится на компактной панели, что позволяет выполнять калибровку в реальном времени, не выходя из терминала.