English Español Deutsch 日本語 Português
preview
Переосмысливаем классические стратегии: Нефть

Переосмысливаем классические стратегии: Нефть

MetaTrader 5Примеры | 7 октября 2024, 12:58
189 2
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Введение

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

В международной нефтяной торговле доминируют две марки: североамериканская West Texas Intermediate (WTI) и общемировая марка Brent.

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

Начнем наше обсуждение с выделения различий между двумя упомянутыми выше марками нефти. Далее мы начнем визуализировать спред Brent-WTI на MQL5 и обсудим классическую стратегию торговли спредом. Это позволит нам понять, как можно использовать машинное обучение с учителем на спреде между ценами на нефть марки West Texas Intermediate и Brent, чтобы потенциально выявить опережающие индикаторы изменения цен. Прочитав эту статью, вы будете иметь четкое представление:

  • О разнице между марками нефти Brent и WTI и о важности их различения.
  • Об использовании матричных и векторных функций MQL5 для создания компактных моделей машинного обучения, которые легко поддерживать и реализовывать с нуля.
  • Об использовании псевдообратного подхода (pseudo inverse technique) для поиска решения методом наименьших квадратов с целью прогнозирования будущей цены Brent, используя спред WTI-Brent.

Марка Brent

Сырая нефть, добытая из недр земли, представляет собой примеси кислорода, углерода, водорода и серы. Brent — это классификация смесей сырой нефти, которые считаются легкими и малосернистыми. Чтобы считаться малосернистой, смесь должна иметь низкую концентрацию примесей серы. Легкой она называется, потому что имеет низкую плотность. Эти свойства желательны, поскольку они говорят нам о том, что смесь будет легко очищаться. Brent считается нефтью более низкого качества, чем WTI. Нефть марки Brent в основном добывается в Северном море. После добычи ее легко хранить в бочках на борту больших нефтяных танкеров. Это дает Brent явное преимущество перед WTI: она очень доступна. В настоящее время нефть марки Brent торгуется с премией к нефти марки WTI.

Цена марки Brent

Рис. 1. Цена нефти марки Brent в MQL5

Марка West Texas Intermediate (WTI)

West Texas Intermediate (WTI) — это классификация, присваиваемая определенной смеси сырой нефти. Она должна быть легкой и малосернистой. Нефть WTI добывается в разных частях США, но в основном в Техасе. Она содержит меньше серы и легче, чем Brent, а значит, ее легче перерабатывать в готовую продукцию. Исторически ее добывали в частях США, не имеющих выхода к морю, и поэтому она была гораздо менее доступна, чем Brent. Однако благодаря масштабным инвестициям в добывающую инфраструктуру побережья Мексиканского залива и отмене запрета на экспорт нефти в 2015 году нефть марки WTI сейчас стала более доступной, чем когда-либо.

West Texas Intermediate

Рис. 2. Цена нефти марки WTI в MQL5

Начало работы: визуализируем спред

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

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

После того, как мы определили правильное количество баров, мы можем построить график спреда.

//+------------------------------------------------------------------+
//|                                             Brent-WTI Spread.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Graphics\Graphic.mqh>
//Set this value between 0 and 1 to control how much data is used
double consumption = 1.0;
int brent_bars = (int) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
int wti_bars = (int) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);
//We want to know which symbol has the least number of bars.
int max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;

//+------------------------------------------------------------------+
//|This event handler is only triggered when the script launches     |
//+------------------------------------------------------------------+
void OnStart()
  {
   CGraphic graphic;
   double from = 0;
   double to  = max_bars;
   double step = 1;
   graphic.Create(0,"G",0,0,0,600,200);
   CColorGenerator generator;
   uint spread = generator.Next();
   CCurve *curve = graphic.CurveAdd(SpreadFunction,from,to,step,spread,CURVE_LINES,"Blue");
   curve.Name("Spread");
   graphic.XAxis().Name("Time");
   graphic.XAxis().NameSize(12);
   graphic.YAxis().Name("Brent-WTI Spread");
   graphic.YAxis().NameSize(12);
   graphic.CurvePlotAll();
   graphic.Update();
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|This function returns the Brent-WTI spread                        |
//+------------------------------------------------------------------+
double SpreadFunction(double x)
  {
   return(iClose("UK Brent Oil",PERIOD_CURRENT,(max_bars - x)) - iClose("WTI_OIL",PERIOD_CURRENT,(max_bars - x)));
  }
//+------------------------------------------------------------------+

Спред Brent WTI

Рис. 3. Визуализация спреда Brent-WTI в MQL5


Обзор торговой стратегии: Применение контролируемого машинного обучения

Предпосылка классической стратегии в отношении сырой нефти заключалась в том, что в долгосрочной перспективе ценовое равновесие всегда будет восстановлено. Классическая стратегия торговли нефтяным спредом предполагает, что мы начинаем с наблюдения за текущим спредом между Brent и WTI. Если спред выше своего базового уровня (например, базовым уровнем может быть 20-дневная скользящая средняя), то мы можем сделать вывод, что спред вернется к своему среднему значению в ближайшем будущем. Поэтому, если бы цены на нефть марки Brent росли, мы бы продавали. И наоборот, если бы цены на нефть марки Brent падали, мы бы ее покупали. 

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

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

A символизирует имеющиеся у нас исторические данные о ценах на нефть марки Brent. Мы будем использовать цену закрытия, спред и точку пересечения, имеющую постоянное значение, равное единице. Затем мы построим отдельный вектор-столбец x, который будет иметь 1 коэффициент для каждого столбца в A. Это значение будет рассчитано непосредственно из рыночных данных и будет использоваться нашей моделью для прогнозирования будущей цены.

Определение А и х

Рис. 4. Формулировка задачи наименьших квадратов

После создания нашей входной матрицы А, нам нужно знать, какие цены закрытия Brent были связаны с каждым из входных данных в А. Сохраним выходную цену в векторе у. Наша цель — найти способ отображения матрицы входных данных А к выходному вектору данных у, при этом аппроксимируя минимально возможную ошибку среди всех имеющихся у нас обучающих наблюдений. Ответ на эту задачу называется решением по методу наименьших квадратов.

Введение в метод наименьших квадратов

Рис. 5. Выходной вектор y

Существует множество допустимых решений для сценариев задач наименьших квадратов. Ниже мы рассмотрим метод, известный как метод псевдообратного преобразования (pseudo-inverse technique). Это отличительная концепция линейной алгебры, которая позволяет нам преобразовывать неквадратные матрицы. Мы применим псевдообратный метод для поиска значений коэффициентов для столбца x, которые отображают A на  с минимально возможной погрешностью. 

Инверсия Мура-Пенроуза

Рис. 6. Псевдообратное решение

Два уравнения выше сначала говорят нам, что мы ищем значение x, которое минимизирует ошибку между нашим прогнозом A * x и фактической ценой закрытия Brent y. Обратите внимание на двойные вертикальные линии вокруг Ax-y. Эти двойные вертикальные линии представляют норму L2. Когда мы имеем дело с физическими объектами в реальном мире, мы можем спросить: "Насколько объект велик?". Когда же мы хотим узнать размер вектора или матрицы, мы спрашиваем его норму. Существуют разные способы вычисления нормы, чаще всего вы будете сталкиваться с нормой L1 или L2. В нашем обсуждении мы будем рассматривать только норму L2.

Норма L2 вычисляется путем возведения в квадрат каждого объекта в векторе, суммирования всех квадратов значений и последующего вычисления квадратного корня из суммы. Ее также называют евклидовой нормой. Проще говоря, мы бы сказали: "Мы ищем значения x, которые уменьшают размер всех ошибок, которые допускает наша модель", а на более техническом языке мы бы сказали: "Находим оптимальные значения x, которые минимизируют норму L2 остатков".

Значение x, удовлетворяющее нашим ограничениям, обозначается как x*. Чтобы найти x*, мы вычисляем скалярное произведение псевдообратного A и y. Крайне маловероятно, что вам когда-либо понадобится реализовать псевдообратную функцию самостоятельно, если только это не будет упражнением по линейной алгебре. В противном случае мы будем полагаться на встроенную функцию в MQL5.

//+------------------------------------------------------------------+
//|Demonstrating the pseudo-inverse solution in action.              |                                                                |
//+------------------------------------------------------------------+
void OnStart()
  {
//Training and test data
   matrix A; //A is the input data. look at the figure above if you need a reminder.
   matrix y,x; //y is the output data, x is the coefficients.
   A.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_OHLC,20,1000);
   y.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,1,1000);
   A.Reshape(1000,4);
   y.Reshape(1000,1);
   Print("Attempting Psuedoinverse Decomposition");
   Print("Attempting to calculate the Pseudoinverse Coefficients: ");
   x = A.PInv().MatMul(y);
   Print("Coefficients: ");
   Print("Open: ",x[0][0],"\nHigh: ",x[1][0],"\nLow: ",x[3][0],"\nClose: ",x[3][0]);
  }
//+------------------------------------------------------------------+

Псевдообратный подход

Рис. 7. Пример реализации псевдообратного подхода

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

  • Symbol name - имя символа, которым мы хотим торговать.
  • Timeframe - таймфрейм, соответствующий нашим уровням риска.
  • Rates mask - определяет, какие цены копировать, что позволяет нам выбрать, например, только цены открытия, если это необходимо.
  • From - начальная дата копирования данных, обеспечивающая промежуток между входными и выходными данными, а также то, что входные данные начинаются с более ранней даты.
  • Count - количество свечей, которые нужно скопировать.

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

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

Мы будем использовать те же шаги для разработки нашей торговой стратегии. Единственный дополнительный шаг, не продемонстрированный здесь, — это использование нашей модели для составления прогнозов. Он будет рассмотрен позже. Имея эту основу, мы готовы приступить к построению нашей торговой стратегии.

Объединяем всё вместе

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

//+------------------------------------------------------------------+
//|                                                     Brent EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//Libraries
#include  <Trade\Trade.mqh>
CTrade ExtTrade;
#include <TrailingStop\ATRTrailingStop3.mqh>
ATRTrailingStop ExtATRTrailingStop;

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

//Inputs
input double lot_multiple = 1.0;
input double profit_target = 10;
input double max_loss = 20;
input int position_size = 2;

Теперь нам нужно узнать, сколько баров доступно на каждом рынке, чтобы убедиться, что мы всегда пытаемся скопировать правильное количество баров, которые будут доступны на обоих рынках. "Правильное количество" в нашем случае — это наименьшее доступное количество баров. Мы также определили переменную consumption, поскольку она позволяет нам контролировать объем используемых данных. В приведенном ниже примере кода мы используем 1% всех доступных исторических данных.

//Set this value between 0 and 1 to control how much data is used
double consumption = 0.01;
//We want to know which symbol has the least number of bars.
double brent_bars = (double) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
double wti_bars = (double) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);

Здесь мы фактически определяем, на каком рынке доступно меньше баров, и используем это количество баров в качестве нашего лимита. Если мы пропустим этот шаг, даты между двумя рынками могут не совпадать, если только ваш брокер не гарантирует одинаковое соответствие наборов данных по историческим ценам для обоих активов. Look Ahead - это наш горизонт прогнозирования, количество шагов в будущее, на которые мы делаем прогноз.

//Select the lowest
double max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;
//How far into the future are we forecasting
double look_ahead = NormalizeDouble((max_bars / 4),0);
//How many bars should we fetch? 
int fetch = (int) (max_bars - look_ahead) - 1;

Теперь нам нужно определить переменные, описанные в нашей нотации. Я приложу копию изображения, чтобы вам не пришлось прокручивать страницу вверх. Не забывайте, что А— это матрица, в которой хранятся наши входные данные. Мы можем выбрать столько входных данных, сколько пожелаем. В этом примере я буду использовать 3 входа. x* представляет собой значение x, которое минимизирует норму L2 наших остатков.

Инверсия Мура-Пенроуза

Рис. 6. Псевдообратное решение (повтор)

//Matrix A stores our inputs. y is the output. x is the coefficients.
matrix A = matrix::Zeros(fetch,6);
matrix y = matrix::Zeros(fetch,1);
vector wti_price = vector::Zeros(fetch);
vector brent_price = vector::Zeros(fetch);
vector spread;
vector intercept = vector::Ones(fetch);
matrix x = matrix::Zeros(6,1);
double forecast = 0;
double ask = 0;
double bid = 0;
double min_volume = 0;

Определим две строковые переменные для хранения названий символов, которыми мы хотим торговать. После этого перейдем к функции OnInit. В нашем случае эта функция проста: нам просто нужно знать минимально допустимый объем торговли по Brent.

string brent = "UK Brent Oil";
string wti = "WTI_OIL";
bool model_initialized = false;
int OnInit()
  {
//Initialise trailing stops
   if(atr_multiple > 0)
      ExtATRTrailingStop.Init(atr_multiple);
   min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN);
   return(INIT_SUCCEEDED);
//---
  }

Сейчас мы работаем над функцией OnTick. Внутри тела мы сначала обновляем отслеживаемые цены bid и ask. Затем проверяем, была ли инициализирована наша модель. Если нет, она будет обучена и адаптирована. Если да, проверяем наличие открытых позиций. Если открытых позиций нет, получаем прогноз от нашей модели, а затем торгуем в направлении, которое прогнозирует наша модель. В противном случае, проверяем, не превысили ли наши позиции целевой показатель прибыли или максимальный уровень просадки.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ask = SymbolInfoDouble(brent,SYMBOL_ASK);
   bid = SymbolInfoDouble(brent,SYMBOL_BID);
   if(model_initialized)
     {
      if(PositionsTotal() == 0)
        {
         forecast = 0;
         forecast = ModelForecast();
         InterpretForecast();
        }

      else
        {
         ManageTrades();
        }
     }

   else
     {
      model_initialized = InitializeModel();
     }

  }
//+------------------------------------------------------------------+

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

void ManageTrades()
  {
   if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target)
      CloseAll();
   if(AccountInfoDouble(ACCOUNT_PROFIT) < (-1 * max_loss))
      CloseAll();
  }

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

void InterpretForecast()
  {
   if(forecast != 0)
     {
      if(forecast > iClose(_Symbol,PERIOD_CURRENT,0))
        {
         check_buy();
        }

      else
         if(forecast < iClose(_Symbol,PERIOD_CURRENT,0))
           {
            check_sell();
           }
     }
  }


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

void check_buy()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Buy(lot_multiple * min_volume,brent,ask,0,0,"BUY");
        }
     }
  }

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

void check_sell()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Sell(lot_multiple * min_volume,brent,bid,0,0,"SELL");
        }
     }
  }

Теперь определим функцию, которая закроет все имеющиеся у нас открытые позиции. Он перебирает имеющиеся у нас открытые позиции и закрывает только те позиции, которые открыты по Brent. Обратите внимание: если вы хотите торговать как Brent, так и WTI с помощью этого советника, просто удалите проверки безопасности, которые я установил, чтобы убедиться, что символом является Brent. Помните, я выбрал Brent только в демонстрационных целях. Вы можете свободно настраивать советник.

void CloseAll(void)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetSymbol(i) == brent)
           {
            ulong ticket;
            ticket = PositionGetTicket(i);
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

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

void close_buy()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_BUY)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

void close_sell()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_SELL)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

Теперь определим, как должна быть инициализирована наша модель:

  1. Убедимся, что оба символа доступны и добавлены в окно рынка.
  2. Копируем выходные данные в матрицу у (цена закрытия Brent, начиная со свечи 1).
  3. Скопируем входные данные в матрицу А (цена закрытия Brent, начиная с 1 плюс наш горизонт прогнозирования).
  4. Изменим матрицу данных А.
  5. Рассчитаем спред между Brent и WTI и добавим его к А.
  6. Добавим ряд единиц в А для перехвата.
  7. Транспонируем A и y.

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

bool InitializeModel()
  {
//Try select the symbols
   if(SymbolSelect(brent,true) && SymbolSelect(wti,true))
     {
      Print("Symbols Available. Bars: ",max_bars," Fetch: ",fetch," Look ahead: ",look_ahead);
      //Get historical data on Brent , our model output
      y.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1 + look_ahead),fetch);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Current Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      y = y.Transpose();
      //Inspect the matrices
      if((A.Cols() == 0 || y.Cols() == 0))
        {
         Print("Error occured when copying historical data");
         Print("A rows: ",A.Rows()," y rows: ",y.Rows()," A columns: ",A.Cols()," y cols: ",y.Cols());
         Print("A");
         Print(A);
         Print("y");
         Print(y);
         return(false);
        }

      else
        {
         Print("No errors occured when copying historical data");
         x = A.PInv().MatMul(y);
         Print("Finished Fitting The Model");
         Print(x);
         return(true);
        }
     }

   Print("Faield to select symbols");
   return(false);
  }

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

double ModelForecast()
  {
   if(model_initialized)
     {
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      double _forecast = (A[0][0]*x[0][0]) + (A[1][0]*x[1][0]) + (A[2][0]*x[2][0]);
      return(_forecast);
     }
   return(0);
  }

Вот что представляет собой наше приложение после объединения всех компонентов.

//+------------------------------------------------------------------+
//|                                                     Brent EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//Libraries
#include  <Trade\Trade.mqh>
CTrade ExtTrade;
#include <TrailingStop\ATRTrailingStop3.mqh>
ATRTrailingStop ExtATRTrailingStop;

//Inputs
input double atr_multiple = 5.0;
input double lot_multiple = 1.0;
input double profit_target = 10;
input double max_loss = 20;
input int position_size = 2;

//Set this value between 0 and 1 to control how much data is used
double consumption = 0.01;
//We want to know which symbol has the least number of bars.
double brent_bars = (double) NormalizeDouble((iBars("UK Brent Oil",PERIOD_CURRENT) * consumption),0);
double wti_bars = (double) NormalizeDouble((iBars("WTI_OIL",PERIOD_CURRENT) * consumption),0);
//Select the lowest
double max_bars = (brent_bars < wti_bars) ? brent_bars : wti_bars;
//How far into the future are we forecasting
double look_ahead = NormalizeDouble((max_bars / 4),0);
//How many bars should we fetch?
int fetch = (int)(max_bars - look_ahead) - 1;
//Matrix A stores our inputs. y is the output. x is the coefficients.
matrix A = matrix::Zeros(fetch,6);
matrix y = matrix::Zeros(fetch,1);
vector wti_price = vector::Zeros(fetch);
vector brent_price = vector::Zeros(fetch);
vector spread;
vector intercept = vector::Ones(fetch);
matrix x = matrix::Zeros(6,1);
double forecast = 0;
double ask = 0;
double bid = 0;
double min_volume = 0;

string brent = "UK Brent Oil";
string wti = "WTI_OIL";
bool model_initialized = false;
int OnInit()
  {
//Initialise trailing stops
   if(atr_multiple > 0)
      ExtATRTrailingStop.Init(atr_multiple);
   min_volume = SymbolInfoDouble(brent,SYMBOL_VOLUME_MIN);
   return(INIT_SUCCEEDED);
//---
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   ask = SymbolInfoDouble(brent,SYMBOL_ASK);
   bid = SymbolInfoDouble(brent,SYMBOL_BID);
   if(model_initialized)
     {
      if(PositionsTotal() == 0)
        {
         forecast = 0;
         forecast = ModelForecast();
         InterpretForecast();
        }

      else
        {
         ManageTrades();
        }
     }

   else
     {
      model_initialized = InitializeModel();
     }

  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|This function closes trades if we reach our profit or loss limit  |                                                              |
//+------------------------------------------------------------------+
void ManageTrades()
  {
   if(AccountInfoDouble(ACCOUNT_PROFIT) > profit_target)
      CloseAll();
   if(AccountInfoDouble(ACCOUNT_PROFIT) < (-1 * max_loss))
      CloseAll();
  }

//+------------------------------------------------------------------+
//|This function judges if our model is giving a long or short signal|                                                                |
//+------------------------------------------------------------------+
void InterpretForecast()
  {
   if(forecast != 0)
     {
      if(forecast > iClose(_Symbol,PERIOD_CURRENT,0))
        {
         check_buy();
        }

      else
         if(forecast < iClose(_Symbol,PERIOD_CURRENT,0))
           {
            check_sell();
           }
     }
  }

//+------------------------------------------------------------------+
//|This function checks if we can open buy positions                 |
//+------------------------------------------------------------------+
void check_buy()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Buy(lot_multiple * min_volume,brent,ask,0,0,"BUY");
        }
     }
  }

//+------------------------------------------------------------------+
//|This function checks if we can open sell positions                |
//+------------------------------------------------------------------+
void check_sell()
  {
   if(PositionsTotal() == 0)
     {
      for(int i = 0; i < position_size; i++)
        {
         ExtTrade.Sell(lot_multiple * min_volume,brent,bid,0,0,"SELL");
        }
     }
  }

//+------------------------------------------------------------------+
//|This function will close all open trades                          |
//+------------------------------------------------------------------+
void CloseAll(void)
  {
   for(int i=PositionsTotal()-1; i>=0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetSymbol(i) == brent)
           {
            ulong ticket;
            ticket = PositionGetTicket(i);
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//|This function closes any open buy trades                          |
//+------------------------------------------------------------------+
void close_buy()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_BUY)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//|This function closes any open sell trades                         |
//+------------------------------------------------------------------+
void close_sell()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal(); i++)
        {
         ticket = PositionGetTicket(i);
         type = (int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_SELL)
           {
            ExtTrade.PositionClose(ticket);
           }
        }
     }
  }


//+------------------------------------------------------------------+
//|This function initializes our model and fits it onto the data     |
//+------------------------------------------------------------------+
bool InitializeModel()
  {
//Try select the symbols
   if(SymbolSelect(brent,true) && SymbolSelect(wti,true))
     {
      Print("Symbols Available. Bars: ",max_bars," Fetch: ",fetch," Look ahead: ",look_ahead);
      //Get historical data on Brent , our model output
      y.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,1,fetch);
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1 + look_ahead),fetch);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,(1+look_ahead),fetch);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Current Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      y = y.Transpose();
      //Inspect the matrices
      if((A.Cols() == 0 || y.Cols() == 0))
        {
         Print("Error occured when copying historical data");
         Print("A rows: ",A.Rows()," y rows: ",y.Rows()," A columns: ",A.Cols()," y cols: ",y.Cols());
         Print("A");
         Print(A);
         Print("y");
         Print(y);
         return(false);
        }

      else
        {
         Print("No errors occured when copying historical data");
         x = A.PInv().MatMul(y);
         Print("Finished Fitting The Model");
         Print(x);
         return(true);
        }
     }

   Print("Faield to select symbols");
   return(false);
  }

//+------------------------------------------------------------------+
//|This function makes a prediction once our model has been trained  |
//+------------------------------------------------------------------+
double ModelForecast()
  {
   if(model_initialized)
     {
      //model input
      A.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      brent_price.CopyRates(brent,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      wti_price.CopyRates(wti,PERIOD_CURRENT,COPY_RATES_CLOSE,0,1);
      //Calculate the spread
      spread = brent_price - wti_price;
      Print("The Spread: ",spread);
      A.Reshape(3,fetch);
      //Add the spread to the input matrix
      A.Row(spread,1);
      //Add a column for the intercept
      A.Row(intercept,2);
      //Reshape the matrices
      A = A.Transpose();
      double _forecast = (A[0][0]*x[0][0]) + (A[1][0]*x[1][0]) + (A[2][0]*x[2][0]);
      return(_forecast);
     }
   return(0);
  }
//+------------------------------------------------------------------+

Теперь мы готовы провести тестирование нашего торгового алгоритма на истории с помощью встроенного тестера стратегий MetaTrader 5.

Тестирование советника

Рис. 7. Тестирование алгоритма количественной торговли на истории

Тестирование советника на истории

Рис. 8. Потенциальная доходность алгоритма при тестировании на истории

Заключение

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

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

Желаю вам мира, процветания и прибыльных сделок!


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

Прикрепленные файлы |
Brent_EA.mq5 (8.37 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
linfo2
linfo2 | 7 июн. 2024 в 22:13

Спасибо еще раз, Гамучирай, еще одна очень интересная, четко написанная и продуманная статья, я думал использовать MQL graph Module :) . Отличный материал и очень интересный ход мыслей. Чтобы помочь другим пользователям, мой брокер использует UKBENT и USWTI в качестве символов, поэтому мне нужно было изменить скрипты в соответствии с (UK Brent Oil и WTI_OIL).

Я с нетерпением жду возможности протестировать и понять это в деталях

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 8 июн. 2024 в 09:57
linfo2 #:

Спасибо еще раз, Гамучирай, еще одна очень интересная, четко написанная и продуманная статья, я думал использовать MQL graph Module :) . Отличный материал и очень интересный ход мыслей. Чтобы помочь другим пользователям, мой брокер использует UKBENT и USWTI в качестве символов, поэтому мне нужно было изменить скрипты в соответствии с (UK Brent Oil и WTI_OIL).

Я с нетерпением жду возможности протестировать и понять это в деталях

Привет, Нил, всегда приятно слышать от тебя, я рад, что смог помочь.

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

Я с нетерпением жду ваших отзывов и предложений по улучшению, если они придут вам в голову.

P.S. Побочное замечание: вы также следите за AUD/JPY? Я хочу войти в длинную позицию и сыграть на фундаментальной слабости иены.

Нейросети в трейдинге: Управляемая сегментация (Окончание) Нейросети в трейдинге: Управляемая сегментация (Окончание)
Продолжаем, начатую в предыдущей статье работу, по построению фреймворка RefMask3D средствами MQL5. Данный фреймворк разработан для всестороннего изучения мультимодального взаимодействия и анализа признаков в облаке точек, с последующей идентификацией целевого объекта на основе описания, предоставленного на естественном языке.
Разработка системы репликации (Часть 48): Концепции для понимания и осмысления Разработка системы репликации (Часть 48): Концепции для понимания и осмысления
Как насчет изучения чего-то нового? В этой статье вы узнаете, как преобразовывать скрипты в сервисы, и почему полезно это делать.
Матричная факторизация: моделирование, которое более практично Матричная факторизация: моделирование, которое более практично
Вы могли не заметить, что моделирование матриц оказалось немного странным, так как указывались не строки и столбцы, а только столбцы. Это выглядит очень странно при чтении кода, выполняющего матричные факторизации. Если вы ожидали увидеть указанные строки и столбцы, то могли бы запутаться при попытке выполнить факторизацию. Более того, данный способ моделирования матриц не самый лучший. Это связано с тем, что когда мы моделируем матрицы таким образом, то сталкиваемся с некими ограничениями, которые заставляют нас использовать другие методы или функции, которые не были бы необходимы, если бы моделирование осуществлялось более подходящим способом.
Прогнозирование валютных курсов с использованием классических методов машинного обучения: Логит и Пробит модели Прогнозирование валютных курсов с использованием классических методов машинного обучения: Логит и Пробит модели
Предпринята попытка построить торговый эксперт для предсказания котировок валютных курсов. За основу алгоритма взяты классические модели классификации — логистическая и пробит регрессия. В качестве фильтра торговых сигналов используется критерий отношения правдоподобия.