
Переосмысливаем классические стратегии (Часть II): Пробои индикатора Bollinger Bands
Введение
Индикатор Bollinger Bands («Полосы Боллинджера») — универсальный инструмент в торговых стратегиях, эффективный как для следования за трендом, так и для обнаружения потенциальных точек разворота. Технически индикатор состоит из экспоненциальной скользящей средней (EMA), которая сглаживает цену закрытия ценной бумаги. Эта центральная линия заключена между двумя дополнительными линиями, расположенными выше и ниже EMA, как правило, на 2 стандартных отклонения.
В этой статье постараемся эмпирически проанализировать преимущества стратегии с самого начала. Наша цель — помочь читателям, которые рассматривают использование полос Боллинджера, решить, подходит ли им эта стратегия. Кроме того, покажем, как можно использовать технические индикаторы для управления ИИ-моделями и, надеемся, для разработки более стабильных торговых стратегий.
Добились мы этого, обучив две равноценные ИИ-модели с помощью алгоритма линейного дискриминантного анализа и сравнив их друг с другом посредством перекрестной проверки временных рядов, опираясь при тестировании исключительно на библиотеку scikit-learn. Первую модель обучили просто предсказывать подъем и спад цены, а вторая научилась прогнозировать, как цена будет перемещаться между четырьмя зонами, обозначенными полосами Боллинджера. К разочарованию поклонников индикатора Bollinger Bands, эмпирические наблюдения привели нас к выводу, что непосредственный прогноз цены может быть эффективнее прогнозирования перехода между созданными полосами Боллинджера четырьмя зонами. Однако стоит отметить, что для настройки параметров индикатора не применялись методы оптимизации.
Цель этой статьи — продемонстрировать:
- Как можно сравнить две возможные торговые стратегии аналитически.
- Как с нуля реализовать линейный дискриминантный анализ на MQL5.
- Как создать стабильные торговые стратегии с применением ИИ.
Обзор стратегии и наших соображений
Термин «искусственный интеллект» (ИИ) небезосновательно считают одним из самых дезориентирующих в истории с точки зрения выбора названия. После прочтения этой статьи вы, вероятно, согласитесь, что «ИИ» — неправильное название. Как для автора, для меня проблема — в слове «интеллект». ИИ-модели не обладают интеллектом в человеческом смысле слова. Вместо этого они представляют собой интеллектуальные приложения алгоритмов оптимизации.
ИИ-модели в первую очередь предназначены для сведения к минимуму ошибок или получения максимальной прибыли в рамках системы. Однако решения на основании этих моделей не всегда практичны. Например, ИИ-система для минимизации потерь на торговом счете может прийти к выводу, что отказ от размещения сделок будет наилучшим решением, поскольку гарантирует отсутствие потерь. Хотя такое решение математически удовлетворяет рассматриваемой задаче, оно не имеет практического применения для торговли.
Как интеллектуальные специалисты по ИИ, мы должны обеспечивать тщательно спланированные ограничения для моделей. В рамках этой статьи будем управлять ИИ-моделями с помощью полос Боллинджера. Выделим четыре возможные зоны, в которых в любой момент времени может оказаться цена. Обратите внимание, что в конкретный момент цена может находиться только в одной из этих четырёх зон:
- Зона 1. Цена полностью находится над полосами Боллинджера
- Зона 2. Цена находится между средней и верхней полосами
- Зона 3. Цена находится между нижней и средней полосами
- Зона 4. Цена находится ниже нижней полосы
Научим модель понимать, как происходят перемещения цены между этими четырьмя зонами, и прогнозировать следующую зону, в которую переместится цена. Генерация торговых сигналов происходит при каждом переходе цены из одной зоны в другую. Например, если по прогнозу модели цена переместится из Зоны 2 в Зону 1, интерпретируем это как восходящее движение и инициируем ордер на покупку. Наши модель и советник будут полностью реализованы на встроенном языке MQL5.
Индикатор Bollinger Bands можно использовать в самых разных торговых стратегиях: от следования за трендом до определения точек поворота или разворота. Технически этот индикатор состоит из экспоненциальной скользящей средней (EMA), которая обычно сглаживает цену закрытия ценной бумаги. Она окружена двумя дополнительными полосами: одна расположена выше, а другая — ниже EMA, и каждая из них обычно устанавливается на уровне 2 стандартных отклонений.
Традиционно полосы Боллинджера используют для выявления уровней перекупленности и перепроданности. При достижении ценами верхней полосы Боллинджера они проявляют тенденцию снова снижаться до медианного значения, и такое поведение нередко справедливо и для нижней полосы. Это можно интерпретировать как дисконтирование ценной бумаги на 2 стандартных отклонения при достижении нижней полосы, что потенциально привлекает инвесторов к покупке актива с выгодной скидкой. Однако случается, что цены резко пробивают уровни полос Боллинджера и продолжают устойчивый тренд. К сожалению, проведенный нами статистический анализ показывает, что прогнозировать прорывы полос Боллинджера может оказаться сложнее, чем предсказать изменения цены.Извлечение данных из терминала MetaTrader 5
Для начала откройте терминал MetaTrader5 и нажмите на иконку «Символ». Должен открыться список символов, доступных на вашем терминале.
Рис. 1. Подготовка к извлечению данных из терминала MetaTrader5.
Теперь нажмите на окне «Бары» и найдите символ, для которого хотите создать модель, а также выберите желаемый таймфрейм. В нашем примере я смоделирую дневной обменный курс для пары GBPUSD.
Рис. 2. Подготовка данных к экспорту.
Затем нажмите кнопку «Export Bars», и продолжим наш анализ на Python.
Анализ результатов наблюдений
Визуализируем взаимодействия между полосами Боллинджера и изменениями уровней цен.
Начнем с импорта необходимых библиотек.
#Import libraries import pandas as pd import numpy as np import seaborn as sns import pandas_ta as taЗатем прочитаем файл .csv, сгенерированный для нашего эмпирического теста. Обратите внимание: мы передали параметр sep="\t" чтобы обозначить разделение файла .csv с помощью табуляции. Это стандартный вывод терминала MetaTrader5.
#Read in the csv file csv = pd.read_csv("/home/volatily/market_data/GBPUSD_Daily_20160103_20240131.csv",sep="\t")Теперь определим наш горизонт прогнозирования.
#Define how far into the future we should forecast look_ahead = 20Рассчитаем полосы Боллинджера для используемых нами данных с помощью библиотеки Pandas TA.
#Add the Bollinger bands csv.ta.bbands(length=30,std=2,append=True)Далее нам понадобится столбец для хранения будущих цен закрытия.
#Add a column to show the future price csv["Future Close"] = csv["Close"].shift(-look_ahead)
Теперь маркируем свои данные. Будем использовать две метки: одна для обозначения изменения цены, а вторая — для обозначения изменения цены между зонами, образованными полосами Боллинджера. Изменения цены пометим как 1 для роста и 0 для снижения. Метки для полос Боллинджера были определены выше.
#Add the normal target, predicting changes in the close price csv["Price Target"] = 0 csv["Price State"] = 0 #Label the data our conditions #If price depreciated, our label is 0 csv.loc[csv["Close"] < csv["Close"].shift(look_ahead),"Price State"] = 0 csv.loc[csv["Close"] > csv["Future Close"], "Price Target"] = 0 #If price appreciated, our label is 1 csv.loc[csv["Close"] > csv["Close"].shift(look_ahead),"Price State"] = 1 csv.loc[csv["Close"] < csv["Future Close"], "Price Target"] = 1 #Label the Bollinger bands #The label to store the current state of the market csv["Current State"] = -1 #If price is above the upper-band, our label is 1 csv.loc[csv["Close"] > csv["BBU_30_2.0"], "Current State"] = 1 #If price is below the upper-band and still above the mid-band,our label is 2 csv.loc[(csv["Close"] < csv["BBU_30_2.0"]) & (csv["Close"] > csv["BBM_30_2.0"]),"Current State"] = 2 #If price is below the mid-band and still above the low-band,our label is 3 csv.loc[(csv["Close"] < csv["BBM_30_2.0"]) & (csv["Close"] > csv["BBL_30_2.0"]),"Current State"] = 3 #Finally, if price is beneath the low-band our label is 4 csv.loc[csv["Close"] < csv["BBL_30_2.0"], "Current State"] = 4 #Now we can add a column to denote the future state the market will be in csv["State Target"] = csv["Current State"].shift(-look_ahead)
Удалим все нулевые записи.
#Let's drop any NaN values csv.dropna(inplace=True)
Теперь мы готовы приступить к визуализации данных, начиная с изменений ценовых уровней с помощью диаграмм типа «ящик с усами». По оси y отобразим цены закрытия, а на оси x у нас будет два значения. Первое значение на оси x представляет собой обозначенные как 0 случаи в наших данных, когда цена падала. В пределах значения 0 можно наблюдать два «ящика с усами». Первый ящик с усами, показанный синим цветом, отображает случаи, когда цена снижалась на протяжении 20 свечей и продолжила падать в течение еще 20 свечей. Оранжевая диаграмма отображает случаи, когда цена падала на протяжении 20 свечей, но потом росла в течение следующих 20 свечей. Обратите внимание, что на собранных нами данных видно, что всякий раз, когда уровни цен опускались ниже 1.1, они всегда восстанавливались. И наоборот, над значением 1 по оси x тоже есть два ящика с усами. Первый ящик с усами (синий) обобщает случаи, когда цена росла, а затем падала, а второй ящик с усами (оранжевый) — случаи, когда цена росли и продолжала расти.
Обратите внимание: для значения 1, или, другими словами, когда цена растет на протяжении 20 свечей, выброс («хвост») от синего ящика с усами больше, чем от оранжевого. Это может указывать на то, что при всяком повышении обменного курса GBPUSD до уровня 1.5 он стремится снизиться, тогда как в столбце 0 при падении обменного курса примерно до уровня 1.1 видно, что цена имеет тенденцию к развороту и началу роста.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Price State",y="Close",hue="Price Target")
Рис. 3. Визуализация изменений ценовых уровней.
Кроме того, можно выполнить сходные визуализации с помощью состояний, определяемых полосами Боллинджера. Как и прежде, цена закрытия будет на оси y, а текущее положение цены в пределах полос Боллинджера будет отмечено четырьмя значениями на оси x. Обратите внимание: хвосты ящиков с усами имеют области, в которых они е перекрываются естественным образом. Такие области потенциально могут служить границами классификации. Например, обратите внимание: когда цена находится в состоянии 4 или полностью под полосами Боллинджера и при этом приближается к уровню 1.1, она, по-видимому, каждый раз отскакивает.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Current State",y="Close",hue="Price Target")
Рис. 4. Визуализация поведения цены в пределах 4 зон индикатора Bollinger Bands.
Более того, мы можем также визуализировать процесс переходов цены между четырьмя состояниями полос Боллинджера с помощью ящика с усами. Например, у приведенного ниже ящика с усами цена закрытия показана на оси y, а четыре значения на оси x обозначают четыре зоны, созданные полосами Боллинджера. Каждая диаграмма показывает в результате, куда перешла цена после появления в той или иной зоне. Проинтерпретируем совокупные данные. Обратите внимание, что для первого значения, состояния 1, есть только три ящика с усами. Это означает, что из состояния 1 цена переходит только в три возможных состояния: либо остается в состоянии 1, либо переходит в состояние 2 или 3.
#Notice that the tails of the box plots have regions where they stop overlapping these zones may guide us as boundaries sns.boxplot(data=csv,x="Current State",y="Close",hue="State Target")
Рис. 5. Визуализация поведения цены в 4 зоах.
#we have very poor separation in the data sns.catplot(data=csv,x="Price State",y="Close",hue="Price Target")
Рис. 6. Визуализация разделения в наборе данных.
Можем выполнить те же визуализации с помощью четырех состояний, заданных полосами Боллинджера. И снова видим, что синие и оранжевые точки лучше всего разделяются на крайних ценовых уровнях.
#Visualizing the separation of data in the Bollinger band zones sns.catplot(data=csv,x="Current State",y="Close",hue="Price Target")
Рис. 7. Визуализация разделения данных в зонах полос Боллинджера.
Теперь можно создать точечную диаграмму со значением закрытия по оси x и будущей ценой закрытия по оси y. Окрасим точки в оранжевый или синий цвет в зависимости от того, выросла или упала цена за предшествующие 20 свечей. Представьте, что вы проводите золотую линию из нижнего левого в верхний правый угол графика. Все точки выше этой золотой линии представляют собой случаи, когда цена в конечном итоге росла в течение следующих 20 свечей, независимо от того, падала она (синие) или росла (оранжевые) за предыдущие 20 свечей. Обратите внимание, что по обе стороны от золотой линии располагаются и синие, и оранжевые точки.
Более того, следует отметить, что при размещении воображаемой красной линии на уровне закрытия 1.3 многие синие и оранжевые точки касались бы этой линии. Это значит, что, помимо текущей цены закрытия, на будущую цену закрытия влияют и другие переменные. Другой способ интерпретации этих наблюдений состоит в том, что одно и то же входное значение может приводить к разным выходным данным, а это указывает на зашумление нашего набора данных!
#Notice that using the price target gives us beautiful separation in the data set sns.scatterplot(data=csv,x="Close",y="Future Close",hue="Price Target")
Рис. 8. В нашем наборе данных очень незначительное естественное разделение.
Теперь выполним ту же визуализацию с помощью целевого состояния полос Боллинджера для окрашивания точечной диаграммы. Обратите внимание, что при использовании полос Боллинджера у нас наблюдается очень незначительное разделение в наборе данных. Визуально это представляется еще худшим вариантом разделения, чем тот, который мы получили, просто использовав саму цену.
#Using the Bollinger bands to define states, however, gives us rather mixed separation sns.scatterplot(data=csv,x="Close",y="Future Close",hue="Current State")
Рис. 9. Визуализация разделения в наборе данных созданного зонами в полосах Боллинджера.
Теперь проведем наши аналитические тесты, чтобы определить, достигнем мы большей точности в прогнозировании изменений ценовых уровней или изменений состояний полос Боллинджера. Сначала импортируем необходимые библиотеки.
#Now let us compare our accuracy forecasting the original price target and the new Bollinger bands target from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score
Далее определим параметры перекрестной проверки временных рядов. Первый параметр, splits, указывает количество разделов, которые нужно создать из наших данных. Второй параметр, gap, определяет размер разрыва для каждого раздела. Этот разрыв должен быть, как минимум, такого же размера, как горизонт прогнозирования.
#Now let us define the cross validation parameters splits = 10 gap = look_ahead
Теперь можно создать наш объект временных серий, который даст нам соответствующие показатели для обучающего и тестового наборов. В нашем примере он сгенерирует 10 пар индексов для обучения и оценки этой модели.
#Now create the cross validation object
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)
Затем создадим DataFrame, чтобы хранить точность нашей модели для прогнозирования каждой цели.
#We need a dataframe to store the accuracy associated with each target target_accuracy = pd.DataFrame(index=np.arange(0,splits),columns=["Price Target Accuracy","New Target Accuracy"])
Теперь определим входные данные для нашей модели.
#Define the inputs predictors = ["Open","High","Low","Close"] target = "Price Target"
Проведем перекрестную проверку.
#Now let us perform the cross validation for i,(train,test) in enumerate(tscv.split(csv)): #First initialize the model model = LinearDiscriminantAnalysis() #Now train the model model.fit(csv.loc[train[0]:train[-1],predictors],csv.loc[train[0]:train[-1],target]) #Now record the accuracy target_accuracy.iloc[i,0] = accuracy_score(csv.loc[test[0]:test[-1],target],model.predict(csv.loc[test[0]:test[-1],predictors]))
Наконец, можно проанализировать результаты тестов.
target_accuracy
Рис. 10. Наша модель показала лучшие результаты при непосредственном прогнозировании изменений цены.
Как упоминалось ранее, тесты показали, что наша модель более эффективна в прогнозировании уровней цен, чем переходов полос Боллинджера. Однако следует отметит, что в среднем эти две стратегии не имеют значительных различий.
Затем реализуем стратегию в коде MQL5 для тестирования ее на исторических данных и посмотрим, как она работает на реальных рыночных данных
Реализация стратегии
Для начала импортируем необходимые библиотеки, которые мы будем использовать везде в программе.
//+------------------------------------------------------------------+ //| Target Engineering.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Libraries we need | //+------------------------------------------------------------------+ /* This Expert Advisor will implement the Linear Discriminant Anlysis algorithm to help us successfully trade Bollinger Band Breakouts. Gamuchirai Zororo Ndawana Selebi Phikwe Botswana Wednesday 10 July 2024 15:42 */ #include <Trade/Trade.mqh>//Trade class CTrade Trade;
Далее определим пользовательские входные данные, например период полос Боллинджера и стандартное отклонение.
//+------------------------------------------------------------------+ //| Input variables | //+------------------------------------------------------------------+ input double bband_deviation = 2.0;//Bollinger Bands standard deviation input int bband_period = 60; //Bollinger Bands Period input int look_ahead = 10; //How far into the future should we forecast? int input lot_multiple = 1; //How many times bigger than minimum lot? int input fetch = 200;//How much data should we fetch? input double stop_loss_values = 1;//Stop loss values
После этого определим глобальные переменные, которые будут использованы в нашем приложении.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int bband_handler;//Technical Indicator Handlers vector bband_high_reading = vector::Ones(fetch);//Bollinger band high reading vector bband_mid_reading = vector::Ones(fetch);//Bollinger band mid reading vector bband_low_reading = vector::Ones(fetch);//Bollinger band low reading double minimum_volume;//The smallest contract size allowed double ask_price;//Ask double bid_price;//Bid vector input_data = vector::Zeros(fetch);//All our input data will be kept in vectors int training_output_array[];//Our output data will be stored in a vector vector output_data = vector::Zeros(fetch); double variance;//This is the variance of our input data int classes = 4;//The total number of output classes we have vector mean_values = vector::Zeros(classes);//This vector will store the mean value for each class vector probability_values = vector::Zeros(classes);//This vector will store the prior probability the target will belong each class vector total_class_count = vector::Zeros(classes);//This vector will count the number of times each class was the target bool model_trained = false;//Has our model been trained? bool training_procedure_running = false;//Have we started the training process? int forecast = 0;//Our model's forecast double discriminant_values[4];//The discriminant function int current_state = 0;//The current state of the system
Далее следует задать функцию инициализации нашего советника. В этой функции инициализируем индикатор Bollinger Band и получим важные рыночные данные.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize the bollinger bands bband_handler = iBands(_Symbol,PERIOD_CURRENT,bband_period,0,bband_deviation,PRICE_CLOSE); //--- Market data minimum_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); //--- End of initilization return(INIT_SUCCEEDED); }
После этого определим основные вспомогательные функции, чтобы разбить наш код на более мелкие и более управляемые сегменты. Первая функция, которую мы создадим, будет отвечать за обновление рыночных данных.
//+------------------------------------------------------------------+ //|This function will update the price and other technical data | //+------------------------------------------------------------------+ void update_technical_data(void) { //--- Update the bid and ask prices ask_price = SymbolInfoDouble(_Symbol,SYMBOL_ASK); bid_price = SymbolInfoDouble(_Symbol,SYMBOL_BID); }
Затем нужно реализовать функцию, которая организует процедуру инициализации. Эта функция гарантирует, что мы получим обучающие данные, подберем максимально подходящие параметры модели и начнем делать прогнозы в правильной последовательности.
//+------------------------------------------------------------------+ //|This function will start training our model | //+------------------------------------------------------------------+ void model_initialize(void) { //--- First we have to fetch the input and output data Print("Initializing the model"); int input_start = 1 + (look_ahead * 2); int output_start = 1+ look_ahead; fetch_input_data(input_start,fetch); fetch_output_data(output_start,fetch); //--- Fit the model fit_lda_model(); }
После этого определим функцию, отвечающую за выборку входных данных для обучения данных. Важно отметить, что входные данные модели будут содержать текущее состояние рынка, а именно — какую зону рынок занимает в настоящий момент. Затем модель спрогнозирует, в какую зону рынок переместится в дальнейшем.
//+------------------------------------------------------------------+ //|This function will fetch the inputs for our model | //+------------------------------------------------------------------+ void fetch_input_data(int f_start,int f_fetch) { //--- This function will fetch input data for our model Print("Fetching input data"); //--- The input for our model will be the current state of the market //--- To know the current state of the market, we have to first update our indicator readings bband_mid_reading.CopyIndicatorBuffer(bband_handler,0,f_start,f_fetch); bband_high_reading.CopyIndicatorBuffer(bband_handler,1,f_start,f_fetch); bband_low_reading.CopyIndicatorBuffer(bband_handler,2,f_start,f_fetch); vector historical_prices; historical_prices.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,f_start,f_fetch); //--- Reshape the input data input_data.Resize(f_fetch); //--- Now we will input the state of the market for(int i = 0; i < f_fetch;i++) { //--- Are we above the bollinger bands entirely? if(historical_prices[i] > bband_high_reading[i]) { input_data[i] = 1; } //--- Are we between the upper and mid band? else if((historical_prices[i] < bband_high_reading[i]) && (historical_prices[i] > bband_mid_reading[i])) { input_data[i] = 2; } //--- Are we between the mid and lower band? else if((historical_prices[i] < bband_mid_reading[i]) && (historical_prices[i] > bband_low_reading[i])) { input_data[i] = 3; } //--- Are we below the bollinger bands entirely? else if(historical_prices[i] < bband_low_reading[i]) { input_data[i] = 4; } } //--- Show the input data Print(input_data); }
В перспективе нам потребуется функция, чтобы извлекать выходные данные для нашей модели. Эта задача сложнее, чем извлечение входных данных. Нам нужно не только зафиксировать конечную зону, куда переместилась цена, но и отследить, сколько раз каждая зона была результатом. Такой подсчет имеет решающее значение для оценки параметров нашей LDA-модели на более позднем этапе.
С этого момента мы готовы внедрить нашу LDA-модель. Есть разные методы подгонки параметров модели; сегодня сконцентрируемся на одном конкретном подходе.
//+---------------------------------------------------------------------+ //|Fetch the output data for our model | //+---------------------------------------------------------------------+ void fetch_output_data(int f_start,int f_fetch) { //--- The output for our model will be the state of the market //--- To know the state of the market, we have to first update our indicator readings Print("Fetching output data"); bband_mid_reading.CopyIndicatorBuffer(bband_handler,0,f_start,(f_fetch)); bband_high_reading.CopyIndicatorBuffer(bband_handler,1,f_start,(f_fetch)); bband_low_reading.CopyIndicatorBuffer(bband_handler,2,f_start,(f_fetch)); vector historical_prices; historical_prices.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,f_start,f_fetch); //--- First we have to ensure that the class count has been reset total_class_count[0] = 0; total_class_count[1] = 0; total_class_count[2] = 0; total_class_count[3] = 0; //--- Now we need to resize the matrix ArrayResize(training_output_array,f_fetch); //--- Now we will input the state of the market to our output vector for(int i =0 ; i < f_fetch;i++) { //--- Are we above the bollinger bands entirely? if(historical_prices[i] > bband_high_reading[i]) { training_output_array[i] = 1; total_class_count[0] += 1; } //--- Are we between the upper and mid band? else if((historical_prices[i] < bband_high_reading[i]) && (historical_prices[i] > bband_mid_reading[i])) { training_output_array[i] = 2; total_class_count[1] += 1; } //--- Are we between the mid and lower band? else if((historical_prices[i] < bband_mid_reading[i]) && (historical_prices[i] > bband_low_reading[i])) { training_output_array[i] = 3; total_class_count[2] += 1; } //--- Are we below the bollinger bands entirely? else if(historical_prices[i] < bband_low_reading[i]) { training_output_array[i] = 4; total_class_count[3] += 1; } } //--- Show the output data Print("Final state of output vector"); ArrayPrint(training_output_array); //--- Show the total number of times each class appeared as the target. Print(total_class_count); }
Процесс довольно непрост и требует подробных разъяснений. Сначала вычислим общую сумму всех входных значений, соответствующих каждому классу на выходе. Например, для каждого случая, где целью была 1, вычислим сумму всех входных значений, сопоставленных с выходом 1, и так далее для каждого класса на выходе. Затем вычислим среднее значение X для каждого класса. Если бы имелось несколько наборов входных данных, мы рассчитали бы среднее значение для каждого. Затем на основе данных обучающего набора приступим к определению вероятности того, что каждый класс окажется фактической целью. После этого вычислим дисперсию X для каждого класса y. Наконец, обновим флаги, чтобы обозначить завершение процедуры обучения.
//+------------------------------------------------------------------+ //|Fit the LDA model | //+------------------------------------------------------------------+ void fit_lda_model(void) { //--- To fit the LDA model, we first need to know the mean value for each our inputs for each of our 4 classes double sum_class_one = 0; double sum_class_two = 0; double sum_class_three = 0; double sum_class_four = 0; //--- In this case we only have 1 input for(int i = 0; i < fetch;i++) { //--- Class 1 if(training_output_array[i] == 1) { sum_class_one += input_data[i]; } //--- Class 2 else if(training_output_array[i] == 2) { sum_class_two += input_data[i]; } //--- Class 3 else if(training_output_array[i] == 3) { sum_class_three += input_data[i]; } //--- Class 4 else if(training_output_array[i] == 4) { sum_class_four += input_data[i]; } } //--- Show the sums Print("Class 1: ",sum_class_one," Class 2: ",sum_class_two," Class 3: ",sum_class_three," Class 4: ",sum_class_four); //--- Calculate the mean value for each class mean_values[0] = sum_class_one / fetch; mean_values[1] = sum_class_two / fetch; mean_values[2] = sum_class_three / fetch; mean_values[3] = sum_class_four / fetch; Print("Mean values"); Print(mean_values); //--- Now we need to calculate class probabilities for(int i=0;i<classes;i++) { probability_values[i] = total_class_count[i] / fetch; } Print("Class probability values"); Print(probability_values); //--- Calculating the variance Print("Calculating the variance"); //--- Next we need to calculate the variance of the inputs within each class of y. //--- This process can be simplified into 2 steps //--- First we calculate the difference of each instance of x from the group mean. double squared_difference[4]; for(int i =0; i < fetch;i++) { //--- If the output value was 1, find the input value that created the output //--- Calculate how far that value is from it's group mean and square the difference if(training_output_array[i] == 1) { squared_difference[0] = MathPow((input_data[i]-mean_values[0]),2); } else if(training_output_array[i] == 2) { squared_difference[1] = MathPow((input_data[i]-mean_values[1]),2); } else if(training_output_array[i] == 3) { squared_difference[2] = MathPow((input_data[i]-mean_values[2]),2); } else if(training_output_array[i] == 4) { squared_difference[3] = MathPow((input_data[i]-mean_values[3]),2); } } //--- Show the squared difference values Print("Squared difference value for each output value of y"); ArrayPrint(squared_difference); //--- Next we calculate the variance as the average squared difference from the mean variance = (1.0/(fetch - 4.0)) * (squared_difference[0] + squared_difference[1] + squared_difference[2] + squared_difference[3]); Print("Variance: ",variance); //--- Update our flags to denote the model has been trained model_trained = true; training_procedure_running = false; } //+------------------------------------------------------------------+
Для создания прогноза с помощью нашей модели начнем с получения последних входных данных с рынка. С помощью этих входных данных рассчитаем дискриминантную функцию для каждого возможного класса. Класс с максимальным значением дискриминантной функции будет нашим прогнозируемым классом.
В MQL5 массивы предлагают полезную функцию ArrayMaximum(), которая возвращает индекс наибольшего значения в одномерном массиве. Поскольку индексы массивов равны нулю, добавляем 1 к результату ArrayMaximum() для получения прогнозируемого класса.
//+------------------------------------------------------------------+ //|This function will obtain forecasts from our model | //+------------------------------------------------------------------+ int model_forecast(void) { //--- First we need to fetch the most recent input data fetch_input_data(0,1); //--- Update the current state of the system current_state = input_data[0]; //--- We need to calculate the discriminant function for each class //--- The predicted class is the one with the largest discriminant function Print("Calculating discriminant values."); for(int i = 0; i < classes; i++) { discriminant_values[i] = (input_data[0] * (mean_values[i]/variance) - (MathPow(mean_values[i],2)/(2*variance)) + (MathLog(probability_values[i]))); } ArrayPrint(discriminant_values); return(ArrayMaximum(discriminant_values) + 1); }
После получения от нашей модели прогноза следующим шагом будет его интерпретация и принятие соответствующего решения. Как упоминалось ранее, торговые сигналы генерируются, когда модель предсказывает, что цена будет перемещаться в другую зону:
- Если по прогнозу предстоит движение из зоны 1 в зону 2, это вызывает сигнал на продажу.
- И наоборот, прогноз перехода из зоны 4 в зону 3 указывает на сигнал к покупке.
- Однако, если прогноз предполагает, что цена останется в той же зоне (например, из зоны 1 в зону 1), это не генерирует сигнал на вход.
//+--------------------------------------------------------------------+ //|This function will interpret out model's forecast and execute trades| //+--------------------------------------------------------------------+ void find_entry(void) { //--- If the model's forecast is not equal to the current state then we are interested //--- Otherwise whenever the model forecasts that the state will remain the same //--- We are uncertain whether price levels will rise or fall if(forecast != current_state) { //--- If the model forecasts that we will move from a small state to a greater state //--- That is from 1 to 2 or from 2 to 4 then that is a down move if(forecast > current_state) { Trade.Sell(minimum_volume * lot_multiple,_Symbol,bid_price,(bid_price + stop_loss_values),(bid_price - stop_loss_values)); } //--- Otherwise we have a buy setup else { Trade.Buy(minimum_volume * lot_multiple,_Symbol,ask_price,(ask_price - stop_loss_values),(ask_price +stop_loss_values)); } } //--- Otherwise we do not have an entry signal from our model }
Наконец, обработчик событий OnTick() отвечает за управление потоком событий и гарантирует, что мы торгуем только в том случае, если наша модель была обучена, а также обеспечивает соблюдение других установленных нами условий торговли.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- We must always update market data update_technical_data(); //--- First we must ensure our model has been trained switch(model_trained) { //--- Our model has been trained case(true): //--- If we have no open positions, let's obtain a forecast from our model if(PositionsTotal() == 0) { //--- Obtaining a forecast forecast = model_forecast(); Comment("Model forecast: ",forecast); //--- Find an entry setup find_entry(); } break; //--- End of case 1 //--- Our model has not been trained default: //--- We haven't started the training procedure! if(!training_procedure_running) { Print("Our model has not been trained, starting the training procedure now."); //--- Initialize the model model_initialize(); } break; //--- End of default case } } //+------------------------------------------------------------------+
Рис. 11. Наша торговая система в действии.
Ограничения
До сих пор наша стратегия сталкивается с существенным ограничением: ее может быть нелегко интерпретировать. Если модель предсказывает, что цена останется в той же зоне, нам не хватает точной информации относительно того, вырастут цены или упадут. Такой компромисс обусловлен нашим решением классифицировать состояния рынка на четыре отдельные зоны, что увеличивает точность, но при этом снижает прозрачность по сравнению с прямым прогнозированием движений цен. Кроме того, при таком подходе генерируется меньше торговых сигналов, поскольку до того, как предпринять какие-либо действия, приходится ждать, пока модель сделает прогноз изменения зоны.
Заключение
В заключение отметим, что наша стратегия использует возможности машинного обучения, в частности линейного дискриминантного анализа (LDA), интегрированные с полосами Боллинджера для получения торговых сигналов. Обеспечивая более высокую точность, выбранный нами подход отчасти жертвует прозрачностью. В целом трейдерам может быть выгоднее прогнозировать изменения цены, чем прорывы полос Боллинджера.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15336





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ознакомьтесь с новой статьей: Переосмысление классических стратегий (часть II): Прорывы полос Боллинджера.
Автор: Гамучирай Зороро Ндавана
Привет, Ндавана
Прежде всего, спасибо за статью и упрощение мифа об ИИ :) Я пытаюсь использовать сигнал, сгенерированный из этой статьи, в своем коде с некоторыми изменениями.
Не могли бы вы объяснить причину (причины), по которой вы использовали векторы, а не простые массивы в своем коде?
Привет, Ндавана
Прежде всего, спасибо за статью и упрощение мифа об ИИ :) Я пытаюсь использовать сигнал, сгенерированный из этой статьи, в своем коде с некоторыми изменениями.
Не могли бы вы объяснить причину (причины), по которой вы использовали векторы, а не простые массивы в вашем коде?
Мое предпочтение векторам исходит из специализированных функций, которые доступны только векторам, помимо этих специальных функций векторы также позволяют нам выполнять вычисления над всеми элементами сразу. Вот простой пример.
Представьте, что если в будущем мы придумаем вычисления, которые могут быть полезны, то модифицировать кодовую базу будет намного проще, так как мы используем векторы.