English Deutsch 日本語
preview
Архитектура системы машинного обучения в MetaTrader 5 (Часть 1): Утечка данных и исправление меток времени

Архитектура системы машинного обучения в MetaTrader 5 (Часть 1): Утечка данных и исправление меток времени

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

Введение

Добро пожаловать в первую часть нашей серии статей "MetaTrader 5: План создания систем машинного обучения". Эта статья посвящена критической, но часто упускаемой из виду проблеме при построении надежных моделей машинного обучения для финансовых рынков с использованием данных MetaTrader 5 — "ловушке временных меток". Мы рассмотрим, как неправильная обработка временных меток может привести к коварной утечке данных, ставя под угрозу целостность ваших моделей и генерируя ненадежные торговые сигналы. Что еще важнее, мы предложим конкретные решения и лучшие практики, опираясь на признанные отраслевые исследования, чтобы гарантировать, что ваши данные будут чистыми, непредвзятыми и готовыми к продвинутому количественному анализу.

Целевая аудитория и необходимые знания


Эта статья предназначена для количественных трейдеров, специалистов по обработке данных и разработчиков, обладающих базовыми знаниями Python, Pandas и API MetaTrader 5. Знакомство с основными концепциями машинного обучения также будет полезно. Наша цель — предоставить вам практические знания и инструменты, необходимые для создания высококачественных наборов данных для разработки надежных моделей машинного обучения в алгоритмической торговле.

План серии


Эта статья открывает обширную серию, посвященную созданию полного плана построения системы машинного обучения для MetaTrader 5. В этой части мы закладываем необходимую основу, обеспечивая целостность данных. Будущие темы позволят глубже погрузиться в более продвинутые этапы конвейера машинного обучения, включая:

  • Часть 2 — Продвинутый инжиниринг признаков и разметка данных: Методы определения целевых переменных, отражающих истинную динамику рынка.
  • Часть 3 — Обучение и валидация модели: Лучшие практики обучения, валидации и выбора моделей машинного обучения, адаптированных для финансовых временных рядов.
  • Часть 4 — Тщательное бэктестирование и развертывание: Методологии оценки эффективности модели в реалистичных торговых условиях и стратегии развертывания моделей в реальном времени.


Ловушка временных меток MetaTrader 5: Понимание и предотвращение

Подсматривание данных или утечка данных могут показаться чем-то незначительным, но их влияние на модели машинного обучения может быть колоссальным и разрушительным. Представьте, что вы готовитесь к экзамену, нечаянно  подсмотрев ответы заранее. Ваш отличный результат кажется заслуженным, но на самом деле это обман. Именно это происходит, когда мы используем временные метки MetaTrader 5 по умолчанию в машинном обучении — утечка данных неожиданно разрушает целостность вашей модели.

Как временные метки MetaTrader 5 вводят вас в заблуждение

EURUSD M5 - MetaTrader5

MetaTrader 5 маркирует 5-минутный бар, начинающийся в 18:55, то есть 2-й последний бар сверху, как:
Время Open High Low Close

2 Apr 18:55

  1.08718

  1.08724

  1.08668

  1.08670

Отмечая время в начале, MetaTrader 5 подразумевает, что данные этого бара были доступны в 18:55:00 -за целых 5 минут до его фактического закрытия! Если ваша модель использует это в обучении, это все равно что дать студенту ответы на экзамен за 5 минут до начала тестирования. Чтобы противостоять этому, мы должны избегать использования предварительно скомпилированных временных баров MetaTrader 5, а вместо этого использовать тиковые данные для создания баров, которые мы используем в наших моделях.

Почему утечка данных имеет значение


Утечка данных может незаметно разрушить весь ваш проект по машинному обучению. Это происходит, когда модель случайно обучается на информации, которую не должна была видеть в процессе обучения — например, заглядывая в будущее. В результате модель выглядит невероятно точной во время обучения, но в реальности ее просто накормили ответами, которые она никогда не получила бы в реальном мире.

Вместо изучения реальных закономерностей модель начинает запоминать шум, становясь похожей на студента, который зубрит ответы, не понимая предмета. Это приводит к плохим результатам, когда приходит время делать реальные прогнозы на новых данных.

Что еще хуже, модель, обученная на "загрязненных" данных, может казаться надежной, но потерпит неудачу при развертывании. Она может создать ложное чувство уверенности и привести к принятию плохих решений — что особенно опасно в таких ответственных сферах, как трейдинг, где даже небольшие ошибки могут дорого стоить.

Исправление утечки данных постфактум — занятие неприятное. Обычно это означает необходимость вернуться назад и переделать большие части вашего конвейера, тратя время, вычислительные ресурсы, а иногда и деньги. Вот почему так важно выявлять и предотвращать утечку данных на раннем этапе.

Почему тиковые бары имеют значение: Взгляд с точки зрения количественного анализа


Финансовые данные часто поступают через неравные промежутки времени, и чтобы мы могли использовать для них машинное обучение (MO), их необходимо регуляризовать, поскольку большинство алгоритмов MO ожидают данные в табличном формате. Строки этих таблиц обычно называют "барами". Графики, которые мы видим в MetaTrader 5 и практически на всех других платформах для построения графиков, представляют собой временные бары, которые преобразуют тиковые данные в столбцы Open, High, Low, Close и Volume путем выборки тиков за фиксированный временной горизонт, например, раз в минуту.

Хотя временные бары, возможно, являются самыми популярными среди практиков и ученых, их следует избегать по двум причинам. Во-первых, рынки не обрабатывают информацию с постоянным временным интервалом. Час после открытия намного активнее, чем час около полудня (или час около полуночи в случае фьючерсов). Как биологические существа, людям имеет смысл организовывать свой день в соответствии с циклом солнечного света.

Но сегодняшними рынками управляют алгоритмы, которые торгуют с нестрогим контролем со стороны человека, для которых циклы процессора (CPU) намного важнее, чем хронологические интервалы. Это означает, что временные бары избыточно отбирают информацию в периоды низкой активности и недостаточно отбирают информацию в периоды высокой активности. Во-вторых, ряды, дискретизированные по времени, часто демонстрируют плохие статистические свойства, такие как автокорреляция, гетероскедастичность и ненормальность распределения доходностей.

(Лопез де Прадо, 2018, стр.26)

Когда речь заходит о построении рыночных баров для машинного обучения, часто возникает ключевой вопрос выбора между традиционными временными барами и барами, основанными на активности (например, тиковые, объемные или долларовые бары). Хотя практики обычно тщательно избегают ошибки предвидения (look-ahead bias), используя только информацию, доступную до момента принятия решения, тонкая форма утечки данных все же может возникать при использовании временных баров. Давайте разберемся, почему даже при аккуратной простановке временных меток это может быть проблемой и как бары, основанные на активности, предлагают надежное решение.
  • Понимание подхода практиков: Опытные специалисты совершенно правильно присваивают временным барам метку на момент окончания интервала (например, 09:01:00 для периода 09:00:00–09:00:59.999). Этот важный шаг гарантирует, что вся информация по завершенному бару действительно известна на момент его зафиксированной метки, что предотвращает классическую ошибку предвидения на основе будущих баров.
  • Тонкая утечка данных внутри бара: Однако более тонкая форма утечки данных все же может возникнуть в процессе формирования самого временного бара. Если значимое событие происходит в середине 1-минутного бара (например, в 09:00:35), любые признаки (features), полученные из этого бара (такие как его максимальная цена или флаг события), неизбежно будут включать эту информацию к моменту закрытия бара.
  • Дилемма прогнозирования: Следовательно, если модель машинного обучения делает прогноз или формирует сигнал в начальный момент открытия бара (например, в 09:00:00), используя эти признаки, отражающие более поздние события внутри той же минуты, она неявно получает нечестное преимущество. В реальной торговле в момент времени 09:00:00 событие в 09:00:35 совершенно неизвестно.
  • Бары, основанные на активности, как решение: Бары, основанные на активности, такие как тиковые бары, принципиально обходят эту проблему, завершаясь только после достижения заданного объема рыночной активности (например, установленного количества транзакций или определенного объема/суммы в долларах). Эта внутренняя структура гарантирует, что все признаки такого бара построены на основе информации, которая была полностью доступна в точный момент завершения его формирования, естественным образом согласуясь с потоком информации в реальном времени и предотвращая внутрибаровую ошибку предвидения.

По вышеуказанным причинам временных баров следует избегать при обучении моделей машинного обучения. Вместо них следует использовать бары, создание которых зависит от торговой активности, такие как тиковые бары, бары на основе объема или оборота. Они создаются путем выборки информации после поступления определенного количества тиков, достижения определенного объема торгов или обмена определенной суммы в долларах. Такие бары обеспечивают доходность, более близкую к нормальному распределению с идентичными, независимыми значениями, что делает их более подходящими для моделей машинного обучения, многие из которых предполагают, что наблюдения извлечены из I.I.D гауссовского процесса.

Ниже приведены сравнения распределений логарифмической доходности для M5, M15 и M30 временных и тиковых баров. Размер тиковых баров рассчитывается с использованием медианного количества тиков за таймфрейм для рассматриваемого периода, и для EURUSD в период с 2023 по 2024 год мы получаем тиковые бары размером 200, 500 и 1000 для таймфреймов M5, M15 и M30 соответственно. Это делается с помощью функции calculate_ticks_per_period, которая показана в следующем разделе. 

Сравнение распределения доходности для временных и тиковых баров

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

Давайте проведем более тщательный анализ статистических свойств временных и тиковых баров, используя графики ниже.

 Анализ волатильности EURUSD на 5-минутном таймфрейме (M5) за 2023-2024 гг.

 Анализ волатильности EURUSD на тиковых барах (Tick-200) за 2023-2024 гг.

Из анализа приведенных выше графиков видно, что примерно 20% временных баров объясняют около 51% общего изменения цены, тогда как 20% тиковых баров объясняют около 46% общего изменения цены. Примечательно, что практически все доли тиковых баров объясняют меньшую часть общего изменения цены, чем те же доли временных баров, что указывает на то, что тиковые бары лучше производят выборку информации, чем временные. Гистограмма подтверждает это, показывая нам, что абсолютное изменение цены тиковых баров подчиняется гораздо более статистически приемлемому распределению (монотонно убывающему), чем у временных баров, которое имеет хаотичные дисперсии.

В этой и последующих статьях мы сосредоточимся на применении машинного обучения к валютным инструментам Forex. Поскольку они не торгуются на централизованной бирже, информация об объеме недоступна, и поэтому я ограничу рамки этой серии временными и тиковыми барами. Читатель должен отметить, что выше я описал только стандартные формы баров. Для получения дополнительной информации о продвинутых формах свечных баров я рекомендую эту статью, в которой подробно рассматривается работа Маркоса Лопеса де Прадо в книге "Машинное обучение: алгоритмы для бизнеса", конспекты семинаров по которой можно найти в интернете .


Решение: Переписываем временную реальность путем создания баров из тиковых данных

Реализация Кода

Давайте начнем с получения данных из нашего терминала и их очистки, чтобы предотвратить возможность использования ошибочных тиков для создания наших баровых данных. Я продемонстрирую, как создавать временные и тиковые бары. Мы будем использовать Python из-за удобства манипуляций с временными рядами в pandas и простоты его использования для машинного обучения. 

Шаг 0: Импорты 


Это импорты, которые мы будем использовать для фрагментов кода в этой статье.

import numpy as np
import pandas as pd
import MetaTrader5 as mt5
import logging
from datetime import datetime as dt

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')



Шаг 1: Извлечение данных

def get_ticks(symbol, start_date, end_date):
    """
     Загружает тиковые данные из терминала MT5.

     Параметры:
        symbol (str): Финансовый инструмент (например, валютная пара или акция).
start_date, end_date (str or datetime): Временный диапазон для данных (ГГГГ-ММ-ДД).

     Returns:
        pd.DataFrame: Тиковые данные с индексом datetime.
    """
    if not mt5.initialize():
        logging.error("MT5 connection not established.")
        raise RuntimeError("MT5 connection error.")

    start_date = pd.Timestamp(start_date, tz='UTC') if isinstance(start_date, str) else (
        start_date if start_date.tzinfo is not None else pd.Timestamp(start_date, tz='UTC')
    )
    end_date = pd.Timestamp(end_date, tz='UTC') if isinstance(end_date, str) else (
        end_date if end_date.tzinfo is not None else pd.Timestamp(end_date, tz='UTC')
    )

    try:
        ticks = mt5.copy_ticks_range(symbol, start_date, end_date, mt5.COPY_TICKS_ALL)
        df = pd.DataFrame(ticks)
        df['time'] = pd.to_datetime(df['time_msc'], unit='ms')
        df.set_index('time', inplace=True)
        df.drop('time_msc', axis=1, inplace=True)
        df = df[df.columns[df.any()]]
        df.info()
    except Exception as e:
        logging.error(f"Error while downloading ticks: {e}")
        return None

    return df



Шаг 2: Очистка данных

def clean_tick_data(df: pd.DataFrame,
                    n_digits: int,
                    timezone: str = 'UTC'
                    ) -> Optional[pd.DataFrame]:
    """
    Очистка и проверка тиковых данных Forex с комплексными проверками качества.

     Параметры:
        df: DataFrame, содержащий тиковые данные с ценами bid/ask и индексом timestamp
        n_digits: Количество знаков после запятой в цене инструмента.
timezone: Часовой пояс для локализации/преобразования временных меток (по умолчанию: UTC)

    Возвращает:
        Очищенный DataFrame или None, если после очистки он пуст
    """
    if df.empty:
        return None

    df = df.copy(deep=False)  # Работаем с копией, чтобы не изменять исходный DataFrame 
    n_initial = df.shape[0] # Сохраняем начальное количество строк для отчетности

    # 1. Ensure proper datetime index
    # Use errors='coerce' to turn unparseable dates into NaT and then drop them.
    if not isinstance(df.index, pd.DatetimeIndex):
        original_index_name = df.index.name
        df.index = pd.to_datetime(df.index, errors='coerce')
        nan_idx_count = df.index.isnull().sum()
        if nan_idx_count > 0:
            logging.info(f"Dropped {nan_idx_count:,} rows with unparseable timestamps.")
            df = df[~df.index.isnull()]
        if original_index_name:
            df.index.name = original_index_name
    
    if df.empty: # Check if empty after index cleaning
        logging.warning("Warning: DataFrame empty after initial index cleaning")
        return None

    # 2. Timezone handling
    if df.index.tz is None:
        df = df.tz_localize(timezone)
    elif str(df.index.tz) != timezone.upper():
        df = df.tz_convert(timezone)
    
    # 3. Проверка валидности цен
    # Применяем округление, а затем фильтрацию
    df['bid'] = df['bid'].round(n_digits)
    df['ask'] = df['ask'].round(n_digits)

    # Проверка цен
    price_filter = (
        (df['bid'] > 0) &
        (df['ask'] > 0) &
        (df['ask'] > df['bid'])
    )
    
    n_before_price_filter = df.shape[0]
    df = df[price_filter]
    n_filtered_prices = n_before_price_filter - df.shape[0]
    if n_filtered_prices > 0:
        logging.info(f"Filtered {n_filtered_prices:,} ({n_filtered_prices / n_before_price_filter:.2%}) invalid prices.")

    if df.empty: # Проверяем, не пуст ли DataFrame после очистки цен
        logging.warning("Предупреждение: DataFrame пуст после очистки цен")
        return None
    
    # Удаление значений NA
    initial_rows_before_na = df.shape[0]
    if df.isna().any().any(): # Use .any().any() to check if any NA exists in the whole DF
        na_counts = df.isna().sum()
        na_cols = na_counts[na_counts > 0]
        if not na_cols.empty:
            logging.info(f'Удаленны значения NA из колонок: \n{na_cols}')
            df.dropna(inplace=True)

    n_dropped_na = initial_rows_before_na - df.shape[0]
    if n_dropped_na > 0:
        logging.info(f"Dropped {n_dropped_na:,} ({n_dropped_na / n_before_price_filter:.2%}) rows due to NA values.")

    if df.empty: # Проверяем, не пуст ли DataFrame после очистки NA
        logging.warning("Предупреждение: DataFrame пуст после очистки NA")
        return None
    
    # 4. Обработка микросекунд
    if not df.index.microsecond.any():
        logging.warning("Предупреждение: Не найдено временных меток с точностью до микросекунд")
    
    # 5. Обработка дубликатов
    duplicate_mask = df.index.duplicated(keep='last')
    dup_count = duplicate_mask.sum()
    if dup_count > 0:
        logging.info(f"Removed {dup_count:,} ({dup_count / n_before_price_filter:.2%}) duplicate timestamps.")
        df = df[~duplicate_mask]

    if df.empty: # Проверяем, не пуст ли DataFrame после очистки дубликатов
        logging.warning("Предупреждение: DataFrame пуст после удаления дубликатов")
        return None

    # 6. Хронологический порядок
    if not df.index.is_monotonic_increasing:
        logging.info("Сортировка DataFrame по индексу для обеспечения хронологического порядка.")
        df.sort_index(inplace=True)

    # 7. Final validation and reporting
    if df.empty:
        logging.warning("Warning: DataFrame empty after all cleaning steps.")
        return None
    
    n_final = df.shape[0]
    n_cleaned = n_initial - n_final
    percentage_cleaned = (n_cleaned / n_initial) if n_initial > 0 else 0
    logging.info(f"Cleaned {n_cleaned:,} of {n_initial:,} ({percentage_cleaned:.2%}) datapoints.")

    return df


Шаг 3: Создание баров и преобразование  к времени закрытия

Сначала мы создадим несколько вспомогательных функций.

set_resampling_freq
def set_resampling_freq(timeframe: str) -> str:
    """
     Преобразует таймфрейм MT5 в частоту пересэмплирования pandas.

 Параметры:
          timeframe (str): Таймфрейм MT5 (например, 'M1', 'H1', 'D1', 'W1').

   Returns:
        str: Строка частоты pandas.
    """
    timeframe = timeframe.upper()
    nums = [x for x in timeframe if x.isnumeric()]
    if not nums:
        raise ValueError("Timeframe must include numeric values (e.g., 'M1').")
    
    x = int(''.join(nums))
    if timeframe == 'W1':
        freq = 'W-FRI'
    elif timeframe == 'D1':
        freq = 'B'
    elif timeframe.startswith('H'):
        freq = f'{x}H'
    elif timeframe.startswith('M'):
        freq = f'{x}min'
    elif timeframe.startswith('S'):
        freq = f'{x}S'
    else:
        raise ValueError("Valid timeframes include W1, D1, Hx, Mx, Sx.")
    
    return freq

calculate_ticks_per_period
def calculate_ticks_per_period(df: pd.DataFrame, timeframe: str = "M1", method: str = 'median', verbose: bool = True) -> int:
    """
    Dynamically calculates the average number of ticks per given timeframe.

 
        timeframe (str): MT5 timeframe.
        method (str): 'median' or 'mean' for the calculation.
        verbose (bool): Whether to print the result.

    Returns:
        int: Rounded average ticks per period.
    """
    freq = set_resampling_freq(timeframe)
    resampled = df.resample(freq).size()
    fn = getattr(np, method)
    num_ticks = fn(resampled.values)
    num_rounded = int(np.round(num_ticks))
    num_digits = len(str(num_rounded)) - 1
    rounded_ticks = int(round(num_rounded, -num_digits))
    rounded_ticks = max(1, rounded_ticks)
    
    if verbose:
        t0 = df.index[0].date()
        t1 = df.index[-1].date()
        logging.info(f"From {t0} to {t1}, {method} ticks per {timeframe}: {num_ticks:,} rounded to {rounded_ticks:,}")
    
    return rounded_ticks
flatten_column_names
def flatten_column_names(df):
    '''
    Joins tuples created by dataframe aggregation 
    with a list of functions into a unified name.
    '''
    return ["_".join(col).strip() for col in df.columns.values]

Теперь основные функции, используемые для создания баров.

make_bar_type_grouper
def make_bar_type_grouper(
        df: pd.DataFrame,
        bar_type: str = 'tick',
        bar_size: int = 100,
        timeframe: str = 'M1'
) -> tuple[pd.core.groupby.generic.DataFrameGroupBy, int]:
    """
    Create a grouped object for aggregating tick data into time/tick/dollar/volume bars.

    Args:
        df: DataFrame with tick data (index should be datetime for time bars).
        bar_type: Type of bar ('time', 'tick', 'dollar', 'volume').
        bar_size: Number of ticks/dollars/volume per bar (ignored for time bars).
        timeframe: Timeframe for resampling (e.g., 'H1', 'D1', 'W1').

    Returns:
        - GroupBy object for aggregation
        - Calculated bar_size (for tick/dollar/volume bars)
    """
    # Create working copy (shallow is sufficient)
    df = df.copy(deep=False)  # OPTIMIZATION: Shallow copy here only once
    
    # Ensure DatetimeIndex
    if not isinstance(df.index, pd.DatetimeIndex):
        try:
            df = df.set_index('time')
        except KeyError:
            raise TypeError("Could not set 'time' as index")

    # Sort if needed
    if not df.index.is_monotonic_increasing:
        df = df.sort_index()

    # Time bars
    if bar_type == 'time':
        freq = set_resampling_freq(timeframe)
        bar_group = (df.resample(freq, closed='left', label='right') # includes data upto, but not including, the end of the period
                    if not freq.startswith(('B', 'W')) 
                    else df.resample(freq))
        return bar_group, 0  # bar_size not used

    # Dynamic bar sizing
    if bar_size == 0:
        if bar_type == 'tick':
            bar_size = calculate_ticks_per_period(df, timeframe)
        else:
            raise NotImplementedError(f"{bar_type} bars require non-zero bar_size")

    # Non-time bars
    df['time'] = df.index  # Add without copying
    
    if bar_type == 'tick':
        bar_id = np.arange(len(df)) // bar_size
    elif bar_type in ('volume', 'dollar'):
        if 'volume' not in df.columns:
            raise KeyError(f"'volume' column required for {bar_type} bars")
        
        # Optimized cumulative sum
        cum_metric = (df['volume'] * df['bid'] if bar_type == 'dollar' 
                      else df['volume'])
        cumsum = cum_metric.cumsum()
        bar_id = (cumsum // bar_size).astype(int)
    else:
        raise NotImplementedError(f"{bar_type} bars not implemented")

    return df.groupby(bar_id), bar_size
make_bars
def make_bars(tick_df: pd.DataFrame,
              bar_type: str = 'tick',
              bar_size: int = 0,
              timeframe: str = 'M1',
              price: str = 'midprice',
              verbose=True):
    '''
    Create OHLC data by sampling ticks using timeframe or a threshold.

    Parameters
    ----------
    tick_df: pd.DataFrame
        tick data
    bar_type: str
        type of bars to create from ['tick', 'time', 'volume', 'dollar']
    bar_size: int 
        default 0. bar_size when bar_type != 'time'
    timeframe: str
        MT5 timeframe (e.g., 'M5', 'H1', 'D1', 'W1').
        Used for time bars, or for tick bars if bar_size = 0.
    price: str
        default midprice. If 'bid_ask', columns (bid_open, ..., bid_close), 
        (ask_open, ..., ask_close) are included.
    verbose: bool
        print information about the data

    Returns
    -------
    pd.DataFrame with columns [open, high, low, close, median_price, tick_volume, volume]
    '''    
    if 'midprice' not in tick_df:
        tick_df['midprice'] = (tick_df['bid'] + tick_df['ask']) / 2

    bar_group, bar_size_ = make_bar_type_grouper(tick_df, bar_type, bar_size, timeframe)
    ohlc_df = bar_group['midprice'].ohlc().astype('float64')
    ohlc_df['tick_volume'] = bar_group['bid'].count() if bar_type != 'tick' else bar_size_
    
    if price == 'bid_ask':
        # Aggregate OHLC data for every bar_size rows
        bid_ask_df = bar_group.agg({k: 'ohlc' for k in ('bid', 'ask')})
        # Flatten MultiIndex columns
        col_names = flatten_column_names(bid_ask_df)
        bid_ask_df.columns = col_names
        ohlc_df = ohlc_df.join(bid_ask_df)

    if 'volume' in tick_df:
        ohlc_df['volume'] = bar_group['volume'].sum()

    if bar_type == 'time':
        ohlc_df.ffill(inplace=True)
    else:
        end_time =  bar_group['time'].last()
        ohlc_df.index = end_time + pd.Timedelta(microseconds=1) # ensure end time is after event
	df.drop('time', axis=1, inplace=True) # Remove 'time' column


        # drop last bar due to insufficient ticks
        if len(tick_df) % bar_size_ > 0: 
            ohlc_df = ohlc_df.iloc[:-1]

    if verbose:
        if bar_type != 'time':
            tm = f'{bar_size_:,}'
            if bar_type == 'tick' and bar_size == 0:
                tm = f'{timeframe} - {bar_size_:,} ticks'
            timeframe = tm
        print(f'\nTick data - {tick_df.shape[0]:,} rows')
        print(f'{bar_type}_bar {timeframe}')
        ohlc_df.info()
    
    # Remove timezone info from DatetimeIndex
    try:
	ohlc_df = ohlc_df.tz_convert(None)
    except:
	pass
    
    return ohlc_df


Графики анализа волатильности, которые мы использовали выше, были созданы с помощью следующего кода:

Визуализируйте волатильность баров, используя Plotly.
df: DataFrame, содержащий данные с колонками 'open' и 'close'.
symbol: Символ актива.    
    start: Начальная дата данных.
    end: Конечная дата данных.
freq: Частота данных.
thres: Порог для фильтрации экстремальных значений, например, 1-0.01 для 99-го квантиля.
bins: Количество интервалов (бинов) для гистограммы.
    """ 
    abs_price_changes = (df['close'] / df['open'] - 1).mul(100).abs()
    thres = abs_price_changes.quantile(1 - thres)
    abs_price_changes = abs_price_changes[abs_price_changes < thres] # filter out large values for visualization

    # Calculate Histogram
    counts, bins = np.histogram(abs_price_changes, bins=bins)
    bins = bins[:-1] # remove the last bin edge

    # Calculate Proportions
    total_counts = len(abs_price_changes)
    proportion_candles_right = []
    proportion_price_change_right = []

    for i in range(len(bins)):
        candles_right = abs_price_changes[abs_price_changes >= bins[i]]
        count_right = len(candles_right)
        proportion_candles_right.append(count_right / total_counts)
        proportion_price_change_right.append(np.sum(candles_right) / np.sum(abs_price_changes))

    fig = go.Figure()

    # Histogram with Hover Template
    fig.add_trace(
        go.Bar(x=bins, y=counts, 
               name='Histogram absolute price change (%)',
               marker=dict(color='#1f77b4'), 
               hovertemplate='<b>Bin: %{x:.2f}</b><br>Frequency: %{y}',  # Custom hover text
               yaxis='y1',
               opacity=.65))

    ms = 3 # marker size
    lw = .5 # line width
    
    # Proportion of Candles at the Right with Hover Text
    fig.add_trace(
        go.Scatter(x=bins, y=proportion_candles_right, 
                   name='Proportion of candles at the right',
                   mode='lines+markers', 
                   marker=dict(color='red', size=ms), 
                   line=dict(width=lw),
                   hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" 
                              for x, y in zip(bins, proportion_candles_right)],  # Hover text list
                   hoverinfo='text',  # Show only the 'text' from hovertext
                   yaxis='y2'))
    

    # Proportion Price Change Produced by Candles at the Right with Hover Text
    fig.add_trace(
        go.Scatter(x=bins, y=proportion_price_change_right, 
                   name='Proportion price change produced by candles at the right',
                   mode='lines+markers', 
                   marker=dict(color='green', size=ms), 
                   line=dict(width=lw),
                   hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" 
                              for x, y in zip(bins, proportion_price_change_right)], # Hover text list
                   hoverinfo='text',  # Show only the 'text' from hovertext
                   yaxis='y2'))
    
    # Indices of proportion_price_change_right at 10% intervals
    search_idx = [.01, .05] + np.linspace(.1, 1., 10).tolist()
    price_idxs = np.searchsorted(sorted(proportion_candles_right), search_idx, side='right')
    for ix in price_idxs:  # Add annotations for every step-th data point as an example
        x = bins[-ix]
        y = proportion_candles_right[-ix]
        fig.add_annotation(
            x=x,
            y=y,
            text=f"{y:.4f}",  # Display the proportion value with 4 decimal points
            showarrow=True,
            arrowhead=1,
            ax=0,
            ay=-15,  # Offset for the annotation text
            font=dict(color="salmon"),
            arrowcolor="red",
            yref='y2'
        )

        y = proportion_price_change_right[-ix]
        fig.add_annotation(
            x=x,
            y=y,
            text=f"{y:.4f}",  # Display the proportion value with 4 decimal points
            showarrow=True,
            arrowhead=1,
            ax=0,
            ay=-25,  # Offset for the annotation text
            font=dict(color="lightgreen"),
            arrowcolor="green",
            yref='y2'
        )

    # Layout Configuration with Legend Inside
    fig.update_layout(
        title=f'Volatility Analysis of {symbol} {freq} from {start} to {end}',
        xaxis_title='Absolute price change (%)',
        yaxis_title='Frequency',
        yaxis2=dict(
            title='Proportion',
            overlaying='y',
            side='right',
            gridcolor='#444'  # Set grid color for the secondary y-axis
        ),
        plot_bgcolor='#222',  # Dark gray background
        paper_bgcolor='#222',
        font=dict(color='white'),
        xaxis=dict(gridcolor='#444'),  # Set grid color for the primary x-axis
        yaxis=dict(gridcolor='#444'),   # Set grid color for the primary y-axis
        legend=dict(
            x=0.3,  # Adjust x coordinate (0 to 1)
            y=0.95,  # Adjust y coordinate (0 to 1)
            traceorder="normal",  # Optional: maintain trace order
            font=dict(color="white")  # Optional: set legend text color
        ),
        # width=750,  # Set width of the figure
        # height=480,  # Set height of the figure
    )

    return fig

Теперь, когда мы увидели, как создавать структурированные данные из наших тиковых данных в форме временных или тиковых баров, давайте проверим, подтверждаются ли некоторые из наших утверждений о превосходстве статистических свойств тиковых баров над временными. Мы будем использовать тиковые данные EURUSD за 2023 год, которые прикреплены в файлах ниже.


EURUSD M5 против Tick-200 15.08.2023

Если вы внимательно посмотрите на области с высоким тиковым объемом на 5-минутном графике (M5), например, в период с 14:00 до 16:00, вы заметите, что сформированные бары на графике Tick-200 накладываются друг на друга из-за более частой выборки в этот период высокой активности. И наоборот, тиковые бары в промежутке с 06:00 до 08:00 встречаются редко, и между ними наблюдаются большие пробелы. Это иллюстрирует, почему тиковые бары называют барами, управляемыми активностью (activity-driven bars), в отличие от временных баров, которые равномерно производят выборку данных через фиксированные временные интервалы.


Масштабируемость и рекомендации по оборудованию

Работа с высокочастотными тиковыми данными и построение специализированных баров может требовать значительных вычислительных мощностей, особенно при работе с большими историческими наборами данных. Для оптимальной производительности и эффективной обработки мы рекомендуем следующую конфигурацию компьютера:
  • ОЗУ: Минимум 16 ГБ ОЗУ, предпочтительно 32 ГБ или более для обширного бэктестинга или обработки тиковых данных за несколько лет.
  • Процессор (CPU): Настоятельно рекомендуется многоядерный процессор (например, Intel i7/i9 или AMD Ryzen 7/9). Возможность распараллеливания задач обработки данных на нескольких ядрах значительно сократит время вычислений.
  • Стратегии распараллеливания: Рассмотрите возможность реализации методов параллельной обработки в вашем коде Python. Такие библиотеки, как Dask для распределенных вычислений или встроенный в Python модуль multiprocessing, могут быть бесценны для ускорения подготовки данных, разработки признаков (feature engineering) и имитации бэктестинга на больших наборах данных.



Следующие шаги

Чтобы эффективно применить концепции, обсуждаемые в этой статье, и подготовиться к последующим частям этой серии, мы рекомендуем следующие практические шаги:

  1. Внедрите исправление временных меток: Интегрируйте предоставленные фрагменты кода в свой конвейер загрузки данных, чтобы гарантировать, что все данные из MetaTrader 5 имеют правильные временные метки и не содержат ошибки предвидения (look-ahead bias).
  2. Экспериментируйте с типами баров: Помимо тиковых баров, исследуйте другие специализированные типы баров, такие как бары объема или долларовые бары. Понаблюдайте, как эти различные методы выборки влияют на характеристики вашего набора данных и какую потенциальную выгоду они могут принести для ваших конкретных торговых стратегий.
  3. Подготовьте свой набор данных: Имея на руках чистые, неискаженные данные, начните систематизировать и подготавливать свой набор данных для следующих этапов конвейера машинного обучения. В Части 2 мы углубимся в продвинутые методы разработки признаков (feature engineering) и разметки данных (labelling).
В следующей части нашей серии мы погрузимся в один из самых ключевых этапов создания мощных моделей машинного обучения с учителем для финансов — создание меток (label creation). В то время как традиционные методы с фиксированным временным горизонтом доминируют в большей части литературы, они часто не позволяют уловить истинную динамику финансовых рынков.

Вот почему в следующей статье мы рассмотрим две новаторские альтернативы доктора Маркоса Лопеса де Прадо: метод тройного барьера (triple-barrier method) и метод сканирования тренда (trend-scanning method). Эти методы не просто переосмысливают маркировку — они переопределяют её.

Если вы когда-либо задавались вопросом, действительно ли ваши метки соответствуют тому, как ведут себя рынки, это то понимание, которого вы ждали.



Заключение

В этой основополагающей статье мы тщательно рассмотрели и предложили решения для критической проблемы, которую мы назвали "ловушкой временных меток MetaTrader 5". Мы продемонстрировали, как неправильная обработка временных меток может привести к серьезной утечке данных, что, в свою очередь, ведет к созданию ошибочных моделей и ненадежных торговых сигналов. Внедрив надежные механизмы коррекции временных меток и используя возможности построения тиковых баров, мы успешно заложили основу для создания высококачественных наборов данных. Это фундаментальное исправление имеет первостепенное значение для обеспечения обоснованности ваших исследований, точности ваших бэктестов и, в конечном итоге, надежности ваших моделей машинного обучения в алгоритмической торговле. Этот важнейший первый шаг необходим любому серьезному специалисту в области количественного анализа, стремящемуся разрабатывать по-настоящему эффективные и заслуживающие доверия торговые системы.

В приложенных документах вы найдете код, использованный выше, а также некоторые вспомогательные функции для входа в терминал MetaTrader 5 с помощью Python API.

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

Прикрепленные файлы |
mt5_login.py (6.6 KB)
bars.py (13.44 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
Stanislav Korotky
Stanislav Korotky | 28 июн. 2025 в 16:29

Бары, ориентированные на активность, не решают всех проблем, которые вы упомянули для временных баров. Например, вы написали:

The Subtle Intra-Bar Leakage: However, a more subtle form of data leakage can still occur within the very formation of that time bar. If a significant event transpires midway through a 1-minute bar (e.g., at 09:00:35), any features derived from that bar (such as its high price or a flag for the event) will inevitably incorporate this information by the bar's end.

Если вы строите пользовательские бары равного объема, равного диапазона или другие тиковые бары, вы все равно пометите такой бар одной меткой, и он будет сливать (точнее, размывать) информацию о высокой цене по всему бару.

Единственный способ решить эту проблему - строить "бары" с учетом специфических особенностей (которые вы собираетесь использовать). Например, если основными характеристиками являются максимумы или минимумы, стоит попробовать, возможно, зигзагообразные "бары" с экстремумами, отмеченными точным временем.

На самом деле, подход с постоянными таймфреймами, да еще и ограничивающий их М1, проблематичен в контексте утечки данных в МТ5. Маркировать бары М1 временем окончания не намного лучше, чем временем начала, имхо.


Для тех, кто интересуется построением пользовательских баров (графиков) нативно в MT5, есть статья с реализацией на MQL5 баров equal-volume, equal-range и renko. Разумеется, в открытом коде можно пометить бары с временем окончания.

Patrick Murimi Njoroge
Patrick Murimi Njoroge | 15 июл. 2025 в 12:59
Stanislav Korotky #:

Бары, ориентированные на активность, не решают всех проблем, которые вы упомянули для временных баров. Например, вы написали:

Если вы строите пользовательские бары равного объема, равного диапазона или другие тиковые бары, вы в любом случае пометите такой бар одной меткой, и он будет сливать (точнее, размывать) информацию о высокой цене по всему бару.

Единственный способ решить эту проблему - строить "бары" с учетом специфических особенностей (которые вы собираетесь использовать). Например, если основными характеристиками являются максимумы или минимумы, то стоит попробовать, возможно, зигзагообразные "бары" с экстремумами, отмеченными точным временем.

На самом деле, подход с постоянными таймфреймами, да еще и ограничивающий их М1, проблематичен в контексте утечки данных в МТ5. Маркировать бары М1 временем окончания не намного лучше, чем временем начала, имхо.


Для тех, кто интересуется построением пользовательских баров (графиков) нативно в MT5, есть статья с реализацией на MQL5 баров equal-volume, equal-range и renko. Разумеется, в открытом коде можно пометить бары с временем окончания.

Бары, управляемые активностью, направлены на улучшение статистических свойств информации, содержащейся в барах, например, уменьшение гетероскедастичности и улучшение нормальности. Предложенное мной решение проблемы тонкой внутрибарной утечки заключается в маркировке баров с помощью времени их окончания, так что все события, происходящие внутри бара, отражаются в метке времени. Полезный пример - использование в обучении модели характеристик, полученных из временной метки, таких как преобразования Фурье. Если вы используете соглашение MetaTrader5, в котором бары маркируются по началу периода, то вы дезинформируете свою модель. Это различие может не иметь большого значения для некоторых моделей, но оно оказывает огромное влияние на те, которые нацелены на использование циклической природы рынков. Надеюсь, я прояснил свои намерения.


Patrick Murimi Njoroge
Patrick Murimi Njoroge | 20 сент. 2025 в 00:05
Stanislav Korotky #:

Бары, основанные на активности, не решают всех проблем, которые вы упомянули для временных баров. Например, вы написали:

Если вы создаете бары того же объема, диапазона или другие тиковые пользовательские бары, вы в любом случае будете помечать такой бар одной меткой, и информация о максимальной цене будет просачиваться (точнее, размываться) по всему бару.

Единственный способ решить эту проблему - создавать "бары" с учетом специфических особенностей (которые вы будете использовать). Например, если основными характеристиками являются максимумы или минимумы, попробуйте создать "зигзагообразный бар" с экстремумами, отмеченными точно по времени.

Подход с постоянным таймфреймом, и в частности ограничение M1, проблематичен в контексте утечки данных MT5. Маркировка баров М1 временем окончания имхо не намного лучше, чем временем начала.


Для тех, кто интересуется созданием пользовательских баров (графиков) нативно в MT5, есть статья с MQL5-реализацией баров Equal Volume, Equal Range и Renko. Разумеется, в открытом коде можно пометить бары временем окончания.

Что вы имеете в виду, когда говорите: "Если вы создаете бары с одинаковым объемом, диапазоном или другие тиковые пользовательские бары, вы в любом случае будете помечать такой бар одной меткой, и информация о максимальной цене будет просачиваться (точнее, размываться) по всему бару"?

Stanislav Korotky
Stanislav Korotky | 20 сент. 2025 в 16:00
Patrick Murimi Njoroge #:

Что вы имеете в виду, когда говорите: "Если вы создаете бары с одинаковым объемом, диапазоном или другие пользовательские бары на основе тиков, вы все равно будете помечать такой бар одной меткой, и информация о максимальной цене будет просачиваться (или, точнее, размываться) по всему бару"?

Я не понимаю, что тут непонятного. Мое высказывание было прямым ответом на ваше высказывание, процитированное в моем предыдущем посте - так что вы можете видеть контекст. Независимо от того, как вы формируете бары, каждому свойству бара приписывается одна временная метка, и фактическое "событие" для свойства не совпадает с этим временем.
Patrick Murimi Njoroge
Patrick Murimi Njoroge | 22 сент. 2025 в 21:15
Stanislav Korotky # :
Я не понимаю, что тут непонятного. Мое предложение - прямой ответ на ваше предложение, которое я процитировал в предыдущем посте. Независимо от того, как вы формируете бар, все свойства бара приписываются одной временной метке, а фактические "события" свойств не совпадают с этим временем. время.

Теперь я понимаю значение слова blur.

Конструктор советников MQL5 (Часть 1): Простой статический шаблон Конструктор советников MQL5 (Часть 1): Простой статический шаблон
В статье разбирается пример многоцелевого шаблона торгового робота, который подойдет как для создания собственных стратегий, так и в качестве кодовой базы для работы на фрилансе. Ключевая особенность решения — торговля по барам, при этом код уже оснащен встроенными режимами усреднения, мартингейла и длительного удержания позиций. Материал будет наиболее полезен новичкам, которые хотят написать свои простые стратегии или познакомиться с распространенными торговыми техниками.
Знакомство с языком MQL5 (Часть 33): Освоение API и функции WebRequest в языке MQL5 (VII) Знакомство с языком MQL5 (Часть 33): Освоение API и функции WebRequest в языке MQL5 (VII)
В этой статье показано, как интегрировать API Google Generative AI в MetaTrader 5 с помощью языка MQL5. Вы научитесь структурировать API-запросы, обрабатывать ответы сервера, извлекать контент, сгенерированный ИИ, управлять лимитами API и сохранять результаты в текстовый файл для удобного доступа.
Возможности Мастера MQL5, которые вам нужно знать (Часть 67): Использование паттернов TRIX и процентного диапазона Уильямса Возможности Мастера MQL5, которые вам нужно знать (Часть 67): Использование паттернов TRIX и процентного диапазона Уильямса
Тройной экспоненциальный осциллятор скользящей средней (Triple Exponential Moving Average Oscillator, TRIX) и осциллятор процентного диапазона Уильямса (Williams Percentage Range Oscillator) — это еще одна пара индикаторов, которые можно использовать совместно в советнике MQL5. Эта пара индикаторов, как и те, которые мы недавно рассматривали, также дополняет друг друга, поскольку TRIX определяет тренд, а процентный диапазон подтверждает уровни поддержки и сопротивления. Как всегда, мы используем Мастер MQL5 для оценки потенциала индикаторов.
Торговые инструменты на языке MQL5 (Часть 6): Динамическая голографическая панель с импульсной анимацией и элементами управления Торговые инструменты на языке MQL5 (Часть 6): Динамическая голографическая панель с импульсной анимацией и элементами управления
В этой статье мы создаем динамическую голографическую панель на MQL5 для мониторинга инструментов и таймфреймов с помощью RSI, оповещений о волатильности и параметров сортировки. Добавляем анимацию импульсов, интерактивные кнопки и голографические эффекты, чтобы сделать инструмент визуально привлекательным и отзывчивым.