
Готовые советники из Мастера MQL5 работают в MetaTrader 4
Клиентские терминалы MetaTrader 4 и MetaTrader 5 предоставляют своим пользователям возможность легко создавать прототипы программ на языке MQL с помощью встроенного Мастера (MQL Wizard). Мастера обоих версий терминалов очень похожи, но все же имеют одно важное отличие. В Мастере MetaTrader 5 есть пункт генерации готовых советников, а в MetaTrader 4 его нет. Дело в том, что такие советники работают на основе классов стандартной библиотеки MQL, т.е. набора заголовочных файлов, поставляемых вместе с терминалом. В MetaTrader 4 такая библиотека тоже есть, но в ней нет торговых классов из MQL5. В частности, там отсутствуют классы, ответственные за подготовку и отправку торговых приказов, вычисление сигналов на основе показаний индикаторов или структуры цен, трейлинга, управления деньгами, а все это — необходимая база для построения автоматически генерируемых советников.
Такая ситуация сложилась исторически в результате поэтапного развития MQL5. Новый язык появился изначально в MetaTrader 5, и именно для этого терминала была разработана стандартная библиотека классов. Только некоторое время спустя MQL5 был интегрирован и в MetaTrader 4, но поскольку торговые функции в API двух версий терминалов сильно разнятся, стандартная библиотека была перенесена в более ранний продукт не полностью — без торговых классов. В результате в Мастере MetaTrader 4 нет опции генерации готовых советников.
Вместе с тем, MetaTrader 4 до сих пор популярен, и возможность генерации готовых советников для него очень пригодилась бы. Поскольку новые функции в MetaTrader 4 больше не добавляются, а лишь вносятся исправления ошибок, мы вряд ли уже увидим усовершенствования в его Мастере. Однако нам никто не мешает использовать Мастер MetaTrader 5, а затем переносить полученный код в MetaTrader 4. Чтобы этот код заработал и там, нужна лишь малость — набор торговых классов стандартной библиотеки, адаптированных для прежнего MQL API MetaTrader 4. Иными словами, нужно скопировать из стандартной библиотеки MetaTrader 5 те классы, которых недостает в MetaTrader 4, и реализовать для них эмуляцию торгового окружения пятой версии.
Планирование
Любую работу лучше выполнять, придерживаясь заранее составленного плана. Пример такого подхода, который применяется при разработке программ, — так называемая модель водопада. Она хорошо подходит для нашего случая, когда разработка не только выполняется как таковая, но еще и описывается в статье вроде этой. Однако на практике портировать код MQL5 в MQL4 (или обратно) эффективнее с применением одного из гибких подходов, таких как экстремальное программирование. Его девиз: меньше планов — больше дела. Буквально это значит, что можно взять исходный код, попытаться откомпилировать его и затем последовательно править все возникающие ошибки. План, который я предлагаю в этой статье, родился не сразу, а постепенно, как раз на основе "подсказок" недовольного компилятора.
При сравнении библиотек двух терминалов легко заметить, что в версии 4 не хватает папок Trade, Expert, и Models. Значит, основная работа будет заключаться в портировании всех имеющихся в этих папках классов под "четверку". Помимо них, нам, очевидно, потребуется подправить что-то и в папке Indicators. Она есть в библиотеке версии 4, но принципы работы с индикаторами в двух терминалах отличаются. Однако в любом случае, следует придерживаться принципа наименьших правок файлов библиотеки, ведь периодически она обновляется, и тогда нужно будет синхронизировать, подгонять наши правки под официальные.
Все скопированные файлы в той или иной степени ссылаются на торговые MQL API пятой версии. Поэтому нам придется разработать более или менее полный набор определений и функций, которые, сохранив тот же программный интерфейс, будут преобразовывать все обращения к унаследованным MQL API четвертой версии. Рассмотрим подробнее, что именно должно войти в эмулируемое торговое окружение. Начнем с типов, так как это кирпичики, на которых будет затем строиться здание, т.е. алгоритм и программа целиком.
Самые простые типы — перечисления. Они используются в большинстве функций напрямую или опосредованно, через структуры. Поэтому порядок адаптации будет такой: перечисления, структуры, константы, функции.
Перечисления
Часть необходимых перечислений уже перенесена в MetaTrader 4. Это, например, свойства ордеров: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE, ENUM_ORDER_PROPERTY_STRING. С одной стороны, это вроде бы удобно, но с другой, не все из этих перечислений определены точно так же, как в MetaTrader 5, и это создает сложности.
Например, ENUM_ORDER_TYPE в MetaTrader 5 содержит больше типов ордеров, чем в MetaTrader 4. Если оставить ENUM_ORDER_TYPE как есть, мы получим ошибки компиляции, поскольку скопированный код ссылается на отсутствующие элементы. Переопределить или доопределить перечисление невозможно. Поэтому наиболее простой вариант — макроопределение для препроцессора, вроде такого:
// ENUM_ORDER_TYPE extension #define ORDER_TYPE_BUY_STOP_LIMIT ((ENUM_ORDER_TYPE)6) #define ORDER_TYPE_SELL_STOP_LIMIT ((ENUM_ORDER_TYPE)7) #define ORDER_TYPE_CLOSE_BY ((ENUM_ORDER_TYPE)8)
Другие перечисления, которых нет в MetaTrader 4, мы можем смело определить по аналогии с "пятеркой", например:
enum ENUM_ORDER_TYPE_FILLING { ORDER_FILLING_FOK, ORDER_FILLING_IOC, ORDER_FILLING_RETURN };
Таким образом, нам следует определить (или дополнить константами) указанные ниже перечисления. Их, на первый взгляд много, но работа тривиальная — достаточно скопировать их из документации (ссылки на соответствующие разделы приведены ниже; звездочкой помечены существующие перечисления, требующие "шлифовки").
- Приказ (Order)
- ENUM_ORDER_TYPE_TIME
- ENUM_ORDER_STATE
- ENUM_ORDER_TYPE_FILLING
- ENUM_ORDER_TYPE (*)
- ENUM_ORDER_PROPERTY_INTEGER (*)
- ENUM_ORDER_PROPERTY_STRING (*)
- Позиция (Position)
- ENUM_POSITION_TYPE
- ENUM_POSITION_PROPERTY_INTEGER
- ENUM_POSITION_PROPERTY_DOUBLE
- ENUM_POSITION_PROPERTY_STRING
- Сделка (Deal)
- ENUM_DEAL_ENTRY
- ENUM_DEAL_TYPE
- ENUM_DEAL_PROPERTY_INTEGER
- ENUM_DEAL_PROPERTY_DOUBLE
- ENUM_DEAL_PROPERTY_STRING
- Типы торговых операций
- ENUM_TRADE_REQUEST_ACTIONS
MetaTrader 4 уже содержит определения перечислений, описывающих символы, такие как ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING. Некоторые элементы в них лишь зарезервированы, но не работают (что отражено в документации). Это ограничения платформы MetaTrader 4 по сравнению с MetaTrader 5, и мы должны это принять как есть. Для нас лишь важно, что данные перечисления не нужно определять в самом проекте.
Структуры
Помимо перечислений, в торговых функциях MetaTrader 5 используются структуры. Их определения тоже можно взять в документации (ссылки на соответствующие разделы приведены ниже).
Макроопределения
В дополнение к перечисленным выше типам, исходные коды "пятерки" используют множество констант, которые в этом проекте проще всего определить с помощью директивы #define препроцессора.
- Коды возврата торгового сервера
- TRADE_RETCODE_...
- Информация об инструментах
- Набор флагов экспирации — SYMBOL_EXPIRATION_...
- Набор флагов исполнения приказов — SYMBOL_FILLING_...
- Набор флагов типов ордеров — SYMBOL_ORDER_...
Торговые функции
Последний и самый важный пункт нашего плана — непосредственно торговые функции. Мы сможем приступить к их реализации только после того, как будут определены все вышеперечисленные типы и константы.
Список торговых функций довольно внушительный. Их можно разбить на 4 группы:
- Ордера
- Позиции
- История ордеров
- История сделок
Наконец, нам пригодятся такие простые подстановки как:
#define MQL5InfoInteger MQLInfoInteger #define MQL5InfoString MQLInfoString
Это, по сути, одни и те же функции ядра терминала, но их имена слегка отличаются в MQL5 и MQL4.
Прежде чем приступить непосредственно к реализации, мы должны придумать, каким образом отображать торговую модель MetaTrader 5 на торговую модель MetaTrader 4.
Отображение
Попробуем провести параллель между сущностями MetaTrader 5 и MetaTrader 4. Начать легче с "четверки". Там есть одно универсальное понятие "ордер", и оно используется практически для всего — для рыночных ордеров, отложенных ордеров, истории торговых операций. Во всех этих случаях ордер находится в разных состояниях. В "пятерке" рыночные ордера — это позиции, отложенные ордера — просто ордера, а история операций записывается с помощью сделок.
В простейшем случае MetaTrader 5 работает примерно так. Для формирования позиции на торговый сервер отправляется приказ о входе в рынок. Для закрытия позиции поступает другой приказ, о выходе из рынка. Исполнение каждого из приказов происходит в рамках соответствующей сделки, которая попадает в историю торговли. Таким образом, один рыночный ордер "четверки" должен отображаться в эмулируемом "пятерочном" торговом окружении как:
- ордер входа
- сделка входа
- позиция
- ордер выхода
- сделка выхода
Сразу же напомним, что MetaTrader 5 изначально был строго неттинговой платформой, то есть одновременно по одному символу могла существовать только одна позиция. Все приказы по одному символу увеличивали, уменьшали или целиком удаляли совокупный объем по символу, а также меняли для него общие уровни стопов и тейков. Такой режим отсутствует в MetaTrader 4, и нам пришлось бы довольно тяжело с воплощением данного проекта, если бы в MetaTrader 5 однажды не появилась поддержка хеджирования. Это тот самый режим, который исповедует MetaTrader 4: исполнение каждого приказа формирует отдельную "позицию" (в терминах MetaTrader 5), так что даже по одному и тому же символу может существовать несколько открытых ордеров, в том числе и разнонаправленных.
Реализация
Эмуляция торгового окружения MetaTrader 5
Всё эмулируемое окружение, включая типы, константы и функции, мы разместим для простоты в одном-единственном заголовочном файле MT5Bridge.mqh. Хороший стиль программирования, вероятно, потребовал бы их разнесения по отдельным файлам. Такое структурирование особенно важно для больших проектов и проектов, над которыми работает группа людей. Однако с точки зрения распространения и установки один файл удобнее.
Итак, согласно плану, определим все перечисления, константы и структуры. Это рутинная работа копирования, без каких-либо сложностей. Вряд ли стоит её пояснять более подробно, чем те замечания, что уже были приведены при планировании. Затем заглянем вновь на страницу документации о Торговых функциях и приступим к более интеллектуальной части — написанию кода всех этих функций.
Начнем с текущих операций, включающих обработку рыночных и отложенных ордеров, а также позиций.
Для этого нужна суперуниверсальная "пятерочная" функция OrderSend.
bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result) {
В ней, в зависимости от типа запроса, необходимо использовать один из типов ордеров MetaTrader 4.
int cmd; result.retcode = 0; switch(request.type) { case ORDER_TYPE_BUY: cmd = OP_BUY; break; case ORDER_TYPE_SELL: cmd = OP_SELL; break; case ORDER_TYPE_BUY_LIMIT: cmd = OP_BUYLIMIT; break; case ORDER_TYPE_SELL_LIMIT: cmd = OP_SELLLIMIT; break; case ORDER_TYPE_BUY_STOP: cmd = OP_BUYSTOP; break; case ORDER_TYPE_SELL_STOP: cmd = OP_SELLSTOP; break; default: Print("Unsupported request type:", request.type); return false; }
Переданный код операции в поле action позволяет по-разному обрабатывать установку, удаление и модификацию ордеров. Например, открытие рыночного или создание отложенного ордера может быть реализовано следующим образом.
ResetLastError(); if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING) { if(request.price == 0) { if(cmd == OP_BUY) { request.price = MarketInfo(request.symbol, MODE_ASK); } else if(cmd == OP_SELL) { request.price = MarketInfo(request.symbol, MODE_BID); } } if(request.position > 0) { if(!OrderClose((int)request.position, request.volume, request.price, (int)request.deviation)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = request.position | 0x8000000000000000; result.order = request.position | 0x8000000000000000; result.volume = request.volume; result.price = request.price; } } else { int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (int)request.deviation, request.sl, request.tp, request.comment, (int)request.magic, request.expiration); if(ticket == -1) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = ticket; result.order = ticket; result.request_id = ticket; if(OrderSelect(ticket, SELECT_BY_TICKET)) { result.volume = OrderLots(); result.price = OrderOpenPrice() > 0 ? OrderOpenPrice() : request.price; result.comment = OrderComment(); result.ask = MarketInfo(OrderSymbol(), MODE_ASK); result.bid = MarketInfo(OrderSymbol(), MODE_BID); } else { result.volume = request.volume; result.price = request.price; result.comment = ""; } } } }
Основную работу выполняет привычная для MetaTrader 4 функция OrderSend с множеством параметров. После её вызова результаты работы соответствующим образом записываются в выходную структуру.
Особо отметим, что в MetaTrader 5 закрытие имеющегося рыночного ордера происходит путем открытия другого ордера противоположного направления, а в поле position передается идентификатор закрываемой позиции. В этом случае, т.е. когда поле position не пустое, приведенный выше код пытается закрыть ордер с помощью функции OrderClose. При этом в качестве идентификатора позиции используется тикет самого ордера. Это логично, так как в "четверке" каждый ордер создает свою собственную позицию. Сделка получает тот же самый тикет.
Что же касается виртуального ордера закрытия позиции (его тут на самом деле нет), то в качестве его тикета искусственным образом используется исходный номер, дополненный установленным в 1 старшим битом. Это будет применяться в дальнейшем при перечислении ордеров и сделок.
Теперь посмотрим, как можно реализовать изменение уровней в открытой позиции.
else if(request.action == TRADE_ACTION_SLTP) // change opened position { if(OrderSelect((int)request.position, SELECT_BY_TICKET)) { if(!OrderModify((int)request.position, OrderOpenPrice(), request.sl, request.tp, 0)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = OrderTicket(); result.order = OrderTicket(); result.request_id = OrderTicket(); result.volume = OrderLots(); result.comment = OrderComment(); } } else { result.retcode = TRADE_RETCODE_POSITION_CLOSED; } }
Вполне очевидно, что для этого используется OrderModify.
Эта же функция используется и для изменения отложенного ордера.
else if(request.action == TRADE_ACTION_MODIFY) // change pending order { if(OrderSelect((int)request.order, SELECT_BY_TICKET)) { if(!OrderModify((int)request.order, request.price, request.sl, request.tp, request.expiration)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; result.deal = OrderTicket(); result.order = OrderTicket(); result.request_id = OrderTicket(); result.price = request.price; result.volume = OrderLots(); result.comment = OrderComment(); } } else { result.retcode = TRADE_RETCODE_INVALID_ORDER; } }
Удаление отложенного ордера выполняет стандартная функция OrderDelete.
else if(request.action == TRADE_ACTION_REMOVE) { if(!OrderDelete((int)request.order)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; } }
Наконец, закрытие одной позиции с помощью другой (встречной) эквивалентно в контексте MetaTrader 4 закрытию встречных ордеров.
else if(request.action == TRADE_ACTION_CLOSE_BY) { if(!OrderCloseBy((int)request.position, (int)request.position_by)) { result.retcode = GetLastError(); } else { result.retcode = TRADE_RETCODE_DONE; } } return true; }
Помимо OrderSend, MetaTrader 5 предоставляет асинхронную функцию OrderSendAsync. Мы её реализовывать не будем, а все случаи применения асинхронного режима в библиотеке отключим, т.е. фактически заменим на синхронный вариант.
Установка рабочих ордеров часто сопровождается вызовами 3 других функций: OrderCalcMargin, OrderCalcProfit, OrderCheck.
Вот один из вариантов, как их можно реализовать с помощью средств, доступных в MetaTrader 4.
int EnumOrderType2Code(int action){ // ORDER_TYPE_BUY/ORDER_TYPE_SELL and derivatives return (action % 2 == 0) ? OP_BUY : OP_SELL; }
bool OrderCalcMargin( ENUM_ORDER_TYPE action, string symbol, double volume, double price, double &margin ) { int cmd = EnumOrderType2Code(action); double m = AccountFreeMarginCheck(symbol, cmd, volume); if(m <= 0 || GetLastError() == ERR_NOT_ENOUGH_MONEY) { return false; } margin = AccountFreeMargin() - m; return true; }
bool OrderCalcProfit( ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit ) { int cmd = EnumOrderType2Code(action); if(cmd > -1) { int points = (int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT)); if(cmd == OP_SELL) points = -points; profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT)); return true; } return false; } bool OrderCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result) { if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT) || request.volume < MarketInfo(request.symbol, MODE_MINLOT) || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP)) { result.retcode = TRADE_RETCODE_INVALID_VOLUME; return false; } double margin; if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin)) { result.retcode = TRADE_RETCODE_NO_MONEY; return false; } if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING) && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET && (request.sl != 0 || request.tp != 0)) { result.retcode = TRADE_RETCODE_INVALID_STOPS; return false; } result.balance = AccountBalance(); result.equity = AccountEquity(); result.profit = AccountEquity() - AccountBalance(); result.margin = margin; result.margin_free = AccountFreeMargin(); result.margin_level = 0; result.comment = ""; return true; }
Здесь активно используются встроенные функции AccountEquity, AccountFreeMargin, AccountFreeMarginCheck, а также стоимость пункта инструмента и прочие его настройки, получаемые с помощью вызовов MarketInfo.
Для получения общего числа позиций достаточно вернуть количество открытых рыночных ордеров.
int PositionsTotal() { int count = 0; for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { count++; } } } return count; }
Для получения символа позиции по её номеру необходимо перебрать в цикле все ордера, подсчитывая лишь рыночные.
string PositionGetSymbol(int index) { int count = 0; for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { if(index == count) { return OrderSymbol(); } count++; } } } return ""; }
Аналогичным образом строится функция для получения тикета позиции по её номеру.
ulong PositionGetTicket(int index) { int count = 0; for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() <= OP_SELL) { if(index == count) { return OrderTicket(); } count++; } } } return 0; }
Для выбора позиции по названию символа также пройдемся в цикле по рыночным ордерам и остановимся на первом, совпадающем по символу.
bool PositionSelect(string symbol) { for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderSymbol() == symbol && (OrderType() <= OP_SELL)) { return true; } } } return false; }
Реализация выбора позиции по тикету не требует цикла.
bool PositionSelectByTicket(ulong ticket) { if(OrderSelect((int)ticket, SELECT_BY_TICKET)) { if(OrderType() <= OP_SELL) { return true; } } return false; }
Свойства выбранной позиции должна возвращать тройка функций, привычных для MetaTrader 5 — _GetDouble, _GetInteger, _GetString. Приведем здесь их реализацию для позиций, а для ордеров и сделок они будут выглядеть очень похоже, и потому останутся за рамками статьи. Желающие могут ознакомиться с их кодом в прилагаемом файле.
// позиция = ордер, только OP_BUY или OP_SELL ENUM_POSITION_TYPE Order2Position(int type) { return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; } bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, long &long_var) { switch(property_id) { case POSITION_TICKET: case POSITION_IDENTIFIER: long_var = OrderTicket(); return true; case POSITION_TIME: case POSITION_TIME_UPDATE: long_var = OrderOpenTime(); return true; case POSITION_TIME_MSC: case POSITION_TIME_UPDATE_MSC: long_var = OrderOpenTime() * 1000; return true; case POSITION_TYPE: long_var = Order2Position(OrderType()); return true; case POSITION_MAGIC: long_var = OrderMagicNumber(); return true; } return false; } bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, double &double_var) { switch(property_id) { case POSITION_VOLUME: double_var = OrderLots(); return true; case POSITION_PRICE_OPEN: double_var = OrderOpenPrice(); return true; case POSITION_SL: double_var = OrderStopLoss(); return true; case POSITION_TP: double_var = OrderTakeProfit(); return true; case POSITION_PRICE_CURRENT: double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK); return true; case POSITION_COMMISSION: double_var = OrderCommission(); return true; case POSITION_SWAP: double_var = OrderSwap(); return true; case POSITION_PROFIT: double_var = OrderProfit(); return true; } return false; } bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, string &string_var) { switch(property_id) { case POSITION_SYMBOL: string_var = OrderSymbol(); return true; case POSITION_COMMENT: string_var = OrderComment(); return true; } return false; }
Аналогично позициям, которые являются рыночными ордерами, следует реализовать и набор функций для обработки отложенных ордеров. Однако тут есть одна сложность. Мы не можем реализовать функцию OrdersTotal и прочие OrderGet_, поскольку они уже определены в ядре, а переопределять встроенные функции нельзя. Компилятор выдает ошибку вида:
'OrderGetString' - override system function MT5Bridge.mqh
Поэтому мы вынуждены дать другие имена всем функциям с именами, начинающимися с префикса Order_. Логично начинать их названия с PendingOrder_, поскольку они обрабатывают исключительно отложенные ордера. Например:
int PendingOrdersTotal() { int count = 0; for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() > OP_SELL) { count++; } } } return count; }
Затем в коде стандартной библиотеки нужно будет заменить все вызовы на наши новые функции из MT5Bridge.mqh.
Функция OrderGetTicket, возвращающая тикет ордера по номеру, отсутствует в MetaTrader 4, поэтому оставим её имя как есть, т.е. в соответствии с API MetaTrader 5.
ulong OrderGetTicket(int index) { int count = 0; for(int i = 0; i < ::OrdersTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS)) { if(OrderType() > OP_SELL) { if(index == count) { return OrderTicket(); } count++; } } } return 0; }
Функция OrderSelect существует в MetaTrader 4 с расширенным списком параметров по сравнению с MetaTrader 5, поэтому оставим её вызовы, дополнив их необходимым параметром SELECT_BY_TICKET.
Полную реализацию функций чтения свойств отложенных ордеров можно найти в прилагаемом заголовочном файле.
Теперь обратимся к функциям для работы с историей ордеров и сделок. Их реализация потребует некоторой изобретательности. Приведенный далее вариант — лишь один из многих возможных — выбран из-за простоты.
Каждый рыночный ордер MetaTrader 4 отображается в истории двумя ордерами а-ля MetaTrader 5: входным и выходным. Кроме того, в истории должна быть и соответствующая пара сделок. Отложенные ордера отображаются как есть. Храниться история будет в двух массивах с тикетами.
int historyDeals[], historyOrders[];
Заполнять их будет функция HistorySelect из MQL5 API.
bool HistorySelect(datetime from_date, datetime to_date) { int deals = 0, orders = 0; ArrayResize(historyDeals, 0); ArrayResize(historyOrders, 0); for(int i = 0; i < OrdersHistoryTotal(); i++) { if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) { if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date) { if(OrderType() <= OP_SELL) // deal { ArrayResize(historyDeals, deals + 1); historyDeals[deals] = OrderTicket(); deals++; } ArrayResize(historyOrders, orders + 1); historyOrders[orders] = OrderTicket(); orders++; } } } return true; }
После того, как массивы заполнены, можно получить размер истории.
int HistoryDealsTotal() { return ArraySize(historyDeals) * 2; } int HistoryOrdersTotal() { return ArraySize(historyOrders) * 2; }
Размеры массивов умножаются на 2, поскольку каждый ордер MetaTrader 4 — это два ордера или две сделки MetaTrader 5. Для отложенных ордеров это не так, но для сохранения общности подхода мы все равно резервируем 2 тикета, только один из них не будет использоваться (см. ниже функцию HistoryOrderGetTicket). Сделка входа в рынок будет получать тот же самый тикет, что у порождающего её ордера МетаТрейдера 4. А для сделки выхода будем этот тикет дополнять единичным старшим битом.
ulong HistoryDealGetTicket(int index) { if(OrderSelect(historyDeals[index / 2], SELECT_BY_TICKET, MODE_HISTORY)) { // odd - enter - positive, even - exit - negative return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000); } return 0; }
Четные номера в истории всегда содержат тикеты на вход (реальные), нечетные — на выход (виртуальные).
Для ордеров все немного сложнее, поскольку среди них могут быть отложенные, которые отображаются один в один. В этом случае, четный номер вернет правильный тикет отложенного ордера, а следующий нечетный — 0.
ulong HistoryOrderGetTicket(int index) { if(OrderSelect(historyOrders[index / 2], SELECT_BY_TICKET, MODE_HISTORY)) { if(OrderType() <= OP_SELL) { return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000); } else if(index % 2 == 0) // pending order is returned once { return OrderTicket(); } else { Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> 0"); } } return 0; }
Выбор сделки по тикету реализуются с учетом этой особенности с установкой старшего бита — здесь его нужно сбрасывать.
bool HistoryDealSelect(ulong ticket) { ticket &= ~0x8000000000000000; return OrderSelect((int)ticket, SELECT_BY_TICKET, MODE_HISTORY); }
Для ордера все полностью аналогично.
#define HistoryOrderSelect HistoryDealSelect
Имея сделку, выбранную с помощью HistoryDealSelect или HistoryDealGetTicket, можно написать реализацию функций доступа к свойствам сделки.
#define REVERSE(type) ((type + 1) % 2) ENUM_DEAL_TYPE OrderType2DealType(const int type) { static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -1, -1, -1, -1, DEAL_TYPE_BALANCE}; return types[type]; } bool HistoryDealGetInteger(ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, long &long_var) { bool exit = ((ticket_number & 0x8000000000000000) != 0); ticket_number &= ~0x8000000000000000; if(OrderSelect((int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY)) { switch(property_id) { case DEAL_TICKET: case DEAL_ORDER: case DEAL_POSITION_ID: long_var = OrderTicket(); return true; case DEAL_TIME: long_var = exit ? OrderCloseTime() : OrderOpenTime(); return true; case DEAL_TIME_MSC: long_var = (exit ? OrderCloseTime() : OrderOpenTime()) * 1000; return true; case DEAL_TYPE: long_var = OrderType2DealType(exit ? REVERSE(OrderType()) : OrderType()); return true; case DEAL_ENTRY: long_var = exit ? DEAL_ENTRY_OUT : DEAL_ENTRY_IN; return true; case DEAL_MAGIC: long_var = OrderMagicNumber(); return true; } } return false; } bool HistoryDealGetDouble(ulong ticket_number, ENUM_DEAL_PROPERTY_DOUBLE property_id, double &double_var) { bool exit = ((ticket_number & 0x8000000000000000) != 0); ticket_number &= ~0x8000000000000000; switch(property_id) { case DEAL_VOLUME: double_var = OrderLots(); return true; case DEAL_PRICE: double_var = exit ? OrderClosePrice() : OrderOpenPrice(); return true; case DEAL_COMMISSION: double_var = exit? 0 : OrderCommission(); return true; case DEAL_SWAP: double_var = exit ? OrderSwap() : 0; return true; case DEAL_PROFIT: double_var = exit ? OrderProfit() : 0; return true; } return false; } bool HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id, string &string_var) { switch(property_id) { case DEAL_SYMBOL: string_var = OrderSymbol(); return true; case DEAL_COMMENT: string_var = OrderComment(); return true; } return false; }
Надеюсь, идея понятна. Точно так же реализуется и группа функций для работы с ордерами в истории.
Изменения в файлах стандартной библиотеки
Некоторые необходимые правки библиотеки уже обсуждались при реализации функций. Вы можете самостоятельно сравнить файлы из поставки MetaTrader 5 и те, что получились в этом проекте, чтобы получить полный список изменений. Далее рассматриваются только наиболее важные моменты, а комментарии по мелким правкам опущены. Во многие файлы вставлена новая директива #include для подключения MT5Bridge.mqh.
Таблица основных изменений в файлах стандартной библиотеки
Файл/Метод | Изменения | |
---|---|---|
Trade.mqh | SetAsyncMode | удалена строка, применяющая асинхронный режим, поскольку он не поддерживается |
SetMarginMode | явно прописан режим ACCOUNT_MARGIN_MODE_RETAIL_HEDGING | |
OrderOpen | явно прописана комбинация флагов, задающих режим экспирации, как SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED | |
OrderTypeCheck | исключены случаи обработки несуществующих типов ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT | |
OrderSend | удален вызов отсутствующей асинхронной функции OrderSendAsync | |
OrderInfo.mqh | все вызовы функций OrderGetInteger, OrderGetDouble, OrderGetString заменены на одноименные функции с префиксом PendingOrder | |
все вызовы OrderSelect(m_ticket) заменены на OrderSelect((int)m_ticket, SELECT_BY_TICKET) | ||
PositionInfo.mqh | FormatPosition SelectByIndex |
установлен режим маржи ACCOUNT_MARGIN_MODE_RETAIL_HEDGING |
SymbolInfo.mqh | Refresh | удалены многие проверки, не поддерживаемые в MetaTrader 4 |
AccountInfo.mqh | MarginMode | возвращает константу ACCOUNT_MARGIN_MODE_RETAIL_HEDGING |
Expert.mqh | TimeframeAdd TimeframesFlags |
удалены неподдерживаемые таймфреймы |
ExpertBase.mqh | добавлен #include <Indicators\IndicatorsExt.mqh> | |
SetMarginMode | установлен безусловно в ACCOUNT_MARGIN_MODE_RETAIL_HEDGING |
Файл IndicatorsExt.mqh необходим для исправления мелких ошибок в стандартном файле Indicators.mqh. Кроме того, он включает другой необходимый для индикаторов заголовочный файл TimeSeriesExt.mqh.
Файл TimeSeriesExt.mqh содержит определение классов, которые нужны для торговли а-ля MetaTrader 5, но отсутствуют в стандартном файле TimeSeries.mqh, поставляемом с MetaTrader 4.
В частности, это классы: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume. Многие из них — всего лишь заглушки, которые ничего не делают (и не могут делать в связи с недоступностью соответствующего функционала в MetaTrader 4).
Тестирование
Установив адаптированные торговые классы стандартной библиотеки в каталог Include MetaTrader 4 (с сохранением иерархии подкаталогов), а также скопировав MT5Bridge.mqh в папку Include/Trade, мы можем компилировать и запускать эксперты, сгенерированные Мастером MetaTrader 5, непосредственно в MetaTrader 4.
MetaTrader 5 поставляется с несколькими примерами сгенерированных экспертов (в папке Experts/Advisors). Возьмем один из них — ExpertMACD.mq5. Скопируем его в папку MQL4/Experts и переименуем в ExpertMACD.mq4. Компиляция в редакторе должна дать примерно такой результат:
Эксперт из Мастера MetaTrader 5 компилируется в MetaTrader 4
Видно, что библиотечные файлы подключены и обрабатываются без ошибок и предупреждений. Конечно, отсутствие ошибок компиляции не гарантирует отсутствие проблем в логике программы, но это уже предмет для дальнейших проверок на практике.
Запустим скомпилированный эксперт с настройками по умолчанию в тестере MetaTrader 4.
Отчет тестирования MetaTrader 4 для эксперта, сгенерированного в MetaTrader 5
При желании можно убедиться, что в журнале отсутствуют явные ошибки обработки приказов.
На графике EURUSD M15 торговля данного эксперта выглядит нормально, включая, в частности, установку уровней стоп-лосса и тейк-профита.
Окно с графиком, иллюстрирующим работу эксперта из Мастера MetaTrader 5 в MetaTrader 4
Сравним с результатами тестера MetaTrader 5.
Отчет тестирования MetaTrader 5 для сгенерированного эксперта
Очевидно, что различия есть. Они могут объясняться расхождениями как в самих котировках (например, MetaTrader 5 использует плавающий спред), так и в алгоритмах тестера. В целом, тесты похожи: примерно совпадает количество трейдов и общий характер кривой баланса.
Разумеется, пользователь может сгенерировать в Мастере собственный эксперт с совершенно произвольным набором модулей, и он должен также просто переноситься в MetaTrader 4. В ходе тестирования проекта были проверены, в частности, эксперты с трейлингом и переменным размером лота.
Заключение
Итак, мы рассмотрели один из возможных способов переноса советников, генерируемых Мастером MetaTrader 5, на платформу MetaTrader 4. Основной его плюс — относительная простота реализации, основанная на максимально полном использовании имеющегося кода торговых классов из стандартной библиотеки MetaTrader 5. Основной недостаток — необходимость иметь на компьютере оба терминала: один — для генерации советников, а второй — для их использования.
Ниже приложены 2 файла:
- архив с модифицированными файлами стандартной библиотеки, который нужно развернуть, с сохранением иерархии подкаталогов, в каталоге Include MetaTrader 4. В архиве находятся только файлы, которых нет в поставке терминала, поэтому нет опасности перезаписи существующих файлов;
- файл MT5Bridge.mqh, который следует скопировать в папку Include/Trade.
Данная версия библиотеки была взята из 1545-й сборки MetaTrader 5. Будущие сборки могут содержать изменения в стандартной библиотеке, которые могут оказаться полезными (что потребует повторно выполнить слияние с правками эмулятора). В идеале было бы здорово когда-нибудь увидеть версию стандартной библиотеки от MetaQuotes, в которой с самого начала с помощью директив условной компиляции совмещены два варианта реализации торговых классов — для MetaTrader 5 и для MetaTrader 4.
Следует отметить, что реализовать полную эмуляцию торгового окружения MetaTrader 5 в MetaTrader 4 не удастся. Новый терминал предоставляет новые возможности, отсутствующие в старом на уровне ядра. Поэтому потенциально возможны ситуации, когда те или иные модули, используемые в генерируемых советниках, откажутся работать.
Также не стоит забывать и о том, что предоставленная реализация эмулятора распространяется в статусе бета-версии и может содержать скрытые ошибки. Лишь долгое и разностороннее тестирование позволит получить окончательный программный продукт, пригодный для реальной торговли. Наличие исходных кодов позволяет делать это сообща.





- Бесплатные приложения для трейдинга
- Форексный VPS бесплатно на 24 часа
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Есть такое явление, как публикация в кодобазе советников, как результат конвертации MT4 -> MT5 через СБ.
Похоже, статью можно использовать и таким авторам для самопроверки. Если конвертация правильная, то обратная конвертация через MT5Bridge должна дать идентичный с MT4-оригиналом результат.
Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий
MT4-Tester VS MT5-Tester
fxsaber, 2017.05.08 15:12
Сконвертировал Ваш код в MT4 через MT5Bridge. MT4build1072
Оригинальный код
Результаты после конвертации идентичны! Скорость упала в два раза.
Мой MT5 советник (сгенерированный, но со своей реализацией CExpertSignal вместо стандартных индикаторов) с вашими инклудами скомпилился и тестировался на MT4 без проблем, спасибо!
Но вот сейчас отправил советник на реал, и оказалось что он совсем не торгует. Никаких ошибок, ничего не показывает. Просто не торгует. Долго копался в коде, нашёл причину - функция bool CTrade::FillingCheck(const string symbol) в Trade.mqh
Для рыночных ордеров срабатывает такая проверка -
В моём случае и m_type_filling и filling равны нулю, поэтому функция возвращает false.
filling по логике кода не должен быть равен нулю, но согласно справке SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4 не поддерживается. Поэтому в тестере проверка почему-то проходит, и может быть у некоторых брокеров на реале тоже. Но для меня не прокатило, я пока-что просто поменял функцию чтоб весь код пропускался и из функции возвращалось true.
Для рыночных ордеров срабатывает такая проверка -
В моём случае и m_type_filling и filling равны нулю, поэтому функция возвращает false.
filling по логике кода не должен быть равен нулю, но согласно справке SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4 не поддерживается. Поэтому в тестере проверка почему-то проходит, и может быть у некоторых брокеров на реале тоже. Но для меня не прокатило, я пока-что просто поменял функцию чтоб весь код пропускался и из функции возвращалось true.
Спасибо за сообщение. Я на такое не натыкался. Данный метод оставлен без изменений. Видимо, нужно все filling переменные устанавливать в конкретные константы в зависимости от типа ордера и/или инструмента (вероятно, это нельзя вытащить через API никак, а тестер пользуется какими-то умолчаниями). Если кто-то знает, как МТ4 внутри себя выбирает filling - поделитесь.
Если кто-то знает, как МТ4 внутри себя выбирает filling - поделитесь.
Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий
Особенности языка mql5, тонкости и приёмы работы
fxsaber, 2017.02.25 16:12
ENUM_ORDER_TYPE_FILLING GetFilling( const string Symb, const uint Type = ORDER_FILLING_FOK )
{
const ENUM_SYMBOL_TRADE_EXECUTION ExeMode = (ENUM_SYMBOL_TRADE_EXECUTION)::SymbolInfoInteger(Symb, SYMBOL_TRADE_EXEMODE);
const int FillingMode = (int)::SymbolInfoInteger(Symb, SYMBOL_FILLING_MODE);
return((FillingMode == 0 || (Type >= ORDER_FILLING_RETURN) || ((FillingMode & (Type + 1)) != Type + 1)) ?
(((ExeMode == SYMBOL_TRADE_EXECUTION_EXCHANGE) || (ExeMode == SYMBOL_TRADE_EXECUTION_INSTANT)) ?
ORDER_FILLING_RETURN : ((FillingMode == SYMBOL_FILLING_IOC) ? ORDER_FILLING_IOC : ORDER_FILLING_FOK)) :
(ENUM_ORDER_TYPE_FILLING)Type);
}
Спасибо, можно этот код чуть изменить чтоб получить аналог SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4. Нужно чтоб функция возвращала не ENUM_ORDER_TYPE_FILLING а (SYMBOL_FILLING_FOK | SYMBOL_FILLING_IOC).
ORDER_FILLING_FOK = 0, ORDER_FILLING_IOC = 1, в то время как SYMBOL_FILLING_FOK = 1 и SYMBOL_FILLING_IOC = 2, так что можно результат просто увеличить на 1.
И потом вызывать код в функции bool CTrade::FillingCheck(const string symbol)
Хотя на самом деле это просто подгонка кода чтоб заработало. Лучше так не делать а найти правильное решение.