English
preview
Инжиниринг признаков для машинного обучения (Часть 1): Дробное дифференцирование — стационарность без потери памяти

Инжиниринг признаков для машинного обучения (Часть 1): Дробное дифференцирование — стационарность без потери памяти

MetaTrader 5Интеграция |
59 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Оглавление

  1. Введение
  2. Проблема: целочисленное дифференцирование уничтожает память
  3. Дилемма стационарности и памяти
  4. Математика дробного дифференцирования
  5. Генерация весов и сходимость
  6. Две стратегии окна: расширяющееся и фиксированной ширины (FFD)
  7. Проверка стационарности с помощью расширенного теста Дики — Фуллера
  8. Поиск оптимального d*
  9. Реализация в afml
  10. Интеграция в пайплайн
  11. Заключение
  12. Ссылки


Введение

Каждый пайплайн машинного обучения для финансовых временных рядов сталкивается с решением предварительной обработки, которое большинство практиков принимает автоматически: как преобразовать исходные цены в признаки. Стандартный подход — вычислять лог-доходности как первые разности логарифмов цен. Доходности стационарны, что соответствует предположениям большинства ML-алгоритмов. Но у доходностей есть критический недостаток: они стирают память. Ряд доходностей не содержит информации о том, на каких ценовых уровнях находился актив, насколько он отклонился от своего долгосрочного среднего и как текущий уровень соотносится с историческими зонами поддержки и сопротивления. Каждое значение зависит ровно от одного предшественника, а все, что было до него, отбрасывается.

Это не просто теоретическая проблема. Равновесным моделям нужна память, чтобы оценивать, насколько ценовой процесс отклонился от ожидаемого значения. Стратегиям возврата к среднему нужна память, чтобы определить само среднее. Трендследящим стратегиям нужна память, чтобы отличить настоящий тренд от шума. Когда целочисленное дифференцирование стирает эту память, ML-алгоритм вынужден восстанавливать ее через построение признаков: лаговые доходности, скользящие статистики, технические индикаторы. Эти признаки являются лишь несовершенными заменителями информации, потерянной на этапе предварительной обработки.

Дробное дифференцирование решает эту дилемму. Вместо дифференцирования целочисленного порядка (0 для цен, 1 для доходностей) мы используем вещественный порядок d между 0 и 1. В результате получается ряд, который является стационарным — удовлетворяет предположениям ML — и при этом сохраняет как можно больше памяти исходного ценового ряда. Лопес де Прадо представил этот метод сообществу финансового ML в Главе 5 AFML, опираясь на фундаментальную работу Хоскинга (1981). В этой статье рассматривается теория, объясняются две стратегии реализации (расширяющееся окно и окно фиксированной ширины) и разбирается реализация промышленного класса на Python в библиотеке afml. Следующая статья серии переносит этот механизм в MQL5 для работы с потоками данных MetaTrader 5 в реальном времени.


Проблема: целочисленное дифференцирование уничтожает память

Чтобы понять, какую проблему решает дробное дифференцирование, нужно увидеть, что уничтожает целочисленное дифференцирование. Рассмотрим ценовой ряд Pt. Любую операцию дифференцирования можно представить с помощью оператора сдвига назад B, определенного как B^k X_t = X_{t−k}. Дифференцирование первого порядка имеет вид:

Backshift 1

Это привычный ряд доходностей. Операция использует вектор весов ω = {1, −1, 0, 0, …}: она смотрит ровно на одно предыдущее значение и отбрасывает все остальное. Вся информация, закодированная в P_{t−2}, P_{t−3}, …, P_1, исчезает.

Дифференцирование нулевого порядка — использование исходной цены — сохраняет всю память, но цены нестационарны. Их среднее и дисперсия со временем дрейфуют. ML-алгоритм, обученный на признаках при уровне цены 100, не сможет корректно переносить закономерности на данные с уровнем цены 200, поскольку соответствие между пространством признаков и пространством целевых переменных изменилось.

Визуальный эффект очень нагляден. Панель (a) ниже показывает синтетический ценовой ряд, сгенерированный геометрическим броуновским движением. Он нестационарен — ADF-тест не может отвергнуть нулевую гипотезу единичного корня. Панель (b) показывает лог-доходности (d = 1): они стационарны, но каждое значение лог-доходности не несет связи с тем уровнем цены, из которого оно получено. Панель (c) показывает ряд, преобразованный FFD при d = 0.4: он стационарен, но общая форма и информация об уровне исходного ряда визуально сохраняются.

Original prices vs. returns vs. FFD-transformed series

Рисунок 1. Три представления одного и того же базового процесса

  • Панель (a): исходные цены (d = 0), нестационарны и обладают полной памятью. Ряд свободно дрейфует — его среднее и дисперсия со временем смещаются, нарушая предположения ML о стационарности.
  • Панель (b): лог-доходности (d = 1), стационарны и не содержат памяти. Каждое значение лог-доходности зависит ровно от одного предшественника; вся информация об уровне цены стерта.
  • Панель (c): ряд FFD (d = 0.4), стационарен и сохраняет память. Общая структура исходного ряда видна, при этом процесс колеблется вокруг стабильного среднего.


Дилемма стационарности и памяти

Ключевое противоречие простое: стационарность и память тянут в противоположные стороны. Стационарность является необходимым условием обучения с учителем: нам нужно сопоставлять новые наблюдения с размеченными обучающими примерами, а это работает только тогда, когда распределение признаков стабильно. Но сама по себе стационарность недостаточна для прогнозирования. Белый шум совершенно стационарен, но не содержит сигнала. Предсказательная сила возникает из памяти — сохранения прошлой информации в текущих наблюдениях.

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

Это не просто недочет инжиниринга признаков — это фундаментальная потеря информации. Лопес де Прадо формулирует ее как дилемму между двумя традиционными эконометрическими парадигмами:

  1. Box-Jenkins: работать с доходностями (стационарными, но без памяти).
  2. Engle-Granger: работать с ценами через коинтеграцию (память сохраняется, но коинтегрирующие векторы нестабильны, а число коинтегрированных переменных ограничено).

Дробное дифференцирование открывает третий путь. Между крайностями d = 0 (цены) и d = 1 (доходности) существует непрерывный спектр преобразований, которые позволяют гибко балансировать между стационарностью и памятью с помощью вещественного параметра d. Вопрос становится таким: какое минимальное d обеспечивает стационарность? Это минимальное d* является оптимальной рабочей точкой — любое дальнейшее дифференцирование удаляет память без необходимости.

The stationarity vs. memory tradeoff

Рисунок 2. Дилемма стационарности и памяти

  • Синяя кривая: сохраняемая память — монотонно уменьшается по мере роста d. При d = 0 (исходные цены) вся память сохранена. При d = 1 (доходности) память фактически равна нулю.
  • Красная кривая: стационарность — монотонно растет вместе с d. При d = 0 ряд нестационарен. При d = 1 достигается полная стационарность.
  • Зеленая пунктирная кривая: стилизованная предсказательная сила — достигает максимума около оптимального d* ≈ 0.35, где стационарность выполнена и сохранена максимальная память.


Математика дробного дифференцирования

Стандартное целочисленное дифференцирование использует биномиальное разложение (1 − B)^n, где n — положительное целое число. При n = 1: (1 − B)^1 = 1 − B, что дает X_t − X_{t−1}. При n = 2: (1 − B)^2 = 1 − 2B + B^2, что дает X_t − 2X_{t−1} + X_{t−2}. Во всех целочисленных случаях биномиальный коэффициент становится нулем после k > n членов, обрезая вектор весов и стирая память за лагом n.

Дробное дифференцирование обобщает показатель n до вещественного числа d с помощью биномиального ряда. Для любого вещественного d:

Backshift Operator

где обобщенный биномиальный коэффициент равен:

Binomial Series


Значение после дробного дифференцирования затем является скалярным произведением вектора весов ω и исторических значений X:

Fractionally Differenced Value

Последовательность весов имеет вид ω = {1, −d, d(d−1)/2!, −d(d−1)(d−2)/3!, …}. Когда d — положительное целое число, произведение ∏(d−i) становится нулем при k > d, и все последующие веса исчезают — память обрывается. Когда d — положительное нецелое число (например, 0.4), произведение никогда не становится нулем, а веса образуют бесконечный ряд, асимптотически убывающий к нулю. Именно так дробное дифференцирование сохраняет память: старые наблюдения получают малые, но ненулевые веса.

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

Вычислять обобщенный биномиальный коэффициент с нуля для каждого k не нужно. Веса удовлетворяют рекуррентному соотношению, которое делает расчет эффективным. Начиная с ω0 = 1:

FFD Weight Vector.png

Эта одна формула итеративно генерирует всю последовательность весов. Каждый вес зависит только от предыдущего веса и текущего индекса — операция выполняется за постоянное время. На рисунке 3 показаны эти последовательности весов для d ∈ [0, 1] и d ∈ [1, 2].

Weight decay curves for varying d

Рисунок 3. Веса дробного дифференцирования ω_k при разных d

  • Панель (a): для d ∈ [0, 1] все веса после ω0 = 1 отрицательны и ограничены интервалом (−1, 0), убывая к нулю. При d = 0 ненулевым остается только ω0 = 1 (тождественное преобразование). При d = 1 получаем ω = {1, −1, 0, 0, …} — стандартные доходности.
  • Панель (b): при d > 1 ω1 < −1, а веса при k ≥ 2 становятся положительными — возникает поведение второй разности.


Генерация весов и сходимость

Рекуррентное соотношение легко реализовать. В Python оно напрямую отображается в функцию, скомпилированную Numba, ради скорости:

@njit(cache=True)
def get_weights(d, size):
    """Expanding window weights (AFML Section 5.4.2, page 79)."""
    weights = [1.0]
    for k in range(1, size):
        weights_ = -weights[-1] * (d - k + 1) / k
        weights.append(weights_)

    weights = np.array(weights[::-1]).reshape(-1, 1)
    return weights

Стоит отметить два момента. Во-первых, веса разворачиваются перед возвратом: функция хранит их от самого старого веса к самому новому весу (ω0 = 1). Такой порядок соответствует соглашению, что prices[0] — самое старое значение, а prices[-1] — самое новое, поэтому скалярное произведение сразу дает дробно-дифференцированное значение. Во-вторых, декоратор @njit(cache=True) компилирует функцию в машинный код через Numba, что важно, поскольку она может вызываться тысячи раз при поиске гиперпараметра d.

Для варианта окна фиксированной ширины добавляется критерий остановки по порогу. Веса асимптотически убывают к нулю; в какой-то момент вес становится настолько малым, что его включение почти не повышает точность, но увеличивает требуемую глубину ретроспективного окна. Генератор весов FFD останавливается, когда |ω_k| падает ниже порога τ (обычно 10^−5):

@njit(cache=True)
def get_weights_ffd(d, thres, lim):
    """Fixed-width window weights (AFML Section 5.4.2, page 83)."""
    weights = [1.0]
    k = 1
    ctr = 0
    while True:
        weights_ = -weights[-1] * (d - k + 1) / k
        if abs(weights_) < thres:
            break
        weights.append(weights_)
        k += 1
        ctr += 1
        if ctr == lim - 1:
            break

    weights = np.array(weights[::-1]).reshape(-1, 1)
    return weights

Количество сгенерированных весов — ширина окна — зависит и от d, и от τ. Меньшие значения d дают веса, которые убывают медленнее, поэтому для достижения порога нужны более длинные окна. На рисунке 4 показана эта зависимость.

FFD weight vectors for different d value

Рисунок 4. Векторы весов FFD при τ = 10^−5

  • Меньшие значения d: создают более длинные векторы весов, потому что веса убывают медленнее. При d = 0.2 окно охватывает сотни баров — каждое наблюдение FFD интегрирует длинную историю.
  • Большие значения d: создают более короткие векторы. При d = 1.0 вектор просто равен {−1, 1} — стандартные доходности с шириной окна 1.

Window width and cumulative weight magnitude

Рисунок 5. Ширина окна FFD и сходимость весов

  • Панель (a): ширина окна как функция d. Меньшие значения d требуют пропорционально больше исторических данных для заполнения окна.
  • Панель (b): накопленная величина весов для d = 0.4. Около 99% общей величины веса захватывается в пределах первых ~30 лагов; оставшиеся веса дают убывающий вклад, но сохраняются ради точности.


Две стратегии окна: расширяющееся и фиксированной ширины (FFD)

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

Расширяющееся окно

Подход с расширяющимся окном использует всю доступную историю для каждой точки. Для последнего наблюдения X̃_T он использует веса {ω_k} при k = 0, …, T−1. Для X̃_{T−l} он использует k = 0, …, T−l−1. Поэтому ранние наблюдения используют меньше весов, чем поздние, что создает асимметрию ретроспективного окна.

Практическое последствие — отрицательный дрейф. По мере расширения окна растущее количество отрицательных весов (напомним, что все веса после ω0 отрицательны при d ∈ (0,1)) добавляет дополнительные отрицательные вклады. Дробно-дифференцированный ряд дрейфует вниз не потому, что базовая цена падает, а из-за артефакта расширяющегося окна.

Лопес де Прадо смягчает это, вводя порог потери веса τ. Для каждого наблюдения относительная потеря веса вычисляется как доля общей величины весов, которая находится за пределами доступного окна. Наблюдения, где эта потеря превышает τ, отбрасываются. Это улучшает ситуацию, но полностью не устраняет дрейф, потому что наблюдения все равно используют окна разной длины.

Окно фиксированной ширины (FFD)

Подход окна фиксированной ширины (FFD) использует один и тот же вектор весов для каждого наблюдения. Последовательность весов усекается на первом индексе l*, где |ω_{l*+1}| < τ, и этот одинаковый усеченный вектор применяется ко всем X̃_t при t = l*, …, T. Первые l* наблюдений отбрасываются, потому что для заполнения окна недостаточно истории.

Этот подход полностью устраняет отрицательный дрейф. Каждое наблюдение вычисляется на одной и той же основе: одинаковое число весов и одинаковая глубина истории. Результат — сочетание информации об уровне ряда и шумовой компоненты без дрейфа. Лопес де Прадо рекомендует FFD для практических приложений, и он используется по умолчанию в нашей реализации afml.

Компромисс носит распределительный характер: ряд FFD перестает быть нормально распределенным. Усечение вектора весов может вносить асимметрию и избыточный эксцесс. Для ML-приложений это допустимо — большинство деревьев решений и ансамблей на их основе не требуют конкретного распределения, — но это важно для последующего анализа, предполагающего нормальность.

Стратегии окна с расширяющейся историей и окна фиксированной ширины

Рисунок 6. Стратегии окон для дробного дифференцирования

  • Панель (a): расширяющееся окно. Вектор весов растет с каждым наблюдением; поздние бары интегрируют больше истории, чем ранние, что приводит к отрицательному дрейфу из-за накопления отрицательных весов.
  • Панель (b): окно фиксированной ширины (FFD). Каждое наблюдение использует одну и ту же ширину окна, поэтому глубина истории остается постоянной по всему ряду. Первые несколько наблюдений отбрасываются, так как окно еще нельзя заполнить.


Проверка стационарности с помощью расширенного теста Дики — Фуллера

Дробное дифференцирование полезно только тогда, когда мы можем определить, стал ли результат стационарным. Расширенный тест Дики — Фуллера (ADF) — стандартный инструмент. Он проверяет нулевую гипотезу о том, что ряд содержит единичный корень (то есть нестационарен), против альтернативы стационарности. Тест возвращает два важных значения: ADF-статистику (чем более отрицательная, тем больше свидетельств стационарности) и p-value (вероятность наблюдать такую статистику при нулевой гипотезе).

На уровне 5% ряд считается стационарным, если (1) ADF-статистика ниже критического значения 5% и (2) p-value ниже 0.05. Библиотека afml объединяет обе проверки в одной функции:

def adf_data(df1, df2, d=0, out_df=None, alpha=0.05):
    """ADF statistics and correlation between original and differenced series."""
    corr = np.corrcoef(df1.loc[df2.index], df2)[0, 1]
    adf = adfuller(df2, maxlag=1, regression="c", autolag=None)
    tc_col = f"{1 - alpha:.0%} conf"

    # Build results row
    columns = ["adfStat", "pVal", "lags", "nObs",
               "window", tc_col, "corr", "stationary"]
    vals = (list(adf[:4]) + [df1.shape[0] - adf[3]]
            + [adf[4][f"{alpha:.0%}"]] + [corr] + [False])

    if out_df is None or out_df.empty:
        out_df = pd.DataFrame.from_dict(
            {d: {k: v for k, v in zip(columns, vals)}}, orient="index"
        )
    else:
        out_df.loc[d, columns] = vals

    stationary = (out_df.loc[d, "adfStat"] < out_df.loc[d, tc_col]) and \
                 (out_df.loc[d, "pVal"] < alpha)
    out_df.loc[d, "stationary"] = stationary
    out_df.index.name = "d"
    return out_df

Функция также вычисляет корреляцию между исходным рядом и дробно-дифференцированным рядом. Эта корреляция измеряет сохранение памяти: чем ближе она к 1.0, тем больше структуры исходного ряда сохранились после преобразования. На рисунке 7 показаны обе величины как функции d.

ADF statistic and correlation as a function of d

Рисунок 7. ADF-статистика и корреляция как функция d

  • Синяя кривая (верхняя панель): корреляция между рядом FFD и исходными лог-ценами. Начинается около 1.0 при малых d и падает к нулю при d = 1, количественно показывая потерю памяти.
  • Зеленая кривая (нижняя панель): ADF-статистика. Становится все более отрицательной (более стационарной) по мере роста d. Красная пунктирная линия отмечает 95% критическое значение (−2.8623).
  • Точка пересечения: ADF-статистика пересекает критическое значение около d* ≈ 0.15, где корреляция с исходным рядом остается высокой.

Лопес де Прадо сообщает, что для 87 наиболее ликвидных фьючерсов мира стационарность достигалась при d < 0.6 во всех случаях, что подтверждает: стандартное целочисленное дифференцирование (d = 1) систематически чрезмерно дифференцирует данные.


Поиск оптимального d*

Поскольку ADF-статистика монотонно убывает по d (чем больше дифференцирование, тем более стационарен ряд), оптимальное d* — это наименьшее значение, которое пересекает порог стационарности. Это задача поиска корня, и естественный алгоритм для нее — бинарный поиск.

Поиск начинается с интервала [0, max_d] (по умолчанию [0, 1]). На каждой итерации вычисляется ряд FFD в середине интервала, выполняется ADF-тест, а интервал сужается: если середина стационарна, верхняя граница сдвигается вниз (пытаемся дифференцировать меньше); если нестационарна, нижняя граница сдвигается вверх (нужно больше дифференцировать). Сходимость до допуска tol требует ⌈log2(max_d / tol)⌉ итераций — около 10 итераций при tol = 10^−3.

Binary search for optimal d*

Рисунок 8. Бинарный поиск оптимального d*

  • Зеленые маркеры: значения d, где ADF-тест проходит (ряд стационарен).
  • Красные маркеры: значения d, где ряд остается нестационарным.
  • Сходимость: интервал делится пополам на каждом шаге. К шагу 7 ширина интервала становится ниже порога допуска, и поиск завершается.

Реализация afml добавляет две оптимизации: (1) кэширование уже проверенных значений d и (2) проверку минимального размера выборки (по умолчанию 100 наблюдений). Если ряд FFD содержит меньше 100 непропущенных значений, верхняя граница поиска сдвигается вниз, к меньшим значениям d.

def fracdiff_optimal(
    series, fixed_width=True, alpha=0.05,
    max_d=1.0, tol=1e-3, use_log=True, verbose=False,
):
    """
    Binary search for minimum d that achieves stationarity.

    Returns (ffd_series, d_optimal, adf_dataframe).
    """
    low, high = 0.0, max_d
    best_d = None
    diff_adf = None
    out_df = None
    frac_diff_cache, adf_cache = {}, {}

    def frac_diff_cached(series, d, use_log):
        cache_key = (d, use_log)
        return frac_diff_cache.setdefault(
            cache_key,
            frac_diff_ffd(series, d, use_log=use_log)
            if fixed_width
            else frac_diff(series, d, use_log=use_log),
        )

    for i in range(20):  # max 20 iterations
        mid = (low + high) / 2
        diff = frac_diff_cached(series, mid, use_log)

        if len(diff) < 100:
            high = mid
            continue

        diff_adf = adf_data(series, diff, d=mid, out_df=diff_adf, alpha=alpha)

        if diff_adf.loc[mid, "stationary"]:
            best_series = diff.copy()
            best_d = mid
            high = mid
        else:
            low = mid

        if high - low < tol:
            break

    d = round(best_d, 4) if best_d is not None else max_d
    return best_series, d, diff_adf

Параметр use_log

Важная деталь: параметр use_log управляет тем, применяется ли логарифмирование перед дифференцированием. Для ценовых рядов оно должно быть True: логарифмирование переводит мультипликативную динамику цен (простые доходности — это отношения) в аддитивную динамику (лог-доходности — это разности), поэтому дробные веса работают в правильной области. Для рядов, которые уже аддитивны (лог-доходности, спреды, лог-цены), установите use_log=False, чтобы избежать двойного преобразования.


Реализация в afml

Промышленная реализация в afml.features.fracdiff использует внутренние циклы, скомпилированные Numba, для обеих стратегий окна. Внешняя логика обрабатывает индексы pandas, итерацию по столбцам DataFrame, распространение NaN и прямое заполнение. Внутренний цикл — скалярное произведение весов и значений — выполняется как скомпилированный машинный код.

Расширяющееся окно: frac_diff

Функция расширяющегося окна вычисляет каждое наблюдение с использованием всей доступной истории до этой точки. Ядро Numba выполняется параллельно по наблюдениям:

@njit(parallel=True, cache=True)
def _frac_diff_numba_core(series_values, weights, skip):
    """Numba-optimized core for expanding-window fractional differencing."""
    N = len(series_values)
    output_values = np.empty(N, dtype=np.float64)
    output_values[:] = np.nan

    for iloc in prange(skip, N):
        output_values[iloc] = np.dot(
            weights[-(iloc + 1):, :].T,
            series_values[:iloc + 1].reshape(-1, 1)
        )[0, 0]

    return output_values

Директива prange указывает Numba распределять итерации по ядрам CPU. Каждая итерация независима: скалярное произведение для наблюдения t не зависит от результата для наблюдения t−1, поэтому параллелизация безопасна. Параметр skip управляет тем, сколько начальных наблюдений остается NaN из-за порога потери веса.

Окно фиксированной ширины: frac_diff_ffd

Функция FFD применяет один и тот же усеченный вектор весов к каждому наблюдению. Поскольку вектор весов постоянен, скалярное произведение для каждого наблюдения имеет фиксированный размер:

@njit(parallel=True, cache=True)
def _frac_diff_ffd_numba_core(series_values, weights, skip):
    """Numba-optimized core for fixed-width window fractional differencing."""
    N = len(series_values)
    weights = weights.T
    arr = np.empty(N, dtype=np.float64)

    for i in prange(skip, N):
        arr[i] = np.dot(
            weights, series_values[i - skip:i + 1]
        )[0, 0]

    return arr[skip:]

Эта функция проще и быстрее версии с расширяющимся окном. Вектор весов транспонируется один раз перед циклом (без повторной транспозиции внутри), а срез series_values[i − skip : i + 1] всегда имеет одинаковую длину. Для типичной ширины окна около 100 и ряда примерно из 10 000 баров это выполняется менее чем за миллисекунду.

Внешняя обертка

Публичная функция frac_diff_ffd выполняет предварительную обработку — логарифмирование, распространение NaN, обработку входов типа Series и DataFrame — перед вызовом ядра Numba:

def frac_diff_ffd(series, d, thres=1e-5, use_log=True):
    """Fixed-width window fractional differentiation (AFML Section 5.5, page 83)."""
    if isinstance(series, pd.Series):
        series = series.copy().to_frame()

    if use_log:
        series_processed = np.log(series.clip(lower=1e-8))
    else:
        series_processed = series.copy()

    series_processed = series_processed.astype("float64")
    weights = get_weights_ffd(d, thres, series_processed.shape[0])
    width = len(weights) - 1

    df = {}
    for name in series_processed.columns:
        series_f = series_processed[[name]].ffill().dropna()
        ffd = _frac_diff_ffd_numba_core(series_f.values, weights, width)
        df[name] = pd.Series(ffd, index=series_f.index[width:])

    df = pd.concat(df, axis=1)
    if len(series_processed.columns) == 1:
        return df.squeeze()
    return df

clip(lower=1e-8) перед логарифмированием обеспечивает численную устойчивость: если какая-либо цена равна нулю или отрицательна (что возможно для спредов или синтетических рядов), логарифм не дает −∞. Цепочка ffill().dropna() заполняет пропуски перед дифференцированием, затем удаляет ведущие строки NaN, гарантируя, что ядро Numba получает чистый непрерывный массив.

FFD series overlaid on original prices

Рисунок 9. Дробное дифференцирование фиксированной ширины, наложенное на цены

  • Зеленая кривая (левая ось): ряд FFD (d = 0.40) — стационарный, колеблется вокруг устойчивого среднего.
  • Синяя кривая (правая ось): исходный ценовой ряд. Тренды, смены режимов и относительные уровни явно отслеживаются рядом FFD.
  • Корреляция ρ: количественно показывает, какая часть исходной структуры сохраняется после преобразования.


Интеграция в пайплайн

В пайплайне afml дробное дифференцирование находится на этапе построения признаков — после вычисления сырых признаков и перед передачей их в модель. Типичный рабочий процесс:

  1. Вычислить сырые признаки из баровых данных (цена закрытия, VWAP, объем, микроструктурные признаки и т. д.).
  2. Для каждого нестационарного признака (цены, накопленные объемы, накопительные суммы) применить fracdiff_optimal, чтобы найти минимальное d* и создать FFD-преобразованный признак.
  3. Проверить стационарность всей матрицы признаков с помощью is_stationary.
  4. Передать стационарные признаки с сохраняемой памятью в пайплайн обучения модели.

Утилита is_stationary проходит по всем столбцам DataFrame и показывает, какие столбцы не удовлетворяют критерию стационарности по ADF-тесту:

def is_stationary(df: pd.DataFrame, alpha: float = 0.05, verbose: bool = True):
    not_stationary = []
    for col in df:
        adf = adfuller(df[col], maxlag=1, regression="c", autolag=None)
        if not (adf[0] < adf[4][f"{alpha:.0%}"] and adf[1] < alpha):
            not_stationary.append(col)
    return not_stationary

Рекомендуемый рабочий процесс Лопеса де Прадо

Для практиков, начинающих с нуля, Лопес де Прадо рекомендует четырехшаговую процедуру, которая гарантирует применимость FFD независимо от исходного вида признака:

  1. Вычислить накопленную сумму временного ряда. Это гарантирует, что потребуется некоторый порядок дифференцирования, даже если исходный ряд уже стационарен: cumsum вводит единичный корень.
  2. Вычислить FFD(d) для различных d ∈ [0, 1].
  3. Определить минимальное d, при котором p-value ADF падает ниже 5%.
  4. Использовать ряд FFD(d) как предиктивный признак.

Эта процедура автоматизирована в fracdiff_optimal. Бинарный поиск заменяет ручную сетку на шаге 2, а функция возвращает и оптимальный ряд, и полную таблицу результатов ADF для проверки.

Особенности кэширования

Значение d* стабильно для заданного актива и таймфрейма: оно меняется только тогда, когда структура памяти ценового процесса существенно сдвигается (смена режима, структурные разрывы). В системе кэширования afml d* можно сохранять и повторно использовать между запусками без повторного бинарного поиска, если базовые данные существенно не изменились. Когда кэш хранит промежуточные ряды FFD, для fracdiff_optimal следует устанавливать auto_versioning=False, поскольку исходный код этой функции стабилен и не требует автоматической инвалидации кэша при каждом редактировании.


Заключение

Дробное дифференцирование решает противоречие между стационарностью и памятью, с которым сталкивается любой пайплайн финансового машинного обучения. Метод окна фиксированной ширины (FFD) — промышленный вариант: он применяет постоянный, заранее вычисленный вектор весов ко всему ряду, создавая бездрейфовый стационарный результат, который сохраняет максимум возможной памяти исходного ценового процесса. Бинарный поиск в fracdiff_optimal автоматизирует выбор минимального порядка дифференцирования d*, балансируя статистическую строгость (ADF-тест на выбранном уровне значимости) и сохранение памяти (корреляцию с исходным рядом).

Три свойства делают FFD особенно подходящим для промышленных пайплайнов:

  1. Вектор весов фиксирован — его можно заранее вычислить один раз и повторно использовать бесконечно для тех же d и τ.
  2. Каждое наблюдение зависит только от ограниченного ретроспективного окна, что делает метод совместимым с потоковыми архитектурами.
  3. Вычисление — простое скалярное произведение O(l*) на бар, легко параллелизуемое и хорошо ложится на аппаратно-эффективную реализацию.

Следующая статья использует эти свойства для реализации FFD-модуля для работы в реальном времени в MQL5 для MetaTrader 5. Вектор весов хранится как статический массив в OnInit(). Ретроспективное окно получается через CopyClose() ровно для l*+1 баров. Скалярное произведение реализуется компактным циклом, который выполняется за микросекунды.


Ссылки

  1. López de Prado, М. (2018). Advances in Financial Machine Learning. Wiley. Глава 5: дробно-дифференцированные признаки.
  2. Hosking, J. R. M. (1981). «Fractional differencing». Biometrika, 68(1), 165–176.
  3. Jensen, A. N. and M. Ø. Nielsen (2014). «A fast fractional difference algorithm». Journal of Time Series Analysis, 35(5), 428–436.
  4. Hamilton, J. D. (1994). Time Series Analysis. Princeton University Press.
  5. Alexander, C. (2001). Market Models. Wiley. Глава 11.

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

Файл Описание
fracdiff.py Полный модуль дробного дифференцирования: get_weights, get_weights_ffd, frac_diff, frac_diff_ffd, fracdiff_optimal, adf_data
stationary.py Утилита проверки стационарности: is_stationary

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

Прикрепленные файлы |
fracdiff.py (17.68 KB)
stationary.py (0.67 KB)
Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5 Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5
Мы создаем набор инструментов промышленного уровня для расчета размера позиции в MQL5: утилиты, фрагменты кода и пользовательские функции, которые повторяют исходные реализации на Python. Методы охватывают преобразование вероятности в размер позиции с коррекцией перекрытия, динамический расчет размера позиции по прогнозной цене (калиброванные сигмоидальная и степенная функции с лимитной ценой), бюджетирование на основе текущей занятости портфеля и резервный метод расчета размера позиции на основе модели смеси (EF3M). Результат — размер позиции со знаком в диапазоне [−1, ..., 1] плюс диагностика, которую можно напрямую подключить к логике ордеров.
Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения
В этой статье представлен инструмент "Multi-Timeframe Harmony Index" – продвинутый советник для MetaTrader 5, который рассчитывает взвешенное смещение рынка по нескольким таймфреймам, сглаживает значения с помощью EMA и выводит результат на аккуратной панели на графике. Он поддерживает настраиваемые алерты и автоматически наносит сигналы покупки и продажи на график, когда значение смещения пересекает значимые пороги. Подходит трейдерам, которые используют анализ нескольких таймфреймов, чтобы соотносить точки входа с общей структурой рынка.
Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5 Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5
Упростите графики MetaTrader 5 с помощью советника Multi Indicator Handler. Этот интерактивный инструмент объединяет индикаторы тренда, моментума и волатильности в единую панель, работающую в реальном времени. Мгновенно переключайтесь между профилями, чтобы сосредоточиться на нужном вам типе анализа. Одним кликом скрывайте и показывайте элементы панели и сохраняйте фокус на движении цены. Читайте дальше, чтобы шаг за шагом узнать, как самостоятельно создать и настроить этот инструмент на MQL5.
Переосмысливаем классические стратегии (Часть 16): Стратегия пробоя двойных полос Боллинджера Переосмысливаем классические стратегии (Часть 16): Стратегия пробоя двойных полос Боллинджера
Эта статья знакомит читателя с переосмысленной версией классической стратегии пробоев полос Боллинджера. В ней определены ключевые недостатки первоначального подхода, такие как его хорошо известная подверженность ложным пробоям. Цель статьи - представить возможное решение: торговую стратегию двойных полос Боллинджера (Double Bollinger Band). Этот относительно малоизвестный подход устраняет слабые места классической версии и предлагает более динамичный взгляд на финансовые рынки. Он помогает преодолеть старые ограничения, определенные первоначальными правилами, предлагая трейдерам более устойчивую и адаптивную систему.