
Переосмысливаем классические стратегии (Часть X): Может ли ИИ управлять MACD?
Пересечение скользящих средних, вероятно, является одной из старейших существующих торговых стратегий. Индикатор схождения-расхождения скользящих средних (Moving Average Convergence Divergence (MACD)) это очень популярный индикатор, построенный на основе понятия пересечения скользящих средних. В нашем сообществе появилось много новых участников, которым, возможно, будет интересно узнать о прогностической силе индикатора MACD в их стремлении разработать наилучшую торговую стратегию из возможных. Кроме того, есть опытные технические аналитики, использующие MACD в своих стратегиях, которые могут держать в голове тот же вопрос. В настоящей статье мы представим вам эмпирический анализ прогностической способности индикатора по паре EURUSD. Кроме того, мы обучим вас методам моделирования, которые вы сможете использовать для улучшения технического анализа с помощью ИИ.
Обзор торговой стратегии
Индикатор MACD в основном используется для определения рыночных тенденций и измерения динамики тренда. Этот индикатор был создан в 1970-х годах покойным Джеральдом Аппелем (Gerrald Appel). Аппель был финансовым менеджером для своих частных клиентов, и его успех был обусловлен его подходом к торговле с помощью технического анализа. Он изобрел индикатор MACD примерно 50 лет назад.
Рис. 1: Джеральд Аппель (Gerald Appel), создатель индикатора MACD
Технические аналитики используют индикатор для определения точек входа и выхода различными способами. На рис. 2 ниже показан скриншот индикатора MACD, примененного к паре GBPUSD с использованием настроек по умолчанию. Индикатор включен в вашу установку MetaTrader 5 по умолчанию. Красная линия, называемая главной линией MACD, рассчитывается как разница между двумя скользящими средними, одной быстрой и другой медленной. Всякий раз, когда главная линия пересекает отметку ниже 0, рынок, скорее всего, находится в нисходящем тренде, и наоборот, когда линия пересекает отметку выше 0.
Аналогичным образом, сама главная линия также может быть использована для определения силы рынка. Только повышение уровня цен приведет к увеличению стоимости главной линии, и, наоборот, снижение уровня цен приведет к падению главной линии. Таким образом, переломные моменты, когда главная линия образует форму, напоминающую чашу, возникают в результате изменения динамики рынка. Вокруг MACD реализованы различные торговые стратегии. На выявление дивергенции MACD направлены более сложные и детально разработанные стратегии.
Дивергенция MACD возникает, когда ценовые уровни оживляются в сильном тренде, прорываясь к новым экстремальным уровням. В то время как, с другой стороны, индикатор MACD находится в тренде, который только ослабевает и начинает падать, что резко контрастирует с сильным движением цены, наблюдаемым на графике. Как правило, дивергенция MACD интерпретируется как раннее предупреждение о развороте тренда, позволяющее трейдерам свернуть свои открытые позиции до того, как рынки станут более волатильными.
Рис. 2: Индикатор MACD с настройками по умолчанию на графике GBPUSD M1
Есть много скептиков, которые ставят под сомнение использование индикатора MACD в целом. Давайте начнем с того, что обратимся к слону в комнате. Все технические индикаторы сгруппированы как запаздывающие индикаторы. Это означает, что технические индикаторы изменяются только после изменения ценовых уровней, они не могут изменяться до изменения ценовых уровней. Макроэкономические показатели, такие как уровень глобальной инфляции, и геополитические новости, такие как начало войны или стихийное бедствие, могут повлиять на уровень спроса и предложения. Они считаются опережающими индикаторами, поскольку могут быстро изменяться до того, как уровни цен отразят это изменение.
Многие трейдеры придерживаются мнения, что эти запаздывающие сигналы, скорее всего, заставят трейдеров открывать свои позиции, когда движение рынка уже исчерпано. Кроме того, обычно наблюдаются развороты тренда, которым не предшествовали расхождения MACD. И точно так же можно наблюдать дивергенцию MACD, за которой не последовало разворота тренда.
Эти факты заставляют нас усомниться в надежности этого показателя и в том, действительно ли он обладает какой-либо прогностической силой, заслуживающей внимания. Мы хотим оценить, возможно ли преодолеть присущий индикатору лаг с помощью ИИ. Если индикатор MACD окажется устойчивым, мы внедрим модель ИИ, которая либо:
- Использует значения индикатора для прогнозирования будущих уровней цен.
- Прогнозирует сам индикатор MACD.
В зависимости от того, какой подход к моделированию дает меньшую погрешность. В противном случае, если наш анализ покажет, что MACD может не обладать прогностической силой в рамках нашей текущей стратегии, мы вместо этого выберем наиболее эффективную модель для прогнозирования уровней цен.
Обзор методологии
Наш анализ начался с создания специального скрипта, написанного на MQL5, для извлечения в CSV-файл ровно 100 000 строк рыночных котировок M1 по паре EURUSD и соответствующих им сигнала MACD и основных значений. Судя по нашим визуализациям данных, индикатор MACD, по-видимому, плохо определяет будущие ценовые уровни. Изменения в уровнях цен, скорее всего, не зависят от значения индикатора, кроме того, расчет индикатора придал данным нелинейную и сложную структуру, которую может быть сложно смоделировать.
Данные, которые мы получили из нашего терминала MetaTrader 5, были разделены на 2 половины. Мы использовали первую половину для оценки точности нашей модели с помощью 5-кратной кросс-валидации. Впоследствии мы создали 3 идентичные модели глубоких нейронных сетей и обучили их на 3 различных подмножествах наших данных:
- Ценовая модель: Прогнозирует уровни цен, используя рыночные котировки OHLC из MetaTrader 5
- Модель MACD: Прогнозирует значения индикатора MACD, используя котировки OHLC и показания MACD
- Полная модель: Прогнозирует уровни цен, используя котировки OHLC и индикатор MACD
Вторая половина раздела была использована для тестирования моделей. Первая модель показала самую высокую точность в тесте - 69%. Наши алгоритмы выбора функций показали, что рыночные котировки, полученные нами из MetaTrader 5, были более информативными, чем значения MACD.
Таким образом, мы начали оптимизировать лучшую модель, которая у нас была, - регрессионную модель, прогнозирующую будущую цену пары EURUSD. Однако мы быстро столкнулись с проблемами, потому что обнаружили шум в наших обучающих данных. К сожалению, нам не удалось превзойти результаты простой линейной регрессии в тестовом наборе. Поэтому мы заменили чрезмерно оптимизированную модель на Метод опорных векторов (SVM).
Впоследствии мы экспортировали нашу SVM-модель в формат ONNX и создали советника, используя комбинированный подход к прогнозированию будущих ценовых уровней EURUSD и индикатор MACD.
Получение нужных нам данных
Чтобы сдвинуть дело с мертвой точки, мы сначала остановились на интегрированной среде разработки (IDE) MetaEditor. Мы создали описанный ниже скрипт для получения наших рыночных данных из терминала MetaTrader 5. Мы запросили 100 000 строк исторических данных M1 и экспортировали их в формат CSV. Приведенный ниже скрипт заполнит наш CSV-файл значениями Time, Open, High, Low, Close и двумя значениями MACD. Просто перетащите скрипт на любую пару, которую вы хотите проанализировать, если хотите следовать за нами.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| Script Inputs | //+------------------------------------------------------------------+ input int size = 100000; //How much data should we fetch? //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int indicator_handler; double indicator_buffer[]; double indicator_buffer_2[]; //+------------------------------------------------------------------+ //| On start function | //+------------------------------------------------------------------+ void OnStart() { //--- Load indicator indicator_handler = iMACD(Symbol(),PERIOD_CURRENT,12,26,9,PRICE_CLOSE); CopyBuffer(indicator_handler,0,0,size,indicator_buffer); CopyBuffer(indicator_handler,1,0,size,indicator_buffer_2); ArraySetAsSeries(indicator_buffer,true); ArraySetAsSeries(indicator_buffer_2,true); //--- File name string file_name = "Market Data " + Symbol() +" MACD " + ".csv"; //--- Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i= size;i>=0;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MACD Main","MACD Signal"); } else { FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i), iOpen(Symbol(),PERIOD_CURRENT,i), iHigh(Symbol(),PERIOD_CURRENT,i), iLow(Symbol(),PERIOD_CURRENT,i), iClose(Symbol(),PERIOD_CURRENT,i), indicator_buffer[i], indicator_buffer_2[i] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Предварительная обработка данных
Теперь, когда мы экспортировали наши данные в формат CSV, давайте ознакомимся с данными в нашем рабочем пространстве Python. Сначала загрузим необходимые нам библиотеки.
#Load libraries import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
Считывание данных.
#Read in the data data = pd.read_csv("Market Data EURUSD MACD .csv")
Определим насколько глубоко в будущее мы должны делать прогнозы.
#Forecast horizon look_ahead = 20
Добавим двоичные цели, чтобы отмечать, если текущее значение больше, чем в предыдущих 20 случаях, как для цены закрытия EURUSD, так и для главной линии MACD.
#Let's add labels data["Bull Bear"] = np.where(data["Close"] < data["Close"].shift(look_ahead),0,1) data["MACD Bull"] = np.where(data["MACD Main"] < data["MACD Main"].shift(look_ahead),0,1) data = data.loc[20:,:] data
Рис. 3: Некоторые из столбцов в нашем фрейме данных
Кроме того, необходимо определить наши целевые показатели.
data["MACD Target"] = data["MACD Main"].shift(-look_ahead) data["Price Target"] = data["Close"].shift(-look_ahead) data["MACD Binary Target"] = np.where(data["MACD Main"] < data["MACD Target"],1,0) data["Price Binary Target"] = np.where(data["Close"] < data["Price Target"],1,0) data = data.iloc[:-20,:]
Разведочный анализ данных
Диаграммы рассеяния помогают нам визуализировать взаимосвязь между зависимой и независимой переменными. Приведенный ниже график показывает, что между будущими уровнями цен и текущим значением MACD определенно существует взаимосвязь, проблема в том, что эта взаимосвязь нелинейна и, по-видимому, имеет сложную структуру. Не сразу становится очевидным, какие изменения в индикаторе MACD приводят к бычьему или медвежьему движению цены.
sns.scatterplot(data=data,x="MACD Main",y="MACD Signal",hue="Price Binary Target")
Рис. 4: Визуализация взаимосвязи между индикатором MACD и ценовыми уровнями
Выполнение 3D-графика только еще раз демонстрирует, насколько запутанной на самом деле является эта взаимосвязь. Здесь нет четких границ, поэтому мы ожидаем, что данные будет сложно классифицировать. Единственный разумный вывод, который мы можем сделать из нашего графика, заключается в том, что рынки, по-видимому, быстро возвращаются к центру после прохождения крайних уровней на MACD.
#Define the 3D Plot fig = plt.figure(figsize=(7,7)) ax = plt.axes(projection="3d") ax.scatter(data["MACD Main"],data["MACD Signal"],data["Close"],c=data["Price Binary Target"]) ax.set_xlabel("MACD Main") ax.set_ylabel("MACD Signal") ax.set_zlabel("EURUSD Close")
Рис. 5: Визуализация взаимодействия между индикатором MACD и рынком EURUSD
Графики Violin позволяют нам одновременно визуализировать распределение данных и сравнивать два распределения. Синяя линия представляет собой сводную информацию о наблюдаемом распределении будущих ценовых уровней после роста или падения MACD. На рисунке 6 ниже нам надо было понять, связан ли рост или падение индикатора MACD с различными распределениями относительно будущих ценовых движений. Как видно, эти два распределения выглядят практически идентичными. Кроме того, ядро каждого распределения имеет блочную диаграмму. Средние значения на обоих блочных диаграммах выглядят почти одинаково, независимо от того, находился ли индикатор в бычьем или медвежьем состоянии.
sns.violinplot(data=data,x="MACD Bull",y="Close",hue="Price Binary Target",split=True,fill=False)
Рис. 6: Визуализация влияния индикатора MACD на будущие уровни цен
Подготовка к моделированию данных
Давайте теперь приступим к моделированию наших данных, в первую очередь нам нужно импортировать наши библиотеки.
#Perform train test splits from sklearn.model_selection import train_test_split,TimeSeriesSplit from sklearn.metrics import accuracy_score train,test = train_test_split(data,test_size=0.5,shuffle=False)
Теперь определим предикторы и цель.
#Let's scale the data ohlc_predictors = ["Open","High","Low","Close","Bull Bear"] macd_predictors = ["MACD Main","MACD Signal","MACD Bull"] all_predictors = ohlc_predictors + macd_predictors cv_predictors = [ohlc_predictors,macd_predictors,all_predictors] #Define the targets cv_targets = ["MACD Binary Target","Price Binary Target","All"]
Масштабирование данных.
#Scaling the data
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(train[all_predictors])
train_scaled = pd.DataFrame(scaler.transform(train[all_predictors]),columns=all_predictors)
test_scaled = pd.DataFrame(scaler.transform(test[all_predictors]),columns=all_predictors)
Загрузим необходимые нам библиотеки.
#Import the models we will evaluate
from sklearn.neural_network import MLPClassifier,MLPRegressor
from sklearn.linear_model import LinearRegression
Создаём объект разделения временного ряда.
tscv = TimeSeriesSplit(n_splits=5,gap=look_ahead)
Индексы нашего фрейма данных будут сопоставлены с набором входных данных, которые мы оценивали.
err_indexes = ["MACD Train","Price Train","All Train","MACD Test","Price Test","All Test"]
Теперь создадим фрейм данных, который будет регистрировать наши оценки точности модели по мере изменения наших входных данных.
#Now let us define a table to store our error levels columns = ["Model Accuracy"] cv_err = pd.DataFrame(columns=columns,index=err_indexes)
Сбросим все наши индексы.
#Reset index
train = train.reset_index(drop=True)
test = test.reset_index(drop=True)
Выполним кросс-валидацию модели. Мы проведем кросс-валидацию модели на обучающем наборе, а затем зафиксируем ее точность на тестовом наборе, не применяя ее к тестовому набору.
#Initailize the model price_model = MLPClassifier(hidden_layer_sizes=(10,6)) macd_model = MLPClassifier(hidden_layer_sizes=(10,6)) all_model = MLPClassifier(hidden_layer_sizes=(10,6)) price_acc = [] macd_acc = [] all_acc = [] #Cross validate each model twice for j,(train_index,test_index) in enumerate(tscv.split(train_scaled)): #Fit the models price_model.fit(train_scaled.loc[train_index,ohlc_predictors],train.loc[train_index,"Price Binary Target"]) macd_model.fit(train_scaled.loc[train_index,all_predictors],train.loc[train_index,"MACD Binary Target"]) all_model.fit(train_scaled.loc[train_index,all_predictors],train.loc[train_index,"Price Binary Target"]) #Store the accuracy price_acc.append(accuracy_score(train.loc[test_index,"Price Binary Target"],price_model.predict(train_scaled.loc[test_index,ohlc_predictors]))) macd_acc.append(accuracy_score(train.loc[test_index,cv_targets[0]],macd_model.predict(train_scaled.loc[test_index,all_predictors]))) all_acc.append(accuracy_score(train.loc[test_index,cv_targets[1]],all_model.predict(train_scaled.loc[test_index,all_predictors]))) #Now we can store our estimates of the model's error cv_err.iloc[0,0] = np.mean(price_acc) cv_err.iloc[1,0] = np.mean(macd_acc) cv_err.iloc[2,0] = np.mean(all_acc) #Estimating test error cv_err.iloc[3,0] = accuracy_score(test[cv_targets[1]],price_model.predict(test_scaled[ohlc_predictors])) cv_err.iloc[4,0] = accuracy_score(test[cv_targets[0]],macd_model.predict(test_scaled[all_predictors])) cv_err.iloc[5,0] = accuracy_score(test[cv_targets[1]],all_model.predict(test_scaled[all_predictors]))
Группа входных данных | Точность модели |
---|---|
MACD Train | 0.507129 |
OHLC Train | 0.690267 |
All Train | 0.504577 |
MACD Test | 0.48669 |
OHLC Test | 0.684069 |
All Test | 0.487442 |
Значимость признаков
Теперь попробуем оценить уровни важности функций для нашей глубокой нейронной сети. Мы выберем важность перестановок для интерпретации нашей модели. Важность перестановки определяет важность каждого входного параметра, сначала путем перетасовки значений этого столбца входных данных, а затем оценивая изменения в точности модели. Идея заключается в том, что важные характеристики приведут к значительному снижению погрешности, в то время как несущественные характеристики приведут к изменениям точности модели, близким к 0.
Однако необходимо принять во внимание некоторые соображения. Во-первых, алгоритм важности перестановки случайным образом перетасовывает все входные данные модели. Это означает, что алгоритм может случайным образом перетасовать цену открытия и установить ее выше максимальной цены. Очевидно, что в реальном мире это невозможно. Поэтому мы должны интерпретировать результаты работы алгоритма с осторожностью. Можно было бы сказать, что алгоритм предвзят, поскольку он оценивает важность признаков в моделируемых условиях, которые потенциально могут никогда не произойти, без необходимости наказывая модель. Кроме того, из-за стохастической природы алгоритмов оптимизации, используемых для адаптации к современным нейронным сетям, обучение одних и тех же нейронных сетей на одном и том же наборе данных может каждый раз давать совершенно разные объяснения.
#Let us try assess feature importance from sklearn.inspection import permutation_importance from sklearn.linear_model import RidgeClassifier
Теперь мы поместим наш объект важности перестановки в нашу обученную модель глубокой нейронной сети. У нас есть выбор: передать обучающие или тестовые данные для перетасовки. Мы выбрали тестовые данные. После этого мы упорядочили данные в порядке снижения точности и нанесли результаты на график. На рисунке 7 ниже показаны наблюдаемые показатели важности перестановок. Мы видим, что результаты перетасовки связанных с MACD входных данных оказываются очень близкими к 0, что означает, что столбцы MACD не так важны для нашей модели.
#Let us fit the model model = MLPClassifier(hidden_layer_sizes=(10,6)) model.fit(train_scaled.loc[:,all_predictors],train.loc[:,"Price Binary Target"]) #Calculate permutation importance scores pi = permutation_importance( model, test_scaled.loc[:,all_predictors], test.loc[:,"Price Binary Target"], n_repeats=10, random_state=42, n_jobs=-1 ) #Sort the importance scores sorted_importances_idx = pi.importances_mean.argsort() importances = pd.DataFrame( pi.importances[sorted_importances_idx].T, columns=test_scaled.columns[sorted_importances_idx], ) #Create the plot ax = importances.plot.box(vert=False, whis=10) ax.set_title("Permutation Importances (test set)") ax.axvline(x=0, color="k", linestyle="--") ax.set_xlabel("Decrease in accuracy score") ax.figure.tight_layout()
Рис. 7: В наших оценках важности перестановок цена закрытия была названа наиболее важным признаком
Обучение более простой модели также могло бы дать нам представление об уровнях важности исходных данных. Ridge classifier - это линейная модель, которая толкает свои коэффициенты всё ближе к 0 в направлении, минимизирующем ее погрешность. Следовательно, если предположить, что ваши данные были стандартизированы и масштабированы, неважные признаки будут иметь наименьшие коэффициенты искажения. Если вам интересно, ridge classifier может достичь этого, расширив обычную линейную модель и включив в нее штрафной член, пропорциональный сумме квадратов коэффициентов модели. Это явление широко известно как L2-регуляризация.
#Let us fit the model model = RidgeClassifier() model.fit(train_scaled.loc[:,all_predictors],train.loc[:,"Price Binary Target"])
Теперь построим график коэффициентов модели.
ridge_importance = pd.DataFrame(model.coef_.tolist(),columns=all_predictors) #Prepare the plot fig,ax = plt.subplots(figsize=(10,5)) sns.barplot(ridge_importance,ax=ax)
Рис. 8: Наши гребневые коэффициенты подсказывают нам, что высокая и низкая цена - это самые информативные признаки, которые у нас есть
Настройка параметров
Теперь попытаемся оптимизировать нашу наиболее эффективную модель. Однако, как мы уже говорили ранее, наша процедура оптимизации на этом этапе не увенчалась успехом. К сожалению, это заложено в природе алгоритмов оптимизации, и мы не гарантируем, что найдем решения. Выполнение оптимизации параметров не обязательно означает, что модель, которую вы получите в итоге, будет лучше. Мы всего лишь пытаемся приблизить оптимальные параметры модели. Загрузим необходимые нам библиотеки.
#Let's tune our model further from sklearn.model_selection import RandomizedSearchCV
Определение модели.
#Reinitialize the model model = MLPRegressor(max_iter=200)
Теперь мы определим объект tuner. Объект оценит нашу модель при различных параметрах инициализации и вернет объект, содержащий найденные наиболее эффективные входные данные.
#Define the tuner tuner = RandomizedSearchCV( model, { "activation" : ["relu","logistic","tanh","identity"], "solver":["adam","sgd","lbfgs"], "alpha":[0.1,0.01,0.001,0.0001,0.00001,0.00001,0.0000001], "tol":[0.1,0.01,0.001,0.0001,0.00001,0.000001,0.0000001], "learning_rate":['constant','adaptive','invscaling'], "learning_rate_init":[0.1,0.01,0.001,0.0001,0.00001,0.000001,0.0000001], "hidden_layer_sizes":[(2,4,8,2),(10,20),(5,10),(2,20),(6,8,10),(1,5),(20,10),(8,4),(2,4,8),(10,5)], "early_stopping":[True,False], "warm_start":[True,False], "shuffle": [True,False] }, n_iter=100, cv=5, n_jobs=-1, scoring="neg_mean_squared_error" )
Обучение объекта tuner.
tuner.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"])
Лучшие параметры, обнаруженные нами.
tuner.best_params_
'tol': 0.01,
'solver': 'sgd',
'shuffle': False,
'learning_rate_init': 0.01,
'learning_rate': 'constant',
'hidden_layer_sizes': (20, 10),
'early_stopping': True,
'alpha': 1e-07,
'activation': 'identity'}
Более глубокая оптимизация
Мы можем провести еще более глубокий поиск лучших настроек ввода, используя библиотеку SciPy. Мы будем использовать библиотеку для оценки результатов глобальной оптимизации по непрерывным параметрам модели.#Deeper optimization from scipy.optimize import minimize from sklearn.metrics import mean_squared_error from sklearn.model_selection import TimeSeriesSplit
Определим объект разделения временного ряда.
#Define the time series split object tscv = TimeSeriesSplit(n_splits=5,gap=look_ahead)
Создадим структуры данных для хранения наших уровней точности.
#Create a dataframe to store our accuracy current_error_rate = pd.DataFrame(index = np.arange(0,5),columns=["Current Error"]) algorithm_progress = []
Нашей функцией стоимости, которую необходимо минимизировать, будет уровень ошибок модели по обучающим данным.
#Define the objective function def objective(x): #The parameter x represents a new value for our neural network's settings model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=500, activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=x[0], tol=x[1], learning_rate_init=x[2] ) #Now we will cross validate the model for i,(train_index,test_index) in enumerate(tscv.split(train)): #Train the model model.fit(train.loc[train_index,ohlc_predictors],train.loc[train_index,"Price Target"]) #Measure the RMSE current_error_rate.iloc[i,0] = mean_squared_error(train.loc[test_index,"Price Target"],model.predict(train.loc[test_index,ohlc_predictors])) #Store the algorithm's progress algorithm_progress.append(current_error_rate.iloc[:,0].mean()) #Return the Mean CV RMSE return(current_error_rate.iloc[:,0].mean())
SciPy ожидает, что мы предоставим ему начальные значения для запуска процедуры оптимизации.
#Define the starting point pt = [tuner.best_params_["alpha"],tuner.best_params_["tol"],tuner.best_params_["learning_rate_init"]] bnds = ((10.00 ** -100,10.00 ** 100), (10.00 ** -100,10.00 ** 100), (10.00 ** -100,10.00 ** 100))
Теперь попробуем оптимизировать модель.
#Searching deeper for parameters result = minimize(objective,pt,method="L-BFGS-B",bounds=bnds)
Похоже, что алгоритму удалось сблизиться. Это означает, что он находил стабильные входные данные с небольшой дисперсией. Таким образом, он пришел к выводу, что лучших решений не существует, поскольку изменения в уровнях ошибок приближались к 0.
#The result of our optimization
result
success: True
status: 0
fun: 3.730365831424036e-06
x: [ 9.939e-08 9.999e-03 9.999e-03]
nit: 3
jac: [-7.896e+01 -1.133e+02 1.439e+03]
nfev: 100
njev: 25
hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
Визуализируем процедуру.
#Store the optimal coefficients optimal_weights = result.x optima_y = min(algorithm_progress) optima_x = algorithm_progress.index(optima_y) inputs = np.arange(0,len(algorithm_progress)) #Plot the performance of our optimization procedure plt.scatter(inputs,algorithm_progress) plt.plot(optima_x,optima_y,'ro',color='r') plt.axvline(x=optima_x,ls='--',color='red') plt.axhline(y=optima_y,ls='--',color='red') plt.xlabel("Iterations") plt.ylabel("Training MSE") plt.title("Minimizing Training Error")
Рис. 9: Визуализация оптимизации глубокой нейронной сети
Тестирование на переобучение
Переобучение - это нежелательный эффект, при котором наша модель обучается бессмысленным представлениям из имеющихся у нас данных. Это нежелательно, поскольку модель в таком состоянии будет отображать низкие уровни точности. Мы можем определить, переобучена ли наша модель, сравнив ее с более слабыми «учениками» и экземплярами по умолчанию аналогичной нейронной сети. Если наша модель изучает шум и не может уловить сигнал в данных, то более слабые «ученики» превзойдут ее. Однако, даже если наша модель превзойдет результаты более слабых «учеников», все равно существует возможность её переобучения.
#Testing for overfitting #Benchmark benchmark = LinearRegression() #Default default_nn = MLPRegressor(max_iter=500) #Randomized NN random_search_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=500, activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=tuner.best_params_["alpha"], tol=tuner.best_params_["tol"], learning_rate_init=tuner.best_params_["learning_rate_init"] ) #LBFGS NN lbfgs_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=500, activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=result.x[0], tol=result.x[1], learning_rate_init=result.x[2] )
Обучаем модели и оцениваем их точность. Мы отчетливо видим разницу в эффективности, модель линейной регрессии превзошла все наши глубокие нейронные сети. Затем я решил попробовать вместо этого обучить линейную SVM. Она работала лучше, чем нейронные сети, но не смогла превзойти линейную регрессию.
#Fit the models on the training sets benchmark = LinearRegression() benchmark.fit(((train.loc[:,ohlc_predictors])),train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],benchmark.predict(((test.loc[:,ohlc_predictors])))) #Test the default default_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],default_nn.predict(test.loc[:,ohlc_predictors])) #Test the random search random_search_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],random_search_nn.predict(test.loc[:,ohlc_predictors])) #Test the lbfgs nn lbfgs_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],lbfgs_nn.predict(test.loc[:,ohlc_predictors])
Линейная регрессия | Дефолтная НС | Случайный поиск | LBFGS НС |
---|---|---|---|
2.609826e-07 | 1.996431e-05 | 0.00051 | 0.000398 |
Давайте обучим нашу LinearSVR, она с большей вероятностью выявит нелинейные взаимодействия в наших данных.
#From experience, I'll try LSVR from sklearn.svm import LinearSVR
Инициализируем модель и подгоняем ее под все имеющиеся у нас данные. Обратите внимание, что уровни ошибок SVR лучше, чем у нейронной сети, но не так хороши, как у линейной регрессии.
#Initialize the model lsvr = LinearSVR() #Fit the Linear Support Vector lsvr.fit(train.loc[:,["Open","High","Low","Close"]],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],lsvr.predict(test.loc[:,["Open","High","Low","Close"]]))
Экспортирование в ONNX
Open Neural Network Exchange (ONNX) позволяет нам создавать модели машинного обучения на одном языке, а затем передавать их на любой другой язык, поддерживающий ONNX API. Протокол ONNX быстро расширяет число сред, в которых можно использовать машинное обучение. ONNX позволяет нам легко интегрировать ИИ в наш советник на MQL5.
#Let's export the LSVR to ONNX import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Создаем новый экземпляр модели.
model = LinearSVR()
Подгоним модель по всем имеющимся у нас данным.
model.fit(data.loc[:,["Open","High","Low","Close"]],data.loc[:,"Price Target"])
Определим входную форму модели.
#Define the input type initial_types = [("float_input",FloatTensorType([1,4]))]
Создадим ONNX-представление модели.
#Create the ONNX representation onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=12)
Сохраним модель ONNX.
# Save the ONNX model onnx.save_model(onnx_model,"EURUSD SVR M1.onnx")
Рис. 10: Визуализация нашей ONNX-модели
Реализация средствами MQL5
Теперь мы можем приступить к реализации нашей стратегии на MQL5. Нам надо создать приложение, которое покупает всякий раз, когда цена будет выше скользящей средней, и искусственный интеллект спрогнозирует, что цены будут расти.
Чтобы начать работу с нашим приложением, мы сначала включим только что созданный файл ONNX в наш советник.
//+--------------------------------------------------------------+ //| EURUSD AI | //+--------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://metaquotes.com/en/users/gamuchiraindawa" #property version "2.1" #property description "Supports M1" //+--------------------------------------------------------------+ //| Resources we need | //+--------------------------------------------------------------+ #resource "\\Files\\EURUSD SVR M1.onnx" as const uchar onnx_buffer[];
Теперь загрузим торговую библиотеку.
//+--------------------------------------------------------------+ //| Libraries | //+--------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade trade;
Определим несколько констант, которые теперь будут изменены.
//+--------------------------------------------------------------+ //| Constants | //+--------------------------------------------------------------+ const double stop_percent = 1; const int ma_period_shift = 0;
Позволим пользователю контролировать параметры технических индикаторов и общее поведение программы.
//+--------------------------------------------------------------+ //| User inputs | //+--------------------------------------------------------------+ input group "TAs" input double atr_multiple =2.5; //How wide should the stop loss be? input int atr_period = 200; //ATR Period input int ma_period = 1000; //Moving average period input group "Risk" input double risk_percentage= 0.02; //Risk percentage (0.01 - 1) input double profit_target = 1.0; //Profit target
Теперь определим все необходимые нам глобальные переменные.
//+--------------------------------------------------------------+ //| Global variables | //+--------------------------------------------------------------+ double position_size = 2; int lot_multiplier = 1; bool buy_break_even_setup = false; bool sell_break_even_setup = false; double up_level = 0.03; double down_level = -0.03; double min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,ask, bid,atr_stop,mid_point,risk_equity; double take_profit = 0; double close_price[3]; double moving_average_low_array[],close_average_reading[],moving_average_high_array[],atr_reading[]; long min_distance,login; int ma_high,ma_low,atr,close_average; bool authorized = false; double tick_value,average_market_move,margin,mid_point_height,channel_width,lot_step; string currency,server; bool all_closed =true; long onnx_model; vectorf onnx_output = vectorf::Zeros(1); ENUM_ACCOUNT_TRADE_MODE account_type;
Наш советник сначала проверит, разрешил ли пользователь советникам торговать на счете, затем попытается загрузить модель ONNX, и, наконец, в случае успеха, загрузим наши технические индикаторы.
//+------------------------------------------------------------------+ //| On initialization | //+------------------------------------------------------------------+ int OnInit() { //--- Authorization if(!auth()) { return(INIT_FAILED); } //--- Load the ONNX model if(!load_onnx()) { return(INIT_FAILED); } //--- Everything went fine else { load(); return(INIT_SUCCEEDED); } }
Если наш советник не используется, мы освободим память, выделенную для модели ONNX.
//+------------------------------------------------------------------+ //| On deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { OnnxRelease(onnx_model); }Всякий раз при получении обновленных данных о ценах, мы обновляем наши глобальные рыночные переменные, а затем проверяем наличие торговых сигналов если у нас нет открытых позиций. В противном случае мы обновим наш трейлинг-стоп-лосс.
//+------------------------------------------------------------------+ //| On every tick | //+------------------------------------------------------------------+ void OnTick() { //On Every Function Call update(); static datetime time_stamp; datetime time = iTime(_Symbol,PERIOD_CURRENT,0); Comment("AI Forecast: ",onnx_output[0]); //On Every Candle if(time_stamp != time) { //Mark the candle time_stamp = time; OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin); calculate_lot_size(); if(PositionsTotal() == 0) { check_signal(); } } //--- If we have positions, manage them. if(PositionsTotal() > 0) { check_atr_stop(); check_profit(); } } //+------------------------------------------------------------------+ //| Check if we have any valid setups, and execute them | //+------------------------------------------------------------------+ void check_signal(void) { //--- Get a prediction from our model model_predict(); if(onnx_output[0] > iClose(Symbol(),PERIOD_CURRENT,0)) { if(above_channel()) { check_buy(); } } else if(below_channel()) { if(onnx_output[0] < iClose(Symbol(),PERIOD_CURRENT,0)) { check_sell(); } } }
Эта функция отвечает за обновление всех наших глобальных рыночных переменных.
//+------------------------------------------------------------------+ //| Update our global variables | //+------------------------------------------------------------------+ void update(void) { //--- Important details that need to be updated everytick ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); buy_stop_loss = 0; sell_stop_loss = 0; check_price(3); CopyBuffer(ma_high,0,0,1,moving_average_high_array); CopyBuffer(ma_low,0,0,1,moving_average_low_array); CopyBuffer(atr,0,0,1,atr_reading); ArraySetAsSeries(moving_average_high_array,true); ArraySetAsSeries(moving_average_low_array,true); ArraySetAsSeries(atr_reading,true); risk_equity = AccountInfoDouble(ACCOUNT_BALANCE) * risk_percentage; atr_stop = (((min_distance + (atr_reading[0]* 1e5) * atr_multiple) * _Point)); mid_point = (moving_average_high_array[0] + moving_average_low_array[0]) / 2; mid_point_height = close_price[0] - mid_point; channel_width = moving_average_high_array[0] - moving_average_low_array[0]; }
Теперь мы должны определить функцию, которая обеспечит разрешение на запуск нашего приложения. Если оно не будет запущено, функция выдаст пользователю инструкции о том, что делать, и вернет значение false, которое остановит инициализацию.
//+------------------------------------------------------------------+ //| Check if the EA is allowed to be run | //+------------------------------------------------------------------+ bool auth(void) { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program"); return(false); } else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading"); return(false); } return(true); }
Во время инициализации нам нужна функция, отвечающая за загрузку всех наших технических индикаторов и получение важной информации о рынке. Функция load сделает именно это за нас, и поскольку она ссылается на глобальные переменные, ее возвращаемый тип будет void.
//+---------------------------------------------------------------------+ //| Load our needed variables | //+---------------------------------------------------------------------+ void load(void) { //Account Info currency = AccountInfoString(ACCOUNT_CURRENCY); server = AccountInfoString(ACCOUNT_SERVER); login = AccountInfoInteger(ACCOUNT_LOGIN); //Indicators atr = iATR(_Symbol,PERIOD_CURRENT,atr_period); ma_high = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_HIGH); ma_low = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_LOW); //Market Information min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); tick_value = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT) * min_volume; lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); average_market_move = NormalizeDouble(10000 * tick_value,_Digits); }
С другой стороны, наша модель ONNX будет загружена с помощью отдельного вызова функции. Функция создаст нашу ONNX-модель из буфера, который мы определили ранее, и проверит форму ввода и вывода.
//+------------------------------------------------------------------+ //| Load our ONNX model | //+------------------------------------------------------------------+ bool load_onnx(void) { onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT); ulong onnx_input [] = {1,4}; ulong onnx_output[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,onnx_input)) { Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA."); return(false); } if(!OnnxSetOutputShape(onnx_model,0,onnx_output)) { Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA."); return(false); } return(true); }
Давайте теперь определим функцию, которая будет получать прогнозы из нашей модели.
//+------------------------------------------------------------------+ //| Get a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { vectorf onnx_inputs = {iOpen(Symbol(),PERIOD_CURRENT,0),iHigh(Symbol(),PERIOD_CURRENT,0),iLow(Symbol(),PERIOD_CURRENT,0),iClose(Symbol(),PERIOD_CURRENT,0)}; OnnxRun(onnx_model,ONNX_DEFAULT,onnx_inputs,onnx_output); }
Наш стоп-лосс будет скорректирован на значение ATR. В зависимости от того, является ли текущая сделка сделкой на покупку или продажу, это является основным определяющим фактором, помогающим нам определить, следует ли нам увеличить наш стоп-лосс, добавив текущее значение ATR, или уменьшить, вычитая текущее значение ATR. Мы также можем использовать значение, кратное текущему значению ATR, чтобы предоставить пользователю более точный контроль над уровнем риска.
//+------------------------------------------------------------------+ //| Update the ATR stop loss | //+------------------------------------------------------------------+ void check_atr_stop() { for(int i = PositionsTotal() -1; i >= 0; i--) { string symbol = PositionGetSymbol(i); if(_Symbol == symbol) { ulong ticket = PositionGetInteger(POSITION_TICKET); double position_price = PositionGetDouble(POSITION_PRICE_OPEN); double type = PositionGetInteger(POSITION_TYPE); double current_stop_loss = PositionGetDouble(POSITION_SL); if(type == POSITION_TYPE_BUY) { double atr_stop_loss = (ask - (atr_stop)); double atr_take_profit = (ask + (atr_stop)); if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)) { trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } else if(type == POSITION_TYPE_SELL) { double atr_stop_loss = (bid + (atr_stop)); double atr_take_profit = (bid - (atr_stop)); if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)) { trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } } } }
Наконец, нам нужно определить 2 функции, отвечающие за открытие позиций на покупку и продажу, а также их дополнительные пары для закрытия позиции.
//+------------------------------------------------------------------+ //| Open buy positions | //+------------------------------------------------------------------+ void check_buy() { if(PositionsTotal() == 0) { for(int i=0; i < position_size;i++) { trade.Buy(min_volume * lot_multiplier,_Symbol,ask,buy_stop_loss,0,"BUY"); Print("Position: ",i," has been setup"); } } } //+------------------------------------------------------------------+ //| Open sell positions | //+------------------------------------------------------------------+ void check_sell() { if(PositionsTotal() == 0) { for(int i=0; i < position_size;i++) { trade.Sell(min_volume * lot_multiplier,_Symbol,bid,sell_stop_loss,0,"SELL"); Print("Position: ",i," has been setup"); } } } //+------------------------------------------------------------------+ //| Close all buy positions | //+------------------------------------------------------------------+ void close_buy() { ulong ticket; int type; if(PositionsTotal() > 0) { for(int i = 0; i < PositionsTotal();i++) { if(PositionGetSymbol(i) == _Symbol) { ticket = PositionGetTicket(i); type = (int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_BUY) { trade.PositionClose(ticket); } } } } } //+------------------------------------------------------------------+ //| Close all sell positions | //+------------------------------------------------------------------+ void close_sell() { ulong ticket; int type; if(PositionsTotal() > 0) { for(int i = 0; i < PositionsTotal();i++) { if(PositionGetSymbol(i) == _Symbol) { ticket = PositionGetTicket(i); type = (int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_SELL) { trade.PositionClose(ticket); } } } } }
Проследим за последними 3 ценовыми уровнями.
//+------------------------------------------------------------------+ //| Get the last 3 quotes | //+------------------------------------------------------------------+ void check_price(int candles) { for(int i = 0; i < candles;i++) { close_price[i] = iClose(_Symbol,PERIOD_CURRENT,i); } }
Эта логическая проверка вернет значение true, если мы окажемся выше скользящей средней.
//+------------------------------------------------------------------+ //| Are we completely above the MA? | //+------------------------------------------------------------------+ bool above_channel() { return (((close_price[0] - moving_average_high_array[0] > 0)) && ((close_price[0] - moving_average_low_array[0]) > 0)); }
Проверим, не находимся ли мы ниже скользящей средней.
//+------------------------------------------------------------------+ //| Are we completely below the MA? | //+------------------------------------------------------------------+ bool below_channel() { return(((close_price[0] - moving_average_high_array[0]) < 0) && ((close_price[0] - moving_average_low_array[0]) < 0)); }
Закроем все имеющиеся у нас позиции.
//+------------------------------------------------------------------+ //| Close all positions we have | //+------------------------------------------------------------------+ void close_all() { if(PositionsTotal() > 0) { ulong ticket; for(int i =0;i < PositionsTotal();i++) { ticket = PositionGetTicket(i); trade.PositionClose(ticket); } } }
Рассчитаем оптимальный размер лота для использования таким образом, чтобы наша маржа была равна сумме капитала, которым мы готовы рискнуть.
//+------------------------------------------------------------------+ //| Calculate the lot size to be used | //+------------------------------------------------------------------+ void calculate_lot_size() { //--- This is the total percentage of the account we're willing to part with for margin, or to keep a position open in other words. Print("Risk Equity: ",risk_equity); //--- Now that we're ready to part with a discrete amount for margin, how many positions can we afford under the current lot size? //--- By default we always start from minimum lot position_size = risk_equity / margin; //--- We need to keep the number of positions lower than 10 if(position_size > 10) { //--- How many times is it greater than 10? int estimated_lot_size = (int) MathFloor(position_size / 10); position_size = risk_equity / (margin * estimated_lot_size); Print("Position Size After Dividing By margin at new estimated lot size: ",position_size); int estimated_position_size = position_size; //--- Can we increase the lot size this many times? if(estimated_lot_size < max_volume_increase) { Print("Est Lot Size: ",estimated_lot_size," Position Size: ",estimated_position_size); lot_multiplier = estimated_lot_size; position_size = estimated_position_size; } } }
Закроем открытые позиции и проверим, сможем ли мы снова торговать.
//--- This function will help us keep track of which side we need to enter the market void close_all_and_enter() { if(PositionSelect(Symbol())) { // Determine the type of position check_signal(); } else { Print("No open position found."); } }
Если мы достигли нашей цели по прибыли, закроем все позиции, которые у нас есть, чтобы получить прибыль, а затем проверим, сможем ли мы снова войти.
//+------------------------------------------------------------------+ //| Chekc if we have reached our profit target | //+------------------------------------------------------------------+ void check_profit() { double current_profit = (AccountInfoDouble(ACCOUNT_EQUITY) - AccountInfoDouble(ACCOUNT_BALANCE)) / PositionsTotal(); if(current_profit > profit_target) { close_all_and_enter(); } if((current_profit * PositionsTotal()) < (risk_equity * -1)) { Comment("We've breached our risk equity, consider closing all positions"); } }
Наконец, нам нужна функция, которая закроет все наши убыточные сделки.
//+------------------------------------------------------------------+ //| Close all losing trades | //+------------------------------------------------------------------+ void close_profitable_trades() { for(int i=PositionsTotal()-1; i>=0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetDouble(POSITION_PROFIT)>profit_target) { ulong ticket; ticket = PositionGetTicket(i); trade.PositionClose(ticket); } } } } //+------------------------------------------------------------------+
Рис. 11: Наш советник
Рис. 12: Параметры, которые мы используем для тестирования приложения
Рис. 13: Наше приложение в действии
Заключение
Несмотря на то, что наши результаты не были обнадеживающими, они далеки от окончательных. Существуют и другие способы интерпретации индикатора MACD, которые, возможно, заслуживают оценки. Например, во время бычьего тренда сигнальная линия MACD пересекается выше главной линии, а при медвежьем тренде опускается ниже главной линии. Просмотр индикатора с этой точки зрения может привести к различным показателям ошибок. Мы не можем просто предполагать, что все стратегии интерпретации MACD будут давать одинаковые уровни ошибок. Прежде чем мы сможем составить мнение об эффективности индикатора, было бы разумно оценить эффективность различных стратегий, основанных на MACD.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16066
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования