English Español Deutsch 日本語 Português
preview
Ложные регрессии в Python

Ложные регрессии в Python

MetaTrader 5Статистика и анализ | 23 сентября 2024, 15:48
700 4
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Краткое содержание

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

К сожалению, можно построить модели, используя наборы данных, не имеющие никакой реальной взаимосвязи. Причем, эти модели могут давать впечатляюще низкие показатели погрешности, усиливая ложное чувство контроля и приводя к чрезмерно оптимистичным прогнозам. Такие ошибочные модели обычно называют "ложными регрессиями" (spurious regressions).

В этой статье мы начнем с формирования интуитивного понимания ложных регрессий. После этого мы сгенерируем синтетические данные временного ряда, чтобы смоделировать ложную регрессию и наблюдать ее характерные эффекты. Затем мы углубимся в методы выявления ложных регрессий, полагаясь на наши знания для проверки модели машинного обучения, созданной на Python. Наконец, если наша модель пройдет проверку, мы экспортируем ее в ONNX и реализуем торговую стратегию на MQL5.


Введение: Ложные регрессии встречаются постоянно

Врач Игнац Земмельвейс жил в Вене в середине 19 века. Он был глубоко разочарован статистикой, которую наблюдал в больнице, где работал.

Игнац Земмельвейс

Рис. 1. Игнац Земмельвейс


Каждая пятая здоровая женщина, рожавшая в больнице, умирала от лихорадки, полученной во время родов. Игнац был полон решимости выяснить причину. Большинство врачей того времени связывали это с "плохим воздухом", который, по их мнению, был носителем злых духов. Как бы комично это ни звучало сегодня, в свое время такое представление было широко распространено. Но Земмельвейса оно не устраивало. Однажды он заметил, что врачи и студенты-медики, проводившие вскрытия в морге в одном крыле больницы, шли принимать роды в другое крыло, не помыв руки. После того, как ему удалось убедить персонал больницы соблюдать гигиену, уровень материнской смертности снизился с 20% до 1%.

К сожалению, открытия Земмельвейса остались незамеченными. Все его попытки достучаться до других врачей и медицинских учреждений лишь отдалили его от медицинского сообщества того времени, которое твердо верило в "плохой воздух". Игнац Земмельвейс умер в психиатрической лечебнице в 46 лет. Чему мы можем научиться у врачей, которые проигнорировали наблюдения Земмельвейса, и почему им было так трудно увидеть свою ошибку?

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

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

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

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

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

Существует множество тестов, которые мы можем использовать для проверки наличия единичных корней у наших остатков, например, расширенный тест Дики-Фуллера или тест Квятковского-Филлипса-Шмидта-Шина. Каждый тест имеет свои сильные и слабые стороны и может дать сбой в разных условиях. Чтобы увидеть ложные регрессии в действии, мы сгенерируем собственные данные временных рядов. Мы создадим два набора данных временных рядов, которые не будут связаны друг с другом, и посмотрим, что произойдет, если мы обучим модель, используя эти два независимых набора данных.


Моделирование ложных регрессий

Ложные регрессии могут возникать по разным причинам, но наиболее распространенной причиной является моделирование двух независимых и нестационарных временных рядов. Давайте разберем это техническое определение. Временной ряд — это просто равномерно записанные наблюдения случайной величины. Когда мы говорим, что временной ряд является стационарным, его статистические свойства, такие как среднее значение, дисперсия и структура автокорреляции, остаются относительно постоянными с течением времени. Временной ряд является нестационарным, когда его статистические свойства колеблются с течением времени.

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

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller , kpss
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

Далее мы определим статистические свойства для наших двух наборов данных: один будет имитировать наши входные данные, а другой — выходные. Оба набора данных будут содержать случайные нормально распределенные числа.

size = 1000
mu , sigma = 0 , 1
mu_y , sigma_y = 2 , 4

Приведенный ниже код генерирует два временных ряда, x_non_stationary и y_non_stationary, представляющие случайные блуждания. Случайность вводится посредством нормального распределения, а кумулятивная сумма гарантирует, что каждое значение в ряду зависит от предыдущих значений, создавая динамическое, нестационарное поведение.

Оба набора данных - x_non_stationary и y_non_stationary - генерируются случайным образом функцией numpy.random.normal. Таким образом, между двумя наборами данных нет никакой связи.

steps = np.random.normal(mu,sigma,size)
steps_y = np.random.normal(mu_y,sigma_y,size)
x_non_stationary =  pd.DataFrame(100 + np.cumsum(steps),index= np.arange(0,1000))
x_non_stationary_lagged = x_non_stationary.shift(1)
x_non_stationary_lagged.dropna(axis=0,inplace=True)
y_non_stationary =   pd.DataFrame(100 + np.cumsum(steps_y),index= np.arange(0,1000))

Давайте рассмотрим наши два случайных набора данных.

plt.plot(x_non_stationary)

Случайное блуждание x

Рис. 2. Случайное блуждание x




Давайте построим график нашего нестационарного временного ряда y.

plt.plot(y_non_stationary)

Случайное блуждание y

Рис. 3. Случайное блуждание y

Теперь рассмотрим результаты регрессии двух независимых и нестационарных временных рядов.

ols = sm.OLS(y_non_stationary,x_non_stationary)
lm = ols.fit()
print(lm.summary())

x_y_non_stationary_regression

Рис. 4. Сводные результаты регрессии двух независимых нестационарных переменных

Во многих временных рядах присутствует случайный трендовый компонент, известный как стохастический тренд. Даже если два временных ряда независимы, их стохастические тенденции могут демонстрировать кратковременные локальные корреляции. К сожалению, эти кратковременные корреляции иногда могут ввести наши модели в заблуждение и привести к ошибочному выводу о существовании связи между двумя независимыми временными рядами. Ложные регрессии, возникающие в таких случаях, часто дают высокие показатели R-квадрат. Важно помнить, что R-квадрат измеряет долю дисперсии в ответе, объясняемую дисперсией независимых переменных. Таким образом, если две переменные имеют коррелированные стохастические тенденции, надежность метрики R-квадрат может быть поставлена под угрозу. Проблема ложной регрессии особенно актуальна в контексте данных временных рядов и требует тщательного рассмотрения.

Скорректированный показатель R-квадрат нашей модели почти достигает 1, что означает, что модель воспринимает соответствие как почти идеальное. Модель утверждает, что приблизительно 90% дисперсии переменной отклика можно объяснить дисперсией предикторов. Однако эта демонстрация служит важным напоминанием о подводных камнях, связанных с ложными регрессиями. Мы знаем, что между входными и выходными данными нет никакой связи, они случайны и не имеют ничего общего.

Проблемы сохраняются, поскольку значение P представляется значимым, а 0 явно отсутствует в наших доверительных интервалах. Хотя это обычно можно интерпретировать как идеальный результат, необходимо проявить осторожность. В данном случае модель ошибочно указывает на сильную связь, которой на самом деле не существует. Мы сами создали входные и выходные данные, поэтому знаем, что никакой взаимосвязи нет.


Отстающий предиктор

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

Мы повторим ту же процедуру, что и выше, однако на этот раз мы также включим запаздывающую версию входных данных.

ols = sm.OLS(y_non_stationary.iloc[0:998,0],x_matrix.loc[0:998,['current_x','lagged_x']])
lm = ols.fit()
print(lm.summary())

Нестационарная регрессия X_y

Рис. 5. Сводная статистика нашей ложной регрессии

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


Статистика Дарбина-Уотсона

  •     Диапазон: Статистика Дурбина-Уотсона имеет значения от 0 до 4.
  •     Интерпретация: Значение, близкое к 2, указывает на отсутствие значимой автокорреляции. Значения значительно ниже 2 предполагают положительную автокорреляцию (остатки положительно коррелируют). Значения, значительно превышающие 2, указывают на отрицательную автокорреляцию (остатки отрицательно коррелируют).

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


Тесты на единичный корень

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

Теперь мы воспользуемся sklearn для подгонки модели к нашему обучающему набору.

lm = LinearRegression()
lm.fit(x[train_start:train_end],y[train_start:train_end])

Затем мы рассчитаем остатки.

residuals = y[test_start:test_end] - lm.predict(x[test_start:test_end])

Давайте построим график остатков.

residuals.plot()

График остатков

Рис. 6. Остатки нашей регрессионной модели, построенной с помощью Scikit-Learn


Теперь мы подвергнем наши остатки расширенному тесту Дики-Фуллера, чтобы определить их стационарность.


Расширенный тест Дики-Фуллера

Расширенный тест Дики-Фуллера (Augmented Dickey-Fuller, ADF) служит основным статистическим инструментом, предназначенным для оценки стационарности временного ряда или выявления единичного корня, указывающего на нестационарность. В области анализа временных рядов стационарность играет первостепенную роль, означая постоянство статистических атрибутов, таких как среднее значение и дисперсия, с течением времени. Наличие единичного корня во временном ряду, указывающее на нестационарность, вызывает обоснованное подозрение, что наблюдения внутри временного ряда могут быть охарактеризованы как стохастические или случайные по своей природе. Таким образом, тест ADF предлагает надежную методологию для изучения временного поведения набора данных, способствуя детальному пониманию его внутренних характеристик и потенциальных последствий для последующего анализа.

  1. Нулевая гипотеза: Нулевая гипотеза теста ADF утверждает, что временной ряд имеет единичный корень, что указывает на нестационарность.
  2. Альтернативная гипотеза: Альтернативная гипотеза расширенного теста Дики-Фуллера (ADF) заключается в том, что временной ряд не имеет единичного корня и является стационарным.
  3. Правило принятия решения: Правило принятия решения в тесте ADF включает сравнение тестовой статистики с критическими значениями. Если тестовая статистика меньше критического значения, нулевая гипотеза (наличие единичного корня) отклоняется, что указывает на стационарность. И наоборот, если тестовая статистика больше критического значения, то нет достаточных доказательств для отклонения нулевой гипотезы, что свидетельствует о нестационарности.

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

adfuller(residuals)

(-12.753804093890963, 8.423533501802878e-24, 2, 497, {'1%': -3.4435761493506294, '5%': -2.867372960189225, '10%': -2.5698767442886696}, 1366.9343966932422)

Наше внимание сосредоточено в основном на p-значении, полученном в ходе этого эксперимента, а именно на втором значении в предоставленном списке, которое равно 8.423533501802878e-24. Примечательно, что это p-значение приближается к нулю и значительно превосходит любое разумное критическое значение. В контексте расширенного теста Дики-Фуллера (ADF), если статистика ADF меньше критического значения, то отклонение нулевой гипотезы становится уместным, что означает наличие стационарности.

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

  1. Малый размер выборки: На эффективность теста ADF могут повлиять небольшие размеры выборки. В таких случаях тест может оказаться недостаточно мощным для обнаружения нестационарности.
  2. Плохой порядок отставания: Выбор порядка запаздывания в тесте ADF имеет решающее значение. Если порядок запаздывания указан неправильно, это может привести к неточным результатам. Использование слишком малого или слишком большого количества запаздываний может повлиять на способность теста фиксировать базовую структуру данных.
  3. Наличие детерминированных тенденций: Если данные содержат детерминированные тенденции (например, линейные тенденции, квадратичные тенденции), которые не учитываются в тестовой модели, тест ADF может не отвергнуть нулевую гипотезу. В таких случаях могут потребоваться этапы предварительной обработки, такие как устранение тренда.
  4. Неадекватное дифференцирование: Если порядок дифференцирования, используемый в тесте ADF, недостаточен для того, чтобы сделать данные стационарными, тест может не отвергнуть нулевую гипотезу.


Квятковски-Филлипс-Шмидт-Шин

Тест Квятковского-Филлипса-Шмидта-Шина (Kwiatkowski-Phillips-Schmidt-Shin, KPSS) является жизнеспособной альтернативой расширенному тесту Дики-Фуллера (ADF) для оценки стационарности данных временных рядов. Хотя оба теста широко распространены в анализе временных рядов, они расходятся в своих нулевых и альтернативных гипотезах, а также в своих базовых моделях. Выбор между тестами ADF и KPSS зависит от конкретных характеристик рассматриваемого временного ряда и общего исследовательского запроса. Совместное использование обоих тестов часто обеспечивает более полный анализ стационарности, предлагая исследователям детальное понимание динамики временного ряда.

  1. Нулевая гипотеза: Нулевая гипотеза теста KPSS заключается в том, что временной ряд является тренд-стационарным. Тренд-стационарность подразумевает, что ряд демонстрирует единичный корень, что свидетельствует о наличии детерминированной тенденции.
  2. Альтернативная гипотеза: Заключается в том, что временной ряд не является стационарным относительно тренда, что указывает на то, что он является стационарным относительно разности или стационарным относительно стохастического тренда.
  3. Правило принятия решения: Правило принятия решения для теста KPSS включает сравнение статистики теста с критическими значениями на выбранном уровне значимости (например, 1%, 5% или 10%). Если тестовая статистика больше критического значения, нулевая гипотеза отклоняется, что свидетельствует о том, что временной ряд не является тренд-стационарным. С другой стороны, если тестовая статистика меньше критического значения, то нулевая гипотеза не может быть отклонена, что подразумевает стационарность тренда.

В случае теста KPSS общепринятым пороговым значением является уровень значимости 0,05. Если статистика KPSS падает ниже этого порога, это свидетельствует о нестационарности данных. В нашем анализе статистика KPSS дала значение 0,016, что подтверждает ее отклонение от критического порога и указывает на тенденцию к нестационарности в наборе данных. Этот результат еще раз подчеркивает важность рассмотрения нескольких диагностических инструментов, таких как тесты ADF и KPSS, для обеспечения тщательной и точной оценки характеристик временного ряда.

kpss(residuals)

(0.6709994557854182, 0.016181867655871072, 1, {'10%': 0.347, '5%': 0.463, '2.5%': 0.574, '1%': 0.739})

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

Вот некоторые ситуации, в которых тест KPSS может ошибочно отвергнуть нулевую гипотезу:

  1. Сезонные закономерности: Тест KPSS чувствителен как к тренду, так и к сезонности. Если временной ряд демонстрирует ярко выраженную сезонность, тест может интерпретировать его как нестационарную тенденцию. В таких случаях может потребоваться дифференциация для устранения сезонности.
  2. Структурные разрывы: Если во временном ряду наблюдаются структурные разрывы, например, внезапные и существенные изменения в базовом процессе генерации данных, тест KPSS может обнаружить их как нестационарные тенденции. Структурные разрывы могут привести к отклонению нулевой гипотезы.
  3. Выбросы: Наличие выбросов в данных может повлиять на эффективность теста KPSS. Выбросы могут восприниматься как отклонения от тренда, что приводит к отклонению от стационарности тренда. Устойчивость к выбросам является важным фактором при интерпретации результатов теста KPSS.
  4. Нелинейные тенденции: Тест KPSS предполагает линейную тенденцию. Если базовая тенденция во временном ряду нелинейна, тест может дать вводящие в заблуждение результаты. Нелинейные тенденции могут не быть адекватно отражены тестом, что приводит к ложному отклонению стационарности.

Крайне важно интерпретировать результаты теста KPSS с осторожностью и учитывать специфические характеристики анализируемого временного ряда. Кроме того, сочетание теста KPSS с другими тестами стационарности, такими как расширенный тест Дики-Фуллера (ADF), может обеспечить более полную оценку свойств стационарности временного ряда.


Объединяем всё вместе

Настало время переключить внимание с синтетических контрольных данных и направить усилия на анализ подлинных рыночных данных, полученных непосредственно из нашего терминала MetaTrader 5. Для облегчения этого перехода разработаем скрипт MetaQuotes Language 5 (MQL5). Скрипт будет специально разработан для извлечения данных из нашего торгового терминала, их форматирования и экспорта в формат CSV.

Первым шагом объявим глобальные переменные, первый набор из которых предназначен для размещения дескрипторов наших технических индикаторов. Эти переменные будут играть ключевую роль в эффективном управлении и доступе к соответствующим индикаторам на протяжении всего выполнения скрипта, способствуя общей согласованности и организации нашей программы MetaQuotes Language 5 (MQL5).

//---Our handlers for our indicators
int ma_handle;
int rsi_handle;
int cci_handle;
int ao_handle;

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

//---Data structures to store the readings from our indicators
double ma_reading[];
double rsi_reading[];
double cci_reading[];
double ao_reading[];

Далее, естественно, мы создаем имя для файла.

//---File name
string file_name = "Market Data.csv";

Теперь определим, какой объем данных необходимо извлечь.

//---Amount of data requested
int size = 3000;

Приступая к разработке нашего обработчика событий OnStart, первым делом мы запускаем инициализацию наших назначенных технических индикаторов.

//---Setup our technical indicators
ma_handle = iMA(_Symbol,PERIOD_CURRENT,20,0,MODE_EMA,PRICE_CLOSE);
rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,60,PRICE_CLOSE);
cci_handle = iCCI(_Symbol,PERIOD_CURRENT,10,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_CURRENT);

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

//---Set the values as series
CopyBuffer(ma_handle,0,0,size,ma_reading);
ArraySetAsSeries(ma_reading,true);
CopyBuffer(rsi_handle,0,0,size,rsi_reading);
ArraySetAsSeries(rsi_reading,true);
CopyBuffer(cci_handle,0,0,size,cci_reading);
ArraySetAsSeries(cci_reading,true);
CopyBuffer(ao_handle,0,0,size,ao_reading);
ArraySetAsSeries(ao_reading,true);

При подготовке к началу процесса записи файла важным этапом является создание обработчика файлов в нашем скрипте.

//---Write to file
int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

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

for(int i=-1;i<=size;i++){
      if(i == -1){
            FileWrite(file_handle,"Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO");
      }
      
        else{
                FileWrite(file_handle,iOpen(_Symbol,PERIOD_CURRENT,i),
                                        iHigh(_Symbol,PERIOD_CURRENT,i),
                                        iLow(_Symbol,PERIOD_CURRENT,i),
                                        iClose(_Symbol,PERIOD_CURRENT,i),
                                        ma_reading[i],
                                        rsi_reading[i],
                                        cci_reading[i],
                                        ao_reading[i]);
      } 
}

После завершения настройки скрипта приступим к его выполнению на выбранном символе. Затем скрипт сгенерирует CSV-файл, давая полное и структурированное представление о соответствующих данных, связанных с выбранным символом.

Торговые данные

Рис. 7. CSV-файл рыночных данных

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

Начиная этот этап, наш первый шаг включает загрузку основных зависимостей. Среди этих зависимостей заметным дополнением является пакет Arch, известный своим комплексным набором инструментов статистического анализа. Интеграция Arch предоставляет нам целый ряд бесценных ресурсов для использования в наших аналитических усилиях, повышая глубину и сложность нашего подхода к анализу рыночных данных.

import pandas as pd
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from arch.unitroot import PhillipsPerron , ADF , KPSS
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import DoubleTensorType

Затем мы считываем CSV-файл, созданный с помощью нашего скрипта MQL5.

csv = pd.read_csv("/enter/your/path/here")

Затем мы готовим нашу цель - рост цены закрытия.

csv["Target"] = csv["Close"] / csv["Close"].shift(-15)
csv.dropna(axis=0,inplace=True)

Изучим наш блок данных.

блок данных

Рис. 8. Чтение CSV-файла с рыночными данными с помощью Pandas

Разделим наши данные на тренировочные и тестовые.

train = np.arange(0,20000)
test = np.arange(20020,89984)


Теперь построим множественную линейную регрессию с использованием statsmodels.

ols = sm.OLS(csv.loc[:,"Target"],csv.loc[:,["Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO"]])
lm = ols.fit()
print(lm.summary())

Линейная регрессия рыночных данных

Рис. 9. Результаты регрессии всех переменных для прогнозирования роста цены

Займемся интерпретацией результатов нашей модели. Доверительный интервал для коэффициента, связанного с функцией цены открытия, охватывает 0, что подразумевает отсутствие статистической значимости. Следовательно, решение исключить эту особенность из нашей модели обусловлено потенциальной незначительностью ее вклада. Дальнейшее изучение показывает, что как индекс относительной силы (RSI), так и индекс товарного канала (CCI) демонстрируют коэффициенты, близкие к 0, а их соответствующие доверительные интервалы демонстрируют аналогичную близость к этому значению. Учитывая это, мы проявляем осторожность, решая исключить эти функции, исходя из их незначительного вклада.

Хотя наша модель может похвастаться весьма высоким значением R-квадрата, что позволяет предположить, что значительная часть дисперсии объясняется включенными признаками, сопутствующее исследование статистики Дарбина-Уотсона (DW) показывает низкое значение. Это требует осторожности, поскольку низкая статистика DW указывает на возможность остаточной корреляции, которая может поставить под угрозу достоверность нашей модели. Следовательно, мы выступаем за тщательный анализ остатков, уделяя особое внимание их стационарности. Этот дополнительный уровень проверки необходим для обеспечения надежности и достоверности нашей модели при выявлении базовых закономерностей в данных.

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

predictors = ["High","Low","Close","MA 20","AO"]

Давайте запишем остатки.

residuals = csv.loc[test[0]:test[-1],"Target"] - lm.predict(csv.loc[test[0]:test[-1],predictors])

Давайте приступим к построению остаточных графиков — важному этапу в процессе оценки модели. 

plt.plot(residuals)

Остаточные рыночные данные

Рис. 10. Построение графика остатков на основе прогнозных рыночных данных

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

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

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

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

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

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

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

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


Тест Филлипса-Перрона

Тест Филлипса-Перрона — статистический тест, используемый в эконометрике и анализе временных рядов для оценки наличия единичного корня в наборе данных временных рядов. Единичный корень подразумевает, что переменная временного ряда имеет стохастическую тенденцию, что делает ее нестационарной. Стационарность является важнейшим допущением во многих статистических анализах, а нестационарные данные временных рядов могут привести к ложным результатам регрессии.

Тест Филлипса-Перрона — это разновидность теста Дики-Фуллера, предложенная Питером Филлипсом и Пьером Перроном в 1988 году. Как и тест Дики-Фуллера, тест Филлипса-Перрона предназначен для обнаружения наличия единичного корня путем изучения поведения переменной временного ряда с течением времени.

Основная идея теста Филлипса-Перрона заключается в регрессии разностной переменной временного ряда по ее запаздывающим значениям. Затем тестовая статистика используется для оценки того, насколько сильно отличается коэффициент запаздывающей переменной от нуля. Если коэффициент значительно отличается от нуля, это свидетельствует против наличия единичного корня и означает, что временной ряд является стационарным.

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

  1. Нулевая гипотеза: Заключается в том, что переменная временного ряда содержит единичный корень, что означает, что она нестационарна.
  2. Альтернативная гипотеза: Переменная временного ряда не содержит единичного корня, что указывает на ее стационарность.
  3. Правило принятия решения: Если вычисленная статистика теста меньше критического значения, нулевая гипотеза о наличии единичного корня отвергается, что свидетельствует о стационарности временного ряда. Если вычисленная статистика теста больше критического значения, нулевая гипотеза не отвергается, свидетельствуя о недостаточности доказательств для вывода о стационарности временного ряда.

Мы воспользуемся библиотекой Arch для проведения теста Филлипса-Перрона.

pp = PhillipsPerron(residuals)

Мы можем получить сводные результаты. Сводная статистика по тесту Филлипса-Перрона:

pp.summary()

Тест Филлипса-Перрона
(Z-tau)

Тестовая статистика -73.916

Значение p 0.000

Запаздывания 62

Тренд: Постоянный
Критические значения: -3.43 (1%), -2.86 (5%), -2.57 (10%)

Нулевая гипотеза: Процесс содержит единичный корень.
Альтернативная гипотеза: Процесс слабо стационарен.

Давайте вместе интерпретируем результаты. Тестовая статистика равна -73,916. Это значение показывает, на сколько стандартных отклонений расчетный коэффициент отличается от предполагаемого значения 1 (что указывает на наличие единичного корня). В этом случае очень большая отрицательная статистика теста свидетельствует о веских доказательствах против наличия единичного корня, подтверждая стационарность временного ряда.

Значение p, связанное со статистикой теста, равно 0,000. Значение p является мерой доказательств против нулевой гипотезы. Значение p, равное 0,000, означает, что наблюдаемая тестовая статистика крайне маловероятна при условии, что нулевая гипотеза верна. С практической точки зрения это чрезвычайно малое значение p является убедительным доказательством против наличия единичного корня.

Учитывая очень низкое значение p (0,000), мы, скорее всего, отвергнем нулевую гипотезу при обычных уровнях значимости (например, 0,05). Данные свидетельствуют о том, что временной ряд, скорее всего, стационарен, поскольку p-значение ниже выбранного уровня значимости. Подводя итог, можно сказать, что на основании предоставленных результатов у нас есть веские доказательства для отклонения нулевой гипотезы о единичном корне, что указывает на то, что временной ряд, скорее всего, стационарен. Однако мы не можем основывать свои решения на тестах, мы хотим наблюдать показатели центральной тенденции из разных тестов.

Расширенный тест Дики-Фуллера

Мы будем использовать библиотеку Arch для выполнения расширенного теста Дики-Фуллера.

adf = ADF(residuals)

Мы можем получить сводную статистику. Сводная статистика расширенного теста Дики-Фуллера

adf.summary()
Результаты расширенного теста Дики-Фуллера

Тестовая статистика -31.300
Значение P 0.000
Запаздывания 60

Тренд: Постоянный
Критические значения: -3.43 (1%), -2.86 (5%), -2.57 (10%)

Нулевая гипотеза: Процесс содержит единичный корень.
Альтернативная гипотеза: Процесс слабо стационарен.


Давайте теперь интерпретируем результаты. Тестовая статистика равна -31,300. Как и тест Филлипса-Перрона, статистика теста ADF используется для оценки наличия единичного корня во временном ряду. В этом случае очень большое отрицательное значение указывает на веские доказательства против нулевой гипотезы о единичном корне, подтверждая идею о том, что временной ряд является стационарным.

Соответствующее p-значение равно 0,000. Подобно тесту Филлипса-Перрона, p-значение 0,000 означает, что наблюдаемая статистика теста крайне маловероятна при условии, что нулевая гипотеза (наличие единичного корня) верна. Очень малое значение p дает весомые доказательства против нулевой гипотезы.

Учитывая низкое значение p (0,000), мы, скорее всего, отвергнем нулевую гипотезу при обычных уровнях значимости. Данные расширенного теста Дики-Фуллера подтверждают вывод о том, что временной ряд, скорее всего, стационарен.

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


Экспортируем нашу модель в формат Open Neural Network Exchange

Open Neural Network Exchange (ONNX) - открытый и взаимозаменяемый формат для представления моделей машинного обучения. Платформа ONNX обеспечивает бесперебойный обмен моделями между различными фреймворками и инструментами, способствуя взаимодействию в экосистеме машинного обучения. Она обеспечивает стандартизированный способ представления и передачи обученных моделей, что упрощает разработчикам развертывание моделей на разных платформах и интеграцию их в разнообразные приложения. ONNX поддерживает широкий спектр моделей и фреймворков машинного обучения, обеспечивая гибкость и эффективность рабочих процессов разработки и развертывания моделей.

Этот код определяет начальный тип для переменной с именем double_input, которая будет использоваться в контексте генерации файла ONNX (Open Neural Network Exchange). Тип DoubleTensorType означает, что входные данные должны иметь двойную точность. Форма входного тензора определяется числом столбцов в DataFrame (предположительно, с именем csv), соответствующих признакам, используемым для прогнозирования (извлекаемым с помощью csv.loc[:, predictors].shape[1]). None указывает на то, что размер первого измерения (вероятно, представляющего количество экземпляров или образцов) на данном этапе не фиксирован.

initial_type_double = [('double_input', DoubleTensorType([None, csv.loc[:,predictors].shape[1]]))]

Код использует функцию convert_sklearn для преобразования нашей обученной модели линейной регрессии (lm) в ее представление ONNX. Переменная initial_type_double, определенная в предыдущем фрагменте кода, указывает ожидаемый тип входных данных как двойную точность. Кроме того, параметр target_opset равен 12, что указывает на желаемую версию набора операторов ONNX. Полученный onnx_model_double будет представлять собой модель ONNX - модель линейной регрессии, пригодную для развертывания и взаимодействия с другими фреймворками, поддерживающими формат ONNX.

onnx_model_double = convert_sklearn(lm, initial_types=initial_type_double, target_opset=12)

Этот код указывает имя файла (EURUSD_ONNX) для сохранения представления модели линейной регрессии. Полученная модель ONNX, преобразованная с помощью ранее упомянутой функции convert_sklearn, будет сохранена с этим именем файла, что сделает ее легко узнаваемой и доступной для будущего использования или развертывания.

onnx_model_filename = "EURUSD_ONNX"

Этот код объединяет ранее определенное имя файла ("EURUSD_ONNX") с суффиксом "_Double.onnx" для создания нового имени файла ("EURUSD_ONNX_Double.onnx"). Затем функция onnx.save_model используется для сохранения модели ONNX (onnx_model_double) в файле с созданным именем файла. Этот процесс гарантирует, что модель ONNX, представляющая собой модель линейной регрессии с двойной точностью, будет сохранена и на нее можно будет легко ссылаться, используя указанное имя файла.

onnx_filename=onnx_model_filename+"_Double.onnx"
onnx.save_model(onnx_model_double, onnx_filename)


Создание советника для файла ONNX на MQL5 с помощью встроенного редактора MetaEditor

Советник — это скрипт, написанный на языке MQL5, который позволяет осуществлять автоматическую торговлю на платформе MetaTrader 5. Советник будет взаимодействовать с файлом ONNX, способствуя бесшовной интеграции моделей машинного обучения в торговые стратегии, тем самым улучшая процессы принятия решений на основе предиктивной аналитики. MetaEditor предоставляет комплексную среду для кодирования, тестирования и оптимизации советников, гарантируя эффективное развертывание и выполнение в рамках платформы MetaTrader 5.

Сначала мы включаем библиотеку Trade в MQL5, которая является стандартной библиотекой для обработки торговых операций в MetaTrader 5. Библиотека Trade предоставляет предопределенные функции и структуры, которые облегчают выполнение различных торговых операций, таких как открытие и закрытие позиций, управление ордерами и обработка событий, связанных с торговлей. Включение этой библиотеки в советник позволяет оптимизировать и эффективно реализовать торговую логику и операции в коде MQL5.

//Our trade class helps us open trades
#include <Trade\Trade.mqh>
CTrade Trade;

Этот фрагмент кода включает использование советника и модели ONNX для предиктивной аналитики. Директива #resource используется для встраивания файла модели ONNX, EURUSD_ONNX_Double.onnx, в ресурсы советника в виде массива байтов с именем ONNXModel. Это облегчает доступ и использование модели машинного обучения в рамках советника.

Переменная ONNXHandle инициализируется как INVALID_HANDLE, что указывает на то, что она будет использоваться для хранения дескриптора или идентификатора, связанного с моделью ONNX, после ее загрузки во время выполнения советника.

Кроме того, PredictedMove инициализируется значением -1, что говорит о том, что прогнозируемый ход или результат на основе модели ONNX еще не определен. Эта переменная, скорее всего, будет обновлена с использованием прогнозируемого значения после того, как советник обработает соответствующие данные с помощью модели ONNX во время своего выполнения. Специфика предиктивной логики и дальнейшей обработки будет зависеть от последующих разделов кода советника.

//Loading our ONNX model
#resource "ONNX\\EURUSD_ONNX_Double.onnx" as uchar EURUSD_ONNX_MODEL[]
long     ONNXHandle=INVALID_HANDLE;

В этом разделе кода MQL5 для советника объявляются два набора переменных: ma_handle и ma_reading[] для скользящей средней, а также ao_handle и ao_reading[] для Awesome Oscillator.

Переменная ma_handle служит ссылкой или идентификатором для индикатора скользящей средней, позволяя советнику взаимодействовать с этим конкретным инструментом технического анализа и получать информацию о нем. Массив ma_reading[] предназначен для хранения рассчитанных значений скользящей средней, что позволяет советнику получать доступ к ее историческим значениям и анализировать их для принятия решений.

Аналогично ожидается, что переменная ao_handle будет представлять идентификатор индикатора Awesome Oscillator, а массив ao_reading[] предназначен для хранения соответствующих вычисленных значений. 

//Handles for our technical indicators and dynamic arrays to store their readings
int ma_handle;
double ma_reading[];
int ao_handle;
double ao_reading[];

//Inputs
int input sl_width = 150; //How wide should the stoploss be?
int input  positions = 1; //How many positions should the we open?
int input lot_multiple = 1; //How many times greater than minimum lot should each position be?

//Symbol variables
double min_volume;
double bid,ask;

//We'll use this time stamp to keep track of the number of candles passing
static datetime time_stamp;

//Our model's forecast will be stored here
vector model_forecast(1);


Функция OnInit

Функция OnInit() является важнейшей частью советника и служит функцией инициализации, которая автоматически выполняется при присоединении советника к графику в MetaTrader 5. В этой функции обычно выполняются разные задачи, связанные с настройкой и подготовкой советника. Остальная часть рассматриваемого кода вложена в обработчик OnInit().

Этот условный оператор в функции OnInit() советника проверяет, используется ли торговый символ EURUSD и установлен ли таймфрейм графика на M1 (минутный интервал). Если условия не выполняются, советник выводит сообщение на консоль с помощью функции Print(), в котором указывается, что модель должна работать именно с валютной парой EURUSD на минутном таймфрейме. Оператор return(INIT_FAILED) используется для завершения инициализации советника, указывая на сбой, если указанные условия не выполнены.

int OnInit(){
    //Validating trading conditions
   if(_Symbol!="EURUSD" || _Period!=PERIOD_M1)
     {
      Print("Model must work with EURUSD on the M1 timeframe");
      return(INIT_FAILED);
     }

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

После выполнения переменной ONNXHandle присваивается хэндл или идентификатор, связанный с созданной моделью ONNX. Затем условный оператор проверяет, является ли ONNXHandle допустимым хэндлом (не равным INVALID_HANDLE). Если хэндл недействителен, советник выводит сообщение об ошибке на консоль с помощью функции Print(), предоставляя информацию о возникшей ошибке с помощью GetLastError(), а затем сигнализирует об ошибке инициализации, возвращая INIT_FAILED.

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

ONNXHandle=OnnxCreateFromBuffer(ONNXModel,ONNX_DEFAULT);

if(ONNXHandle==INVALID_HANDLE)
{
   Print("OnnxCreateFromBuffer error ",GetLastError());
   return(INIT_FAILED);
}

Строка кода объявляет константный массив input_shape с двумя элементами. Массив обозначается как long и содержит целые числа. Элементы массива представляют собой форму входных данных для модели или алгоритма машинного обучения. В этом случае массив input_shape инициализируется значениями {1, 5}, что указывает на то, что входные данные, как ожидается, будут иметь размер в одну строку и пять столбцов.

//Defining the model's input shape
const long input_shape[] = {1,5};

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

Оператор if оценивает, выполняется ли отрицание условия OnnxSetInputShape (возвращает true, если настройка не удалась). При true советник выводит сообщение об ошибке на консоль с помощью функции Print(), передавая сведения о возникшей ошибке, полученные с помощью GetLastError(). Впоследствии функция возвращает INIT_FAILED, что указывает на ошибку инициализации.

if(!OnnxSetInputShape(ONNXHandle,ONNX_DEFAULT,input_shape)) 
{
    Print("OnnxSetInputShape error ",GetLastError());
    return(INIT_FAILED); 
}

Эта строка кода объявляет константный массив output_shape с двумя элементами. Подобно предыдущему объявлению для input_shape, массив имеет тип long и содержит целые числа. В этом случае output_shape присваиваются значения {1, 1}, указывая на то, что ожидаемая форма выходных данных модели машинного обучения представляет собой одномерный массив с одним элементом.

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

//Defining the model's output shape
const long output_shape[] = {1,1};

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

Оператор if оценивает, выполняется ли отрицание условия OnnxSetOutputShape (возвращает true, если настройка не удалась). При true советник выводит сообщение об ошибке на консоль с помощью функции Print(), передавая сведения о возникшей ошибке, полученные с помощью GetLastError(). Впоследствии функция возвращает INIT_FAILED, что указывает на ошибку инициализации.

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

if(!OnnxSetOutputShape(ONNXHandle,0,output_shape))
{
    Print("OnnxSetOutputShape error ",GetLastError());
    return(INIT_FAILED);
}

В этой части функции OnInit() инициализируются два технических индикатора.

  1. ma_handle присваивается хэндл или идентификатор для 20-периодной экспоненциальной скользящей средней (EMA), рассчитанной на основе цен закрытия (PRICE_CLOSE) одноминутного графика (PERIOD_M1) для символа, указанного _Symbol.
  2. ao_handle присвоен идентификатор индикатора Awesome Oscillator (AO), рассчитанного на минутном графике для того же символа.
  3. min_volume - наименьший размер контракта, разрешенный брокером.
//Setting up our technical indicators
ma_handle = iMA(_Symbol,PERIOD_M1,20,0,MODE_EMA,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_M1);
min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
return(INIT_SUCCEEDED);
}


Функция OnDeinit

Функция OnDeinit служит обработчиком деинициализации советника. Она автоматически выполняется при удалении советника или закрытии торговой платформы.

В этой функции условный оператор проверяет, содержит ли переменная ONNXHandle допустимый хэндл (не равный INVALID_HANDLE). Если условие истинно, это означает, что модель ONNX была инициализирована в течение срока службы советника. В таких случаях вызывается функция OnnxRelease для освобождения ресурсов, связанных с моделью ONNX, и впоследствии ONNXHandle устанавливается в значение INVALID_HANDLE.

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

void OnDeinit(const int reason)
  {
      //OnDeinit we should freeup resources we don't require
      //We'll begin by releasing the ONNX models then removing the Expert Advisor
      if(ONNXHandle!=INVALID_HANDLE)
     {
      OnnxRelease(ONNXHandle);
      ONNXHandle=INVALID_HANDLE;
     }
   //Lastly remove the Expert Advisor
   ExpertRemove();
  }


Функция OnTick

Функция OnTick является важнейшим компонентом советника MQL5, представляющим собой код, выполняемый на каждом входящем тике. В этой функции:

Исторические данные индикаторов Moving Average (MA) и Awesome Oscillator (AO) извлекаются с помощью функции CopyBuffer. Последние 10 значений копируются в массивы (ma_reading и ao_reading), а затем применяется ArraySetAsSeries для организации массивов в последовательном порядке.

Текущая временная метка (current_time) получается с помощью функции iTime для минутного графика (PERIOD_M1) и указанного символа (_Symbol).

Условный оператор проверяет, отличается ли текущая временная метка от сохраненной временной метки (time_stamp). Если есть разница, указывающая на новый тик, вызывается функция ModelForecast.

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

void OnTick()
{
   //Update the arrays storing the technical indicator values to their current values
   //Then set the arrays as series so that the current reading is at the top and the oldest reading is last
   CopyBuffer(ma_handle,0,0,10,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ao_handle,0,0,10,ao_reading);
   ArraySetAsSeries(ao_reading,true);
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
//Update time marker
   datetime current_time = iTime(_Symbol,PERIOD_M1,0);
   //Periodically make forecasts if we have no open positions
   if(time_stamp != current_time){
      //No open positions
      if(PositionsTotal() == 0){
               ModelForecast();
      }  
      //We have open positions to manage
      else if(PositionsTotal() > 0){
               ManageTrade();
      }
   }
  }


Вывод модели

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

Внутри функции объявлены два вектора:

  1. model_forecast - вектор, используемый для хранения выходных данных модели машинного обучения (прогноз).
  2. model_input - вектор, содержащий входные данные, необходимые для модели. К этим данным относятся максимальная, минимальная цена и цена закрытия текущей свечи, значение скользящей средней (ma_reading[0]) и значение Awesome Oscillator (ao_reading[0]).

Функция OnnxRun вызывается для выполнения вывода с использованием модели ONNX (ONNXHandle). Флаг ONNX_NO_CONVERSION указывает, что в процессе вывода преобразование типов данных не применяется. Предоставляются входные данные (model_input), а полученный прогноз сохраняется в векторе model_forecast.

Условный оператор проверяет, был ли процесс вывода успешным. В противном случае на консоль с помощью функции Print() выводится сообщение об ошибке, содержащее сведения об обнаруженной ошибке, полученные с помощью GetLastError().

Если вывод успешен, прогноз, сохраненный в векторе model_forecast, выводится на консоль.

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

//This function provides these utilities:
//          1) Inferencing using our ONNX model 
//          2) Calling the next function responsible for intepreting model forecasts and other subroutines
void ModelForecast(void){
      //These are the inputs for our ONNX model:
      //          1)High 
      //          2)Low
      //          3)Close
      //          4)20 PERIOD MA MODE: EMA  APPLIED_PRICE:PRICE CLOSE 
      //          5)Awesome Oscilator
      vector model_input{iHigh(_Symbol,PERIOD_M1,0),iLow(_Symbol,PERIOD_M1,0),iClose(_Symbol,PERIOD_M1,0),ma_reading[0],ao_reading[0]};
      //Inferencing with our model
      if(!OnnxRun(ONNXHandle,ONNX_NO_CONVERSION,model_input,model_forecast)){
            Print("Error performing inference: ",GetLastError());
      }
      //Pring model forecast to the terminal
      else{
               Print(model_forecast);
                NextMove();
      }
}


Интерпретация модели

Для интерпретации нашей модели необходимо выполнить две задачи:

  1. Интерпретация выходных данных модели: для интерпретации выходных данных прогностической модели используется функция InterpretForecast. Функция InterpretForecast вызывается дважды: сначала с аргументом 1, а затем с аргументом -1. Цель этих вызовов — проверить, указывают ли выходные данные модели на определенное направление: 1 для положительного прогноза и -1 для отрицательного.
  2. Действие на основе интерпретаций: в зависимости от интерпретации выходных данных модели функция выполняет определенные действия. Если модель предсказывает положительный результат (1), она вызывает функцию CheckOrder с аргументом 1, а затем возвращается. Если модель предсказывает отрицательный результат (-1), она вызывает функцию CheckOrder с аргументом -1.

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

//This function provides these utilities:
//          1) Getting the model output intepreted
//          2) Acting on the intepretations
void NextMove(){
      if(InterpretForecast(1)){
            CheckOrder(1);
            return;
      }     
      else if(InterpretForecast(-1)){
            CheckOrder(-1);
      }
}


Функция InterpretForecast служит для интерпретации выходных данных прогностической модели на основе указанного направления. Функция принимает аргумент direction, который может быть равен 1 или -1. Интерпретация зависит от того, предсказала ли модель значение больше 1 или меньше 1.

Если направление равно 1, функция проверяет, является ли первый элемент (model_forecast[0]) массива model_forecast больше 1. Если это так, функция возвращает true, указывая на то, что модель прогнозирует рост, превышающий текущую цену.

Если направление равно -1, функция проверяет, является ли первый элемент (model_forecast[0]) массива model_forecast меньше 1. Если это так, функция возвращает true, указывая на то, что модель прогнозирует рост ниже текущей цены.

Если направление не равно ни 1, ни -1, функция возвращает false. Это указывает на то, что указанное направление не распознано и функция не может провести интерпретацию.

Подводя итог, функция InterpretForecast проверяет прогнозируемое значение модели на основе указанного направления и возвращает true, если условие выполняется. В противном случае функция возвращает false.

//This function provides these utilities:
//          1) Check whether the model forecasted a reading greater than or less than 1.
bool InterpretForecast(int direction){
      //1 means check if the model is forecasting growth greater than the current price
      if(direction == 1){
            return(model_forecast[0] > 1);
      }
      //-1 means check if the model is forecasting growth less than the current price
      if(direction == -1){
            return(model_forecast[0] < 1);
      }
      //Otherwise return false.
      return false;
}


Исполнение ордеров

Следующий код определяет функцию CheckOrder. На эту функцию возложена ответственность за открытие торговых позиций на основе указанного направления ордера.

Проверка открытых позиций: начальный условный оператор (PositionsTotal() == 0) служит в качестве предварительной проверки, гарантируя, что новые позиции открываются исключительно при отсутствии активных сделок в портфеле.

Выполнение ордеров на покупку: если параметр order_direction равен 1 (что указывает на ордер на покупку), функция использует цикл for для итеративного выполнения требуемого количества позиций (positions). В этом цикле функция Trade.PositionOpen вызывается для инициализации позиций на покупку. В качестве аргументов этой функции предоставляются соответствующие параметры, такие как символ (_Symbol), тип ордера (ORDER_TYPE_BUY), объем (min_volume * lot_multiple) и цена исполнения (ask).

Выполнение ордеров на продажу: И наоборот, если параметр order_direction равен -1 (что указывает на ордер на продажу) и в данный момент нет активных позиций (PositionsTotal() == 0 переоценивается), функция переходит к открытию позиций на продажу с помощью аналогичного итеративного процесса. Снова применяется функция Trade.PositionOpen с параметрами, адаптированными для позиций продажи.

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

//This function is responsible for opening positions
void CheckOrder(int order_direction){
      //Only open new positions if we have no open positions
     if(PositionsTotal() == 0){
            //Buy
            if(order_direction == 1){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                           Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,0,0,"Volatitlity Doctor AI");
                  }
            }
            //Sell
            else if(order_direction == -1 && PositionsTotal() == 0){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                        Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume  * lot_multiple ,bid,0,0,"Volatitlity Doctor AI");
            }
         }
   }
}


Управление сделками

Функция ManageTrade действует как центральный модуль управления торговлей, делегируя ответственность за настройку уровней стоп-лосс и тейк-профит функции CheckStop.

Функция CheckStop перебирает все открытые позиции, извлекая соответствующую информацию, такую как символ, тикет, тип позиции, текущий стоп-лосс и цена открытия позиции. Он гарантирует, что символ позиции соответствует символу, по которому ведется активная торговля (_Symbol). Для каждой действительной позиции код рассчитывает новые уровни стоп-лосс и тейк-профит на основе предопределенных параметров, таких как цены bid и ask, ширина Stop Loss (sl_width) и значение пункта (_Point).

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

Использование NormalizeDouble гарантирует, что рассчитанные уровни соответствуют указанному количеству цифр (_Digits). Функция Trade.PositionModify используется для изменения существующей сделки с обновленными уровнями стоп-лосс и тейк-профит только при необходимости.

//This function handles our trade management
void ManageTrade(){
   CheckStop();
}

//This funciton will update our S/L & T/P 
void CheckStop(){
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           long type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the stop we calculated above
                                  double new_stop_loss = NormalizeDouble(ask - ((sl_width * _Point) / 2) ,_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(ask + (sl_width * _Point),_Digits);
                                  //If our current stop loss is less than our calculated stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }  
                           }
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                  //The new stop loss value is just the ask price minus the stop we calculated above
                                  double new_stop_loss = NormalizeDouble(bid + ((sl_width * _Point)/2),_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(bid - (sl_width * _Point),_Digits);
                                 //If our current stop loss is greater than our calculated stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }
                           }  
                  }  
            }
}


Теперь наш советник будет содержать эти параметры в своем меню.


Советник

Рис. 11. Наш советник


Также он должен автоматически устанавливать уровни стоп-лосс и тейк-профит для каждой открываемой сделки.

Советник в действии

Рис. 12. Наш советник в действии


Заключение

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

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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Carl Schreiber
Carl Schreiber | 21 июн. 2024 в 11:31
Очень интересная статья! Но трейдерам, не обладающим глубокими статистическими знаниями, было бы полезно вкратце объяснить основные термины, такие как остатки (разница с прогнозом), стационарность (вариация и среднее постоянны или нет) и т.д.
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 21 июн. 2024 в 12:31
Carl Schreiber #:
Очень интересная статья! Но для трейдеров, не обладающих глубокими статистическими знаниями, было бы полезно кратко объяснить основные термины, такие как остатки (разница с прогнозом), стационарность (вариация и среднее постоянны или нет) и т.д.

Спасибо, Карл, вы правы, в следующий раз я буду помнить, что нужно быть кратким и милым, чтобы максимизировать полезность.

Aleksey Vyazmikin
Aleksey Vyazmikin | 23 сент. 2024 в 20:47

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

Из статьи создалось ощущение, что временные ряды котировок стационарны, но из всех источников нам сообщают обратное. Думаю, это ошибка восприятия материала.

Так же не освещён вопрос точности модели, как я понял, она вовсе не точна, а если так, то можно ли применять тогда разные тесты при таком сильном разбросе ошибок в ответах модели?

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

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

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 24 сент. 2024 в 09:37
Aleksey Vyazmikin регрессионной модели.

Я думаю, что необходимо больше статей на эту тему, которые действительно можно применить к цитатам.

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


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

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

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


Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Теория Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Теория
Статья посвящена метаэвристическому алгоритму Atmosphere Clouds Model Optimization (ACMO), который моделирует поведение облаков для решения задач оптимизации. Алгоритм использует принципы генерации, движения и распространения облаков, адаптируясь к "погодным условиям" в пространстве решений. Статья раскрывает, как метеорологическая симуляция алгоритма находит оптимальные решения в сложном пространстве возможностей и подробно описывает этапы работы ACMO, включая подготовку "неба", рождение облаков, их перемещение и концентрацию дождя.
Разработка системы репликации (Часть 47): Проект Chart Trade (VI) Разработка системы репликации (Часть 47): Проект Chart Trade (VI)
Наконец, наш индикатор Chart Trade начинает взаимодействовать с советником, позволяя передавать информацию в интерактивном режиме. Поэтому в этой статье мы доработаем индикатор, сделав его функциональным настолько, чтобы его можно было использовать вместе с каким-либо советником. Это позволит нам получить доступ к индикатору Chart Trade и работать с ним, как если бы он действительно был связан с советником. Но сделаем мы это гораздо более интересным способом чем ранее.
Нейросети в трейдинге: Безмасочный подход к прогнозированию ценового движения Нейросети в трейдинге: Безмасочный подход к прогнозированию ценового движения
В данной статье предлагаем познакомиться с методом Mask-Attention-Free Transformer (MAFT) и его применение в области трейдинга. В отличие от традиционных Transformer, требующих маскирования данных при обработке последовательностей, MAFT оптимизирует процесс внимания, устраняя необходимость в маскировании, что значительно повышает вычислительную эффективность.
Нейросети в трейдинге: Superpoint Transformer (SPFormer) Нейросети в трейдинге: Superpoint Transformer (SPFormer)
В данной статья предлагаем познакомиться с методом сегментации 3D-люъектов на основе Superpoint Transformer (SPFormer), который устраняет необходимость в промежуточной агрегации данных. Что ускоряет процесс сегментации и повышает производительность модели.