English Deutsch 日本語
preview
Переосмысливаем классические стратегии в MQL5 (Часть III): Прогнозирование индекса FTSE 100

Переосмысливаем классические стратегии в MQL5 (Часть III): Прогнозирование индекса FTSE 100

MetaTrader 5Примеры | 25 апреля 2025, 09:16
135 4
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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


Обзор торговой стратегии

Лондонская фондовая биржа (LSE) - одна из старейших фондовых бирж в развитых странах мира. Она основана в 1801 году и является основной фондовой биржей Соединенного Королевства. Она считается частью "большой тройки" наряду с Нью-Йоркской фондовой биржей и Токийской фондовой биржей. Лондонская фондовая биржа является крупнейшей фондовой биржей в Европе, и, согласно ее официальному веб-сайту, текущая общая рыночная капитализация всех компаний, котирующихся на бирже, составляет примерно 4,4 трлн. британских фунтов стерлингов.

Financial Times Stock Exchange (FTSE) 100 - это индекс, производный от LSE, отслеживающий 100 крупнейших компаний, котирующихся на LSE. Эти компании обычно называют "голубыми фишками" и считаются относительно безопасными инвестициями, учитывая репутацию, заработанную компаниями с течением времени, и их проверенный послужной список. Мы можем воспользоваться нашим пониманием того, как рассчитывается индекс FTSE 100, и потенциально создать новую торговую стратегию, которая будет прогнозировать будущую цену закрытия FTSE 100, учитывая текущую цену закрытия индекса, а также динамику акций 10 крупных компаний, включенных в индекс.


Обзор методологии

Мы создали наш советник на базе искусственного интеллекта полностью на MQL5. Это дает нам гибкость, поскольку нашу модель можно использовать на разных таймфреймах без необходимости постоянной корректировки. Кроме того, мы можем динамически настраивать параметры модели, например, на какое отдаленное будущее она должна прогнозировать. Наша модель использовала в общей сложности 12 входных данных для прогнозирования будущей цены закрытия FTSE 100, на 20 шагов вперед.

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

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



Первоначальная разработка в виде скрипта

Изначально мы представим нашу идею в виде простого скрипта на MQL5, чтобы оценить, как части нашей системы работают вместе. Начнем с определения наших глобальных переменных.
//+------------------------------------------------------------------+
//|                                                        UK100.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                                    |
//+------------------------------------------------------------------+

//1) ADM.LSE  - Admiral
//2) AAL.LSE  - Anglo American
//3) ANTO.LSE - Antofagasta
//4) AHT.LSE  - Ashtead
//5) AZN.LSE  - AstraZeneca
//6) ABF.LSE  - Associated British Foods
//7) AV.LSE   - Aviva
//8) BARC.LSE - Barclays
//9) BP.LSE   - BP
//10) BKG.LSE - Berkeley Group 
//11) UK100   - FTSE 100 Index

//+-------------------------------------------------------------------+
//| Global variables                                                  |
//+-------------------------------------------------------------------+
int fetch = 2000;
int look_ahead = 20;
double mean_values[11],std_values[11];
string list_of_companies[11] = {"ADM.LSE","AAL.LSE","ANTO.LSE","AHT.LSE","AZN.LSE","ABF.LSE","AV.LSE","BARC.LSE","BP.LSE","BKG.LSE","UK100"};
vector intercept = vector::Ones(fetch);
matrix target = matrix::Zeros(1,fetch);
matrix coefficients;
matrix input_matrix = matrix::Zeros(12,fetch);

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

void OnStart()
  {
//--- Fetch the target
target.CopyRates("UK100",PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);

//--- Fill in the input matrix
for(int i = 0; i < 11; i++)
   {
      //--- Add the symbol to market watch
      SymbolSelect(list_of_companies[i],true);
      //--- Fetch historical data
      vector temp = vector::Zeros(fetch);
      temp.CopyRates(list_of_companies[i],PERIOD_CURRENT,COPY_RATES_CLOSE,1+look_ahead,fetch);
      //--- Store the mean value and standard deviation, also scale the data
      mean_values[i] = temp.Mean();
      std_values[i] = temp.Std();
      temp = ((temp - mean_values[i]) / std_values[i]);
      //--- Add the data to the matrix
      input_matrix.Row(temp,i);
   }
//--- Add the intercept
input_matrix.Row(intercept,11);

//--- Show the input data
Print("Input data:");
Print(input_matrix);

Рис 1: Пример выходных данных, сгенерированных нашим скриптом

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

//--- Calculating coefficient values
coefficients = target.MatMul(input_matrix.PInv());

//--- Display the coefficient values
Print("UK100 Coefficients:");
Print(coefficients.Transpose());

UK100 Coefficients

Рис 2: Параметры нашей модели

Давайте интерпретируем результаты вместе, первое значение коэффициента представляет собой среднее значение целевого показателя, когда все входные данные модели равны 0. Это математическое определение параметра смещения. Однако в нашем торговом приложении он имеет очень мало интуитивного смысла. С технической точки зрения, если бы все акции в FTSE 100 оценивались в 0 британских фунтов, то средняя будущая стоимость FTSE 100 также составляла бы 0 британских фунтов. Второй коэффициент представляет собой незначительное изменение будущей стоимости FTSE 100 при условии, что все остальные акции закрываются по той же цене. Поэтому всякий раз, когда акции Admiral растут на одну единицу, мы наблюдаем, что будущая цена закрытия индекса, по-видимому, несколько снижается.

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



Реализация нашего советника

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

//+------------------------------------------------------------------+
//|                                                  FTSE 100 AI.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"

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input int look_ahead = 20;       // How far into the future should we forecast?
input int rsi_period = 20;       // The period of our RSI
input int profit_target = 20;     // After how much profit should we close our positions?
input bool ai_auto_close = true; // Should the AI automatically close positions?

Затем импортируем торговую библиотеку, которая поможет нам управлять нашими позициями.

//+------------------------------------------------------------------+
//| Libraries we need                                                |
//+------------------------------------------------------------------+
#include  <Trade/Trade.mqh>
CTrade Trade;

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

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double position_profit = 0;
int fetch = 20;
matrix coefficients;
matrix input_matrix = matrix::Zeros(12,fetch);
double mean_values[11],std_values[11],rsi_buffer[1];
string list_of_companies[11] = {"ADM.LSE","AAL.LSE","ANTO.LSE","AHT.LSE","AZN.LSE","ABF.LSE","AV.LSE","BARC.LSE","BP.LSE","BKG.LSE","UK100"};
ulong open_ticket;

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

//+------------------------------------------------------------------+
//| This function will fetch our training data                       |
//+------------------------------------------------------------------+
void fetch_training_data(void)
  {
//--- Fetch the target
   target.CopyRates("UK100",PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);

//--- Add the intercept
   input_matrix.Row(intercept,0);

//--- Fill in the input matrix
   for(int i = 0; i < 11; i++)
     {
      //--- Add the symbol to market watch
      SymbolSelect(list_of_companies[i],true);
      //--- Fetch historical data
      vector temp = vector::Zeros(fetch);
      temp.CopyRates(list_of_companies[i],PERIOD_CURRENT,COPY_RATES_CLOSE,1+look_ahead,fetch);
      //--- Store the mean value and standard deviation, also scale the data
      mean_values[i] = temp.Mean();
      std_values[i] = temp.Std();
      temp = ((temp - mean_values[i]) / std_values[i]);
      //--- Add the data to the matrix
      input_matrix.Row(temp,i+1);
     }
  }

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

//+---------------------------------------------------------------------+
//| This function will fit our multiple linear regression model         |
//+---------------------------------------------------------------------+
void model_fit(void)
  {
//--- Calculating coefficient values
   coefficients = target.MatMul(input_matrix.PInv());
  }

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

//+---------------------------------------------------------------------+
//| This function will fetch a prediction from our model                |
//+---------------------------------------------------------------------+
void model_predict(void)
  {
//--- Add the intercept
   intercept = vector::Ones(1);
   input_matrix.Row(intercept,0);

//--- Fill in the input matrix
   for(int i = 0; i < 11; i++)
     {
      //--- Fetch historical data
      vector temp = vector::Zeros(1);
      temp.CopyRates(list_of_companies[i],PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      //--- Normalize and scale the data
      temp = ((temp - mean_values[i]) / std_values[i]);
      //--- Add the data to the matrix
      input_matrix.Row(temp,i+1);
     }

//--- Calculate the model forecast
   forecast = (
                 (1 * coefficients[0,0]) +
                 (input_matrix[0,1] * coefficients[0,1]) +
                 (input_matrix[0,2] * coefficients[0,2]) +
                 (input_matrix[0,3] * coefficients[0,3]) +
                 (input_matrix[0,4] * coefficients[0,4]) +
                 (input_matrix[0,5] * coefficients[0,5]) +
                 (input_matrix[0,6] * coefficients[0,6]) +
                 (input_matrix[0,7] * coefficients[0,7]) +
                 (input_matrix[0,8] * coefficients[0,8]) +
                 (input_matrix[0,9] * coefficients[0,9]) +
                 (input_matrix[0,10] * coefficients[0,10]) +
                 (input_matrix[0,11] * coefficients[0,11])
              );
//--- Store the model's state
//--- Whenever the system and model state aren't the same, we may have a potential reversal
   if(forecast > iClose("UK100",PERIOD_CURRENT,0))
     {
      model_state = 1;
     }

   else
      if(forecast < iClose("UK100",PERIOD_CURRENT,0))
        {
         model_state = -1;
        }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Update our market data                                           |
//+------------------------------------------------------------------+
void update_market_data(void)
  {
//--- Update the bid and ask prices
   bid = SymbolInfoDouble("UK100",SYMBOL_BID);
   ask = SymbolInfoDouble("UK100",SYMBOL_ASK);
//--- Update the RSI readings
   CopyBuffer(rsi_handler,0,1,1,rsi_buffer);
  }

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

//+------------------------------------------------------------------+
//| Check if we have an opportunity to sell                          |
//+------------------------------------------------------------------+
void check_sell(void)
  {
   if(iClose("UK100",PERIOD_W1,0) < iClose("UK100",PERIOD_W1,12))
     {
      if(rsi_buffer[0] < 50)
        {
         Trade.Sell(0.3,"UK100",bid,0,0,"FTSE 100 AI");
         //--- Remeber the ticket
         open_ticket = PositionGetTicket(0);
         //--- Whenever the system and model state aren't the same, we may have a potential reversal
         system_state = -1;
        }
     }
  }

//+------------------------------------------------------------------+
//| Check if we have an opportunity to buy                           |
//+------------------------------------------------------------------+
void check_buy(void)
  {
   if(iClose("UK100",PERIOD_W1,0) > iClose("UK100",PERIOD_W1,12))
     {
      if(rsi_buffer[0] > 50)
        {
         Trade.Buy(0.3,"UK100",ask,0,0,"FTSE 100 AI");
         //--- Remeber the ticket
         open_ticket = PositionGetTicket(0);
         //--- Whenever the system and model state aren't the same, we may have a potential reversal
         system_state = 1;
        }
     }
  }

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Prepare the technical indicator
   rsi_handler = iRSI(Symbol(),PERIOD_CURRENT,rsi_period,PRICE_CLOSE);

//--- Validate the indicator handler
   if(rsi_handler == INVALID_HANDLE)
     {
      //--- We failed to load the indicator
      Comment("Failed to load the RSI indicator");
      return(INIT_FAILED);
     }

//--- This function will fetch our training data and scaling factors
   fetch_training_data();

//--- This function will fit our multiple linear regression model
   model_fit();

//--- Ensure the user's inputs are valid
   if((ai_auto_close == false && profit_target == 0))
     {
      Comment("Either set AI auto close true, or define a profit target!")
      return(INIT_FAILED);
     }

//--- Everything went well
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free up the resources we are no longer using
   IndicatorRelease(rsi_handler);
   ExpertRemove();
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Since we are dealing with a lot of different symbols, be sure to select the UK1OO (FTSE100)
//--- Select the symbol
   SymbolSelect("UK100",true);

//--- Update market data
   update_market_data();

//--- Fetch a prediction from our AI model
   model_predict();

//--- Give the user feedback
   Comment("Model forecast: ",forecast,"\nPosition Profit: ",position_profit);



//--- Look for a position
   if(PositionsTotal() == 0)
     {
      //--- We have no open positions
      open_ticket = 0;

      //--- Check if our model's prediction is validated
      if(model_state == 1)
        {
         check_buy();
        }

      else
         if(model_state == -1)
           {
            check_sell();
           }
     }

//--- Do we have a position allready?
   if(PositionsTotal() > 0)
     {

      //--- Should we close our positon manually?
      if(PositionSelectByTicket(open_ticket))
        {
         if((profit_target > 0) && (ai_auto_close == false))
           {
            //--- Update the position profit
            position_profit = PositionGetDouble(POSITION_PROFIT);
            if(profit_target < position_profit)
              {
               Trade.PositionClose("UK100");
              }
           }
        }

      //--- Should we close our positon using a hybrid approach?
      if(PositionSelectByTicket(open_ticket))
        {
         if((profit_target > 0) && (ai_auto_close == true))
           {
            //--- Update the position profit
            position_profit = PositionGetDouble(POSITION_PROFIT);
            //--- Check if we have passed our profit target or if we are expecting a reversal
            if((profit_target < position_profit) || (model_state != system_state))
              {
               Trade.PositionClose("UK100");
              }
           }
        }


      //--- Are we closing our system just using AI?
      else
         if((system_state != model_state) &&
            (ai_auto_close == true) &&
            (profit_target == 0))
           {
            Trade.PositionClose("UK100");
           }
     }
  }
//+------------------------------------------------------------------+

UK100 Backtest

Рис. 3: Бэк-тестирование нашего советника


Оптимизация нашего советника

Пока что наше торговое приложение работает нестабильно. Мы можем попытаться улучшить стабильность нашего торгового приложения, используя идеи, изложенные американским экономистом Гарри Марковицем (Harry Markowitz). Марковицу приписывают концептуализацию основ современной портфельной теории (СПТ) в том виде, в каком мы ее знаем сегодня. По сути, он понял, что эффективность любого отдельного актива ничтожно мала по сравнению с эффективностью всего портфеля инвестора.

Рис. 4: Фотография нобелевского лауреата Гарри Марковица (Harry Markowitz)

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

#Import the libraries we need
import pandas            as pd
import numpy             as np
import seaborn           as sns
import MetaTrader5       as mt5
import matplotlib.pyplot as plt
from   scipy.optimize    import  minimize

Нам нужно составить список акций, которые мы будем рассматривать.

#Create the list of stocks
stocks = ["ADM.LSE","AAL.LSE","ANTO.LSE","AHT.LSE","AZN.LSE","ABF.LSE","AV.LSE","BARC.LSE","BP.LSE","BKG.LSE","UK100"]

Инициализируем терминал.

#Initialize the terminal
if(!mt5.initialize()):
    print('Failed to load the MT5 Terminal')
True

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

#Create a dataframe to store our returns
amount  = 10000
returns = pd.DataFrame(columns=stocks,index=np.arange(0,amount))

Извлекаем необходимые нам данные из нашего терминала MetaTrader 5.

#Fetch the data
for stock in stocks:
     temp = pd.DataFrame(mt5.copy_rates_from_pos(stock,mt5.TIMEFRAME_M1,0,amount))
     returns[[stock]] = temp[['close']]

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

#Store the data as returns
returns = returns.pct_change()
returns.dropna(inplace=True)

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

#Let's look at our dataframe
returns = returns * (10.0 ** 2)

Давайте теперь рассмотрим данные, которые у нас есть.

returns

Рис. 5: Некоторые из доходов по акциям из нашей корзины акций FTSE100

Теперь построим график рыночной доходности каждой имеющейся у нас акции. Мы ясно видим, что некоторые акции, такие как Ashtead Group (AHT.LSE), имеют сильные хвосты, которые отличаются от средних показателей портфеля из 11 имеющихся у нас акций. По сути, алгоритм Марковица поможет нам эмпирически отобрать меньше акций с высокой дисперсией и больше акций с меньшей дисперсией. Подход Марковица является аналитическим и исключает с нашей стороны любые догадки в процессе.

#Let's visualize our market returns
returns.plot()

Рис. 6: Рыночная доходность от нашей корзины акций на FTSE 100

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

#Let's analyze the correlation coefficients
fig, ax = plt.subplots(figsize=(8,8)) 
sns.heatmap(returns.corr(),annot=True,linewidths=.5, ax=ax)

Рис. 7: Наша корреляционная тепловая карта по FTSE 100

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

Мы начнем с того, что отодвинем нашу корзину акций на 20 позиций назад, а символ UK100 - на 20 позиций вперед.

# Let's also analyze for lead-lag correlation
look_ahead  = 20
lead_lag    = pd.DataFrame(columns=stocks,index=np.arange(0,returns.shape[0] - look_ahead))
for stock in stocks:
    if stock == 'UK100':
        lead_lag[[stock]] = returns[[stock]].shift(-20)
    else:
        lead_lag[[stock]] = returns[[stock]].shift(20)

# Returns
lead_lag.dropna(inplace=True)

Посмотрим, были ли какие-либо изменения в корреляционной матрице. К сожалению, эта процедура не принесла нам каких-либо улучшений.

#Let's see if there are any stocks that are correlated with the future UK100 returns
fig, ax = plt.subplots(figsize=(8,8)) 
sns.heatmap(lead_lag.corr(),annot=True,linewidths=.5, ax=ax)

Рис. 8: Наша тепловая карта корреляции опережения и запаздывания

Теперь попытаемся свести к минимуму различия во всем нашем портфолио. Мы будем моделировать наш портфель таким образом, чтобы нам было разрешено покупать и продавать различные акции для минимизации рисков для нашего портфеля. Для достижения нашей цели мы будем использовать библиотеку оптимизации SciPy. У нас будет 11 различных весов, которые необходимо оптимизировать; каждый вес представляет собой распределение капитала, которое должно быть произведено для каждой соответствующей акции. Коэффициент каждого веса будет символизировать, следует ли нам покупать (положительный коэффициент) или продавать (отрицательный коэффициент) каждую конкретную акцию. Чтобы быть более конкретными, нам надо быть уверенными, что все наши коэффициенты находятся в диапазоне от -1 до 1 включительно, или в интервальной записи [-1,1].

Вдобавок ко всему, мы хотели бы использовать весь имеющийся у нас капитал, и не более того. Следовательно, мы должны установить ограничения для нашей процедуры оптимизации. В частности, мы должны обеспечить, чтобы сумма всех весов в портфеле равнялась 1. Это будет означать, что мы выделили весь имеющийся у нас капитал. Напомним, что некоторые из наших коэффициентов будут отрицательными, поэтому может возникнуть проблема с тем, чтобы сумма всех наших коэффициентов равнялась 1. Следовательно, мы должны изменить это ограничение, чтобы учитывать только абсолютные значения каждого веса. Другими словами, если мы храним наши 11 весов в векторе, мы хотим убедиться, что L1-norm равен 1.

Для начала мы сперва инициализируем наши веса случайными значениями и вычисляем ковариацию матрицы наших доходов.

#Let's attempt to minimize the variance of the portfolio
weights = np.array([0,0,0,0,0,0,-1,1,1,0,0])
covariance = returns.cov()

Давайте теперь проследим за начальными уровнями дисперсии.

#Store the initial portfolio variance
initial_portfolio_variance = np.dot(weights.T,np.dot(covariance,weights))
initial_portfolio_variance
0.011959689589562724

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

#Cost function
def cost_function(x):
    return(np.dot(x.T,np.dot(covariance,x)))

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

#Constraints
def l1_norm(x):
    return(np.sum(np.abs(x))) - 1

constraints = {'type': 'eq', 'fun':l1_norm}

SciPy ожидает, что при выполнении оптимизации мы предоставим ему первоначальное предположение.

#Initial guess
initial_guess = weights

Напомним, что мы хотим, чтобы все наши веса находились в диапазоне от -1 до 1; мы передаем эти инструкции SciPy, используя набор границ.

#Add bounds
bounds =   [(-1,1)] * 11

Теперь мы сведем к минимуму дисперсию нашего портфеля, используя алгоритм последовательного программирования методом наименьших квадратов (SLSQP). Алгоритм SLSQP был первоначально разработан выдающимся немецким инженером Дитером Крафтом (Dieter Kraft) в 1980-х годах.  Первоначальная программа была реализована на языке FORTRAN. Оригинальную научную статью Крафта с описанием алгоритма можно найти по этой ссылке, здесь. SLSQP - это квазиньютоновский алгоритм, который означает, что он оценивает вторую производную (матрицу Гесса) целевой функции, чтобы найти оптимумы целевой функции.

#Minimize the portfolio variance
result = minimize(cost_function,initial_guess,method="SLSQP",constraints=constraints,bounds=bounds)

Мы успешно выполнили эту процедуру оптимизации, давайте посмотрим на наши результаты.

result
 message: Optimization terminated successfully
 success: True
  status: 0
     fun: 0.0004706570068070814
       x: [ 5.845e-02 -1.057e-01  8.800e-03  2.894e-02 -1.461e-01
            3.433e-02 -2.625e-01  6.867e-02  1.653e-01  3.450e-02
            8.675e-02]
     nit: 12
     jac: [ 3.820e-04 -9.886e-04  3.242e-05  4.724e-04 -1.544e-03
            4.151e-04 -1.351e-03  5.850e-04  8.880e-04  4.457e-04
            4.392e-05]
    nfev: 154
    njev: 12

Давайте сохраним оптимальные веса, найденные для нас нашим решателем SciPy.

#Store the optimal weights
optimal_weights = result.x

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

#Validating the weights add up to one
np.sum(np.abs(optimal_weights))
0.9999999998893961

Сохраним новую дисперсию портфеля.

#Store the new portfolio variance
otpimal_variance = cost_function(optimal_weights)

Создадим фрейм данных, который позволит нам сравнить нашу эффективность.

#Portfolio variance
portfolio_var = pd.DataFrame(columns=['Old Var','New Var'],index=[0])

Сохраним наши уровни дисперсии во фрейме данных.

portfolio_var.iloc[0,0] = initial_portfolio_variance * (10.0 ** 7)
portfolio_var.iloc[0,1] = otpimal_variance * (10.0 ** 7)

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

portfolio_var.plot.bar()

Рис. 9: Новая дисперсия нашего портфеля

Теперь подсчитаем количество позиций, которые мы должны открыть на каждом рынке, в соответствии с нашими оптимальными весами. Наши данные свидетельствуют о том, что всякий раз, когда мы открываем 1 долгосрочную позицию по символу UK100, мы не должны открывать каких-либо позиций в Admiral Group (ADM.LSE) и должны открывать 2 эквивалентные краткосрочные позиции в Anglo-American (AAL.LSE).

int_weights = (optimal_weights / optimal_weights[-1]) // 1
int_weights
array([ 0., -2.,  0.,  0., -2.,  0., -4.,  0.,  1.,  0.,  1.])



Обновление нашего советника

Теперь мы можем обратить наше внимание на обновление нашего торгового алгоритма, чтобы воспользоваться преимуществами нашего нового понимания рынка FTSE 100. Давайте сначала загрузим оптимальные веса, которые мы рассчитали с помощью SciPy.

int    optimization_weights[11] = {0,-2,0,0,-2,0,-4,0,1,0,1};

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

input double  loss_limit = 20;        // After how much loss should we optimize our portfolio?

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

      //--- Should we optimize our portfolio variance using the optimal weights we have calculated
      if((loss_limit > 0))
        {
         //--- Update the position profit
         position_profit = AccountInfoDouble(ACCOUNT_EQUITY) - AccountInfoDouble(ACCOUNT_BALANCE);
         //--- Check if we have passed our profit target or if we are expecting a reversal
         if(((loss_limit * -1) < position_profit))
           {
            minimize_variance();
           }
        }
        

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

//+------------------------------------------------------------------+
//| This function will minimize the variance of our portfolio        |
//+------------------------------------------------------------------+
void minimize_variance(void)
  {
   risk_minimized = true;

   if(!risk_minimized)
     {
      for(int i = 0; i < 11; i++)
        {
         string current_symbol = list_of_companies[i];

         //--- Add that stock to the portfolio to minimize our variance, buy
         if(optimization_weights[i] > 0)
           {
            for(int i = 0; i < optimization_weights[i]; i++)
              {
               Trade.Buy(0.3,current_symbol,ask,0,0,"FTSE Optimization");
              }
           }
         //--- Add that stock to the portfolio to minimize our variance, sell
         else
            if(optimization_weights[i] < 0)
              {
               for(int i = 0; i < optimization_weights[i]; i--)
                 {
                  Trade.Sell(0.3,current_symbol,bid,0,0,"FTSE Optimization");
                 }
              }
        }
     }
  }

Рис. 10: Форвард-тестирование нашего алгоритма

Рис. 11: Бэк-тестирование нашего алгоритма

Рис. 12: Результаты бэк-тестирования нашего алгоритма

Рис. 13: Дополнительные результаты бэк-тестирования нашего алгоритма



Заключение

В настоящей статье мы продемонстрировали, как легко можно создать комплекс технического анализа и искусственного интеллекта, используя Python и MQL5. Наше приложение способно динамически подстраиваться под все таймфреймы, доступные в терминале MetaTrader 5. Мы изложили основные принципы современной оптимизации портфеля ценных бумаг с использованием современных алгоритмов оптимизации. Мы продемонстрировали, как свести к минимуму необъективность со стороны человека в процессе выбора портфолио. Кроме того, мы продемонстрировали, как использовать рыночные данные для принятия оптимальных решений. В нашем алгоритме еще есть возможности для совершенствования, например, в будущих статьях мы продемонстрируем, как оптимизировать 2 критерия одновременно, например, оптимизация уровней риска с учетом безрисковой нормы доходности.

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

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

Прикрепленные файлы |
UK100.mq5 (3.16 KB)
FTSE_100_AI.mq5 (12.63 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Ugochukwu Mobi
Ugochukwu Mobi | 24 сент. 2024 в 12:27
Статья призвана информировать, обучать или развлекать читателей. Если в ней повторяются одни и те же вещи, только меняются названия индикаторов или акций, это не помогает и просто тратит время читателя.
linfo2
linfo2 | 24 сент. 2024 в 19:31
Ugochukwu Mobi #:
Статья призвана информировать, обучать или развлекать читателей. Если в ней повторяется одно и то же, только меняются индикаторы или названия акций, это не помогает и просто тратит время читателя.

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

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 26 сент. 2024 в 08:56
Ugochukwu Mobi #:
Статья призвана информировать, обучать или развлекать читателей. Если в ней повторяются одни и те же вещи, только меняются названия индикаторов или акций, это не помогает и просто тратит время читателя.
Привет Mobi.

Я пишу 3 разных цикла статей. Я хотел бы понять, когда вы говорите, что статьи повторяются, вы имеете в виду все 3 серии, или в пределах одной серии? Кроме того, что бы вы хотели сделать по-другому?
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 26 сент. 2024 в 11:01
linfo2 #:

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

Спасибо, Нил. Я верю, что мы найдем его. Он не может прятаться от нас вечно.

Нейросети в трейдинге: Оптимизация LSTM для целей прогнозирования многомерных временных рядов (DA-CG-LSTM) Нейросети в трейдинге: Оптимизация LSTM для целей прогнозирования многомерных временных рядов (DA-CG-LSTM)
Статья знакомит с алгоритмом DA-CG-LSTM, который предлагает новые подходы к анализу временных рядов и их прогнозированию. Из нее вы узнаете, как инновационные механизмы внимания и гибкость модели позволяют улучшить точность прогнозов.
Парный трейдинг: Алготорговля с автооптимизацией на разнице Z-оценки Парный трейдинг: Алготорговля с автооптимизацией на разнице Z-оценки
В этой статье разберем, что такое парный трейдинг и как происходит торговля на корреляциях. Также создадим советник для автоматизации парного трейдинга и добавим возможность автоматической оптимизации такого торгового алгоритма на исторических данных. Кроме того, в рамках проекта узнаем, как рассчитывать расхождения двух пар с помощью z-оценки.
Разрабатываем мультивалютный советник (Часть 26): Информер для торговых инструментов Разрабатываем мультивалютный советник (Часть 26): Информер для торговых инструментов
Прежде, чем двигаться дальше в разработке мультивалютных советников, попробуем переключиться на создание нового проекта, использующего разработанную библиотеку. На этом примере выявим, как лучше организовать хранение исходного кода, и как нам может помочь использование нового репозитория кода от MetaQuotes.
Классы таблицы и заголовка на базе модели таблицы в MQL5: Применение концепции MVC Классы таблицы и заголовка на базе модели таблицы в MQL5: Применение концепции MVC
Это вторая часть статьи, посвященной реализации модели таблицы в MQL5 с использованием архитектурной парадигмы MVC (Model-View-Controller). В статье рассматривается разработка классов таблицы и её заголовка, основанных на ранее созданной модели таблицы. Разработанные классы станут основой для дальнейшей реализации компонентов представления (View) и управления (Controller), которые будут рассмотрены в следующих статьях.