English Deutsch 日本語
preview
Пишем первую модель стеклянного ящика (Glass Box) на Python и MQL5

Пишем первую модель стеклянного ящика (Glass Box) на Python и MQL5

MetaTrader 5Торговые системы | 4 апреля 2024, 10:55
401 8
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Введение

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

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

  1. Устаревший подход — простейший из возможных подходов. Мы просто подключим модель к терминалу MetaTrader 5, используя встроенную библиотеку Python в MetaTrader 5. После этого мы на MQL5 напишем советник для использования этой модели.
  2. Современный подход — это рекомендуемый способ интеграции моделей машинного обучения в советник. Мы экспортируем нашу модель "стеклянного ящика" в формат ONNX (Open Neural Network Exchange), а затем загрузим ее непосредственно в советник в качестве ресурса. Это позволит использовать все полезные функции, доступные в MetaTrader 5, и объединить их со всеми возможностями нашей модели стеклянного ящика.

AI

Рисунок 1. Имитация человеческого мозга через искусственный интеллект


Модели черного ящика и стеклянного ящика

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

Исследователи, разработчики и группа экспертов в предметной области из Microsoft Research открыли исходный код и на момент написания активно поддерживают пакет Python под названием Interpret ML. В пакет включен набор объяснителей черного ящика и моделей стеклянного ящика. Объяснители — это набор алгоритмов, которые пытаются понять внутреннюю работу модели черного ящика. Большинство алгоритмов объяснителей в Interpret ML не зависят от модели, то есть их можно применять к любой модели черного ящика. Однако такие объяснители могут дать оценки только моделей черного ящика. В следующем разделе статьи мы увидим, в чем здесь кроется проблема. Interpret ML также включает в себя набор моделей "стеклянного ящика", которые по точности прогнозов могут конкурировать с черными ящиками, но при этом обладают прозрачностью. Такой подходит идеально подходит для всех, кто использует машинное обучение, поскольку ценность интерпретируемости модели не зависит от области применения и уровня опыта.

Дополнительная информация:

1. Более подробно об этом можно почитать в документации по Interpret ML.

2. Также полезно будет ознакомиться с Interpret ML White Paper

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

Проблема моделей черного ящика: проблема разногласий

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

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

Дополнительная информация:

1. Рекомендую прочитать замечательную научно-исследовательскую работу от команды выпускников Гарварда, Массачусетского технологического института, Дрекселя и Карнеги-Меллона.

Давайте сразу посмотрим, как проявляется эта проблема разногласий:

Сначала импортируем пакеты Python для анализа.

#Import MetaTrader5 Python package
#pip install --upgrade MetaTrader5, if you don't have it installed
import MetaTrader5 as mt5

#Import datetime for selecting data
#Standard python package, no installation required
from datetime import datetime

#Plotting Data
#pip install --upgrade matplotlib, if you don't have it installed
import matplotlib.pyplot as plt

#Import pandas for handling data
#pip install --upgrade pandas, if you don't have it installed
import pandas as pd

#Import library for calculating technical indicators
#pip install --upgrade pandas-ta, if you don't have it installed
import pandas_ta as ta

#Scoring metric to assess model accuracy
#pip install --upgrade scikit-learn, if you don't have it installed
from sklearn.metrics import precision_score

#Import mutual information, a black-box explanation technique
from sklearn.feature_selection import mutual_info_classif

#Import permutation importance, another black-box explanation technique
from sklearn.inspection import permutation_importance

#Import our model
#pip install --upgrade xgboost, if you don't have it installed
from xgboost import XGBClassifier

#Plotting model importance
from xgboost import plot_importance

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

#Enter your account number
login = 123456789

#Enter your password
password = '_enter_your_password_'

#Enter your Broker's server
server = 'Deriv-Demo'

Теперь можем инициализировать терминал MetaTrader 5 и войти в торговый счет.

#We can initialize the MT5 terminal and login to our account in the same step
if mt5.initialize(login=login,password=password,server=server):
    print('Logged in successfully')
else:
    print('Failed To Log in')

Успешная авторизация.

Теперь у нас есть полный доступ к терминалу MetaTrader 5, и мы можем запрашивать данные графиков, тиковые данные, текущие котировки и многое другое.

#To view all available symbols from your broker
symbols = mt5.symbols_get()

for index,value in enumerate(symbols):
    print(value.name)

Volatility 10 Index

Volatility 25 Index

Volatility 50 Index

Volatility 75 Index

Volatility 100 Index

Volatility 10 (1s) Index

Boom 1000 Index

Boom 500 Index

Crash 1000 Index

Crash 500 Index

Step Index

...

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

#We need to specify the dates we want to use in our dataset
date_from = datetime(2019,4,17)
date_to = datetime.now()

Теперь можно запросить данные графика по символу.
#Fetching historical data
data = pd.DataFrame(mt5.copy_rates_range('Boom 1000 Index',mt5.TIMEFRAME_D1,date_from,date_to))

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

#Let's convert the time from seconds to year-month-date
data['time'] = pd.to_datetime(data['time'],unit='s')

data

Датафрейм после преобразования времени

Рис. 2. Наш датафрейм теперь отображает время в удобочитаемом формате. Обратите внимание, что столбец real_volume заполнен нулями.

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

#Let's create a function to preprocess our data
def preprocess(df):
    #All values of real_volume are 0 in this dataset, we can drop the column
    df.drop(columns={'real_volume'},inplace=True) 
    #Calculating 14 period ATR
    df.ta.atr(length=14,append=True)
    #Calculating the growth in the value of the ATR, the second difference
    df['ATR Growth'] = df['ATRr_14'].diff().diff()
    #Calculating 14 period RSI
    df.ta.rsi(length=14,append=True)    
    #Calculating the rolling standard deviation of the RSI
    df['RSI Stdv'] = df['RSI_14'].rolling(window=14).std()
    #Calculating the mid point of the high and low price
    df['mid_point'] = ( ( df['high'] + df['low'] ) / 2 )  
    #We will keep track of the midpoint value of the previous day
    df['mid_point - 1'] = df['mid_point'].shift(1) 
    #How far is our price from the midpoint?
    df['height'] = df['close'] - df['mid_point']  
    #Drop any rows that have missing values
    df.dropna(axis=0,inplace=True)

Далее вызываем функцию предобработки.

preprocess(data)

data

Фрейм данных после предварительной обработки

Рис. 3. Фрейм после предварительной обработки.

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

#We want to predict whether tomorrow's close will be greater than today's close
#We can encode a dummy variable for that: 
#1 means tomorrow's close will be greater.
#0 means today's close will be greater than tomorrow's.

data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

data

#The first date is 2019-05-14, and the first close price is 9029.486, the close on the next day 2019-05-15 was 8944.461
#So therefore, on the first day, 2019-05-14, the correct forecast is 0 because the close price fell the following day.


Создаем Target

Рис. 4. Создаем target

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

#Seperating predictors and target
predictors = ['open','high','low','close','tick_volume','spread','ATRr_14','ATR Growth','RSI_14','RSI Stdv','mid_point','mid_point - 1','height']
target     = ['target']

#The training and testing split definition
train_start = 27
train_end = 1000

test_start = 1001

Создаем выборки для обучения и тестирования.

#Train set
train_x = data.loc[train_start:train_end,predictors]
train_y = data.loc[train_start:train_end,target]

#Test set
test_x = data.loc[test_start:,predictors]
test_y = data.loc[test_start:,target]

Теперь можем обучить модель.

#Let us fit our model
black_box = XGBClassifier()
black_box.fit(train_x,train_y)

Проверяем прогнозы нашей модели на тестовом наборе.

#Let's see our model predictions
black_box_predictions = pd.DataFrame(black_box.predict(test_x),index=test_x.index)

Оцениваем точность нашей модели.

#Assesing model prediction accuracy
black_box_score = precision_score(test_y,black_box_predictions)

#Model precision score
black_box_score

0.4594594594594595

Наша модель имеет точность 45 %. Какие признаки работают на точность, а какие нет? В поставку XGBoost входит встроенная функция измерения важности признаков, которая упрощает нашу жизнь. Однако это относится конкретно к этой реализации XGBoost. Не все черные ящики позволяют легко оценить важность признаков. Например, нейронные сети и машины опорных векторов не имеют эквивалентной функции, и, чтобы лучше понять модель, вам придется трезво анализировать и тщательно интерпретировать веса модели самостоятельно. Функция plot_importance в XGBoost позволяет заглянуть внутрь нашей модели.

plot_importance(black_box)

Важность признаков в XGBoost

Рис. 5. Важность признаков в XGBClassifier. Обратите внимание, что в таблице нет никаких терминов взаимодействия. Означает ли это, что их не существует? Не обязательно.

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

Преимущества

  1. Независимость от модели — важность пермутации можно использовать в любой модели черного ящика без какой-либо предварительной обработки, необходимой для модели или функции важности перестановки, что упрощает интеграцию в существующий рабочий процесс машинного обучения. 
  2. Интерпретируемость — результаты легко интерпретируются последовательно независимо от оцениваемой базовой модели. Его достаточно просто использовать.
  3. Справляется нелинейностью — подходит для фиксации нелинейных связей между предикторами и ответом. 
  4. Работает с выбросами — важность перестановки не зависит от необработанных значений предикторов; это касается влияния признаков на производительность модели. Этот подход делает его устойчивым к выбросам, которые могут присутствовать в необработанных данных.

Недостатки

  1. Вычислительные затраты — для больших наборов данных с множеством признаков вычисление важности перестановок может быть дорогостоящим с точки зрения вычислений, поскольку при этому нужно перебирать каждый объект, переставлять его и оценивать модель, затем переходить к следующему объекту и повторять процесс.
  2. Проблема коррелирующих признаков — может давать необъективные результаты при оценке сильно коррелирующих признаков.
  3. Чувствительность к сложности модели — возможно, что слишком сложная модель будет демонстрировать высокую дисперсию при перестановке ее признаков, что затрудняет получение надежных выводов.
  4. Независимость признаков — метод предполагает, что объекты в наборе данных независимы и могут быть переставлены случайным образом без каких-либо последствий. Это упрощает вычисления, но в реальном мире большинство признаков зависят друг от друга и имеют взаимодействия, которые не учитываются методом важности пермутации. 

Посчитаем важность пермутации для нашего классификатора черного ящика.

#Now let us observe the disagreement problem
black_box_pi = permutation_importance(black_box,train_x,train_y)

# Get feature importances and standard deviations
perm_importances = black_box_pi.importances_mean
perm_std = black_box_pi.importances_std

# Sort features based on importance
sorted_idx = perm_importances.argsort()

Теперь построим график рассчитанных значений важности пермутации.

#We're going to utilize a bar histogram
plt.barh(range(train_x.shape[1]), perm_importances[sorted_idx], xerr=perm_std[sorted_idx])
plt.yticks(range(train_x.shape[1]), train_x.columns[sorted_idx])
plt.xlabel('Permutation Importance')
plt.title('Permutation Importances')
plt.show()

Важность пермутации

Рис. 6. Важность пермутации нашего черного ящика.

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

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

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

#Let's see if our black-box explainers will disagree with each other by calculating mutual information
black_box_mi = mutual_info_classif(train_x,train_y)
black_box_mi = pd.Series(black_box_mi, name="MI Scores", index=train_x.columns)
black_box_mi = black_box_mi.sort_values(ascending=False)

black_box_mi

RSI_14:              0.014579

open:                0.010044

low:                  0.005544

mid_point - 1:    0.005514

close:                0.002428

tick_volume :    0.001402

high:                 0.000000

spread:             0.000000

ATRr_14:           0.000000

ATR Growth:     0.000000

RSI Stdv:          0.000000

mid_point:       0.000000

height:             0.000000

Name: MI Scores, dtype: float64

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

Преимущества моделей стеклянного ящика

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

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

Создаем первую модель стеклянного ящика на Python

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

Для начала нужно установить Interpret ML.

#Installing Interpret ML
pip install --upgrade interpret

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

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Import matplotlib for plotting
import matplotlib.pyplot as plt

#Intepret glass-box model for classification
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Visualising our model's performance in one graph
from interpret.perf import ROC

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

Создаем данные для входа и логинимся в терминал MT5, как и раньше. На этом шаге не останавливаемся.

Снова выбираем символ, который нужно смоделировать, как и раньше. На этом шаге не останавливаемся.

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

Затем можем получить исторические данные. На этом шаге не останавливаемся.

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

После предварительной обработки данных добавляем target, как раньше. На этом шаге не останавливаемся.

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

Обучим модель.

#Let us fit our glass-box model
#Please note this step can take a while, depending on your computational resources
glass_box = ExplainableBoostingClassifier()
glass_box.fit(train_x,train_y)

Теперь можем заглянуть внутрь нашей стеклянной модели.

#The show function provides an interactive GUI dashboard for us to interface with out model
#The explain_global() function helps us find what our model found important and allows us to identify potential bias or unintended flaws
show(glass_box.explain_global())


Глобальное состояние стеклянного ящика

Рис. 7. Глобальное состояние Glass Box

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

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

Получение прогнозов белого ящика

#Obtaining glass-box predictions
glass_box_predictions = pd.DataFrame(glass_box.predict(test_x))

Теперь измерим точность модели.

glass_box_score = precision_score(test_y,glass_box_predictions)

glass_box_score

0.49095022624434387

Итак, модель белого ящика показала точность 49%. Очевидно, модель EBC (Explainable Boosting Classifier) может иметь больший вес по сравнению с XGBClassifier. Это демонстрирует возможности белых ящиков, обеспечивающих высокую точность без ущерба для понимания.

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

#We can also obtain individual explanations for each prediction
show(glass_box.explain_local(test_x,test_y))

Локальные объяснения

Рис. 8. Локальные объяснения из EBC (Explainable Boosting Classifier).

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

Далее будем смотреть на фактический класс и прогнозируемый класс. В данном случае фактический класс был равен 0, что означает, что цена закрытия упала, но мы классифицировали его как 1. Нам также представлены расчетные вероятности каждого класса. Наша модель неправильно оценила 53% вероятность того, что следующая свеча закроется выше. Также есть разбивка вклада каждого признака в предполагаемую вероятность. Признаки, выделенные синим цветом, работают против прогноза, сделанного нашей моделью, а оранжевые признаки стали основой для прогноза. Получается, что RSI внес наибольший вклад в эту неправильную классификацию, и при этом взаимодействие между разбросом и высотой указывали в правильном направлении. Эти признаки, возможно, заслуживают дальнейшей разработки, но необходимо более тщательное изучение локальных объяснений, прежде чем мы сможем сделать какие-либо выводы.

Теперь проверим работу модели с помощью графика ROC-кривой. График ROC позволяет оценить работу нашего классификатора. Нас интересует площадь под кривой или AUC. Теоретически идеальный классификатор будет иметь общую площадь под кривой равную 1. Это позволяет оценить классификатор с помощью всего одного графика.

glass_box_performance = ROC(glass_box.predict_proba).explain_perf(test_x,test_y, name='Glass Box')
show(glass_box_performance)

График ROC

Рис. 9: ROC-кривая нашей модели стеклянного ящика.

В модели AUC = 0.49. Эта простая метрика позволяет оценить работу модели с использованием единиц, которые легко понять. Кроме того, кривая не зависит от модели и может использоваться для сравнения различных классификаторов независимо от лежащих в ее основе методов классификации.

Подключение модели стеклянного ящика к терминалу MT5

Мы подошли к сути - подключим нашу модель к терминалу, используя сначала более простой подход. 

Для начала определим состояние текущего счета.

#Fetching account Info
account_info = mt5.account_info()

# getting specific account data
initial_balance = account_info.balance
initial_equity = account_info.equity

print('balance: ', initial_balance)
print('equity: ', initial_equity)

balance: 912.11 equity: 912.11

Получим все символы.

symbols = mt5.symbols_get()

Настроим глобальные переменные.

#Trading global variables
#The symbol we want to trade
MARKET_SYMBOL = 'Boom 1000 Index'

#This data frame will store the most recent price update
last_close = pd.DataFrame()

#We may not always enter at the price we want, how much deviation can we tolerate?
DEVIATION = 100

#For demonstrational purposes we will always enter at the minimum volume
#However,we will not hardcode the minimum volume, we will fetch it dynamically
VOLUME = 0
#How many times the minimum volume should our positions be
LOT_MUTLIPLE = 1

#What timeframe are we working on?
TIMEFRAME = mt5.TIMEFRAME_D1

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

В нашем случае будем открывать каждую сделку с минимальным объемом или с коэффициентом 1.

for index,symbol in enumerate(symbols):
    if symbol.name == MARKET_SYMBOL:
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        VOLUME = symbol.volume_min * LOT_MULTIPLE

Boom 1000 Index имеет минимальный объем 0,2.

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

# function to send a market order
def market_order(symbol, volume, order_type, **kwargs):
    #Fetching the current bid and ask prices
    tick = mt5.symbol_info_tick(symbol)
    
    #Creating a dictionary to keep track of order direction
    order_dict = {'buy': 0, 'sell': 1}
    price_dict = {'buy': tick.ask, 'sell': tick.bid}

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": volume,
        "type": order_dict[order_type],
        "price": price_dict[order_type],
        "deviation": DEVIATION,
        "magic": 100,
        "comment": "Glass Box Market Order",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_FOK,
    }

    order_result = mt5.order_send(request)
    print(order_result)
    return order_result

Далее определим вспомогательную функцию для закрытия сделок на основе тикета.

# Closing our order based on ticket id
def close_order(ticket):
    positions = mt5.positions_get()

    for pos in positions:
        tick = mt5.symbol_info_tick(pos.symbol) #validating that the order is for this symbol
        type_dict = {0: 1, 1: 0}  # 0 represents buy, 1 represents sell - inverting order_type to close the position
        price_dict = {0: tick.ask, 1: tick.bid} #bid ask prices

        if pos.ticket == ticket:
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "position": pos.ticket,
                "symbol": pos.symbol,
                "volume": pos.volume,
                "type": type_dict[pos.type],
                "price": price_dict[pos.type],
                "deviation": DEVIATION,
                "magic": 100,
                "comment": "Glass Box Close Order",
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": mt5.ORDER_FILLING_FOK,
            }

            order_result = mt5.order_send(request)
            print(order_result)
            return order_result

    return 'Ticket does not exist'

Чтобы постоянно не запрашивать с сервера много данных, обновим диапазон дат.

#Update our date from and date to
date_from = datetime(2023,11,1)
date_to = datetime.now()

Нам также нужна функция для получения прогноза от модели Glass Box и использования прогноза в качестве торговых сигналов.

#Get signals from our glass-box model
def ai_signal():
    #Fetch OHLC data
    df = pd.DataFrame(mt5.copy_rates_range(market_symbol,TIMEFRAME,date_from,date_to))
    #Process the data
    df['time'] = pd.to_datetime(df['time'],unit='s')
    df['target'] = (df['close'].shift(-1) > df['close']).astype(int)
    preprocess(df)
    #Select the last row
    last_close = df.iloc[-1:,1:]
    #Remove the target column
    last_close.pop('target')
    #Use the last row to generate a forecast from our glass-box model
    #Remember 1 means buy and 0 means sell
    forecast = glass_box.predict(last_close)
    return forecast[0]

Теперь напишем основную часть торгового робота на Python.

#Now we define the main body of our Python Glass-box Trading Bot
if __name__ == '__main__':
    #We'll use an infinite loop to keep the program running
    while True:
        #Fetching model prediction
        signal = ai_signal()
        
        #Decoding model prediction into an action
        if signal == 1:
            direction = 'buy'
        elif signal == 0:
            direction = 'sell'
        
        print(f'AI Forecast: {direction}')
        
        #Opening A Buy Trade
        #But first we need to ensure there are no opposite trades open on the same symbol
        if direction == 'buy':
            #Close any sell positions
            for pos in mt5.positions_get():
                if pos.type == 1:
                    #This is an open sell order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_totoal():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        #Opening A Sell Trade
        elif direction == 'sell':
            #Close any buy positions
            for pos in mt5.positions_get():
                if pos.type == 0:
                    #This is an open buy order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_get():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        print('time: ', datetime.now())
        print('-------\n')
        time.sleep(60)

Прогноз AI: Sell

Время:  2023-12-04 15:31:31.569495

-------

Торговый советник на основе Glass Box

Рис. 10. Торговый советник на основе стеклянного ящика на Python показывает прибыль

Создание советника для работы с моделью

Переходим к созданию помощника на MQL5 для нашей модели стеклянного ящика. Создадим советник, который будет перемещать стоп-лосс и тейк-профит на основе показаний ATR. Приведенный ниже код будет обновлять значения TP и SL на каждом тике. Выполнение этой задачи с использованием модуля интеграции Python было бы кошмаром и потребовало бы более-менее частые обновления, например раз в минуту или час. Нам же нужно обновлять SL и TP на каждом тике. Пользователь должен будет указать расстояние между входом и уровнем SL/TP. Умножим показания ATR на введенные пользователем данные, чтобы определить расстояние от SL или TP до точки входа. Второй параметр, который должен указать пользователь — период ATR.

//Meta Properties 
#property copyright "Gamuchirai Ndawana"
#property link "https://twitter.com/Westwood267"

//Classes for managing Trades And Orders
#include  <Trade\Trade.mqh>
#include <Trade\OrderInfo.mqh>

//Instatiating the trade class and order manager
CTrade trade;
class COrderInfo;

//Input variables
input double atr_multiple =0.025;  //How many times the ATR should the SL & TP be?
input int atr_period = 200;      //ATR Period

//Global variables
double ask, bid,atr_stop; //We will use these variables to determine where we should place our ATR
double atr_reading[];     //We will store our ATR readings in this arrays
int    atr;               //This will be our indicator handle for our ATR indicator
int min_volume;

int OnInit(){     
                  //Check if we are authorized to use an EA on the terminal
                  if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)){
                           Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
                           //Remove the EA from the terminal
                           ExpertRemove();
                           return(INIT_FAILED);
                  }
                  
                  //Check if we are authorized to use an EA on the terminal
                  else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
                            Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
                            //Remove the EA from the terminal
                            ExpertRemove();
                            return(INIT_FAILED);
                  }
                  
                  //If we arrive here then we are allowed to trade using an EA on the Terminal                
                  else{
                        //Symbol information
                        //The smallest distance between our point of entry and the stop loss
                        min_volume = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);//SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)
                        //Setting up our ATR indicator
                        atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
                        return(INIT_SUCCEEDED);
                  }                       
}

void OnDeinit(const int reason){

}

void OnTick(){
               //Get the current ask
               ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
               //Get the current bid
               bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
               //Copy the ATR reading our array for storing the ATR value
               CopyBuffer(atr,0,0,1,atr_reading);
               //Set the array as series so the natural time ordering is preserved
               ArraySetAsSeries(atr_reading,true); 
               
               //Calculating where to position our stop loss
               //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
               atr_stop = ((min_volume + atr_reading[0]) * atr_multiple);

               //If we have open positions we should adjust the stop loss and take profit 
               if(PositionsTotal() > 0){
                        check_atr_stop();          
               }
}

//--- Functions
//This funciton will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //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
                           double 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 ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}

Советник-помощник для нашего "стеклянного ящика"

Рис. 11. Советник-помощник для нашей модели "стеклянного ящика"

Экспорт модели стеклянного ящика в формат Open Neural Network Exchange (ONNX)


ONNX

Рис. 12. Логотип Open Neural Network Exchange.

ONNX (Open Neural Network Exchange) — открытый стандарт представления моделей нейронных сетей. Он широко поддерживается благодаря коллективным усилиям компаний со всего мира и из разных отраслей. Это такие компании, как Microsoft, Facebook, MATLAB, IBM, Qualcomm, Huawei, Intel, AMD, и др. На момент написания статьи ONNX является универсальной стандартной формой для представления любой модели машинного обучения независимо от того, в какой среде она была разработана, и, кроме того, позволяет разрабатывать и развертывать модели машинного обучения на разных языках программирования и в разных средах. Основная идея заключается в том, что любую модель машинного обучения можно представить в виде графа узлов и ребер. Каждый узел представляет собой математическую операцию, а каждое ребро — поток данных. Используя это простое представление, можно представить любую модель машинного обучения независимо от того, в какой среде она создана.

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

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

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

1. Lag height — высота со смещением; высота определяется как: (((High + Low) / 2) – Close), а lag height — это предыдущее значение высоты.

2. Height growth — оценкой второй производной показаний высоты. Для расчета дважды берется разница между последовательными историческими значениями высоты. Полученное значение дает представление о скорости изменения. Проще говоря, это помогает понять, ускоряется или замедляется рост высоты с течением времени.

3. Midpoint — рассчитывается как ((High + Low) / 2)

4. Midpoint growth — рост средней точки, это производный признак от показаний средней точки. Для расчета дважды берется разница между последовательными историческими значениями средней точки. Полученное значение дает представление о скорости изменения средней точки. Показывает, ускоряется или замедляется рост средней точки с течением времени. Говоря более простыми и менее техническими терминами, значение помогает понять, удаляется ли средняя точка от нуля с возрастающей скоростью или приближается к нулю со скоростью, которая растет все быстрее и быстрее.

Кроме того, мы поменяли символы: в первой половине статьи мы моделировали символ Boom 1000 Index, а теперь будем моделировать Volatility 75 Index.

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

Большая часть осталась прежней, за исключением двух новых добавлений: ONNX и ebm2onnx. Эти два пакета позволяют конвертировать нашу EBM (объяснимую машину повышения) в формат ONNX. 

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Keeping track of time
import time

#Import matplotlib
import matplotlib.pyplot as plt

#Intepret glass-box model
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

#ONNX
import onnx

#Import ebm2onnx
import ebm2onnx

#Path handling
from sys import argv

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

#Let's create a function to preprocess our data
def preprocess(data):
    data['mid_point'] = ((data['high'] + data['low']) / 2)

    data['mid_point_growth'] = data['mid_point'].diff().diff()

    data['mid_point_growth_lag'] = data['mid_point_growth'].shift(1)

    data['height'] = (data['mid_point'] - data['close'])

    data['height - 1'] = data['height'].shift(1)

    data['height_growth'] = data['height'].diff().diff()
    
    data['height_growth_lag'] = data['height_growth'].shift(1)
    
    data['time'] = pd.to_datetime(data['time'],unit='s')
    
    data.dropna(axis=0,inplace=True)
    
    data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

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

После настройки мы готовы перейти к экспорту модели в формат ONNX.

Сначала нужно указать путь, по которому мы сохраним модель. При каждой установке MetaTrader 5 создается специальная папка для файлов, которые можно использовать в терминале. Можно очень просто получить абсолютный путь, используя библиотеку Python.

terminal_info=mt5.terminal_info()
print(terminal_info)
TerminalInfo(community_account=False, community_connection=False, connected=True, dlls_allowed=False, trade_allowed=True, tradeapi_disabled=False, email_enabled=False, ftp_enabled=False, notifications_enabled=False, mqid=True, build=4094, maxbars=100000, codepage=0, ping_last=222088, community_balance=0.0, retransmission=0.030435223698894183, company='MetaQuotes Software Corp.', name='MetaTrader 5', language='English', path='C:\\Program Files\\MetaTrader 5', data_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075', commondata_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common')

Искомый путь сохраняется как data path в объекте terminal_info.

file_path=terminal_info.data_path+"\\MQL5\\Files\\"
print(file_path)

C:\Users\Westwood\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\

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

data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

data path to save onnx model C:\Users\Westwood\AppData\Local\Programs\Python\Python311\Lib\site-packages\

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

onnx_model = ebm2onnx.to_onnx(glass_box,ebm2onnx.get_dtype_from_pandas(train_x))
#Save the ONNX model in python
output_path = data_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)
#Save the ONNX model as a file to be imported in our MetaEditor
output_path = file_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)

Теперь мы готовы работать с файлом ONNX в MetaEditor 5. MetaEditor — это интегрированная среда разработки на языке MQL5. 

Открываем MetaEditor, дважды кликаем на Volatility Doctor 75 EBM и видим:

Первое открытие нашей модели ONNX

Рис. 13: Входные и выходные данные нашей модели ONNX.


Теперь создадим советник и импортируем нашу модель ONNX.

Начнем с указания общей информации о файле.

//+------------------------------------------------------------------+
//|                                                         ONNX.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
//Meta properties
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

Укажем глобальные переменных.

//Trade Library
#include <Trade\Trade.mqh>           //We will use this library to modify our positions

//Global variables
//Input variables
input double atr_multiple =0.025;    //How many times the ATR should the SL & TP be?
int input lot_mutliple = 1;          //How many time greater than minimum lot should we enter?
const int atr_period = 200;          //ATR Period

//Trading variables
double ask, bid,atr_stop;            //We will use these variables to determine where we should place our ATR
double atr_reading[];                //We will store our ATR readings in this arrays
int    atr;                          //This will be our indicator handle for our ATR indicator
long min_distance;                   //The smallest distance allowed between our entry position and the stop loss
double min_volume;                   //The smallest contract size allowed by the broker
static double initial_balance;       //Our initial trading balance at the beginning of the trading session
double current_balance;              //Our trading balance at every instance of trading
long     ExtHandle = INVALID_HANDLE; //This will be our model's handler
int      ExtPredictedClass = -1;     //This is where we will store our model's forecast
CTrade   ExtTrade;                   //This is the object we will call to open and modify our positions

//Reading our ONNX model and storing it into a data array
#resource "\\Files\\Volatility_75_EBM.onnx" as uchar ExtModel[] //This is our ONNX file being read into our expert advisor

//Custom keyword definitions
#define  PRICE_UP 1
#define  PRICE_DOWN 0

Указываем функцию OnInit(). Используем OnInit для настройки нашей модели ONNX. Для настройки модели ONNX нужно выполнить 3 простых шага. Сначала создадим модель ONNX из буфера, который использовали в глобальных переменных выше, когда нам требовалась модель ONNX в качестве ресурса. Читаем модель, указываем форму каждого отдельного входного и выходного параметра. После этого проверяем, не возникли ли какие-либо ошибки при попытке установить форму. Если все хорошо, получим минимальный объема контракта, разрешенного брокером, минимальное расстояния между стоп-лоссом и позицией входа, а также настроим индикатор ATR.

int OnInit()
  {
   //Check if the symbol and time frame conform to training conditions
   if(_Symbol != "Volatility 75 Index" || _Period != PERIOD_M1)
       {
            Comment("Model must be used with the Volatility 75 Index on the 1 Minute Chart");
            return(INIT_FAILED);
       }
    
    //Create an ONNX model from our data array
    ExtHandle = OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT);
    Print("ONNX Create from buffer status ",ExtHandle);
    
    //Checking if the handle is valid
    if(ExtHandle == INVALID_HANDLE)
      {
            Comment("ONNX create from buffer error ", GetLastError());
            return(INIT_FAILED);
      }
   
   //Set input shape
   long input_count = OnnxGetInputCount(ExtHandle);   
   const long input_shape[] = {1};
   Print("Total model inputs : ",input_count);
   
   //Setting the input shape of each input
   OnnxSetInputShape(ExtHandle,0,input_shape);
   OnnxSetInputShape(ExtHandle,1,input_shape);
   OnnxSetInputShape(ExtHandle,2,input_shape);
   OnnxSetInputShape(ExtHandle,3,input_shape);
   
   //Check if anything went wrong when setting the input shape
   if(!OnnxSetInputShape(ExtHandle,0,input_shape) || !OnnxSetInputShape(ExtHandle,1,input_shape) || !OnnxSetInputShape(ExtHandle,2,input_shape) || !OnnxSetInputShape(ExtHandle,3,input_shape))
      {
            Comment("ONNX set input shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
      
   //Set output shape
   long output_count = OnnxGetOutputCount(ExtHandle);
   const long output_shape[] = {1};
   Print("Total model outputs : ",output_count);
   //Setting the shape of each output
   OnnxSetOutputShape(ExtHandle,0,output_shape);
   //Checking if anything went wrong when setting the output shape
   if(!OnnxSetOutputShape(ExtHandle,0,output_shape))
      {
            Comment("ONNX set output shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
    //Get the minimum trading volume allowed  
    min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);  
    //Symbol information
    //The smallest distance between our point of entry and the stop loss
    min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
    //Initial account balance
    initial_balance = AccountInfoDouble(ACCOUNT_BALANCE);
    //Setting up our ATR indicator
    atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
    return(INIT_SUCCEEDED);
//---
  }

Функция DeInit удаляет обработчик ONNX, чтобы не занимать ресурсы, которые не используем.

void OnDeinit(const int reason)
  {
//---
   if(ExtHandle != INVALID_HANDLE)
      {
         OnnxRelease(ExtHandle);
         ExtHandle = INVALID_HANDLE;
      }
  }

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

void OnTick()
  {
//---
   //Time trackers
   static datetime time_stamp;
   datetime time = iTime(_Symbol,PERIOD_M1,0);

   //Current bid price
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   //Current ask price
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   
   //Copy the ATR reading our array for storing the ATR value
   CopyBuffer(atr,0,0,1,atr_reading);
   
   //Set the array as series so the natural time ordering is preserved
   ArraySetAsSeries(atr_reading,true); 
   
   //Calculating where to position our stop loss
   //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
   atr_stop = ((min_distance + atr_reading[0]) * atr_multiple);
   
   //Current Session Profit and Loss Position
   current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   Comment("Current Session P/L: ",current_balance - initial_balance);
   
   //If we have a position open we need to update our stoploss
   if(PositionsTotal() > 0){
        check_atr_stop();          
   }
   
    //Check new bar
     if(time_stamp != time)
      {
         time_stamp = time;
         
         //If we have no open positions let's make a forecast and open a new position
         if(PositionsTotal() == 0){
            Print("No open positions making a forecast");
            PredictedPrice();
            CheckForOpen();
         }
      }
   
  }
Далее определяем функцию, которая будет обновлять позицию тейк-профита и стоп-лосса по ATR. Функция перебирает каждую открытую позицию и проверяет, соответствует ли позиция торгуемому символу. Если соответствует, функция получает дополнительную информацию о позиции, на основе которой корректирует стоп-лосс и тейк-профит в зависимости от направления позиции. Обратите внимание: если сделка движется против позиции, тейк-профит и стоп-лосс останутся на своих местах.
//--- Functions
//This function will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //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 further 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 ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}
Нужна еще одна функция для открытия новой позиции. Обратите внимание, что мы используем глобальные переменные для значений Bid и Ask. Это гарантирует, что вся программа будет использовать одну и ту же цену. Кроме того, мы устанавливаем стоп-лосс и тейк-профит в значение 0, поскольку их значениями будет управлять функция check_atr_stop.
void CheckForOpen(void)
   {
      ENUM_ORDER_TYPE signal = WRONG_VALUE;
      
      //Check signals
      if(ExtPredictedClass == PRICE_DOWN)
         {
            signal = ORDER_TYPE_SELL;
         }
      else if(ExtPredictedClass == PRICE_UP)
         {
            signal = ORDER_TYPE_BUY;
         }
         
      if(signal != WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
         {
            double price, sl = 0 , tp = 0;
            
            if(signal == ORDER_TYPE_SELL)
               {
                  price = bid;
               }
               
           else
               {
                  price = ask;
               }
               
            Print("Opening a new position: ",signal);  
            ExtTrade.PositionOpen(_Symbol,signal,min_volume,price,0,0,"ONNX Order");
         }
   }
   

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

void PredictedPrice(void)
   {
      long output_data[] = {1};
      
      double lag_2_open = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_high = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_close = double(iClose(_Symbol,PERIOD_M1,3));
      double lag_2_low = double(iLow(_Symbol,PERIOD_M1,3));
      double lag_2_mid_point = double((lag_2_high + lag_2_low) / 2);
      double lag_2_height = double(( lag_2_mid_point - lag_2_close));
      
      double lag_open = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_high = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_close = double(iClose(_Symbol,PERIOD_M1,2));
      double lag_low = double(iLow(_Symbol,PERIOD_M1,2));
      double lag_mid_point = double((lag_high + lag_low) / 2);
      double lag_height = double(( lag_mid_point - lag_close));
      
      double   open  =  double(iOpen(_Symbol,PERIOD_M1,1));
      double   high  = double(iHigh(_Symbol,PERIOD_M1,1));
      double   low   = double(iLow(_Symbol,PERIOD_M1,1));
      double   close = double(iClose(_Symbol,PERIOD_M1,1));
      double   mid_point = double( (high + low) / 2 );
      double   height =  double((mid_point - close)); 
      
      double first_height_delta = (height - lag_height);
      double second_height_delta = (lag_height - lag_2_height);
      double height_growth = first_height_delta - second_height_delta;
      
      double first_midpoint_delta = (mid_point - lag_mid_point);
      double second_midpoint_delta = (lag_mid_point - lag_2_mid_point);
      double mid_point_growth = first_midpoint_delta - second_midpoint_delta;
      
      vector input_data_lag_height = {lag_height};
      vector input_data_height_grwoth = {height_growth};
      vector input_data_midpoint_growth = {mid_point_growth};
      vector input_data_midpoint = {mid_point};
      
       if(OnnxRun(ExtHandle,ONNX_NO_CONVERSION,input_data_lag_height,input_data_height_grwoth,input_data_midpoint_growth,input_data_midpoint,output_data))
         {
            Print("Model Inference Completed Successfully");
            Print("Model forecast: ",output_data[0]);
         }
       else
       {
            Print("ONNX run error : ",GetLastError());
            OnnxRelease(ExtHandle);
       }
        
       long predicted = output_data[0];
       
       if(predicted == 1)
         {
            ExtPredictedClass = PRICE_UP;
         }
         
       else if(predicted == 0)
         {
            ExtPredictedClass = PRICE_DOWN;
         }
   }

Далее можем скомпилировать нашу модель и протестировать ее на демо-счете в терминале MetaTrader 5.

Форвард-тестирование модели ONNX

Рис. 14. Форвард-тестирование советника Glass-box ONNX.

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

Проверяем наличие ошибок

Рис. 15. Проверяем наличие ошибок на вкладке Эксперты

Проверяем наличие ошибок на вкладке Журнал

Рис. 16. Проверяем наличие ошибок на вкладке Журнал

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

Настройка параметров модели

Рис. 17. Настройка параметров советника

Частые проблемы

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

Неправильная установка входных или выходные данных.

Наиболее часто встречающаяся проблема возникает из-за неправильной формы входов и выходов. Нужно определять входную форму для каждой функции, которую ожидает модель. Нужно при этом перебрать каждый индекс и определить входную форму для каждого объекта в этом индексе. Если не указать форму для каждого объекта, модель все равно может скомпилироваться без каких-либо ошибок. Но при попытке запустить работу модели мы получим ошибку. Код ошибки — 5808, в документации MQL5 он описан как "Размерность тензора не задана или указана неверно". В этом примере у нас есть 4 входных параметра. Для примера зададим форму только для одного. 

Неправильная установка формы входных данных

Рис. 18. Советник компилируется без исключений

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

Сообщение об ошибке 5808

Рис. 19. Сообщение об ошибке 5808

Неправильное приведение типов

Неправильное приведение типов иногда может привести к полной потере данных или просто к крэшу советника. В примере ниже использовался целочисленный массив для хранения выходных данных модели ONNX. Напомню, изначально модель ONNX имеет выходные данные типа int64. Как вы думаете, почему это приведет к ошибке? Ошибка связана с тем, что типу int не хватает памяти для хранения выходных данных нашей модели. Для вывода модели требуется 8 байт, но массив int предоставляет только 4. Решение простое: убедитесь, что используете правильный тип данных для хранения входных и выходных данных, а если необходимо приведение типов, убедитесь, что все происходит согласно правилам приведения, указанным в Документации по MQL5. Код ошибки — 5807, а описание — "Некорректный размер параметра".

Ошибка приведения типов

Рис. 20. Неправильное приведение типов

Сообщение об ошибке 5807

Рис. 21. Сообщение об ошибке 5807.

Ошибка вызова ONNX Run

Функция ONNX Run ожидает, что каждый из входных данных модели будет передан в отдельном массиве. В примере ниже мы объединили все входные данные в один массив и передаем этот единственный массив функции запуска ONNX. Код компилируется, но при запуске на вкладке Эксперты выдается ошибка. Код ошибки — 5804, и в документации он кратко описан как "В OnnxRun передано неверное количество параметров".

Ошибка вызова ONNX Run

Рис. 22. Ошибка вызова функции ONNXRun

Сообщение об ошибке 5804

Рис. 23. Сообщение об ошибке 5804.

Заключение

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

У моделей стеклянного ящика есть один большой недостаток, о котором мы пока не говорили: они менее гибкие по сравнению с моделями черного ящика. Модели стеклянного ящика представляют собой открытую область исследований, и со временем наверняка появятся более гибкие модели в будущем, однако на момент написания этой статьи проблема гибкости остается актуальной. А это означает, что есть такие отношения, которые лучше моделируется моделью черного ящика. Более того, текущие реализации моделей стеклянного ящика основаны на деревьях решений, поэтому текущая реализация объясненных классов BoostingClassifiers в InterpretML наследует все недостатки деревьев решений.

До новых встреч! Желаю вам мира, любви, согласия и выгодных сделок.

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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (8)
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 12 янв. 2024 в 12:04
linfo2 демо-счет ) попробовал один, который был там AUDHKD, но застрял в разногласиях проблема строки 204,
требуется хотя бы один массив или dtype

ValueError: требуется хотя бы один массив или dtype

Попробовал с NZDCNH, вроде работает через несколько итераций, но проваливается в sklern\multiclass в строке 167 с datahandling

debug говорит мне, что valueerror в строке 204 требуется один массив или dtype - возможно, мне нужно проверить мою демонстрационную среду, так как я создал ее только сегодня :)


на стандартном индексе Boom1000 проблема в строке 100 с датой и временем. raise KeyError(key)

KeyError: 'time' . Возможно, проблема в том, что мой часовой пояс - Новая Зеландия.

Сегодня у меня нет времени на тестирование, попробую завтра.

Привет, Линфо, надеюсь, это поможет:

1) Столбец 'time' - это имя, которое мой брокер дал временной метке UNIX, которая отмечает каждую строку в данных, которые я извлек. Возможно, ваш брокер использует другое имя, например 'date'. Проверьте кадр данных, который вы получаете после вызова copy_rates_range. Тот факт, что вы получаете ошибку "KeyError", может означать, что либо датафрейм абсолютно пуст, либо в нем нет столбца с именем 'time', возможно, на вашей стороне он имеет другое имя.

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

Если эти шаги не сработают, дайте мне знать.

linfo2
linfo2 | 15 янв. 2024 в 04:13

Спасибо за оперативную обратную связь и советы.

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

1) Я создал новый демо-счет для тестирования и не все валюты были доступны для Это решается путем открытия счета и убедиться, что валюты, которые вы хотите активны (золотой цвет)

2) Не было индекса Boom1000 (данных), предоставленных мне сервером, он был в списке, но не на моем счете (убедитесь, что вы изменили значение по умолчанию на то, к которому у вас есть доступ, и это может дать результат).

3) Для меня результаты интерпретации не отображались в std python, я мог работать только с установленной anaconda (было бы проще, если бы я сначала установил ее).

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

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

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 15 янв. 2024 в 19:45
linfo2 демо-счет, чтобы проверить это, и не все валюты были доступны для Это решается путем открытия счета и убедиться, что валюты, которые вы хотите активны (золотой цвет)

2) Не было индекса Boom1000 (данные), предоставленные мне сервером, он был в списке, но я не против моего счета (убедитесь, что вы изменить по умолчанию, чтобы быть то, что у вас есть доступ, и это может дать результат).

3) Для меня результаты интерпретации не отображались в std python, я мог работать только с установленной anaconda (было бы проще, если бы я сначала установил ее).

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

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

Я рад видеть, что вы добились существенного прогресса, Нил.

Stanislav Korotky
Stanislav Korotky | 4 апр. 2024 в 15:11

Удивительно: самая главная для понимания материала фраза - в самом конце статьи:

текущие реализации моделей стеклянного ящика основаны на деревьях решений

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 4 апр. 2024 в 15:21
Stanislav Korotky # :

Surprisingly: the most important phrase for understanding the material is at the very end of the article:

Yes you're right, looking back that information should've been covered in the introduction or the synopsis, your feedback will be applied in future. 
Разметка данных в анализе временных рядов (Часть 4): Декомпозиция интерпретируемости с использованием разметки данных Разметка данных в анализе временных рядов (Часть 4): Декомпозиция интерпретируемости с использованием разметки данных
В этой серии статей представлены несколько методов разметки временных рядов, которые могут создавать данные, соответствующие большинству моделей искусственного интеллекта (ИИ). Целевая разметка данных может сделать обученную модель ИИ более соответствующей пользовательским целям и задачам, повысить точность модели и даже помочь модели совершить качественный скачок!
Интерпретация моделей: Более глубокое понимание моделей машинного обучения Интерпретация моделей: Более глубокое понимание моделей машинного обучения
Машинное обучение — сложная и полезная область для любого человека независимо от опыта. В этой статье мы погрузимся во внутренние механизмы, лежащие в основе создаваемых моделей, исследуем сложный мир функций, прогнозов и эффективных решений и получим четкое понимание интерпретации моделей. Научитесь искусству поиска компромиссов, улучшения прогнозов, ранжирования важности параметров и принятия надежных решений. Статья поможет вам повысить производительность моделей машинного обучения и извлечь больше пользы от применения методологий машинного обучения.
Шаблоны проектирования в программировании на MQL5 (Часть 3): Поведенческие шаблоны 1 Шаблоны проектирования в программировании на MQL5 (Часть 3): Поведенческие шаблоны 1
В новая статье серии, посвященной шаблонам проектирования, мы рассмотрим поведенческие шаблоны, чтобы понять, как эффективно создавать методы взаимодействия между созданными объектами. Спроектировав эти шаблоны поведения, мы сможем понять, как создавать многоразовое, расширяемое и тестируемое программное обеспечение.
Популяционные алгоритмы оптимизации: Алгоритм птичьего роя (Bird Swarm Algorithm, BSA) Популяционные алгоритмы оптимизации: Алгоритм птичьего роя (Bird Swarm Algorithm, BSA)
В статье исследуется алгоритм BSA, основанный на поведении птиц, который вдохновлен коллективным стайным взаимодействием птиц в природе. Различные стратегии поиска индивидов в BSA, включая переключение между поведением в полете, бдительностью и поиском пищи, делают этот алгоритм многоаспектным. Он использует принципы стайного поведения, коммуникации, адаптивности, лидерства и следования птиц для эффективного поиска оптимальных решений.