Пишем первую модель стеклянного ящика (Glass Box) на Python и MQL5
Введение
Алгоритмы стеклянного (или белого) ящика — это прозрачные алгоритмы машинного обучения, которые учитывают внутренние механизмы системы. Они ставят под сомнение общепринятое мнению о том, что машинное обучение строится на компромиссе между точностью прогнозов и интерпретируемостью. Для этого такие модели обеспечивают высокий уровень точности и прозрачности. Это означает, что их экспоненциально легче отлаживать, поддерживать и улучшать после очередной итерации по сравнению с более распространенным "черным ящиком". Модели черного ящика — это когда слишком сложный процесс внутри системы скрыт от внешней среды. Такие модели зачастую представляют собой многомерные и нелинейные отношения, которые нам, людям, нелегко понять.
Как правило, модели черного ящика следует использовать только в тех случаях, когда "стеклянный ящик" не может обеспечить тот же уровень точности. В этой статье мы построим прозрачную модель и рассмотрим потенциальные преимущества ее использования. Рассмотрим два способа работы модели с терминалом MetaTrader 5:
- Устаревший подход — простейший из возможных подходов. Мы просто подключим модель к терминалу MetaTrader 5, используя встроенную библиотеку Python в MetaTrader 5. После этого мы на MQL5 напишем советник для использования этой модели.
- Современный подход — это рекомендуемый способ интеграции моделей машинного обучения в советник. Мы экспортируем нашу модель "стеклянного ящика" в формат ONNX (Open Neural Network Exchange), а затем загрузим ее непосредственно в советник в качестве ресурса. Это позволит использовать все полезные функции, доступные в MetaTrader 5, и объединить их со всеми возможностями нашей модели стеклянного ящика.
Рисунок 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.
Рис. 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)
Рис. 5. Важность признаков в XGBClassifier. Обратите внимание, что в таблице нет никаких терминов взаимодействия. Означает ли это, что их не существует? Не обязательно.
Теперь давайте посмотрим на первую технику объяснения черного ящика под названием Permutation importance (Важность пермутации/перестановочного признака). Она пытается оценить важность каждого признака путем случайного перетасовки значений каждого признака и последующего измерения изменения функции потерь модели. Объяснение здесь такое: чем больше ваша модель полагается на этот признак, тем хуже будут ее показатели, если мы случайным образом перетасуем эти значения. Посмотрим на преимущества и недостатки важности пермутации.
Преимущества
- Независимость от модели — важность пермутации можно использовать в любой модели черного ящика без какой-либо предварительной обработки, необходимой для модели или функции важности перестановки, что упрощает интеграцию в существующий рабочий процесс машинного обучения.
- Интерпретируемость — результаты легко интерпретируются последовательно независимо от оцениваемой базовой модели. Его достаточно просто использовать.
- Справляется нелинейностью — подходит для фиксации нелинейных связей между предикторами и ответом.
- Работает с выбросами — важность перестановки не зависит от необработанных значений предикторов; это касается влияния признаков на производительность модели. Этот подход делает его устойчивым к выбросам, которые могут присутствовать в необработанных данных.
Недостатки
- Вычислительные затраты — для больших наборов данных с множеством признаков вычисление важности перестановок может быть дорогостоящим с точки зрения вычислений, поскольку при этому нужно перебирать каждый объект, переставлять его и оценивать модель, затем переходить к следующему объекту и повторять процесс.
- Проблема коррелирующих признаков — может давать необъективные результаты при оценке сильно коррелирующих признаков.
- Чувствительность к сложности модели — возможно, что слишком сложная модель будет демонстрировать высокую дисперсию при перестановке ее признаков, что затрудняет получение надежных выводов.
- Независимость признаков — метод предполагает, что объекты в наборе данных независимы и могут быть переставлены случайным образом без каких-либо последствий. Это упрощает вычисления, но в реальном мире большинство признаков зависят друг от друга и имеют взаимодействия, которые не учитываются методом важности пермутации.
Посчитаем важность пермутации для нашего классификатора черного ящика.
#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)
Рис. 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
-------
Рис. 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)
Рис. 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)
Искомый путь сохраняется как 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 и видим:
Рис. 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.
Рис. 14. Форвард-тестирование советника Glass-box ONNX.
Чтобы убедиться, что модель работает без ошибок, нужно проверить вкладки Эксперты и Журнал.
Рис. 15. Проверяем наличие ошибок на вкладке Эксперты
Рис. 16. Проверяем наличие ошибок на вкладке Журнал
Как видите, модель работает. Помните, что настройки советника можно изменить в любое время.
Рис. 17. Настройка параметров советника
Частые проблемы
Давайте посмотрим на некоторые ошибки, с которыми можно столкнуться при первой настройке. Разберем, что вызывает ошибку, и посмотрим, как эти проблемы решить.
Неправильная установка входных или выходные данных.
Наиболее часто встречающаяся проблема возникает из-за неправильной формы входов и выходов. Нужно определять входную форму для каждой функции, которую ожидает модель. Нужно при этом перебрать каждый индекс и определить входную форму для каждого объекта в этом индексе. Если не указать форму для каждого объекта, модель все равно может скомпилироваться без каких-либо ошибок. Но при попытке запустить работу модели мы получим ошибку. Код ошибки — 5808, в документации MQL5 он описан как "Размерность тензора не задана или указана неверно". В этом примере у нас есть 4 входных параметра. Для примера зададим форму только для одного.
Рис. 18. Советник компилируется без исключений
Вот как выглядит ошибка на вкладке Эксперты. Уточню, что к статье приложен правильный код.
Рис. 19. Сообщение об ошибке 5808
Неправильное приведение типов
Неправильное приведение типов иногда может привести к полной потере данных или просто к крэшу советника. В примере ниже использовался целочисленный массив для хранения выходных данных модели ONNX. Напомню, изначально модель ONNX имеет выходные данные типа int64. Как вы думаете, почему это приведет к ошибке? Ошибка связана с тем, что типу int не хватает памяти для хранения выходных данных нашей модели. Для вывода модели требуется 8 байт, но массив int предоставляет только 4. Решение простое: убедитесь, что используете правильный тип данных для хранения входных и выходных данных, а если необходимо приведение типов, убедитесь, что все происходит согласно правилам приведения, указанным в Документации по MQL5. Код ошибки — 5807, а описание — "Некорректный размер параметра".
Рис. 20. Неправильное приведение типов
Рис. 21. Сообщение об ошибке 5807.
Ошибка вызова ONNX Run
Функция ONNX Run ожидает, что каждый из входных данных модели будет передан в отдельном массиве. В примере ниже мы объединили все входные данные в один массив и передаем этот единственный массив функции запуска ONNX. Код компилируется, но при запуске на вкладке Эксперты выдается ошибка. Код ошибки — 5804, и в документации он кратко описан как "В OnnxRun передано неверное количество параметров".
Рис. 22. Ошибка вызова функции ONNXRun
Рис. 23. Сообщение об ошибке 5804.
Заключение
В этой статье мы увидели, в чем может быть польза от использования модели стеклянного ящика при программировании для финансовых рынков. Такие варианты дают ценную информацию с небольшими затратами труда по сравнению с количеством усилий, которые потребовались бы для точного извлечения той же информации из модели черного ящика. Кроме того, модели стеклянного ящика легче отлаживать, поддерживать, интерпретировать и объяснять. Недостаточно предполагать, что модели ведут себя как предполагалось. Нужно убедиться в этом, заглянув под капот.
У моделей стеклянного ящика есть один большой недостаток, о котором мы пока не говорили: они менее гибкие по сравнению с моделями черного ящика. Модели стеклянного ящика представляют собой открытую область исследований, и со временем наверняка появятся более гибкие модели в будущем, однако на момент написания этой статьи проблема гибкости остается актуальной. А это означает, что есть такие отношения, которые лучше моделируется моделью черного ящика. Более того, текущие реализации моделей стеклянного ящика основаны на деревьях решений, поэтому текущая реализация объясненных классов BoostingClassifiers в InterpretML наследует все недостатки деревьев решений.
До новых встреч! Желаю вам мира, любви, согласия и выгодных сделок.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13842
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
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, из того, что вы описали, я думаю, что именно здесь все может развалиться. Проверьте имена столбцов данных, которые возвращаются вам после выполнения вызова.
Если эти шаги не сработают, дайте мне знать.
Спасибо за оперативную обратную связь и советы.
Обновляю здесь, так как это может быть полезно другим. Мои проблемы ;
1) Я создал новый демо-счет для тестирования и не все валюты были доступны для Это решается путем открытия счета и убедиться, что валюты, которые вы хотите активны (золотой цвет)
2) Не было индекса Boom1000 (данных), предоставленных мне сервером, он был в списке, но не на моем счете (убедитесь, что вы изменили значение по умолчанию на то, к которому у вас есть доступ, и это может дать результат).
3) Для меня результаты интерпретации не отображались в std python, я мог работать только с установленной anaconda (было бы проще, если бы я сначала установил ее).
После этой заминки документация была ясной и полезной, я все еще перевариваю результаты, поэтому пока не перешел к стороне mql5.
Еще раз спасибо за публикацию, и я с нетерпением жду, когда смогу лучше понять процесс. С уважением, Нил
2) Не было индекса Boom1000 (данные), предоставленные мне сервером, он был в списке, но я не против моего счета (убедитесь, что вы изменить по умолчанию, чтобы быть то, что у вас есть доступ, и это может дать результат).
3) Для меня результаты интерпретации не отображались в std python, я мог работать только с установленной anaconda (было бы проще, если бы я сначала установил ее).
После этой заминки документация была понятной и полезной, я все еще перевариваю результаты, поэтому пока не перешел к стороне mql5.
Еще раз спасибо за публикацию, и я с нетерпением жду, когда смогу лучше понять процесс. С уважением, Нил
Я рад видеть, что вы добились существенного прогресса, Нил.
Удивительно: самая главная для понимания материала фраза - в самом конце статьи:
текущие реализации моделей стеклянного ящика основаны на деревьях решений
Surprisingly: the most important phrase for understanding the material is at the very end of the article: