English 中文 Español Deutsch 日本語 Português
Треугольный арбитраж

Треугольный арбитраж

MetaTrader 5Трейдинг | 12 сентября 2017, 14:54
19 755 86
Alexey Oreshkin
Alexey Oreshkin

Описание идеи

Тема треугольного арбитража с завидной периодичностью поднимается на тематических форумах в Сети. Что же это такое?

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

Возьмём самый популярный пример: треугольник "евро — фунт — доллар". В валютных парах он описывается так: EURUSD + GBPUSD + EURGBP. Требуемая нейтральность заключается в попытке купить и продать одновременно одни и те же инструменты, заработав при этом профит. 

Выглядит это следующим образом. Любую пару из этого примера представляем через две остальные:

EURUSD=GBPUSD*EURGBP,

или GBPUSD=EURUSD/EURGBP,

или EURGBP=EURUSD/GBPUSD.

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

Теперь разберемся с ценами bid и ask. Порядок действий будет такой:

  1. Покупаем EURUSD, т.е. используем цену ask. На балансе у нас плюс евро и минус доллары. 
  2. Выражаем EURUSD через две другие пары.
  3. GBPUSD: евро тут нет, но есть доллар, а доллары мы должны продать. Чтобы продать доллары в GBPUSD, надо эту пару купить. Значит, используем ask. При покупке мы получим на баланс плюс фунт и минус доллар.
  4. EURGBP: евро нам надо купить, а фунт, который нам не нужен, — продать. Покупаем EURGBP, используем ask. На балансе у нас плюс евро и минус фунт. Всё сходится.

Итого имеем: (ask) EURUSD = (ask) GBPUSD * (ask) EURGBP. Мы получили необходимое равенство. Чтобы теперь на нём заработать, мы должны купить одну сторону и продать другую. Здесь возможны два варианта:

  1. Купить EURUSD дешевле, чем можем его продать, но выраженный по-другому: (ask) EURUSD < (bid) GBPUSD * (bid) EURGBP 
  2. Продать EURUSD дороже, чем можем его купить, но выраженный по-другому: (bid) EURUSD > (ask) GBPUSD * (ask) EURGBP 

Дело осталось за малым: найти такую ситуацию и заработать на ней.

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

И ещё одно важное дополнение: отслеживая эту ситуацию, мы можем искать момент для одновременной покупки и продажи. В этом случае профит будет моментальный, но такие моменты — большая редкость.
Немного чаще возникает ситуация, когда одну сторону мы можем купить дешевле, чем другую, но продать сейчас с прибылью не можем. Тогда мы ждём, когда этот дисбаланс пропадёт. Находиться в сделке нам безопасно, потому что наша позиция почти нулевая, т.е. мы вне рынка. Но надо понимать, откуда берется это "почти". Для идеального выравнивания торговых объёмов необходима точность, которая нам недоступна. Напомню, что чаще всего торговые объёмы округляются до 2 знака после запятой, а это для нашей стратегии слишком грубое округление.

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


Краткое описание робота

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

Вся эта информация хранится в массиве структур MxThree. У каждого треугольника есть поле status. Начальное его значение = 0. Если треугольник нужно открыть, то статусу присваивается значение = 1. После подтверждения того, что треугольник открылся полностью, его статус меняется на 2. Если треугольник открылся не весь или его пора закрывать, то статус меняется на 3. Как только треугольник успешно закроется, статус вновь возвращается в положение 0.

Открытие и закрытие треугольников робот записывает в лог-файл, позволяющий проверить корректность действий и восстановить историю. Имя лог-файла: Three Point Arbitrage Control YYYY.DD.MM.csv.

Для тестирования загрузите в Тестер все необходимые валютные пары. Для этого нужно перед запуском Тестера запустить робота в режиме Create file with symbols. Если этого файла не будет, то робот прогонит тест по дефолтному треугольнику EUR+GBP+USD. 


Используемые переменные

Код любого робота в моем исполнении начинается с включения заголовочного файла. В нем перечислены все инклюды, библиотеки и т.д. Этот робот — не исключение: сразу после блока описания следует строка #include "head.mqh" и т.д.:

#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>  
#include <Trade\TerminalInfo.mqh> 

#include "var.mqh"
#include "fnWarning.mqh"
#include "fnSetThree.mqh"
#include "fnSmbCheck.mqh"
#include "fnChangeThree.mqh"
#include "fnSmbLoad.mqh"
#include "fnCalcDelta.mqh"
#include "fnMagicGet.mqh"
#include "fnOpenCheck.mqh"
#include "fnCalcPL.mqh"
#include "fnCreateFileSymbols.mqh"
#include "fnControlFile.mqh"
#include "fnCloseThree.mqh"
#include "fnCloseCheck.mqh"
#include "fnCmnt.mqh"
#include "fnRestart.mqh"
#include "fnOpen.mqh"

Сейчас этот список не совсем понятен читателю, но статья написана в режиме следования за кодом, поэтому структура программы здесь не нарушается. По мере прочтения всё станет ясно. Все функции, классы, единицы кода для удобства разложены по отдельным файлам. Каждый включаемый файл, кроме стандартной библиотеки, у меня также начинается со строки #include "head.mqh". Это позволяет использовать IntelliSense во включаемых файлах и не держать в памяти названия всех необходимых сущностей.

Затем подключим файл для Тестера. Нигде в другом месте этого сделать нельзя, поэтому объявим его здесь. Эта строка нужна для загрузки символов в мультивалютный тестер:

#property tester_file FILENAME

Далее опишем используемые в программе переменные. Их описание тоже содержится в отдельном файле var.mqh:

// макросы
#define DEVIATION       3                                                                 // Максимально возможное проскальзывание
#define FILENAME        "Three Point Arbitrage.csv"                                       // Здесь хранятся символы для работы
#define FILELOG         "Three Point Arbitrage Control "                                  // Часть имени лог-файла
#define FILEOPENWRITE(nm)  FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV)  // Открытие файла для записи
#define FILEOPENREAD(nm)   FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV)   // Открытие файла для чтения
#define CF              1.2                                                               // Повышаюший коэффициент для маржи
#define MAGIC           200                                                               // Диапазон используемых мэджиков
#define MAXTIMEWAIT     3                                                                 // Максимальное время ожидания открытия треугольника в секундах

// структура для валютной пары
struct stSmb
   {
      string            name;            // Валютная пара
      int               digits;          // Количество знаков после запятой в котировке
      uchar             digits_lot;      // Количество знаков после запятой в лоте, для округления
      int               Rpoint;          // 1/point, чтобы в формулах на это значение умножать а не делить
      double            dev;             // Возможное проскальзывание. Переводим сразу в кол-во пойнтов
      double            lot;             // Объём торговли для валютной пары
      double            lot_min;         // Минимальный объём
      double            lot_max;         // Максимальный объём
      double            lot_step;        // Шаг лота
      double            contract;        // Размер контракта
      double            price;           // Цена открытия пары в треугольнике. Нужна для неттинга
      ulong             tkt;             // Тикет ордера, которым открыта сделка. Нужна только для удобства в хедж-счетах
      MqlTick           tick;            // Текущие цены пары
      double            tv;              // Текущая стоимость тика
      double            mrg;             // Текущая необходимая маржа для открытия
      double            sppoint;         // Спред в целых пунктах
      double            spcost;          // Спред в деньгах на текущий открываемый лот
      stSmb(){price=0;tkt=0;mrg=0;}   
   };

// Структура для треугольника
struct stThree
   {
      stSmb             smb1;
      stSmb             smb2;
      stSmb             smb3;
      double            lot_min;          // Минимальный объём для всего треугольника
      double            lot_max;          // Максимальный объём для всего треугольника     
      ulong             magic;            // Мэждик треугольника
      uchar             status;           // Статус треугольника. 0 - не используется. 1 - отправили на открытие. 2 - успешно открыт. 3- отправили на закрытие
      double            pl;               // Профит треугольника
      datetime          timeopen;         // Время отправки треугольника на открытие
      double            PLBuy;            // Сколько можно заработать, если купить треугольник
      double            PLSell;           // Сколько можно заработать, если продать треугольник
      double            spread;           // Стоимость суммарная всех трёх спредов (с комиссией!)
      stThree(){status=0;magic=0;}
   };

  
// Режимы работы эксперта  
enum enMode
   {
      STANDART_MODE  =  0, /*Symbols from Market Watch*/                  // Обычный режим работы. Символы из обзора рынка
      USE_FILE       =  1, /*Symbols from file*/                          // Использовать файл символов
      CREATE_FILE    =  2, /*Create file with symbols*/                   // Создать файл для тестера или для работы
      //END_ADN_CLOSE  =  3, /*Not open, wait profit, close & exit*/      // Закрыть все свои сделки и закончить работу
      //CLOSE_ONLY     =  4  /*Not open, not wait profit, close & exit*/
   };


stThree  MxThree[];           // Основной массив, где хранятся рабочие треугольники и все необходимые дополнительные данные

CTrade         ctrade;        // Класс CTrade стандартной библиотеки
CSymbolInfo    csmb;          // Класс CSymbolInfo стандартной библиотеки
CTerminalInfo  cterm;         // Класс CTerminalInfo стандартной библиотеки

int         glAccountsType=0; // Тип счёта: хеджинг или неттинг
int         glFileLog=0;      // Хэндл лог-файла


// Входные параметры

sinput      enMode      inMode=     0;          // Режим работы
input       double      inProfit=   0;          // Комиссия
input       double      inLot=      1;          // Торговый объем
input       ushort	inMaxThree= 0;          // Открыто треугольников
sinput      ulong       inMagic=    300;        // Мэджик советника
sinput      string      inCmnt=     "R ";       // Комментарий

Сначала идут дефайны, они простые и снабжены комментариями. Думаю, с их пониманием проблем не будет.

Затем идут две структуры stSmb и stThree. Логика у них следующая: любой треугольник состоит из трёх валютных пар. Поэтому, описав одну из них однажды и использовав ее три раза, мы получим треугольник. stSmb — это и есть структура, описывающая валютную пару и её спецификацию: возможные торговые объёмы, переменные _Digits и _Point, текущие цены на момент открытия и некоторые другие. А в структуре stThree три раза используется stSmb —  это и есть наш треугольник. Также сюда добавлены некоторые свойства, относящиеся только к треугольнику: текущий профит, мэджик, время открытия и т.д. Далее следуют режимы работы, о которых мы поговорим позже, и входные переменные. Входные переменные тоже описаны в комментариях, но на двух из них мы остановимся чуть подробнее:

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

Параметр inProfit содержит размер комиссии, если она есть.


Начальная настройка

Итак, включаемые файлы и используемые переменные описаны. Далее приступаем к блоку OnInint().

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

Только один из 6 входных параметров может привести к невозможности работы эксперта — это торговый объём. Мы не можем открывать сделки с отрицательным объёмом. Все остальные настройки на корректность работы не влияют. Проверки проводятся в самой первой функции блока OnInit().

Ознакомимся с её кодом.

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // Проверим корректность выставления объёма торговли, отрицательным объёмом торговать не можем
      if (lot<0)
      {
         Alert("Trade volume < 0");  
         ExpertRemove();         
      }      
      
      // Если торговый объём = 0, то предупреждаем что робот автоматически будет использовать минимально возможный объём.
      if (lot==0) Alert("Always use the same minimum trading volume");  

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

В работе эксперт  использует 2 файла: файл с найденными треугольниками (который создается только при соответствующем выборе пользователя) и лог-файл, куда записывается время открытия и закрытия треугольника, цены открытия и некоторая дополнительная информация для удобства контроля. Лог-файл ведется всегда.

      // Создаём лог-файл, только если не выбран режим создания файла треугольников, т.к. в этом случае это неактуально.                                  
      if(inMode!=CREATE_FILE)
      {
         string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv";      
         FileDelete(name);      
         fh=FILEOPENWRITE(name);
         if (fh==INVALID_HANDLE) Alert("The log file is not created");      
      }   
      
      // В подавляющем большинстве размер контракта для валютных пар у брокеров = 100000, но иногда бывают исключения.
      // Они настолько редки, что проще один раз при старте проверить это значение, и если оно не равно 100000, то сообщить об этом,
      // чтобы пользователь сам принял решение, важно это или нет. Дальше он продолжит работу, не описывая более моменты, когда 
      // в треугольнике попадаются пары с разным размером контракта.
      for(int i=SymbolsTotal(true)-1;i>=0;i--)
      {
         string name=SymbolName(i,true);
         
         // Функция проверки символа на доступность торговли также используется при составлении треугольников.
         // Там её и рассмотрим более подробно
         if(!fnSmbCheck(name)) continue;
         
         double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
         if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0));      
      }
      
      // Получили тип счёта, с хеджингом или неттинговый
      accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   }


Составление треугольников

Чтобы составить треугольники, нам нужно рассмотреть следующие аспекты:

  1. Источник треугольников — из окна "Обзор рынка" или из заранее подготовленного файла.
  2. В тестере ли мы? Если да, то в нужно загрузить символы в Обзор рынка. Нет смысла загружать всё возможное, поскольку обычный домашний компьютер просто не справится с нагрузкой. Будем искать заранее подготовленный файл с символами для тестера. Если же его нет, то протестируем стратегию на стандартном треугольнике: EUR+USD+GBP.
  3. Чтобы упростить код, введём ограничение: все символы в треугольнике должны иметь одинаковый размер контракта.
  4. Не забываем, что треугольники можно составить только из валютных пар.

Первая необходимая функция — составление треугольников из Обзора рынка.

void fnGetThreeFromMarketWatch(stThree &MxSmb[])
   {
      // Получаем общее количество символов
      int total=SymbolsTotal(true);
      
      // Переменные для сравнения размера контрактов    
      double cs1=0,cs2=0;              
      
      // В первом цикле берём первый символ из списка
      for(int i=0;i<total-2 && !IsStopped();i++)    
      {//1
         string sm1=SymbolName(i,true);
         
         // Проверяем символ на различные ограничения
         if(!fnSmbCheck(sm1)) continue;      
              
         // Получаем размер контракта и сразу его нормализуем, т.к. впоследствии будем сравнивать это значение 
         if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; 
         cs1=NormalizeDouble(cs1,0);
         
         // Получаем базовую валюту и валюту прибыли, т.к. сравнение проводим именно по ним, а не по наименованию пары
         string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE);     
         string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT);
         
         // Во втором цикле берём следующий символ из списка
         for(int j=i+1;j<total-1 && !IsStopped();j++)
         {//2
            string sm2=SymbolName(j,true);
            if(!fnSmbCheck(sm2)) continue;
            if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) continue;
            cs2=NormalizeDouble(cs2,0);
            string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE);
            string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT);
            // У первой и второй пары должно быть одно совпадение по любой из валют.
            // Если его нет — значит, треугольник из них никак составить не сможем.    
            // При этом проверку на полную идентичность проводить смысла нет, потому что, например, из 
            // eurusd и eurusd.xxx треугольник всё равно не составится.
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // Размеры контрактов должны быть одинаковыми            
            if (cs1!=cs2) continue;
            
            // В третьем цикле ищем последний символ для треугольника
            for(int k=j+1;k<total && !IsStopped();k++)
            {//3
               string sm3=SymbolName(k,true);
               if(!fnSmbCheck(sm3)) continue;
               if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue;
               cs1=NormalizeDouble(cs1,0);
               string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE);
               string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT);
               
               // Мы знаем, что у первого и второго символа есть одна общая валюта. Чтобы составить треугольник, надо найти такую
               // третью валютную пару, одна валюта которой совпадает с любой валютой из первой пары, а другая — с
               // любой валютой из второй. Если совпадения нет, значит, эта пара не подходит.
               if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else continue;
               if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else continue;
               if (cs1!=cs2) continue;
               
               // Если дошли до этого шага — значит, все проверки пройдены, и из трёх этих найденных пар можно составить треугольник
               // Записываем его в наш массив
               int cnt=ArraySize(MxSmb);
               ArrayResize(MxSmb,cnt+1);
               MxSmb[cnt].smb1.name=sm1;
               MxSmb[cnt].smb2.name=sm2;
               MxSmb[cnt].smb3.name=sm3;
               break;
            }//3
         }//2
      }//1    
   }

Вторая необходимая функция — чтение треугольников из файла

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // Если файл с символами не найден, то выводим сообщение об этом и завершаем работу
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("File with symbols not read!");
         ExpertRemove();
      }
      
      // Переводим каретку в начало файла
      FileSeek(fh,0,SEEK_SET);
      
      // Пропускаем заголовок (первую строку файла)      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // Получим три символа треугольника. Сделаем базовую проверку на доступность данных
         // Файл с треугольниками робот умеет составлять автоматически. Если пользователь вдруг
         // изменил его самостоятельно и некорректно, то считаем, что он это сделал осознанно
         string smb1=FileReadString(fh);
         string smb2=FileReadString(fh);
         string smb3=FileReadString(fh);
         
         // Если данные по символам доступны, то промотав до конца строки, запишем их в наш массив треугольников
         if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {while(!FileIsLineEnding(fh)) FileReadString(fh);continue;}
         
         int cnt=ArraySize(MxSmb);
         ArrayResize(MxSmb,cnt+1);
         MxSmb[cnt].smb1.name=smb1;
         MxSmb[cnt].smb2.name=smb2;
         MxSmb[cnt].smb3.name=smb3;
         while(!FileIsLineEnding(fh)) FileReadString(fh);
      }
   }

Последняя функция, которая нужна в этом разделе — это обёртка двух предыдущих функций. Она отвечает за выбор источника треугольников в зависимости от входных настроек робота. Также в ней проверим, где запускается робот. Если в Тестере, то вне зависимости от выбора пользователя загружаем треугольники из файла. Если файла нет — загружаем дефолтный треугольник EURUSD+GBPUSD+EURGBP.

void fnSetThree(stThree &MxSmb[],enMode mode)
   {
      // Сбрасываем наш массив треугольников
      ArrayFree(MxSmb);
      
      // Смотрим, в тестере мы или нет
      if((bool)MQLInfoInteger(MQL_TESTER))
      {
         // Если да, то ищем файл символов и запускаем загрузку треугольников из файла
         if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb);
         
         // Если файл не найден, то перебираем все доступные символы и ищем среди них дефолтный треугольник EURUSD+GBPUSD+EURGBP
         else{               
            char cnt=0;         
            for(int i=SymbolsTotal(false)-1;i>=0;i--)
            {
               string smb=SymbolName(i,false);
               if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="GBP" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD"))
               {
                  if (SymbolSelect(smb,true)) cnt++;
               }               
               else SymbolSelect(smb,false);
               if (cnt>=3) break;
            }  
            
            // После загрузки дефолтного треугольника в Обзор рынка, запустим составление треугольника         
            fnGetThreeFromMarketWatch(MxSmb);
         }
         return;
      }
      
      // Если мы не в тестере, то смотрим, какой режим работы выбрал пользователь: 
      // взять символы или из Обзора рынка, или из файла
      if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb);
      if(mode==USE_FILE) fnGetThreeFromFile(MxSmb);     
   }

Здесь мы использовали одну вспомогательную функцию — fnSmbCheck(). В ней проверяется, есть ли ограничения на работу с символом. Если есть, то мы его пропускаем. Вот её код.

bool fnSmbCheck(string smb)
   {
      // Треугольник можно составить только из валютных пар
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false);
      
      // Если есть ограничения на торговлю, то пропускаем этот символ
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false);   
      
      // Если есть дата начала или окончания контракта, то тоже пропускаем, поскольку у валют этот параметр не используется
      if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false);
      if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false);
      
      // Доступность на типы ордеров. Хотя робот торгует только рыночными ордерами, всё же ограничений быть не должно
      int som=(int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE);
      if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else return(false);
      if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else return(false);
      if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else return(false);
      if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else return(false);
      if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else return(false);
      if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else return(false);
       
      // Проверка стандартной библиотекой на достуность данных         
      if(!csmb.Name(smb)) return(false);
      
      // Проверка ниже нужна только в реальной работе, поскольку иногда почему-то SymbolInfoTick работает, и цены как бы 
      // получены, а по факту аск или бид=0.
      // В тестере отключаем, так как там цены могут появиться позже.
      if(!(bool)MQLInfoInteger(MQL_TESTER))
      {
         MqlTick tk;      
         if(!SymbolInfoTick(smb,tk)) return(false);
         if(tk.ask<=0 ||  tk.bid<=0) return(false);      
      }

      return(true);
   }

Итак, треугольники составлены. Функции их составления помещены во включаемый файл fnSetThree.mqh. Функция проверки символа на ограничения помещена в отдельный файл fnSmbCheck.mqh.

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

символ 1 символ 2
символ 3
1 EURUSD = GBPUSD  х EURGBP
2 EURUSD = EURGBP  х GBPUSD
3 GBPUSD = EURUSD  / EURGBP
4 GBPUSD = EURGBP  0 EURUSD
5 EURGBP = EURUSD  / GBPUSD
6 EURGBP = GBPUSD  0 EURUSD

'x' = умножить, '/' = разделить. '0' = действие невозможно

В приведённой выше таблице видно, что треугольник можно составить 6 способами, но два из них —  строки 4 и 6 — не позволяют выразить первый символ через два оставшихся. Значит, эти варианты отпадают. Остальные 4 варианта идентичны. Неважно, какой символ и через какие другие мы выражаем, но нам важна скорость. Операция деления более медленная чем умножение, поэтому отбросим варианты 3 и 5. Осталось два варианта: строки 1 и 2.

Остановимся на варианте в строке 2 из-за удобства восприятия. Таким образом нам не придётся вводить в робота дополнительные поля ввода для первого, второго и третьего символов, да это и невозможно, поскольку мы торгуем не один треугольник, а сразу все возможные.

Удобство нашего выбора: так как мы торгуем арбитраж, а эта стратегия подразумевает нейтральную позицию, то мы должны купить и продать одно и то же. Пример: Buy 0.7 лота EURUSD и Sell 0.7 лота EURGBP — мы купили и продали 70000€. То есть, мы имеем позицию, несмотря на то, что находимся вне рынка, поскольку в покупке и продаже фигурировал один объём, но выраженный в разных деньгах. Нам нужно скорректировать их, проведя сделку по GBPUSD. Другими словами, мы сразу знаем, что у символов 1 и 2 всегда должен быть одинаковый объём, но разное направление. Также заранее известно, что у третьей пары объём равен цене второй пары.

Функция которая правильно расставляет пары в треугольнике:

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // Сначала определимся, что стоит на третьем месте. 
         // Это пара, базовая валюта которой не совпадает с двумя другими базовыми валютами
         string sm1base="",sm2base="",sm3base="";
         
         // Если вдруг почему-то мы не смогли получить базовую валюту, то данный треугольник не используем в работе
         if(!SymbolInfoString(MxSmb[i].smb1.name,SYMBOL_CURRENCY_BASE,sm1base) ||
         !SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_BASE,sm2base) ||
         !SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE,sm3base)) {MxSmb[i].smb1.name="";continue;}
                  
         // Если базовая валюта 1 и 2 символа совпадают, то пропустим этот шаг. Если нет, то меняем местами пары
         if(sm1base!=sm2base)
         {         
            if(sm1base==sm3base)
            {
               string temp=MxSmb[i].smb2.name;
               MxSmb[i].smb2.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
            
            if(sm2base==sm3base)
            {
               string temp=MxSmb[i].smb1.name;
               MxSmb[i].smb1.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
         }
         
         // Теперь определим первое и второе места. 
         // На втором месте пара, валюта прибыли у которой совпадает с валютой базы у третьей. 
         // В таком случае мы всегда используем умножение.
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // Меняем первую и вторую пару местами. 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // Выводим сообщение об обработанном треугольнике. 
         Print("Use triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // Сообщаем об общем количестве треугольников в работе. 
      Print("All used triangles: "+(string)count);
   }

Функция целиком расположена в отдельном файле fnChangeThree.mqh.

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

  1. минимальный и максимальный объём торговли по каждому символу;
  2. количество знаков в цене и в объёме для округления;
  3. переменная Point и Ticksize. Я не сталкивался с ситуацией, когда у валютных пар они различны, но всё же получим все данные и будем использовать их в нужных местах.
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // Простенький макрос для принта   
      #define prnt(nm) {nm="";Print("NOT CORRECT LOAD: "+nm);continue;}
      
      // Перебираем в цикле все собранные треугольники. Здесь у нас будет перерасход времени на повторные запросы данных по одним и 
      // тем же символам, но так как эта операция происходит только при зарузке робота, то ради сокращения кода можно поступить и так.
      // Для получения данных используем стандартную библиотеку. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Загружая в класс CSymbolInfo символ, мы инициализируем сбор всех необходимых нам данных
         // и заодно проверяем их доступность. Если что-то не так, то треуголник помечаем нерабочим.                  
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // Получили _разрядность по каждому символу
         MxSmb[i].smb1.digits=csmb.Digits();
         
         //Переводим проскальзывание из целых пунктов в десятичные. Такой формат нам нужен будет дальше для расчётов
         MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION;         
         
         // Чтобы переводить котировки в количество пунктов, нам часто придётся цену делить на значение _Point.
         // Лучше это значение представить в виде 1/Point, и тогда мы деление заменим умножением. 
         // Тут нет проверки csmb.Point() на 0: она не может быть равной 0, но если по какой-то причине 
         // параметр не будет получен, то этот треугольник будет отсеян строкой if (!csmb.Name(MxSmb[i].smb1.name)).            
         MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         
         // До стольки знаков мы округляем лот. 
         MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep());
         
         // Ограничения по объёмам, сразу нормализованные
         MxSmb[i].smb1.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb1.digits_lot); 
         
         //Размер контракта 
         MxSmb[i].smb1.contract=csmb.ContractSize();
         
         // То же, что и выше, но взято для символа 2
         if (!csmb.Name(MxSmb[i].smb2.name))    prnt(MxSmb[i].smb2.name);
         MxSmb[i].smb2.digits=csmb.Digits();
         MxSmb[i].smb2.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb2.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb2.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb2.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb2.digits_lot);         
         MxSmb[i].smb2.contract=csmb.ContractSize();
         
         // То же, что и выше, но взято для символа 3
         if (!csmb.Name(MxSmb[i].smb3.name))    prnt(MxSmb[i].smb3.name);
         MxSmb[i].smb3.digits=csmb.Digits();
         MxSmb[i].smb3.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb3.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb3.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb3.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb3.digits_lot);           
         MxSmb[i].smb3.contract=csmb.ContractSize();   
         
         // Выравниваем объём торговли. Здесь есть ограничения как для каждой валютной пары, так и для всего треугольника в целом. 
         // Ограничения для пары записаны здесь: MxSmb[i].smbN.lotN
         // Ограничения для треугольника записаны здесь: MxSmb[i].lotN
         
         // Выбираем из всех минимальных значений максимальное. Тут же округляем по самому крупному значению.
         // Весь этот блок кода сделан только для случая, когда попадается примерно такая ситуация по объёмам: 0.01+0.01+0.1. 
         // В этом случае минимально возможный торговый объём будет установлен 0.1 и округлён до 1 знака после запятой.
         double lt=MathMax(MxSmb[i].smb1.lot_min,MathMax(MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min));
         MxSmb[i].lot_min=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Из максимальных значений объёма берём самое минимальное и тоже сразу округляем. 
         lt=MathMin(MxSmb[i].smb1.lot_max,MathMin(MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max));
         MxSmb[i].lot_max=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Если во входных параметрах торгового объёма стоит 0, значит используем минимально возможный объём, но берём не минимальный по каждой паре, 
         // а минимальный для всех. 
         if (lot==0)
         {
            MxSmb[i].smb1.lot=MxSmb[i].lot_min;
            MxSmb[i].smb2.lot=MxSmb[i].lot_min;
            MxSmb[i].smb3.lot=MxSmb[i].lot_min;
         } else
         {
            // Если объём необходимо выравнивать, то у 1 и 2 пары он известен, а объём третьей пары будет вычисляться непосредственно перед входом. 
            MxSmb[i].smb1.lot=lot;  
            MxSmb[i].smb2.lot=lot;
            
            // Если входной торговый объём не попадает в текущие ограничения, то треугольник не используем в работе. 
            // Сообщаем об этом алертом
            if (lot<MxSmb[i].smb1.lot_min || lot>MxSmb[i].smb1.lot_max || lot<MxSmb[i].smb2.lot_min || lot>MxSmb[i].smb2.lot_max) 
            {
               MxSmb[i].smb1.name="";
               Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - not correct the trading volume");
               continue;
            }            
         }
      }
   }

Функция располагается в отдельном файле fnSmbLoad.mqh

На этом раздел о составлении треугольников можно считать закрытым. Двигаемся дальше.


Режимы работы эксперта

При запуске робота мы можем выбрать один из доступных режимов работы:
  1. Symbols from Market Watch.
  2. Symbols from file.
  3. Create file with symbols.

Режим "Symbols from Market Watch" подразумевает, что мы запускаем робота на текущем символе и составляем рабочие треугольники из окна Обзор рынка. Это основной режим работы, и он не требует дополнительной обработки.

Режим "Symbols from file" отличается от первого только источником получения треугольников — из ранее подготовленного файла.

Режим "Create file with symbols" как раз и создаёт файл с треугольниками, которые мы в будущем используем или во втором режиме работы, или в тестере. Данный режим предполагает только составление треугольников, после которого работа эксперта завершается.

Опишем эту логику:

      if(inMode==CREATE_FILE)
      {
         // Удаляем файл, если он есть.
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("File with symbols not created");
            ExpertRemove();
         }
         // Пишем треугольники и некоторую дополнительную информацию в файл
         fnCreateFileSymbols(MxThree,fh);
         Print("File with symbols created");
         
         // Закрываем файл и завершаем работу эксперта
         FileClose(fh);
         ExpertRemove();
      }

Функция записи данных в файл простая и особых комментариев не требует:

void fnCreateFileSymbols(stThree &MxSmb[], int filehandle)
   {
      // Определяем заголовки в файле
      FileWrite(filehandle,"Symbol 1","Symbol 2","Symbol 3","Contract Size 1","Contract Size 2","Contract Size 3",
      "Lot min 1","Lot min 2","Lot min 3","Lot max 1","Lot max 2","Lot max 3","Lot step 1","Lot step 2","Lot step 3",
      "Common min lot","Common max lot","Digits 1","Digits 2","Digits 3");
      
      // Заполняем файл в соответствии с вышеуказанными заголовками
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         FileWrite(filehandle,MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name,
         MxSmb[i].smb1.contract,MxSmb[i].smb2.contract,MxSmb[i].smb3.contract,
         MxSmb[i].smb1.lot_min,MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min,
         MxSmb[i].smb1.lot_max,MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max,
         MxSmb[i].smb1.lot_step,MxSmb[i].smb2.lot_step,MxSmb[i].smb3.lot_step,
         MxSmb[i].lot_min,MxSmb[i].lot_max,
         MxSmb[i].smb1.digits,MxSmb[i].smb2.digits,MxSmb[i].smb3.digits);         
      }
      FileWrite(filehandle,"");      
      // Оставим пустую строку после всех символов
      
      // После завершения работы перенесем все данные на диск в целях безопасности 
      FileFlush(filehandle);
   }

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

Данная функция размещена в отдельном файле fnCreateFileSymbols.mqh


Рестарт робота

Мы практически завершили стартовые настройки эксперта. Нам осталось ответить еще на один вопрос: как обработать восстановление после сбоя? Если произошла кратковременная потеря интернета — это не страшно. Робот продолжит нормально работать после повторного подключения к Сети. А вот если придётся перезапустить робота, то нам необходимо найти свои позиции и продолжить работу с ними.

Функция которая решает проблемы с перезапуском робота:

void fnRestart(stThree &MxSmb[],ulong magic,int accounttype)
   {
      string   smb1,smb2,smb3;
      long     tkt1,tkt2,tkt3;
      ulong    mg;
      uchar    count=0;    //Счётчик восстановленных треугольников
      
      switch(accounttype)
      {
         // С хеджинговым счётом восстановить позиции несложно: пройтись по всем открытым позициям,по мэджику найти свои и 
         // сформировать их в треугольники.
         // С неттингом сложнее - нужно обратиться к собственной базе, в которой хранятся открытые роботом позиции. 
         
         // Алгоритм поиска своих позиций и восстановление их в треугольник реализовано "в лоб", без изысков и 
         // оптимизации. Но поскольку этот этап нужен нечасто, то можно пренебречь производительностью ради
         // сокращения кода. 
         
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            // Перебираем все открытые позиции и смотрим совпадение по мэджику. 
            // Запомним мэджик первой найденной позиции: по нему будем искать две остальные. 

            
            for(int i=PositionsTotal()-1;i>=2;i--)
            {//for i
               smb1=PositionGetSymbol(i);
               mg=PositionGetInteger(POSITION_MAGIC);
               if (mg<magic || mg>(magic+MAGIC)) continue;
               
               // Запоминаем тикет, чтобы далее было проще обращаться к данной позиции. 
               tkt1=PositionGetInteger(POSITION_TICKET);
               
               // Ищем вторую позицию с таким же мэджиком. 
               for(int j=i-1;j>=1;j--)
               {//for j
                  smb2=PositionGetSymbol(j);
                  if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;  
                  tkt2=PositionGetInteger(POSITION_TICKET);          
                    
                  // Ищем последнюю позицию.
                  for(int k=j-1;k>=0;k--)
                  {//for k
                     smb3=PositionGetSymbol(k);
                     if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;
                     tkt3=PositionGetInteger(POSITION_TICKET);
                     
                     // Если дошли до этого места, значит, найден открытый треугольник. Данные о нём уже загружены. Всё остальное робот подсчитает на следующем тике.
                     
                     for(int m=ArraySize(MxSmb)-1;m>=0;m--)
                     {//for m
                        // Пройдем по массиву треугольников, игнорируя уже открытые.
                        if (MxSmb[m].status!=0) continue; 
                        
                        // Перебор делается "в лоб". На первый взгляд может показаться, что при этом мы можем несколько раз 
                        // обратиться к одной и той же валютной паре. Однако это не так, потому что в циклах перебора 
                        // после нахождения очередной валютной пары мы продолжаем поиск не с начала, а со следующей пары

                        if (  (MxSmb[m].smb1.name==smb1 || MxSmb[m].smb1.name==smb2 || MxSmb[m].smb1.name==smb3) &&                               (MxSmb[m].smb2.name==smb1 || MxSmb[m].smb2.name==smb2 || MxSmb[m].smb2.name==smb3) &&                               (MxSmb[m].smb3.name==smb1 || MxSmb[m].smb3.name==smb2 || MxSmb[m].smb3.name==smb3)); else continue;                                                  // Мы нашли этот треугольник и присваиваем ему соответствующий статус                         MxSmb[m].status=2;                         MxSmb[m].magic=magic;                         MxSmb[m].pl=0;                                                  // Расставляем тикеты в нужной последовательности. Треугольник вновь в работе.                         if (MxSmb[m].smb1.name==smb1) MxSmb[m].smb1.tkt=tkt1;                         if (MxSmb[m].smb1.name==smb2) MxSmb[m].smb1.tkt=tkt2;                         if (MxSmb[m].smb1.name==smb3) MxSmb[m].smb1.tkt=tkt3;                                if (MxSmb[m].smb2.name==smb1) MxSmb[m].smb2.tkt=tkt1;                         if (MxSmb[m].smb2.name==smb2) MxSmb[m].smb2.tkt=tkt2;                         if (MxSmb[m].smb2.name==smb3) MxSmb[m].smb2.tkt=tkt3;                                  if (MxSmb[m].smb3.name==smb1) MxSmb[m].smb3.tkt=tkt1;                         if (MxSmb[m].smb3.name==smb2) MxSmb[m].smb3.tkt=tkt2;                         if (MxSmb[m].smb3.name==smb3) MxSmb[m].smb3.tkt=tkt3;                                                    count++;                                                 break;                        }//for m                                 }//for k                              }//for j                     }//for i                  break;          default:          break;       }              if (count>0) Print("Restore "+(string)count+" triangles");                }

Как и ранее, данная функция находится в отдельном файле: fnRestart.mqh

Последние шаги:

      ctrade.SetDeviationInPoints(DEVIATION);
      ctrade.SetTypeFilling(ORDER_FILLING_FOK);
      ctrade.SetAsyncMode(true);
      ctrade.LogLevel(LOG_LEVEL_NO);
      
      EventSetTimer(1);

Обратите внимание на асинхронный режим отправки ордеров. Стратегия предполагает максимально оперативные действия, поэтому используем этот режим выставления. Будут и сложности: нам понадобится дополнительный код для отслеживания, успешно ли открыта позиция. Рассмотрим всё это ниже.

На этом блок OnInit() закончен, можно переходить к телу робота.


OnTick

Сначала посмотрим, есть ли у нас в настройках ограничение по максимально возможному количеству открытых треугольников. Если есть и мы достигли установленного предела, то существенную часть кода на этом тике можно пропустить:

      ushort OpenThree=0;                          // Количество открытых треугольников
      for(int j=ArraySize(MxThree)-1;j>=0;j--)
      if (MxThree[j].status!=0) OpenThree++;       // Считаем незакрытые тоже         

Проверка проста. Мы объявили локальную переменную для подсчёта открытых треугольников и перебрали в цикле наш основной массив. Если статус треугольника не 0 — значит, он в работе. 

После подсчёта открытых треугольников, если ограничение позволяет, начинаем просматривать все остальные треугольники и следить за их состоянием. За это отвечает функция fnCalcDelta():

      if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree))
         fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree);   // Считаем расхождение и сразу открываемся

Рассмотрим её код подробнее:

void fnCalcDelta(stThree &MxSmb[],double prft, string cmnt, ulong magic,double lot, ushort lcMaxThree, ushort &lcOpenThree)
   {     
      double   temp=0;
      string   cmnt_pos="";
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for i
         // Если треугольник в работе, то мы его пропускаем
         if(MxSmb[i].status!=0) continue; 
         
         // Снова делаем проверку на доступность всех трёх пар: если хоть одна из них недоступна,
         // то считать весь треугольник нет смысла
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  //вдруг по какой-то паре закрыли торги
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // Количество открытых треугольников считаем в начале каждого тика,
         // но их можно открыть и внутри тика. Поэтому постоянно отслеживаем их количество
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;}     

         
         // Далее получаем все необходимые данные для расчётов. 
         
         // Получили стоимость тика по каждой паре.
         if(!SymbolInfoDouble(MxSmb[i].smb1.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb1.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb2.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb2.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb3.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb3.tv)) continue;
         
         // Получили текущие цены.
         if(!SymbolInfoTick(MxSmb[i].smb1.name,MxSmb[i].smb1.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb2.name,MxSmb[i].smb2.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb3.name,MxSmb[i].smb3.tick)) continue;
         
         // Проверяем, не равен ли 0 аск или бид.
         if(MxSmb[i].smb1.tick.ask<=0 || MxSmb[i].smb1.tick.bid<=0 || MxSmb[i].smb2.tick.ask<=0 || MxSmb[i].smb2.tick.bid<=0 || MxSmb[i].smb3.tick.ask<=0 || MxSmb[i].smb3.tick.bid<=0) continue;
         
         // Считаем объём для третьей пары. У первых двух пар объём известен — он одинаковый и фиксированный.
         // Объём третьей пары всегда меняется. Но он считается, только если в стартовых переменных значение лота не равно 0.
         // При нулевом лоте везде будет использоваться минимальный, одинаковый объём.
         // Логика расчёта объёма простая. Вспоминаем наш вариант треугольника: EURUSD=EURGBP*GBPUSD. Количество купленных или проданных фунтов
         // напрямую зависит от котировки EURGBP, а в третьей паре эта третья валюта стоит на первом месте. Мы избавляем себя от части расчётов,
         // беря в качестве объёма просто цену второй пары. Я взял среднее между аском и бидом.
         // Не забываем о поправке на входной торговый объём.
         
         if (lot>0)
         MxSmb[i].smb3.lot=NormalizeDouble((MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.tick.bid)/2*MxSmb[i].smb1.lot,MxSmb[i].smb3.digits_lot);
         
         // Если рассчитанный объём выходит за допустимые границы, то сообщаем об этом пользователю.
         // Данный треугольник помечаем как нерабочий
         if (MxSmb[i].smb3.lot<MxSmb[i].smb3.lot_min || MxSmb[i].smb3.lot>MxSmb[i].smb3.lot_max)
         {
            Alert("The calculated lot for ",MxSmb[i].smb3.name," is out of range. Min/Max/Calc: ",
            DoubleToString(MxSmb[i].smb3.lot_min,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot_max,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); 
            Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - DISABLED");
            MxSmb[i].smb1.name="";   
            continue;  
         }
         
         // Считаем наши затраты, т.е. спред+комиссии. pr = спред в целых пунктах.
         // Именно спред мешает нам зарабатывать данной стратегией, поэтому его необходимо учитывать обязательно. 
         // Можно использовать не разницу цен, умноженную на обратный пойнт, а взять сразу спред в пунктах.

         
         MxSmb[i].smb1.sppoint=NormalizeDouble(MxSmb[i].smb1.tick.ask-MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits)*MxSmb[i].smb1.Rpoint;
         MxSmb[i].smb2.sppoint=NormalizeDouble(MxSmb[i].smb2.tick.ask-MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits)*MxSmb[i].smb2.Rpoint;
         MxSmb[i].smb3.sppoint=NormalizeDouble(MxSmb[i].smb3.tick.ask-MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)*MxSmb[i].smb3.Rpoint;
         if (MxSmb[i].smb1.sppoint<=0 || MxSmb[i].smb2.sppoint<=0 || MxSmb[i].smb3.sppoint<=0) continue;
         
         // Теперь рассчитаем спред в валюте депозита. 
         // В валюте стоимость 1 тика всегда равна параметру SYMBOL_TRADE_TICK_VALUE.
         // Также не забываем о торговых объёмах
         MxSmb[i].smb1.spcost=MxSmb[i].smb1.sppoint*MxSmb[i].smb1.tv*MxSmb[i].smb1.lot;
         MxSmb[i].smb2.spcost=MxSmb[i].smb2.sppoint*MxSmb[i].smb2.tv*MxSmb[i].smb2.lot;
         MxSmb[i].smb3.spcost=MxSmb[i].smb3.sppoint*MxSmb[i].smb3.tv*MxSmb[i].smb3.lot;
         
         // Итак, вот наши затраты на указанный торговый объём с добавленной комиссией, которую указывает пользователь
         MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft;
         
         // Мы можем отслеживать ситуацию, когда аск портфеля < бида, но такие ситуации очень редки, 
         // и их можно не рассматривать отдельно. При этом абитраж, разнесённый во времени, данную ситуацию тоже обработает.
         // Итак, нахождение в позиции освобождено от рисков, и вот почему: к примеру, мы купили eurusd,
         // и здесь же его сразу продали, но через eurgbp и gbpusd. 
         // То есть мы увидели, что ask eurusd< bid eurgbp * bid gbpusd. Таких ситуаций множество, но для успешного входа этого мало.
         // Посчитаем еще затраты на спред. Входить надо не просто когда аск < бид, а когда разница между
         // ними больше затрат на спред.          
         
         // Договоримся, что покупка — это значит купили  первый символ и продали два оставшихся,
         // а продажа — продали первую пару и купили две остальных.
         
         temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot;
         
         // Разберём подробнее формулу расчёта. 
         // 1. В скобках каждая цена коректируется на проскальзывание в худшую сторону: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev
         // 2. Как показано в формуле выше, bid eurgbp * bid gbpusd - цены второго и третьего символа перемножаем:
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. Далее считаем разницу между аском и бидом
         // 4. Мы получили разницу в пунктах, которую теперь надо перевести в деньги: перемножить 
         // стоимость пункта и торговый объём. Для этих целей берём значения первой пары. 
         // Если же мы бы строили треугольник, переместив все пары в одну сторону и проводя сравнение с 1, то расчётов было бы больше. 

         MxSmb[i].PLBuy=((MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)-(MxSmb[i].smb1.tick.ask+MxSmb[i].smb1.dev))*temp;
         MxSmb[i].PLSell=((MxSmb[i].smb1.tick.bid-MxSmb[i].smb1.dev)-(MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.ask+MxSmb[i].smb3.dev))*temp;
         
         // Мы получили расчет суммы, которую можем заработать или потерять, если купим или продадим треугольник. 
         // Осталось сравнить ее с затратами, чтобы решить, входить ли в сделку. Нормализуем всё до 2 знака. 
         MxSmb[i].PLBuy=   NormalizeDouble(MxSmb[i].PLBuy,2);
         MxSmb[i].PLSell=  NormalizeDouble(MxSmb[i].PLSell,2);
         MxSmb[i].spread=  NormalizeDouble(MxSmb[i].spread,2);                  
         
         // Если есть потенциальная прибыль, то проводим проверку на достаточность средств для открытия.         
         if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread)
         {
            // Я просто посчитал всю маржу для покупки. Поскольку она всё равно выше, чем для продажи, то можно не учитывать направление сделки.  
            // Обратим внимание и на повышающий коэффициент. Нельзя открывать треугольник, когда маржи едва хватает. Повышающий коэффициент по умолчанию = 20%

            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb1.name,MxSmb[i].smb1.lot,MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb2.name,MxSmb[i].smb2.lot,MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb3.name,MxSmb[i].smb3.lot,MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.mrg))
            if(AccountInfoDouble(ACCOUNT_MARGIN_FREE)>((MxSmb[i].smb1.mrg+MxSmb[i].smb2.mrg+MxSmb[i].smb3.mrg)*CF))  //проверили сводобную маржу
            {
               // Мы почти готовы к открытию, осталось найти свободный мэджик из нашего диапазона. 
               // Начальный мэджик указан во входных параметрах, в переменной inMagic, по умолчанию равен 300. 
               // Диапазон мэждиков указан в дефайне MAGIC, по умолчанию стоит 200.
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // Если вернули 0 — значит, все мэджики заняты. Отсылаем об этом сообщение и выходим.
                  Print("Free magic ended\nNew triangles will not open");
                  break;
               }  
               
               // Устанавливаем найденный мэджик роботу
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // Создадим комментарий для треугольника
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" Open";               
               
               // Открываемся, попутно запомнив время отправки треугольника на открытие. 
               // Это нужно, чтобы не висеть в ожидании. 
               // По умолчанию, в дефайне MAXTIMEWAIT установлено время ожидания до полного открытия 3 секунды.
               // Если за это время мы не открылись, то отправляем то, что успело открыться, на закрытие.
               
               MxSmb[i].timeopen=TimeCurrent();
               
               if (MxSmb[i].PLBuy>MxSmb[i].spread)    fnOpen(MxSmb,i,cmnt_pos,true,lcOpenThree);
               if (MxSmb[i].PLSell>MxSmb[i].spread)   fnOpen(MxSmb,i,cmnt_pos,false,lcOpenThree);               
               
               // Печатаем сообщение об открытии треугольника. 
               if (MxSmb[i].status==1) Print("Open triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic);
            }
         }         
      }//for i
   }

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

Вот так мы выбираем доступный мэджик:

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // Можно перебрать все открытые треугольники в машем массиве. 
      // Но я выбрал другой вариант - пройтись по диапазону мэджиков,
      // и уже выбранный прогнать по массиву. 
      for(ulong i=magic;i<magic+MAGIC;i++)
      {
         find=false;
         
         // Мэджик в i. Проверим, присвоен ли он какому-нибудь треугольнику из открытых.
         for(int j=0;j<mxsize;j++)
         if (MxSmb[j].status>0 && MxSmb[j].magic==i)
         {
            find=true;
            break;   
         }   
         
         // Если мэджик не используется, то выходим из цикла, не дожидаясь его окончания.    
         if (!find) return(i);            
      }  
      return(0);  
   }

А вот так открываем треугольник:

bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt)
   {
      // Флаг открытия первого ордера. 
      bool openflag=false;
      
      // Если нет разрешения на торговлю, то не торгуем. 
      if (!cterm.IsTradeAllowed())  return(false);
      if (!cterm.IsConnected())     return(false);
      
      switch(side)
      {
         case  true:
         
         // Если после отправки ордера на открытие возвращается false, то уже точно нет смысла отправлять на открытие 2 остальных пары. 
         // Лучше попробуем заново на следующем тике. Также робот не занимается дооткрыванием треугольника. 
         // Если после отправления приказов что-то не открылось, то после ожидания
         // времени, указанного в дефайне MAXTIMEWAIT, закрываем недооткрытый треугольник. 
         if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;
            opt++;
            // Далее логика та же - если не смогли открыть, то треугольник уйдёт в закрываемые. 
            if(ctrade.Sell(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Sell(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);               
         }            
         break;
         case  false:
         
         if(ctrade.Sell(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;  
            opt++;        
            if(ctrade.Buy(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Buy(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);         
         }           
         break;
      }      
      return(openflag);
   }

Как обычно, функции выше расположены в отдельных файлах fnCalcDelta.mqh, fnMagicGet.mqh и fnOpen.mqh.

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

Рассмотрим функцию контроля открытия позиций:

void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh)
   {
      uchar cnt=0;       // Счётчик открытых позиций в треугольнике
      ulong   tkt=0;     // Текущий тикет
      string smb="";     // Текущий символ
      
      // Проверяем наш массив треугольников
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Рассматриваем только треугольники со статусом 1, т.е., отправленные на открытие
         if(MxSmb[i].status!=1) continue;
                          
         if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT)
         {     
            // Если превышено время, отведённое на открытие, то помечаем треугольник как готовый к закрытию         
            MxSmb[i].status=3;
            Print("Not correct open: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
            continue;
         }
         
         cnt=0;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            
            // Проверим все открытые позиции. Эту проверку делаем для каждого треугольника. 

            for(int j=PositionsTotal()-1;j>=0;j--)
            if (PositionSelectByTicket(PositionGetTicket(j)))
            if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic)
            {
               // Получаем символ и тикет рассматриваемой позиции. 

               tkt=PositionGetInteger(POSITION_TICKET);                smb=PositionGetString(POSITION_SYMBOL);                               // Проверяем, есть ли текущая позиция среди нужных нам в рассматриваемом треугольнике.                // Если есть, то увеличиваем счётчик и запоминаем тикет и цену открытия.                if (smb==MxSmb[i].smb1.name){ cnt++;   MxSmb[i].smb1.tkt=tkt;  MxSmb[i].smb1.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb2.name){ cnt++;   MxSmb[i].smb2.tkt=tkt;  MxSmb[i].smb2.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb3.name){ cnt++;   MxSmb[i].smb3.tkt=tkt;  MxSmb[i].smb3.price=PositionGetDouble(POSITION_PRICE_OPEN);}                               // Если нашли три необходимых позиции, значит, наш треугольник успешно открыт. Меняем его статус на 2 (открытый).                // Запишем данные об открытии в лог-файл                if (cnt==3)                {                   MxSmb[i].status=2;                   fnControlFile(MxSmb,i,fh);                   break;                  }             }             break;             default:             break;          }       }    }

Используемая нами функция записи в лог-файл проста :

void fnControlFile(stThree &MxSmb[],int i, int fh)
   {
      FileWrite(fh,"============");
      FileWrite(fh,"Open:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
      FileWrite(fh,"Tiket:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt);
      FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
      FileWrite(fh,"Margin",DoubleToString(MxSmb[i].smb1.mrg,2),DoubleToString(MxSmb[i].smb2.mrg,2),DoubleToString(MxSmb[i].smb3.mrg,2));
      FileWrite(fh,"Ask",DoubleToString(MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.digits));
      FileWrite(fh,"Bid",DoubleToString(MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits));               
      FileWrite(fh,"Price open",DoubleToString(MxSmb[i].smb1.price,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.price,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.price,MxSmb[i].smb3.digits));
      FileWrite(fh,"Tick value",DoubleToString(MxSmb[i].smb1.tv,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tv,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tv,MxSmb[i].smb3.digits));
      FileWrite(fh,"Spread point",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0));
      FileWrite(fh,"Spread $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3));
      FileWrite(fh,"Spread all",DoubleToString(MxSmb[i].spread,3));
      FileWrite(fh,"PL Buy",DoubleToString(MxSmb[i].PLBuy,3));
      FileWrite(fh,"PL Sell",DoubleToString(MxSmb[i].PLSell,3));      
      FileWrite(fh,"Magic",string(MxSmb[i].magic));
      FileWrite(fh,"Time open",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS));
      FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
      
      FileFlush(fh);       
   }

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

void fnCalcPL(stThree &MxSmb[], int accounttype, double prft)
   {
      // Вновь перебираем наш массив треугольников. 
      // Скорость открытия и закрытия - крайне важные моменты данной стратегии. 
      // Поэтому, как только мы находим треугольник для закрытия — без промедления закрываем его.
      
      bool flag=cterm.IsTradeAllowed() & cterm.IsConnected();      
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for
         // Нас интересуют только треугльники со статусом 2 или 3.
         // Статус 3 - закрыть треугольник - мы могли получить, если треугольник открылся не полностью
         if(MxSmb[i].status==2 || MxSmb[i].status==3); else continue;                             
         
         // Посчитаем, сколько заработал треугольник 
         if (MxSmb[i].status==2)
         {
            MxSmb[i].pl=0;         // Сбросили профит
            switch(accounttype)
            {//switch
               case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:  
                
               if (PositionSelectByTicket(MxSmb[i].smb1.tkt)) MxSmb[i].pl=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb2.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb3.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);                           
               break;
               default:
               break;
            }//switch
            
            // Округлили до 2 знака
            MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2);
            
            // На закрытии остановимся немного подробнее. Я использую следующую логику:
            // ситуация с арбитражем ненормальна и возникать не должна, т.е. при её появлении мы можем рассчитывать на возврат 
            // в состояние, когда арбитража нет. Сможем ли мы заработать? Иными словами, мы не можем сказать, 
            // продолжится ли получение профита. Поэтому я предпочитаю закрыть позицию сразу же после того, как оказались покрыты спред и комиссия. 
            // Счет в треугольном арбитраже идет на пункты, здесь не надо рассчитывать на большие движения. 
            // Впрочем, можете в переменной "Комиссия" во входных параметрах желаемую приыбль и ждать, пока она придёт. 
            // Итак, если мы заработали больше, чем потратили - присваиваем позиции статус "отправить на закрытие".

            if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3;                    
         }
         
         // Закрытие треугольника - только если разрешена торговля.
         if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); 
      }//for         
   }

За закрытие треугольника отвечает простая функция:

void fnCloseThree(stThree &MxSmb[], int accounttype, int i)
   {
      // Перед закрытием обязательно проверим на доступность всех пар в треугольнике. 
      // Разрывать треугольник крайне неправильно и опасно, а если работать на неттинговом счёте,
      // то впоследствии в позициях образуется неразбериха. 
      
      if(fnSmbCheck(MxSmb[i].smb1.name))
      if(fnSmbCheck(MxSmb[i].smb2.name))
      if(fnSmbCheck(MxSmb[i].smb3.name))          
      
      // Если всё доступно, то с помощью стандартной библиотеки закрываем все 3 позиции. 
      // После закрытия необходимо проверить успешность действия. 
      switch(accounttype)
      {
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:     
         
         ctrade.PositionClose(MxSmb[i].smb1.tkt);
         ctrade.PositionClose(MxSmb[i].smb2.tkt);
         ctrade.PositionClose(MxSmb[i].smb3.tkt);              
         break;
         default:
         break;
      }       
   }  

Мы практически подошли к концу, осталось проверить успешность закрытия и вывести что-нибудь на экран. Если робот ничего не пишет, то кажется что он не работает.

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

Проверяем успешность закрытия: 

void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh)
   {
      // Пройдемся по массиву треугольников. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Нам интересны только те, чей статус = 3, то есть уже закрытые или отправленные на закрытие. 
         if(MxSmb[i].status!=3) continue;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 
            
            // Если не смогли выделить ни одной пары из всего треугольника, значит закрыли успешно. Возвращаем статус в 0
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // Значит, закрыли успешно
               MxSmb[i].status=0;   
               
               Print("Close triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic+"  P/L: "+DoubleToString(MxSmb[i].pl,2));
               
               // Записали в лог-файл информацию о закрытии. 
               if (fh!=INVALID_HANDLE)
               {
                  FileWrite(fh,"============");
                  FileWrite(fh,"Close:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
                  FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
                  FileWrite(fh,"Tiket",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt));
                  FileWrite(fh,"Magic",string(MxSmb[i].magic));
                  FileWrite(fh,"Profit",DoubleToString(MxSmb[i].pl,3));
                  FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
                  FileFlush(fh);               
               }                   
            }                  
            break;
         }            
      }      
   }

И напоследок выведем что-нибудь в виде комментариев на экран. Это своеобразная "косметика" для визуального сопровождения. Выводим следующее:

  1. Всего треугольников отслеживается
  2. Открытых треугольников
  3. 5 самых ближайших к открытию треугольников
  4. Открытые треугольники, если есть

Вот код данной функции:

void fnCmnt(stThree &MxSmb[], ushort lcOpenThree)
   {     
      int total=ArraySize(MxSmb);
      
      string line="=============================\n";
      string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON\n";
      txt=txt+"Total triangles: "+(string)total+"\n";
      txt=txt+"Open triangles: "+(string)lcOpenThree+"\n"+line;
      
      // Максимальное количество треугольников, которое выводится на экран
      short max=5;
      max=(short)MathMin(total,max);
      
      // Вывод 5 ближайших к открытию 
      short index[];                    // Массивов индексов
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // Не используется
      short cnt=0,num=0;
      while(cnt<max && num<total)       // Взяли для старта первые max неоткрытых индексов треугольников
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // Сортировать и искать есть смысл только если элементов больше, чем можно вывести на экран. 
      if (total>max) 
      for(short i=max;i<total;i++)
      {
         // Открытые треугольники выводятся ниже.
         if(MxSmb[i].status!=0) continue;
         
         for(short j=0;j<max;j++)
         {
            if (MxSmb[i].PLBuy>MxSmb[index[j]].PLBuy)  {index[j]=i;break;}
            if (MxSmb[i].PLSell>MxSmb[index[j]].PLSell)  {index[j]=i;break;}
         }   
      }
      
      // Выводим ближайшие к открытию треугольники.
      bool flag=true;
      for(short i=0;i<max;i++)
      {
         cnt=index[i];
         if (cnt<0) continue;
         if (flag)
         {
            txt=txt+"Smb1           Smb2           Smb3         P/L Buy        P/L Sell        Spread\n";
            flag=false;
         }         
         txt=txt+MxSmb[cnt].smb1.name+" + "+MxSmb[cnt].smb2.name+" + "+MxSmb[cnt].smb3.name+":";
         txt=txt+"      "+DoubleToString(MxSmb[cnt].PLBuy,2)+"          "+DoubleToString(MxSmb[cnt].PLSell,2)+"            "+DoubleToString(MxSmb[cnt].spread,2)+"\n";      
      }            
      
      // Выводим открытые треугольники. 
      txt=txt+line+"\n";
      for(int i=total-1;i>=0;i--)
      if (MxSmb[i].status==2)
      {
         txt=txt+MxSmb[i].smb1.name+"+"+MxSmb[i].smb2.name+"+"+MxSmb[i].smb3.name+" P/L: "+DoubleToString(MxSmb[i].pl,2);
         txt=txt+"  Time open: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
         txt=txt+"\n";
      }   
      Comment(txt);
   }


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


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

Результаты работы из практики показывают, что в среднем можно рассчитывать на 3-4 сделки в неделю. Чаще всего позиция открывается ночью и, как правило, в треугольнике присутствует низколиквидная валюта типа TRY, NOK, SEK и похожие. Профит робота зависит от торгуемого объёма, а поскольку сделки возникают нечасто, то этот советник легко может манипулировать большими объёмами, при этом работая параллельно с другими роботами.

Риск робота легко поддаётся расчётам: 3 спреда * на количество открытых треугольников.

Для подготовки валютных пар, с которыми можно работать, рекомендую сначала открыть все символы, затем скрыть те, у которых запрещена торговля и которые не являются валютными парами. Более быстро эту операцию можно провести с использованием незаменимого для любителей мультивалютных стратегий скрипта: https://www.mql5.com/ru/market/product/25256

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


Перспективы развития

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

Также можно ускорить и результаты тестирования. Для этого есть два пути, которые можно совместить. Первый шаг — ввести дискретность расчёта, постоянно при этом отслеживая только те треугольники, где вероятность входа очень высокая. Второй путь — использовать OpenCL, что для данного робота крайне разумно.


Файлы, используемые в статье

Имя файла Описание
1 var.mqh Описаны все используемые переменные, дефайны и инпуты.
2 fnWarning.mqh Проверки начальных условий для корректной работы эксперта: входные переменные, окружение, настройки.
3 fnSetThree.mqh Составляем треугольники валютных пар. Здесь же происходит выбор источника пар — Обзор рынка или ранее подготовленный файл.
4 fnSmbCheck.mqh Функция проверки символа на доступность торговли и иные ограничения. NB: Торговая и котировочная сессия в роботе не проверяются
5 fnChangeThree.mqh Меняем расположение валютных пар в треугольнике, чтобы они все были построены по одному принципу.
6 fnSmbLoad.mqh Загружаем различные данные по символам, цены, пункты, ограничения по объёмам и т.д.
7 fnCalcDelta.mqh Считаем все раздвижки в треугольнике, здесь же учитываем все дополнительные затраты: спред, комиссии, проскальзывание.
8 fnMagicGet.mqh Ищем мэджик, который можно использовать для текущего треугольника
fnOpenCheck.mqh Проверяем, успешно ли открылся треугольник
10 fnCalcPL.mqh  Считаем прибыль/убыток треугольника
11  fnCreateFileSymbols.mqh Функция, которая создаёт файл с треугольниками для торговли. В файле присутствуют ещё и дополнительные данные (для информационных целей).
12  fnControlFile.mqh Функция ведёт лог-файл. Здесь описаны все открытия и закрытия с необходимыми данными. 
13  fnCloseThree.mqh Закрытие треугольника 
14  fnCloseCheck.mqh Проверяем, полностью ли закрылся треугольник
15  fnCmnt.mqh Вывод комментариев на экран 
16  fnRestart.mqh Проверяем при старте робота, есть ли ранее открытые треугольники, и если есть, то продолжаем их вести. 
17  fnOpen.mqh Открываем треугольник
18 Support.mqh Дополнительный класс поддержки. В нём только одна функция — подсчёт количества знаков после запятой у дробного числа.
19 head.mqh В данном файле описаны заголовки всех вышеперечисленных файлов.

Прикрепленные файлы |
MQL5.ZIP (235.99 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (86)
Alexey Oreshkin
Alexey Oreshkin | 10 янв. 2018 в 11:07
transcendreamer:

В статье было чтото запрещённое?


нет, к статье это не имеет отношение.

Denis Sartakov
Denis Sartakov | 16 янв. 2018 в 16:03

вот тут подробно, с примерами обсуждается концепция треугольного арбитража:

https://sites.google.com/site/marketformula/articles/triangular-arbitrage-101

Renat Akhtyamov
Renat Akhtyamov | 26 июл. 2020 в 16:48

на скрине dEURUSD/dt-dGBPUSD/dt-EURGBP*dEURGBP/dt

стратегия мертвая

чо собрались ловить?


Renat Akhtyamov
Renat Akhtyamov | 26 июл. 2020 в 17:32

Допустим лоты одинаковые, тогда есть пять сделок за 10 дней, при гарантии не потерять планируемый профит (итоговый около 8 пунктов за 10 дней) на плавающем спреде при открытии треугольника:

Вобщем на любителя острых ощущений это все......

Ivan Klochkov
Ivan Klochkov | 13 февр. 2022 в 01:51
Alexey Viktorov #:

Прошу объяснить мне

Вроде всё логично, но

1. Покупаем EURUSD. Висит открытая позиция...

3. а доллары мы должны продать. Чтобы продать доллары в GBPUSD, надо эту пару купить. Висит вторая позиция.

4. евро нам надо купить, а фунт, который нам не нужен, — продать. Покупаем EURGBP. И третья позиция.

Чтобы закрыть все эти позиции что надо учесть, чтобы была выгода от этого колдовства?

Учесть следующее:

Нужно, чтобы первые две транзакции сценария «треугольный арбитраж» трансформировались в антипод третьей, а третья закрывала свой антипод. Иначе идея дохлая. Это не работает в МТ4 или 5, т.к. мы по сути делаем ставки на валютных парах, но не получаем власть над отдельно взятыми валютами. Иными словами, например, #1 лонг EURUSD и #2 шорт EURJPY с одинаковыми суммами не превратятся в шорт USDJPY, хоть вторая транзакция и якобы продаёт то же количество Евро что первая купила. Добавление #3 лонг USDJPY с правильно вычисленной суммой также ничего не даст, все три будут хеджировать друг друга с отрицательными начальными значениями нереализованного профита/убытка (иными словами, ведут себя независимо друг от друга).

Хедж-фонды и другие трейдинговые фирмы, тем не менее, имеют возможность выполнять эту стратегию на межбанковском валютном рынке; всё потому что они, в отличие от нас (ритейл-Форекс трейдеров на МТ4/5) имеют возможность покупать и продавать отдельно взятые валюты, а не делать ставки на парах. При этом, они имеют доли секунды на имплементацию, и там конкуренция очень суровая; кто первый успел, тот и красавчик.

В МТ4/5 это невозможно, хоть и перекосы цен, которые по идее можно было бы использовать (если бы работало!!!…) длятся секунды. (Из-за брокерской охоты на клиентские стоп-лоссы, а не отнюдь из-за Межбанка)

Хватит переливать из пустого в порожнее. Дискуссия на тему данной стратегии бесполезна, не нужно городить никаких MQL-макросов. Расходимся, нас обманули.

Пост скриптум: забыл добавить, если какой-то брокер вам предоставит многовалютный торговый терминал, то идея может быть рабочей. Например, у вас есть на счету американские доллары (USD); открываете лонг  EURUSD, и баланс теперь отображается не только в долларах, но и в евро, т.к. вы их купили.
Это не в МТ.
Нечеткая логика в торговых стратегиях Нечеткая логика в торговых стратегиях
В статье рассматривается пример использования нечеткой логики для построения простой торговой системы, с использованием библиотеки Fuzzy. Предложены варианты улучшения системы путем сочетания нечеткой логики, генетических алгоритмов и нейронных сетей.
Создание и тестирование пользовательских символов в MetaTrader 5 Создание и тестирование пользовательских символов в MetaTrader 5
Возможность создавать собственные символы открывает новые горизонты в разработке торговых систем и анализе любых финансовых рынков. Теперь трейдеры могут строить графики и тестировать торговые стратегии на неограниченном количестве финансовых инструментов.
Глубокие нейросети (Часть IV). Создание, обучение и тестирование модели нейросети Глубокие нейросети (Часть IV). Создание, обучение и тестирование модели нейросети
В статье рассматриваются новые возможности пакета darch (v.0.12.0). Описаны результаты обучения глубокой нейросети с различными типами данных, структурой и последовательностью обучения. Проанализированы результаты.
Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio" Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio"
Рассмотрен еще один пользовательский критерий оптимизации торговых стратегий, основанный на анализе графика баланса. Для этого использовалось вычисление линейной регрессии с помощью функции из библиотеки ALGLIB.