LifeHack для трейдера: замешиваем ForEach на дефайнах (#define)
Vladimir Karputov | 13 февраля, 2018
— В чем сила, брат?
—А сила, брат, в дефайнах
(с) fxsaber
Вы все ещё пишете на MQL4 и хотите перейти на MQL5, но не знаете с чего начать? Тогда мы идём к вам! Теперь появилась возможность комфортно работать в редакторе MetaEditor MQL5 и при этом использовать MQL4-нотацию (справедливости ради замечу, что появилась она чуть ранее, а в этой статье я хочу предоставить более полное и развёрнутое описание по переносу функций MQL4 на MQL5).
Хороший программист — ленивый программист
Создание советников или торговых роботов — это почти всегда много работы с циклами. Циклы окружают нас везде: перебор ордеров, сделок в истории, объектов на графике, символов в Обзоре рынка, баров в индикаторном буфере. И чтобы немного облегчить жизнь программиста, в MetaEditor добавлены сниппеты — это когда вы набираете начальные символы, и по нажатию Tab они автоматически разворачиваются в небольшой кусок кода. Вот как работает сниппет для цикла for:
Неплохо, но он не покрывает всех наших потребностей. Самый простой пример: пусть нам требуется перебрать все символы в Обзоре рынка.
int total=SymbolsTotal(true); for(int i=0;i<total;i++) { string symbol=SymbolName(i,true); PrintFormat("%d. %s",i+1,symbol); }
Здорово было бы создать в MetaEditor свой сниппет, который начинался бы с fes (for_each_symbol) и разворачивался бы в такой блок:
Но в MetaEditor нет пользовательских сниппетов, поэтому мы пойдем другим путем — возьмемся за дефайны. Макроподстановка #define была придумана ленивыми умными программистами, которые преследовали несколько целей. Среди них — простота чтения и удобство написания многократно повторяемого кода.
Во многих языках программирования, кроме классического цикла for , есть его разновидности вида for(<typename> element:Collection) или for each (type identifier in expression). Если бы можно было писать код вида
for(ulong order_id in History) { работа c order_id }
, то жизнь программиста стала бы немного легче. В интернете вы найдете противников и сторонников такого подхода. Здесь я покажу, как сделать что-то подобное с помощью макросов #define.
Начнем с простой задачи — получим имена всех символов в Обзоре рынка. Пойдем в лоб и напишем такой макрос:
#define ForEachSymbol(s,i) string s; int total=SymbolsTotal(true); for(int i=0;i<total;i++,s=SymbolName(i,true)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachSymbol(symbol,index) { PrintFormat("%d. %s",index,symbol); } }
Компилятор отлично понимает эту запись и не выдает ошибок. Запускаем отладку по F5 и видим, что что-то пошло не так:
1. (null) 2. GBPUSD 3. USDCHF 4. USDJPY ...
Проблема в том, что выражение s=SymbolName(i,true) в цикле for вычисляется после выполнения итерации, и наша переменная s никак не инициализирована на первой итерации, когда i=0. Оператор цикла for:
Оператор for состоит из трех выражений и выполняемого оператора:
for(выражение1; выражение2; выражение3) |
Выражение1 описывает инициализацию цикла. Выражение2 — проверка условия завершения цикла. Если оно истинно, то выполняется оператор тела цикла for. Все повторяется, пока выражение2 не станет ложным. Если оно ложно, цикл заканчивается и управление передается следующему оператору. ВыражениеЗ вычисляется после каждой итерации.
Это решается просто. Делаем пару правок:
#define ForEachSymbol(s,i) string s=SymbolName(0,true); int total=SymbolsTotal(true); for(int i=1;i<total;i++,s=SymbolName(i,true)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachSymbol(symbol,index) { PrintFormat("%d. %s",index,symbol); // вместо index+1 теперь стоит просто index } }
и получаем нужный результат. Мы написали макрос ForEachSymbol с параметрами symbol и index, как будто это обычный цикл for, и в теле псевдоцикла работали с этими переменными, как будто они уже объявлены и проинициализированы нужными значениями. Таким образом, мы можем получать нужные свойства символов из Обзора рынка c помощью функций вида SymbolInfoXXX(). Например, так:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachSymbol(symbol,index) { //--- готовим данные double spread=SymbolInfoDouble(symbol,SYMBOL_ASK)-SymbolInfoDouble(symbol,SYMBOL_BID); double point=SymbolInfoDouble(symbol,SYMBOL_POINT); long digits=SymbolInfoInteger(symbol,SYMBOL_DIGITS); string str_spread=DoubleToString(spread/point,0); string str_point=DoubleToString(point,digits); //--- выводим информацию Print(index,". ",symbol," spread=",str_spread," points (", digits," digits",", point=",str_point,")"); } /* Пример вывода 1. EURUSD spread=3 points (5 digits, point=0.00001) 2. USDCHF spread=8 points (5 digits, point=0.00001) 3. USDJPY spread=5 points (3 digits, point=0.001) 4. USDCAD spread=9 points (5 digits, point=0.00001) 5. AUDUSD spread=5 points (5 digits, point=0.00001) 6. NZDUSD spread=10 points (5 digits, point=0.00001) 7. USDSEK spread=150 points (5 digits, point=0.00001) */ }
Теперь мы можем написать подобный макрос и для перебора графических объектов на графике:
#define ForEachObject(name,i) string name=ObjectName(0,0); int total=ObjectsTotal(0); for(int i=1;i<=total;i++,name=ObjectName(0,i-1)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachObject(objectname,index) { Print(index,": objectname=\"",objectname,"\", objecttype=", EnumToString((ENUM_OBJECT)ObjectGetInteger(0,objectname,OBJPROP_TYPE))); } /* Пример вывода 1: objectname="H1 Arrow 61067", objecttype=OBJ_ARROW_UP 2: objectname="H1 Rectangle 31152", objecttype=OBJ_RECTANGLE 3: objectname="H1 StdDev Channel 56931", objecttype=OBJ_STDDEVCHANNEL 4: objectname="H1 Trendline 6605", objecttype=OBJ_TREND */ }
Правда, строка определения макроса ForEachObject стала еще немного длиннее, да и понимать код замены, написанный в одну строку, становится не так просто. Но, оказывается, и эта проблема дано уже решена: определение макроса можно делить на строки с помощью обратного слеша '\'. Вот так:
#define ForEachObject(name,i) string name=ObjectName(0,0); \ int ob_total=ObjectsTotal(0); \ for(int i=1;i<=ob_total;i++,name=ObjectName(0,i-1))
Для компилятора все эти три строчки будут выглядеть как одна длинная строка, а вот программисту читать такой код гораздо проще. Осталось создать подобные макросы для работы с торговыми сущностями — ордерами, позициями и сделками.
Начнем с перебора ордеров. Здесь добавляется только функция выбора истории HistorySelect():
#define ForEachOrder(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=OrderGetTicket(0); \ int or_total=OrdersTotal(); \ for(int i=1;i<or_total;i++,ticket=OrderGetTicket(i)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachOrder(orderticket,index) { Print(index,": #",orderticket," ",OrderGetString(ORDER_SYMBOL)," ", EnumToString((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE))); } /* Пример вывода 1: #13965457 CADJPY ORDER_TYPE_SELL_LIMIT 2: #14246567 AUDNZD ORDER_TYPE_SELL_LIMIT */ }
Можно заметить, что в этом макросе (и двух предыдущих) нет обработки ошибок. Например, что если HistorySelect() вернет false? Можно ответить только тем, что в этом случае мы не сможем пройти в цикле по всем ордерам. И кроме того — кто из вас анализирует результат выполнения HistorySelect()? Получается, что в этом макросе нет ничего запрещенного и для обычного способа написания программы.
Но самой сильной критикой использования #define является тот факт, что использование макроподстановок не позволяет провести отладку кода. Я с этим согласен, хотя, как говорит fxsaber, "Хорошо зафиксированный пациент не требует анестезии отлаженный макрос не требует отладки".
Дальше по аналогии делаем макрос для перебора позиций:
#define ForEachPosition(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=PositionGetTicket(0); \ int po_total=PositionsTotal(); \ for(int i=1;i<=po_total;i++,ticket=PositionGetTicket(i-1)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachPosition(positionid,index) { Print(index,": ",PositionGetString(POSITION_SYMBOL)," postionID #",positionid); } /* Пример вывода 1: AUDCAD postionID #13234934 2: EURNZD postionID #13443909 3: AUDUSD postionID #14956799 4: EURUSD postionID #14878673 */
Перебор сделок в истории:
#define ForEachDeal(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=HistoryDealGetTicket(0); \ int total=HistoryDealsTotal(); \ for(int i=1;i<=total;i++,ticket=HistoryDealGetTicket(i-1)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachDeal(dealticket,index) { Print(index,": deal #",dealticket,", order ticket=", HistoryDealGetInteger(dealticket,DEAL_ORDER)); } }
Перебор ордеров в истории:
#define ForEachHistoryOrder(ticket,i) HistorySelect(0,TimeCurrent());\ ulong ticket=HistoryOrderGetTicket(0); \ int total=HistoryOrdersTotal(); \ for(int i=1;i<=total;i++,ticket=HistoryOrderGetTicket(i-1)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachHistoryOrder(historyorderticket,index) { Print(index,": #",historyorderticket); } }
Соберем все макроподстановки в один файл ForEach.mqh4:
//+------------------------------------------------------------------+ //| ForEach.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, MetaQuotes Software Corp." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Цикл перебора символов в Обзоре рынка | //+------------------------------------------------------------------+ #define ForEachSymbol(symbol,i) string symbol=SymbolName(0,true); \ int os_total=SymbolsTotal(true); \ for(int i=1;i<os_total;i++,symbol=SymbolName(i,true)) //+------------------------------------------------------------------+ //| Цикл перебора объектов на главном окне графика | //+------------------------------------------------------------------+ #define ForEachObject(name,i) string name=ObjectName(0,0); \ int ob_total=ObjectsTotal(0); \ for(int i=1;i<=ob_total;i++,name=ObjectName(0,i-1)) //+------------------------------------------------------------------+ //| Цикл перебора действующих ордеров | //+------------------------------------------------------------------+ #define ForEachOrder(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=OrderGetTicket(0); \ int or_total=OrdersTotal(); \ for(int i=1;i<or_total;i++,ticket=OrderGetTicket(i)) //+------------------------------------------------------------------+ //| Цикл перебора открытых позиций | //+------------------------------------------------------------------+ #define ForEachPosition(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=PositionGetTicket(0); \ int po_total=PositionsTotal(); \ for(int i=1;i<=po_total;i++,ticket=PositionGetTicket(i-1)) //+------------------------------------------------------------------+ //| Цикл перебора сделок в истории | //+------------------------------------------------------------------+ #define ForEachDeal(ticket,i) HistorySelect(0,TimeCurrent()); \ ulong ticket=HistoryDealGetTicket(0); \ int dh_total=HistoryDealsTotal(); \ for(int i=1;i<=dh_total;i++,ticket=HistoryDealGetTicket(i-1)) //+------------------------------------------------------------------+ //| Цикл перебора ордеров в истории | //+------------------------------------------------------------------+ #define ForEachHistoryOrder(ticket,i) HistorySelect(0,TimeCurrent());\ ulong ticket=HistoryOrderGetTicket(0); \ int oh_total=HistoryOrdersTotal(); \ for(int i=1;i<=oh_total;i++,ticket=HistoryOrderGetTicket(i-1)) //+------------------------------------------------------------------+
Обратите внимание: нам пришлось для каждого макроса для переменной total добавить префикс, чтобы не было конфликтов, если мы решим использовать в своем коде больше одного макроса. Это самый большой недостаток данного макроса: мы прячем за ним объявление переменной, которая будет видна снаружи. Это может привести к труднообнаруживаемым ошибкам при компиляции.
Кроме того, при использовании параметрического макроса компилятор не дает никаких подсказок, как он это делает для функций. Вам придется заучить эти 6 макросов, если хотите ими пользоваться. Хотя это и не так сложно — первый параметр всегда является сущностью, которую перебирают в цикле, а второй параметр — всегда индекс цикла, который начинается с 1 (единицы).
Напоследок добавим еще макросы для обхода в обратном порядке. В этом случае необходимо двигаться от конца списка к его началу. Добавим к имени макроса суффикс Back и сделаем небольшие правки. Вот как будет выглядеть макрос для обхода символов в обзоре рынка.
#define ForEachSymbolBack(symbol,i) int s_start=SymbolsTotal(true)-1;\ string symbol=SymbolName(s_start,true); \ for(int i=s_start;i>=0;i--,symbol=SymbolName(i,true)) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- ForEachSymbolBack(symbol,index) { //--- готовим данные double spread=SymbolInfoDouble(symbol,SYMBOL_ASK)-SymbolInfoDouble(symbol,SYMBOL_BID); double point=SymbolInfoDouble(symbol,SYMBOL_POINT); long digits=SymbolInfoInteger(symbol,SYMBOL_DIGITS); string str_spread=DoubleToString(spread/point,0); string str_point=DoubleToString(point,digits); //--- выводим информацию Print(index,". ",symbol," spread=",str_spread," points (", digits," digits",", point=",str_point,")"); } /* Пример вывода 3. USDJPY spread=5 points (3 digits, point=0.001) 2. USDCHF spread=8 points (5 digits, point=0.00001) 1. GBPUSD spread=9 points (5 digits, point=0.00001) 0. EURUSD spread=2 points (5 digits, point=0.00001) */ }
Как видите, в этом случае значение переменной index меняется от size-1 до 0. На этом мы заканчиваем знакомство с #define и переходим к написанию функций для простого доступа в MQL4-стиле.
1. Какие группы MQL4 функций описаны в статье
Внимание: Такие переменные, как Point, Digits и Bar Вам нужно самостоятельно менять на Point(), Digits() и Bar(Symbol(),Period())
На MQL5 будут переведены группы MQL4 AccountXXXX, MQL4 MarketInfo, MQL4 Проверка состояния, MQL4 Предопределенные переменные. Таким образом, в папку [date folder]\MQL5\Include\SimpleCall\ добавятся четыре файла: AccountInfo.mqh, MarketInfo.mqh, Check.mqh и Predefined.mqh. Всего в папке окажутся семь файлов для перевода MQL4 функций в MQL5: AccountInfo.mqh, Check.mqh, IndicatorsMQL4.mqh, IndicatorsMQL5.mqh, MarketInfo.mqh, Predefined.mqh и Series.mqh.
Для работы вам нужно будет самостоятельно подключать все эти файлы. При этом есть ограничение: файлы IndicatorsMQL4.mqh и IndicatorsMQL5.mqh вместе подключать нельзя — нужно выбирать какой-то один. Поэтому в папке есть два файла: SimpleCallMQL4.mqh — он подключает все файлы плюс IndicatorsMQL4.mqh и SimpleCallMQL5.mqh — он подключает все файлы плюс IndicatorsMQL5.mqh.
Пример подключения на базе MACD Sample.mq4
Копируем файл MACD Sample.mq4 в MQL5-папку с экспертами — например, в [data folder]\MQL5\Experts\, и меняем расширение файла на mq5. Таким образом получаем файл MACD Sample.mq5. Компилируем и сразу получаем 59 ошибок и одно предупреждение.
Теперь подключаем файл SimpleCallMQL4.mqh:
//+------------------------------------------------------------------+ //| MACD Sample.mq4 | //| Copyright 2005-2014, MetaQuotes Software Corp. | //| http://www.mql4.com | //+------------------------------------------------------------------+ #property copyright "2005-2014, MetaQuotes Software Corp." #property link "http://www.mql4.com" //--- #include <SimpleCall\SimpleCallMQL4.mqh> //--- input double TakeProfit =50;
Снова компилируем и получаем уже 39 ошибок и одно предупреждение. Теперь нужна ручная работа по замене (при помощи Ctrl+H) Bars на Bars(Symbol(),Period()) и Point на Point(). Компилируем. Остается 35 ошибок, все они относятся к торговым функциям. Но про торговые функции мы поговорим не в этой статье.
1.1. MQL4 AccountXXXX
Перевод MQL4 AccountXXXX функций выполнен в файле [date folder]\MQL5\Include\SimpleCall\AccountInfo.mqh
Таблица соответствия функций из MQL4 с функциями в MQL5 выглядит так:
MQL4 | MQL5 AccountInfoXXXX | MQL5 CAccountInfo | Примечания |
---|---|---|---|
AccountInfoDouble | AccountInfoDouble | CAccountInfo::InfoDouble или CAccountInfo::double методы | |
AccountInfoInteger | AccountInfoInteger | CAccountInfo::InfoInteger или CAccountInfo::целочисленные методы | |
AccountInfoString | AccountInfoString | CAccountInfo::InfoString или CAccountInfo::текстовые методы | |
AccountBalance | AccountInfoDouble(ACCOUNT_BALANCE) или | CAccountInfo::Balance | |
AccountCredit | AccountInfoDouble(ACCOUNT_CREDIT) или | CAccountInfo::Credit | |
AccountCompany | AccountInfoString(ACCOUNT_COMPANY) или | CAccountInfo::Company | |
AccountCurrency | AccountInfoString(ACCOUNT_CURRENCY) или | CAccountInfo::Currency | |
AccountEquity | AccountInfoDouble(ACCOUNT_EQUITY) или | CAccountInfo::Equity | |
AccountFreeMargin | AccountInfoDouble(ACCOUNT_FREEMARGIN) или | CAccountInfo::FreeMargin | |
AccountFreeMarginCheck | --- /--- | CAccountInfo::FreeMarginCheck | |
AccountFreeMarginMode | Аналога нет | Аналога нет | |
AccountLeverage | AccountInfoInteger(ACCOUNT_LEVERAGE) | CAccountInfo::Leverage | В MQL4 имеет тип int, в MQL5: long |
AccountMargin | AccountInfoDouble(ACCOUNT_MARGIN) | CAccountInfo::Margin | |
AccountName | AccountInfoString(ACCOUNT_NAME) | CAccountInfo::Name | |
AccountNumber | AccountInfoInteger(ACCOUNT_LOGIN) | CAccountInfo::Login | В MQL4 имеет тип int, в MQL5: long |
AccountProfit | AccountInfoDouble(ACCOUNT_PROFIT) | CAccountInfo::Profit | |
AccountServer | AccountInfoString(ACCOUNT_SERVER) | CAccountInfo::Server | |
AccountStopoutLevel | AccountInfoDouble(ACCOUNT_MARGIN_SO_SO) | CAccountInfo::MarginStopOut | В MQL4 имеет тип int, в MQL5: double |
AccountStopoutMode | AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE) | CAccountInfo::StopoutMode |
Обратите внимание, для MQL4 AccountFreeMarginMode аналога в MQL5 нет. Дальше вы используете MQL4 AccountFreeMarginMode на свой страх и риск. При встрече MQL4 AccountFreeMarginMode в лог будет печататься предупреждение и возвращаться NaN — "не число".
Для остальных MQL4 AccountXXXX функций аналоги есть, причем сразу в двух вариантах: через AccountInfoXXXX или через торговый класс CAccountInfo. Для AccountInfoDouble, AccountInfoInteger и AccountInfoString различий между MQL4 и MQL5 нет.
В шапку файла вынесен такой код. Как это работает?
//--- #define OP_BUY ORDER_TYPE_BUY #define OP_SELL ORDER_TYPE_SELL //--- returns balance value of the current account
Для начала заглянем в справку по #define:
Директива #define может быть использована для присвоения мнемонических имен выражениям. Существует две формы:
#define identifier expression // беспараметрическая форма #define identifier(par1,... par8) expression // параметрическая форма
Директива #define подставляет expression вместо всех последующих найденных вхождений identifier в исходном тексте.
В случае с нашим кодом это описание будет выглядеть так (мы использовали беспараметрическую форму): директива #define подставляет ORDER_TYPE_BUY вместо всех последующих найденных вхождений OP_BUY в исходном тексте. Директива #define подставляет ORDER_TYPE_SELL вместо всех последующих найденных вхождений OP_SELL в исходном тексте. То есть после того, как вы подключите файл [date folder]\MQL5\Include\SimpleCall\AccountInfo.mqh к своему советнику MQL5, то сможете в нём привычным образом использовать MQL4 типы OP_BUY и OP_SELL. Компилятор не выдаст ошибку.
Итак, первая группа функций, которые мы приведем к MQL5-виду: AccountBalance(), AccountCredit(), AccountCompany(), AccountCurrency(), AccountEquity() и AccountFreeMargin():
//--- returns balance value of the current account #define AccountBalance(void) AccountInfoDouble(ACCOUNT_BALANCE) //--- returns credit value of the current account #define AccountCredit(void) AccountInfoDouble(ACCOUNT_CREDIT) //--- returns the brokerage company name where the current account was registered #define AccountCompany(void) AccountInfoString(ACCOUNT_COMPANY) //--- returns currency name of the current account #define AccountCurrency(void) AccountInfoString(ACCOUNT_CURRENCY) //--- returns equity value of the current account #define AccountEquity(void) AccountInfoDouble(ACCOUNT_EQUITY) //--- returns free margin value of the current account #define AccountFreeMargin(void) AccountInfoDouble(ACCOUNT_MARGIN_FREE)Здесь в MQL4 XXXX(void) функциях используется уже параметрическая форма #define, где "void" выступает в качестве параметра. Это легко проверить: если убрать "void", то при компиляции получим ошибку "unexpected in macro format parameter list":
В случае с MQL4 AccountFreeMarginCheck поступим иначе — оформим AccountFreeMarginCheck как обычную функцию, внутри которой используется только MQL5-код:
//--- returns free margin that remains after the specified order has been opened //--- at the current price on the current account double AccountFreeMarginCheck(string symbol,int cmd,double volume) { double price=0.0; ENUM_ORDER_TYPE trade_operation=(ENUM_ORDER_TYPE)cmd; if(trade_operation==ORDER_TYPE_BUY) price=SymbolInfoDouble(symbol,SYMBOL_ASK); if(trade_operation==ORDER_TYPE_SELL) price=SymbolInfoDouble(symbol,SYMBOL_BID); //--- double margin_check=EMPTY_VALUE; double margin=EMPTY_VALUE; margin_check=(!OrderCalcMargin(trade_operation,symbol,volume,price,margin))?EMPTY_VALUE:margin; //--- return(AccountInfoDouble(ACCOUNT_FREEMARGIN)-margin_check); }
Как было сказано выше, для AccountFreeMarginMode нет аналога в MQL5, поэтому при встрече в коде AccountFreeMarginMode будет выдано предупреждение и возвращено "не число":
//--- returns the calculation mode of free margin allowed to open orders on the current account double AccountFreeMarginMode(void) { string text="MQL4 functions \"AccountFreeMarginMode()\" has no analogs in MQL5. Returned \"NAN - not a number\""; Alert(text); Print(text); return(double("nan")); }
Для остальных MQL4-функций поступаем по аналогии с предыдущими:
//--- returns leverage of the current account #define AccountLeverage(void) (int)AccountInfoInteger(ACCOUNT_LEVERAGE) //--- returns margin value of the current account #define AccountMargin(void) AccountInfoDouble(ACCOUNT_MARGIN) //--- returns the current account name #define AccountName(void) AccountInfoString(ACCOUNT_NAME) //--- returns the current account number #define AccountNumber(void) (int)AccountInfoInteger(ACCOUNT_LOGIN) //--- returns profit value of the current account #define AccountProfit(void) AccountInfoDouble(ACCOUNT_PROFIT) //--- returns the connected server name #define AccountServer(void) AccountInfoString(ACCOUNT_SERVER) //--- returns the value of the Stop Out level #define AccountStopoutLevel(void) (int)AccountInfoDouble(ACCOUNT_MARGIN_SO_SO) //--- returns the calculation mode for the Stop Out level int AccountStopoutMode(void) { ENUM_ACCOUNT_STOPOUT_MODE stopout_mode=(ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE); if(stopout_mode==ACCOUNT_STOPOUT_MODE_PERCENT) return(0); return(1); }
1.2. MQL4 MarketInfo
Перевод MQL4 MarketInfotXXXX функций выполнен в файле [date folder]\MQL5\Include\SimpleCall\MarketInfo.mqh
MQL4 MarketInfo имеет тип double, но в MQL5 аналоги MarketInfo есть и в SymbolInfoInteger() (возвращает тип long), и в SymbolInfoDouble() (возвращает тип double). Здесь в глаза бросается вся неуклюжесть использования устаревшей функции MarketInfo: преобразование типов. На примере преобразования MQL5 SYMBOL_DIGITS:
MarketInfo(Symbol(),MODE_DIGITS) <= (double)SymbolInfoInteger(symbol,SYMBOL_DIGITS)
1.2.1 Неоднозначность с MQL4 MODE_TRADEALLOWED
В MQL4 это простой флаг bool, тогда как в MQL5 для символа может быть несколько видов разрешений/запретов:
ENUM_SYMBOL_TRADE_MODE
Идентификатор | Описание |
---|---|
SYMBOL_TRADE_MODE_DISABLED | Торговля по символу запрещена |
SYMBOL_TRADE_MODE_LONGONLY | Разрешены только покупки |
SYMBOL_TRADE_MODE_SHORTONLY | Разрешены только продажи |
SYMBOL_TRADE_MODE_CLOSEONLY | Разрешены только операции закрытия позиций |
YMBOL_TRADE_MODE_FULL | Нет ограничений на торговые операции |
Предлагаю отдавать false только в случае SYMBOL_TRADE_MODE_DISABLED, а для частичных ограничений или для полного доступа — true (при этом выдавать Alert и Print с предупреждением о частичном ограничении).
1.2.2. Неоднозначность с MQL4 MODE_SWAPTYPE
В MQL4 MODE_SWAPTYPE возвращает всего четыре значения (метод вычисления свопов. 0 — в пунктах; 1 — в базовой валюте инструмента; 2 — в процентах; 3 — в валюте залоговых средств), тогда как в MQL5 перечисление ENUM_SYMBOL_SWAP_MODE содержит девять значений и они пересекаются по значениям в MQL4:
MQL4 MODE_SWAPTYPE | MQL5 ENUM_SYMBOL_SWAP_MODE |
---|---|
Нет соответствия | SYMBOL_SWAP_MODE_DISABLED |
0 - в пунктах | SYMBOL_SWAP_MODE_POINTS |
1 - в базовой валюте инструмента | SYMBOL_SWAP_MODE_CURRENCY_SYMBOL |
3 - в валюте залоговых средств | SYMBOL_SWAP_MODE_CURRENCY_MARGIN |
Нет соответствия | SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT |
2 - в процентах | SYMBOL_SWAP_MODE_INTEREST_CURRENT |
2 - в процентах | SYMBOL_SWAP_MODE_INTEREST_OPEN |
Нет соответствия | SYMBOL_SWAP_MODE_REOPEN_CURRENT |
Нет соответствия | SYMBOL_SWAP_MODE_REOPEN_BID |
В MQL5 для расчёта свопов в процентах может быть два варианта:
/*
SYMBOL_SWAP_MODE_INTEREST_CURRENT
Свопы начисляются в годовых процентах от цены инструмента на момент расчета свопа(банковский режим – 360 дней в году)
SYMBOL_SWAP_MODE_INTEREST_OPEN
Свопы начисляются в годовых процентах от цены открытия позиции по символу (банковский режим – 360 дней в году)
*/
Будем считать их за один тип. Для остальных MQL5-вариантов, которые не имеют аналога в MQL4, будет выдаваться предупреждение и возвращаться "не число".
1.2.3. Неоднозначность с MQL4 MODE_PROFITCALCMODE и MODE_MARGINCALCMODE
В MQL4 MODE_PROFITCALCMODE (способ расчета прибыли) возвращает всего три значения: 0 — Forex; 1 — CFD; 2 — Futures, а MODE_MARGINCALCMODE (способ расчета залоговых средств) — четыре: 0 — Forex; 1 — CFD; 2 — Futures; 3 — CFD на индексы. В MQL5 для инструмента можно определить способ расчёта залоговых средств (перечисление ENUM_SYMBOL_CALC_MODE), а вот способа расчёта прибыли нет. Предполагаю, что в MQL5 способ расчёта залоговых средств равен способу расчёта прибыли, поэтому для MQL4 MODE_PROFITCALCMODE и MODE_MARGINCALCMODE будет возвращаться одинаковое значение с MQL5 ENUM_SYMBOL_CALC_MODE.
Перечисление MQL5 ENUM_SYMBOL_CALC_MODE содержит десять способов. Сопоставим MQL4 MODE_PROFITCALCMODE c MQL5 ENUM_SYMBOL_CALC_MODE:
MQL4 MODE_PROFITCALCMODE "Forex" <==> MQL5 SYMBOL_CALC_MODE_FOREX
MQL4 MODE_PROFITCALCMODE "CFD" <==> MQL5 SYMBOL_CALC_MODE_CFD
MQL4 MODE_PROFITCALCMODE "Futures" <==> MQL5 SYMBOL_CALC_MODE_FUTURES
Для остальных MQL5 вариантов, которые не имеют аналога в MQL4, будет выдаваться предупреждение и возвращаться "не число":
... #define MODE_PROFITCALCMODE 1000//SYMBOL_TRADE_CALC_MODE #define MODE_MARGINCALCMODE 1001//SYMBOL_TRADE_CALC_MODE ... case MODE_PROFITCALCMODE: { ENUM_SYMBOL_CALC_MODE profit_calc_mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE); switch(profit_calc_mode) { case SYMBOL_CALC_MODE_FOREX: return((double)0); case SYMBOL_CALC_MODE_FUTURES: return((double)2); case SYMBOL_CALC_MODE_CFD: return((double)1); default : { string text="MQL4 MODE_PROFITCALCMODE returned MQL5 "+EnumToString(profit_calc_mode); Alert(text); Print(text); return(double("nan")); } } } case MODE_MARGINCALCMODE: { ENUM_SYMBOL_CALC_MODE profit_calc_mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE); switch(profit_calc_mode) { case SYMBOL_CALC_MODE_FOREX: return((double)0); case SYMBOL_CALC_MODE_FUTURES: return((double)2); case SYMBOL_CALC_MODE_CFD: return((double)1); default : { string text="MQL4 MODE_MARGINCALCMODE returned MQL5 "+EnumToString(profit_calc_mode); Alert(text); Print(text); return(double("nan")); } } }
1.3. MQL4 Проверка состояния
Перевод MQL4-функций проверки состояния выполнен в файле [date folder]\MQL5\Include\SimpleCall\Check.mqh
Сюда входят такие элементы:
- Digits
- Point
- IsConnected
- IsDemo
- IsDllsAllowed
- IsExpertEnabled
- IsLibrariesAllowed
- IsOptimization
- IsTesting
- IsTradeAllowed
- IsTradeContextBusy
- IsVisualMode
- TerminalCompany
- TerminalName
- TerminalPath
MQL4 | MQL5 | MQL5 classes | Примечение |
---|---|---|---|
Digits | MQL4 позволяет одновременно использовать и Digits и Digits() | ||
Point | MQL4 позволяет одновременно использовать и Point и Point() | ||
IsConnected | TerminalInfoInteger(TERMINAL_CONNECTED) | CTerminalInfo::IsConnected | |
IsDemo | AccountInfoInteger(ACCOUNT_TRADE_MODE) | CAccountInfo::TradeMode | Возвращается одно из значений из перечисления ENUM_ACCOUNT_TRADE_MODE |
IsDllsAllowed | TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) и MQLInfoInteger(MQL_DLLS_ALLOWED) | CTerminalInfo::IsDLLsAllowed | TERMINAL_DLLS_ALLOWED - имеет наивысший статус, и MQL_DLLS_ALLOWED можно игнорировать |
IsExpertEnabled | TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) | CTerminalInfo::IsTradeAllowed | Состояние кнопки "Автоторговля" в терминале |
IsLibrariesAllowed | MQLInfoInteger(MQL_DLLS_ALLOWED) | -/- | Проверка бессмысленна: если программа использует DLL и вы ей не разрешаете их использовать (вкладка "Зависимости" программы), то вы просто не запустите программу. |
IsOptimization | MQLInfoInteger(MQL_OPTIMIZATION) | -/- | MQL5-программа может иметь четыре режима: отладки, профилирования кода, тестера, оптимизации |
IsTesting | MQLInfoInteger(MQL_TESTER) | -/- | MQL5-программа может иметь четыре режима: отладки, профилирования кода, тестера, оптимизации |
IsTradeAllowed | MQLInfoInteger(MQL_TRADE_ALLOWED) | -/- | Состояние чекбокса "Разрешить автоматическую торговлю" в свойствах программы |
IsTradeContextBusy | -/- | -/- | Будет возвращено "false" |
IsVisualMode | MQLInfoInteger(MQL_VISUAL_MODE) | MQL5-программа может иметь четыре режима: отладки, профилирования кода, тестера, оптимизации | |
TerminalCompany | TerminalInfoString(TERMINAL_COMPANY) | CTerminalInfo::Company | |
TerminalName | TerminalInfoString(TERMINAL_NAME) | CTerminalInfo::Name | |
TerminalPath | TerminalInfoString(TERMINAL_PATH) | CTerminalInfo::Path |
Первые два пункта (Digits и Point) полностью нереализуемы из-за того, что здесь напрочь перепутаны MQL4 и MQL5. А именно: в MQL4 может одновременно встречаться и Digits и Digits() и Point и Point(). Например, лично я не знаю способа, каким образом через define можно Digits и Digits() свести к Digits(). Остальные пункты однозначно встречаются в виде XXXX() и могут быть заменены MQL5-аналогами.
И сама реализация:
//+------------------------------------------------------------------+ //| Check.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://wmua.ru/slesar/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, MetaQuotes Software Corp." #property link "http://wmua.ru/slesar/" #property version "1.003" //--- checks connection between client terminal and server #define IsConnected (bool)TerminalInfoInteger(TERMINAL_CONNECTED) //--- checks if the Expert Advisor runs on a demo account #define IsDemo (bool)(AccountInfoInteger(ACCOUNT_TRADE_MODE)==(ENUM_ACCOUNT_TRADE_MODE)ACCOUNT_TRADE_MODE_DEMO) //--- checks if the DLL function call is allowed for the Expert Advisor #define IsDllsAllowed (bool)TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) //--- checks if Expert Advisors are enabled for running #define IsExpertEnabled (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) //--- checks if the Expert Advisor can call library function #define IsLibrariesAllowed (bool)MQLInfoInteger(MQL_DLLS_ALLOWED) //--- checks if Expert Advisor runs in the Strategy Tester optimization mode #define IsOptimization (bool)MQLInfoInteger(MQL_OPTIMIZATION) //--- checks if the Expert Advisor runs in the testing mode #define IsTesting (bool)MQLInfoInteger(MQL_TESTER) //--- checks if the Expert Advisor is allowed to trade and trading context is not busy #define IsTradeAllowed (bool)MQLInfoInteger(MQL_TRADE_ALLOWED) //--- returns the information about trade context #define IsTradeContextBusy false //--- checks if the Expert Advisor is tested in visual mode #define IsVisualMode (bool)MQLInfoInteger(MQL_VISUAL_MODE) //--- returns the name of company owning the client terminal #define TerminalCompany TerminalInfoString(TERMINAL_COMPANY) //--- returns client terminal name #define TerminalName TerminalInfoString(TERMINAL_NAME) //--- returns the directory, from which the client terminal was launched #define TerminalPath TerminalInfoString(TERMINAL_PATH) //+------------------------------------------------------------------+
1.4. MQL4 Предопределенные переменные
Реализация в файле [data folder]\MQL5\Include\SimpleCall\Predefined.mqh
MQL4 предопределённые переменные:
- _Digits
- _Point
- _LastError
- _Period
- _RandomSeed
- _StopFlag
- _Symbol
- _UninitReason
- Ask
- Bars
- Bid
- Close
- Digits
- High
- Low
- Open
- Point
- Time
- Volume
Предопределённые переменные _XXXX переводятся в функции MQL5 при помощи беспараметрической формы #define:
//--- the _Digits variable stores number of digits after a decimal point, #define _Digits Digits() //--- the _Point variable contains the point size of the current symbol in the quote currency #define _Point Point() //--- the _LastError variable contains code of the last error #define _LastError GetLastError() //--- the _Period variable contains the value of the timeframe of the current chart #define _Period Period() //#define _RandomSeed //--- the _StopFlag variable contains the flag of the program stop #define _StopFlag IsStopped() //--- the _Symbol variable contains the symbol name of the current chart #define _Symbol Symbol() //--- the _UninitReason variable contains the code of the program uninitialization reason #define _UninitReason UninitializeReason() //#define Bars Bars(Symbol(),Period()); //#define Digits //#define Point
Исключение только для "_RandomSeed" — в этой переменной хранится текущее состояние генератора псевдослучайных целых чисел, и для Bars тоже нет перевода в MQL5 (точнее, Bars оставлен для ручной замены). Для Digits и Point решения нет. Как уже говорилось выше, в тексте могут встречаться одновременно и Digits и Digits(), и Point и Point().
MQL4 Ask и Bid заменяются на пользовательские (самописные) функции GetAsk() и GetBid():
//--- the latest known seller's price (ask price) for the current symbol #define Ask GetAsk() //--- the latest known buyer's price (offer price, bid price) of the current symbol #define Bid GetBid() ... //--- the latest known seller's price (ask price) for the current symbol double GetAsk() { MqlTick tick; SymbolInfoTick(Symbol(),tick); return(tick.ask); } //--- the latest known buyer's price (offer price, bid price) of the current symbol double GetBid() { MqlTick tick; SymbolInfoTick(Symbol(),tick); return(tick.bid); }
Мы, конечно, могли бы написать макрос для Ask проще:
#define Ask SymbolInfoDouble(__Symbol,SYMBOL_ASK)
Но в примечании к SymbolInfoDouble сказан:
Если функция используется для получения информации о последнем тике, то лучше использовать SymbolInfoTick(). Вполне возможно, что по данному символу с момента подключения терминала к торговому счету не было еще ни одной котировки. В таком случае запрашиваемое значение будет неопределенным.
В большинстве случаев достаточно использовать функцию SymbolInfoTick(), которая позволяет получить за один вызов значения Ask, Bid, Last, Volume и время прихода последнего тика.
А теперь кое-что новенькое: используем "operator"
Ключевое слово operator будем использовать для перегрузки (переназначения) оператора индексации []. Это понадобится для перевода MQL4 массивов-таймсерий (Open[], High[], Low[], Close[], Time[], Volume[]) в MQL5 форму.
Что нам говорит справка по "operator":
Перегрузка операций позволяет использовать операционную нотацию (запись в виде простых выражений) к сложным объектам - структурам и классам.
Исходя из справки, можно предположить, что для перегрузки оператора индексации [] нам понадобится создать класс.
Помня о #define:
Директива #define может быть использована для присвоения мнемонических имен выражениям. Существует две формы:
#define identifier expression // беспараметрическая форма #define identifier(par1,... par8) expression // параметрическая форма
Директива #define подставляет expression вместо всех последующих найденных вхождений identifier в исходном тексте.
следующий код можно прочитать как: директива #define подставит 159 вместо всех последующих найденных вхождений в исходном тексте.
#define SeriesVolume(Volume,T) 159 //+-----------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { long a=SeriesVolume(Volume,long); }
То есть код в OnStart преобразовывается к виду:
long a=159;
Шаг 2
Здесь внутрь #define поместили целый класс,
//+------------------------------------------------------------------+ //| Test_en.mq5 | //| Copyright 2012, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #define SeriesVolume(Volume,T) class CVolume \ { \ public: \ T operator[](const int i) const \ { \ long val[1]; \ if(CopyTickVolume(Symbol(),Period(),i,1,val)==1)\ return(val[0]); \ else \ return(-1); \ } \ }; \ CVolume Volume; //--- SeriesVolume(Volume,long) //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { Print(Volume[4]); } //+------------------------------------------------------------------+
Эту замену можно представить так:
//+------------------------------------------------------------------+ //| Test_en.mq5 | //| Copyright 2012, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ class CVolume { public: long operator[](const int i) const { long val[1]; if(CopyTickVolume(Symbol(),Period(),i,1,val)==1) return(val[0]); else return(-1); } }; CVolume Volume; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { Print(Volume[4]); }
То есть в OnStart мы, по сути, обращаемся к объекту Volume класса CVolume, к методу [], в котором передаём индекс i. По такому же принципу пишем для MQL4-серий Open, High, Low, Close и Time.
И напоследок у нас остаются MQL4 iXXX функции: iOpen, iHigh, iLow, iClose, iTime и iVolume. Для них применим метод объявления пользовательской функции.
Пример для iClose:
//--- returns Close price value for the bar of specified symbol with timeframe and shift double iClose( string symbol, // symbol ENUM_TIMEFRAMES timeframe, // timeframe int shift // shift ) { double result=0.0; //--- double val[1]; ResetLastError(); int copied=CopyClose(symbol,timeframe,shift,1,val); if(copied>0) result=val[0]; else Print(__FUNCTION__,": CopyClose error=",GetLastError()); //--- return(result); }
2. Изменения в других файлах
В [data folder]\MQL5\Include\SimpleCall\IndicatorsMQL4.mqh все MQL4-названия линий теперь прописаны в шапке:
double NaN=double("nan"); #define MODE_MAIN 0 #define MODE_SIGNAL 1 #define MODE_PLUSDI 1 #define MODE_MINUSDI 2 #define MODE_GATORJAW 1 #define MODE_GATORTEETH 2 #define MODE_GATORLIPS 3 #define MODE_UPPER 1 #define MODE_LOWER 2 #define MODE_TENKANSEN 1 #define MODE_KIJUNSEN 2 #define MODE_SENKOUSPANA 3 #define MODE_SENKOUSPANB 4 #define MODE_CHIKOUSPAN 5
В [data folder]\MQL5\Include\SimpleCall\Series.mqh убраны
#define MODE_OPEN 0 //#define MODE_LOW 1 //#define MODE_HIGH 2 #define MODE_CLOSE 3 #define MODE_VOLUME 4 //#define MODE_TIME 5
Теперь они прописаны в [data folder]\MQL5\Include\SimpleCall\Header.mqh :
//+------------------------------------------------------------------+ //| Header.mqh | //| Copyright 2018, MetaQuotes Software Corp. | //| http://wmua.ru/slesar/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, MetaQuotes Software Corp." #property link "http://wmua.ru/slesar/" //--- #define MODE_LOW 10001 #define MODE_HIGH 10002 #define MODE_TIME 10005
и iClose, iHigh, iLow, iOpen, iTime и iVolume — теперь они прописаны в [data folder]\MQL5\Include\SimpleCall\Predefined.mqh
Заключение
В предыдущей статье мы посмотрели, как можно писать вызовы индикаторов в MQL4 стиле и к чему это приводит. Оказалось, что за простоту написания приходится платить замедлением работы советников, в которых нет контроля создаваемых индикаторов. В этой статье мы продолжили искать пути по упрощению написания кода и рассмотрели макроподстановку #define.
В результате мы видим, что с помощью приложенных к статье кодов можно практически любой MQL4-советник заставить работать и в MetaTrader 5. Нужно только подключить нужные файлы, которые перегружают или добавляют необходимые функции и предопределенные переменные.
Для полной совместимости не хватает только упрощенных торговых функций MQL4. Но и этот вопрос при желании решается. Подводя итоги, повторим еще раз плюсы и минусы этого подхода:
Минусы:
- ограничение в обработке возвращаемой ошибки при доступе к индикаторам;
- падение скорости тестирования при одновременном доступе более чем к одному индикатору;
- необходимость правильно указывать линии индикаторов в зависимости от подключения "IndicatorsMQL5.mqh" или "IndicatorsMQL4.mqh";
- невозможность отладки макроподстановки #define;
- нет всплывающей подсказки об аргументах параметрического #define;
- потенциальные коллизии переменных, спрятанными за макросами.
- простота написания кода — одна строка вместо нескольких;
- наглядность и краткость — чем меньше кода, тем проще его понимать;
- макроподстановки подсвечиваются в редакторе красным цветом — легче видеть пользовательские идентификаторы и функции;
- можно создавать свои суррогаты сниппетов.
Сам я рассматриваю приведенные в статье приемы именно как лайфхак и остаюсь приверженцем классического MQL5-подхода. Возможно, тем, кто привык писать в MQL4-стиле, эти статьи помогут преодолеть психологический барьер при переходе на платформу MetaTrader 5, которая по всем параметрам гораздо удобнее.