Инжиниринг признаков для машинного обучения (Часть 3): Временные признаки с учетом торговых сессий на рынке Forex
Оглавление
- Введение
- Проблема сырых временных меток
- Циклическое кодирование: почему целые числа не работают
- Гармоники Фурье: моделирование сложных паттернов
- Рыночные часы Forex: четыре сессии и их структура
- Перекрытия сессий и кластеризация волатильности
- Календарные эффекты Forex
- encode_cyclical_features: реализация
- trading_session_encoded_features: реализация
- Оркестрация: get_time_features
- Интеграция в конвейер
- Заключение
- Список литературы
Введение
Каждый бар финансового временного ряда несет временную метку. Большинство ML-конвейеров ее отбрасывает. Временная метка преобразуется в целевую метку, задается горизонт lookahead, признаки строятся по истории цен и объемов, а datetime-индекс больше не используется. Это существенная потеря информации. Само время кодирует богатую структуру — внутридневный ритм рынка, границы торговых сессий, приближение месячных фиксингов, ритм недельного открытия, — и ничто из этого не видно непосредственно в ряду цен или доходностей.
Сложность заключается в представлении. Сырые целочисленные временные метки бессмысленны для регрессионной или классификационной модели: число 1735689600 (Unix epoch) не передает циклической структуры. Наивный целочисленный столбец часа подразумевает, что час 23 дальше от часа 0, чем от часа 18, что геометрически неверно. Бинарный флаг сессии фиксирует присутствие в сессии, но теряет положение внутри нее. Одна гармоника отражает доминирующий дневной цикл, но пропускает асимметрию между спокойной азиатской сессией и волатильным перекрытием Лондона и Нью-Йорка.
В статье предлагается методически обоснованный подход к построению временных признаков для финансового ML. Рассматриваются теория циклического кодирования по Фурье, структура четырех основных торговых сессий Forex, сессионные признаки волатильности и календарные эффекты, влияющие на институциональный поток ордеров около границ периодов. Реализация — функция get_time_features из библиотеки afml, которая объединяет все компоненты в единый готовый для ML DataFrame признаков. Часть 1 серии была посвящена дробному дифференцированию ценовых признаков; в части 2 этот механизм был реализован в MQL5. Здесь рассматривается ортогональный вопрос: как кодировать временной контекст, в котором возникает ценовое наблюдение.
Проблема сырых временных меток
Рассмотрим, что видит алгоритм обучения с учителем, когда получает datetime-столбец, закодированный целыми числами. Регрессионное дерево, выполняющее разбиение по hour = 12, считает расстояние между часами 12 и 13 таким же, как между часами 0 и 23. Линейная модель назначает коэффициент целочисленному часу и экстраполирует: если признак в 10 часов дает некоторый эффект, модель подразумевает, что в 20 часов эффект будет ровно вдвое больше. Ни одно из этих представлений не отражает работу финансовых рынков.
Корневая проблема состоит в том, что время циклично. Цикл часа суток имеет период 24: 23:59 и 00:00 разделены одной минутой, а не 23 часами 59 минутами. Цикл дня недели имеет период 7: воскресенье и суббота соседствуют, а не находятся далеко друг от друга. Целочисленное кодирование отображает эти циклы на линейную ось и разрушает их топологию. Модель не может обнаружить соседство границ периода, не исправив сначала ущерб, внесенный предобработкой.
В финансовых приложениях возникают еще три осложнения:
- Неоднородная сессионная волатильность. Модель, обученная на всей торговой неделе, неявно предполагает, что наблюдения взяты из стационарного распределения. Это не так. Годовая волатильность основной валютной пары в токийскую сессию может быть вдвое (или более) выше волатильности в лондонскую сессию. Без флагов сессий модель не может обучиться поведению, зависящему от торговой сессии.
- Нерегулярные календарные эффекты. Даты окончания месяца и квартала концентрируют институциональные потоки ребалансировки. Закрытие пятницы перед длинными выходными концентрирует активность по снятию стопов. Это дискретные календарные события, а не паттерны, видимые только в ценах или доходностях.
- Зависимость от таймфрейма. Минутный бар и дневной бар одного инструмента с одной и той же временной меткой несут очень разную временную информацию. Часовой бар 13:00 UTC богат внутридневным сессионным контекстом; дневному бару той же даты важнее контекст месяца и квартала. Универсальный набор признаков тратит степени свободы на нерелевантные циклы и отбрасывает релевантные.
Цель — получить кодирование, которое: (a) геометрически корректно: циклически соседние наблюдения дают похожие векторы признаков; (b) информационно насыщено: представлены разные уровни временной структуры; (c) учитывает частоту: признаки, нерелевантные таймфрейму бара, исключаются.
Циклическое кодирование: почему целые числа не работают
Стандартное решение проблемы циклического кодирования — спроецировать каждую периодическую переменную на единичную окружность с помощью пары тригонометрических функций. Для переменной x с длиной цикла T:
x_sin = sin(2π · x / T), x_cos = cos(2π · x / T)
Пара (x_sin , x_cos) кодирует положение x как точку на единичной окружности. Два наблюдения, близкие внутри цикла, оказываются близкими и по евклидову расстоянию между их парами (sin, cos), независимо от близости их сырых целочисленных значений. Для часа суток при T = 24: час 23 отображается в (sin(2π · 23/24), cos(2π · 23/24)), а час 0 — в (sin(0), cos(0)) = (0, 1). Евклидово расстояние между этими двумя точками окружности примерно равно 0.26, что корректно отражает соседство полуночи и 23:00.

Рисунок 1. Двухпанельная иллюстрация проблемы циклического кодирования
- Панель (a): целочисленное кодирование назначает расстояние 23 между часами 23 и 0. Модель, обучающаяся на таком кодировании, не может обнаружить их соседство без явной инженерии признаков, исправляющей ошибку.
- Панель (b): проекция sin/cos размещает каждый час на единичной окружности. Часы 23, 0 и 1 группируются около cos = 1 на правой стороне окружности, правильно представляя их временную близость. Предположения модели не нарушаются.
Та же логика применима к каждой периодической datetime-переменной: день недели (T = 7), день года (T = 366, с учетом високосных лет), месяц ((T = 12) и неделя года ((T = 52). Функция encode_cyclical_features одновременно применяет это преобразование к часу, дню недели и дню года, при необходимости расширяя каждую переменную несколькими гармониками.
Практическое замечание: одна пара sin/cos кодирует положение на окружности, но не может различать скорость движения по циклу. Две временные метки, разделенные одним часом около середины дня, и две метки, разделенные одним часом около полуночи, дают разные координаты sin/cos, но обе разницы соответствуют одной длине дуги. Это желательное поведение. Одна гармоника не может представить форму волны, которая асимметрично растет и падает в пределах цикла; для этого нужны более высокие гармоники.
Гармоники Фурье: моделирование сложных паттернов
Проекция на единичную окружность — это первый член (k = 1) ряда Фурье. Любой периодический сигнал с периодом T можно с произвольной точностью аппроксимировать суммой синусоид на целых кратных фундаментальной частоты:

Для финансовых временных рядов более высокие гармоники важны, поскольку кодируемые ими паттерны активности не являются простыми синусоидами. Внутридневной профиль волатильности валютной пары имеет выраженную бимодальную форму: умеренный подъем во время азиатской сессии, провал в тихий тихоокеанский промежуток, резкий всплеск на открытии Лондона, плато в период перекрытия Лондон–Нью-Йорк и постепенное снижение к закрытию Азии. Одна пара sin/cos может закодировать только один пик и одну впадину за цикл. Для представления бимодальной структуры Лондон–Нью-Йорк нужна как минимум вторая гармоника (k = 2). Третья гармоника (k = 3) захватывает трехпиковые паттерны, например ритм сессий Токио–Лондон–Нью-Йорк.

Рисунок 2. Трехпанельная иллюстрация гармоник Фурье для часа суток
- Панель (a): первая гармоника (k = 1) дает одну синусоиду с одним пиком и одной впадиной за 24-часовой цикл. Этого достаточно, чтобы отличать утро от вечера, но недостаточно для внутридневных пиков на отдельных открытиях сессий.
- Панель (b): добавление второй гармоники (k = 2) вводит дополнительное колебание, позволяя функции представлять два разных пика в течение дня — например, активность азиатской сессии и европейское открытие.
- Панель (c): третья гармоника добавляет третье колебание и дает разрешение, необходимое для представления трехсессионной структуры глобального рынка Forex внутри одного 24-часового цикла.
На практике параметр n_terms управляет количеством генерируемых гармоник. Значение по умолчанию 3 является консервативным: три гармоники на временной признак дают шесть столбцов (sin_h1, cos_h1, sin_h2, cos_h2, sin_h3, cos_h3), а полный набор для часа, дня недели и дня года добавляет 18 столбцов. Более высокое n_terms повышает разрешение, но увеличивает размерность и риск переобучения на небольших обучающих выборках. Оптимизация по частоте, обсуждаемая в разделе 10, смягчает это за счет подавления внутридневных гармоник для дневных и недельных баров, где детализация уровня сессий нерелевантна.
Рыночные часы Forex: четыре сессии и их структура
Глобальный рынок Forex работает 24 часа в сутки в течение рабочей недели, но эта непрерывность обманчива: ликвидность, волатильность и институциональное участие распределены по торговому дню крайне неравномерно. Рынок организован вокруг четырех основных сессий, каждая из которых связана с географическим финансовым центром и своим набором институциональных участников.

Рисунок 3. Двухпанельная иллюстрация сессий Forex и внутридневной волатильности
- Панель (a): четыре сессии на шкале UTC. Сидней (синий) пересекает полночь и идет с 21:00 до 06:00 UTC. Токио (зеленый) идет 00:00–09:00. Лондон (оранжевый) — 07:00–16:00. Нью-Йорк (фиолетовый) — 13:00–22:00. Окна перекрытия 07:00–09:00 (Токио/Лондон) и 13:00–16:00 (Лондон/Нью-Йорк) выделены желтым.
- Панель (b): стилизованная внутридневная волатильность по часам. Часы перекрытия (желтые столбцы) стабильно дают максимальную волатильность. Перекрытие Лондон–Нью-Йорк в 13:00–16:00 UTC — самый активный период глобального рынка Forex. Закрытие Нью-Йорка в пятницу в 21:00 UTC отмечает начало низколиквидного периода выходных.
Границы сессий в реализации заданы фиксированными часами UTC, а не локальными рабочими часами, которые меняются при переходах на летнее время. Это осознанный выбор: часы UTC стационарны по годам и инструментам, тогда как кодирование локальных рабочих часов потребовало бы календарной корректировки DST и внесло бы разрывы. Границы сессий сдвигаются на один час относительно локального времени во время DST. Для ML-признаков такая аппроксимация приемлема, поскольку цель — уловить статистическую склонность каждой сессии, а не ее точное институциональное определение.
Сиднейская сессия — единственная, которая пересекает полночь UTC, поэтому расчет флага требует особой обработки. Бар в 22:00 UTC находится внутри сиднейской сессии (start = 21:00), но бар в 05:00 UTC также находится внутри нее (сессия заканчивается в 06:00 не включительно, значит последний полный час — 05:00–05:59 UTC). Логика перехода через полночь использует условие OR, а не AND:
# Cross-midnight session (Sydney): 21:00 to 05:59 UTC is_session = (hours >= start_hour) | (hours < end_hour) # Standard session (e.g. London): 07:00 to 15:59 UTC is_session = (hours >= start_hour) & (hours < end_hour)
Столбцы флагов хранятся как int8, а не как bool или int64, чтобы минимизировать расход памяти. DataFrame флагов сессий за один год часовых баров (примерно 8760 строк) занимает около 70 КБ при int8 против 560 КБ при int64 — существенная разница, когда матрица признаков дублируется по нескольким инструментам в портфельном бэктесте.
Перекрытия сессий и кластеризация волатильности
Самый важный временной признак для внутридневных торговых моделей — не отдельный флаг сессии, а индикатор session_overlap: бинарный флаг, равный 1, когда одновременно активны две или более сессии. Перекрытия концентрируют ликвидность двух географических пулов участников, сужают спреды между ценой покупки и продажи, ускоряют процесс ценового обнаружения и создают максимальную потиковую волатильность торгового дня.
На рынке Forex есть два значимых перекрытия. Перекрытие Токио–Лондон (07:00–09:00 UTC) обозначает переход от азиатского к европейскому участию. Перекрытие Лондон–Нью-Йорк (13:00–16:00 UTC) — самое ликвидное и волатильное окно глобального рынка, на которое приходится непропорционально большая доля дневного диапазона в пунктах для основных валютных пар. Модель, не отличающая часы перекрытия от остальных, неявно смешивает наблюдения с очень разными распределительными свойствами.
Признак session_overlap вычисляется по сумме всех флагов сессий:
out["session_overlap"] = np.where(out.sum(axis=1) > 1, 1, 0)
Расширение волатильности добавляет сессионно-условное скользящее стандартное отклонение логарифмических доходностей для каждой сессии. Для каждого столбца флага сессии функция маскирует ряд логарифмических доходностей только барами внутри этой сессии и вычисляет 20-барное скользящее стандартное отклонение. Значения протягиваются вперед на все бары и сдвигаются на один бар, чтобы избежать заглядывания вперед:
returns = np.log(df["close"]).diff() for session in session_feat: session_mask = session_feat[session] == 1 if session_mask.sum() > 0: session_vol = returns[session_mask].rolling(20, min_periods=1).std() session_feat[f"{session}_vol"] = session_vol.reindex( df.index, method="ffill" ).shift(1)
Сдвиг на один бар критически важен. Без него скользящее стандартное отклонение на баре t включает логарифмическую доходность бара t, то есть величину, которую модель пытается предсказать. Это тонкое смещение lookahead, которое не проявляется как очевидная ошибка, но незаметно завышает прогнозное качество в бэктесте. shift(1) гарантирует, что признак сессионной волатильности на баре t отражает реализованную волатильность сессии, известную на баре t − 1.
Шаг заполнения предыдущим наблюдением не менее важен. Сессионная волатильность вне сессии не определена, поскольку там нет значений доходности, по которым ее считать. Без такого заполнения эти позиции были бы NaN и переходили бы в модель как пропуски. Метод переносит последнюю рассчитанную сессионную волатильность вперед до следующего бара внутри сессии, давая модели чистую оценку того, как выглядела волатильность этой сессии при ее последней активности.
Календарные эффекты Forex
Помимо дневной структуры сессий, рынок Forex демонстрирует повторяющиеся паттерны, связанные с календарным месяцем и кварталом. Они обусловлены предсказуемыми институциональными потоками, а не стохастической ценовой динамикой, и повторяются достаточно регулярно, чтобы оправдать дискретные признаки.
Реализация включает четыре флага календарных эффектов, все они получены из полей day-of-week, day-of-month и month объекта DatetimeIndex:
day_of_week = datetime_index.dayofweek.values # 0=Monday, 6=Sunday day_of_month = datetime_index.day.values month = datetime_index.month.values out["friday_ny_close"] = ((day_of_week == 4) & (hours >= 21)).astype(int) out["sunday_open"] = ((day_of_week == 6) & (hours <= 2)).astype(int) out["month_end"] = (day_of_month >= 28).astype(int) out["quarter_end"] = ((month % 3 == 0) & (day_of_month >= 28)).astype(int)
Каждый флаг отражает отдельное институциональное явление:
- friday_ny_close: последние два часа нью-йоркской сессии в пятницу — финальное окно ликвидности перед выходными. Институциональные маркет-мейкеры сокращают позиции, хедж-фонды переносят или закрывают недельные сделки, а розничные участники могут сталкиваться с маржин-коллами по открытым позициям. Результат — систематический паттерн усиленной направленной активности, за которым следуют низкообъемные гэпы на воскресном открытии.
- sunday_open: первые два часа торговли в Сиднее в воскресенье — возвращение глобального рынка после гэпа выходных. Цена может существенно гэпнуть, если в субботу вышли важные макро-новости, а начальные бары часто показывают поведение с возвратом к среднему, пока рынок закрывает гэпы выходных перед возобновлением направленного движения прошлой недели.
- month_end: дни 28–31 календарного месяца концентрируют институциональные потоки ребалансировки. Управляющие портфелями облигаций ребалансируют duration, фонды акций приводят веса к бенчмаркам, а FX-дески исполняют связанные валютные хеджи. Результат — систематическое направленное давление в отдельных валютных парах, которое резко разворачивается на смене месяца.
- quarter_end: последние три дня марта, июня, сентября и декабря накладывают квартальную ребалансировку поверх эффекта конца месяца. Более крупные институциональные потоки, корректировки валютного overlay пенсионных фондов и приукрашивание отчетности управляющими активами усиливают месячный паттерн на границах квартала.
Эти флаги генерируются, но удаляются частотным фильтром для внутридневных таймфреймов. Для внутридневных баров флаги сессий и признаки часа, кодированные по Фурье гораздо информативнее, чем флаги календарного месяца на уровне M1 или H1, а их включение добавляет скорее шум, чем сигнал.
encode_cyclical_features: реализация
Функция encode_cyclical_features принимает DatetimeIndex и создает DataFrame временных признаков, закодированных через Фурье. Помимо индекса она принимает два параметра: n_terms управляет количеством гармоник (по умолчанию 3), а extra_fourier_features — это необязательный список имен признаков, для которых нужно сгенерировать несколько гармоник. Когда extra_fourier_features не равен None, дополнительные гармоники получают только перечисленные признаки, остальные кодируются одной парой.
def encode_cyclical_features( datetime_index: pd.DatetimeIndex, n_terms: int = 3, extra_fourier_features: list = None, ) -> pd.DataFrame: out = pd.DataFrame(index=datetime_index) features = { "hour": (datetime_index.hour, 24), "dayofweek": (datetime_index.dayofweek, 7), "dayofyear": (datetime_index.dayofyear, 366), } for name, (series, cycle_length) in features.items(): radians = 2 * np.pi * series / cycle_length out[f"{name}_sin"] = np.sin(radians) out[f"{name}_cos"] = np.cos(radians) if n_terms >= 1 and ( extra_fourier_features is None or name in extra_fourier_features ): out.rename(columns={ f"{name}_sin": f"{name}_sin_h1", f"{name}_cos": f"{name}_cos_h1", }, inplace=True) for k in range(2, n_terms + 1): radians_k = 2 * np.pi * k * series / cycle_length out[f"{name}_sin_h{k}"] = np.sin(radians_k) out[f"{name}_cos_h{k}"] = np.cos(radians_k) return out
В этой функции заслуживают комментария три проектных решения:
- Векторизованные операции NumPy над массивами индекса. Функция извлекает datetime_index.hour, datetime_index.dayofweek и datetime_index.dayofyear как целочисленные массивы и применяет np.sin и np.cos напрямую. Это избегает построчной итерации и масштабируется до миллионов баров без деградации производительности.
- Переименование столбцов для гармоник. При запросе мультигармонического вывода первая пара переименовывается из hour_sin в hour_sin_h1 для согласованности с более высокими гармониками. Благодаря этому имена столбцов явно содержат индекс гармоники, что удобно для отладки и для последующего отбора признаков по именам столбцов.
- Выборочное расширение гармониками через extra_fourier_features. Не каждый циклический признак одинаково выигрывает от более высоких гармоник. Для дневных баров цикл дня года может оправдать три гармоники для захвата квартальной сезонности, тогда как цикл дня недели с всего семью позициями уже хорошо представлен одной парой. Параметр extra_fourier_features позволяет вызывающему коду расширять только те признаки, где дополнительные гармоники добавляют сигнал.
Длина цикла для dayofyear задана равной 366, а не 365, чтобы учитывать високосные годы. В невисокосные годы день 366 не встречается, поэтому максимальное наблюдаемое значение — 365. Небольшое сжатие кодирования (день 365 отображается в угол 2π × 365/366 вместо 2π) пренебрежимо мало по сравнению с альтернативой — вычислять таблицу длины года, которую нужно ежегодно обновлять.
trading_session_encoded_features: реализация
Функция сессионных признаков создает два слоя вывода: бинарные флаги сессий и оценки сессионно-условной волатильности. Полная реализация разделена на два логических блока — генерация флагов и расширение волатильности, — которые разнесены в кодовой базе, поскольку блоку волатильности нужен ценовой ряд close из родительского DataFrame, недоступный внутри самой функции сессий.
def trading_session_encoded_features( datetime_index: pd.DatetimeIndex, ) -> pd.DataFrame: # Treat tz-naive index as UTC; convert tz-aware index to UTC if datetime_index.tz is not None: dt_utc = datetime_index.tz_convert("UTC") else: dt_utc = datetime_index.tz_localize("UTC") hours = dt_utc.hour.values out = pd.DataFrame(index=datetime_index) sessions = { "Sydney": {"start": 21, "end": 6, "cross_midnight": True}, "Tokyo": {"start": 0, "end": 9, "cross_midnight": False}, "London": {"start": 7, "end": 16, "cross_midnight": False}, "New_York": {"start": 13, "end": 22, "cross_midnight": False}, } for session_name, params in sessions.items(): s, e, xm = params["start"], params["end"], params["cross_midnight"] is_session = (hours >= s) | (hours < e) if xm else (hours >= s) & (hours < e) col = session_name.replace("New_York", "ny").lower() + "_session" out[col] = is_session.astype("int8") out["session_overlap"] = np.where(out.sum(axis=1) > 1, 1, 0) # Calendar effects appended here (passed through frequency gate in caller) day_of_week = datetime_index.dayofweek.values day_of_month = datetime_index.day.values month = datetime_index.month.values out["friday_ny_close"] = ((day_of_week == 4) & (hours >= 21)).astype(int) out["sunday_open"] = ((day_of_week == 6) & (hours <= 2)).astype(int) out["month_end"] = (day_of_month >= 28).astype(int) out["quarter_end"] = ((month % 3 == 0) & (day_of_month >= 28)).astype(int) return out
Обработка часового пояса в начале функции решает распространенную практическую проблему: MetaTrader 5 экспортирует данные с временными метками в часовом поясе брокера, которые могут быть UTC+2 или UTC+3, тогда как pandas в Python может интерпретировать их как timezone-naive. Вызов tz_localize("UTC") для индекса без часового пояса назначает ему UTC без сдвига временных меток (это корректно, когда данные уже в UTC). Вызов tz_convert("UTC") для индекса с часовым поясом преобразует временные метки в UTC. Это условие поддерживает оба случая без предварительной нормализации.
Оркестрация: get_time_features
Две функции генерации признаков оркестрируются функцией get_time_features, которая обрабатывает ветвление по типу бара, отбор признаков по частоте и финальную конкатенацию в один DataFrame, выровненный по входному индексу.

Рисунок 4. Архитектура оркестрационного конвейера get_time_features()
- На вход верхней части конвейера поступают три входа: DataFrame с DatetimeIndex и столбцом close, строка таймфрейма и опции.
- Условие ветвления проверяет, равен ли bar_type == "time". Для нетаймовых баров (volume bars, dollar bars, tick bars) в начало списка признаков добавляются bar-duration и bar-duration-acceleration признаки.
- Две функции признаков выполняются параллельно, а их результаты собираются в список.
- Частотный фильтр отсекает часть внутридневных признаков на старших таймфреймах и регулирует набор гармоник часа для минутных баров.
- Календарные эффекты добавляются для дневных таймфреймов и выше; для внутридневных они удаляются.
- Все компоненты конкатенируются по внутреннему объединению индексов, сохраняя только бары, присутствующие во всех DataFrame признаков.
Ключевой код оркестрации:
def get_time_features( df: pd.DataFrame, timeframe: str, n_terms: int = 3, bar_type: str = "time", forex: bool = True, ) -> pd.DataFrame: features = [] # Bar duration features for non-time bars if bar_type != "time": durations = df.index.to_series(name="bar_duration").diff().dt.total_seconds() duration_accel = durations.diff().rename("bar_duration_accel") features += [durations, duration_accel] # Frequency-based feature selection timeframe = timeframe.upper() if timeframe.startswith(("H", "D", "W", "MN")): extra_features = [] elif timeframe.startswith("M"): extra_features = ["hour"] else: extra_features = [] cyclical_feat = encode_cyclical_features( df.index, n_terms=n_terms, extra_fourier_features=extra_features ) if forex: session_feat = trading_session_encoded_features(df.index) returns = np.log(df["close"]).diff() for session in session_feat: session_mask = session_feat[session] == 1 if session_mask.sum() > 0: session_vol = returns[session_mask].rolling(20, min_periods=1).std() session_feat[f"{session}_vol"] = session_vol.reindex( df.index, method="ffill" ).shift(1) else: session_feat = pd.DataFrame() features += [cyclical_feat, session_feat] features = pd.concat(features, axis=1, join="inner") if not timeframe.startswith(("D", "W", "MN")): features.drop( columns=["quarter_end", "month_end", "sunday_open", "friday_ny_close"], inplace=True ) return features
Логика частотного фильтра
Переменная extra_features управляет тем, какие временные признаки получают мультигармоническое расширение. Для часовых и более высоких таймфреймов (H, D, W, MN) она задается пустым списком — значит, ни один признак не получает дополнительных гармоник сверх первой пары. Для минутных баров дополнительные гармоники получает только hour, поскольку минутные внутридневные паттерны достаточно плотны, чтобы выиграть от разрешения третьей гармоники. День недели и день года на минутном разрешении кодируются одной парой; на такой гранулярности недельную или годовую циклическую структуру лучше представлять через флаги сессий и календарные эффекты, а не через дополнительные члены Фурье.
Финальный вызов drop удаляет четыре флага календарных эффектов для всех таймфреймов ниже дневного. Это жесткий фильтр: эффекты конца месяца и квартала определены на дневной гранулярности и не имеют смысла для отдельных M5- или H1-баров. Их включение на внутридневном разрешении добавило бы четыре почти всегда нулевых столбца, расходуя емкость модели.
Признаки длительности бара
Для нетаймовых баров (volume bars, dollar bars, tick bars) временное расстояние между барами само по себе информативно. Dollar bar, сформированный за 45 секунд, несет совсем иную информацию о срочности рынка, чем бар, формировавшийся 4 часа. Признак bar_duration измеряет это в секундах. bar_duration_accel — первая разность длительности бара; она отражает ускорение: формируются ли бары быстрее или медленнее предыдущего. Оба признака вычисляются без lookahead с помощью diff(), который дает NaN на первом баре. Этот NaN попадает в выход, но затем обрабатывается стандартным шагом заполнения NaN в последующем конвейере.
Интеграция в конвейер
Выход get_time_features — DataFrame с тем же DatetimeIndex, что и вход, готовый к конкатенации с признаками, производными от цены, перед обучением модели. Типичная интеграция в конвейере afml выглядит так:
import pandas as pd from afml.features.time_features import get_time_features from afml.features.fracdiff import fracdiff_optimal # 1. Load OHLCV data (H1 EURUSD, UTC index) df = pd.read_parquet("eurusd_h1.parquet") # 2. Fractionally differentiated close (from Article 01) ffd_close, _ = fracdiff_optimal(df[["close"]]) # 3. Time features time_feat = get_time_features(df, timeframe="H1", forex=True) # 4. Concatenate into final feature matrix X = pd.concat([ffd_close, time_feat], axis=1, join="inner") X = X.dropna() # remove warm-up rows
Несколько практических соображений определяют взаимодействие временных признаков с более широким конвейером:
- Выравнивание индексов. Внутреннее объединение в pd.concat гарантирует, что сохраняются только бары, присутствующие во всех DataFrame признаков. Если fracdiff_optimal удаляет первые l* баров из-за lookback фиксированного окна, а get_time_features удаляет первый бар из-за NaN после diff(), inner join незаметно выравнивает оба DataFrame по общему индексу. Это желательное поведение: бар не попадает в матрицу признаков, если для него недоступны все признаки.
- Стационарность временных признаков. Временные признаки, кодированные по Фурье стационарны по построению: они колеблются между −1 и 1 без дрейфа. Флаги сессий бинарны и имеют стабильное долгосрочное среднее. Флаги календарных эффектов имеют медленно меняющуюся долю из-за календарных нерегулярностей (високосные годы, сдвиги распределения дней недели), но стационарны при любом разумном ML-предположении. Ни один из этих признаков не требует дробного дифференцирования или другой предобработки для стационарности.
- Масштабирование признаков. Стандартное масштабирование переводит sin/cos признаки из диапазона [−1, 1] примерно к нулевому среднему и единичной дисперсии. Это не искажает циклические отношения, поскольку линейное масштабирование сохраняет относительные позиции. Флаги сессий (бинарные) после стандартного масштабирования становятся примерно Bernoulli-нормализованными. Флаги календарных эффектов имеют очень низкую базовую частоту (quarter_end срабатывает примерно на 3% дневных баров) и в моделях, чувствительных к дисбалансу классов, могут выиграть от иной обработки — передача булевых признаков без преобразования или робастное масштабирование.
- Пошаговая валидация на историческом окне. Временные признаки полностью выводятся из DatetimeIndex и не используют будущие наблюдения. Их безопасно включать в walk-forward validation без дополнительных мер. Признаки сессионной волатильности используют лаг в один бар, что также предотвращает lookahead. Единственное ограничение — у скользящего стандартного отклонения есть 20-барный период прогрева; бары с недостаточной историей получают шумную оценку при min_periods=1. Лучшая практика — добавлять прогревочный буфер минимум 20 баров в начало обучающего окна каждого фолда.
Заключение
Время — богатый источник признаков, который большинство финансовых ML-конвейеров рассматривает как вспомогательный индексный столбец, который затем отбрасывается. Разработанный здесь подход извлекает три ортогональных слоя временной информации: циклические кодировки по Фурье, корректно представляющие топологию периодических временных переменных; флаги сессий и оценки волатильности, которые позволяют модели учитывать институциональную структуру торгового дня; и маркеры календарных эффектов, отражающие повторяющиеся институциональные потоки на границах периодов.
Четыре инженерных решения лежат в основе реализации промышленного уровня. Во-первых, sin/cos-кодирование через проекцию на единичную окружность устраняет искажения расстояний, вносимые целочисленными часами и днями, позволяя моделям с евклидовыми или градиентными предположениями изучать временные паттерны без геометрической предобработки. Во-вторых, мультигармоническое расширение Фурье позволяет набору признаков представлять бимодальные и тримодальные паттерны волатильности глобального рынка Forex, которые одна пара sin/cos захватить не может. В-третьих, признаки сессионной волатильности используют лагированное скользящее стандартное отклонение, замаскированное по внутрисессионным логарифмическим доходностям, что дает модели чистую оценку недавней реализованной волатильности каждой сессии без смещения заглядывания вперед. В-четвертых, частотный фильтр подбирает набор признаков под информационное содержание каждого таймфрейма, удаляя столбцы, которые добавляют шум вместо сигнала.
Вместе с дробно дифференцированными ценовыми признаками из Части 1 и Части 2 серии временные признаки завершают слой временного контекста матрицы признаков. Следующая статья серии посвящена проблеме разметки: как строить метки с заглядыванием вперед для финансового ML, соблюдая метод triple-barrier и избегая смещений заглядывания вперед, которые делают недействительными большинство наивных систем бэктестинга. В последующей статье мы реализуем обсужденные выше методы в MQL5.
Список литературы
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Chapter 4: Labeling, Chapter 5: Fractionally Differentiated Features.
- Brockwell, P. J. and R. A. Davis (2002). Introduction to Time Series and Forecasting. 2nd ed. Springer. Chapter 4: Spectral Analysis.
- Dacorogna, M. et al. (2001). An Introduction to High-Frequency Finance. Academic Press. Chapter 3: Statistical Properties of Foreign Exchange Data.
- Hyndman, R. J. and G. Athanasopoulos (2021). Forecasting: Principles and Practice. 3rd ed. OTexts. Chapter 12: Advanced Forecasting Methods.
- Rasekhschaffe, K. C. and R. C. Jones (2019). "Machine Learning for Stock Selection." Financial Analysts Journal, 75(3), 70–88.
Прикрепленные файлы
| Файл | Описание |
|---|---|
| trading_session.py | Полный модуль временных признаков: encode_cyclical_features, trading_session_encoded_features, get_time_features |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22516
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа Price Action (Часть 64): Синхронизация вручную построенных трендовых линий с автоматическим мониторингом
От начального до среднего уровня: События в объектах (IV)
Знакомство с языком MQL5 (Часть 43): Руководство для начинающих по работе с файлами в MQL5 (V)
Торговые инструменты MQL5 (Часть 28): Полигональная заливка кривой-бабочки в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования