Программирование экспертов в языке MQL5

Простейший эксперт

При создании этого эксперта в наибольшей степени воспользуемся возможностями, предоставляемыми стандартными классами. Напишем эксперта торгующего по индикатору RSI. При пересечении уровня 30 снизу вверх – покупка, при пересечении уровня 70 сверху вниз – продажа. Перед открытием новой позиции противоположная позиция закрывается.

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

Первый шаг создания файла эксперта

Рис. 1. Первый шаг создания файла эксперта

Создание эксперта, так же как и создание скрипта и индикатора, начинается с использования помощника «Мастера MQL». В навигаторе необходимо выделить папку, в которой будет создаваться файл, и выполнить команду главного меню: Фал – Создать или щелкнуть на имени папки, в контекстном меню выбрать команду «Новый файл». В открывшемся окне необходимо выбрать вариант «Советник (шаблон)» и нажать на кнопку «Далее» (рис. 1).

На следующем шаге в поле «Имя» надо ввести имя создаваемого эксперта, в данном случае будет имя «001 RSI Simple», создать один внешний параметр и нажать кнопку «Далее» (рис. 1).

На следующем шаге выполняется выбор дополнительных обработчиков событий: OnTrade, OnTradeTrasaction, OnTimer, OnChartEvent, OnBookEvent (рис. 2). Обработчики OnTimer, OnChartEvent были рассмотрены при изучении индикаторов, в экспертах они работают точно так же. Обработчики OnTrade и OnTradeTrasaction выполняются при каких либо торговых событиях на счете, например при установке ордера, его срабатывании и т.п. В обработчик OnTradeTrasaction, в отличие от обработчика OnTrade, передаются параметры с дополнительной информацией о происшедшем событии. Обработчик OnBookEvent реагирует на события стакана цен и тоже имеет параметры с информацией о событии.

Второй шаг создания файла эксперта

Рис. 1. Второй шаг создания файла эксперта – вводи имени файла и создание внешних параметров

В общем, обработчики OnTrade, OnTradeTrasaction, OnBookEvent используются для решения довольно специфических задач или при каком-то особом, характерном подходе к созданию экспертов. Наибольшая часть задач, решаемых этими обработчиками, может быть легко решена путем слежения за состоянием рынка и историей торговли. Обычно при создании экспертов в них нет необходимости. В создаваемом эксперте ни один из этих обработчиков не потребуется, так что снимает все галки и нажимает кнопку «Далее».

На следующем шаге продолжается выбор дополнительных обработчиков: OnTester, OnTesterInit, OnTesterPass, OnTesterDeinit (рис. 3). Эти обработчики выполняются только в тестере. Обработчик OnTester срабатывает по завершению тестирования или одного прохода оптимизации (вызывается непосредственно перед OnDeinit). Обычно этот обработчик используется для создания собственного критерия оптимизации. Для этого в функции OnTester() необходимо провести расчеты критерия и оператором return вернуть результат типа double.

При этом в отчете оптимизации появляется дополнительная колонка, по которой можно выполнить сортировку результатов. Обработчики OnTesterInit и OnTesterDeinit выполняются в начале и после оптимизации. Обработчик OnTesterPass срабатывает при поступлении так называемого фрейма данных от агента тестирования. Отправка фрейма выполняется функцией FrameAdd(). Дело в том, что терминал MetaTrader5, выполняя оптимизацию, задействует все ядра процессора, кроме того могут задействоваться удаленные компьютеры (из собственной сети или из облака сообщества mql5.com). Так что, обычное использование файлов для сохранения данных при оптимизации невозможно, для этого используется передача данных во фреймах. Здесь работа с фреймами рассматриваться не будет, поскольку необходимость их использование возникает крайне редко.

Третий шаг создания файла эксперта

Рис. 2. Третий шаг создания файла эксперта – выбор обработчиков событий

Продолжение выбора обработчиков событий

Рис. 3. Продолжение выбора обработчиков событий

После нажатия на кнопку «Готово» создание файла завершается, и он открывается в редакторе. Так же как в файле индикатора, в его верней части располагаются свойства, внешние параметры, затем идут стандартные обработчики событий (функции): OnInit(), OnDeinit() и OnTick() (вместо индикаторного обработчика OnCalculate()).

Приступаем непосредственно к программированию эксперта. Во внешние параметры добавляем переменную для объема сделки:

input double lot = 0.1;

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

input int rsiPeriod = 14;
input ENUM_APPLIED_PRICE rsiPrice = PRICE_CLOSE;
input double levelBuy = 30;
input double levelSell = 70;

Начинающим бывает интересно поэкспериментировать с использованием торговых сигналов, возникающих на формирующемся баре, для этого потребуется переменная, определяющая индекс бара, с которого эксперт будет брать показания индикатора:

input int shift = 1;

Чуть ниже внешних параметров объявляем переменную для хэндла:

int h;

В функции OnInit() выполняется загрузка индикатора и проверка хэндла:

h=iRSI(Symbol(),Period(),rsiPeriod,rsiPrice); if(h==INVALID_HANDLE){
Print("Ошибка загрузки индикатора iRSI"); return(INIT_FAILED);
}

В функции Deinit() ускоряем выгрузку индикатора при завершении работы эксперта. Но сначала надо проверить, действительно ли индикатор был загружен:

if(h!=INVALID_HANDLE){
IndicatorRelease(h);
}

Пока в функции OnInit() выполняется одна проверка, после которой может произойти завершение работы эксперта возвратом кода INIT_FAILED. Но прогресс обычно не стоит на месте и почти каждый советник подвергается множеству доработок в дальнейшем. Может получиться так, что будет добавлена еще одна подобная проверка с завершением работы эксперта, а располагаться она будет перед загрузкой индикатора. В результате чего не исключена работа функции OnDeinit() с неинициализированной переменной для хэндла. Так что желательно поправить объявление этой переменной – добавить ее инициализацию:

int h=INVALID_HANDLE;

При разработке эксперта нужно подумать не только о точной реализации задуманного алгоритма торговли, но и о его устойчивой работе в реальных торговых условиях, а также о его высокой скорости работы в тестере. При работе на сформированном баре (при значении параметра shift равном 1), для обеспечения высокого быстродействия в тестере, достаточно выполнять проверку на наличие торгового сигнала и выполнять все торговые действия один раз в момент открытия нового бара. Однако в реальных условиях (при работе на счете) нет гарантии, что торговое действие будет выполнено с первой попытки, поэтому необходимо обеспечить возможность повторения попытки.

На глобальном уровне (ниже переменой для хэндла) объявляем массив типа datetime размером в один элемент и простую переменную этого же типа:

datetime curTime[1]; datetime lstTime;

В самом начале функции OnTick() копируем время последнего бара с проверкой результата, если копирование не выполнено (функция вернула -1), то завершаем работу функции OnTick():

if(CopyTime(Symbol(),Period(),0,1,curTime)==-1){ return;
}

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

if(curTime[0]!=lstTime || shift==0){

//...

lstTime=curTime[0];
}

Весь дальнейший код, добавляемый в функцию OnTick(), будет располагаться внутри составного оператора if (место отмечено знаком комментария). Если при выполнении этого кода произойдет ошибка, надо будет завершить работу функции OnTick(). На следующем тике произойдет повторное выполнение этого кода. Если все действия будут выполнены без ошибок, будет выполнена последняя строка составного оператора – произойдет обновление значения переменной lstTime, и от этого на следующем тике весь этот код уже не будет выполняться.

Переменная lstTime пока используется не инициализированной. Есть несколько вариантов ее инициализации в зависимости и от личных предпочтений. Глобальная переменная в эксперте имеют одну особенность, если выполняется ее инициализации при объявлении, то эта инициализация выполняется только один раз при прикреплении эксперта на график, в дальнейшем (при изменении параметров, переключении таймфрейма и т.п.) переменная сохраняет свое значение. Если нужно, чтобы глобальная переменная сбрасывалась при смене параметров и переключении таймфрейма, ее необходимо присваивать инициализирующее значение в функции OnInit(). Оптимальным подходом для данного эксперта будет инициализация переменной при объявлении:

datetime lstTime=0;

Однако при инициализации нулем (хоть при объявлении переменной, хоть в функции OnInit()), а также и без инициализации, возникает дилемма. Если эксперта присоединить на график и в это время существует торговый сигнал, то сразу будет открыта позиция. Кому-то нужно именно это – вдруг увидел по индикатору торговый сигнал и решил запустить эксперта. А кто-то считает, что эксперт должен открывать позицию только в начале бара и никак иначе. Чтобы открытие было только в начале бара, в функции OnTick() после получения времени формирующегося бара надо проверить значение переменой lstTime, если оно равно нулю, присвоить ей полученное время:

if(lstTime==0)lstTime=curTime[0];

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

Проверку существования торговых сигналов будем выполнять в отдельной функции Signals(). Возвращать функция будет true или false в зависимости от успешности ее работы. Торговые сигналы функция будет возвращать через параметры по ссылке:

bool Signals(bool & aBuySig,bool & aSellSig){

//...

return(true);
}

Внутри функции копируем данные индикатора с двух баров, для этого потребуется массив размером в два элемента:

double ind[2];

Копирование данных:

if(CopyBuffer(h,0,shift,2,ind)==-1){ return(false);
}

Вычисление торговых сигналов:

aBuySig==(ind[1]>levelBuy && ind[0]<=levelBuy); aSellSig==(ind[1]<levelSell && ind[0]>=levelSell);

Ниже приведен весь код функции Signals():

bool Signals(bool & aBuySig,bool & aSellSig){ double ind[2]; if(CopyBuffer(h,0,shift,2,ind)==-1){
return(false);
}
aBuySig= (ind[1]>levelBuy && ind[0]<=levelBuy); aSellSig=(ind[1]<levelSell && ind[0]>=levelSell); return(true);
}

В функции OnTick() объявляем переменные для торговых сигналов и вызываем функцию для их получения:

bool buySig, sellSig; if(!Signals(buySig,sellSig)){
return;
}

Если функция Signal() отработала с ошибкой, происходит завершение работы функции OnTick() и произойдет повтор попытки на следующем тике.

После вызова функции Signals() поверяем, есть ли торговые сигналы:

if(buySig || sellSig){

//...

}

Весь дальнейший код, добавляемый в функцию OnTick(), располагается внутри этого составного оператора (место обозначено знаком комментария). Необходимо проверить существование позиции, если существует сигнал на покупку и позиция на продажу (или сигнал на продажу и позиция покупку), то позицию надо закрыть. Нужно написать функцию, определяющую существование позиции. Имя функции CheckPosition(), возвращать функция будет true или false, в зависимости от успешности ее работы. В функцию будет передавать по ссылке два параметра типа bool, показывающих существование позиции на покупки и на продажу:

bool CheckPosition(bool & aBuyExists, bool & aSellExists){

//... return(true);
}

Следующий код располагается внутри этой функции. В самом начале функции «обнулим» переменные aBuyExists и aSellExists:

aBuyExists=false; aSellExists=false;

На нетинговых и хеджинговых счетах существование позиции проверяется по-разному, поэтому сначала надо выполнить проверку типа счета. Можно использовать функцию AccountInfoInteger(), но лучше приучаться использовать стандартные классы, что в дальнейшем значительно сократит трудоемкость разработки. В верхней части файла подключаем файл с классом CAccountInfo и создаем объект:

#include <Trade/AccountInfo.mqh> CAccountInfo ai;

Проверяем тип счета:

if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
// определение позиции для неттингового счета
}
else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
// определение позиции для хеджингового счета
}

Для неттингового счета делаем попытку выделения позиции. Если произошло выделение позиции, значит, она существует, проверяем ее тип. Для работы с позициями воспользуемся классом CPositionInfo. Подключаем класс и создаем объект:

#include <Trade/PositionInfo.mqh> CPositionInfo p;

Проверяем существование позиции и ее тип:

if(p.Select(Symbol())){ aBuyExists=(p.PositionType()==POSITION_TYPE_BUY);

aSellExists=(p.PositionType()==POSITION_TYPE_SELL);
}

Для работы на хеджиноговом счете позиции могут помечаться магическим номером, поэтому объявим для него внешнюю переменную:

input int magicNum=123;

Хотелось бы иметь для этой переменой простое имя «magic», но оно уже используется в классе CAccountInfo, а компилятор не позволяет иметь одинаковые имена глобальных и локальных переменных – нужно иметь в виду эту особенность языка MQL5.

Продолжаем с функцией CheckPosition(). В цикле по всем позициям находим первую позицию с подходящим символом и магиком, проверяем ее тип и завершаем цикл:

for(int i=PositionsTotal()-1;i>=0;i--){ if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()==magicNum){ aBuyExists=(p.PositionType()==POSITION_TYPE_BUY); aSellExists=(p.PositionType()==POSITION_TYPE_SELL); break;
}
}
else{
return(false);
}
}

Ниже приведен весь код функции CheckPosition():

bool CheckPosition(bool & aBuyExists, bool & aSellExists){ aBuyExists=false;
aSellExists=false; if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
// определение позиции для неттингового счета
if(p.Select(Symbol())){ aBuyExists=(p.PositionType()==POSITION_TYPE_BUY); aSellExists=(p.PositionType()==POSITION_TYPE_SELL);
}
}
else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
// определение позиции для хеджингового счета
for(int i=PositionsTotal()-1;i>=0;i--){ if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()==magicNum){ aBuyExists=(p.PositionType()==POSITION_TYPE_BUY); aSellExists=(p.PositionType()==POSITION_TYPE_SELL); break;
}
}
else{
return(false);
}
}
}
return(true);
}

Продолжаем в функции OnTick(). Объявляем две переменные и вызываем функцию

CheckPosition():

bool buyExists, sellExists; if(!CheckPosition(buyExists,sellExists)){
return;
}

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

#include <Trade/Trade.mqh> CTrade t;

Сначала выполняется закрытия и тут же выполняется открытие. Но открытие должно выполняться, только если нет позиции, поэтому сразу после закрытия необходимо или «обнулить» переменные buyExists и sellExists, или снова вызвать функцию CheckPosition(). Выберем второй вариант. Терминал имеет одну особенность – торговое действие выполнено, но оно еще не отражено в списках терминала. В этом случае функция CheckPosition() может показать существование позиции. Необходимо дать терминалу небольшое время для обновления списков – после выполнения закрытия вызовем функцию Sleep() для создания паузы длительностью в одну секунду, а после этого проверим существование позиции.

if((buySig && sellExists) || (sellSig && buyExists)){

if(!t.PositionClose(Symbol())){ return;
}
Sleep(1000); if(!CheckPosition(buyExists,sellExists)){
return;
}

}

Если позиции нет, нужно открыть ее:

if(!buyExists && !sellExists){ if(buySig){
if(!t.Buy(lot)){ return;
}
}

if(sellSig){ if(!t.Sell(lot)){
return;
}
}
}

Ниже приведен весь код функции OnTick():

void OnTick(){

if(CopyTime(Symbol(),Period(),0,1,curTime)==-1){ return;
}

//if(lstTime==0)lstTime=curTime[0]; if(curTime[0]!=lstTime || shift==0){
bool buySig, sellSig; if(!Signals(buySig,sellSig)){
return;
}

if(buySig || sellSig){

bool buyExists, sellExists; if(!CheckPosition(buyExists,sellExists)){
return;
}

if((buySig && sellExists) || (sellSig && buyExists)){ if(!t.PositionClose(Symbol())){
return;
}
Sleep(1000); if(!CheckPosition(buyExists,sellExists)){
return;
}
}

if(!buyExists && !sellExists){ if(buySig){
if(!t.Buy(lot)){ return;
}
}
if(sellSig){ if(!t.Sell(lot)){
return;
}
}
}
}
lstTime=curTime[0];
}
}

Остался еще один важный момент. При открытии позиции ее необходимо помечать магическим номером. Для этого перед использованием объекта класса CTrade необходимо вызвать метод SetExpertMagicNumber() и передать в него переменную magicNum. Сделать это нужно в функции OnInit(), в любом ее месте:

t.SetExpertMagicNumber(magicNum);

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

Эксперт с учетом количества позиций

Теперь будет сделана более серьезная работа, результат которой в дальнейшем можно убудет использовать как основу для других подобных экспертов – работающих рыночными ордерами по сигналам индикаторов. Вход в рынок будет выполняться по индикатору «004 RSI Color Signal», и будет выполняться дополнительная проверка индикатора ADX и/или положения цены относительно скользящей средней. Эксперт вряд ли получится прибыльным, но это не важно, поскольку основной задачей в данном случае задачей является изучение именно программирования экспертов.

В эксперте будет полноценная функция подсчета количества позиций и параметры для регулирования их допустимого количества на хеджиноговых счетах. На неттинговых счетах эксперт будет тоже работать, но только с одной позицией. Кроме того будут, использоваться стоплосс, тейкпрофит, будет отключение функции закрытия по противоположному сигналу и отключаемые функции закрытия по каждому отдельному индикатору. Кроме постоянного лота будет возможность использовать лот, пропорциональный средствам, а также функция увеличения лота после убытка. Вдобавок будут сделаны функции трейлинга, безубытка и будет использоваться обработчик событий OnTester() для создания собственного критерия оптимизации. В общем, предстоит достаточно большая работа.

Разберем несколько особых задач, которые надо будет решить при создании этого эксперта. Некоторых брокеры или дилинговые центры не разрешают открывать рыночную позицию с заранее установленными стоплоссом и/или тейкпрофитом. В таком случае сначала нужно открыть позицию без стоплосса и тейкпрофита, а после этого выполнить ее модификацию.

Вторая особая задача – функция умножения лота после убытка. Для создания этой функции потребуется находить первую позицию – позицию с начальным лотом. А для этого позиции потребуется как-то помечать. Есть несколько способов для этого: комментарий ордера, запись данных в файл, использование глобальных переменных терминала, а также можно использовать кодирование данных в магическом номере.

Рассмотрим преимущества и недостатки всех этих способов. Преимуществом комментария является то, что метка ордера сохраняется в истории, недостатком же является опасность того, что брокер может заменить комментарий. Однако на практике с такой проблемой встречаться не приходилось, за исключением одного случая, причем, после обращения в техподдержку дилингового центра, проблема с использованием комментариев была устранена. Запись данных в файл – надежный и удобный способ, простой в реализации. Его недостатком является опасность утраты файлов с данными, это может произойти по случайности или при смене оборудования. Особенно досадна потеря данных при выходе из строя жесткого диска компьютера. Глобальные переменные – наиболее просты и удобны, но они не хранятся вечно. Кодирование данных в магическом номере – абсолютно надежный способ, но немного неудобный в эксплуатации, поскольку настоящим магическим номером является только его часть. Выберем самый надежный способ – магический номер.

Магический номер, как у ордера, так и у сделки представляет собой число типа ulong, его максимальное значение: 18446744073709551615 – как видим, это достаточно длинное число. Вряд у кого-то в терминале будет работать более тысячи экспертов, поэтому, для идентификации отдельного эксперта будет использоваться только три правых знака переменной, а остальные места в числе можно будет использовать для сохранения каких либо внутренних идентификаторов позиций. Такой подход удобен тем, что даже если у вас в терминале будет работать эксперт с простым магическим номером (без кодирования в нем дополнительных данных), у него надо будет использовать магический номер, не превышающий 999, таким образом, эксперты смогут работать без конфликтов.

Значит, три знака справа будем использовать для идентификации эксперта. Извлечь их можно получением остатка от деления на 1000. Следующие три знака магического номера будут использоваться для сохранения номера позиции в ряду прогрессии ее объема (лота). Для их извлечения нужно выполнить деление на 1000 и получить остаток от деления на 1000.

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

Приступаем к созданию эксперта. Имя файла «002 RSI Color Signal», один внешний параметр (чтобы обозначить место для параметров), один дополнительный обработчик событий OnTester.

Описание процесса создания предыдущего эксперта (файл «001 RSI Simple») выполнялось точно в том порядке, в каком он создавался. Поэтому приходилось несколько раз возвращаться то к объявлению глобальных переменных, то к функции OnInit(), то к OnTick() и т.д. Теперь же, сначала работа над экспертом была полностью завершена, после чего было сделано описание его создания. Иначе описание было бы очень запутанным.

Эксперт работает на пользовательском индикаторе «004 RSI Color Signal», для этого индикатор должен находиться в папке индикаторов терминала. Очевидно, если индикатор отсутствует, то эксперт не сможет правильно работать. Поэтому было бы удобно обеспечить себе легкий способ узнать, на каких индикаторах работает эксперт. Для этого имя индикатора указывается не непосредственно при вызове функции iCustom(), а используется макроподстановка, располагающаяся в верхней части файла:

#define IND "Изучение MQL5/004 RSI Color Signal"

При таком подходе, открыв файл эксперта в редакторе, станет сразу понятно, какие индикаторы нужны для него и в каких подпапках они должны располагаться. Все это без необходимости искат в коде вызовы функции iCustom().

Далее подключаем файлы со вспомогательными классами и создаем их объекты:

#include <Trade/AccountInfo.mqh> CAccountInfo ai;

#include <Trade/PositionInfo.mqh> CPositionInfo p;

#include <Trade/Trade.mqh> CTrade t;

#include <Trade/SymbolInfo.mqh> CSymbolInfo s;

#include <Trade/DealInfo.mqh> CDealInfo d;

Используются как можно более короткие имена объектов, поскольку к ним придется очень часто обращаться.

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

input ENUM_TIMEFRAMES timeFrame = PERIOD_CURRENT;
input int magicNum = 123;
input bool sltpInOrder = true;
input bool auto5Digits = true;

Переменная timeframe используется для явного указания таймфрейма, на котором должен работать эксперт. Использование этой переменной при работе эксперта на счете обеспечивает защиту от случайного переключения таймфрейма графика, к которому прикреплен эксперт. Переменная magicMumber – идентификатор ордеров, сделок и позиций эксперта, точнее – это три правых знака магического числа. Значение этой переменной не должно превышать 999.

Переменная sltpInOrder переключает вариант установки стоплосса и тейпрфита. При значении true позиция стразу открывается со стоплоссом и тейпрофитом (конечно, если они используются). При значении false позиция открывается без стплосса и тейпрофита, а следом выполняется ее модификация – устанавливается стоплосс и тейкпрофит. Переменная auto5Digits включает режим, при котором выполняется автоматическое умножение всех параметров, измеряющихся в пунктах, при работе эксперта на 3-х и 5-и знаковых котировках. Это обеспечивает некоторую универсальность эксперта.

Следующая группа определяет параметры сделки:

input string de = "=== Сделка ===";
input double lot = 0.1;
input double riskPerc = 10.0;
input bool useRisk = false;
input int stopLoss = 50;
input int takeProfit = 50;

Переменная lot определяет размер первой позиции при использовании фиксированного лота. Переменная riskPerc определяет процент свободных средств, используемых на первую сделку. Переменной useRisk выполняется выбор использования переменной lot (при false) или riskPerc (при true). Переменная StopLoss определяет величину стоплосса в пунктах, при значении 0 стоплосс не используется. Переменная TakeProfit определяет величину тейкпрофита в пунктах, при значении 0 тейкпрофит не используется.

Следующая группа определяет параметры функции умножения лота после убытка:

input string lp = "=== Прогрессия лота ===";
input bool useLotMult = false;
input double lotK = 2;
input int multCount = -1;

Переменная useLotMult служит для включения этой функции. Переменная lotK – это коэффициент умножения лота. Переменная multCount ограничивает количество умножений. Если достигнуто

предельное количество умножений, а прибыль не получена, следующая позиции открывается с объемом, как у последней позиции. При значении -1 ограничения отсутствуют.

Группа «Учет позиций» используется для ограничения количества открытых позиций:

input string oc = "=== Учет позиций ===";
input int buyCount = -1;
input int sellCount = -1;
input int totalCount = 1;
input bool oneSide = false;

Переменная buyCount ограничивает количество позиций на покупку, sellCount – на продажу, totalCount – общее количество (сумму позиций на покупку и продажу). Если переменной oneSide установить значение true, эксперт будет открывать позиции только в одну сторону. Например, если эксперт открыл позицию на покупку, то позицию на продажу он сможет открыть только когда будет закрыты все позиции на покупку. При использовании функции умножения лота (useLotMult равно true) эксперт автоматически включает ограничение не более, чем на одну позицию.

Раздел включения сигналов закрытия:

input string cl = "=== Закрытие ===";
input bool closeRev = false;
input bool closeRSI = false;
input bool closePDIMDI = false;
input bool closeMA = false;

Эксперт может закрывать противоположные позиции перед открытием новой позиции (переменная closeRev), а также могут использоваться сигналы каждого из индикаторов отдельно. Переменная closeRSI – закрытие по стрелкам основного индикатора, по которому выполняется открытие. Переменная closePDIMDI включает закрытие при смене положения линий PDI и MDI индикатора ADX. Переменная closeMA выполняет закрытие по положению цены относительно линии скользящей средней.

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

input string i0 = "=== Индикаторы ===";
input int shift = 1;

Если переменная shift равно 1, эксперт «смотрит» индикатор на первом сформированном баре, а если 0 – на формирующемся.

Следующая секция содержит параметры главного индикатора:

input string i1 = "=== "+IND+" ===";
input int rsiPeriod = 14;
input ENUM_APPLIED_PRICE rsiPrice = PRICE_CLOSE;
input double levelBuy = 30;
input double levelSell = 70;
input double levelMiddle = 50;
input bool arrowsMain = false;

Обратите внимание, в заголовке используется макроподстановка IND, таким образом, даже в окне свойств будет видно полное имя используемого индикатора.

Далее следуют параметры индикаторов ADX и MA:

input string i2 = "=== ADX ===";
input bool useADX = false;
input bool usePDIMDI = false;
input int adxPeriod = 14;
input double adxLevel = 20;
input string i3 = "=== MA ===";
input bool useMA = false;
input int maPeriod = 50;
input ENUM_MA_METHOD maMethod = MODE_EMA;
input ENUM_APPLIED_PRICE maPrice = PRICE_CLOSE;
input int maShift = 0;

Следующая секция – параметры функции трейлинга:

input string i4 = "=== Трейлинг ===";
input bool useTrailing = false;
input bool trEachTick = false;
input int trStart = 15;
input int trLevel = 10;
input int trStep = 10;

Переменной useTrailing выполняется включение функции. Переменная trEachTick определяет, будет ли функция работать на каждом тике (true) или раз на бар (false). Работа эксперта на сформированных барах (переменная shift равна 1) и трейлинга один раз на бар (переменная trEachTick равна false) обеспечивает идентичность работы эксперта на счете с работой эксперта при тестировании его по ценам открытия, что позволяет значительно ускорить процесс оптимизации эксперта. Переменная trStart определяет прибыль позиции в пунктах, при которой начинается перемещение стоплосса. Переменная trLevel – это уровень (дистанция) в пунктах, на котором стоплосс перемещается вслед за ценой. Перемещение стоплосса выполняется только в том случае, если его можно переместить на дистанцию не меньшую, чем указанно в переменной trStep (а пунктах), это снижает нагрузку на сервер брокера.

Последняя группа – параметры функции безубытка:

input string i5 = "=== Безубыток ===";
input bool useBreakeven = false;
input bool beEachTick = false;
input int beStart = 15;
input int beProfit = 3;

Переменной useBreakeven выполняется включение функции. Переменная beEachTick определяет, будет ли проводиться проверка на необходимость перемещения стоплосса на каждом тике (true) или раз на бар (false). Переменная beStart – это прибыль позиции (в пунктах), при которой выполняется перемещение стоплосса. Переменная beProfit определяет величину прибыли (в пунктах), остающейся у позиции при срабатывании стполосса.

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

ENUM_TIMEFRAMES _timeFrame;

Также необходимо проверить и подкорректировать переменные stopLoss и takeProfit:

int _stopLoss,_takeProfit;

Также и переменные секции учета позиций:

int _buyCount,_sellCount,_totalCount;

Кроме того и некоторые параметры функций трейлинга и безубытка:

int _trStart,_trLevel,_trStep; int _beStart,_beProfit;

Потребуются переменные для хэндлов индикаторов:

int h_rsi=INVALID_HANDLE; int h_adx=INVALID_HANDLE; int h_ma=INVALID_HANDLE;

Переменная h_rsi – для основного индикатора, h_adx – для индикатора ADX, h_ma – для скользящей средней.

Потребуются переменные для времени, уже знакомые по предыдущему эксперту:

datetime curTime[1]; datetime lstTime=0;

Еще пара переменных:

string prefix; bool setSLTP;

Переменная prefix будет использоваться при работе с глобальными переменными терминала, а переменная setSLTP – для функции установки стоплосса и тейкпрофита. Функция, проверяющая необходимость установки стоплосса и тейкпрофита, будет работать только если значение переменной setSLTP равно true, это значительно ускорит работу эксперта в тестере.

Переходим в функцию OnInit(). Проверяем и корректируем значение переменной timeframe:

_timeFrame=timeFrame; if(_timeFrame==PERIOD_CURRENT){
_timeFrame=Period();
}

Может быть, это лишнее, но когда-то, при загрузке пользовательского индикатора с указанием таймфрейма константой PERIOD_CURRENT, возникала ошибка, приходилось использовать функцию Period(). Скорее всего, это был временный баг, тем нее менее привычка выполнять такую проверку осталась.

Проверка и коррекция всех переменных, измеряющихся в пунктах, для работы функции, включаемой переменой auto5Digits:

_stopLoss=stopLoss;
_takeProfit=takeProfit;
_trStart=trStart;
_trLevel=trLevel;
_trStep=trStep;
_beStart=beStart;
_beProfit=beProfit;
if(auto5Digits && (Digits()==5 || Digits()==3)){
_stopLoss*=10;
_takeProfit*=10;
_trStart*=10;
_trLevel*=10;
_trStep*=10;
_beStart*=10;
_beProfit*=10;
}

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

_buyCount=buyCount;
_sellCount=sellCount;
_totalCount=totalCount;
if(useLotMult || ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){ if(_buyCount!=0)_buyCount=1;
if(_sellCount!=0)_sellCount=1; if(_totalCount!=0)_totalCount=1;
}

Получается, что переменные могут иметь значение 0 или 1.

Далее выполняется загрузка индикаторов с проверкой хэндлов. Основной индикатор загружается всегда:

h_rsi=iCustom(Symbol(),_timeFrame,IND,
rsiPeriod,rsiPrice,levelBuy, levelSell,levelMiddle);
if(h_rsi==INVALID_HANDLE){
Print("Ошибка загрузки индикатора ",IND); return(INIT_FAILED);
}

Остальные индикаторы загружаются только при их использовании:

if(useADX || usePDIMDI){ h_adx=iADX(Symbol(),_timeFrame,adxPeriod); if(h_adx==INVALID_HANDLE){
Print("Ошибка загрузки индикатора ADX"); return(INIT_FAILED);
}
}

if(useMA){
h_ma=iMA(Symbol(),_timeFrame,maPeriod,0,maMethod,maPrice); if(h_ma==INVALID_HANDLE){
Print("Ошибка загрузки индикатора MA"); return(INIT_FAILED);
}

Перед использованием объекта класса CSymbolInfo необходимо указать ему символ и выполнить обновление методом Refresh():

s.Name(Symbol());
if(!s.Refresh()){
Print("Ошибка CSymbolInfo"); return(INIT_FAILED);
}

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

prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+Symbol()+"_"+(string)magicNum+"_";

Переменной setSLTP присвоим =true:

setSLTP=true;

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

Переходим к функции OnTick(). Если стоплосс и тейкпрофит устанавливаются после открытия позиции, то проверка на необходимость их установки выполняется сразу после открытия позиции. Однако не исключено, что попытка первая окажется неудачной, поэтому потребуется выполнить ее повтор. Следующая попытка будет выполняться в самом начале функции OnTick():

if(setSLTP){ if(SetSLTP()){
setSLTP=false;
}
}

Этот участок кода будет выполняться, только если переменная setSLTP равна true, что будет случаться при неудачной первой попытке установить стоплосс и тейкпрофит и на запуске эксперта. Все действия по проверке и установке стоплосса и тейкпрофита выполняются в функции SetSLTP(). Если функция SetSLTP() отработала успешно, то есть вернула значение true, переменой setSLTP присваивается значение false. Рассмотрение функции SetSLTP() и всех других вспомогательных функций будет выполнено после рассмотрения функции OnTick().

Так же, как и в предыдущем эксперте, копируем время формирующегося бара:

if(CopyTime(Symbol(),Period(),0,1,curTime)==-1){ return;
}

Выполняем все основные действия раз на бар или на каждом тике, в зависимости от значения переменой shift:

if(curTime[0]!=lstTime || shift==0){
//… lstTime=curTime[0];

Следующий код располагается внутри вышеприведенного оператора if. Сначала проверяется наличие торговых сигналов. У этого эксперта могут быть как сигналы открытия, так и сигналы закрытия, поэтому объявляется четыре переменных:

bool buySig, sellSig, buyCloseSig, sellCloseSig;

Переменная buySig – сигнал открытия покупки, sellSig – сигнал открытия продажи. Переменные buyCloseSig и sellCloseSig – это сигналы закрытия. По логике своего действия они аналогичны переменным открытия, то есть переменная buyCloseSig это как бы сигнал открытия покупки, то есть он указывает в сторону движения цены вверх, но используется для закрытия позиций на продажу. Аналогично с переменной sellCloseSig, она указывает на необходимость закрытия покупки. Рекомендуется использовать именно такой подход в наименовании переменных, поскольку при написании более сложных экспертов, одни и те же сигналы могут использоваться как для открытия, так и для закрытия, в итоге очень легко запутаться.

Получение торговых сигналов выполняется в функции Signals(), в которую передаются все четыре переменные по ссылке. Сама функция возвращает true или false, в зависимости от успешности ее работы:

if(!Signals(buySig,sellSig,buyCloseSig, sellCloseSig)){ return;
}

Если функция вернула false, то есть не удалось правильно определить торговые сигналы, то нет смысла продолжать, поэтому выполняется завершение функции OnTick(). Если же функция отработал успешно, проверяется наличие каких-либо торговых сигналов:

if(buySig || sellSig || buyCloseSig || sellCloseSig){
//…
}

Если хоть какой-то сигнал есть, выполняется код внутри вышеприведенного оператора if. Чтобы определиться, нужно ли что-то закрывать или можно ли что-то открывать, необходимо подсчитать позиции. Для этого используется две переменные и функция:

int cntBuy,cntSell; if(!CountPositions(cntBuy,cntSell)){
return;
}

Переменная cntBuy – количество позиций на покупку, cntSell – на продажу. Эти переменные передаются в функцию CountPositions() по ссылке. Сама функция, как обычно, возвращает true или false, и, в случае ее неудачной работы, выполняется прерывание работы функции OnTick().

Если существует закрывающий сигнал на продажу и есть позиция на покупки или наоборот – существует закрывающий сигнал на покупку и существует позиция на продажу, то выполняем закрытие соответствующих позиций функцией CloseAll(). После выполнения закрытия переменные cntBuy и cntSell будут содержать данные, не соответствующие действительности, поэтому надо пересчитать позиции. Но прежде надо сделать паузу, чтобы терминал успел обновить свои списки:

if((sellCloseSig && cntBuy>0) || (buyCloseSig && cntSell>0)){ if(!CloseAll(buyCloseSig,sellCloseSig)){
return;

}
Sleep(1000); if(!CountPositions(cntBuy,cntSell)){
return;
}
}

Теперь проверяем существование сигналов на открытие и допустимое количество открытых позиций:

if(buySig || sellSig){
if(_totalCount==-1 || (cntBuy+cntSell<_totalCount)){
//…
}
}

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

if(buySig && !sellCloseSig && !sellSig){
if((_buyCount==-1 || cntBuy<_buyCount) && (!oneSide || cntSell==0)){ if(GVGet("LBT")!=curTime[0]){
if(!OpenBuy()){ return;
}
GVSet("LBT",curTime[0]);
}
}
}

Рассмотрим эту часть кода более подробно. Проверяется не только существование сигнала на покупку, но и отсутствие сигнала на закрытие покупки, а также отсутствие сигнала на продажу. Предполагается, что эксперт будет активно использоваться для экспериментов, будет меняться функция для получения торговых сигналов, и не исключена ситуация одновременного существование сигналов на покупки и продажу – или по причине ошибки (невозможно сразу все предусмотреть) или по причине особенностей стратегии. Так вот, в таком случае (при одновременном существовании сигналов на покупку и продажу) надо выполнить закрытии позиций и того и другого направления, но новые позиции не открывать.

Продолжаем рассмотрение вышеприведенного фрагмента кода. Проверка допустимого количества открытых позиций состоит из двух частей. Первая часть:

(_buyCount==-1 || cntBuy<_buyCount)

Вторая часть:

(!oneSide || cntSell==0)

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

проверки будет пройдена, если нет ограничений на позиции столько в одну сторону или же, если позиции в противоположную сторону отсутствуют.

Для запрета повторного открытия на одном баре используется глобальная переменная терминала, чтобы обеспечить надежность ее работы независимо от перезапуска терминала. Если все три проверки пройдены (торговый сигнал, допустимое количество, повторное открытие на одном баре), то функцией Buy() выполняется открытие позиции. Если функция Buy() отработала неудачно, то завершается работа функции OnTick(), чтобы повторить попытку на следующем тике (попытки будет повторяться до тех пор, пока сигнал к открытию существует). Если открыть позицию удалось, глобальной переменой присваивается время бара, на котором оно выполнено.

Аналогичный код для продажи:

if(sellSig && !buyCloseSig && !buySig){
if((_sellCount==-1 || cntSell<_sellCount) && (!oneSide || cntBuy==0)){ if(GVGet("LST")!=curTime[0]){
if(!OpenSell()){ return;
}
GVSet("LST",curTime[0]);
}
}
}

В самом конце функции OnTick(), за пределами всех предшествующих условий выполняется вызов функций сопровождения позиций (трейлинг и безубыток):

if(useTrailing){ Trailing();
}

if(useBreakeven){ Breakeven();
}

Ниже приведен весь код функции OnTick():

void OnTick()
{

if(setSLTP){ if(SetSLTP()){
setSLTP=false;
}
}

if(CopyTime(Symbol(),Period(),0,1,curTime)==-1){ return;
}

if(curTime[0]!=lstTime || shift==0){

bool buySig, sellSig, buyCloseSig, sellCloseSig; if(!Signals(buySig,sellSig,buyCloseSig, sellCloseSig)){
return;
}

if(buySig || sellSig || buyCloseSig || sellCloseSig){

int cntBuy,cntSell; if(!CountPositions(cntBuy,cntSell)){
return;
}

if((sellCloseSig && cntBuy>0) || (buyCloseSig && cntSell>0)){ if(!CloseAll(buyCloseSig,sellCloseSig)){
return;
}
Sleep(1000); if(!CountPositions(cntBuy,cntSell)){
return;
}
}

if(buySig || sellSig){
if(_totalCount==-1 || (cntBuy+cntSell<_totalCount)){ if(buySig && !sellCloseSig && !sellSig){
if((_buyCount==-1 || cntBuy<_buyCount) && (!oneSide || cntSell==0)){ if(GVGet("LBT")!=curTime[0]){
if(!OpenBuy()){ return;
}
GVSet("LBT",curTime[0]);
}
}
}
if(sellSig && !buyCloseSig && !buySig){
if((_sellCount==-1 || cntSell<_sellCount) && (!oneSide || cntBuy==0)){ if(GVGet("LST")!=curTime[0]){
if(!OpenSell()){ return;
}
GVSet("LST",curTime[0]);
}
}
}
}
}
}
lstTime=curTime[0];
}

if(useTrailing){ Trailing();
}

if(useBreakeven){ Breakeven();
}
}

Рассмотрим все вспомогательные функции, вызываемые из функции OnTick().

Функция Signals():

bool Signals(bool & aBuySig,
bool & aSellSig, bool & aBuyCloseSig, bool & aSellCloseSig

){
//…
}

В функцию по ссылке передается четыре переменных для торговых сигналов, сама функция возвращает true/false в зависимости от успешности ее работы. Параметры: aBuySig – сигнал на открытие покупки, aSellSig – сигнал на открытие продажи, aBuyCloseSig – сигнал на покупку для закрытия продаж, aSellCloseSig – сигнал на продажу для закрытия покупок.

В самом начале функции «обнуляем» переменные для сигналов:

aBuySig=false; aSellSig=false; aBuyCloseSig=false; aSellCloseSig=false;

Объявляем массивы для получения данных функциями CopyBuffer() и т.п. функциями:

double arb[1],ars[1]; double ind1[1],ind2[1];

Можно было ограничиться всего двумя массивами, но так будет проще ориентироваться в коде. Массивы arb и ars будут использоваться для копирования данных основного индикатора, а массивы ind1 и ind2 – для всех остальных индикаторов. Современные объемы оперативной памяти позволяют писать код, не особо беспокоясь о ее экономии.

Далее объявляем вспомогательные переменные для сигналов основного индикатора:

bool rsiBuy=false; bool rsiSell=false;

Копируем данные основного индикатора:

if(CopyBuffer(h_rsi,0,shift,1,arb)==-1 || CopyBuffer(h_rsi,2,shift,1,ars)==-1
){
return(false);
}

В индикаторе два цветных стрелочных буфера. Сначала нужно определить только факт наличия стрелки, поэтому выполняется копирование буферов 0 и 2. Наличие стрелки определяется неравенством скопированного значения пустому значению и нулю. С данным индикатором можно было ограничиться только проверкой на пустое значение. Используя же проверку сразу на пустое значение и на ноль можно не тратить время на изучение индикатора, в подавляющем большинстве случаев это будет работать. Тем не менее, следуем иметь в виду, что встречаются пользовательские индикаторы с отрицательными числами в качестве пустого значения, например, может использоваться значение -1. После обнаружения стрелки проверятся индекс цвета, для этого копируется данные буфера 1 или 3. После этого скопированное значение используется в логическом выражении, результат которого присваивается вспомогательной переменной. Используется два разных выражения с разными значениями индекса цвета, в зависимости от значения внешнего параметра arrowsMain:

if(arb[0]!=EMPTY_VALUE && arb[0]!=0){ if(CopyBuffer(h_rsi,1,shift,1,arb)==-1){
return(false);

}
if(arrowsMain){ rsiBuy=(arb[0]==0);
}
else{
rsiBuy=(arb[0]==1);
}
}

Аналогичный код для сигнала на продажу:

if(ars[0]!=EMPTY_VALUE && ars[0]!=0){ if(CopyBuffer(h_rsi,3,shift,1,ars)==-1){
return(false);
}
if(arrowsMain){ rsiSell=(ars[0]==0);
}
else{
rsiSell=(ars[0]==1);
}
}

Далее проверяется индикатор ADX. Используется основная линия, показывающая силу тренда. Данный сигнал не связан с направлением торговли, а только показывает, разрешена ли торговля. Поэтому объявляется одна переменная. Она инициализируются значением true:

bool adxAllow=true;

Дело в том, что после получения всех отдельных сигналов, итоговые сигнал будет рассчитываться как их логическое «И». Поэтому, если дополнительные проверки будут отключены, итоговый сигнал будет определяться только основным индикатором, для этого все остальные составляющие логического выражения должны равняться true.

Если проверка индикатора ADX включена, то выполняется следующий код, в котором вспомогательная переменная сначала «обнуляется», затем копируются данные индикатора и вычисляется значение сигнала:

bool adxAllow=true; if(useADX){
adxAllow=false; if(CopyBuffer(h_adx,MAIN_LINE,shift,1,ind1)==-1){
return(false);
}
adxAllow=(ind1[0]>adxLevel);
}

Поскольку переменой adxAllow присваивается результат вычисления логического выражения, то ее предварительное «обнуление» является избыточным. Тем не менее, не рекомендуется удалять лишний код раньше времени. Не всегда сразу понятно, какой код будет использоваться для определения торгового сигнала. Если бы использовался оператор if, то предварительное «обнуление» было бы обязательным:

if(ind1[0]>adxLevel){ adxAllow=true;
}

Далее проверяем положения линий PDI и MDI. Этот сигнал имеет направление, поэтому объявляется две вспомогательные переменные с инициализацией значением true. Если включена данная проверка, выполняется копирование данных индикатора и вычисление сигнала:

bool pdimdiBuy=true; bool pdimdiSell=true; if(usePDIMDI){
pdimdiBuy=false; pdimdiSell=false;
if(CopyBuffer(h_adx,PLUSDI_LINE,shift,1,ind1)==-1 || CopyBuffer(h_adx,MINUSDI_LINE,shift,1,ind2)==-1
){
return(false);
}
pdimdiBuy=(ind1[0]>ind2[0]); pdimdiSell=(ind1[0]<ind2[0]);
}

Подобным образом определяется подтверждающий сигнал по положению цены относительно скользящей средней:

bool maBuy=true; bool maSell=true; if(useMA){
maBuy=false; maSell=false;
if(CopyBuffer(h_ma,0,shift,1,ind1)==-1){ return(false);
}
if(CopyClose(Symbol(),_timeFrame,shift,1,ind2)==-1){ return(false);
}
maBuy=(ind2[0]>ind1[0]); maSell=(ind2[0]<ind1[0]);
}

Имея сигналы отдельных индикаторов, получаем итоговый торговый сигнал:

aBuySig=(rsiBuy && adxAllow && pdimdiBuy && maBuy); aSellSig=(rsiSell && adxAllow && pdimdiSell && maSell);

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

if(closeRev){ aBuyCloseSig=aBuySig; aSellCloseSig=aSellSig;
}

Примерно также делаем для сигналов основного индикатора, но теперь обязательно используется оператор if, потому что, если какая-то переменная для сигналов уже имеет значение true, его нужно сохранить:

if(closeRSI){ if(rsiBuy){
aBuyCloseSig=true;
}

if(rsiSell){ aSellCloseSig=true;
}
}

Далее проверка сигнала закрытия по линиям PDI и MDI индикатора ADX:

if(closePDIMDI){ if(CopyBuffer(h_adx,PLUSDI_LINE,shift,1,ind1)==-1 ||
CopyBuffer(h_adx,MINUSDI_LINE,shift,1,ind2)==-1
){
return(false);
}
if(ind1[0]>ind2[0])aBuyCloseSig=true; if(ind1[0]<ind2[0])aSellCloseSig=true;
}

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

Проверка закрытия по цене и скользящей средней:

if(closeMA){ if(CopyBuffer(h_ma,0,shift,1,ind1)==-1){
return(false);
}
if(CopyClose(Symbol(),_timeFrame,shift,1,ind2)==-1){ return(false);
}
if(ind2[0]>ind1[0])aBuyCloseSig=true; if(ind2[0]<ind1[0])aSellCloseSig=true;
}

В конце функции возвращаем true:

return(true);

Ниже приведен весь код функции Signals():

bool Signals(bool & aBuySig,
bool & aSellSig, bool & aBuyCloseSig, bool & aSellCloseSig

){

aBuySig=false; aSellSig=false; aBuyCloseSig=false; aSellCloseSig=false;

double arb[1],ars[1]; double ind1[1],ind2[1];

bool rsiBuy=false; bool rsiSell=false;

if(CopyBuffer(h_rsi,0,shift,1,arb)==-1 || CopyBuffer(h_rsi,2,shift,1,ars)==-1

){
return(false);
}

if(arb[0]!=EMPTY_VALUE && arb[0]!=0){ if(CopyBuffer(h_rsi,1,shift,1,arb)==-1){
return(false);
}
if(arrowsMain){ rsiBuy=(arb[0]==0);
}
else{
rsiBuy=(arb[0]==1);
}
}
if(ars[0]!=EMPTY_VALUE && ars[0]!=0){ if(CopyBuffer(h_rsi,3,shift,1,ars)==-1){
return(false);
}
if(arrowsMain){ rsiSell=(ars[0]==0);
}
else{
rsiSell=(ars[0]==1);
}
}

//===
bool adxAllow=true; if(useADX){
adxAllow=false; if(CopyBuffer(h_adx,MAIN_LINE,shift,1,ind1)==-1){
return(false);
}
adxAllow=(ind1[0]>adxLevel);
}

bool pdimdiBuy=true; bool pdimdiSell=true; if(usePDIMDI){
pdimdiBuy=false; pdimdiSell=false;
if(CopyBuffer(h_adx,PLUSDI_LINE,shift,1,ind1)==-1 || CopyBuffer(h_adx,MINUSDI_LINE,shift,1,ind2)==-1
){
return(false);
}
pdimdiBuy=(ind1[0]>ind2[0]); pdimdiSell=(ind1[0]<ind2[0]);
}

//===

bool maBuy=true; bool maSell=true; if(useMA){
maBuy=false; maSell=false;

if(CopyBuffer(h_ma,0,shift,1,ind1)==-1){ return(false);
}
if(CopyClose(Symbol(),_timeFrame,shift,1,ind2)==-1){ return(false);
}
maBuy=(ind2[0]>ind1[0]); maSell=(ind2[0]<ind1[0]);
}

//===

aBuySig=(rsiBuy && adxAllow && pdimdiBuy && maBuy); aSellSig=(rsiSell && adxAllow && pdimdiSell && maSell);

//===

if(closeRev){ aBuyCloseSig=aBuySig; aSellCloseSig=aSellSig;
}

if(closeRSI){ if(rsiBuy){
aBuyCloseSig=true;
}
if(rsiSell){ aSellCloseSig=true;
}
}

if(closePDIMDI){ if(CopyBuffer(h_adx,PLUSDI_LINE,shift,1,ind1)==-1 ||
CopyBuffer(h_adx,MINUSDI_LINE,shift,1,ind2)==-1
){
return(false);
}
if(ind1[0]>ind2[0])aBuyCloseSig=true; if(ind1[0]<ind2[0])aSellCloseSig=true;
}

if(closeMA){ if(CopyBuffer(h_ma,0,shift,1,ind1)==-1){
return(false);
}
if(CopyClose(Symbol(),_timeFrame,shift,1,ind2)==-1){ return(false);
}
if(ind2[0]>ind1[0])aBuyCloseSig=true; if(ind2[0]<ind1[0])aSellCloseSig=true;
}

return(true);

}

Функция CountPositions():

bool CountPositions(int & aCntBuy,int & aCntSell){
//…
}

В функцию по ссылке передается две переменные: aCntBuy – для количества позиций на покупку,

aCntSell – для количества позиций на продажу. В начале функции эти переменные обнуляются:

aCntBuy=0; aCntSell=0;

Далее выполняются различные участки кода в зависимости от типа счета. На неттинговом счете:

if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
if(p.Select(Symbol())){ if(p.PositionType()==POSITION_TYPE_BUY){
aCntBuy=1;
}
else if(p.PositionType()==POSITION_TYPE_SELL){ aCntSell=1;
}
}
}

На хеджинговом счете используется цикл с проверкой успешности выделения позиции, а также с проверкой символа и магического номер:

else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ for(int i=PositionsTotal()-1;i>=0;i--){
if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()%1000==magicNum){ if(p.PositionType()==POSITION_TYPE_BUY){
aCntBuy++;
}
else if(p.PositionType()==POSITION_TYPE_SELL){ aCntSell++;
}
}
}
else{
return(false);
}
}
}

Обратите внимание, от фактического магического номера отделяются три последних знака, и только они проверяются на равенство с внешним параметром magicNum.

В конце функции возвращаем true:

return(true);

Функция CloseAll(). В функцию передается две переменных с сигналами на закрытие:

bool CloseAll(bool aBuyCloseSig, bool aSellCloseSig){
//…
}

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

bool rv=true;

Далее будет выполняться различный код в зависимости от типа счета. На неттинговом счете:

if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
if(p.Select(Symbol())){ if(!t.PositionClose(Symbol())){
rv=false;
}
}
}

Обратите внимание, закрытие выполняется только в том случае, если позицию удалось выделить, то есть, если она существует. Если позиции не существует, метод PositionClose() класса CTrade возвращает false, но ошибкой это не является. В данном эксперте перед вызовом функции закрытия выполняется проверка существования позиции, и в этом выделении нет необходимости, но во многих других случаях оно пригодится.

Для хеджингового счета:

else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ for(int i=PositionsTotal()-1;i>=0;i--){
if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()%1000==magicNum){ if(aSellCloseSig && p.PositionType()==POSITION_TYPE_BUY){
if(!t.PositionClose(p.Ticket())){ rv=false;
}
}
else if(aBuyCloseSig && p.PositionType()==POSITION_TYPE_SELL){ if(!t.PositionClose(p.Ticket())){
rv=false;
}
}
}
}
else{
rv=false;
}
}
}

В конце возвращаем переменную rv:

return(rv);

Функции GVSet(), GVGet(), GVCheck() вам уже должны быть знакомы, к ним добавляется функция для удаления глобальной переменной терминала – GVDel():

void GVSet(string name,double value){ GlobalVariableSet(prefix+name,value);
}

double GVGet(string name){ return(GlobalVariableGet(prefix+name));
}

double GVCheck(string name){ return(GlobalVariableCheck(prefix+name));
}

double GVDel(string name){ return(GlobalVariableDel(prefix+name));
}

Функция OpenBuy(). В функции выполняются подготовительные действия, необходимые для открытия позиции – вызываются функции для вычисления стоплосса и тейкпрофита, лота, выполняется открытие позиции и, если необходимо, вызывается функции для установки стоплосса и тейкпрофита. Сначала объявляются переменные, которые будут нужны в этой функции:

double sl,tp,lt,sl2,tp2; int index;

Переменные sl, tp, lt предназначены для расчетных значений стоплосса, тейкпрофита и лота. Переменные sl2 и tp2 – для стоплосса и тейпрофита в ордере на открытие позиции. Переменная index будет использоваться для номера лота в ряду ее прогрессии (для функции умножения лота).

Прежде чем вычислять стоплосс и тейпрофит выполняем обновление рыночных данных:

if(!s.RefreshRates()){ return(false);
}

Вычисление стоплосса и тейкпрофита выполняется в отдельных функциях. В эти функции по ссылке передаются переменные sl и tp:

SolveBuySL(sl);
SolveBuyTP(tp);

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

Вычисление лота выполняется в функции SolveLot(). Функция возвращает true/false в зависимости от успешности ее работы, а в функцию по ссылке передается две переменные для получения лота и его номера в ряду прогресси лота:

if(!SolveLot(lt,index)){ return(false);
}

Если в соответствии с внешним параметром sltpInOrder позиция может открываться сразу со стоплоссом и тейкпрофитом, переменным sl2 и tp2 присваиваем расчетные значения, иначе – нули:

if(sltpInOrder){ sl2=sl; tp2=tp;
}
else{
sl2=0;

tp2=0;
}

Рассчитываем и устанавливаем магический номер: t.SetExpertMagicNumber(1000*index+magicNum); Открываем позицию:

bool rv=t.Buy(lt,Symbol(),s.Ask(),sl2,tp2);

Если позиция открылась (переменная rv имеет значение true) и необходимо установить стоплосс и тейпрофит, то создаются глобальные переменные терминала для стлпосса и тейпрофита и вызывается функция setSLTP() в которой выполняется установка стоплосса и тейприта:

if(rv && !sltpInOrder){ MqlTradeResult res; t.Result(res);
GVSet((string)res.order+"_sl_",sl); GVSet((string)res.order+"_tp_",tp); if(!SetSLTP()){
setSLTP=true;
}
}

Рассмотрим эту часть кода более подробно. Объявляется переменная res (структура) и передается по ссылке в метод Result() торгового класса, после этого структура содержит результаты выполнения торгового запроса. Далее создается две глобальных переменных терминала, имена которых включают тикет только что отрытой позиции. После этого вызывается функция SetSLTP(), если функция отработала неудачно, переменной setSLTP устанавливается значение true — от этого на следующем тике будет выполнена повторная попытка вызова функции SetSLTP(). На этом можно бы закончить функцию, но было замечено значительное снижение скорости тестирования советника в тестере, когда при использовании функции умножения лота заканчивались средства. Поэтому добавлена проверка, если в тестере позиция не открывается по причине нехватки средств, то тестирование завершается (через вызов функции ExpertRemove()):

if(!rv){
if(MQLInfoInteger(MQL_TESTER)){ if(t.ResultRetcode()==10019){
Print("Тестирование завершено из-за отсутствия средств"); ExpertRemove();
}
}
}

После этого возвращаем из функции переменную rv:

return(rv);

Ниже приведен весь код функций Buy() и Sell():

bool OpenBuy(){

double sl,tp,lt,sl2,tp2; int index;

if(!s.RefreshRates()){ return(false);
}

SolveBuySL(sl);
SolveBuyTP(tp);

if(!SolveLot(lt,index)){ return(false);
}

if(sltpInOrder){ sl2=sl; tp2=tp;
}
else{
sl2=0; tp2=0;
}

t.SetExpertMagicNumber(1000*index+magicNum);
bool rv=t.Buy(lt,Symbol(),s.Ask(),sl2,tp2); if(rv && !sltpInOrder){
MqlTradeResult res; t.Result(res);
GVSet((string)res.order+"_sl_",sl); GVSet((string)res.order+"_tp_",tp); if(!SetSLTP()){
setSLTP=true;
}
}

if(!rv){
if(MQLInfoInteger(MQL_TESTER)){ if(t.ResultRetcode()==10019){
Print("Тестирование завершено из-за отсутствия средств"); ExpertRemove();
}
}
}

return(rv);
}

bool OpenSell(){

double sl,tp,lt,sl2,tp2; int index;

if(!s.RefreshRates()){ return(false);
}

SolveSellSL(sl);
SolveSellTP(tp);

if(!SolveLot(lt,index)){ return(false);
}

if(sltpInOrder){ sl2=sl; tp2=tp;
}
else{
sl2=0; tp2=0;
}

t.SetExpertMagicNumber(1000*index+magicNum);
bool rv=t.Sell(lt,Symbol(),s.Bid(),sl2,tp2); if(rv && !sltpInOrder){
MqlTradeResult res; t.Result(res);
GVSet((string)res.order+"_sl_",sl); GVSet((string)res.order+"_tp_",tp); if(!SetSLTP()){
setSLTP=true;
}
}

if(!rv){
if(MQLInfoInteger(MQL_TESTER)){ if(t.ResultRetcode()==10019){
Print("Тестирование завершено из-за отсутствия средств"); ExpertRemove();
}
}
}

return(rv);
}

Рассмотрим функции расчета стоплосса и тейпрофита. Стоплосс для покупки:

void SolveBuySL(double & aValue){ if(_stopLoss==0){
aValue=0;
}
else{
aValue=s.NormalizePrice(s.Ask()-s.Point()*_stopLoss);
}
}

Если во внешних параметрах эксперта стоплосс не задан (переменная _stopLoss равна нулю), переменой aValue присваивается 0, иначе выполняется расчет от цены ask. Остальные функции аналогичны, отличие только в вычислении значения. Расчет тейкпрфита при покупке:

void SolveBuyTP(double & aValue){ if(_takeProfit==0){
aValue=0;
}
else{
aValue=s.NormalizePrice(s.Ask()+s.Point()*_takeProfit);
}
}

Расчет стоплосса при продаже:

void SolveSellSL(double & aValue){ if(_stopLoss==0){
aValue=0;
}
else{
aValue=s.NormalizePrice(s.Bid()+s.Point()*_stopLoss);
}
}

Расчет тейкпрофита при продаже:

void SolveSellTP(double & aValue){ if(_takeProfit==0){
aValue=0;
}
else{
aValue=s.NormalizePrice(s.Bid()-s.Point()*_takeProfit);
}
}

Теперь самая сложная часть эксперта – вычисление лота – функцией SolveLot():

bool SolveLot(double & aValue,int & aIndex){
//…
}

Если умножение лота после убытка не используется (внешний параметр useLotMult равен false), переменной aValue всегда присваивается величина, возвращаемая функцией SolveStartLot(), а переменной aIndex значение 1. Функция SolveStartLot() через параметр по ссылке возвращает значение внешнего параметра lot, или величину, рассчитанную с учетом параметра riskPerc.

Если же включено умножение, то в функции SolveLot() выполняется анализ истории сделок. Для этого историю необходимо выделить:

if(!HistorySelect(0,TimeCurrent())){ return(false);
}

Из истории сделок надо получить номер последней сделки, ее прибыль и лот. Для этих значений используются переменные:

int lastIndex=0; double lastProfit=0; double lastLot=0;

В цикле от конца истории сделок к началу находим последнюю закрывающую сделку (тип DEAL_ENTRY_OUT) с соответствующим символом и магическим номером, ее параметры присваиваются переменным и выполняется завершение цикла:

for(int i=HistoryDealsTotal()-1;i>=0;i--){ if(d.SelectByIndex(i)){
if(d.Symbol()==Symbol() && d.Magic()%1000==magicNum){ if(d.Entry()==DEAL_ENTRY_OUT){
lastIndex=(int)((d.Magic()/1000)%1000); lastProfit=d.Profit()+d.Swap()+d.Commission()*2.0; lastLot=d.Volume();
break;
}

}
}
else{
return(false);
}
}

В данном случае можно было бы обойтись и без проверки на тип сделки DEAL_ENTRY_OUT, поскольку у этого эксперта данный код работает только при отсутствии открытых сделок. Тем не менее, выполнение некоторых хоть и избыточных, но разумных проверок, может избавить от проблем в будущем при выполнении модернизации эксперта.

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

Также из истории надо получить величину начального лота и общую прибыль всего ряда лотов. Если бы эксперт работал только с постоянным лотом (заданным параметром lot), то можно было бы использовать его значение. А вот с лотом пропорциональным средствам не остается другого варианта, как искать его в истории. Да и с постоянным лотом не исключено изменение параметров эксперта, например, при затяжной убыточной серии пользователь решил, что следующую серию надо начинать с уменьшенного лота, и изменил параметр. Так что, в любом случае начальный лот лучше искать в истории сделок. Для этого потребуются две переменные: одна для лота, вторая для прибыли. Переменой для лота на всякий случай присвоим новое расчетное значение начального лота:

double startLot=SolveStartLot(); double totalProfit=0;

В цикле по сделкам ищем начальный лот и считаем прибыль:

for(int i=HistoryDealsTotal()-1;i>=0;i--){ if(d.SelectByIndex(i)){
if(d.Symbol()==Symbol() && d.Magic()%1000==magicNum){ if(d.Entry()==DEAL_ENTRY_IN){
if((d.Magic()/1000)%1000==1){
startLot=d.Volume(); break;
}
}
else if(d.Entry()==DEAL_ENTRY_OUT){ totalProfit+=d.Profit()+d.Swap()+d.Commission()*2.0;
}
}
}
else{
return(false);
}
}

Начальный лот ищется у сделок типа DEAL_ENTRY_IN, а прибыль считается по сделкам DEAL_ENTRY_OUT.

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

lastProfit=NormalizeDouble(lastProfit,2); totalProfit=NormalizeDouble(totalProfit,2);

Если общая прибыль всей последовательности положительна или хотя бы нулевая, начинается новая последовательность:

if(totalProfit>=0){ aValue=SolveStartLot(); aIndex=1;
}

Иначе выполняется следующий код:

else{
if(lastProfit>=0){ aValue=lastLot; aIndex=lastIndex;
}
else{
if(lastIndex<=multCount || multCount==-1){ aValue=LotsNormalize(Symbol(),startLot*MathPow(lotK,lastIndex)); aIndex=lastIndex+1;
}
else{
aValue=lastLot; aIndex=lastIndex;
}
}
}

Разберем его подробно. Если последняя сделка не является убыточной, то используется ее лот и номер. Если последняя сделка убыточна и не достигнута предельная длина прогрессии лота (или длина прогрессии не ограничена), то производится вычисление нового лота, а номер увеличивается на 1. Если же предельная длина прогрессии достигнута, используется лот и номер последней сделки. Конечно, данный участок кода можно было написать более компактно, но сделано так, чтобы он был более нагляден при изучении.

Ниже приведен весь код функции SolveLot():

bool SolveLot(double & aValue,int & aIndex){ if(useLotMult){
if(!HistorySelect(0,TimeCurrent())){ return(false);
}

int lastIndex=0; double lastProfit=0; double lastLot=0;

for(int i=HistoryDealsTotal()-1;i>=0;i--){ if(d.SelectByIndex(i)){
if(d.Symbol()==Symbol() && d.Magic()%1000==magicNum){ if(d.Entry()==DEAL_ENTRY_OUT){

lastIndex=(int)((d.Magic()/1000)%1000); lastProfit=d.Profit()+d.Swap()+d.Commission()*2.0; lastLot=d.Volume();
break;
}
}
}
else{
return(false);
}
}

double startLot=SolveStartLot(); double totalProfit=0;

for(int i=HistoryDealsTotal()-1;i>=0;i--){ if(d.SelectByIndex(i)){
if(d.Symbol()==Symbol() && d.Magic()%1000==magicNum){ if(d.Entry()==DEAL_ENTRY_IN){
if((d.Magic()/1000)%1000==1){
startLot=d.Volume(); break;
}
}
else if(d.Entry()==DEAL_ENTRY_OUT){ totalProfit+=d.Profit()+d.Swap()+d.Commission()*2.0;
}
}
}
else{
return(false);
}
}

lastProfit=NormalizeDouble(lastProfit,2); totalProfit=NormalizeDouble(totalProfit,2);

if(totalProfit>=0){ aValue=SolveStartLot(); aIndex=1;
}
else{
if(lastProfit>=0){ aValue=lastLot; aIndex=lastIndex;
}

else{
if(lastIndex<=multCount || multCount==-1){ aValue=LotsNormalize(Symbol(),startLot*MathPow(lotK,lastIndex)); aIndex=lastIndex+1;
}
else{
aValue=lastLot; aIndex=lastIndex;
}
}
}
}
else{

aValue=SolveStartLot(); aIndex=1;
}

return(true);
}

Функция SolveStartLot():

double SolveStartLot(){ if(useRisk){
return(LotsNormalize(Symbol(),(ai.FreeMargin()/1000)*(riskPerc/100)));
}
else{
return(lot);
}
}

Вычисление лота пропорционально средствам основывается на предположении, что на один лот необходимы средства в объеме 1000 единиц валюты депозита, поэтому величина свободных средств делится на 1000.

Функция нормализации лота :

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);
}

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

bool SetSLTP(){ bool rv=true;
if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
if(p.Select(Symbol())){ if(!SetSLTPSelected()){
rv=false;
}
}
}
else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ for(int i=PositionsTotal()-1;i>=0;i--){
if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()%1000==magicNum){ if(!SetSLTPSelected()){
rv=false;
}

}
}
else{
rv=false;
}
}
}
return(rv);
}

Выделение позиции выполняется по-разному на неттинговом и хеджинговом счетах, дальнейшая работа с выделенной позицией выполняется в функции SetSLTPSelected().

Функция SetSLTPSelected(). Сначала в функции формируются имена глобальных переменных:

string sln=(string)p.Ticket()+"_sl_"; string tpn=(string)p.Ticket()+"_tp_";

Проверяется их существование:

if(GVCheck(sln) && GVCheck(tpn)){
//…
}

Если они существуют, выполняется следующий код (располагается внутри вышеприведенного условия if). Выполняется обновление рыночных данных:

if(!s.Refresh()){ return(false);
}

Выполняется получений значений стоплосса и тейкпрофита. Стоплосс:

double sl=GVGet(sln);

Тейкпрофит:

double tp=GVGet(tpn);

Далее, в зависимости от направления позиции, выполняется коррекция значений по минимальному стопуровню:

if(p.PositionType()==POSITION_TYPE_BUY){ sl=MathMin(sl,s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1))); tp=MathMax(tp,s.NormalizePrice(s.Ask()+s.Point()*(s.StopsLevel()+1)));
}
else if(p.PositionType()==POSITION_TYPE_SELL){ sl=MathMax(sl,s.NormalizePrice(s.Ask()+s.Point()*(s.StopsLevel()+1))); tp=MathMin(tp,s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1)));
}

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

Модификация позиции:

if(!t.PositionModify(p.Ticket(),sl,tp)){ return(false);
}

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

GVDel(sln);
GVDel(tpn);

Ниже приведен весь код функции SetSLTPSelected():

bool SetSLTPSelected(){

string sln=(string)p.Ticket()+"_sl_"; string tpn=(string)p.Ticket()+"_tp_";

if(GVCheck(sln) && GVCheck(tpn)){

if(!s.Refresh()){ return(false);
}

double sl=GVGet(sln); double tp=GVGet(tpn);

if(p.PositionType()==POSITION_TYPE_BUY){ sl=MathMin(sl,s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1))); tp=MathMax(tp,s.NormalizePrice(s.Ask()+s.Point()*(s.StopsLevel()+1)));
}
else if(p.PositionType()==POSITION_TYPE_SELL){ sl=MathMax(sl,s.NormalizePrice(s.Ask()+s.Point()*(s.StopsLevel()+1))); tp=MathMin(tp,s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1)));
}
if(!t.PositionModify(p.Ticket(),sl,tp)){ return(false);
}

GVDel(sln);
GVDel(tpn);
}

return(true);

}

На этом эксперт практически готов, остались функции сопровождения – трейлингстоп и безубыток, а также функция OnTester().

Работа трейлингстопа, как и установка стоплосса и тейкпрофита, обеспечивается двумя функциями. В функции Trailing() выполняется выделении позиций, а в функции TrailingSelected() их модификация. Функция трейлинга может работать как на каждом тике, таки раз на бар, для этого в начале функции объявлена статическая переменная для времени бара:

static datetime lastTime=0;

Затем выполняется проверка, включена ли работа на каждом тике или время бара не равно времени переменой lastTime:

if(trEachTick || curTime[0]!=lastTime){
//…
}

Весь следующий код располагается внутри вышеприведенного оператора if. Если при работе функции трейлинга происходит ошибка выделения позиции или неудачная попытка модификации, то необязательно прерывать работу всей функции. Если идет работа на каждом тике, то на следующем тике будет повторная попытка. При работе раз на бар необходимо принять меры для обеспечения повторной работы функции на следующем тике. Для этого используется переменная для ошибки:

bool er=false;

Далее выполняется выделение позиций (разный код для неттинговых и хеджинговых счетов) и вызов функции TrailingSelected(). Если в части кода для хеджинговго счете произошла ошибка выделения, переменной er присваивается значение true. Так же переменной er присваивается true при ошибке модификация функцией TrailingSelected():

if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_NETTING){
if(p.Select(Symbol())){ if(!TrailingSelected()){
er=true;
}
}
}
else if(ai.MarginMode()==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ for(int i=PositionsTotal()-1;i>=0;i--){
if(p.SelectByIndex(i)){
if(p.Symbol()==Symbol() && p.Magic()%1000==magicNum){ if(!TrailingSelected()){
er=true;
}
}
}

else{
er=true;
}
}
}

В конце проверяем переменную er, если она равна false, значит, ошибок не было, поэтому обновляем значение переменной lastTime, чтобы на этом баре больше не было попыток выполнения модификации:

if(!er){
lastTime=curTime[0];
}

В функции TrailingSelected() сначала выполняется обновление рыночных данных:

if(!s.RefreshRates()){ return(false);
}

Далее объявляется две вспомогательных переменных:

double newStopLoss; double tmp;

В переменной newStopLoss будет находиться значение стоплосса, соответствующее текущей ситуации, а переменная tmp будет использоваться для различных вспомогательных расчетов.

Проверяем тип позиции:

if(p.PositionType()==POSITION_TYPE_BUY){
//…
}
else if(p.PositionType()==POSITION_TYPE_SELL){
//…
}

Рассмотрим подробно часть код для позиции на покупку (должен располагаться в первой части вышеприведенного условия if). Сначала вычисляется новое значение стоплосса, для этого от текущей рыночной цены bid вычитается величина соответствующая параметру trLevel:

newStopLoss=s.NormalizePrice(s.Bid()-s.Point()*trLevel);

Используется цена bid, потому что закрытие покупки выполняется по цене bid. Параметр trLevel умножается на величину пункта, поскольку он задан в пунктах, а надо вычислить ценовой уровень. Результат расчетов нормализуется.

Вычисляется минимально близкий уровень стоплосса (с запасом в 1 пункт): tmp=s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1)); Расчетный уровень стоплосса корректируется по минимальному уровню: newStopLoss=MathMin(newStopLoss,tmp);Теперь в переменной newStopLoss находится уровень, на который гарантированно можно установить стоплосс.

Далее проверяется условие по параметру trStep. К имеющемуся значению сптолосса позиции прибавляется величина шага:

tmp=s.NormalizePrice(p.StopLoss()+s.Point()*trStep);

Выполняется проверка, чтобы новый стоплосс был на дистанции шага или дальше от имеющегося стоплосса:

if(newStopLoss>=tmp){
//…
}

Следующий код располагается внутри вышеприведенного оператора if. Вычисляется минимальная прибыль, которую должна зафиксировать функция трейлинга. Она равно разнице начальной прибыли trStart и уровня trLevel.

tmp=s.NormalizePrice(p.PriceOpen()+s.Point()*(trStart-trLevel));

Если новый стоплосс отрезает минимальную или большую прибыль, то выполняется модификация позиции и сразу возвращается результат:

if(newStopLoss>=tmp){
return (t.PositionModify(p.Ticket(),newStopLoss,p.TakeProfit()));
}

Ниже приведен весь код функции TrailingSelected():

bool TrailingSelected(){ if(!s.RefreshRates()){
return(false);
}

double newStopLoss; double tmp;

if(p.PositionType()==POSITION_TYPE_BUY){

newStopLoss=s.NormalizePrice(s.Bid()-s.Point()*trLevel); tmp=s.NormalizePrice(s.Bid()-s.Point()*(s.StopsLevel()+1)); newStopLoss=MathMin(newStopLoss,tmp); tmp=s.NormalizePrice(p.StopLoss()+s.Point()*trStep);

if(newStopLoss>=tmp){ tmp=s.NormalizePrice(p.PriceOpen()+s.Point()*(trStart-trLevel));
if(newStopLoss>=tmp){
return (t.PositionModify(p.Ticket(),newStopLoss,p.TakeProfit()));
}
}

}
else if(p.PositionType()==POSITION_TYPE_SELL){

newStopLoss=s.NormalizePrice(s.Ask()+s.Point()*trLevel); tmp=s.NormalizePrice(s.Ask()+s.Point()*(s.StopsLevel()+1)); newStopLoss=MathMax(newStopLoss,tmp); tmp=s.NormalizePrice(p.StopLoss()-s.Point()*trStep);

if(newStopLoss<=tmp || p.StopLoss()==0){ tmp=s.NormalizePrice(p.PriceOpen()-s.Point()*(trStart-trLevel)); if(newStopLoss<=tmp){
return (t.PositionModify(p.Ticket(),newStopLoss,p.TakeProfit()));
}
}

}

return(true);
}

Обратите внимание, для позиции на продажу при сравнении переменной newStopLoss с переменной tmp дополнительно выполняется проверка стоплосса на равенство нулю. Дело в том, что если у позиции нет стоплосса, то первая часть выражение никогда не будет равна true, а выполнить модификацию позиции надо.

Особенность данного трейлинга в том, что он всегда будет работать, даже если параметр trLevel

имеет значение менее минимального стопуровня.

Безубыток также реализован двумя функциями. Первая функция Breakeven() идентична функции Trailing() за исключением того, что из нее вместо функции TrailingSelected() вызывается функция BreakevenSelected(). Рассмотрим фрагмент кода функции BreakevenSelected(), обрабатывающий позицию на покупку. Сначала вычисляется уровень требуемого стоплосса:

beStopLoss=s.NormalizePrice(p.PriceOpen()+s.Point()*beProfit);

Выполняется проверка, не находится ли действующий стоплосс ниже требуемого:

if(p.StopLoss()<beStopLoss){
//…
}

Если ли действующий стоплосс находится ниже требуемого, то выполняется следующий код (располагается внутри вышеприведенного оператора if). Выполняется расчет цены, на которой прибыль ордера соответствует параметру beStart:

tmp=s.NormalizePrice(p.PriceOpen()+s.Point()*beStart);

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

if(s.Bid()>=tmp){
tmp=s.NormalizePrice(s.Bid()-s.Point()*s.StopsLevel()); if(beStopLoss<tmp){
return (t.PositionModify(p.Ticket(),beStopLoss,p.TakeProfit()));
}

}

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

Осталось поработать над собственным критерием оптимизации – функцией OnTester(). Сначала выделяем историю:

HistorySelect(0,TimeCurrent());

Поскольку функция OnTester() работает только в тестере, можно особо не беспокоиться о проверке успешности вызова из нее других функций. В тестере нет причин по которым может произойти ошибка выделения история. К тому же второго шанса для выполнения функции OnTester() нет.

Объявляем вспомогательные переменные:

int index; int cnt=0; int mx=0;

Переменная index будет использоваться для номера лота при его увеличении после убытка. Переменная cnt – для подсчета количества позиций в одной последовательности лотов, а в переменной mx будет фиксироваться максимальное значение переменной cnt.

В цикле проходим по истории сделок от конца к началу. Выбираем только один тип сделок – DEAL_ENTRY_OUT. Извлекаем номер лота в ряду прогрессии, увеличиваем переменную cnt, если обнаружена первая сделка в последовательности лотов, обновляем значение переменной mx и обнуляем переменную cnt:

for(int i=HistoryDealsTotal()-1;i>=0;i--){ if(d.SelectByIndex(i)){
if(d.Entry()==DEAL_ENTRY_OUT){ index=(int)((d.Magic()/1000)%1000); if(index==0)continue;
cnt++; if(index==1){
mx=MathMax(mx,cnt); cnt=0;
}
}
}
}

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

return(mx);

На этом работа над экспертом полностью завершена.

После тестирования увидеть пользовательский критерий оптимизации можно в терминале в окне «Тестер стратегий» во вкладке «Бэктест» в правой колонке (рис. 4).

отчет о тестировании с пользовательским критерием оптимизации

Рис. 4. Фрагмент отчета о тестировании с пользовательским критерием оптимизации

После оптимизации пользовательский критерий можно использовать для сортировки таблицы результатов. Для этого в окне «Тестер стратегий» во вкладке «Оптимизация» из выпадающего списка, расположенного в правом верхнем углу, необходимо выбрать вариант «Максимум пользовательского критерия» (рис. 5).

Выбор критерия сортировке в таблице отчета после оптимизации

Рис. 5. Выбор критерия сортировке в таблице отчета после оптимизации

После этого в колонке «Результат» появятся значения пользовательского критерия. Чтобы изменить порядок сортировки результатов оптимизации, необходимо щелкнуть на заголовке колонки «Результаты».

Простая торговая панель

Рассмотрим создание простой торговой панели. Панель будет включать в себя три кнопки для открытия позиций на покупку, на продажу и для закрытия. Также панель будет иметь текстовое поле для ввода объема сделки с двумя небольшими копками для изменения значения (рис. 6).

Простая торговая панель

Рис. 6. Простая торговая панель

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

Имя файла эксперта «003 Panel». Подключаем файлы с классами:

#include <Trade/AccountInfo.mqh> CAccountInfo ai;

#include <Trade/PositionInfo.mqh> CPositionInfo p;

#include <Trade/Trade.mqh> CTrade t;

Добавляем два внешних параметра для лота и магического номера:

input double lot = 0.1;
input int magicNum = 0;

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

Основной проблемой при разработке данной панели является особенность функционирования графических объектов в тестере. Основная особенность в том, что в тестере невозможно полноценно использовать функцию OnChartEvent(). Из всех графических объектов на действия пользователя в тестере реагирует только кнопка. Собственно поэтому к текстовому полю добавлены кнопки «+» и «-». В тестере значение текстового поля можно будет менять только кнопками, а при работе на счете и кнопками и с клавиатуры.

При работе с графическими объектами приходится программно обращаться к ним из разных мест эксперта, поэтому будет удобно использовать глобальные переменные для их имен. Всего будет использоваться шесть объектов, соответственно, объявляем шесть переменных:

string name_buy,name_sell,name_close; string name_lot,name_plus,name_minus;

Переменная name_buy – для имени кнопки на покупку, name_sell – для кнопки на продажу, name_close – для кнопки закрытия, name_lot – для поля ввода, name_plus – для кнопки увеличения лота, name_minus – для кнопки уменьшения лота.

Потребуется еще пара глобальный переменных – переменная для фактически используемого лота и переменная для количества знаков после запятой у лота:

double _lot; int lotDigits;

Переменная _lot – для величины лота, lotDigits – для количества знаков после запятой у лота. Переменная lotDigits нужна всего лишь для правильного отображения величины лота в текстовом поле. К сожалению, даже нормализованное число иногда отображалось в текстовом поле с хвостом из нулей. Поэтому нужно использовать функцию DoubleToString(), а для нее нужно знать количество знаков после запятой.

В функции OnInit() присваиваем переменной _lot значение внешней переменной lot:

_lot=lot;

Вычисляем количество знаков после запятой у шага изменения лота. Это делается при помощи десятичного логарифма:

lotDigits=(int)MathMax(-MathLog10(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP)),0);

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

Торговому классу устанавливаем магический номер:

t.SetExpertMagicNumber(magicNum);

Формируем имена графических объектов, чтобы они начинались с имени эксперта:

name_buy=MQLInfoString(MQL_PROGRAM_NAME)+"_buy"; name_sell=MQLInfoString(MQL_PROGRAM_NAME)+"_sell"; name_close=MQLInfoString(MQL_PROGRAM_NAME)+"_close";

Создание кнопок выполняется посредством вспомогательной функции CreateButton():

void CreateButton(string name,
int x, int y, int w, int h,
string text, color bgcolor, color txtcolor

){
ObjectCreate(0,name,OBJ_BUTTON,0,0,0); ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_LOWER); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x+w); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y+h); ObjectSetInteger(0,name,OBJPROP_XSIZE,w); ObjectSetInteger(0,name,OBJPROP_YSIZE,h); ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bgcolor); ObjectSetInteger(0,name,OBJPROP_COLOR,txtcolor); ObjectSetString(0,name,OBJPROP_TEXT,text);
}

В функцию передаются следующие параметры: x – координата x левого верхнего угла кнопки, y – координата y, w – ширина кнопки, h – высота, text – текст кнопки, bgcolor – цвет фона кнопки, txtcolor – цвет текста. Панель будет располагаться в правом нижнем углу, поэтому кнопке устанавливается правый нижний угол отсчета координат. Поскольку угол привязки у кнопки не меняется, значит необходимо вычислить ее координаты так, чтобы получился эффект привязки к нижнему правому углу.

Продолжаем с функцией OnInit(), создаем три основных кнопки:

CreateButton(name_close,5,5,90,25,"close",C'220,220,80',C'20,20,20'); CreateButton(name_sell,5,35,90,25,"sell",C'220,80,80',C'20,20,20'); CreateButton(name_buy,5,65,90,25,"buy",C'0,100,200',C'20,20,20');

Размер кнопок 90 на 25 пикселей, отступ с правого и нижнего края по 5 пикселей. Кнопки располагаются снизу вверх с интервалом в 5 пикселей: кнопка закрытия, кнопка продажи и кнопка покупки. Кнопке покупки устанавливается фон синего оттенка, кнопке продажи – красного, кнопке закрытия – желтого. Надписи имеют темно-серый цвет, чтобы не выглядеть очень контрастно.

Формируем имена для текстового поля и кнопок изменения лота:

name_lot=MQLInfoString(MQL_PROGRAM_NAME)+"_lot"; name_plus=MQLInfoString(MQL_PROGRAM_NAME)+"_plus"; name_minus=MQLInfoString(MQL_PROGRAM_NAME)+"_minus";

Создание текстового поля выполняется функцией CreateEdit():

void CreateEdit( string name,
int x, int y, int w, int h,
string text, color bgcolor, color txtcolor
){
ObjectCreate(0,name,OBJ_EDIT,0,0,0); ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_RIGHT_LOWER); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x+w); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y+h); ObjectSetInteger(0,name,OBJPROP_XSIZE,w); ObjectSetInteger(0,name,OBJPROP_YSIZE,h); ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bgcolor); ObjectSetInteger(0,name,OBJPROP_COLOR,txtcolor); ObjectSetString(0,name,OBJPROP_TEXT,text);

if(MQLInfoInteger(MQL_TESTER)){ ObjectSetInteger(0,name,OBJPROP_READONLY,true);
}
else{
ObjectSetInteger(0,name,OBJPROP_READONLY,false);
}
}

Функция практически идентична функции создания кнопки. Отличие в том, что при работе в тестере объекту устанавливается свойство OBJPROP_READONLY, запрещающее редактирование текста, а при работе на счете редактирование разрешается.

Создание текстового поля и кнопок «+» и «-»:

CreateEdit(name_lot,25,95,50,20,DoubleToString(_lot,lotDigits),clrWhite,C'20,20,20'); CreateButton(name_minus,75,95,20,20,"-",clrGray,C'20,20,20'); CreateButton(name_plus,5,95,20,20,"+",clrGray,C'20,20,20');

После создания графических объектов не забываем ускорить перерисовку графика:

ChartRedraw();

В функции OnDeinit() удаляем все графические объекты:

ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw();

В функции OnTick()обеспечим работу основных кнопок. Сначала надо определить существование позиции, для этого воспользуемся функцией CheckPosition() из эксперта «001 RSI Simple»:

bool buyExists, sellExists; if(!CheckPosition(buyExists,sellExists)){
return;

}

Проверяем состояние кнопки на покупку, если кнопка нажата и нет открытых позиций, то выполняем покупку. После этого делаем короткую паузу (функция Pause()) и «отжимаем» кнопку:

if(ObjectGetInteger(0,name_buy,OBJPROP_STATE)){ if(!buyExists && !sellExists){
t.Buy(_lot);
}
Pause(); ObjectSetInteger(0,name_buy,OBJPROP_STATE,false);
}

Пауза нужна для создания эффекта нажатия кнопки, а не щелчка по изображению кнопки. Как работает функция Pause() рассмотрим чуть позже.

Точно такая же проверка кнопок на продажу:

if(ObjectGetInteger(0,name_sell,OBJPROP_STATE)){ if(!buyExists && !sellExists){
t.Sell(_lot);
}
Pause();

ObjectSetInteger(0,name_sell,OBJPROP_STATE,false);
}

Проверка кнопки на закрытие:

if(ObjectGetInteger(0,name_close,OBJPROP_STATE)){ t.PositionClose(Symbol());
Pause(); ObjectSetInteger(0,name_close,OBJPROP_STATE,false);
}

В конце ускоряем перерисовку графика:

ChartRedraw();

Функция Pause(). В тестере в функции выполняется вход в цикл на время 100 миллисекунд, а при работе на счете используется стандартная функция Sleep():

void Pause(){ if(MQLInfoInteger(MQL_TESTER)){
long st=GetTickCount();
while(GetTickCount()>=st && GetTickCount()-st<100);
}
else{
Sleep(100);
}
}

На данном этапе эксперт уже работоспособен, только невозможно менять лот. Проведем доработку для обеспечения этой возможности. Лот должен меняться двумя способами: путем редактирования текстового поля и посредством кнопок «+» и «-».

Изменение лота редактированием текстового поля выполняется в функции OnChartEvent().

Проверяется тип события:

if(id==CHARTEVENT_OBJECT_ENDEDIT){
//…
}

Событие CHARTEVENT_OBJECT_ENDEDIT происходит по окончанию ввода в текстовое поле –

при нажатии клавиши Enter или щелчке мышью за пределами текстового поля.

Следующий код располагается внутри вышеприведенного оператора if. Проверяется имя графического объекта. При событии с графическими объектами, в переменной sparam находится имя объекта:

if(sparam==name_lot){
//…
}

Следующий код располагается внутри вышеприведенного оператора if. В переменную tmp

получаем введенное значение:

string tmp=ObjectGetString(0,name_lot,OBJPROP_TEXT);

Меняем запятую на точку, чтобы в качестве десятичного разделителя пользователь мог использовать оба знака:

StringReplace(tmp,",",".");

Нормализуем значение функцией LotsNormalize():

_lot=LotsNormalize(Symbol(),(double)tmp);

Полученное значение выводим в текстовое поле, чтобы пользователь мог его видеть: ObjectSetString(0,name_lot,OBJPROP_TEXT,DoubleToString(_lot,lotDigits)); Не забываем перерисовку графика:

ChartRedraw();

Код для изменения лота кнопка «+» и «-» располагается в начале функции OnTick(). Если кнопка «+» нажата, выполняется пауза и выполняется изменение лота функцией LotsChange(), после чего кнопка отжимается:

if(ObjectGetInteger(0,name_plus,OBJPROP_STATE)){ Pause();
LotsChange(SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP)); ObjectSetInteger(0,name_plus,OBJPROP_STATE,false);
}

Аналогично для кнопки «-»:

if(ObjectGetInteger(0,name_minus,OBJPROP_STATE)){ Pause();
LotsChange(-SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP)); ObjectSetInteger(0,name_minus,OBJPROP_STATE,false);
}

Функция LotsChange(). В функцию передается величина, на которую надо изменить лот, в одном случае она положительная, в другом отрицательная, а по значению соответствует шагу изменения лота. В функции к действующему лоту прибавляется параметр функции, выполняется нормализация и полученное значение отображается в текстовом поле:

void LotsChange(double delta){
_lot=LotsNormalize(Symbol(),_lot+delta); ObjectSetString(0,name_lot,OBJPROP_TEXT,DoubleToString(_lot,lotDigits)); ChartRedraw();
}

Все основные действия создаваемой панели выполняются в функции OnTick(), при работе в тестере тики поступают часто и возникает впечатления быстрой реакции панели на нажатые кнопки. При работе на счете надо ждать следующего тика, поэтому, для обеспечения мгновенной реакции на нажатия кнопок, вызовем из функции OnChartEvent() функцию OnTick(). Ниже приведен весь код функции OnChartEvent():

void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam)
{
if(id==CHARTEVENT_OBJECT_ENDEDIT){
if(sparam==name_lot){
string tmp=ObjectGetString(0,name_lot,OBJPROP_TEXT); StringReplace(tmp,",",".");

_lot=LotsNormalize(Symbol(),(double)tmp); ObjectSetString(0,name_lot,OBJPROP_TEXT,DoubleToString(_lot,lotDigits)); ChartRedraw();
}
}
OnTick();
}

На этом создание панели закончено.

Несколько завершающих заметок

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

Если эксперт работает на пользовательском индикаторе, откомпилированный файл индикатора должен находиться в папке «Indicators» или в подпапке. При создании экспертов исключительно для себя данная особенность не составляет проблемы. Однако при распространении эксперта, как в виде исходного кода (файла с расширением mql5), так и в виде откомпилированного файла (файла с расширением ex5), нужно распространять и индикатор (достаточно откомпилированного файла индикатора). Если планируется распространять откомпилированный файл эксперта, все пользовательские индикаторы могут быть включены в откомпилированный файл эксперта. Для этого необходимо добавить в эксперт файлы индикаторов как ресурсы, а при вызове функции iCustom() указывать путь к ресурсу. В верхней части файла добавляем ресурс:

#resource "\\Indicators\\Изучение MQL5\\004 RSI Color Signal.ex5"

Заметьте, путь начинается со слеша и указывается имя папки Indicators, а имя файла, добавляемого как ресурс, указывается с расширением.

Вызов индикатора:

int h;
h=iCustom(Symbol(),Period(),"::Indicators\\Изучение MQL5\\004 RSI Color Signal.ex5");

Единственное отличие в том, что теперь путь начинается с двух двоеточий.

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

#property tester_file "test1.txt"; #property tester_file "test2.txt";

При использовании функций CopyBuffer(), CopyRates(), CopyClose() и тому подобных, может возникнуть путница с отсчетом индекса бара. При указании начала копируемых данных, отсчет ведется справа налево, потом слева направо. В языке mql5 есть функция, позволяющая «перевернуть» индексацию массива – ArraySetAsSerias(). Нужно перевернуть индексацию массива, в который выполняется копирование данных, и выполнять копирование с нулевого бара:

double price[]; ArraySetAsSeries(price,true);
if(CopyClose(Symbol(),Period(),0,3,price)!=-1){
Alert(price[2]," ",price[1]," ",price[0]);
}

Чтобы это сработало, массив, в который выполняется копирование, должен быть динамическим, нельзя указывать его размер при объявлении.

Сохранение шаблонов функцией ChartSaveTemplate() можно выполнять не только в папку «Templates», но и в папку «Files». Для этого надо указать соответствующий путь к файлу:

ChartSaveTemplate(0,"\\Files\\test.tpl");

Также можно применять к графику шаблон из папки «Files»:

ChartApplyTemplate(0," \\Files\\test.tpl ");

Файл шаблона, находящийся в папке «Files», можно открыть стандартными средствами языка mq5, обработать и снова применить к графику, что открывает очень интересные и полезные возможности.

При работе с событиями графика, в частности с событиями клавиатуры, имеется возможность отслеживать нажатия сочетаний клавиш. Для этого используется функция TerminalInfoInteger() с соответствующим идентификатором клавиши. Следующий пример кода показывает отслеживание отдельного нажатия клавиши «Z» и в сочетании ее с клавишей «Shift»:

void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam){

if(id==CHARTEVENT_KEYDOWN){
if(lparam==90){ if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT)<0){
Print("Shift+Z");
}
else{
Print("Z");
}
}
}
}

Однако с клавишей «Ctrl» (идентификтар TERMINAL_KEYSTATE_CONTROL) будут работать далеко не все клавиши. Дело в том, что с клавигей «Ctrl» существует очнеь много клавиатурных комбинаций, которые используются для управления графиком, соответственно, эти клавиши не будут доступны.

Кроме выделения истории ордеров и сделок по времени и тикету, существует функция для выделения истории по идентификатору позиции, это функция HistorySelectByPosition(). В функцию передается один параметр – идентификатор позиции. Для открытой позиции идентификатором является ее тикет. Для ордера в истории идентификатор позиции можно узнать функцией HistoryOrderGetInteger() с идентификатором ORDER_POSITION_ID. Для сделки в истории – функцией HistoryDealGetInteger() с идентификатором DEAL_POSITION_ID.

При использовании функции HistorySelectByPosition() следует иметь в виду, если сначала выделить историю по времени, а потом по идентификатору позиции, то выделение по времени будет утеряно. Примене данной функции будет целесообразно, если идентификатор позиции сохранен в переменной. При необходимости работать с историей ордеров или сделок, рациональным подходом будет сначала пройти в цикле по всей истории ордеров или сделок и собрать в массив все идентификаторы позиций. По мере увеличения размера истории, надо добавлять в массив новые идентификаторы. А затем, проходя в цикле по массиву, применять функцию HistorySelectByPosition().