English 中文 Español Deutsch 日本語 Português
preview
Переосмысливаем классические стратегии (Часть II): Пробои индикатора Bollinger Bands

Переосмысливаем классические стратегии (Часть II): Пробои индикатора Bollinger Bands

MetaTrader 5Примеры | 10 марта 2025, 08:44
535 2
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Введение

Индикатор Bollinger Bands («Полосы Боллинджера») — универсальный инструмент в торговых стратегиях, эффективный как для следования за трендом, так и для обнаружения потенциальных точек разворота. Технически индикатор состоит из экспоненциальной скользящей средней (EMA), которая сглаживает цену закрытия ценной бумаги. Эта центральная линия заключена между двумя дополнительными линиями, расположенными выше и ниже EMA, как правило, на 2 стандартных отклонения.

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

Добились мы этого, обучив две равноценные ИИ-модели с помощью алгоритма линейного дискриминантного анализа и сравнив их друг с другом посредством перекрестной проверки временных рядов, опираясь при тестировании исключительно на библиотеку scikit-learn. Первую модель обучили просто предсказывать подъем и спад цены, а вторая научилась прогнозировать, как цена будет перемещаться между четырьмя зонами, обозначенными полосами Боллинджера. К разочарованию поклонников индикатора Bollinger Bands, эмпирические наблюдения привели нас к выводу, что непосредственный прогноз цены может быть эффективнее прогнозирования перехода между созданными полосами Боллинджера четырьмя зонами. Однако стоит отметить, что для настройки параметров индикатора не применялись методы оптимизации.

Цель этой статьи — продемонстрировать:

  1. Как можно сравнить две возможные торговые стратегии аналитически.
  2. Как с нуля реализовать линейный дискриминантный анализ на MQL5.
  3. Как создать стабильные торговые стратегии с применением ИИ.

    Обзор стратегии и наших соображений

    Термин «искусственный интеллект» (ИИ) небезосновательно считают одним из самых дезориентирующих в истории с точки зрения выбора названия. После прочтения этой статьи вы, вероятно, согласитесь, что «ИИ» — неправильное название. Как для автора, для меня проблема — в слове «интеллект». ИИ-модели не обладают интеллектом в человеческом смысле слова. Вместо этого они представляют собой интеллектуальные приложения алгоритмов оптимизации.

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

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

    • Зона 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")
    


    Визуализация поведения цены с помошью зон индикатора Bollinger Bands

    Рис. 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")
    

    Визуализация поведения цены в 4 зонах.

    Рис. 5. Визуализация поведения цены в 4 зоах.

    Создадим точный график с ценой закрытия по оси y и двумя значениями по оси x. Первое значение (Price State 0) указывает на примеры падения цены за пределы предыдущих 10 свечей. Над состоянием State 0 находится облако синих и оранжевых точек. Эти точки представляют собой случаи, когда после падения в течение 10 свечей цена либо продолжала падать, либо разворачивалась и начинала расти в течение последующих 10 свечей соответственно. Обратите внимание: нет четкого разделения между случаями, когда цена продолжала падать и когда она разворачивалась и начинала расти. Видимо, четко определенная точка разделения — только момент, когда цена приближается к крайним значениям. Например, на всех ценовых уровнях ниже 1.1 в состоянии 0 цена последовательно восстанавливалась.

    #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. Если по прогнозу предстоит движение из зоны 1 в зону 2, это вызывает сигнал на продажу.
    2. И наоборот, прогноз перехода из зоны 4 в зону 3 указывает на сигнал к покупке.
    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

    Прикрепленные файлы |
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
    Anil Varma
    Anil Varma | 10 авг. 2024 в 08:52

    Привет, Ндавана

    Прежде всего, спасибо за статью и упрощение мифа об ИИ :) Я пытаюсь использовать сигнал, сгенерированный из этой статьи, в своем коде с некоторыми изменениями.

    Не могли бы вы объяснить причину (причины), по которой вы использовали векторы, а не простые массивы в своем коде?

    Gamuchirai Zororo Ndawana
    Gamuchirai Zororo Ndawana | 10 авг. 2024 в 12:35
    Anil Varma #:

    Привет, Ндавана

    Прежде всего, спасибо за статью и упрощение мифа об ИИ :) Я пытаюсь использовать сигнал, сгенерированный из этой статьи, в своем коде с некоторыми изменениями.

    Не могли бы вы объяснить причину (причины), по которой вы использовали векторы, а не простые массивы в вашем коде?

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

    Мое предпочтение векторам исходит из специализированных функций, которые доступны только векторам, помимо этих специальных функций векторы также позволяют нам выполнять вычисления над всеми элементами сразу. Вот простой пример.
    //+------------------------------------------------------------------+
    //|                                                         Anil.mq5 |
    //|                                        Gamuchirai Zororo Ndawana |
    //|                          https://www.mql5.com/en/gamuchiraindawa |
    //+------------------------------------------------------------------+
    #property copyright "Gamuchirai Zororo Ndawana"
    #property link      "https://www.mql5.com/en/gamuchiraindawa"
    #property version   "1.00"
    //+------------------------------------------------------------------+
    //| Script program start function                                    |
    //+------------------------------------------------------------------+
    void OnStart()
      {
    //--- Here is my problem with arrays
    double anil_array[3];
    ArrayFill(anil_array,0,3,0);
    ArrayPrint(anil_array);
    //--- We have to iterate over all the elements to perform calculations
    for(int i = 0; i < 3; i++)
       {
          anil_array[i] += 1;
       }
    ArrayPrint(anil_array);
    //--- And the same operation with vector
    vector anil_vector = vector::Zeros(3); //Simillar to an array full of Zeros   
    Print(anil_vector);   
    //--- Vectors allow us to perform calculations on all the elements at once
    anil_vector = anil_vector + 1;
    Print(anil_vector);
      }
    //+------------------------------------------------------------------+
    

    Векторы VS массивы.

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

    Создание панели торгового администратора на MQL5 (Часть I): Создание интерфейса обмена сообщениями Создание панели торгового администратора на MQL5 (Часть I): Создание интерфейса обмена сообщениями
    В данной статье рассматривается создание интерфейса обмена сообщениями для MetaTrader 5, предназначенного для системных администраторов, чтобы облегчить общение с другими трейдерами непосредственно внутри платформы. Недавняя интеграция социальных платформ с MQL5 позволяет быстро транслировать сигнал по разным каналам. Представьте, что вы можете проверять отправленные сигналы одним щелчком мыши — либо "ДА", либо "НЕТ". Читайте дальше, чтобы узнать больше.
    Cоздание стратегии возврата к среднему на основе машинного обучения Cоздание стратегии возврата к среднему на основе машинного обучения
    В данной статье предлагается очередной оригинальный подход к созданию торговых систем на основе машинного обучения, с использованием кластеризации и разметки сделок для стратегий возврата к среднему.
    Автоматизация торговли с помощью трендовой стратегии Parabolic SAR на MQL5: Создаем эффективный советник Автоматизация торговли с помощью трендовой стратегии Parabolic SAR на MQL5: Создаем эффективный советник
    В этой статье мы автоматизируем торговлю с помощью стратегии Parabolic SAR на MQL5, создав эффективный советник. Советник будет совершать сделки по трендам, определяемым индикатором Parabolic SAR.
    Формулировка динамического советника на нескольких парах (Часть 1): Корреляция и обратная корреляция валютных пар Формулировка динамического советника на нескольких парах (Часть 1): Корреляция и обратная корреляция валютных пар
    Динамический советник на нескольких парах использует как корреляционные, так и обратные корреляционные стратегии для оптимизации эффективности торговли. Анализируя рыночные данные в режиме реального времени, он определяет и использует взаимосвязь между валютными парами.