Специальные возможности языка MQL5
Обзор рынка
Во-первых, имеется возможность программно получить список всех доступных в терминале символов. Кроме того, можно управлять набором символов окна «Обзор рынка», а также проверять существование какого-либо символа. Создайте папку «017 Special», а в ней скрипт «001 MarketWatch», пишем код в нем.
Для получения общего числа всех доступных символов используется функция SymbolsTotal(), в функцию передается один параметр типа bool. При значении false функция возвращает количество всех доступных в терминале символов, а при true только находящихся в окне «Обзор рынка». Функция SymbolName() позволяет получить наименование символа по его индексу. В функцию передается два параметра: первый – индекс, второй идентичен параметру функции SymbolsTotsl().
Следующий код получает в переменную str список всех символов:
bool selected=true; string str="";
int total=SymbolsTotal(selected); for(int i=0;i<total;i++){
str=str+SymbolName(i,selected)+", ";
}
if(str!=""){ str=StringSubstr(str,0,StringLen(str)-2);
}
Alert(str);
Обратите внимание, имена символов собираются в строку str через запятую и пробел, а после цикла строка укорачивается на два символа, чтобы обрезать последнюю запятую и пробел. Переменная selected позволяет выбрать тип списка: true – только символы из окна «Обзор рынка», false – все возможные символы, доступные в терминале.
Иногда бывает нужно проверить, существует ли какой-то символ в окне обзора рынка, и, если не существует, то его надо добавить, но если же его вообще нет в терминале, то надо вывести уведомление. Создайте скрипт с именем «002 SymbolsCheck», будем писать код в нем. Создаем массив со списком символов, массив может быть глобальным, а может располагаться в начале функции OnStart():
string lst[]={"EURUSD","GBPUSD","AUDUSD"};
Для проверки существования символа в терминале используется функция SymbolExist(). Первым параметром в функцию передается имя символа, а вторым параметром по ссылке передается вспомогательная переменная, в которой будет указан признак символа, является ли он стандартным или пользовательским. В терминале MetaTrader5 имеется возможность создания собственных символов по любой своей формуле, например индекса доллара, при этом данные отображаются не как линия индикатора технического анализа, а в виде стандартного графика из баров или свечей, что позволяет применять к нему все возможные индикаторы технического анализа, а также запускать на нем экспертов.
Для добавления символа в окно «Обзор рынка» используется функция SymbolSelect(). Первый параметр функции – имя символа, второй – true/false (добавить или убрать). Проходим в цикле по массиву символов, проверяем существование символа в терминале, если он не существует, уведомляем пользователя и завершаем работу скрипта. Если же символ существует, то включаем:
bool cust;
int sz=ArraySize(lst); for(int i=0;i<sz;i++){
if(!SymbolExist(lst[i],cust)){ Alert("Нет символа ",lst[i]); return;
}
if(!SymbolSelect(lst[i],true)){
Alert("Не удалось включить символ ",lst[i]); return;
}
}
Alert("Успешное выполнение скрипта");
Обратите внимание, функция SymbolSelect() возвращает значение типа bool, оно тоже проверяется. Если все символы существуют, и их удалось добавить в окно «Обзор рынка» (или они уже там были), откроется окно с сообщением «Успешное выполнение скрипта», после этого можно приступать к получению данных этих символов.
Данные о символе
Для получения данных о символе существует три основных функции: SymbolInfoDouble(), SymbolInfoInteger() и SymbolInfoString(). Судя по именам функций, очевидно, что они используются для получения данных типа double, целочисленных и строковых. Все эти функции являются перегруженными, существует по два их варианта.
Первый вариант: в функцию передается имя символа и идентификатор, определяющий, какие конкретно данные необходимо получить, сама функция возвращает эти данные.
Второй вариант: функция возвращает true/false, в зависимости от успешности ее работы, а данные возвращается по ссылке через дополнительный параметр. Обычно проблем с получением данных по символу не возникает, поэтому используется первый вариант функции.
Функция SymbolInfoDouble() чаще всего используется для получения рыночных цен bid и ask. Получим цены по тому символу, на котором запущен скрипт. Чтобы узнать символ, на котором исполняется скрипт или любая друга программа, написанная на MQL5, применяется функция Symbol(). Создайте скрипт с именем «003 SymbolInfo», пишем код в нем:
double bid=SymbolInfoDouble(Symbol(),SYMBOL_BID); double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); Alert("bid=",bid,", ask=",ask);
Существует огромное количество идентификаторов, позволяющих получать различные данные по символу. При создании экспертов обязательно придется иметь дело со следующими идентификаторами:
SYMBOL_POINT – величина пункта. Под пунктом здесь понимается минимальная единица измерения величины изменения цены. Например, у четрыехзнакового символа EURUSD пункт равен 0.0001. На эту же величину и меняется цена EURUSD. Однако существуют символы, курс которых меняется не на 0.0001, а на 0.0005, но и в этом случае пункт равен 0.0001, поскольку это минимальная единица измерения, но не изменения. А минимальная единица изменения цены может быть получена при помощи следующего идентификатора.
SYMBOL_TRADE_TICK_SIZE – величина минимального изменения цены.
Если размер тика не равен пункту, то при расчете уровней стоплосса, текйпрофита, а также уровней отложенных ордеров может потребоваться коррекция значения так, чтобы получилась существующая цена. Для этого надо разделить цену на величину тика, округлить, умножить на величину тика и нормализовать:
double LevelNormalize(string symbol, double price){
double tickSize=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE); int d=SymbolInfoInteger(symbol,SYMBOL_DIGITS); price=NormalizeDouble(MathRound(price/tickSize)*tickSize,d); return(price);
}
SYMBOL_TRADE_TICK_VALUE – стоимость минимального изменения цены. Допустим, открыта позиция 1 лот, цена сделала свое минимальное изменение, при этом прибыль позиции изменится на величину, получаемую при помощи данного идентификатора. Для символа EURUSD это $10 (если депозит в долларах). При создании экспертов обычно бывает нужна стоимость пункта, ее можно вычислить, используя следующих код:
double tickValue=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_VALUE); double tickSize=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE); double point=SymbolInfoDouble(Symbol(),SYMBOL_POINT);
double pointValue=tickValue*point/tickSize; Alert("pointValue=",pointValue);
SYMBOL_VOLUME_MAX – максимально допустимый лот сделки. SYMBOL_VOLUME_MIN – минимально допустимый лот сделки. SYMBOL_VOLUME_STEP – шаг изменения лота.
Иногда в экспертах при выполнении сделки величина ее лота вычисляется, но торговые условия у разных брокеров разные, поэтому рассчитанное значение надо подкорректировать в соответствии с ними. От расчетного значения лота вычтем величину минимально допустимого лота, оставшуюся величину поделим на величину шага, округлим, снова умножим на величину шага и прибавим минимальный лот. Получается следующая функция:
double LotsNormalize(string symbol,double lots){
double max=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX); double min=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN); double stp=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP); lots-=min;
lots/=stp; lots=MathRound(lots); lots*=stp;
lots+=min; lots=NormalizeDouble(lots,8); lots=MathMin(lots,max); lots=MathMax(lots,min); return(lots);
}
Обратите внимание, после всех вычислений выполняется нормализация и коррекция по максимальному и минимальному значениям. Таким образом, получается лот, с которым наиболее вероятно получится выполнить сделку. Не смотря на то что обычно лот делится с точностью до одного или двух знаков, нормализация до восьми знаков в данном случае работает нормально.
Протестируем функцию, вызвав ее из функции OnStart():
double lot=LotsNormalize(Symbol(),0.33333); Alert("lot=",lot);
Функция SymbolInfoInteger() чаще всего используется со следующими идентификаторами:
SYMBOL_DIGITS – количество знаков после запятой у котировок.
SYMBOL_SPREAD – величина спреда в пунктах (например, 3).
int digits=(int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS); int spread=(int)SymbolInfoInteger(Symbol(),SYMBOL_SPREAD); Alert("digits=",digits,", spread=",spread);
Есть и другие важные идентификаторы, которые необходимы при самостоятельной подготовке торгового запроса, но они будут рассмотрены в соответствующей теме.
Функция SymbolInfoString() при разработке советников индикаторов обычно не используется. Некоторые разработчики для получения рыночных цен предпочитают функцию SymbolInfoTick().
В функцию передается два параметра: имя символа и по ссылке структура MqlTick. При вызове этой функции необходимо проверить результат ее работы:
MqlTick tick; if(!SymbolInfoTick(Symbol(),tick)){
return;
}
Теперь, имея заполненную структуру tick, обращаемся к нужным полям:
Alert("tick.bid=",tick.bid,", tick.ask=",tick.ask);
Структура MqlTick имеет поля не только bid и ask, но и большое количество других, ознакомьтесь с ними по справочному руководству. Использование функции SymbolInfoTick() целесообразно, если нужны не только цены bid и ask, но и другие рыночные данные. В большинстве же случаев достаточно функции SymbolInfoDouble() с идентификаторами SYMBOL_ASK и SYMBOL_BID. Можно даже написать пару очень удобных функций, которые значительно облегчат разработку экспертов, поскольку избавят от необходимости каждый раз писать длинные имена функций и длинные идентификаторы:
double Bid(){ return(SymbolInfoDouble(Symbol(),SYMBOL_BID));
}
double Ask(){ return(SymbolInfoDouble(Symbol(),SYMBOL_ASK));
}
Вызов:
Alert("Bid()=",Bid(),", Ask()=",Ask());
Функция MarketBookGet() используется для получения содержимого стакана цен, то есть для получения списка заявок, действующих на рынке по указанному символу. Первым параметром в функцию передается символ, вторым – по ссылке – массив структур MqlBookInfo, который будет заполнен данными. Возвращает функция значение true или false в зависимости от успешности ее работы. Получим данные стакана:
MqlBookInfo b[];
bool r=MarketBookGet(Symbol(),b);
Структура MqlBookInfo имеет следующие поля: type – тип заявки, price – цена заявки, volume – объем заявки (тип long), volume_real – объем повышенной точности (тип double). Поле type может иметь одно из следующих значений: BOOK_TYPE_SELL – заявка на продажу, BOOK_TYPE_BUY — на покупку, BOOK_TYPE_SELL_MARKET – на продажу по рыночной цене, BOOK_TYPE_BUY_MARKET – на покупку по рыночной цене. Посмотрим содержимое массива b[]:
if(r){
for(int i=0;i<ArraySize(b);i++){ Alert(EnumToString(b[i].type),
", price=",b[i].price,
", volume=",b[i].volume,
", volume_real=",b[i].volume_real
);
}
}
Данные о счете
Узнать баланс, объем свободных средств, торговое плечо, имя брокера и т.п. можно при помощи функций AccountInfoDouble(), AccountInfoInteger() и AccountInfoString(). В эти функции передается один параметр – идентификатор дынных, которые необходимо получить.
С функцией AccountInfoDouble() чаще всего используются следующие идентификаторы:
ACCOUNT_BALANCE – баланс счета.
ACCOUNT_EQUITY – эквити счета (сумма баланса и прибыли). ACCOUNT_MARGIN– маржа (зарезервированные средства).
ACCOUNT_MARGIN_FREE – свободные средства в валюте депозита. ACCOUNT_MARGIN_LEVEL – уровень свободных средств в процентах. ACCOUNT_PROFIT – текущая прибыль.
Следующий код располагается в скрипте с именем «004 AccountInfo»:
Alert(
"BALANCE=",AccountInfoDouble(ACCOUNT_BALANCE),"\n", "EQUITY=",AccountInfoDouble(ACCOUNT_EQUITY),"\n", "MARGIN=",AccountInfoDouble(ACCOUNT_MARGIN),"\n", "MARGIN_FREE=",AccountInfoDouble(ACCOUNT_MARGIN_FREE),"\n",
"MARGIN_LEVEL=",AccountInfoDouble(ACCOUNT_MARGIN_LEVEL),"\n", "PROFIT=",AccountInfoDouble(ACCOUNT_PROFIT)
);
Данные выводимые этим скриптом соответствуют данным, которые доступны в терминале (рис.
51).
Рис. 51. Данные о счете в терминале
Функция AccountInfoInteger(), бывает полезна в первую очередь для определения типа счета – того, каким образом на счете выполняется учет позиций. Наиболее распространены два типа счетов: хеджинговый и неттинговый. На хеджинговом счете возможно существование нескольких рыночных позиций. На неттинговом счете может существовать только одна позиция, а все сделки или добавляют объем существующей позиции или снижают его вплоть до полного закрытия позиции или смены ее направления. Для определения типа счета используется идентификатор ACCOUNT_MARGIN_MODE. С этим идентификатором функция может вернуть одно из трех значений:
ACCOUNT_MARGIN_MODE_RETAIL_HEDGING – на хеджинговом счете. ACCOUNT_MARGIN_MODE_RETAIL_NETTING – на неттинговом счете. ACCOUNT_MARGIN_MODE_EXCHANGE – используется на биржевых рынках.
При создании универсальных экспертов, которые могут работать на счетах различного типа, может пригодиться следующая функция:
int AccountType(){
long mmode=AccountInfoInteger(ACCOUNT_MARGIN_MODE); if(mmode==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
return(0);
}
else if(mmode==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){ return(1);
}
else if(mmode==ACCOUNT_MARGIN_MODE_EXCHANGE){ return(2);
}
return(-1);
}
Функция возвращает 0 на хеджинговых счетах, 1 на неттинговых и 2 на биржевых. Пример вызов этой функции из функции OnStart() скрипта:
Alert("AccountType()=",AccountType());
В остальных идентификаторах функции AccountInfoInteger() особой необходимости не бывает, но некоторые из них могут быть полезны, например, для формирования собственных отчетов о торговле:
ACCOUNT_LOGIN – номер торгового счета.
ACCOUNT_CURRENCY_DIGITS – количество знаков после запятой у валюты депозита, может использоваться для функции DoubleToString().
ACCOUNT_LEVERAGE – величина кредитного плеча.
Еще пара интересных идентификаторов, но которые вряд ли будут полезны на практике:
ACCOUNT_TRADE_ALLOWED – разрешена ли торговля на счете.
ACCOUNT_TRADE_EXPERT – разрешена ли на счете работа экспертов. Запрет выполняется через запрет выполнения торговых операций экспертом на клиентском терминале, а не путем отклонения сделок на сервере брокера. То есть, если работа экспертом не запрещена, брокер не может определить, выполняется ли сделка вручную или экспертом.
Функция AccountInfoString() может использоваться для получения строковых данных типа наименования валюты депозита, имени владельца счета и т.п.
Alert("CURRENCY=",AccountInfoString(ACCOUNT_CURRENCY));
Остальные идентификаторы для этой функции не составит труда изучить самостоятельно по справочному руководству, их не много.
Данные о терминале
Функции TerminalInfoDouble(), TerminalInfoInteger() и TerminalInfoString() позволяют получить различные данные о терминале. Из этих функций может быть очень полезна функция TerminalInfoString() со следующими идентификаторами:
TERMINAL_DATA_PATH – путь к папке данных терминала. Это та папка, в которой находится папка Files. Стандартные средства по работе с файлами язык MQL5 позволяют работать только с файлами, находящимися в этой папке. Если необходимо обратиться к другим файлам, можно скопировать их в папку Files при помощи функции CopyFileW() из Windows API, а для этого нужно знать полный путь к этой папке.
TERMINAL_COMMONDATA_PATH – путь к общей папке данных.
Из всего обилия идентификаторов для функции TerminalInfoInteger() могут пригодиться следующие два:
TERMINAL_CONNECTED – проверка наличия связи с торговым сервером.
TERMINAL_MAXBARS – максимально количество баров в окне. Это параметр из настроек терминала. А не фактическое количество баров на графике.
TERMINAL_LANGUAGE – язык интерфейса терминала. Данный идентификатор позволит, например, делать вывод информационных сообщений эксперта или индикатора на этом же языке.
Пример получения данных о терминале и вывода сообщения на разных языках приведен в скрипте «005 TerminalInfo»:
Alert(
"DATA_PATH=",TerminalInfoString(TERMINAL_DATA_PATH),"\n", "COMMONDATA_PATH=",TerminalInfoString(TERMINAL_COMMONDATA_PATH),"\n",
"CONNECTED=",TerminalInfoInteger(TERMINAL_CONNECTED),"\n", "MAXBARS=",TerminalInfoInteger(TERMINAL_MAXBARS),"\n", TerminalInfoString(TERMINAL_LANGUAGE)=="Russian"?"Привет":"Hello"
);
Чтобы узнать точное наименование того или иного языка, переключите терминал на интересующий язык (главное меню в терминале – Вид – Languages) и посмотрите, что возвращает функция TerminalInfoString() с идентификатором TERMINAL_LANGUAGE.
Для функции TerminalInfoInteger() существует идентификатор TERMINAL_DLLS_ALLOWED, позволяющий определить, разрешено ли в терминале использование DLL. Иногда бывает нужно это узнать, чтобы дать пользователю подсказу о необходимости включения этого разрешения. Однако надо иметь в виду, данный идентификатор касается общих настроек терминала, а фактическое разрешение зависит от настроек в окне свойств эксперта. В данном случае будет более полезна функция MQLInfoInteger().
Функции MQLInfoInteger() и MQLInfoString() предоставляют возможность получения множества полезной информации, делающей программы MQL более удобными и функциональными.
Наиболее используемые идентификаторы функции MQLInfoInteger(): MQL_DLLS_ALLOWED – разрешение использовать библиотеки DLL. MQL_TESTER – работа эксперта или индикатора в тестере.
MQL_OPTIMIZATION – работа эксперта в тестере в режиме оптимизации.
MQL_VISUAL_MODE – работа в тестере в визуальном режиме.
MQL_TRADE_ALLOWED – разрешение выполнять торговые сделки (в том числе и модификацию позиций и ордеров).
У функции MQLInfoString() всего два идентификатора и один из них очень часто используется:
MQL_PROGRAM_NAME – имя программы (эксперта, индикатора, скрипта) из которой выполнен вызов этой функции. На практике используется при работе с графическими объектами – добавляется к именам объектов. Таким образом, различные программы работают только со своими графическими объектами и не мешают друг другу.
Пример использования функций MQLInfoInteger() и MQLInfoString() из скрипта «006 MQLInfo»:
Alert(
"DLLS_ALLOWED=",MQLInfoInteger(MQL_DLLS_ALLOWED),"\n", "TESTER=",MQLInfoInteger(MQL_TESTER),"\n", "OPTIMIZATION=",MQLInfoInteger(MQL_OPTIMIZATION),"\n", "VISUAL_MODE=",MQLInfoInteger(MQL_VISUAL_MODE),"\n", "TRADE_ALLOWED=",MQLInfoInteger(MQL_TRADE_ALLOWED),"\n", "PROGRAM_NAME=",MQLInfoString(MQL_PROGRAM_NAME),"\n"
);
Исторические данные
Исторические данные – это данные представленные в терминале в виде графиков из баров или свечей. У индикаторов существует очень простой доступ к данным графика, на котором он работает. Однако данные различных графиков (символов и таймфреймов) бывают нужны и в экспертах, также и некоторые индикаторы работают с использованием данных других таймфреймов и символов.
Существует универсальная функция CopyRates(), позволяющая получать все данные графика – все цены баров, время, объемы. В некоторых случаях все цены бара не нужны, а нужно только время или цена close, для таких случаев предусмотрены функции CopyTime(), CopyClose() и пр.
Кроме того существует несколько разновидностей каждой функции (перегруженные функции). Начнем рассмотрение этих функций с функции CopyRates() с ее наиболее часто используемой разновидности. Создайте скрипт с именем «007 CopyRates», следующий код будем писать в нем.
Первым и вторым параметрами в функцию передаются символ и таймфрейм графика, с которого надо получить данные. Если нужно получить данные с того же графика, на котором запущена программа, используем функции Symbol() и Period(). Третьим параметром указывается начальная позиция, с которой выполняется копирование, отсчет этой позиции выполняется справа налево от нуля. Четвертым параметром указывается количество копируемых баров. Это количество тоже отсчитывается справа налево. Таким образом, если нужно скопировать данные двух баров – нулевого (формирующегося) и первого сформированного, третий параметр должен быть равен 0, а четвертый – 2.
Пятым параметром по ссылке в функцию передается массив структур типа MqlRates. А вот в этом массиве данные будут располагаться слева направо. То есть данные того бара, который был указан как начальный, будут располагаться в последнем элементе массива. Такие особенности требуют аккуратности и повышенного внимания при использовании функции CopyRates().
Обязательно нужно проверять результат, возвращаемый функцией, она возвращает количество скопированных баров или -1 в случае неудачи. Скопируем данные одного первого бара:
MqlRates data[]; if(CopyRates(Symbol(),Period(),1,1,data)==-1){
return;
}
Alert("time=",data[0].time,"\n",
"open=",data[0].open,"\n",
"high=",data[0].high,"\n",
"low=",data[0].low,"\n",
"close=",data[0].close,"\n", "tick_vol=",data[0].tick_volume,"\n", "real_vol=",data[0].real_volume,"\n", "spread=",data[0].spread
);
Если нужны данные нескольких баров, во избежание путницы с направлениями отсчета их можно скопировать по одному, но в этом случае программа будет работать медленнее, поэтому лучше скопировать сразу два бара. Скопируем данные первого и второго баров:
if(CopyRates(Symbol(),Period(),1,2,data)==-1){ return;
}
Alert(data[0].time," ",data[1].time);
При выполнении этой части кода на графике H1 был получен результат «2020.07.10 16:00:00 2020.07.10 17:00:00», у формирующегося бара было время 18:00.
Массив структур MqlRates может быть как динамическим, так и статическим, но в случае использования статического массива, его размер не должен быть меньше количества копируемых элементов.
Остальные функции (копирующие только один ряд данных) вызываются точно также, как функция CopyRates(), отличие только в типе 5-го параметра. Для функций CopyOpen(), CopyHigh(), CopyLow(), CopyClose() используются массивы типа double, для функции CopyTime() – массив типа datetime, для функций CopyTickVolume() и CopyRealVolume() – массив типа long, для функции CopySpread() – массив типа int.
Рассмотрим перегруженные варианты функций. Во втором варианте функций третьим параметром вместо индекса начального бара, указывается начальное время. В третьем варианте третьим параметром указывается начальное время, а четвертым – конечное время. Вот тут уже могут возникнуть сложности с пониманием, какое время считается начальным, какое конечным. Но, как известно, критерием истины является практика. Используя функцию CopyTime() рассмотрим все варианты на практике. Причем, еще возникают вопросы, что будет, если указывать не точное время бара, а время между барами или время пропущенного бара, поэтому для экспериментов будет использоваться участок графика на стыке между неделями (рис. 52).
Рис. 52. График с пропущенными барами. Сплошной линией показан последний бар пятницы, пунктирной – первый бар понедельника. Все другие бары идут с интервалом ровно в один час.
Создайте скрипт с именем «008 CopyTime», пишем код в нем. Сначала скопируем два бара. Начальным баром будем считать бар с датой «2020.07.06 03:00» (высокий белый бар в правой части графика на рис. 52).
datetime tm[];
if(CopyTime(Symbol(),Period(),D'2020.07.06 03:00',2,tm)==-1){ return;
}
Alert("1: ",tm[0]," ",tm[1]);
Результат: «1: 2020.07.06 02:00:00 2020.07.06 03:00:00» – скопирован бар, указанный как начальный, и скопирован предшествующий ему бар.
Попробуем в качестве начального бара указать время между барами, укажем время 2020.07.06 03:30′ (это время чуть больше упомянутого выше высокого белого бара, но меньше времени бара, следующего за ним):
if(CopyTime(Symbol(),Period(),D'2020.07.06 03:30',2,tm)==-1){ return;
}
Alert("2: ",tm[0]," ",tm[1]);
Результат: «2: 2020.07.06 02:00:00 2020.07.06 03:00:00» – точно такой же результат, как и при точном указании начального времени, что логично.
Укажем начальным баром первый бар понедельника:
if(CopyTime(Symbol(),Period(),D'2020.07.06 00:00',2,tm)==-1){ return;
}
Alert("3: ",tm[0]," ",tm[1]);
Результат: «3: 2020.07.03 23:00:00 2020.07.06 00:00:00» – скопирован первый бар понедельника и предшествующий ему последний бар пятницы – тоже ожидаемый логичный результат.
Теперь в качестве начального времени используем время отсутствующего бара (время первого бара субботы):
if(CopyTime(Symbol(),Period(),D'2020.07.04 00:00',2,tm)==-1){ return;
}
Alert("4: ",tm[0]," ",tm[1]);
Результат: «4: 2020.07.03 22:00:00 2020.07.03 23:00:00» – скопированы два последний бара пятницы – тоже логичный результат.
Рассмотрим третий вариант функции, когда указывается начальное и конечное время. По аналогии с предыдущим вариантом, очевидно, что конечное время предшествует начальному времени. При использовании этого варианта изначально неизвестно, сколько баров будет скопировано, поэтому необходимо сохранять в переменную значение, возвращаемое функцией. Начальным временем укажем время чуть больше времени первого бара понедельника, а конечном – чуть больше времени последнего бара пятницы:
int cnt=CopyTime(Symbol(),Period(),D'2020.07.06 00:05',D'2020.07.03 23:05',tm); if(cnt==-1){
return;
}
string str="";
for(int i=0;i<cnt;i++){ str=str+TimeToString(tm[i])+" ";
}
Alert("5: ",str);
Результат: «5: 2020.07.06 00:00» – скопирован один бар, не смотря на то, что конечное время попадает внутрь последнего бара пятницы, оно не захватывает начало бара. Чтобы скопировался и последний бар пятницы, конечное время должно быть или точно равно ему или быть чуть меньше:
cnt=CopyTime(Symbol(),Period(),D'2020.07.06 00:05',D'2020.07.03 23:00',tm);
if(cnt==-1){ return;
}
str="";
for(int i=0;i<cnt;i++){ str=str+TimeToString(tm[i])+" ";
}
Alert("6: ",str);
Результат: «6: 2020.07.03 23:00 2020.07.06 00:00» – скопировано два бара (послденй бар пятницы и первый бар понедельника).
Интересно, какой будет результат, если между начальным и конечным временем вообще не будет баров, скопируем бары в промежутки от начала субботы до начала воскресенья:
cnt=CopyTime(Symbol(),Period(),D'2020.07.05 00:00',D'2020.07.04 00:00',tm);
Alert("7: ",cnt);
Результат: «7: 0» – ошибки нет, но и данных тоже нет.
Пока что мы копировали небольшое количество данных – один или два бара, больше обычно не бывает нужно при разработке советника. А вот при создании индикатора бывает нужно скопировать все доступные данные другого графика. Во-первых, надо убедиться, что данные другого графика существуют и готовы к использованию. В этом случае очень пригодится функция Bars(). Это функция возвращает количество баров на графике указанного символа и таймфрейма. Но кроме этого, функция запускает сформирования этого графика, если он еще не был сформирован. Значит, прежде чем копировать значительное количество данных с другого графика, необходимо вызвать функцию Bars(), если она вернет значение -1, значит, данные еще не готовы. В функцию передается два параметра: символ и таймфрейм.
Создайте скрипт с именем «009 Bars», пишем код в нем:
int bars=Bars("EURUSD",PERIOD_H4); if(bars==-1){
Alert("Данные не готовы, повторите попытку"); return;
}
После этого значение переменной bars необходимо подкорректировать по количеству баров графика, указанному в настройках терминала:
bars=MathMin(bars,TerminalInfoInteger(TERMINAL_MAXBARS));
После этого можно скопировать данные:
double data[]; if(CopyClose("EURUSD",PERIOD_H4,0,bars,data)==-1){
Alert("Ошибка копирования");
}
Есть второй вариант вызова функции Bars() – с указанием начального и конечного времени. Результат работы функции должен совпадать с количеством скопированных данных при использовании третьего варианта функций CopyRates(), CopyTime() и т.п.
bars=Bars(Symbol(),Period(),D'2020.07.06 00:05',D'2020.07.03 23:00');
Alert("bars=",bars);
Результат: «bars=2» – такое количество, как в примере выше при вызове функции CopyTime() с такими же параметрами.
При копировании большого количества данных может пригодиться функция SeriesInfoInteger(). В функцию передается символ, период и идентификатор, указывающий какие данные надо получить. Пример:
datetime fd=SeriesInfoInteger(Symbol(),Period(),SERIES_TERMINAL_FIRSTDATE); Alert("firstDate=",fd);
С идентификатором SERIES_TERMINAL_FIRSTDATE функция возвращает самую первую дату в истории по указанному символу и таймфрейму. С остальными идентификаторами ознакомьтесь самостоятельно по справочному руководству.
Существует еще один набор функций для получения данных с других символов и таймфреймов, это функции iClose(), iHigh(), iLow(), iClose(), iTime(), iVolume(), iTickVolume(),iRealVolume(), iSpread(). Изначально этих функций не было в языке MQL5, они были добавлены гораздо позже, для сохранения преемственности с языком MQL4 и терминалом MetaTrader4. Поэтому здесь они приводятся не в качестве рекомендации их использования, а только с ознакомительной целью.
Назначение этих функций очевидно из их названий. Во все эти функции передается по три параметра: символ, таймфрейм и индекс бара (отсчет баров справа налево от нуля). Особенность этих функций в том, что они не требует предварительного формирования графика путем вызова функции Bars() и не требуют никаких дополнительных проверок. При вызове любой их этих функций выполняется формирование соответствующего графика, а потом возвращается требуемое значение. В связи с этим первый вызов функции может занимать относительно много времени, вплоть до нескольких секунд или даже десятков секунд. К этой же категории относится еще несколько функций: iBars() – количество баров на графике, iBarsShift() – получение индекса бара по его времени, iHighest() – получение максимального бара за указанный период, iLowest() – получение минимального бара за указанный период.
Тиковые данные
Для получения тиковых данных используется функция CopyTicks(), у функции два обязательных параметра: символ и массив структур MqlTick, передаваемый в функцию по ссылке. Возвращает функция количество полученных тиков или -1 в случае ошибки. При таком наборе параметров функция возвращает все доступные в терминале тики, но при необходимости можно использовать три дополнительных параметра, определяющих тип копируемых данных, их начальное время и количество. Данные в массиве располагаются слева направо, то есть самый старый тик в элементе 0, а самый последний (тик с начальным временем) в последнем элементе массива. Первый вызов функции может потребовать загрузки данных с сервера и, соответственно, занять заметное время.
Создайте скрипт с именем «010 CopyTicks», следующие примеры пишем в нем. Простое копирование тиковых данных:
MqlTick t[];
int cnt=CopyTicks(Symbol(),t); if(cnt==-1){
return;
}
Alert(cnt," ",t[0].time," ",t[cnt-1].time);
Третий параметр функции позволяет указать тип копируемых данных. Возможно сочетание следующих флагов:
COPY_TICKS_INFO – тики, вызванные изменениями цены Bid или Ask. COPY_TICKS_TRADE – тики с изменением цены Last или объема Volume. COPY_TICKS_ALL – все тики (применяется по умолчанию).
Четвертый параметр определяет начальное время копируемых тиков (время последнего тика) и количество копируемых тиков. Пример:
cnt=CopyTicks(Symbol(),t,COPY_TICKS_ALL,TimeCurrent(),3000); if(cnt==-1){
return;
}
Alert("2: ",cnt," ",t[0].time," ",t[cnt-1].time);
Если скопированы все тики (когда третий параметр равен COPY_TICKS_ALL), по значению поля flag структуры MqlTick можно определить тип тика. Для этого надо выполнить операцию побитового «и» поля с соответствующей константой и проверить результат на равенство этой константе:
bool info=((t[cnt-1].flags©_TICKS_INFO)==COPY_TICKS_INFO); bool trade=((t[cnt-1].flags©_TICKS_TRADE)==COPY_TICKS_TRADE); Alert("3: info=",info,", trade=",trade);
Технические индикаторы
В терминал встроено большое количество индикаторов технического анализа – скользящие средние, стохастик и т.п. Кроме того, можно создавать свои собственные индикаторы. Всеми ими можно пользоваться из любой программы написанной на языке MQL5. Для всех стандартных (встроенных) индикаторов существуют соответствующие функции языка MQL5, например iMA() – скользящая средняя, iStochastic() – стохастик. С полным перечнем функций технических индикаторов можно познакомиться по справочному руководству к языку MQL5 (раздел «Технические индикаторы»). Для обращения к пользовательским индикаторам применяется функция iCustom() или IndicatorCreate().
Начнем с рассмотрения использования стандартных индикаторов. Создайте скрипт с именем «011 Indicators», следующий код будем писать в нем. Первым делом необходимо загрузить индикатор в память и получить его хэндл (идентификатор), делается это только один раз на запуске программы. Загрузка и получение хэндла выполняется вызовом соответствующей функции (iMA(), iStochastic() т.п.).
После вызова функции необходимо проверить успешность действия – проверить значение хэндла. Если значение хэндла равно константе INVALID_HANDLE, необходимо уведомить пользователя и прервать выполнение скрипта (или эксперта, индикатора). Придется немного забежать вперед. В скрипте есть только одна функция OnStart(), а в эксперте и в индикаторе есть функция, которая срабатывает один раз на запуске и есть функция, которая срабатывает на каждом изменении цены. Хэндл надо получать в той функции, которая срабатывает один раз на запуске. Но пока в учебном скрипте пишем весь код подряд.
Попробуем получить данные стандартной скользящей средней – это функция iMA(). Первым параметром в функцию передается символ, вторым – таймфрейм. Затем период индикатора, смещение, тип сглаживания и тип цены. В функции всех стандартных индикаторов, у которых есть параметр цены, вместо цены можно передавать хэндл другого индикатора. Таким образом, индикатор будет рассчитываться не от цены, а от данных другого индикатора.
Объявим переменные для параметров индикатора. В учебном скрипте можно объявить переменные в любом месте – как глобальные или как локальные в функции OnStart():
int period=14;
ENUM_MA_METHOD method=MODE_SMA; ENUM_APPLIED_PRICE price=PRICE_CLOSE;
Вызываем функцию iMA(), сохраняя возвращаемой ей значение в переменную:
int h=iMA(Symbol(),Period(),period,0,method,price);
Проверяем значение хэндла (переменной h):
if(h==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор iMA"); return;
}
Для получения данных индикатора используется функция CopyBuffer(), эта функция во многом схожа с функциями CopyRates(), CopyTime(), CopyClose() и т.п. Отличие в том, что теперь два первых параметра – это хэндл индикатора и индекс буфера (узнается из справочного руководства из описания функции индикатора), все остальное абсолютно точно так же (в том числе и три перегруженных варианта).
double ind[]; if(CopyBuffer(h,0,1,1,ind)==-1){
Alert("Ошибка копирования данных iMA"); return;
}
Alert("MA=",ind[0]);
Обратите внимание, при вызове функции iMA() параметр смещения (4-ый параметр) не используется. Вообще не рекомендуется использовать параметр смещения при вызове функций технических индикаторов. Если необходимо получить данные со смещением, указывайте соответствующий индекс бара при вызове функции CopyBuffer().
Теперь получим данные индикатора RSI (Relative Strength Index – индекс относительной силы) от этой скользящей средней. Объявляем переменную для параметра индикатора:
int rsi_period=14;
Загружаем индикатор, получаем хэндл и проверяем его:
int rsih=iRSI(Symbol(),Period(),rsi_period,h); if(rsih==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор iRSI"); return;
}
Получаем данные RSI от скользящей средней:
if(CopyBuffer(rsih,0,1,1,ind)==-1){ Alert("Ошибка копирования данных iRSI"); return;
}
Alert("RSI=",ind[0]);
С пользовательскими индикаторами все точно так же, только добавляется еще один параметр. Третьим параметром указывается имя пользовательского индикатора, а параметры вызываемого (загружаемого) индикатора расположены начиная с четвертого параметра. Попробуем получить данные пользовательского индикатора RSI из папки Examples. Сначала объявляем переменную для параметра индикатора:
int rsi_period2=14;
Загружаем индикатор, получаем хэндл и проверяем его:
int ch=iCustom(Symbol(),Period(),"Examples\\RSI",rsi_period2); if(ch==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор RSI"); return;
}
Обратите внимание на третий параметр функции iCustom() – указан путь от корневой папки
«Indicators», если бы файл индикатора размещался непосредственно в папке «Indicators», то достаточно было бы указать только имя. Также можно использовать индикаторы, купленные в маркете на сайте mql5.com, эти индикатора располагаются в папке «Market», располагающейся в папке «Indicators», соответственно перед именем индикатора указывается путь «Market\\».
Копируем данные буфера:
if(CopyBuffer(ch,0,1,1,ind)==-1){
Alert("Ошибка копирования данных пользовательского RSI"); return;
}
Alert("Custom RSI=",ind[0]);
Рис. 53. Пользовательский индикатор ADX (папка Examples), вкладка цвета
Узнать индекс буфера для пользовательского индикатора по справочному руководству уже не получится. С индикатором RSI все просто, у него один буфер, поэтому указывается 0. Если буферов больше одного, прикрепите индикатор на график и посмотрите на его вкладку «Цвета». Индексы буферов обычно соответствуют порядку их расположения в этой вкладке, отсчет с нуля (рис. 53).
Однако с индексами буферов пользовательских индикаторов не всегда бывает так однозначно. В пользовательском индикаторе могут применяться особые разноцветные буферы, в этом случае, особенно если буферов много, бывает не так просто определить индекс нужного буфера. Дело в том, что на самом деле цветной буфер представляет собой несколько буферов, но во вкладке «Цвета» отображается только один из них, а через функцию iCustom() доступны все. Проблема решается через изучение кода пользовательского индикатора или методом проб и ошибок. Позже, при изучении создания пользовательских индикаторов, все это будет объяснено подробно.
У некоторых пользовательских индикаторов кроме вкладки «Входные параметры» существует еще вкладка «Параметры» (рис. 54), в этой вкладке выполняется выбор цены, по которой рассчитывается индикатор.
Рис. 54. Дополнительная вкладка «Параметры» у пользовательского индикатора RSI из папки Examples
Для таких индикаторов при вызове из функций iCustom() можно указывать еще один параметр – цену, этот параметр должен быть последним. Значит, первые три параметра: символ, таймфрейм имя. Затем, параметры пользовательского индикатора и цена. Если указывается цена, то нужно указывать все параметры пользовательского индикатора. Если же цена не указывается, параметры пользовательского индикатора можно указывать не все (только несколько начальных) или даже вообще их не указывать, в этом случае для пропущенных параметров индикатор будет использовать значения по умолчанию (те, которые указаны в окне свойств индикатора).
Пример применения пользовательского индикатора RSI с указанием цены:
ch=iCustom(Symbol(),Period(),"Examples\\RSI",rsi_period2,PRICE_OPEN); if(ch==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор RSI(OPEN)"); return;
}
if(CopyBuffer(ch,0,1,1,ind)==-1){
Alert("Ошибка копирования данных пользовательского RSI(OPEN)"); return;
}
Alert("Custom RSI(OPEN)=",ind[0]);
Пример применения пользовательского индикатора RSI без указания параметров:
ch=iCustom(Symbol(),Period(),"Examples\\RSI"); if(ch==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор RSI(no parameters)"); return;
}
if(CopyBuffer(ch,0,1,1,ind)==-1){
Alert("Ошибка копирования данных пользовательского RSI(no parameters)"); return;
}
Alert("Custom RSI(no parameters)=",ind[0]);
Результат работы этого примера соответствует результату при вызове индикатора с параметрами, но без указания цены (rsi_period2=14 и значение по умолчанию в окне свойств индикатора тоже 14).
Еще один способ получения данных технического индикатора – функция IndicatorCreate(). Эта функция используется вместо функции iCustom() для загрузки индикатора и получения его хэндла, затем иcпользуется функция CopyBuffer(). Функция IndicatorCreate() может использоваться как для стандартных индикаторов, так и пользовательских, что делает ее универсальной.
Первые два параметра функции – это символ и таймфрейм. Третий параметр определяет тип индикатора, для его указания используется специальный идентификатор, начинающийся с префикса IND_. Стоит только начать его вводить, как откроется список с выбором: IND_AC, IND_AD, IND_ADX и т.д. – назначение идентификаторов очевидно из названий, они соответствуют именам функций стандартных технических индикаторов. Для пользовательского индикатора указывается идентификатор IND_CUSTOM (с полным списком идентификаторов можно ознакомиться через справочное руководство). Четвертый параметр используется для указания количества параметров индикатора, а точнее – размера массива с параметрами, который передается пятым параметром.
Для передачи параметров индикатора (5-ый параметр функции) используется массив структур типа MqlParam. В этой структуре четыре поля, три из них используются для передачи значения: double_value, integer_value, string_value. Четвертое поле используется для указания типа применяемого поля со значением – для этого используются идентификаторы: TYPE_DOUBLE, TYPE_INT, TYPE_STRING и п.р. (см. справочное руководство). Точное указание целочисленного типа, например, использовать ли идентификатор TYPE_INT или TYPE_LONG, определяется не типом параметра индикатора, а типом переменной, значение которой присваивается полю integer_value. То есть, нет необходимости в скрупулезном исследовании пользовательского индикатора, достаточно разобраться с тремя типами: дробное число, целое или строка.
Если применяется пользовательский индикатор, первый элемент массива структур MqlParam должен быть типа string, а в поле для значения указывается имя индикатора. Если четвертый параметр функции IndicatorCreate() не передавать или передать 0, пятый параметр можно не передавать, в этом случае будут использоваться значения параметров по умолчанию.
Применим функцию IndicatorCreate() для индикатора «Моментум». Первым делом смотрим по справочному руководству описание функции iMomentum() – сколько у нее параметров и объявляем соответствующие переменные:
int MomPeriod=14;
ENUM_APPLIED_PRICE MomPrice=PRICE_CLOSE;
Параметров всего два, соответственно объявляем массив такого же размера:
MqlParam p[2];
Заполняем массив:
p[0].integer_value=MomPeriod; p[0].type=TYPE_INT; p[1].integer_value=MomPrice; p[1].type=TYPE_INT;
Получаем хэндл:
int mh=IndicatorCreate(Symbol(),Period(),IND_MOMENTUM,2,p); if(mh==INVALID_HANDLE){
Alert("Не удалось загрузить индикатор Momentum"); return;
}
Копируем данные из буфера:
if(CopyBuffer(mh,0,1,1,ind)==-1){
Alert("Ошибка копирования данных Momentum"); return;
}
Alert("Momentum=",ind[0]);
Теперь применим функцию InicatorCreate() для пользовательского индикатора RSI. У индикатора один параметр, еще один нужен для имени, то есть нужен массив структур из двух элементов, воспользуемся массивом p:
p[0].string_value="Examples\\RSI"; p[0].type=TYPE_STRING;
p[1].integer_value=rsi_period2; p[1].type=TYPE_INT;
Загружаем индикатор, получаем хэндл и проверяем его:
int ch2=IndicatorCreate(Symbol(),Period(),IND_CUSTOM,2,p); if(ch2==INVALID_HANDLE){
Alert("Ошибка IndicatorCreate() для пользовательского RSI"); return;
}
Копируем данные из буфера:
if(CopyBuffer(ch2,0,1,1,ind)==-1){ Alert("Ошибка копирования RSI2"); return;
}
Alert("Custom RSI2=",ind[0]);
Результат «Custom RSI2» должен быть таким же, как полученный ранее «Custom RSI».
Если пользовательский индикатор имеет вкладку «Параметры» для выбора цены (рис. 54), то в массив структур MqlParam можно добавить еще один элемент для указания цены.
Торговые запросы
Торговый запрос, это заявка, на выполнение торгового действия: открытия позиции, установки отложенного ордера и т.п. Абсолютно для всех видов торговых запросов используется одна функция OrderSend(). В эту функцию передается всего два параметра. Первый параметр – структура MqlTradeRequest, этот параметр определяет, какой конкретно запрос выполняется: рыночный ордер, отложенный ордер, модификация и т.п. Второй параметр – структура MqlTradeResult, в этой структуре будут находиться сведения о результате выполнении запроса. Функция возвращает значение true или false при успешном или неуспешном выполнении запроса, а через второй параметр функции становится доступно большее количество подробностей о результате выполнения этого запроса.
Правильное заполнение структуры MqlTradeRequest является довольно не простой задачей со множеством своих нюансов. Поэтому не рекомендуется самостоятельно заниматься их заполнением, лучше использовать специально подготовленные для этих целей торговые классы, которые подробно будет рассмотрены в дальнейшем. Здесь же примеры выполнения торговых запросов приводятся исключительно с ознакомительной целью.
С описанием полей структуры MqlTradeRequest вы можете ознакомиться по справочному руководству, поэтому сразу займемся ее заполнением для различных типов торговых запросов. Создайте скрипт «012 OrderSend», будем писать код в нем.
Рыночный ордер. Объявляем переменные типа MqlTradeRequest и MqlTradeResult:
MqlTradeRequest request; MqlTradeResult result;
В первую очередь структуры необходимо очистить:
ZeroMemory(request);
ZeroMemory(result);
В поле symbol указывается символ, на котором выполняется торговое действие:
request.symbol=Symbol();
В поле action указывается требуемое торговое действие, для рыночного ордера это идентификатор
TRADE_ACTION_DEAL:
request.action=TRADE_ACTION_DEAL;
Направление сделки указывается в поле type, это может быть идентификатор ORDER_TYPE_BUY для сделки на покупку или ORDER_TYPE_SELL для сделки на продажу. Будем совершать покупку:
request.type=ORDER_TYPE_BUY;
В поле volume указывается объем сделки (лот ы):
request.volume=0.1;
В поле price – цена открытия. Для рыночного ордера это должна быть текущая рыночная цена Bid (для продажи) или Ask (для покупки):
request.price=SymbolInfoDouble(aSymbol,SYMBOL_ASK);
За время, пока запрос дойдет до брокера, цена может измениться, поэтому необходимо указать допустимое отклонение цены, с которым допускается выполнение сделки. Для этого используется поле deviation, величина указывается в пунктах, укажем три спреда:
request.deviation=SymbolInfoInteger(Symbol(),SYMBOL_SPREAD)*3;
В полях sl и tp указываются цены стоплосса и тейкпрофита. Но пока не будем их устанавливать, поэтому укажем значения 0.
request.sl=0; request.tp=0;
В случае необходимости можно указать идентификатор «мэджик» (или «магик»), для этого используется поле magic. Обычно он используется для того, что бы можно было отличать ордера и сделки, выполненные разными экспертами. Разные эксперты, работающие на одном символе, должны использовать разные значения этого идентификатора. Также можно добавить ордеру комментарий:
request.magic=123; request.comment="My first request";
Если комментарий не нужен, можно не заполнять это поле или присвоить ему пустую строку:
request.comment="";
Пока все было просто, но теперь нужно указать тип так называемой «заливки» ордера – поле
type_filling. Всего может быть три варианта заливки:
ORDER_FILLING_FOK – заявка должна быть исполнено в требуемом объеме или отклонена.
ORDER_FILLING_IOC – заявка может быть исполнена частично. Оставшаяся часть заявки отклоняется.
ORDER_FILLING_RETURN – заявка может быть исполнена частично. Оставшаяся часть продолжает действовать.
Доступность того или иного способа заливки зависит от режима заключения сделок, установленного брокером. Этот режим можно узнать функцией SymbolInfoInteger() с идентификатором SYMBOL_TRADE_EXEMODE. Но, как уже было сказано ранее, примеры подготовки торговых запросов приводятся исключительно в ознакомительных целях, поэтому воспользуемся упрощенным способом. Примем, что вариант ORDER_FILLING_FOK является более предпочтительным, после него следует ORDER_FILLING_IOC и в конце ORDER_FILLING_RETURN. Будем проверять доступность этих способов по порядку, если какой- то способ доступен, используем его. Для получения набора доступных способов заливки (флагов) используется функция SymbolInfoInteger() с идентификатором SYMBOL_FILLING_MODE. Для проверки вхождения того или иного флага в набор используется побитовая операция «и». Получаем следующую функцию:
ENUM_ORDER_TYPE_FILLING GetTypeFilling(){
long fm=SymbolInfoInteger(Symbol(),SYMBOL_FILLING_MODE); if((fm&SYMBOL_FILLING_FOK)==SYMBOL_FILLING_FOK){
return(ORDER_FILLING_FOK);
}
else if((fm&SYMBOL_FILLING_IOC)==SYMBOL_FILLING_IOC){
return(ORDER_FILLING_IOC);
} return(ORDER_FILLING_RETURN);
}
Используя эту функцию, заполняем поле type_filling:
request.type_filling=GetTypeFilling();
Теперь можно вызвать функцию OrderSend():
bool rv=OrderSend(request,result);
Если функция вернула false, разбираемся с данными из структуры MqlTradeResult. В первую очередь интересует значение поля retcode – код возврата сервера. Расшифровку этого кода можно посмотреть в справочном руководстве (Коды возврата торгового сервера (Константы, перечисления и структуры – Коды ошибок и предупреждений – Коды возврата торгового сервера). Также расшифровка кода должна быть в поле comment.
Если сделка прошла успешно, ее тикет можно посмотреть в поле deal, а в поле order – тикет позиции:
if(!rv){
Alert("Ощибка ",result.retcode," (",result.comment,")");
}
else{
Alert("Тикет ",result.deal," ",result.order);
}
Выполним модификацию только что открытой позиции. Сохраняем тикет в переменной:
ulong ticket=result.order;
Очищаем структуры:
ZeroMemory(request);
ZeroMemory(result);
Заполняем структуру MqlTradeRequest. Поле action:
request.action=TRADE_ACTION_SLTP;
Заполняем поля sl (цена стоплосса) и tp (цена тейкпрофита). Стоплосс поставим на дистанции 100 пунктов от текущей цены, тейкпрфит на дистанции 200 пунктов, результаты расчетов обязательно нормализуем:
double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); request.sl=NormalizeDouble(ask-Point()*100,Digits()); request.tp=NormalizeDouble(ask+Point()*200,Digits());
Вызываем функцию OrderSend() и проверяем результаты ее работы:
rv=OrderSend(request,result);
if(!rv){
Alert("Ощибка модификацими ",result.retcode," (",result.comment,")");
}
else{
Alert("Модификация выполнена");
}
Чтобы выполнить закрытие позиции, необходимо выполнить сделку по открытию рыночной позиции такого же объема, но в противоположном направлении (все так же, как при открытии):
ZeroMemory(request);
ZeroMemory(result);
request.symbol=Symbol(); request.action=TRADE_ACTION_DEAL; request.type=ORDER_TYPE_SELL; request.volume=0.1;
request.price=SymbolInfoDouble(Symbol(),SYMBOL_BID); request.deviation=SymbolInfoInteger(Symbol(),SYMBOL_SPREAD)*3; request.magic=123;
request.comment="My second request"; request.type_filling=GetTypeFilling();
А для того, чтобы это сработало и на хеджинговом счете, необходимо указать тикет закрываемой позиции:
request.position=ticket;
После заполнения структуры вызываем функцию OrderSend() и проверяем результат:
rv=OrderSend(request,result); if(!rv){
Alert("Ощибка закрытия ",result.retcode," (",result.comment,")");
}
else{
Alert("Закрытие выполнено");
}
Теперь выполним установку отложенного ордера, его модификацию (перемещение) и удаление. Создайте скрипт «013 PendingOrder», пишем код в нем. Так же объявляем две переменных для запроса и результата, после чего очищаем их:
MqlTradeRequest request; MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
Поле symbol:
request.symbol=Symbol();
В полк action указывается действие TRADE_ACTION_PENDING:
request.action=TRADE_ACTION_PENDING;
В поле type укажем отложенный ордер байстоп:
request.type=ORDER_TYPE_BUY_STOP;
Объем ордера:
request.volume=0.1;
В поле price указывается уровень установки ордера. Установим его на уровне 50 пунктов от рыночной цены:
double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); request.price=NormalizeDouble(ask+Point()*50,Digits());
Устанавливаем «мэджик» и комментарий:
request.magic=123;
request.comment="My first pending order";
Для типа заливки воспользуется той же функцией, что и для рыночных ордеров:
request.type_filling=GetTypeFilling();
При подготовке запроса на установку отложенного ордера есть еще одна особенная задача – установка режима экспирации. Здесь необходимо заполнить два поля: type_time – тип экспирации и expiration – время экспирации. Если время экспирации устанавливается, то в поле type_time указывается идентификатор ORDER_TIME_SPECIFIED, а в поле expiration желаемое время экспирации. Если время экспирации не устанавливается, то указывается тип ORDER_TIME_GTC. Однако некоторых брокеры могут не поддерживать такой тип экспирации, необходимо проверить, существование этого флага в наборе методов экспирации по символу, если него нет, то использовать тип ORDER_TIME_DAY. В поле expiration при этом указывается 0. Все это сделано в отдельной функции SetExpiration(). В функцию передается два параметра: время экспирации (0 – без экспирации) и структура:
void SetExpiration(datetime expiration,MqlTradeRequest & request){ if(expiration==0){
int ex=(int)SymbolInfoInteger(Symbol(),SYMBOL_EXPIRATION_MODE); if((ex&SYMBOL_EXPIRATION_GTC)==SYMBOL_EXPIRATION_GTC){
request.type_time=ORDER_TIME_GTC;
}
else{
request.type_time=ORDER_TIME_DAY;
}
request.expiration=0;
}
else{
request.type_time=ORDER_TIME_SPECIFIED; request.expiration=expiration;
}
}
Заметьте, при установке времени экспирации не выполняется проверка, поддерживает ли брокер вариант ORDER_TIME_SPECIFIED. Подразумевается, что если установка ордера выполняется, например, из эксперта, то в нем имеются опции для выбора, устанавливать ли время экспирации или нет, а пользователь этого эксперта подходит к делу с пониманием сути и со всей
ответственностью. То есть пользователь должен самостоятельно сделать правильные настройки эксперта, он должен знать, поддерживает ли брокер экспирацию отложенного ордера.
Вызываем функцию SetExpiration() для установки экспирации. Время «TimeCurrent()+3600»
означает, что ордер будет удален через 3600 секунд (1 час) от момента установки ордера:
SetExpiration(TimeCurrent()+3600,request);
Вызываем функцию OrderSend() и проверяем результат:
bool rv=OrderSend(request,result); if(!rv){
Alert("Ощибка ",result.retcode," (",result.comment,")");
}
else{
Alert("Тикет ",result.deal," ",result.order);
}
Выполним модификацию только что установленного ордера, переместим его на дистанцию 100
пунктов от текущей цены. Очищаем структуры:
ZeroMemory(request);
ZeroMemory(result);
В поле action указываем тип TRADE_ACTION_MODIFY:
request.action=TRADE_ACTION_MODIFY;
В поле order указываем ткет:
request.order=ticket;
В поле price указываем новую цену ордера:
request.price=NormalizeDouble(ask+Point()*100,Digits());
В полях sl и tp указываются цены стоплосса и тейкпрофита, не будем их ставить:
request.sl=0; request.tp=0;
Установим экспирацию, точнее отменим ее:
SetExpiration(0,request);
Отправляем заявку и проверяем результат:
rv=OrderSend(request,result); if(!rv){
Alert("Ощибка модификации ",result.retcode," (",result.comment,")");
}
else{
Alert("Модификация выполнена");
}
Для удаления ордера надо заполнить только два поля: action и order:
ZeroMemory(request);
ZeroMemory(result);
request.action=TRADE_ACTION_REMOVE; request.order=ticket;
rv=OrderSend(request,result); if(!rv){
Alert("Ощибка удвления ",result.retcode," (",result.comment,")");
}
else{
Alert("Удаление выполнено");
}
В терминале MetqTrader 5 еще есть возможность работы с ордерами типа стоп-лимит. Такой ордер сначала представляет собой стоп-ордер, когда цена достигает его уровня, он превращается в лимитный ордер. Если это бай-стоп-лимит, цена должна подняться до уровня срабатывания ордера, после этого он превращается в бай-лимит ордер и цена должна опуститься вниз, чтобы открылась рыночная позиция. При работе с такими ордерами в структуре MqlTradeRequest задействуется еще одно поле – stoplimit. Значит, поле price – это первая цена срабатывания (цена стоп-ордера), поле stoplimit – вторая цена срабатывания (цена лимит-ордера).
Одной из частых задач при подготовке торгового запроса является расчет размера лота в зависимости от размера имеющихся средств, другими словами – расчет лота пропорционально депозиту. Под депозитом в данном случае подразумевается объем свободных средств на счете, но, в зависимости от поставленной задачи, может использоваться и баланс. Есть два подхода к решению данной задачи. В первом случае предполагается, что средства в размере 1000 единиц соответствуют одному лоту, исходя из этого, и из значения параметра риска, рассчитывается лот:
double SolveLots(double risk){
double means=AccountInfoDouble(ACCOUNT_MARGIN_FREE); return(means/1000*risk);
}
В функцию передается величина риска – число от 0 до 1 (1 – это 100%, полная нагрузка на депозит). Полученное от функции значение необходимо нормализовать, например, рассмотренной ранее функцией LotsNormalize(). Недостатком данного метода является то, что на самом деле величина риска не соответствует действительности. Например, на счете с кредитным плечом 200 и размером свободных средств $1000 не получится открыть позицию с лотом 1.0, а только с лотом 0.7. То есть на самом деле риску 100% соответствует лот 0.7. С другой стороны, данный подход удобен тем, что заранее понятно, какой примерно получится лот.
Второй способ расчета лота в зависимости от риска выполняется с учетом маржинальных требований. Чтобы узнать объем средств, необходимых для открытия позиции, используется функция OrderCalcMargin(). В функцию передается тип позиции, символ, объем, цена открытия и переменная по ссылке для возврата рассчитанного объема средств. Сама функция возвращает true или false в зависимости от успешности ее работы.
Напишем функцию для расчета лота в зависимости от типа позиции и риска. В функцию будет передаваться тип позиции, величина риска и переменная для возврата величины лота по ссылке, сама функция будет возвращать true или false:
bool SolveLots2(ENUM_ORDER_TYPE type,double risk,double & lot){ double pr;
if(type==ORDER_TYPE_BUY){ pr=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
}
else{
pr=SymbolInfoDouble(Symbol(),SYMBOL_BID);
}
double m;
bool r=OrderCalcMargin(type,Symbol(),1.0,pr,m); if(!r)return(false); lot=AccountInfoDouble(ACCOUNT_MARGIN_FREE)/m; lot*=risk;
lot=LotsNormalizeDn(Symbol(),lot); return(true);
}
Рассмотрим более подробно код этой функции. В зависимости от направления сделки, в переменную pr получаем цену ask или bid. Затем, вызываем функцию OrderCalcMargin() для получения объема средств, необходимых для открытия одного лота. Объем свободных средств делим на полученное в переменную m значение и получаем предельный лот, который можно открыть на имеющиеся средства. Тут же умножаем этот лот на риск, чтобы получить требуемое значение. Полученное значение надо нормализовать, но функция LotsNormalize() в данном случае не подойдет, потому что в результате округления может получиться значение, превышающее исходное. В данном случае надо использовать отбрасывание дробного остатка, поэтому используется функция LotsNormalizeDn():
double LotsNormalizeDn(string symbol,double lots){
double max=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MAX); double min=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN); double stp=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP); lots-=min;
lots/=stp; lots=MathFloor(lots); lots*=stp;
lots+=min; lots=NormalizeDouble(lots,8); lots=MathMin(lots,max); lots=MathMax(lots,min); return(lots);
}
У данного метода уже два недостатка. Во-первых, результат не соответствует разумным ожиданиям, например, на практике при депозите 700 и риске 0.1 на символе EURUSD у одного брокера получен лот 0.29. А во-вторых, позиция с таким размером в данном случае может привести к потере депозита при убытке примерно сорок пунктов, что никак не может являться риском 10%.
Позиции в рынке
На неттинговых счетах, чтобы узнать о существовании позиции, используется функция PositionSelect(). В функцию передается один параметр – символ. Если функция вернула true, значит, позиция выделена, то есть она существует, false означает, что позиции не существует:
if(PositionSelect(Symbol())){
// что-то делаем с позицией
}
Примеры кода по работе с позициями располагаются в скрипте с именем «014 Position».
После выделения позиции, используя функции PositionGetInteger(), PositionGetDouble(), PositionGetString() можно получить различные сведения о ней. Например, определить направление позиции (покупка или продажа):
if(PositionSelect(Symbol())){
long type=PositionGetInteger(POSITION_TYPE); if(type==POSITION_TYPE_BUY){
// обработка позиции на покупку\
}
else if(type==POSITION_TYPE_SELL){
// обработка позиции на продажу
}
}
На хеджинговых счетах необходимо пройти в цикле по всем позициям, обрабатывая только позиции с нужным символом и идентификатором «мэджик». Общее количество позиций определяется функций PositionsTotal(). Выделение позиции выполняется функцией PositionGetTicket(),в нее передается один параметр – индекс позиции. Возвращает функция тикет позиции или ноль. Если функция PositionGetTicket() возвращает ноль, это означает, что произошла ошибка выделения. Это может произойти от того, что в данный момент на торговом сервере выполняется обработка этой позиции, например, сработал стоплосс или тейкпрофит и т.п. На данную ситуацию можно реагировать двумя способами. Первый способ – не делать ничего, это вариант подходит, например, для функции обработки позиции типа трейлинг стопа, все равно на следующем тике попытка будет повторена. Второй способ – прерывать работу функции, в которой выполнялся вызов функции PositionGetTicket(), этот вариант необходимо использовать, например, при подсчете позиций. При ошибке выделения неизвестно, существует позиция или нет, соответственно невозможен точный подсчет.
Значит, получаем следующий код для работы с позициями на хеджинговом счете:
int Magic=123;
int total=PositionsTotal(); for(int i=0;i<total;i++){
ulong ticket=PositionGetTicket(i); if(ticket!=0){
long magic=PositionGetInteger(POSITION_MAGIC); string symbol=PositionGetString(POSITION_SYMBOL); if(magic==Magic && symbol==Symbol()){
long type=PositionGetInteger(POSITION_TYPE); if(type==POSITION_TYPE_BUY){
// обработка позиции на покупку
}
else if(type==POSITION_TYPE_SELL){
// обработка позиции на продажу
}
}
}
}
С функцией PositionGetInteger() часто используются идентификатор POSITION_TYPE – тип позиции. Бывает два типа позиции: POSITION_TYPE_BUY и POSITION_TYPE_SELL. Также, бывает, используется идентификатор POSITION_TIME – время открытия позиции. Кроме того, иногда может использоваться идентификатор POSITION_TICKET – тикет позиции. Конечно, если позиция была выделена функцией PositionGetTicket(), то тикет, скорее всего, сохранялся в переменную и следует использовать ее. Существуют множество других идентификаторов, но они используются не очень часто, ознакомьтесь с ними по справочному руководству.
Функция PositionGetString() может использоваться для получения комментария позиции, для этого она вызывается с идентификатором POSITION_COMMENT.
С функцией PositionGetDouble() очень часто используется следующие идентификаторы:
POSITION_PRICE_OPEN – цена открытия позиции.
POSITION_PROFIT – прибыль позиции в валюте депозита.
POSITION_SL – цена стоплосса позиции. Если 0, значит, стоплосс не установлен.
POSITION_SWAP – своп (банковское процентное начисление при переносе позиции на следующие сутки).
POSITION_TP – цена тейкпрофита позиции. Если 0, значит, тейкпрофит не установлен.
POSITION_VOLUME – объем позиции (лоты).
Ордера в рынке
Работа с отложенными ордерами отчасти подобна работе с позициями на хеджинговых счетах, только используются функции с другими именами. Для определения общего количества ордеров используется функция OrdersTotal(), для выделения и получения тикета – OrderGetTicket(), для получения различных сведений о позиции: OrderGetInteger(), OrderGetDouble(), OrderGetString().
Примеры кода этого раздела располагаются в файле «015 Orders»:
int Magic=123;
int total=OrdersTotal(); for(int i=0;i<total;i++){
ulong ticket=OrderGetTicket(i); if(ticket!=0){
long magic=OrderGetInteger(ORDER_MAGIC); string symbol=OrderGetString(ORDER_SYMBOL); if(magic==Magic && symbol==Symbol()){
long type=OrderGetInteger(ORDERS_TYPE);
//...
}
}
}
Обратите внимание, в данном примере выполняется проверка идентификатора «мэджик» и символа, однако при работе не неттинговых счетах можно оставить только проверку символа.
При переборе ордеров, находящихся в рынке, функция OrderGetInteger() с идентификатором
ORDER_TYPE может вернуть один из следующих типов:
ORDER_TYPE_BUY_LIMIT – байлимит. ORDER_TYPE_SELL_LIMIT – селллимит. ORDER_TYPE_BUY_STOP – байстоп.
ORDER_TYPE_SELL_STOP – селлстоп. ORDER_TYPE_BUY_STOP_LIMIT – байстоплимит. ORDER_TYPE_SELL_STOP_LIMIT – селлстоплимит.
Кроме проверки типа и идентификатора «мэджик», функция OrderGetInteger() может использоваться с идентификатором ORDER_TIME_SETUP для определения времени установки ордера и с идентификатором ORDER_TICKET. Вообще существует большое количество других идентификаторов для функции OrderGetInteger(), которые используются для решения своих специфических задач, которые будут рассмотрены позже в соответствующих разделах.
Функция OrderGetString() может использоваться для получения комментария ордера, для этого она вызывается с идентификатором POSITION_COMMENT.
С функцией OrderGetDouble() очень часто используется следующие идентификаторы: ORDER_PRICE_OPEN – цена срабатывания ордера.
ORDER_SL – цена стоплосса ордера. Если 0, значит, стоплосс не используется. ORDER_TP – цена тейкпрофита ордера. Если 0, значит, тейкпрофит не используется. ORDER_PRICE_STOPLIMIT – цена лимитного ордера для ордера типа стоплимит.
ORDER_VOLUME_INITIAL – объем ордера.
В некоторых случаях может использоваться идентификатор ORDER_VOLUME_CURRENT для определения оставшегося объема у частично выполненного ордера.
Ордера и сделки в истории
Разберем, как происходит открытие рыночной позиции. Все начинается с ордера. Из клиентского терминала на сервер брокера отправляется заявка – рыночный ордер. Затем, выполняется сделка, соответствующая этому ордеру и появляется рыночная позиция. Таким образом, при открытии рыночной позиции (или при ее изменении на неттинговом счете), в истории терминала появляется ордер и сделка. То же самое происходит и при срабатывании отложенного ордера. Отложенный ордер переносится в историю ордеров, а в истории сделок появляется соответствующая сделка. Историю терминала можно увидеть в окне «Инструменты» во вкладке «История». Чтобы просматривать историю было удобней, в контекстом меню можно выбрать тип истории: сделки, ордера, ордера и сделки.
Для получения программного доступа к истории ордеров и/или сделок, ее необходимо выделить, для этого используется функция HistorySelect(). В функцию передается два параметра: начальное
время и конечное. Лучше всего, всегда выделять всю существующую историю, то есть первым параметром указывать 0, а вторым – текущее время. Возвращает функция true или false в зависимости от успешности ее работы, возвращаемое значение обязательно нужно проверять. Создайте скрипт с именем «016 History», следующий код будет располагаться в нем. Значит, работа с историей ордеров и/или позиций начинается с выделения истории:
if(!HistorySelect(0,TimeCurrent())){ return;
}
Затем, используется цикл. Разберем сначала работу с ордерами. Для получения количеств ордеров в истории используется функция HistoryOrdersTotal(). Поскольку чаще всего нас интересуют недавние ордера, проход в цикле выполняется в обратном порядке:
for(int i=HistoryOrdersTotal()-1;i>=0;i--){
//...
}
Внутри цикла при помощи функции HistoryOrderGetTicket() выполняется получение тикета ордера. При вызове функции ей передается один параметр – индекс ордера в истории. Здесь так же, как при работе с отложенными ордерами, необходимо проверять значение тикета, чтобы оно не было равно нулю. Далее, для получения сведений об ордере, используются функции HistoryOrderGetInteger(), HistoryOrderGetString(), HistoryOrderGetDouble(). Первым параметром в эти функции передается тикет ордера. Второй параметр – идентификатор данных, используются те же самые идентификаторы, что и при работе с отложенными ордерами, да и общий принцип работы с ордерами в истории точно такой же. В итоге получается следующий цикл для работы с ордерами в истории:
for(int i=HistoryOrdersTotal()-1;i>=0;i--){ ulong ticket=HistoryOrderGetTicket(i); if(ticket!=0){
long magic=HistoryOrderGetInteger(ticket,ORDER_MAGIC); string symbol=HistoryOrderGetString(ticket,ORDER_SYMBOL); long type=HistoryOrderGetInteger(ticket,ORDER_TYPE);
//...
}
}
Теперь к типам ордеров добавляется еще два: ORDER_TYPE_BUY и ORDER_TYPE_SELL –
рыночные ордера.
В функции HistoryOrderGetInteger(), HistoryOrderGetString(), HistoryOrderGetDouble() первым параметром передается тикет. Значит, располагая тикетом, можно получать данные отдельной позиции (необходимо только предварительное выделение истории):
ulong ticket=1; Alert(HistoryOrderGetDouble(ticket,ORDER_PRICE_OPEN));
Функция HistoryOrderSelect() дает возможность получить данные только одного ордера без выделения всей истории:
if(HistoryOrderSelect(ticket)){ Alert(HistoryOrderGetDouble(ticket,ORDER_PRICE_OPEN));
Если перед этим выполнялось выделение истории функцией HistorySelect(), оно сбрасывается.
При работе с ордером из истории может потребоваться узнать его состояние. Для этого используется функция HistoryOrderGetInteger() с идентификатором ORDER_STATE. Возможны следующие состояния:
ORDER_STATE_STARTED – брокер начал работу с ордером, проверяется его корректность. ORDER_STATE_PLACED – ордер принят брокером (но это не означает, что ордер исполнен). ORDER_STATE_CANCELED – ордер снят клиентом. Это относится к отложенным ордерам. ORDER_STATE_PARTIAL – ордер исполнен частично.
ORDER_STATE_FILLED – ордер исполнен полностью. ORDER_STATE_REJECTED – ордер отклонен.
ORDER_STATE_EXPIRED – ордер снят по истечении срока его действия. Это относится к отложенным ордерам.
ORDER_STATE_REQUEST_ADD – ордер в состоянии регистрации. ORDER_STATE_REQUEST_MODIFY – ордер в состоянии модификации. ORDER_STATE_REQUEST_CANCEL – ордер в состоянии удаления.
На самом деле, на практике потребность в данном свойстве ордера возникает крайне редко, тем не менее, знать он нём необходимо.
При работе с историей сделок, для определения ее размера используется функция
HistoryDealsTotal():
for(int i=HistoryDealsTotal()-1;i>=0;i--){
//...
}
Функцией HistoryDealGetTicket() выполняется получение тикета сделки. В функцию передается один параметр – индексу сделки. Для получения сведений о сделке используются функции HistoryDealGetInteger(), HistoryDealGetString(), HistoryDealGetDouble(). В эти функции передается по два параметра: тикет и идентификатор. Ниже приведен пример кода по работе с историей сделок:
for(int i=HistoryDealsTotal()-1;i>=0;i--){
ticket=HistoryDealGetTicket(i); if(ticket!=0){
long magic=HistoryDealGetInteger(ticket,DEAL_MAGIC); string symbol=HistoryDealGetString(ticket,DEAL_SYMBOL); long type=HistoryDealGetInteger(ticket,DEAL_TYPE);
//...
}
В функции HistoryDealGetInteger(), HistoryDealGetString(), HistoryDealGetDouble() первым параметром передается тикет, значит, располагая тикетом сделки, можно получить данные о ней ( необходимо только предварительное выделение истории):
ticket=1; Alert(HistoryDealGetDouble(ticket,DEAL_PRICE));
Функция HistoryDelSelect() дает возможность получить данные одной сделки по ее тикету без выделения всей истории:
if(HistoryDealSelect(ticket)){ Alert(HistoryDealGetDouble(ticket,DEAL_PRICE));
}
Так же, как с ордерами, если перед этим выполнялось выделение истории функцией
HistorySelect(), оно сбрасывается.
Наиболее часто используемые идентификаторы функции HistoryDealGetInteger():
DEAL_TYPE – направление сделки: DEAL_TYPE_BUY (покупка) или DEAL_TYPE_SELL (продажа). Кроме того, существует большое количество вариантов, имеющих отношение к финансовым операциям на счете: пополнение счета, снятие средств и т.п.
DEAL_ENTRY – тип сделки:
DEAL_ENTRY_IN – вход в рынок, то есть открытие позиции, DEAL_ENTRY_OUT – выход из рынка, то есть закрытие позиции, DEAL_ENTRY_INOUT – разворот,
DEAL_ENTRY_OUT_BY – закрытие встречной позицией.
DEAL_REASON – причина, по которой выполнена сделка. Вообще существует большое количество причин, с их полным перечнем рекомендуется ознакомиться по справочному руководству. Наиболее часто используются следующие варианты:
DEAL_REASON_SL – сделка является результатом закрытия позиции по стоплоссу.
DEAL_REASON_TP – сделка является результатом закрытия позиции по тейкпрофиту.
DEAL_MAGIC – идентификатор «мэджик».
DEAL_TIME – время появления позиции.
DEAL_ORDER – тикет ордера, на основании которого выполнена сделка.
DEAL_POSITION_ID – тикет позиции, в открытии, закрытии или изменении которой участвовала эта сделка. На хеджинговых счетах иногда бывает нужно найти две сделки – сделку, открывшую позицию, и сделку, закрывшую позиции, найти их можно по равенству данного идентификатора.
Наиболее часто используемые идентификаторы функции HistoryDealGetString(): DEAL_SYMBOL – символ сделки.
DEAL_COMMENT – комментарий сделки. Переходит к сделке от ордера. Наиболее часто используемые идентификаторы функции HistoryDealGetDouble():
DEAL_PRICE – цена сделки. DEAL_VOLUME – объем сделки.
DEAL_PROFIT – прибыль, полученная в результате сделки.
DEAL_COMMISSION – комиссионные. DEAL_SWAP – своп.
Одна из наиболее частых задач при создании эксперта, это определение прибыли последней закрытой позиции. Задач решается путем поиска в истории последней сделки с типом DEAL_ENTRY_OUT – при ее выполнении фиксируется значение прибыли. Если, используя значения DEAL_POSITION_ID этой сделки, продолжить перебор сделок и найти предшествующую сделку типа DEAL_ENTRY_IN с таким же значением DEAL_POSITION_ID, то цена исполнения этой сделки будет соответствовать цене открытия позиции. Разумеется, это касается хеджинговых счетов. На неттинговых счетах цена открытия позиции не столь конкретна, поскольку позиция может открывать в несколько этапов. Таким же образом учитывается комиссия к прибыли закрывающей сделки прибавляется ее комиссия, затем выполняется поиск открывающей сделка и прибавляется ее комиссия.
Глобальные переменные терминала
Если в процессе работы эксперта необходимо сохранить какие-то данные так, чтобы они были доступны после перезапуска эксперта и даже терминала или операционной системы компьютера, используются так называемые глобальные переменные терминала. Они так же могут использоваться и со всеми другими видами программ MQL5, но наиболее удобными и полезными они оказались именно при разработке экспертов. Конечно, для длительного хранения данные можно сохранять в файлы, однако преимущество глобальных переменных терминала в простоте и быстроте их использования.
Создайте скрипт с именем «017 GV», следующий код пишем код в нем. Для создания глобальной переменной (далее в этом разделе будем называть ее просто «переменной») используется функция GlobalVariableSet(). Эта же функция используется и для присвоения переменной нового значения. В функцию передается два параметра. Первый параметр типа string – имя переменой, но не более чем 63 знака. Второй параметр типа double – собственно значение.
GlobalVariableSet("VarName",1.2345);
Для получения значения переменной используется функция GlobalVariableGet(). Существует два варианта функции – с одним параметром и с двумя параметрами. В функцию с одним параметром передается имя переменой, значение возвращается обычным образом:
double a=GlobalVariableGet("VarName"); Alert("a=",a);
Результат: «a=0.0».
Более правильно такой способ возврата значения из функции называется возвратом по значению или прямым возвратом.
В функцию с двумя параметрами передается имя переменной и обычная переменная по ссылке для возвращаемого значения. Сама функция возвращает true/false в зависимости от успешности ее работы:
double b; if(GlobalVariableGet("VarName",b)){
Alert("b=",b);
}
Результат: «b=0.0».
Если глобальной переменной не существует, то первый вариант функции GlobalVariableGet() вернет значение 0. Второй вариант функции установит переменой передаваемой вторым параметром значение 0, но сама функция вернет false:
double c=GlobalVariableGet("VarName2"); double d=25;
bool r=GlobalVariableGet("VarName2",d); Alert("c=",c,", d=",d,", r=",r);
Результат: «c=0.0, d=0.0, r=false».
Если необходимо проверить существование переменной, используется функция GlobalVariableCheck(), в функцию передается один параметр – имя переменной, возвращает функция true, если переменная существует или false, при ее отсутствии:
bool r1=GlobalVariableCheck("VarName"); bool r2=GlobalVariableCheck("VarName2"); Alert("r1=",r1,", r2=",r2);
Результат: «r1=true, r2=false».
Для удаления переменной используется функция GlobalVariableDel(), в функцию передается один параметр – имя переменой:
GlobalVariableDel("VarName");
Поскольку глобальные переменные терминала сохраняются и после закрытия терминала, очевидно, что для их сохранения используется файл. Запись данных в этот файл происходит при закрытии терминала. Для повышения надежности работы данные можно сохранять принудительно, для этого используется функция GlobalVariablesFlush().
Если какие-то глобальные переменные терминала не используются, они автоматически удаляются через четыре недели после последнего к ним обращения. Под обращением к переменной подразумевается использование функций GlobalVariableSet() и GlobalVariableGet(). Значить, при необходимости более длительного хранения данных, к переменным необходимо периодически обращаться.
В терминале существует специальное окно для просмотра глобальных переменных. Для его открытия выполните в терминале команду главного меню: Сервис – Глобальные переменные. В этом окне располагается таблица с трема колонками: для имени переменной, значения и времени последнего обращения (рис. 55).
Рис. 55. Окно просмотра глобальных переменных терминала
Время последнего обращения к переменной можно узнать при помощи функции
GlobalVariableTime(), в функцию передается один параметр – имя переменной:
datetime tm=GlobalVariableTime("VarName"); Alert("tm=",tm);
Все глобальные переменные терминала хранятся вместе, то есть они не привязаны к какому-то конкретному экземпляру эксперта, работающему в терминале. Для того чтобы каждый экземпляр эксперта работал только со своими глобальными переменными, следует добавлять именам переменных уникальный префикс. В первую очередь эксперты отличаются друг от друга именем, затем символом графика, на котором они работают. Одинаковые эксперты, работающие на одном и том же символе, отличаются друг от друга идентификатором «мэджик». Значит, надо сформировать префикс глобальной переменной из имени эксперта, символа и идентификатора «мэджик». На глобальном уровне скрипта объявим две переменные:
int Magic=123; string prefix;
Переменная Magic – для идентификатора, prefix – для префикса имен. Формируем префикс:
prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+Symbol()+"_"+(string)Magic+"_";
Теперь, вызывая какую-то функцию, работающую с глобальными переменными терминала, надо добавлять этот префикс перед именем переменой. Намного удобней будет пользоваться собственными функциями с короткими именами. Вместо функции GlobalVariableSet():
void GVSet(string name,double value){ GlobalVariableSet(prefix+name,value);
}
Вместо функции GlobalVariableGet():
double GVGet(string name){ return(GlobalVariableGet(prefix+name));
}
Вместо функции GlobalVariableCheck():
double GVCheck(string name){ return(GlobalVariableCheck(prefix+name));
}
После этого работа с глобальными переменными терминала значительно упрощается:
GVSet("VarName3",6.789);
double e=GVGet("VarName3"); Alert("e=",e);
Результат: «e=6.789».
Для удаления группы глобальных переменных с одинаковым префиксом используется функция
GlobalVariablesDeleteAll():
GlobalVariablesDeleteAll(prefix);
Если необходимо обработать глобальные переменные терминала, выбирая их более сложным образом, используется цикл. Общее количество переменных можно получить переменной GlobalVariablesTotal(). Для получения имени переменой по ее индексу используется функция GlobalVariableName(). Следующий код является аналогом функции GlobalVariablesDeleteAll():
int total=GlobalVariablesTotal(); for(int i=total-1;i>=0;i--){
string name=GlobalVariableName(i); if(StringFind(name,prefix)==0){
GlobalVariableDel(name);
}
}
Обратите внимание, проход в цикле выполняется в обратном направлении – от больших индексов к нулю. Если бы цикл выполнялся в прямом направлении, то была бы удалена только половина переменных. После удаления переменой с индексом 0, индексация всех переменных меняется, то есть появляется другая переменная с индексом 0, а счетчик i уже имеет значение 1.
Существуют и другие функции по работе с глобальными переменными, но потребность в них практически не возникает. Если интересно, прочитайте о них в справочном руководстве.
Работа со временем
Как вам уже должно быть известно, для получения текущего серверного времени (времени котировок) используется функция TimeCurrent(). Для получения локального времени (времени установленного в операционной системе клиентского терминала) используется функция TimeLocal(). Эти функции возвращают целое число – количество секунд, прошедших с первого января 1970 года. Таким образом, время включает в себя и дату. В целочисленном формате время удобно для проведения над ним математических вычислений. Для преобразования времени в формат, удобный для восприятия человеком (в строковый формат) используется функция TimeToString().
Иногда бывает нужно извлечь отдельные компоненты даты и времени: год, месяц, число месяца, час, минуты и секунды. Также бывает нужно узнать день недели. Для этого используется функция TimeToStruct(), в функцию передается два параметра: время и структура MqlDateTime. Второй параметр (структура) передается по ссылке и заполняется компонентами даты. Структура MqlDateTime имеет следующие поля:
- year – год,
- mon – месяц,
- day – день,
- hour – час,
- min – минуты,
- sec – секунды,
- day_of_week – день недели (воскресенье – 0, понедельник – 1 и т.д.),
- day_of_year – порядковый номер в году (отсчет от нуля, 0 – это 1-е января).
Следующий код располагается в скрипте с именем «018 Time». Разложение времени на компоненты:
datetime tm=TimeCurrent(); MqlDateTime ts; TimeToStruct(tm,ts);
Alert(TimeToString(tm,TIME_DATE)," ",TimeToString(tm,TIME_SECONDS),"\n", "year=",ts.year,", mon=",ts.mon,", day=",ts.day,"\n"
"hour=",ts.hour,", min=",ts.min,", sec=",ts.sec,"\n", "day_of_week=",ts.day_of_week,", ","day_of_year=",ts.day_of_year
);
Результат: «2020.07.30 20:34:16
year=2020, mon=7, day=30 hour=20, min=34, sec=16 day_of_week=4, day_of_year=211».
Для обратного преобразования структуры во время используется функция StructToTime(). Перед этим преобразованием значения полей структуры MqlDateTime можно изменить. Определим время на начало текущего месяца. Для этого поля hour, min, sec надо обнулить, а полю day присвоить значение 1:
ts.hour=0; ts.min=0; ts.sec=0; ts.day=1;
datetime tm2=StructToTime(ts); Alert("tm2=",tm2," (",(long)tm2,")");
Результат: «tm2=2020.07.01 00:00:00 (1593561600)».
С полями day_of_week и day_of_year данный прием не сработает.
При разработке экспертов наиболее частой задачей является ограничение торговли по времени суток, то есть торговля в заданную временную сессию. Допустим, время начала сессии задается двумя переменными, в одной переменной задан час времени, в другой минуты, также и конец сессии задан двумя переменными:
int StartHour=9; int StartMinute=0; int EndHour=21; int EndMinute=0;
Рассчитаем это время в секундах от начала суток, для этого часы умножим на количество секунд в одном часе, а минуты на количество секунд в одной минуте:
int StartTime=3600*StartHour+60*StartMinute; int EndTime=3600*EndHour+60*EndMinute;
Переведем текущее время так же в количество секунд от начала суток, для этого получим остаток от деления времени на количество секунд в сутках:
int CurTime=(int)(TimeCurrent()%86400);
Остается сравнить время, текущее время должно попадать в промежуток от времени начала до времени окончания сессии. Необходимо учесть, что время начала может быть позже времени конца, например, если сессия начинается в 23:00, а заканчивается в 01:00, поэтому должно быть две проверки. Проверка для случая, когда время окончания больше времени начала:
CurTime>=StartTime && CurTime<EndTime
Для случая, когда время окончания меньше времени начала:
CurTime>=StartTime || CurTime<EndTime
Таким образом, получаем следующий код:
bool CanTrade; if(StartTime<EndTime){
CanTrade=(CurTime>=StartTime && CurTime<EndTime);
}
else{
CanTrade=(CurTime>=StartTime || CurTime<EndTime);
}
Alert("CanTrade=",CanTrade);
Вторая частая задача – разрешить торговлю только в определенные дни недели. В этом случае используется семь переменных типа bool разрешающих/запрещающих торговлю для каждого дня недели (true – торговля разрешена, false – запрещена):
bool Sunday=true; bool Monday=true; bool Tuesday=true; bool Wednesday=true; bool Thursday=true; bool Friday=true; bool Saturday=true;
Объявим массив из семи элементов и присвоим его элементам значения этих переменных (если бы этот код работал в эксперте, то это следовало бы сделать один раз при его запуске):
bool wd[7]; wd[0]=Sunday; wd[1]=Monday; wd[2]=Tuesday; wd[3]=Wednesday; wd[4]=Thursday; wd[5]=Friday; wd[6]=Saturday;
Теперь из текущего времени извлекаем день недели и проверяем значение соответствующего элемента массива:
TimeToStruct(TimeCurrent(),ts); bool CanTrade2=wd[ts.day_of_week]; Alert("CanTrade2=",CanTrade2);
Графические объекты
Графические объекты предназначены для выполнения графических построений на графике цены. Это различные линии, каналы, надписи, кнопка, поле текстового ввода и т.п. С полным перечнем графических объектов можно ознакомиться через главное меню терминала: Вставка – Объекты. Некоторые графические объекты доступны через панель инструментов терминала, эта панель так и называется «Графические инструменты». Если у вас она не видима, ее можно включить командой главного меню: Вид – Панели инструментов – Графические инструменты.
Несмотря на огромное количество графических объектов, работа с ними выполняется по идентичному принципу. Создание объекта выполняется функцией ObjectCreate(), в функцию передается шесть обязательных параметров:
long chart_id – идентификатор графика. Данный параметр позволяет создавать графические объекты на любом графике, открытом в терминале. Пока будем создавать объекты только на том графике, на котором выполняется запуск скрипта, для этого достаточно передать значение 0.
string name – имя объекта. Каждый объект, располагающийся на одном графике должен иметь уникальное имя.
ENUM_OBJECT type – тип объекта. Изменение одного этого параметра позволяет создавать любой из всех возможных графических объектов. С полным перечнем идентификаторов можно познакомиться в справке в описании функции ObjectCreate().
sub_window – индекс подокна. На графике могут находиться индикаторы, отображаемые в подокне, например, Стохасктик, RSI и т.п. Возможно создание объектов как непосредственно на графике цены, так и в этих подокнах. График цены имеет индекс 0, верхнее подокно имеет индекс 1 и т.д. Все подокна относятся к одному графику, то есть имена объектов разных подокон одного графика не могут быть одинаковыми.
datetime time1, double price1 – время и цена точки привязки объекта а графику. Графический объект обычно создается на графике цены, соответственно координаты его расположения определяются временем и ценой. Однако существуют объекты, привязываемые не к цене и времени, а к пикселям графика, то есть их расположение не зависит от прокрутки графика. Кроме того, объекты, создаваемые в подокне, привязываются не к цене. Обычно, при создании объекта, для этих параметров указываются значения 0. Позиционирование же объекта выполним позже другим способом.
Рассмотрим работу с основными графическими объектами.
Трендовая линия
Создайте скрипт с именем «019 GObjects», следующий код будет располагаться в нем. Разберем работу с трендовой линией. Объявляем переменную для имени графического объекта:
string name="line_1";
Создаем объект:
ObjectCreate(0,name,OBJ_TREND,0,0,0);
Осталось переместить объект на нужное место, для этого используется функция ObjectMove(), в функцию передается следующие параметры: идентификатор графика, имя объекта, индекс точки привязки и координаты. Поскольку у трендовой линии две точки привязки, функцию ObjectMove() надо вызвать два раза. Но сначала надо определиться со значениями координат, установим линии на минимум 10-го бара и максимум 1-го, получаем необходимые данные:
MqlRates p1[1],p2[1]; if(CopyRates(Symbol(),Period(),10,1,p1)==-1){
return;
}
if(CopyRates(Symbol(),Period(),1,1,p2)==-1){ return;
}
Перемещаем линию:
ObjectMove(0,name,0,p1[0].time,p1[0].low);
ObjectMove(0,name,1,p2[0].time,p2[0].high);
После того как все свойства графического объекта установлены, необходимо ускорить его отрисовку, это делается функцией ChartRedraw(), в функцию передается один необязательный параметр – идентификатор графика, по умолчанию значение параметра равно нулю, так что его можно не передавать:
ChartRedraw();
Теперь можно выполнить запуск скрипта. После этого на графике должна появиться линия, но пока она не очень похожа на настоящую трендовую линию, а представляет собой отрезок, ограниченный точками привязки (рис. 56), к тому же ее невозможно выделить и переместить вручную.
Рис. 56. Трендовая линия
Для управления различными свойствами графического объекта используется три функции: ObjectSetInteger(), ObjectSetString() и ObjectGetDouble(). Существует по два варианты каждой из этих функций. Первый вариант с четырьмя параметрами: идентификатор графика, имя объекта, идентификатор свойства и значение свойства. Второй вариант имеет пять параметров. В этом случае после идентификатора свойства указывается дополнительный идентификатор
(модификатор), а после него значение свойства. Однако свойств, требующих указания дополнительного модификатора, не очень много.
Изменим цвет линии: ObjectSetInteger(0,name,OBJPROP_COLOR,clrBlue); Толщину: ObjectSetInteger(0,name,OBJPROP_WIDTH,2); Стиль:
ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_DASH);
Всего доступны следующие стили:
- STYLE_SOLID – сплошная,
- STYLE_DASH – из длинных штрихов,
- STYLE_DOT – из коротких штрихов,
- STYLE_DASHDOT – из длинных и коротких штрихов,
- STYLE_DASHDOTDOT – длинный штрих через два коротких.
Все стили доступны только для линий с толщиной 1. Для линий с толщиной 2 и больше доступны только стили STYLE_DASH и STYLE_DASHDOTDOT.
Для продления линии за точку привязки используется идентификаторы OBJPROP_RAY_RIGHT и OBJPROP_RAY_LEFT. Продлим линию вправо, а слева оставим ее ограниченной до точки привязки:
ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,true); ObjectSetInteger(0,name,OBJPROP_RAY_LEFT,false);
Имейте в виду, под направлением лево/право здесь подразумевается не фактическое направление, а направление в сторону первой точки привязки и в сторону второй токи привязки соответственно.
Сделаем так, чтобы линией можно было управлять на графике вручную (выделять, перемещать и пр.):
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,true); Можно сделать так, чтобы линия сразу была выделенной: ObjectSetInteger(0,name,OBJPROP_SELECTED,true);
Со всеми графическими объектами имеется возможность сделать так, чтобы они были видны только на каких-то отдельных таймфреймах. Ограничим видимость линии таймфреймами H1 и H4:
ObjectSetInteger(0,name,OBJPROP_TIMEFRAMES,OBJ_PERIOD_H1|OBJ_PERIOD_H4);
Если теперь запустить скрипт, чтобы свойства линии обновились, и переключать таймфрейм, то можно увидеть, что линия видна только на таймфреймах H1 и H4. Обратите внимания, что для указания флагов видимости используются специальные идентификаторы, начинающиеся с префикса OBJ: OBJ_PERIOD_H1, OBJ_PERIOD_H4, а не идентификаторы общего назначения PERIOD_H1, PERIOD_H4. Чтобы объект бы виден на всех таймфреймах, используется флаг OBJ_ALL_PERIODS.
Применим функцию ObjectSetString() – добавим линии всплывающую подсказку:
ObjectSetString(0,name,OBJPROP_TOOLTIP,"Подсказка...");
Рис. 57. Выделенная трендовая линия с лучом вправо, толщиной 2, стилем STYLE_DASH и всплывающей подсказкой
Если теперь запустить скрипт и подвести указатель мыши к линии, должна открыться подсказка (рис. 57).
Если открыть окно свойств трендовой линии (щелкнуть правой кнопкой на линии и выбрать команду «свойства line_1»), в нем можно увидеть поле «Описание» (рис. 58).
Изменим поле «Описание»:
Рис. 58. Окно свойств трендовой линии
ObjectSetString(0,name,OBJPROP_TEXT,"Описание...");
Рассмотрим функцию ObjectSetDouble(). Что касается трендовой линии, то при работе с ней, можно обойтись без этой функции. Если нужно переместить линию – использовать функцию ObjectMove(). Больше у трендовой линии нет никаких свойств с типом double. Тем не менее, для указания координат точек привязки можно использоваться функцию ObjectSetDouble(), особенно если необходимо изменить только одну координату из пары. В учебных целях изменим обе координаты первой точки привязки, переместим ее на минимум 15-го бара. Получаем данные 15- го бара:
if(CopyRates(Symbol(),Period(),15,1,p1)==-1){ return;
}
Устанавливаем время и цену точки. Это тот случай, когда используются варианты функций с дополнительным модификатором свойства:
ObjectSetInteger(0,name,OBJPROP_TIME,0,p1[0].time); ObjectSetDouble(0,name,OBJPROP_PRICE,0,p1[0].low);
Обратите внимание, четвертый параметр равен 0, если бы было нужно изменить координаты второй точки, надо было бы указать 1.
Горизонтальная и вертикальная линии
Разобравшись с трендовыми линиям вы теперь сможете легко разобраться с горизонтальными и вертикальными линиями, с ними все точно также, только они имеют по одной точке привязки с одной координатой (время – для вертикальной лини и цена – для горизонтальной). Как раз для этих объектов подходит только что рассмотренный вариант установки координат. Тем не менее, и с ними можно использовать функцию ObjectMove(), просто вместо неиспользуемой координаты указывается 0. Горизонтальная линия:
name="line_2"; ObjectCreate(0,name,OBJ_HLINE,0,0,0); ObjectMove(0,name,0,0,p1[0].low);
Вертикальная линия:
name="line_3"; ObjectCreate(0,name,OBJ_VLINE,0,0,0); ObjectMove(0,name,0,p1[0].time,0);
Рис. 59. Вертикальная линия на переднем плане (слева) и на заднем (справа).
Все линии, так же и многие другие графические объекты могут быть нарисованы как поверх баров (на переднем плане), так и под ними (на заднем плане). Особенно заметна разница в таком рисовании у вертикальных линий. Только что созданную вертикальную линию расположим на переднем плане:
ObjectSetInteger(0,name,OBJPROP_BACK,false);
Создадим еще одну вертикальную линию и расположим ее на заднем плане:
name="line_4"; ObjectCreate(0,name,OBJ_VLINE,0,0,0);
ObjectMove(0,name,0,p2[0].time,0); ObjectSetInteger(0,name,OBJPROP_BACK,true);
Линия, нарисованная на заднем плане, удобна тем, что не закрывает бары, но у нее отсутствует надпись на шкале времени (рис. 59), к тому же линия может оказаться под линией сетки и стать пунктирной. Тоже самое касается надписи с ценой для горизонтальных линий.
Следующий объект по частоте использования – текст. Еще существует текстовая метка, их отличие в том, что текст привязывается к цене и времени, а текстовая метка к пикселям графика, она тоже будет рассмотрена.
Текст
Создайте скрипт с именем «020 Text», следующий код располагается в нем. Сначала скопируем ценовые данные, чтобы использовать их как координаты, скопируем сто баров:
MqlRates r[100]; if(CopyRates(Symbol(),Period(),0,100,r)==-1){
return;
}
Создаем объект:
string name="text_1"; ObjectCreate(0,name,OBJ_TEXT,0,0,0); ObjectMove(0,name,0,r[0].time,r[0].low);
Выводим через него свой текст:
ObjectSetString(0,name,OBJPROP_TEXT,"Мой текст...");
Меняем параметры отображения (цвет, размер шрифта и тип шрифта):
ObjectSetInteger(0,name,OBJPROP_COLOR,clrBlue); ObjectSetInteger(0,name,OBJPROP_FONTSIZE,12); ObjectSetString(0,name,OBJPROP_FONT,"Verdana");
Разрешим выделение и выделим объект:
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,true); ObjectSetInteger(0,name,OBJPROP_SELECTED,true);
Запускаем скрипт и видим точку привязки, расположенную в левом верхнем углу надписи (рис. 60).
Рис. 60. Точка привязки объекта OBJ_TEXT
Точкой привязки можно управлять, переместим ее в центр верхнего края:
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_UPPER);
Создадим еще один объект с точкой привязки в центре нижнего края и расположим его на максимально цене этого же бара (рис. 61):
name="text_2"; ObjectCreate(0,name,OBJ_TEXT,0,0,0); ObjectMove(0,name,0,r[0].time,r[0].high);
ObjectSetString(0,name,OBJPROP_TEXT,"Мой текст 2"); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LOWER);
Вообще доступны следующие точки привязки:
- ANCHOR_LEFT_UPPER – левый верхний угол,
- ANCHOR_LEFT – центр левого края,
- ANCHOR_LEFT_LOWER – левый нижний угол,
- ANCHOR_LOWER – центр нижнего края,
- ANCHOR_RIGHT_LOWER – правый нижний угол,
- ANCHOR_RIGHT – центр правого края,
- ANCHOR_RIGHT_UPPER – правый верхний угол,
- ANCHOR_UPPER – центр верхнего края,
- ANCHOR_CENTER – центр объекта.
Еще есть точки привязки ANCHOR_TOP и ANCHOR_BOTTOM, но они используются только с графическими объектом типа OBJ_ARROW (стрелка), не путайте их с ANCHOR_LOWER и ANCHOR_UPPER.
Текст можно вращать. Создадим еще один объект и расположим его на минимальной цене бара:
name="text_3"; ObjectCreate(0,name,OBJ_TEXT,0,0,0); ObjectMove(0,name,0,r[10].time,r[10].low);
ObjectSetString(0,name,OBJPROP_TEXT,»Мой текст 3″); Повернем его на 90 градусов по часовой стрелке: ObjectSetDouble(0,name,OBJPROP_ANGLE,-90);
Что бы надпись располагалась под баром надо использовать точки привязки, расположенные по левому краю объекта, используем точку привязки в центре левого края:
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT);
Текст располагается ровно под баром (рис. 61), это может быть удобным при создании стрелочных индикаторов, для вывода комментария о сигнале.
Чтобы текст не касался бара, его следует начать с пробела.
Текстовый объект можно использовать для рисования значков, стрелок и т.п. Для этого ему надо указать шрифт «Wingdings» и преобразовать код значка с текст. С набором значков шрифта «Wingdings» можно ознакомиться в справочном руководстве (Константы, перечисления и структуры – Константы объектов – Wingdings), там же можно узнать код необходимого значка. Создаем текстовый объект, с привязкой по центру верхнего края к минимальной цене:
name="text_4"; ObjectCreate(0,name,OBJ_TEXT,0,0,0); ObjectMove(0,name,0,r[20].time,r[20].low);
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_UPPER); Устанавливаем шрифт: ObjectSetString(0,name,OBJPROP_FONT,"Wingdings");
Нарисуем стрелку вверх, код этого символа 233. Чтобы преобразовать код в символ используется функция CharToString():
string ar=CharToString(233);
Выводим полученный текст (рис. 62):
ObjectSetString(0,name,OBJPROP_TEXT,ar);
Рис. 61. Текст 3 повернут на -90 градусов
Рис. 62. Текстовым объектом можно «рисовать» стрелки и другие значки
Надпись
Еще один часто используемый графический объект – текстовая метка, или надпись. Он ничем не отличается от только что рассмотренного текстового объекта, кроме того, что его координаты указываются в пикселях, то есть он привязывается к координатам экрана. В связи с этим текстовой метке можно устанавливать еще одно свойство – угол графика, от которого идет отсчет координат. Угол графика устанавливается функцией ObjectSetInteger() со следующими идентификаторами:
- CORNER_LEFT_UPPER – левый верхний,
- CORNER_RIGHT_UPPER – правый верхний,
- CORNER_LEFT_LOWER – левый нижний,
- CORNER_RIGHT_LOWER – правый нижний.
С текстовой меткой не работает функция ObjectMove(), для ее перемещения используется функция ObjectSetInteger() с идентификаторами OBJPROP_XDISTANCE для установки горизонтальной координаты и OBJPROP_YDISTANCE для установки вертикальной координаты.
Создадим две надписи и расположим их по двум верхним углам графика:
name="text_5"; ObjectCreate(0,name,OBJ_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,20); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,20);
ObjectSetString(0,name,OBJPROP_TEXT,"левый верхний угол"); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
name="text_6"; ObjectCreate(0,name,OBJ_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,20); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,20);
ObjectSetString(0,name,OBJPROP_TEXT,"правый верхний угол"); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER); ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_UPPER);
Линии Фибоначчи
У вас уже давно мог возникнуть вопрос о том, как самостоятельно узнать, какие свойства можно менять у того или иного графического объекта. К сожалению, в справочном руководстве нет таких данных. Для этого надо создать требуемый графический объект вручную и изучить его окно свойств. Попробуем разобраться с рисованием линий Фибоначчи. Для создания этого объекта выполните команду главного меню: Вставка – Объекты – Фибоначчи – Линии Фибоначчи, затем установите указатель мыши в какое-нибудь место на графике, нажмите левую кнопку, перетащите мышь и отпустите кнопку, на графике появится наклонная опорная линия и горизонтальные уровни (рис. 63).
Рис. 63. Графический объект «Линии Фибоначчи»
После создания объекта, перемещая опорные точки опорной линии, можно установить его более точно.
Для открытия окна свойств наведите указатель мыши на опорную линию и щелкните левой кнопкой, линия должна выделиться (появиться опорные точки), щелкните правой кнопкой и в контекстном меню выберите команду «свойства» (самая верхняя строка контекстного меню). В окне свойств линий Фибоначчи четыре вкладки: «Общие», «Уровни», «Параметры», «Отображение» (рис. 64).
Рис. 64. Окно свойств графического объекта «Линии Фибоначчи»
Рассмотрим вкладки по порядку. Вкладка «Общие»:
Поле «Имя» – это то имя, которое задается объекту при его создании функцией ObjectCreate(). Изменить имя объекта невозможно. Если возникнет такая необходимость, надо сохранить все свойства объекта во временные переменные, удалить объект, создать новый с другим именем и установить ему сохраненные свойства. Для удаления объекта используется функция ObjectDelete().
Поле «Описание» – для изменения этого поля используется функция ObjectSetString() с идентификатором OBJPROP_TEXT.
Набор элементов управления «Стиль» предназначен для управления отображением опорной линии — установки ее цвета, стиля и толщины. Они соответствуют функции ObjectSetInteger() с идентификаторами OBJPROP_COLOR, OBJPROP_WIDTH и OBJPROP_STYLE.
Чекбокс «Рисовать объект как фон» соответствует функции ObjectSetInteger() с идентификатором OBJPROP_BACK.
Чекбокс «Отключить выделение» – соответствует функции ObjectSetInteger() с идентификатором OBJPROP_SELECTABLE. Если у объекта отключена возможность выделения, добраться до его окна свойств можно через список графических объектов. Для этого надо щелкнуть правой кнопкой мыши на графике и выбрать команду «Список объектов», затем, в открывшемся окне, нажать кнопку «Показать все», найти в списке строку с именем требуемого объекта и дважды щелкнуть на ней, это откроет окно свойств объекта.
Вкладка «Уровни» (рис. 65). Эта вкладка предназначена для настройки горизонтальных уровней. О том, как можно управлять этими уровнями программно будет чуть позже.
Рис. 65. Окно свойств графического объекта «Линии Фибоначчи», вкладка «Уровни»
Вкладка «Параметры» (рис. 66).
Эта вкладка предназначена для управления положением опорной линией. По сути, опорная линия является трендовой линией, соответственно, ей модно устанавливать все свойства точно так же, как и трендовой линии. Два набора полей «Дата» и «Значение» соответствуют координатам двух точек привязки линии. Их положение можно менять функцией ObjectMove() или функциями ObjectSetInteger() с идентификатором OBJPROP_TIME (время) и ObjectSetDouble() с идентификатором OBJPROP_PRICE (значение). Чекбоксы «Луч вправо» и «Луч влево» соответствуют функции ObjectSetInteger() с идентификаторами OBJPROP_RAY_RIGHT и OBJPROP_RAY_LEFT.
Вкладка «Отображение» совершенно идентична у всех графических объектов. Через эту вкладку выполняется управление видимостью объекта на различных таймфреймах. Это идентично функции ObjectSetInteger() с идентификатором OBJPROP_TIMEFRAMES.
Рис. 66. Окно свойств графического объекта «Линии Фибоначчи», вкладка «Параметры»
Разберем программное создание объекта «Линии Фибонначи». Создайте скрипт с именем «021 Fibo», следующий код располагается в нем. Сначала скопируем данные двух баров, которые используем как координаты точек привязки опорной линии:
MqlRates p1[1],p2[1]; if(CopyRates(Symbol(),Period(),10,1,p1)==-1){
return;
}
if(CopyRates(Symbol(),Period(),1,1,p2)==-1){ return;
}
Создаем объект:
string name="fibo_1"; ObjectCreate(0,name,OBJ_FIBO,0,0,0);
Перемещаем его на требуемую позицию:
ObjectMove(0,name,0,p1[0].time,p1[0].high);
ObjectMove(0,name,1,p2[0].time,p2[0].low);
Теперь займемся настройкой уровней. Значения уровней зададим через массивы, таким образом, добавляя или удалее элементы массива, можно будет менять количество уровней:
double lev[]={0,0.5,1,1.5};
В отличие от окна свойств, программно можно менять цвет каждому отдельному уровню, так что добавим массив для цветов:
color col[]={clrBlue,clrMaroon,clrBlue,clrGreen};
Установим количество уровней (оно соответствует размеру массива):
int cnt=ArraySize(lev); ObjectSetInteger(0,name,OBJPROP_LEVELS,cnt);
Рис. 67. Графический объект «Линии Фибоначчи» после изменения уровней
Теперь в цикле по всем элементам массива уровней установим значения и цвет:
for(int i=0;i<cnt;i++){ ObjectSetDouble(0,name,OBJPROP_LEVELVALUE,i,lev[i]);
ObjectSetInteger(0,name,OBJPROP_LEVELCOLOR,i,col[i]); ObjectSetString(0,name,OBJPROP_LEVELTEXT,i,lev[i]);
}
Функцией ObjectSetDouble() с идентификатором OBJPROP_LEVELVALUE устанавливается значение уровня. Функцией ObjectSetInteger() с идентификатором OBJPROP_LEVELCOLOR устанавливается цвет уровня. Функцией ObjectSetString() с идентификатором OBJPROP_LEVELTEXT устанавливается надпись возле уровня (рис. 67)
Кроме того, можно установить толщину линий и стиль, для этого используется функция
ObjectSetInteger() с идентификаторами OBJPROP_LEVELWIDTH и OBJPROP_LEVELSTYLE.
Получение свойств графических объектов
Пока что рассматривалась только установка свойств графических объектов. Все свойства, которые можно установить, так же можно и получить от объекта. Для этого используется функции ObjectGetInteger(),ObjectGetString() и ObjectGetDouble(). Существует по два варианта каждой функции. Первый вариант имеет четыре параметра: идентификатор графика, имя объекта, идентификатор свойства и необязательный модификатор свойства.
В этом варианте запрашиваемое значение возвращается функцией. Второй вариант имеет еще один – пятый параметр, через который по ссылке возвращается значение (четвертый параметр становится обязательным, если запрашиваемое свойство не требует модификатора, следует передать 0). В этом варианте функция возвращает true/false в зависимости от успешности ее выполнения. Кроме этого существует еще две функции: ObjectGetValueByTime() – получение значения по времени и ObjectGetTimeByValue() – получение времени по значению. Эти функции применимы только к объектам OBJ_TREND, OBJ_TRENDBYANGLE, OBJ_GANNLINE, OBJ_CHANNEL, OBJ_REGRESSION, OBJ_STDDEVCHANNEL, OBJ_ARROWED_LINE.
Все графические объекты имеют одну особенность, они работают только на обычном графике и на графике визуального тестирования. Причем, на графике визуального тестирования их можно создавать программно и программно считывать их значения, но невозможно перемещать вручную, открывать окно свойств и т.п.
Допустим, эксперт работает на пересечении трендовой линии, он рисует ее (создает графический объект) и ждет ее пересечения с ценой. Такой эксперт будет невозможно оптимизировать в тестере. Данная особенность крайне ограничивает применение графических объектов. Следовательно, если торговая система основана на графическом анализе с применением графических объектов, необходимо подготовить собственные программные реализации этих графических объектов (только расчет без отображения), а сами же графические объекты использовать только для отображения.
Допустим, используется трендовая линия, и надо получить ее значение на каком-то баре – для этого как раз подходит функция ObjectGetValueByTime(), но вместо ее применения рекомендуется самостоятельно вычислить значение линии исходя из координат ее точек привязки и координаты требуемого бара, причем для вычислений следует использовать не время, а индекс бара.
Единственный графический объект, полноценно работающий на графике тестера при визуальном тестировании, это кнопка (OBJ_BUTTON). Значит, следует иметь в виду, что основное назначение графических объектов – исключительно информационное. Если же необходимо обеспечить интерактивность – взаимодействие пользователя и эксперта посредством графических объектов, то надо помнить, что тестировать такую систему в тестере не получится. Если необходимо обеспечить интерактивность и в тестере, то единственный объект, который в нашем распоряжении – это кнопка. Однако развитие терминала MetaTrader5, не стоит на месте, поэтому не исключено, что когда-нибудь ситуация с графическими объектами изменится.
Кнопка
Код с примером использования кнопки располагается в скрипте с именем «022 Button».
Создание:
string name="button_1"; ObjectCreate(0,name,OBJ_BUTTON,0,0,0);
Установка позиции:
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,50); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,50);
Установка размеров:
ObjectSetInteger(0,name,OBJPROP_XSIZE,100); ObjectSetInteger(0,name,OBJPROP_YSIZE,30);
Установка цвета фона и цвета текста:
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrSilver); ObjectSetInteger(0,name,OBJPROP_COLOR,C'60,60,60');
Текст надписи:
ObjectSetString(0,name,OBJPROP_TEXT,"Нажми");
Если теперь запустить скрипт, на графике появится кнопка, если на нее нажать, она останется в нажатом положении, если еще раз нажать, она окажется в отжатом состоянии. Узнать состояние кнопки можно функцией ObjectGetInteger() с идентификатором OBJPROP_STATE. Сделаем так, чтобы кнопка сама отжималась после нажатия. После создания объекта ускорим его отрисовку:
ChartRedraw();
Теперь в цикле будем следить за состоянием кнопки, если она будет нажата, откроем окно с сообщением, сделаем короткую паузу и вернем кнопке отжатое состояние:
while(!IsStopped()){ if(ObjectGetInteger(0,name,OBJPROP_STATE)){
Alert("Нажата кнопка"); Sleep(200);
ObjectSetInteger(0,name,OBJPROP_STATE,false); ChartRedraw();
}
Sleep(1);
}
После выхода из цикла, который произойдет при отсоединении скрипта от графика, удалим кнопку:
ObjectDelete(0,name);
ChartRedraw();
Кнопка имеет одну особенность, для нее можно менять угол отсчета координат (функция ObjectSetInteger() с идентификатором OBJPROP_CORNER), но невозможно менять точку привязки (функция ObjectSetInteger() с идентификатором OBJPROP_ANCHOR). Точкой привязки кнопки всегда является верхний левый угол.
График
Среди обилия графических объектов, есть один очень необычный – график, это почти копия обычного графика из терминала. Он интересен тем, что позволят создавать очень удобные информационные панели с несколькими дополнительными графиками уменьшенного размера на основном графике.
Код с примером использования графика располагается в скрипте с именем «023 GO Chart».
Создание, установка координат и размеров:
ObjectCreate(0,name,OBJ_CHART,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,50); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,50); ObjectSetInteger(0,name,OBJPROP_XSIZE,300); ObjectSetInteger(0,name,OBJPROP_YSIZE,300);
Указываем данные, которые график должен отображать (тот же символ, что и символ графика, на котором запущен скрипт и дневной таймрейм):
ObjectSetString(0,name,OBJPROP_SYMBOL,Symbol()); ObjectSetInteger(0,name,OBJPROP_PERIOD,PERIOD_D1);
Установим более удобный масштаб, для просмотра графика:
ObjectSetInteger(0,name,OBJPROP_CHART_SCALE,2);
Для установки масштаба используется числа от 0-5. 0 – самые мелкие бары, 5 – самые крупные.
С установкой остальных свойств вы сможете разобраться самостоятельно, изучив окно свойств объекта и изучив пример использования объекта из справочного руководства.
Принимать какие либо меры для обновления данных графика не нужно, данные обновляются сразу после создания объекта.
На такой график можно даже присоединить индикатор. Для этого надо загрузить требуемый индикатор, то есть получить его хэндл. Загрузим скользящую среднюю:
int h=iMA(Symbol(),PERIOD_D1,14,0,MODE_SMA,PRICE_CLOSE); if(h==INVALID_HANDLE){
Alert("Ошибка загрузки индикатора");
}
Еще надо получить идентификатор графика, для этого используется функция ChartGetInteger() с идентификатором OBJPROP_CHART_ID:
long id=ObjectGetInteger(0,name,OBJPROP_CHART_ID);
Наконец, при помощи функции ChartIndicatorAdd(), добавляем индикатор на график:
ChartIndicatorAdd(id,0,h);
Также к графику можно применять шаблон функцией ChartApplyTemplate().
Пиксельное изображение
Объекты «Рисунок» (OBJ_BITMAP) и «Графическая метка» (OBJ_BITMAP_LABEL) позволяют добавлять на график пиксельные изображения (файлы *.bmp). Разница этих объектов в том, что координаты объекта «Рисунок» задаются временем и ценой, а объекта «Графическая метка» в пикселях. Файлы изображений могут располагаться в любом месте в папке MQL5, расположенной в папке данных терминала. Существует несколько форматов изображений *.bmp, если какой-то файл не будет отображаться в объекте, откройте его программой Paint, входящей в комплект операционной системы Windows, и сохраните командой «Сохранить как».
Следующий код располагается в скрипте с именем «024 GO Bitmap». Скопируем данные одного бара, они будут использоваться как координата для установки объекта:
MqlRates p[1]; if(CopyRates(Symbol(),Period(),0,1,p)==-1){
return;
}
Создание объекта и установка его координат:
string name="bitmap"; ObjectCreate(0,name,OBJ_BITMAP,0,0,0); ObjectSetInteger(0,name,OBJPROP_TIME,0,p[0].time); ObjectSetDouble(0,name,OBJPROP_PRICE,0,p[0].low);
Осталось назначить файл изображения. В комплект терминала входит пара файлов с изображениями доллара и евро, расположены они в папке «MQL5/Images». Используем функцию ObjectSetString() с идентификатором OBJPROP_BMPFILE:
ObjectSetString(0,name,OBJPROP_BMPFILE,"/Images/dollar.bmp");
Обратите внимание, что путь начинается со слеша, и указывается имя папки, расположенной в папке «MQL5».
У объекта «Графическая метка» есть еще одно отличие, он может использоваться как кнопка – ему можно назначить два изображения, одно для состояния «включено», другое для состояния «выключено». Создаем объект и устанавливаем его координаты:
name="bitmap2"; ObjectCreate(0,name,OBJ_BITMAP_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,20); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,20);
Теперь можно назначить одно изображение (точно так же, как для объекта «Рисунок»), а можно два, делается это с использованием модификатора (0 – для состояния «включено», 1 – для состояния «выключено»):
name="bitmap2"; ObjectCreate(0,name,OBJ_BITMAP_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,20); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,20);
ObjectSetString(0,name,OBJPROP_BMPFILE,0,"/Images/dollar.bmp"); ObjectSetString(0,name,OBJPROP_BMPFILE,1,"/Images/euro.bmp");
После запуска скрипта на графике должно появиться два изображения, одно должно находиться на графике неподвижно, а другое прокручиваться вместе с ценой. Если щелкнуть по неподвижному изображению, оно должна измениться. Состояние такой «кнопки» определяется функцией ObjectGetInteger() с идентификатором OBJPROP_STATE.
Еще больший интерес представляют собой объекты «Рисунок» и «Графический метка» тем, что на них можно рисовать программно и в том числе выводить текст. Создаем еще один объект «Графическая метка», устанавливаем его координаты и размеры:
name="bitmam3"; uint width=300; uint height=200;
ObjectCreate(0,name,OBJ_BITMAP_LABEL,0,0,0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,20); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,70); ObjectSetInteger(0,name,OBJPROP_XSIZE,300); ObjectSetInteger(0,name,OBJPROP_YSIZE,200);
Теперь вместо изображения объекту надо назначить ресурс:
string rname="::resource1"; ObjectSetString(0,name,OBJPROP_BMPFILE,rname);
Конечно же, надо создать этот ресурс. Он представляет собой массив типа uint с количеством элементов, соответствующих количеству пикселей изображения, то есть один элемент массива соответствует одному пикселу, а значение этого элемента соответствует цвету этого пикселя. Используется одномерный массив, данные изображения в нем располагаются последовательно строка за строкой. В следующем примере коде массив масштабируется в соответствии с количеством пикселей изображения и инициализируется значением 255*255, соответствующим зеленому цвету:
uint pixels[]; ArrayResize(pixels,width*height); ArrayInitialize(pixels,255*255);
Сначала надо заполнить массив данными, потом вызвать функцию ResourceCreate(), после этого данные из массива отобразятся на графике. В функцию ResourceCreate() передаются следующие параметры: имя ресурса, массив с данными изображения, ширина и высота изображения. Если размер массива точно соответствует произведению ширины на высоту, то следующие три параметра должны быть равны нулю. Последний параметр – цветовой формат, это один из вариантов перечисления ENUM_COLOR_FORMAT. Используем вариант COLOR_FORMAT_XRGB_NOALPHA – без прозрачности.
Также выведем на изображение текст, для этого сначала надо подготовить его параметры при помощи функции TextSetFont(). В функцию передается четыре параметра. Первый параметр – название шрифта.
Второй параметр, его размер. Размер шрифта может задаваться двумя способами – отрицательным числом и положительным числом. Отрицательное число означает соответствие пункту системного шрифта, при этом размер указывается в десятых долях пункта, то есть значение -120 соответствует шрифту размером 12.
Третий параметр – набор флагов стиля отображения, будем выводить текст нормальной толщины с наклоном и подчеркнутый, для этого используем набор флагов FONT_ITALIC|FONT_UNDERLINE|FW_NORMAL. Четвертый параметр — угол наклона текста. Он указывается в десятых долях градуса, например, 450 означает 45 градусов. Вывод текста выполняется функцией TextOut(), в функцию передаются следующие параметры: выводимая строка, координата x точки привязки, координата y точки привязки, точка привязки, массив ресурса, ширина ресурса, высота ресурса, цвет и формат. Точка привязки определяется набором специальных идентификаторов, для левого верхнего угла это набор TA_LEFT|TA_TOP. Поскольку формат указывается при вызове функции ResourceCreate(), и при вызове функции TextOut(), объявим для него переменную:
ENUM_COLOR_FORMAT colfmt=COLOR_FORMAT_XRGB_NOALPHA;
Установим параметры текста:
TextSetFont("Arial",-120,FONT_ITALIC|FONT_UNDERLINE|FW_NORMAL,0);
Кодирование цвета здесь отличается от кодирования, рассмотренного при изучении переменной типа color. Поэтому для преобразования компонентов RGB в значение цвета воспользуется макросом:
#define XRGB(r,g,b) (0xFF000000|(uchar(r)<<16)|(uchar(g)<<8)|uchar(b))
Этот макрос надо добавить в верхнюю часть файла. Кодирование цвета отличается тем, что байты, которые в переменной color определяют красный цвет, здесь определяют синий, а байты синего цвета определяют красный.
Осталось совсем немного. Сделаем так, чтобы цвет изображения переключался с красного на синий каждую секунду и также менялся цвет текста:
int n=0;
uint txtcol=0; uint bgcol=0;
while(!IsStopped()){ if(n==0){
txtcol=XRGB(255,255,0); bgcol=XRGB(255,0,0); n=1;
}
else{
txtcol=XRGB(0,255,255); bgcol=XRGB(0,0,255); n=0;
}
for(uint i=0;i<width;i++){ for(uint j=0;j<height;j++){
pixels[j*width+i]=bgcol;
}
}
TextOut("Строка-1",10,10,TA_LEFT|TA_TOP,pixels,width,height,txtcol,colfmt); TextOut("Строка-2",10,30,TA_LEFT|TA_TOP,pixels,width,height,txtcol,colfmt); TextOut("Строка-3",10,50,TA_LEFT|TA_TOP,pixels,width,height,txtcol,colfmt);
ResourceCreate(rname,pixels,width,height,0,0,0,colfmt); ChartRedraw(0);
Sleep(1000);
}
По завершению работы скрипта удалим все объекты:
ResourceFree(rname); ObjectDelete(0,name); ObjectDelete(0,"bitmap"); ObjectDelete(0,"bitmap2"); ChartRedraw(0);
После запуска скрипта на графике должно появиться два маленьких изображения, одно из них должно прокручиваться вместе с графиком цены, другое должно находиться на графике неподвижно (если по нему щелкнуть, его изображение должна измениться) и еще одно более крупного размера, оно должно мигать (рис. 68).
Рис. 68. Результаты работы скрипта «024 GO Bitmap»
Заметьте, смена цвета изображения происходит без мерцания, то есть данный подход может быть использован даже для создания непрерывной анимации.
При создании своих пиксельных изображений может пригодиться функция TextGetSize(), в функцию передается три параметры: измеряемый текст и две переменные, передаваемые по ссылке, для получения ширины и высоты. Сама функция возвращает true или false в зависимости от успешности ее работы. Функция определяет размер текста в соответствии с его параметрами, установленными функцией TextSetFont().
Графические объекты в подокне
Чтобы узнать номер подокна какого-либо индикатора, используется функция ChartWindowFind(). В функцию передается два параметра: идентификатор графика и короткое имя индикатора. Короткое имя, это то имя, которое отображается в левом верхнем углу подокна. Присоедините на график пользовательский индикатор RSI (расположен в навигаторе в папке «Индикаторы/Examples») и увидите в левом верхнем углу надпись «RSI(14)» (рис. 69).
Следующий код располагается в скрипте «025 Subwin». Находим подокно:
int win=ChartWindowFind(0,"RSI(14)");
Проверяем значение переменой win, вдруг подокно не найдено, в этом случае в переменной win будет значение -1, тогда будем создавать графический объект на графике цены:
if(win==-1){ win=0;
}
Рис. 69. Короткое имя индикатора в левом верхнем углу его подокна
Создаем надпись в левом нижнем углу:
string name="lbl"; ObjectCreate(0,name,OBJ_LABEL,win,0,0);
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,5); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,5); ObjectSetString(0,name,OBJPROP_TEXT,"Надпись для подокна"); ChartRedraw();
Есть еще один вариант вызова функции ChartWindowFind() – без параметров. Этот вариант используется в индикаторах, когда нужно узнать номер своего подокна.
На этом закончим рассмотрение графических объектов. Рассмотрена лишь незначительная часть из их количества, но это основная и наиболее часто используемая часть. Так что, если вам потребуется использовать другие объекты, вы наверняка справитесь с ними самостоятельно.
Графики
Графики терминала, те самые, в которых отображается история изменения котировок в виде баров или свечей, и на которые очень много и часто приходится смотреть пользователям терминала, имеют очень много параметров: цвет фона, цвета баров или свечей, отступы, масштаб и много других. Для получения этих параметров используются функции ChartGetInteger(), ChartGetString(), ChartGetDouble() с различными идентификаторами. Соответственно для изменения этих параметров используются функции ChartSetInteger(), ChartSetString(), ChartSetDouble().
Щелкните на графике правой кнопкой и выберите команду «Свойства» – откроется окно настройки отображения графика с вкладками «Общие», «Показывать», «Цвета», все параметры графика, изменяемые через это окно, можно менять программно. Еще отображением графиков можно управлять через главное меню, вкладку «Графики», все команды этого меня так же могут быть выполнены программно. Если подробно рассматривать все идентификаторы, относящиеся к функциям графиков, это будет переписыванием справочного руководства, поэтому рекомендуется ознакомиться с ними самостоятельно, это не займет много времени, все они собраны в три таблицы и расположены в одном разделе (Константы, перечисления и структуры – Константы графиков – Свойства графиков). Здесь рассмотрим только основные идентификаторы.
Код примеров располагается в скрипте с именем «026 Chart». Немного настроим внешний вид графика. Включим отображение графика в виде свечей:
ChartSetInteger(0,CHART_MODE,CHART_CANDLES);
Изменим цвет фона на белый, а цвет текста и границ на черный:
ChartSetInteger(0,CHART_COLOR_BACKGROUND,clrWhite); ChartSetInteger(0,CHART_COLOR_FOREGROUND,clrBlack);
Установим черный цвет обводки свечей:
ChartSetInteger(0,CHART_COLOR_CHART_UP,clrBlack); ChartSetInteger(0,CHART_COLOR_CHART_DOWN,clrBlack); ChartSetInteger(0,CHART_COLOR_CHART_LINE,clrBlack);
Тела медвежьих свечей сделаем красными, а бычьих – синими:
ChartSetInteger(0,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(0,CHART_COLOR_CANDLE_BULL,clrBlue);
Установим масштаб:
ChartSetInteger(0,CHART_SCALE,2);
Отключим автопрокрутку и включим отступ 20 баров:
ChartSetInteger(0,CHART_AUTOSCROLL,false); ChartSetInteger(0,CHART_SHIFT,20);
Для отключения отступа указывается величина 0.
Далее присоединим на график несколько индикаторов, но сначала проверим, нет ли на графике каких-то индикаторов, и удалим их. Для этого сначала получим количество подокон графика:
int subwin_total=(int)ChartGetInteger(0,CHART_WINDOWS_TOTAL);
Затем, проходя в цикле по всем подокном, для каждого из них получим количество индикаторов при помощи функции ChartIndicatorsTotal(). Далее, проходя в цикле по всем индикатором подокна, будем получать их имена функцией ChartIndicatorName(), и удалять функцией ChartIndicatorDelete():
int subwin_total=(int)ChartGetInteger(0,CHART_WINDOWS_TOTAL); for(int i=0;i<subwin_total;i++){
int ind_total=ChartIndicatorsTotal(0,i); for(int j=ind_total-1;j>=0;j--){
string name=ChartIndicatorName(0,i,j); ChartIndicatorDelete(0,i,name);
}
}
Используя имя индикатора, полученного функцией ChartIndicatorName(), можно получить хэндл этого индикатора, для этого используется функция ChartIndicatorGet(). Располагая хэндлом, возможно применение функции CopyBuffer(). Таким образом, возможно создание гибкой системы, следящей за показаниями индикаторов, присоединенных на график вручную. Но это не является сейчас основной задачей.
Загрузим требуемые индикаторы, это будет две скользящих средних и два индикатора RSI:
int h1=iMA(Symbol(),Period(),13,0,MODE_SMA,PRICE_CLOSE); int h2=iMA(Symbol(),Period(),21,0,MODE_SMA,PRICE_CLOSE); int h3=iRSI(Symbol(),Period(),7,PRICE_CLOSE);
int h4=iRSI(Symbol(),Period(),14,PRICE_CLOSE);
Прикрепляем на график скользящие средние:
СhartIndicatorAdd(0,0,h1); ChartIndicatorAdd(0,0,h2);
При присоединении индикатора, располагающегося в подокне, надо указать номер подокна. Это должен быть номер существующего подокна или следующего по порядку. Поэтому надо получить количество подокон:
subwin_total=(int)ChartGetInteger(0,CHART_WINDOWS_TOTAL);
Если подокон на графике нет, функция возвращает значение 1 (график цены считается за подокно). Оба RSI добавляем в одно подокно:
ChartIndicatorAdd(0,subwin_total,h3); ChartIndicatorAdd(0,subwin_total,h4);
Номером подокна указывается количество подокон, таким образом, для первого индикатора RSI
создается новое подокно, а второй индикатор RSI присоединяется в это же окно. Все, что получилось, сохраним в шаблон:
ChartSaveTemplate(0,"2ma_rsi");
Откроем еще пару графиков того же символа, что и график, на котором будет запущен скрипт, но с таймфреймами H4 и D1:
ChartOpen(Symbol(),PERIOD_H4); ChartOpen(Symbol(),PERIOD_D1);
Пройдем по все графикам, открытым в терминале и к каждому применим только что сохраненный шаблон:
long id=ChartFirst(); while(id!=-1){
if(id!=ChartID()){ ChartApplyTemplate(id,"2ma_rsi");
}
id=ChartNext(id);
}
При программном открытии графика он становится активным, то есть оказывается на переднем плане. Снова активизируем тот график, на котором был запущен скрипт:
ChartSetInteger(0,CHART_BRING_TO_TOP,true);
На этом все возможности по работе с графиками еще не заканчиваются. Располагая идентификатором графика, можно определить его символ и тайфмфрейм. Для этого используются функции ChartSymbol() и ChartPeriod(). Символ и таймфрейм можно изменить. Для этого используется функция ChartSetSymbolPeriod(). Также график может быть закрыт, это выполняется функцией ChartClose(). Если для какой-то цели потребуется узнать идентификатор графика, на котором работает скрипт, эксперт или индикатор, используется функция ChartID().
Скрипты, эксперты, индикаторы могут запускаться не только двойным щелчка в окне «Навигатор», но и перетаскиванием на график, в этом случае можно определить координаты точки, в которой мышь была отпущена. Узнать точку сброса можно как в координатах цена-время, так и в пикселях. Создайте скрипт с именем «027 Drop», следующий код располагается в нем:
int subwin=ChartWindowOnDropped(); datetime tm=ChartTimeOnDropped(); double pr=ChartPriceOnDropped(); int x=ChartXOnDropped();
int y=ChartYOnDropped();
Alert("subwin=",subwin,", tm=",tm,", pr=",pr,", x=",x,", y=",y);
Этот скрипт нужно перетаскивать на график, иначе все значения будут равны нулю. Еще существуют функции для перевода координат время-цена в пиксели и назад. Перевод из времени и цены в пиксели:
int x2,y2; ChartTimePriceToXY(0,subwin,tm,pr,x2,y2); Alert("x2=",x2,", y2=",y2);
Из пикселей в цену и время:
datetime tm2; double pr2; int subwin2;
ChartXYToTimePrice(0,x,y,subwin2,tm2,pr2); Alert("subwin2=",subwin2,", tm=",tm2,", pr=",pr2);
Был получен следующий результат: «subwin=1, tm=2020.08.04 18:45:00, pr=34.746, x=430, y=316», «x2=428, y2=314», «subwin2=1, tm=2020.08.04 18:52:30, pr=33.051» – есть небольшая погрешность как при переводе времени-цены в пиксели, так и при переводе пикселей во время-цену.
Функция ChartNavigate() позволят программно прокручивать график на заданное количество баров. В функцию передается три параметра. Первый параметр – идентификатор графика. Второй параметр – идентификатор позиции, от которой выполняется отсчет баров:
- CHART_BEGIN – от начала графика (от левого края),
- CHART_END – от конца (от правого края),
- CHART_CURRENT_POS – от текущей позиции.
Третий параметр – величина смещения в барах, отрицательно значение означает смещение влево (в прошлое), положительное – вправо.
Функция ChartScreenShot() позволят делать снимки экрана. В функцию передается пять параметров: идентификатор графика, имя файла (нужно указывать расширение png, jpg или gif), ширина, высота, выравнивание (ALIGN_LEFT – по левому краю, ALIGN_RIGHT – по правому краю, ALIGN_CENTER – по центру). Ширина и высота действуют следующим образом: по высоте выполняется масштабирование, а по ширине выполняется обрезание.
Выравнивание работает относительно всех данных графика, то есть при выравнивании по левому краю (ALIGN_LEFT) на скриншоте будут находиться самые старые данные. При выравнивании по правому краю (ALIGN_RIGHT) – самые последние. А их количество будет определять шириной скриншота. При выравнивании по центру (ALIGN_CENTER), происходит выравнивание по левому краю видимой части данных. Сохранение скриншотов выполняется в паку «Files» папки данных терминала или в ее подпапку.
Напишем скрипт, пошагово прокручивающий график на величину экрана и делающий скриншоты. Имя скрипта «028 ScreenShot». Отключим автопрокрутку и смещение:
ChartSetInteger(0,CHART_AUTOSCROLL,false); ChartSetInteger(0,CHART_SHIFT,0);
Определим общее количество баров на графике и количество баров, видимых на экране:
int tbars=Bars(Symbol(),Period());
int wbars=(int)ChartGetInteger(0,CHART_WIDTH_IN_BARS);
Определим высоту графика в пикселях, для этого пройдем по всем подокнам и просуммируем их высоту:
int h=0;
int wt=(int)ChartGetInteger(0,CHART_WINDOWS_TOTAL); for(int i=0;i<wt;i++){
h+=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,i);
}
Определим ширину.
int w=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS)+60;
Функция ChartGetInteger() с идентификатором CHART_WIDTH_IN_PIXELS возвращает ширину только той части, на которой нарисованы бары, а скриншот сохраняется с осью цен, расположенной по правому краю графика, ее ширина 60 пикселей.
Подготовим график, прокрутив его до конца влево, чтобы были видно последние бары:
ChartNavigate(0,CHART_END,0);
Подготовим имя папки, в которой будут сохраняться скриншоты, сформируем его из текущего времени и заменим знаки «:» на «-»:
string folder=TimeToString(TimeCurrent(),TIME_DATE)+" "+TimeToString(TimeCurrent(),TIME_SECONDS); StringReplace(folder,":","-"); folder="ScreenShot_"+folder+"/";
В цикле по всем барам с шагом, соответствующим количеству видимых баров на графике, делаем скриншоты и выполняем прокрутку:
for(int i=0;i<tbars && !IsStopped();i+=wbars){ string fn=folder+StringFormat("%010d",i)+".png"; ChartScreenShot(0,fn,w,h,ALIGN_CENTER); ChartNavigate(0,CHART_CURRENT_POS,-wbars);
}
Снова прокрутим график до конца влево и уведомим пользователя:
ChartNavigate(0,CHART_END,0); Alert("См. папку "+folder);
Прочее
Из всего обилия возможностей языка ML5, довольно значительная часть остается не рассмотренной. Это экономический календарь, управление сигналами, сетевые функции, работа с OpenCL, DirectX, базы данных, интеграция с Pyton и, может быть, еще что-нибудь. Дело в том, что уже и так рассмотрено достаточно возможностей языка для полноценного программирования торговых стратегий – для создания экспертов, которые могут работать как на счете, так и тестироваться и оптимизироваться в тестере стратегий. Рассмотрен не просто достаточный минимум, а достаточный для полноценной разработки полноценных экспертов и индикаторов. Причем, рассмотрено даже немного лишнего. Если же продолжить изучение абсолютно всех возможностей языка, процесс может затянуться на долгие месяцы, а потом может оказаться так, что огромное количество времени потрачено на изучение того, что никогда не будет вами использоваться.