English 中文 Español Deutsch 日本語
preview
Анализ нескольких символов с помощью Python и MQL5 (Часть 3): Треугольные курсы валют

Анализ нескольких символов с помощью Python и MQL5 (Часть 3): Треугольные курсы валют

MetaTrader 5Примеры |
708 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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

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

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

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

Около 90% мировых товаров оцениваются в долларах США. Однако некоторые товары торгуются так часто, что их часто котируют в нескольких валютах одновременно. 

Драгоценные металлы, такие как серебро, обычно котируются одновременно в американских долларах и евро. В этой статье в качестве примера мы рассмотрим серебро. Вы можете обнаружить, что ваш брокер предлагает котировки цен на серебро в долларах (XAGUSD) и в евро (XAGEUR) одновременно.

Если мы хотим торговать XAGUSD и знаем, что он принципиально связан с XAGEUR, как мы можем использовать это понимание в своих интересах, чтобы принимать или отклонять предлагаемые нам торговые возможности, не дожидаясь подтверждения бесконечно?

Принимая во внимание преобладающий обменный курс между евро и долларом (EURUSD), можно сформировать треугольную торговую стратегию, которая находит предсказуемые закономерности, формирующиеся между ценой серебра в долларах, евро и справедливым обменным курсом евро к доллару (EURUSD). Наша цель — предоставить читателю торговую стратегию, устойчивую к шуму и помогающую выявить "скрытые" рыночные настроения на основе обычных рыночных котировок, полученных в терминале MetaTrader 5. 


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

Прежде чем углубляться в детали нашей торговой стратегии, рассмотрим понятия базовой и котируемой валюты в торгуемой паре. Если в качестве примера взять EURUSD, то базовой будет EUR (евро). По мере того как обменный курс, отображаемый на графике, удаляется от 0, стоимость базовой валюты увеличивается. Таким образом, если мы смотрим на график обменного курса EURUSD, и график растет, это сигнализирует о том, что нам нужно продать больше американских долларов, чтобы заработать 1 евро на спотовом рынке Форекс.

Рис. 1. Разница между базовой и котируемой валютой в паре

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

Рис. 2. Котируемая валюта

Мы рассматриваем 3 разных рынка и намерены торговать только на 1. Наша цель — XAGUSD. Если мы хотим понять, в каком направлении может двигаться XAGUSD, начнем с проверки курса EURUSD.

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

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

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

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

Рис. 3. Визуализация наших правил для открытия короткой позиции по XAUUSD

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

Рис. 4. Визуализация торговых правил для открытия длинной позиции по XAGUSD

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


Обзор периода тестирования на истории

Мы будем тестировать нашу стратегию с первого ноября 2023 года по первое января 2025 года. Оставшиеся у нас данные с 1 ноября 2022 года по конец октября 2023 года будут использованы для обучения нашего приложения. В будущих версиях статьи мы заменим нашу простую модель взаимосвязанной природы рынков на модели, которые наш компьютер может изучать самостоятельно. Поэтому, хотя мы не будем использовать раздел обучения в этой статье, мы воспользуемся им в будущих обсуждениях.

Скриншот 2

Рис. 5. Период тестирования на истории


Начало работы в MQL5

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

//+------------------------------------------------------------------+
//|                                               Baseline Model.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System Constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL_ONE   "XAGUSD"                                                       //--- Our primary   symbol, the price of Silver in USD
#define SYMBOL_TWO   "XAGEUR"                                                       //--- Our secondary symbol, the price of Silver in EUR
#define SYMBOL_THREE "EURUSD"                                                       //--- Our EURUSD exchange rate.
#define FETCH        24                                                             //--- How many bars of data should we fetch?
#define TF_1         PERIOD_H1                                                      //--- Our intended time frame
#define VOLUME       SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10            //--- Our trading volume

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

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
vector eurusd,xagusd,xageur;
double eurusd_growth,xagusd_growth,xageur_growth,bid,ask;
double sl_width = 3e2 * _Point;

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

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

Язык MQL5 создан, чтобы помочь нам эффективно торговать. Каждое событие, которое, по вашему мнению, может произойти на рынке, сопоставляется с определенными событиями. Получение новых цен — это событие. При регистрации события вызывается обработчик событий OnTikc(). Функции внутри этого обработчика событий будут выполнены.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our technical indicators
   setup();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- New prices have been quoted
   new_quotes_received();
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Custom functions                                                 |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Updates system variables accordingly                             |
//+------------------------------------------------------------------+
void new_quotes_received(void)
  {
   static datetime time_stamp;
   datetime time = iTime(SYMBOL_ONE,TF_1,0);

   if(time_stamp != time)
     {
      time_stamp = time;
      update();
     }
  }

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

//+------------------------------------------------------------------+
//| Setup our technical indicators and select the symbols we need    |
//+------------------------------------------------------------------+
void setup(void)
  {
//--- Select the symbols we need
   SymbolSelect(SYMBOL_ONE,true);
   SymbolSelect(SYMBOL_TWO,true);
   SymbolSelect(SYMBOL_THREE,true);
  }

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

//+------------------------------------------------------------------+
//| Update our system setup                                          |
//+------------------------------------------------------------------+
void update(void)
  {
//--- Fetch updated prices
   xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH);
   xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH);
   eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH);

//--- Calculate the growth in market prices
   eurusd_growth = eurusd[0] / eurusd[FETCH - 1];
   xageur_growth = xageur[0] / xageur[FETCH - 1];
   xagusd_growth = xagusd[0] / xagusd[FETCH - 1];

//--- Update system variables
   SymbolSelect(SYMBOL_ONE,true);

   bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID);
   ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK);

//--- Check if we need to setup a new position
   if(PositionsTotal() == 0)
      find_setup();

//--- Check if we need to manage our positions
   if(PositionsTotal() > 0)
      manage_setup();

//--- Give feedback on the market growth
   Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth);
  }

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

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {
   //--- Check if the current market setup matches our expectations for selling
   if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1))
     {
      Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),"");
     }

   //--- Check if the current market setup matches our expectations for buying
   if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1))
     {
      Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),"");
     }
  }

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

//+------------------------------------------------------------------+
//| Manage setup                                                     |
//+------------------------------------------------------------------+
void manage_setup(void)
  {
   //--- Select our open position
   if(PositionSelect(SYMBOL_ONE))
     {
      double current_sl = PositionGetDouble(POSITION_SL);
      double current_tp = PositionGetDouble(POSITION_TP);

      //--- Buy setup
      if(current_sl < current_tp)
        {
         if((bid - sl_width) > current_sl)
            Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width));
        }

      //--- Sell setup
      if(current_sl > current_tp)
        {
         if((ask + sl_width) < current_sl)
            Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width));
        }
     }
  }
Наконец, мы отменим определения системных констант, которые мы определили ранее. 
//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef TF_1
#undef SYMBOL_ONE
#undef SYMBOL_TWO
#undef SYMBOL_THREE
#undef VOLUME
#undef FETCH

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

//+------------------------------------------------------------------+
//|                                               Baseline Model.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System Constants                                                 |
//+------------------------------------------------------------------+
#define SYMBOL_ONE   "XAGUSD"                                                       //--- Our primary   symbol, the price of Silver in USD
#define SYMBOL_TWO   "XAGEUR"                                                       //--- Our secondary symbol, the price of Silver in EUR
#define SYMBOL_THREE "EURUSD"                                                       //--- Our EURUSD exchange rate.
#define FETCH        24                                                             //--- How many bars of data should we fetch?
#define TF_1         PERIOD_H1                                                      //--- Our intended time frame
#define VOLUME       SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10            //--- Our trading volume

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
vector eurusd,xagusd,xageur;
double eurusd_growth,xagusd_growth,xageur_growth,bid,ask;
double sl_width = 3e2 * _Point;

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our technical indicators
   setup();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- New prices have been quoted
   new_quotes_received();
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Custom functions                                                 |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Updates system variables accordingly                             |
//+------------------------------------------------------------------+
void new_quotes_received(void)
  {
   static datetime time_stamp;
   datetime time = iTime(SYMBOL_ONE,TF_1,0);

   if(time_stamp != time)
     {
      time_stamp = time;
      update();
     }
  }

//+------------------------------------------------------------------+
//| Setup our technical indicators and select the symbols we need    |
//+------------------------------------------------------------------+
void setup(void)
  {
//--- Select the symbols we need
   SymbolSelect(SYMBOL_ONE,true);
   SymbolSelect(SYMBOL_TWO,true);
   SymbolSelect(SYMBOL_THREE,true);
  }

//+------------------------------------------------------------------+
//| Update our system setup                                          |
//+------------------------------------------------------------------+
void update(void)
  {
//--- Fetch updated prices
   xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH);
   xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH);
   eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH);

//--- Calculate the growth in market prices
   eurusd_growth = eurusd[0] / eurusd[FETCH - 1];
   xageur_growth = xageur[0] / xageur[FETCH - 1];
   xagusd_growth = xagusd[0] / xagusd[FETCH - 1];

//--- Update system variables
   SymbolSelect(SYMBOL_ONE,true);

   bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID);
   ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK);

//--- Check if we need to setup a new position
   if(PositionsTotal() == 0)
      find_setup();

//--- Check if we need to manage our positions
   if(PositionsTotal() > 0)
      manage_setup();

//--- Give feedback on the market growth
   Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth);
  }

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {
  
   //--- Check if the current market setup matches our expectations for selling
   if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1))
     {
      Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),"");
     }

   //--- Check if the current market setup matches our expectations for buying
   if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1))
     {
      Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),"");
     }
  }

//+------------------------------------------------------------------+
//| Manage setup                                                     |
//+------------------------------------------------------------------+
void manage_setup(void)
  {
   //--- Select our open position
   if(PositionSelect(SYMBOL_ONE))
     {
      double current_sl = PositionGetDouble(POSITION_SL);
      double current_tp = PositionGetDouble(POSITION_TP);

      //--- Buy setup
      if(current_sl < current_tp)
        {
         if((bid - sl_width) > current_sl)
            Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width));
        }

      //--- Sell setup
      if(current_sl > current_tp)
        {
         if((ask + sl_width) < current_sl)
            Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width));
        }
     }
  }

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef TF_1
#undef SYMBOL_ONE
#undef SYMBOL_TWO
#undef SYMBOL_THREE
#undef VOLUME
#undef FETCH

Как я уже говорил, наше тестирование на истории будет проводиться с первого ноября 2023 года по январь 2025 года. Для проведения нашего теста мы будем использовать таймфрейм H1. Мы надеемся, что H1 предоставит нам больше торговых возможностей, чем более крупные таймфреймы, такие как Daily, и при этом не будет сбивать нас с толку таким количеством шума, когда мы торгуем даже на меньших таймфреймах, таких как M1.

Рис. 6. Даты, которые мы будем использовать для тестирования нашей стратегии XAGUSD

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

Рис. 7. Наиболее реалистичные торговые условия с использованием реальных тиков

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

Рис. 8. Кривая эквити, полученная нашей торговой стратегией

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

Рис. 9. Подробный анализ эффективности нашей торговой стратегии

Улучшение первоначальных показателей

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

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

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

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- File name
string file_name = "XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv";

//--- Amount of data requested
input int size = 3000;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close","XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close","EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close","Open Squared","High Squared","Low Squared","Close Squared","Open Cubed","High Cubed","Low Cubed","Close Cubed","Open Squre Root","High Square Root","Low Square Root","Close Square Root","Open Growth","High Growth","Low Grwoth","Close Growth","O / H","O / L","O / C","H / L","Log Open Growth","Log High Grwoth","Log Low Growth","Log Close Grwoth","Sin H / L","Cos O / C");
        }

      else
        {
         FileWrite(file_handle,
                   iTime("XAGUSD",PERIOD_CURRENT,i),
                   iOpen("XAGUSD",PERIOD_CURRENT,i), 
                   iHigh("XAGUSD",PERIOD_CURRENT,i),
                   iLow("XAGUSD",PERIOD_CURRENT,i),
                   iClose("XAGUSD",PERIOD_CURRENT,i),
                   iOpen("XAGEUR",PERIOD_CURRENT,i), 
                   iHigh("XAGEUR",PERIOD_CURRENT,i),
                   iLow("XAGEUR",PERIOD_CURRENT,i),
                   iClose("XAGEUR",PERIOD_CURRENT,i),
                   iOpen("EURUSD",PERIOD_CURRENT,i), 
                   iHigh("EURUSD",PERIOD_CURRENT,i),
                   iLow("EURUSD",PERIOD_CURRENT,i),
                   iClose("EURUSD",PERIOD_CURRENT,i),
                   MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),2),
                   MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),2),
                   MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),2),
                   MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),2),
                   MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),3),
                   MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),3),
                   MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),3),
                   MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),3),
                   MathSqrt(iOpen("XAGUSD",PERIOD_CURRENT,i)),
                   MathSqrt(iHigh("XAGUSD",PERIOD_CURRENT,i)),
                   MathSqrt(iLow("XAGUSD",PERIOD_CURRENT,i)),
                   MathSqrt(iClose("XAGUSD",PERIOD_CURRENT,i)),
                   (iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iOpen("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iOpen("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)),
                   (iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)),
                   MathLog10(iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)),
                   MathLog10(iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)),
                   MathLog10(iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)),
                   MathLog10(iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)),
                   (MathSin(iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i))),
                   (MathCos(iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i)))
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Анализ данных в Python

После сбора обучающих данных мы готовы приступить к построению статистической модели данных. Сначала импортируем необходимые нам библиотеки Python.

#Import libraries we need
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

Теперь маркируем данные. Не забывайте, что мы торгуем на таймфрейме H1, поэтому давайте установим в качестве метки изменение уровней цен за 24 часа (1 торговый день).

#Clean up the data
LOOK_AHEAD = 24
data = pd.read_csv("../XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv")
data["Target"] = data["XAGUSD Close"].shift(-LOOK_AHEAD) - data["XAGUSD Close"]
data.dropna(inplace=True)
data.reset_index(inplace=True,drop=True)

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

Поэтому мы удалим все рыночные данные, которые пересекаются с предполагаемым периодом тестирования на истории. Как вы помните, на рис. 6 период нашего тестирования на истории явно начинается с 1 ноября 2023 года, а на рис. 10 наши тренировочные данные заканчиваются 31 октября 2023 года.

#Drop the dates corresponding to our backtest
_    = data.iloc[-((24 * 365) - 918):,:]
#Keep the dates before our backtest
data = data.iloc[:-((24 * 365) - 918),:]
data

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

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

plt.title("Comparing XAGUSD & XAGEUR Growth")
plt.plot((data['XAGUSD Close'] / data.loc[0,"XAGUSD Close"]) / (data['XAGUSD Close'].max() - data['XAGUSD Close'].min()),color="red")
plt.plot((data['XAGEUR Close'] / data.loc[0,"XAGEUR Close"]) / (data['XAGEUR Close'].max() - data['XAGEUR Close'].min()),color="green")
plt.ylabel("Commodity Growth")
plt.xlabel("Time")
plt.legend(["XAGUSD","XAGEUR"])
plt.grid()

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

Обозначим наши входные данные и цель. 

X = data.iloc[:,1:-1].columns
y = "Target"

Подготовимся к нанесению дерева градиентного бустинга (Gradient Boosting Tree) на наши данные и его экспорту в формат ONNX. Деревья градиентного бустинга известны своей способностью обнаруживать взаимодействия, происходящие в заданном наборе данных. Мы надеемся воспользоваться их мощными возможностями обнаружения закономерностей для улучшения нашей торговой стратегии.

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorTypea
from sklearn.ensemble import GradientBoostingRegressor

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

Начнем с построения модели рынка XAGUSD.

model = GradientBoostingRegressor()
model.fit(data.loc[:,["XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close"]],data.loc[:,y])
initial_types = [("float_input",FloatTensorType([1,4]))]
xagusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(xagusd_model_proto,"../XAGUSD State Model.onnx")

Во-вторых, мы последуем за рынком XAGEUR.

model = GradientBoostingRegressor()
model.fit(data.loc[:,["XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close"]],data.loc[:,"XAGEUR Target"])
initial_types = [("float_input",FloatTensorType([1,4]))]
xageur_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(xageur_model_proto,"../XAGEUR State Model.onnx")

И наконец, мы экспортируем нашу статистическую модель рынка EURUSD.

model = GradientBoostingRegressor()
model.fit(data.loc[:,["EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close"]],data.loc[:,"EURUSD Target"])
initial_types = [("float_input",FloatTensorType([1,4]))]
eurusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12)
onnx.save(eurusd_model_proto,"../EURUSD State Model.onnx")

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


Реализация улучшений на MQL5

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

#define XAGUSD_MA_PERIOD 8

Загрузите только что созданные нами модели ONNX в качестве системных ресурсов.

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\XAGUSD State Model.onnx" as  uchar xagusd_onnx_buffer[]
#resource "\\Files\\XAGEUR State Model.onnx" as  uchar xageur_onnx_buffer[]
#resource "\\Files\\EURUSD State Model.onnx" as  uchar eurusd_onnx_buffer[]

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

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
vector  eurusd,xagusd,xageur;
double  eurusd_growth,xagusd_growth,xageur_growth,bid,ask;
double  sl_width = 3e2 * _Point;
int     xagusd_f_ma_handler,xagusd_s_ma_handler;
double  xagusd_f[],xagusd_s[];
vectorf model_output = vectorf::Zeros(1);
long    onnx_model;
vectorf xageur_model_output = vectorf::Zeros(1);
long    xageur_onnx_model;
vectorf eurusd_model_output = vectorf::Zeros(1);
long    eurusd_onnx_model;

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   OnnxRelease(onnx_model);
   OnnxRelease(xageur_onnx_model);
   OnnxRelease(eurusd_onnx_model);
   IndicatorRelease(xagusd_f_ma_handler);
   IndicatorRelease(xagusd_s_ma_handler);
   Print("System deinitialized");
  }

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

//+------------------------------------------------------------------+
//| Setup our technical indicators and select the symbols we need    |
//+------------------------------------------------------------------+
bool setup(void)
  {
//--- Select the symbols we need
   SymbolSelect(SYMBOL_ONE,true);
   SymbolSelect(SYMBOL_TWO,true);
   SymbolSelect(SYMBOL_THREE,true);

//--- Setup the moving averages
   xagusd_f_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_OPEN);
   xagusd_s_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_CLOSE);

   if((xagusd_f_ma_handler == INVALID_HANDLE) || (xagusd_s_ma_handler == INVALID_HANDLE))
     {
      Comment("Failed to load our technical indicators correctly. ", GetLastError());
      return(false);
     }

//--- Setup our statistical models
   onnx_model        = OnnxCreateFromBuffer(xagusd_onnx_buffer,ONNX_DEFAULT);
   xageur_onnx_model = OnnxCreateFromBuffer(xageur_onnx_buffer,ONNX_DEFAULT);
   eurusd_onnx_model = OnnxCreateFromBuffer(eurusd_onnx_buffer,ONNX_DEFAULT);

   if(onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create our XAGUSD ONNX model correctly. ",GetLastError());
      return(false);
     }

   if(xageur_onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create our XAGEUR ONNX model correctly. ",GetLastError());
      return(false);
     }

   if(eurusd_onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create our EURUSD ONNX model correctly. ",GetLastError());
      return(false);
     }

   ulong input_shape[] = {1,4};
   ulong output_shape[] = {1,1};

   if(!(OnnxSetInputShape(onnx_model,0,input_shape)))
     {
      Comment("Failed to specify XAGUSD model input shape. ",GetLastError());
      return(false);
     }


   if(!(OnnxSetInputShape(xageur_onnx_model,0,input_shape)))
     {
      Comment("Failed to specify XAGEUR model input shape. ",GetLastError());
      return(false);
     }


   if(!(OnnxSetInputShape(eurusd_onnx_model,0,input_shape)))
     {
      Comment("Failed to specify EURUSD model input shape. ",GetLastError());
      return(false);
     }

   if(!(OnnxSetOutputShape(onnx_model,0,output_shape)))
     {
      Comment("Failed to specify XAGUSD model output shape. ",GetLastError());
      return(false);
     }

   if(!(OnnxSetOutputShape(xageur_onnx_model,0,output_shape)))
     {
      Comment("Failed to specify XAGEUR model output shape. ",GetLastError());
      return(false);
     }

   if(!(OnnxSetOutputShape(eurusd_onnx_model,0,output_shape)))
     {
      Comment("Failed to specify EURUSD model output shape. ",GetLastError());
      return(false);
     }

   Print("System initialized succefully");

//--- If we have gotten this far, everything went fine.
   return(true);
  }

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

//+------------------------------------------------------------------+
//| Fetch a prediction from our model                                |
//+------------------------------------------------------------------+
void model_predict(void)
  {
   vectorf model_inputs =  { (float) iOpen(SYMBOL_ONE,TF_1,1), (float) iClose(SYMBOL_ONE,TF_1,1)};
   OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output);
   Print(StringFormat("Model forecast: %d",model_output));
  }

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

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

//+------------------------------------------------------------------+
//| Find setup                                                       |
//+------------------------------------------------------------------+
void find_setup(void)
  {
   model_predict();

//--- Check if the current market setup matches our expectations for selling
   if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth <  1))
     {
      if(xagusd_s[0] < xagusd_f[0])
        {
         if(model_output[0] < 0)
           {
            //--- If all our systems align, we have a high probability trade setup
            Trade.Sell(VOLUME * 2,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),"");
           }
         //--- Otherwise, we should trade conservatively
         Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),"");
        }
     }

//--- Check if the current market setup matches our expectations for buying
   if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1))
     {
      if(xagusd_s[0] > xagusd_f[0])
        {
         if(model_output[0] > 0)
           {
            Trade.Buy(VOLUME * 2,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),"");
           }

         Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),"");
        }
     }
  }

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

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

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

Рис. 13. В идеале эти настройки должны быть одинаковыми для обоих тестов

Давайте теперь проанализируем полученные нами результаты. В нашем первоначальном тестировании на исторических данных мы продемонстрировали коэффициент Шарпа 0,14, тогда как наша пересмотренная стратегия демонстрирует коэффициент Шарпа 1,85. Это существенное улучшение нашего коэффициента Шарпа, а это значит, что нам удалось фактически стать более прибыльными, при этом ответственно принимая на себя дополнительные риски. Низкие коэффициенты Шарпа связаны с высокой дисперсией при низкой доходности. 

Более того, наш средний убыток снизился примерно со USD 115 за сделку до USD 109 за сделку, в то время как наша средняя прибыль, с другой стороны, выросла в среднем с USD 188 до USD 213. Для нас это хороший знак. Наша общая прибыль также выросла с USD 395 в первой итерации стратегии до USD 1 449 в текущей итерации. И все это при меньшем количестве сделок, чем в нашей вручную настроенной версии стратегии. 

Рис. 14. Подробный обзор исторической эффективности нашей торговой стратегии на рынке XAGUSD

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

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

Заключение

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

Имя файла Описание
Baseline_Model Первоначальная версия треугольной торговой стратегии.
Second Version Переработанная и более прибыльная версия торговой стратегии.
EURUSD State Model Статистическая модель рынка EURUSD.
XAGEUR State Model Статистическая модель рынка XAGEUR.
XAGUSD State Model Статистическая модель рынка XAGUSD.
Triangular Exchange Rates Jupyter notebook, который мы использовали для анализа наших рыночных данных и построения наших статистических моделей рынка.

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

Прикрепленные файлы |
Baseline_Model.mq5 (7.05 KB)
Second_Version.mq5 (12.44 KB)
Знакомство с языком MQL5 (Часть 18): Введение в паттерн "Волны Вульфа" Знакомство с языком MQL5 (Часть 18): Введение в паттерн "Волны Вульфа"
В этой статье подробно объясняется паттерн волн Вульфа – как медвежьи, так и бычьи его вариации. В статье также проводится пошаговый разбор логики, используемой для выявления действительных сетапов на покупку и продажу на основе этого продвинутого графического паттерна.
Квантовые вычисления и градиентный бустинг в торговле EUR/USD Квантовые вычисления и градиентный бустинг в торговле EUR/USD
Статья описывает практическую реализацию гибридной системы алгоритмического трейдинга, объединяющей квантовые вычисления (IBM Qiskit) и градиентный бустинг (CatBoost) для предсказания движения EUR/USD на часовом таймфрейме. Система извлекает четыре уникальных квантовых признака из вероятностного распределения по 256 состояниям через восемь кубитов, которые в комбинации с классическими индикаторами и дельта-кодированием временных категорий достигают точности 62% на 15,000 свечах.
Нейросети в трейдинге: Двусторонняя адаптивная временная корреляция (Основные компоненты) Нейросети в трейдинге: Двусторонняя адаптивная временная корреляция (Основные компоненты)
В этой статье мы продолжаем реализацию фреймворка BAT средствами MQL5, показывая, как двунаправленная корреляция и модуль SATMA позволяют анализировать динамику рынка в контексте текущего состояния. Представлены ключевые архитектурных решения, позволяющие адаптировать фреймворк к анализу финансовых данных.
Знакомство с языком MQL5 (Часть 17): Создание советников для разворотов тренда Знакомство с языком MQL5 (Часть 17): Создание советников для разворотов тренда
Эта статья обучает новичков тому, как создать советник на языке MQL5, который торгует на основе распознавания графических паттернов с использованием пробоев трендовых линий и разворотов. Изучив, как динамически извлекать значения трендовой линии и сравнивать их с ценовым действием, читатели смогут разрабатывать советники, способные выявлять графические паттерны, такие как восходящие и нисходящие трендовые линии, каналы, клинья, треугольники и многие другие, и торговать по ним.