![Треугольный арбитраж](https://c.mql5.com/i/articles/overlay.png)
Треугольный арбитраж
Описание идеи
Тема треугольного арбитража с завидной периодичностью поднимается на тематических форумах в Сети. Что же это такое?
Слово "арбитраж" в этом термине подразумевает некоторую нейтральность к рынку. "Треугольный" означает, что портфель состоит из трёх инструментов.
Возьмём самый популярный пример: треугольник "евро — фунт — доллар". В валютных парах он описывается так: EURUSD + GBPUSD + EURGBP. Требуемая нейтральность заключается в попытке купить и продать одновременно одни и те же инструменты, заработав при этом профит.
Выглядит это следующим образом. Любую пару из этого примера представляем через две остальные:
EURUSD=GBPUSD*EURGBP,
или GBPUSD=EURUSD/EURGBP,
или EURGBP=EURUSD/GBPUSD.
Все эти варианты идентичны, и выбор какого-либо из них мы подробнее рассмотрим ниже. Пока же остановимся на первом варианте.
Теперь разберемся с ценами bid и ask. Порядок действий будет такой:
- Покупаем EURUSD, т.е. используем цену ask. На балансе у нас плюс евро и минус доллары.
- Выражаем EURUSD через две другие пары.
- GBPUSD: евро тут нет, но есть доллар, а доллары мы должны продать. Чтобы продать доллары в GBPUSD, надо эту пару купить. Значит, используем ask. При покупке мы получим на баланс плюс фунт и минус доллар.
- EURGBP: евро нам надо купить, а фунт, который нам не нужен, — продать. Покупаем EURGBP, используем ask. На балансе у нас плюс евро и минус фунт. Всё сходится.
Итого имеем: (ask) EURUSD = (ask) GBPUSD * (ask) EURGBP. Мы получили необходимое равенство. Чтобы теперь на нём заработать, мы должны купить одну сторону и продать другую. Здесь возможны два варианта:
- Купить EURUSD дешевле, чем можем его продать, но выраженный по-другому: (ask) EURUSD < (bid) GBPUSD * (bid) EURGBP
- Продать 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); }
Составление треугольников
Чтобы составить треугольники, нам нужно рассмотреть следующие аспекты:
- Источник треугольников — из окна "Обзор рынка" или из заранее подготовленного файла.
- В тестере ли мы? Если да, то в нужно загрузить символы в Обзор рынка. Нет смысла загружать всё возможное, поскольку обычный домашний компьютер просто не справится с нагрузкой. Будем искать заранее подготовленный файл с символами для тестера. Если же его нет, то протестируем стратегию на стандартном треугольнике: EUR+USD+GBP.
- Чтобы упростить код, введём ограничение: все символы в треугольнике должны иметь одинаковый размер контракта.
- Не забываем, что треугольники можно составить только из валютных пар.
Первая необходимая функция — составление треугольников из Обзора рынка.
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.
И последний шаг, который нужен, чтобы закончить подготовку треугольников: сразу загрузим все данные по используемым парам, чтобы впоследствии не тратить время на обращение за ними. Нам понадобится следующее:
- минимальный и максимальный объём торговли по каждому символу;
- количество знаков в цене и в объёме для округления;
- переменная 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
На этом раздел о составлении треугольников можно считать закрытым. Двигаемся дальше.
Режимы работы эксперта
При запуске робота мы можем выбрать один из доступных режимов работы:- Symbols from Market Watch.
- Symbols from file.
- 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; } } }
И напоследок выведем что-нибудь в виде комментариев на экран. Это своеобразная "косметика" для визуального сопровождения. Выводим следующее:
- Всего треугольников отслеживается
- Открытых треугольников
- 5 самых ближайших к открытию треугольников
- Открытые треугольники, если есть
Вот код данной функции:
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 | Ищем мэджик, который можно использовать для текущего треугольника |
9 | 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 | В данном файле описаны заголовки всех вышеперечисленных файлов. |
![Нечеткая логика в торговых стратегиях](https://c.mql5.com/2/29/Avatar.png)
![Создание и тестирование пользовательских символов в MetaTrader 5](https://c.mql5.com/2/29/MQL5_Custom_Symbols.png)
![Глубокие нейросети (Часть IV). Создание, обучение и тестирование модели нейросети](https://c.mql5.com/2/48/Deep_Neural_Networks_04.png)
![Оптимизируем стратегию по графику баланса и сравниваем результаты с критерием "Balance + max Sharpe Ratio"](https://c.mql5.com/2/29/loqekqlg1xfv_0uf48ukgw_89_1k4r4rf_daa1n9z2.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
В статье было чтото запрещённое?
нет, к статье это не имеет отношение.
вот тут подробно, с примерами обсуждается концепция треугольного арбитража:
https://sites.google.com/site/marketformula/articles/triangular-arbitrage-101
на скрине dEURUSD/dt-dGBPUSD/dt-EURGBP*dEURGBP/dt
стратегия мертвая
чо собрались ловить?
Допустим лоты одинаковые, тогда есть пять сделок за 10 дней, при гарантии не потерять планируемый профит (итоговый около 8 пунктов за 10 дней) на плавающем спреде при открытии треугольника:
Вобщем на любителя острых ощущений это все......
Прошу объяснить мне
Вроде всё логично, но
1. Покупаем EURUSD. Висит открытая позиция...
3. а доллары мы должны продать. Чтобы продать доллары в GBPUSD, надо эту пару купить. Висит вторая позиция.
4. евро нам надо купить, а фунт, который нам не нужен, — продать. Покупаем EURGBP. И третья позиция.
Чтобы закрыть все эти позиции что надо учесть, чтобы была выгода от этого колдовства?