English 中文 Español Deutsch 日本語 Português
MetaEditor: Опираясь на силу шаблонов

MetaEditor: Опираясь на силу шаблонов

MetaTrader 4Примеры | 27 марта 2008, 10:11
6 100 61
MetaQuotes
MetaQuotes


Дайте мне точку опоры, и я переверну Землю.
Архимед

Введение

Язык MQL4 предоставляет много возможностей для многократного использования однажды написанного и отлаженного кода. К этим возможностям относятся:

  • включаемые файлы с расширением mqh. В них можно хранить в исходном виде необходимые функции и константы, которые подключаются к вашему коду с помощью директивы #include.
  • библиотеки функций, которые компилируются как обычные MQL4-программы и подключатся к вашему коду в реальном времени с помощью директивы #import.
  • пользовательские индикаторы, которые дают возможность экономно проводить расчеты на массивах-таймсериях. Вызываются в реальном времени с помощью функции iCustom().

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


Что такое шаблон?

Что такое шаблон в применении к редактору языка MQL4? Шаблоны - это те файлы с расширением mqt, которые лежат в одноименной папке терминала Корневой_каталог_MetaEditor_4/experts/templates/..

На рисунке мы видим 10 таких файлов, основные это:

  • Expert.mqt - шаблон для создания экспертов;
  • Script.mqt - шаблон для создания скриптов;
  • Include.mqt - шаблон для создания скриптов;
  • indicator.mqt - шаблон для создания индикаторов;
  • Library.mqt - шаблон для создания бибиотеки.

Остальные шаблоны (Alligator.mqt и другие) служат для создания индикаторов по подобию тех, что указаны в названии конкретного шаблона. Например, откроем редактором MetaEditor шаблон Library.mqt, для этого необходимо указать тип файлов "Все файлы (*.*)":

Мы увидим, что содержимое файла не очень велико.
<expert>
type=LIBRARY_ADVISOR
</expert>
#header#
#property copyright "#copyright#"
#property link      "#link#"
 
//+------------------------------------------------------------------+
//| My function                                                      |
//+------------------------------------------------------------------+
// int MyCalculator(int value,int value2)
//   {
//    return(value+value2);
//   }
//+------------------------------------------------------------------+

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


<expert>
type=LIBRARY_ADVISOR
</expert>

Очевидно, что строка type=LIBRARY_ADVISOR является указанием редактору, что это шаблон библиотеки. MetaEditor использует необходимый шаблон, основываясь на вашем выборе: советник, пользовательский индикатор, и так далее.


Далее идет подстановочный макрос #header#, который будет в реальности заменен тем именем, которое вы сами зададите при работе "Мастера создания советников".



Например, если вы дали имя эксперту My_Best_Expert_Advisor, то вместо макроса #header# будут сформированы такие строки:

//+------------------------------------------------------------------+
//|                                       My_Best_Expert_Advisor.mq4 |
//|                      Copyright © 2007, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net/|
//+------------------------------------------------------------------+

В этом блоке комментариев мы видим информацию об имени эксперта, авторе, и ссылку на сайт. Все эти данные были в соотвествующих полях "Мастера создания Советника". Следующие строки:

#property copyright "#copyright#"
#property link      "#link#"

содержат макросы #copyright# и #link#, которые также, очевидно, сооотносятся с полями "Мастера создания Советника".


Как этим пользоваться?

Но главный наш интерес состоит в том, что мы можем вставить в шаблон свой собственный код, который будет автоматически генерироваться при каждом создании эксперта. Например, если вы создавали скрипты, то знаете, что скрипты предназначены для однократного запуска на графике. При этом иногда требуется указывать какие-то внешние параметры для алгоритма работы скрипта, и необходима возможность изменять эти параметры в момент запуска скрипта. По умолчанию такой возможности "Мастер создания Советника" не предусматривает. Хотя для этого служит директива компилятору

#property show_inputs

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

<expert>
type=SCRIPT_ADVISOR
</expert>
#header#
#property copyright "#copyright#"
#property link      "#link#"
 
#extern_variables#
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+

и добавим всего одну строчку перед макросом подстановки внешних параметров (#extern_variables#). Получится вот такой код:

<expert>
type=SCRIPT_ADVISOR
</expert>
#header#
#property copyright "#copyright#"
#property link      "#link#"
 
#property show_inputs
 
#extern_variables#
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+

Сохраняем изменения и с помощью "Мастера создания Советника" начинаем писать скрипт. Назовем его TestScript и после нажатия кнопки "Готово"


получим результат:

//+------------------------------------------------------------------+
//|                                                   TestScript.mq4 |
//|                      Copyright © 2007, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net/|
//+------------------------------------------------------------------+
#property copyright "Copyright © 2007, MetaQuotes Software Corp."
#property link      "https://www.metaquotes.net/"
 
#property show_inputs
 
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+

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

#property show_inputs

в каждый новый скрипт, она будет появляться автоматически. А уж закомментировать ее (если она нам не нужна) нам будет гораздо проще, чем набивать каждый раз заново. Добавим вручную только одну строчку, которая содержит некий параметр emptyParam.

//+------------------------------------------------------------------+
//|                                                   TestScript.mq4 |
//|                      Copyright © 2007, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net/|
//+------------------------------------------------------------------+
#property copyright "Copyright © 2007, MetaQuotes Software Corp."
#property link      "https://www.metaquotes.net/"
 
#property show_inputs
 
extern int emptyParam=1;
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+

Скомпилируем его и запустим на графике. Теперь скрипт не запускается сразу, а дает нам возможность изменить внешние параметры.



Это самый простой пример использования шаблонов. Идея состоит в том, чтобы добавить в шаблон те строки кода, которые вам наиболее необходимы при каждом создании новой программына MQL4, и эти строки всегда будут автоматически добавляться каждый раз при работе "Мастера создания Советника".

Важно: все шаблоны перезаписываются при установке терминала MetaTrader 4. Поэтому делайте копии своих шаблонов самостоятельно.


Что это нам дает?

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

  1. Получить значения короткопериодной и длиннопериодной средней.
  2. Проверить факт их пересечения.
  3. Если короткая пробивает длинную среднюю вверх, то купить, со стопом StopLoss=N пунктов.
  4. Если короткая пробивает длинную среднюю вниз, то продать со стопом StopLoss=N пунктов.
  5. Если есть открытая длинная позиция, то закрыть ее по сигналу на продажу.
  6. Если есть открытая короткая позиция, то закрыть ее по сигналу на покупку.

Советник написан, проведена оптимизация. И тут приходит идея добавить новый параметр, например,TakeProfit = S пунктов. Или заменить блок торговых сигналов вместо пересечения средних использовать значения Стохастика. И так далее. Для каждого нового варианта советника нужно немного менять код, и тут вдруг оказывается, что это не так просто. И в какой-то момент выясняется, что нужно заново писать новый советник и добавлять в него новую функциональность. Конечно, опытные экспертописатели набили уже себе шишки на этой стезе и используют свои любимые наработки и подходы. В этом им помогают приемы, перечисленные в самом начале статьи, а именно (повторим):

  • включаемые файлы (инклюдники);
  • библиотеки;
  • пользовательские индикаторы.

Но и они не дают максимальной эффективности без использования шаблонов. Так что же такое шаблон советника, и каким он должен быть? В моем понимании, шаблон советника - это некий универсальный прототип реального советника, лишенный конкретных специфических черт. Фактически, это некий СуперКласс, имеющий несколько чисто виртуальных функций (в терминах объектно-ориентированного программирования) или интерфейс (в терминах языка Java), если можно так выразиться. Шаблон описывает, что делает советник, но не описывает, как он это делает. Поэтому, прежде чем создавать данный шаблон, нам нужно произвести анализ требуемой функциональности. Нам необходимо предварительно создать для себя структуру советника.


Структура советника

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

  1. Блок получения торговых сигналов.
  2. Открытие ордера по рынку или выставление отложенного ордера.
  3. Закрытие открытых и удаление отложенных ордеров.

Мы рассекли общую идею автоматической торговли с помощью программы на три подзадачи. Теперь мы знаем, что нам нужно реализовать некоторую функцию (блок 1), которая будет принимать решения о текущем сигнале: купить, продать или оставаться вне рынка (торгового сигнала нет). Далее (блок 2) мы можем на основании торгового сигнала открыть позицию или выставить отложенный ордер. Почему можем, а не обязаны? Потому что блок открытия позиций принимает во внимание торговые сигналы, но исполнять их не обязан. Например, у нас уже есть открытые ранее позиции, и открытие новых может подвергнуть торговый счет излишнему риску. Ну, и последняя необходимая функциональность советника (блок 3) - закрывать открытые позиции и удалять отложенные приказы на открытие - также является независимой. Мы можем закрывать позиции как по торговому сигналу, так и исходя из других соображений (время удержания позиции, конец торговой сессии и так далее ). Таким образом, разделение логики советника на три отдельные части не выглядит надуманным.


Дальнейшая детализация

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

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

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

Ну и третье - унификация торговых сигналов. Нам требуется строгая систематизация. Раз мы имеем только три вида торговых сигналов, то пусть наш блок выдает нам их строго описанными константами, лучше всего в терминах MQL4:

  • OP_BUY - сигнал на покупку;
  • OP_SELL - сигнал на продажу;
  • OP_BALANCE - нет сигнала, оставаться вне рынка.

Таким образом, первый блок выглядит так:

/**
      1. Trade Signals . Получение торговых сигналов
      a) Каждый тик                       
      б) Каждый бар заданного периода     
      OP_BUY      - в покупку
      OP_SELL     - в продажу
      OP_BALANCE  - нет сигнала
*/

Далее нам необходим блок вычисления важных уровней для открытия новых ордеров или модификации уже существующих. Это - также независимая от остальных частей функциональность.

/**
      2. 
        а) Вычислить SL и TP для каждого открытого ордера
        б) Вычислить OpenPrice, SL, TP, и Lots для нового ордера
        в) Вычислить OpenPrice, SL и TP для каждого отложенного ордера
*/

Вычисление размера позиции Lots здесь не указано, хотя также является параметром. Но возможно, что функцию вычисления размера открываемой позиции мы захотим вынести в отдельный блок, который условно можем назвать блоком управления капиталом (Money Management).

Следующий блок - блок модификаций уровней ордеров. Он также должен быть независимым. Ведь мы можем производить модификации не только уровня StopLoss, но и уровня TakeProfit. Кроме того, эти модификации можно опять-таки проводить с каждым тиком или только по открытию нового бара. Если мы заложем такую гибкость в советник, то в будущем нам будет проще проводить оптимизацию.

/**
      3. 
        а) Модификация каждого открытого ордера на каждый тик (SL и TP)        
        б) Модификация каждого отложенного ордера на каждый тик (OpenPrice, SL и TP)
        в) Модификация каждого открытого ордера на каждом новом баре выбранного периода (SL и TP)
        г) Модификация каждого отложенного ордера на каждом новом баре (OpenPrice, SL и TP)
*/

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

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

/**
      4. 
        а) Закрыть открытый ордер по времени
        б) Закрыть открытый ордер по сигналу
*/

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


/**
      5. 
        а) Удалить отложенный ордер по времени
        б) Удалить отложенный ордер по условию
*/

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

Ну и последний блок: блок открытия позиций и блок выставления отложенных ордеров.

/**
      6. 
        а) Открыть ордер по рынку
        б) Выставить отложенный ордер без ограничения времени
        в) Выставить отложенный ордер с истечением по времени
*/

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

/**
      7. 
        а) Открыть ордер по рынку
        б) Выставить отложенный ордер без ограничения времени
        в) Выставить отложенный ордер с истечением по времени
*/

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

В результате получилась такая структура шаблона советника:

<expert>
  type=EXPERT_ADVISOR
</expert>
#header#
#property copyright "#copyright#"
#property link      "#link#"
 
#extern_variables#
 
//+------------------------------------------------------------------+
//| expert initialization function                                   |
//+------------------------------------------------------------------+
int init()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+
//| expert deinitialization function                                 |
//+------------------------------------------------------------------+
int deinit()
  {
//----
   
//----
   return(0);
  }
//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
  {
//----
 
/**
      1. Trade Signals . Получение торговых сигналов
      a) Каждый тик                       
      б) Каждый бар заданного периода     
      OP_BUY      - в покупку
      OP_SELL     - в продажу
      OP_BALANCE  - нет сигнала
*/
 
 
/**
      2. 
        а) Вычислить SL и TP для каждого открытого ордера
        б) Вычислить OpenPrice, SL, TP, и Lots для нового ордера
        в) Вычислить OpenPrice, SL и TP для каждого отложенного ордера
*/
 
 
/**
      3. 
        а) Модификация каждого открытого ордера на каждый тик (SL и TP)        
        б) Модификация каждого отложенного ордера на каждый тик (OpenPrice, SL и TP)
        в) Модификация каждого открытого ордера на каждом новом баре выбранного периода (SL и TP)
        г) Модификация каждого отложенного ордера на каждом новом баре (OpenPrice, SL и TP)
*/
 
 
/**
      4. 
        а) Закрыть открытый ордер по времени
        б) Закрыть открытый ордер по сигналу
*/
 
/**
      5. 
        а) Удалить отложенный ордер по времени
        б) Удалить отложенный ордер по условию
*/
 
 
/**
      6. 
        а) Открыть ордер по рынку
        б) Выставить отложенный ордер без ограничения времени
        в) Выставить отложенный ордер с истечением по времени
*/
 
//----
   return(0);
  }
//+------------------------------------------------------------------+

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


Приступаем к реализации

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


Функция появления нового бара для заданного таймфрейма

Так как мы оговорили, что в блоке #1 будут выдаваться торговые сигналы либо на каждый тик либо при появлении нового бара на определенном таймфрейме, указанном пользователем, то первым делом нам необходима функция, которая сообщала бы о наступлении события "новый бар на таком-то таймфрейме". Эта функция несложная, выглядеть она будет так:

//+------------------------------------------------------------------+
//|  возвращает признак появления нового бара для указанного периода |
//+------------------------------------------------------------------+
bool isNewBar(int timeFrame)
   {
   bool res=false;
   
   // массив содержит время открытия текущего (нулевого) бара
   // по 7 (семь) таймфреймам
   static datetime _sTime[7];  
   int i=6;
 
   switch (timeFrame) 
      {
      case 1  : i=0; break;
      case 5  : i=2; break;
      case 15 : i=3; break;
      case 30 : i=4; break;
      case 60 : i=5; break;
      case 240: break;
      case 1440:break;
      default:  timeFrame = 1440;
      }
//----
   if (_sTime[i]==0 || _sTime[i]!=iTime(Symbol(),timeFrame,0))
      {
      _sTime[i] = iTime(Symbol(),timeFrame,0);
      res=true;
      }
      
//----
   return(res);   
   }

Задание торгового таймфрейма и проверка на корректность

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

extern int  TradeSignalBarPeriod       = 0;

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

//+------------------------------------------------------------------+
//|  если период указан не корректно, то возвращет ноль              |
//+------------------------------------------------------------------+
int getCorrectTimeFrame(int period)
   {
   int res=period;
//----
   switch (period)
      {
      case PERIOD_D1:  break;  // допустимый период, ничего не делаем
      case PERIOD_H4:  break;  // допустимый период, ничего не делаем
      case PERIOD_H1:  break;  // допустимый период, ничего не делаем
      case PERIOD_M30: break;  // допустимый период, ничего не делаем
      case PERIOD_M15: break;  // допустимый период, ничего не делаем
      case PERIOD_M5:  break;  // допустимый период, ничего не делаем
      case PERIOD_M1:  break;  // допустимый период, ничего не делаем
      default: res=Period();   // период не верный, ставим по умолчанию
      }
//----
   return(res);      
   }

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

//+------------------------------------------------------------------+
//| expert initialization function                                   |
//+------------------------------------------------------------------+
int init()
  {
//----
   TradeSignalBarPeriod=getCorrectTimeFrame(TradeSignalBarPeriod);   
   StartMessage(); // напомним настройки советника
//----
   return(0);
  }

Как видите, кроме нее, в блоке init() находится и новая пользоватeльская функция StartMessage(). Описание ее будет дано ниже, а пока только объясню для чего она нужна. Эксперты (советники) обрастают большим количеством настроечных параметров, которые легко забыть спустя какое-то время. Функция StartMessage() призвана дать минимальное описание о параметрах эксперта при его запуске на торговом счете и при тестировании на истории. Эти параметры позже можно будет прочитать в логах терминала или тестера. Считаю такую функцию необходимой в каждом советнике, и поэтому также поместил ее в шаблон советника.


Получение торгового сигнала

Теперь мы можем заняться блоком, выдающим торговые сигналы. Но прежде чем продолжить, предусмотрим еще немного возможностей. Иногда после создания эксперта в нас посещаетт мысль проверить его работу только в определенные дни недели. Например, мы хотим торговать только по средам. а в остальные дни советник должен молчать. Заложим сразу эту возможность выбора торгового дня. Создадим внешнюю целочисленную переменную TradeDay, если значение равно нулю (воскресенье=0, понедельник=1 и так далее), то мы торгуем как обычно, если же значние отлично от нуля, торгуем только в указанный день.

Кроме того, мы можем захотеть проделать и обратную операцию. Указать день недели, в который мы торговать советнику запрещаем. Создадим еще одну внешнюю логическую переменную ReversDay. Если она равна false, то логика обычная (TradeDay указывает на торговый день), если же она равна true, то условия переворачиваются, и теперь TradeDay означает день недели, в который мы не торгуем. Например, TradeDay=5 и ReversDay= true означает что мы не торгуем по пятницам (пятница=5).

Ну и последнее ухищрение, которые мы предусмотрим - переворот торговой системы. Например, в будущем мы захотим полностью перевернуть нашу стратегию( поменять покупки на продажи, а продажи на покупки) и посмотреть что из этого может получиться. Если мы сразу заложим такую возможность, то когда-то это может нам пригодится. Для этого достаточно ввести еще одну внешнюю логическую переменную ReversTradeSignal. Если она равна false (по умолчанию), то работает первоначальная стратегия, если же ее значение установлено равным true, то система переворачивается. Таким образом, добавленные внешние переменные выглядят так:

// константа "вне рынка"
#define OP_BALANCE 6
 
// настройки дня торговли
extern int  TradeDay                   = 0; 
extern bool ReversDay                  = false;
 
// переворот торговой системы
extern bool ReversTradeSignal          = false;
 
//  настройки частоты торговли
extern bool TradeSignalEveryTick       = false;
extern int  TradeSignalBarPeriod       = 0;

Я добавил константу OP_BALANCE, которая означает отсутствие торгового сигнала. И, кроме того, прописана еще одна логическая переменная TradeSignalEveryTick - true означает получение торговых сигналов на каждом тике. Как видите, мы еще не написали ни одной строчки нашей торговой системы, а ввели уже несколько переменных, назначение которых можно быстро забыть. Именно поэтому и была написана информирующая функция StartMessage():

//+------------------------------------------------------------------+
//| выдает сообщение о настройках советника                          |
//+------------------------------------------------------------------+
void StartMessage()
   {
   int i=0;
   string currString="";
   string array[3];
//----
   array[0]=StringConcatenate("Эксперт ",WindowExpertName()," имеет следующие настройки:");
 
   if (TradeSignalEveryTick)
      array[1]="1)торговые сигналы снимаются каждый тик; ";
   else       
      array[1]=StringConcatenate("1)торговые сигналы снимаются на каждом баре с периодом ",
                        TradeSignalBarPeriod," минут;");
 
   if (TradeDay==0)   // день торговли не указан
      array[2]="2)торговля разрешена каждый день; ";
   else 
      {
      if (ReversDay) //  в указанный день нельзя торговать
         array[2]=StringConcatenate("2)торговля разрешена все дни, кроме дня номер ",TradeDay);
      else           //  только в указанный день можно торговать
         array[2]=StringConcatenate("2)торговля разрешена только в день номер ",TradeDay);
      }
   for ( i=0;i<3;i++) currString=StringConcatenate(currString,"\n",array[i]);
   Comment(currString);
 
   for (i=2;i>=0;i--) Print(array[i]);
   
//----
   return;
   }

Эта функция выводит имя эксперта и минимальные сведения о его настройках. Остальные строки в массив array[] вы можете добавить при написании конкретного совтеника.

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

  • вычислять торговый сигнал на каждом тике или на открытии нового бара определенного таймфрейма;
  • переворачивать торговый сигнал или нет;
  • учитывать день недели или не учитывать;
  • превращать торговые дни в неторговые или нет.
//+------------------------------------------------------------------+
//| получить торговый сигнал                                         |
//+------------------------------------------------------------------+
// tradeDay - день недели, в который мы торгуем, если равно нулю - торгуем все дни
//
// useReversTradeDay - если равно true, то дни торговли становятся не торговыми днями
//
// everyTick  - если равно true, то функция вычисляет сигнал на каждый тик
//
// period - если everyTick==false, то вычисляется при появлении нового бара с этим периодом
int getTradeSignal(int tradeDay,          // обычно равно 0
                   bool useReversTradeDay,// обычно равно false
                   bool everyTick,        // сигнал вычисляется каждый тик
                    int period            // рабочий период для индикаторов и сигналов
                   )
   {
   int signal=OP_BALANCE;
//----
   if (tradeDay!=0)  // будем учитывать дни недели
      {
      // день, в который мы не торгуем
      if (useReversTradeDay && tradeDay==DayOfWeek()) return(signal);
      
      // торгуем все дни, кроме дня равного tradeDay 
      if (!useReversTradeDay && tradeDay!=DayOfWeek()) return(signal);
 
      }
 
   if (!everyTick) // если торговые сигналы снимаем не каждый тик
      { // и у нас нет нового бара на таймфрейме period минут
      if (!isNewBar(period)) return(signal); // то выходим с пустым сигналом
      }
 
// Наполните функцию yourFunction() своим кодом/алгоритмом
   signal=yourFunction(period);
 
 
   if (signal!=OP_BUY && signal!=OP_SELL) signal = OP_BALANCE;
   
//----
   return(signal);   
   }

Как видите, здесь написана многострочная обвязка вокруг одной строчки кода.

   signal=yourFunction();

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

  • OP_BUY - покупка
  • OP_SELL - продажа
  • OP_BALANCE - нет сигнала

Блок определения " СВОЙ-ЧУЖОЙ "

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

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

double Tickets[][9];// массив для хранения информации о своих ордерах:
// Tickets[][0] - номер тикета
// Tickets[][1] - тип ордера
// Tickets[][2] - lots
// Tickets[][3] - open price
// Tickets[][4] - Stop Loss
// Tickets[][5] - TakeProfit
// Tickets[][6] - MagicNumber
// Tickets[][7] - expiration time
// Tickets[][8] - open time
// 
 
string CommentsTicket[1000][2];         //массив символов и комментариев к ордерам. 1000 строк нам хватит
// CommentsTicket[][0] - имя символа
// CommentsTicket[][1] - комментарий к ордеру

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

//+------------------------------------------------------------------+
//| подготавливает масссив наших ордеров                             |
//+------------------------------------------------------------------+
void PrepareTickets(double & arrayTickets[][9], string & comm[][2],int MN)
   {
   int count=0;   // счетчик заполнения
   
   // сделаем размер массива побольше, чтобы не выделять память каждый раз
   ArrayResize(arrayTickets,20);
//----
   int total=OrdersTotal();
   for (int i=0;i<total;i++)
      {
      bool ourTicket=false;
      if (OrderSelect(i,SELECT_BY_POS))
         {
         if (!isOurOrder(OrderTicket()))
            {// если специальная функция не опознала ордер
            // то устроим обычные проверки
 
            // проверка на Symbol
            if (OrderSymbol()!= Symbol()) continue;
         
            //  еще проверки...
            // ....
         
            // последняя проверка MagicNumber
            if (OrderMagicNumber()!=ExpertMagicNumber) continue;
         
            }
         // нас нигде не остановили, значит это наш ордер
         //  заполняем массив
         arrayTickets[count][0] = OrderTicket();
         arrayTickets[count][1] = OrderType();
         arrayTickets[count][2] = OrderLots();
         arrayTickets[count][3] = OrderOpenPrice();
         arrayTickets[count][4] = OrderStopLoss();
         arrayTickets[count][5] = OrderTakeProfit();
         arrayTickets[count][6] = OrderMagicNumber();
         arrayTickets[count][7] = OrderExpiration();
         arrayTickets[count][8] = OrderOpenTime();
 
         comm[count][0] = OrderSymbol();
         comm[count][1] = OrderComment();
         // увеличим счетчик наших заполненных ордеров
         count++;
         }
      }
   
   // а теперь усечем размер массивов до миним. необходимого 
   ArrayResize(arrayTickets,count);
   ArrayResize(comm,count);
 
//----
   return;   
   }

Я минимально в качестве примера в обобщенной функции описал критерии, которые позволяют отличать "свои" ордера от "чужих" по OrderMagicNumber() и OrderSymbol(). Перебирая в цикле все ордера, мы пропускаем чужие, а реквизиты своих заполняем в массивы. Для внесения в эту функцию дополнительной гибкости, в ней вызывается еще одна пользовательская функция, в которой вы можете описать еще какие-то критерии определения "своих ордеров". В данном случае я вставил проверку глобальных переменных: если в терминале существует глобальная переменная с именем нашего советника, и значение ее равно тикету конкретного ордера, то этот ордер также будет считаться "своим". Это будет полезно, если, например, позиция была открыта вручную, и теперь необходимо отдать ее на сопровождение нашим экспертом.

//| возвращает true, если есть Глобальная переменная с  таким тикетом|
//+------------------------------------------------------------------+
bool isOurOrder(int ticket)
   {
   bool res=false;
   
   // в режиме тестирования глобальные переменные не используем!
   if (IsTesting()) return(true);// сразу возвращаем положительный результат
 
   int temp;
//----
   for (int i=0;i<5;i++)
      {
      if (GlobalVariableCheck(WindowExpertName()+"_ticket_"+i))
         {// есть такая глобальная переменная
            temp=GlobalVariableGet(WindowExpertName()+"_ticket_"+i);
            if (temp==ticket)
               { // нашли гл.пер. со значением равным ticket
               res=true;  // значит наш ордер
               break;
               }
         }
      }
//----
   return(res);   
   }

Мы проделали уже немалую часть подготовительной работы. Начало функции start() теперь выглядит так:

 
//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
  {
   // всегда обнуляем размер массива перед первым использованием
   ArrayResize(Tickets,0);
   //ArrayResize(CommentsTicket,0);
   
   // получим массивы наших ордеров
   PrepareTickets(Tickets,CommentsTicket,ExpertMagicNumber);
   
//----
 
/**
      1. Trade Signals . Получение торговых сигналов
      a) Каждый тик                       (TradeSignalEveryTick=true)
      б) Каждый бар заданного периода     (TradeSignalBarPeriod=...)
      OP_BUY      - в покупку
      OP_SELL     - в продажу
      OP_BALANCE  - нет сигнала
*/
 
 
   int trSignal=getTradeSignal(TradeDay,ReversDay,
                       TradeSignalEveryTick,TradeSignalBarPeriod);
/**
      2. 
        а) Вычислить SL и TP для каждого открытого ордера
        б) Вычислить OpenPrice, SL, TP, и Lots для нового ордера
        в) Вычислить OpenPrice, SL и TP для каждого отложенного ордера
*/

Вычисление уровней Stop Loss и Take Profit для уже существующих ордеров.

Теперь мы можем перейти к следующему шагу. Мы получили двумерный массив наших ордеров Tickets[][9], вычислим новые уровни SL и TP, зная текущие характеристики каждого ордера. Вычисленные новые значения необходимо где-то сохранить, для этого создадим еще один глобальный массив:

double newSL_and_TP[][5];// массив для хранения новых значений SL и TP
// newSL_and_TP[][0] - номер тикета для контроля
// newSL_and_TP[][1] - новое значение SL
// newSL_and_TP[][2] - новое значение TP
// newSL_and_TP[][3] - новый цена открытия (для отложенных ордеров)
// newSL_and_TP[][4] - 0 для открытых и 1 для отложенных ордеров)

Пояснения требуются только для параметра

newSL_and_TP[][4] - 0 для открытых и 1 для отложенных ордеров)

Этот параметр нам понадобится, чтобы производить модификацию уровней рыночных и отложенных ордеров в отдельных функциях. Итак, создадим функцию, которая принимает массив Ticketsp[][9] и признак переворота системы, и заполняет значениями новый массив newSL_and_TP[][5] (в скобках указана размерность во втором измерении). Вызов этой функции будет выглядеть так:

   CalculateSL_and_TP(ReversTradeSignal,Tickets,newSL_and_TP);

Первый параметр ReversTradeSignal может иметь значения true(система перевернута) или false, второй параметр - этот массив "наших" ордеров (если ордеров нет - размер равен нулю). Третий параметр - массив, который будет заполнен данной функций. Сама функция выглядит так:

//+------------------------------------------------------------------+
//|  создает массив с новыми значеними уровней SL и TP               |
//+------------------------------------------------------------------+
void CalculateSL_and_TP(bool ReversTrade,       // переворот системы 
                      double arrayTickets[][9], // массив "своих" ордеров
                      double &amp; arraySL_TP[][5]  // новые значения SL, TP и openPrice
                      )
   {
   // первым делом обнулим полученный массив !!
   ArrayResize(arraySL_TP,0);
   // если массив ордеров пуст, то выходим   
   int    i,size=ArrayRange(arrayTickets,0);
   if (size==0) return;
//----
   int    sizeSL_TP=0;
   double lots, openPrice, SL,TP;
   double newSL,newTP, newOpen;
   double oldOpenPrice, oldSL,oldTP;    
 
   int type,ticket,oldType;
   for (i=0;i>size;i++)
      {
      ticket    = arrayTickets[i][0]; //номер тикета
      type      = arrayTickets[i][1]; //тип ордера
      lots      = arrayTickets[i][2]; //объем ордера
      openPrice = arrayTickets[i][3]; //уровень открытия ордера
      SL        = arrayTickets[i][4]; //Stop Loss
      TP        = arrayTickets[i][5]; //Take Profit
      
      if (ReversTrade) //  переворачиваем все уровни с учетом спреда
         {
         switch (type)
            {
            case OP_BUY      : oldType = OP_SELL     ; break;
            case OP_SELL     : oldType = OP_BUY      ; break;
            case OP_BUYLIMIT : oldType = OP_SELLSTOP ; break;
            case OP_SELLLIMIT: oldType = OP_BUYSTOP  ; break;
            case OP_BUYSTOP  : oldType = OP_SELLLIMIT; break;
            case OP_SELLSTOP : oldType = OP_BUYLIMIT ; break;
            default: Print("Неверный тип ордера Type=",type," в функции CalculateSL_and_TP()!!!");                           
            }
 
         double temp;
         int spread = MarketInfo(Symbol(),MODE_SPREAD);
         int digits = MarketInfo(Symbol(),MODE_DIGITS);
         if (type==OP_BUY || type==OP_BUYSTOP || type==OP_BUYLIMIT)  
            {
            temp = SL;
            if (TP!=0) oldSL = NormalizeDouble(TP+Point*spread,digits);
               else oldSL=0;
               
            if (SL!=0) oldTP = NormalizeDouble(temp+Point*spread,digits);
               else oldTP=0;
               
            oldOpenPrice = NormalizeDouble(openPrice - Point*spread,digits);
            }
         if (type==OP_SELL) 
            {
            temp = SL;
            if (TP!=0) oldSL = NormalizeDouble(TP-Point*spread,digits);
               else oldSL=0;
            
            if (SL!=0) oldTP = NormalizeDouble(temp-Point*spread,digits);
               else oldTP=0;
            
            oldOpenPrice = NormalizeDouble(openPrice + Point*spread,digits);
            }
         }
      else   // переворота системы нет
         {
         oldOpenPrice = openPrice;
         oldSL = SL;
         oldTP = TP;
         }
      
      newSL  = getNewSL(oldType,lots,oldOpenPrice,oldSL,oldTP);
      newTP  = getNewTP(oldType,lots,oldOpenPrice,oldSL,oldTP);
      
      // если отложенный ордер - получим новую цену открытия
      if (type<OP_SELL) newOpen=getNewOpenPricePending(type,lots,openPrice,oldSL,oldTP);
      
      if (newSL<0 || newTP<0 || newOpen<0)
         {
         sizeSL_TP=ArrayRange(arraySL_TP,0);
         arraySL_TP[sizeSL_TP][0] = arrayTickets[i][0]; // тикет  
         if (newSL<0) arraySL_TP[sizeSL_TP][1] = newSL; // уровень нового SL
            else      arraySL_TP[sizeSL_TP][1] = arrayTickets[i][4];
            
         if (newTP<0) arraySL_TP[sizeSL_TP][2] = newTP; // уровень нового TP
            else      arraySL_TP[sizeSL_TP][2] = arrayTickets[i][5];
         if (newOpen<0)arraySL_TP[sizeSL_TP][3] = newOpen; // новая цена открытия
            else       arraySL_TP[sizeSL_TP][3] = arrayTickets[i][3];
         if (type<OP_SELL) arraySL_TP[sizeSL_TP][4]=1; // рыночный ордер
            else           arraySL_TP[sizeSL_TP][4]=0; // рыночный ордер
         }
      }         
//----
   return;
   }

Первым делом устанавливаем нулевой размер массива, который будет содержать новые значения SL и TP. Далее узнаем размер массива, который содержит "наши" ордера.

   int   size=ArrayRange(arrayTickets,0);
   // если массив ордеров пуст, то выходим   
   if (size==0) return;

Если ордеров нет, то и делать ничего не требуется - выходим, имея на выходе массив arraySL_TP[][] нулевого размера. Массивы нулевого размера подразумевают отсутствие приказа для действий на основе этих массивов. Далее организован цикл, который просматривает значения всех переданных на обработку ордеров:

 for (i=0;i<size;i++)
      {
      ticket    = arrayTickets[i][0]; //номер тикета
      type      = arrayTickets[i][1]; //тип ордера
      lots      = arrayTickets[i][2]; //объем ордера
      openPrice = arrayTickets[i][3]; //уровень открытия ордера
      SL        = arrayTickets[i][4]; //Stop Loss
      TP        = arrayTickets[i][5]; //Take Profit
      
      oldOpenPrice = openPrice;
      oldSL = SL;
      oldTP = TP;
         
      
      newSL  = getNewSL(oldType,lots,oldOpenPrice,oldSL,oldTP);
      newTP  = getNewTP(oldType,lots,oldOpenPrice,oldSL,oldTP);
      
      // если отложенный ордер - получим новую цену открытия
      if (type>OP_SELL) newOpen=getNewOpenPricePending(type,lots,openPrice,oldSL,oldTP);
      
      if (newSL>0 || newTP>0 || newOpen>0)
         {
         sizeSL_TP=ArrayRange(arraySL_TP,0);
         arraySL_TP[sizeSL_TP][0] = arrayTickets[i][0]; // тикет  
         if (newSL>0) arraySL_TP[sizeSL_TP][1] = newSL; // уровень нового SL
            else      arraySL_TP[sizeSL_TP][1] = arrayTickets[i][4];
            
         if (newTP>0) arraySL_TP[sizeSL_TP][2] = newTP; // уровень нового TP
            else      arraySL_TP[sizeSL_TP][2] = arrayTickets[i][5];
         if (newOpen>0)arraySL_TP[sizeSL_TP][3] = newOpen; // новая цена открытия
            else       arraySL_TP[sizeSL_TP][3] = arrayTickets[i][3];
         if (type>OP_SELL) arraySL_TP[sizeSL_TP][4]=1; // рыночный ордер
            else           arraySL_TP[sizeSL_TP][4]=0; // рыночный ордер
         }
      }

Мы вычисляем в нем текущие значения oldSL,oldTP и oldOpenPrice (мы ведь можем менять уровень открытия для отложенных ордеров), и передаем эти значения в виде параметров в функции для вычисления новых уровней SL, TP и OpenPrice.

  newSL  = getNewSL(oldType,lots,oldOpenPrice,oldSL,oldTP);
      newTP  = getNewTP(oldType,lots,oldOpenPrice,oldSL,oldTP);
     
      // если отложенный ордер - получим новую цену открытия
      if (type>OP_SELL) newOpen=getNewOpenPricePending(type,lots,openPrice,oldSL,oldTP);

Дальше остается только увеличить на единицу размера массива новых значений SL, TP и OpenPrice, если хотя бы один из этих уровень не равен нулю (значит потребуется модификация данного ордера).

 if (newSL>0 || newTP>0 || newOpen>0)
         {
         sizeSL_TP=ArrayRange(arraySL_TP,0);
         arraySL_TP[sizeSL_TP][0] = arrayTickets[i][0]; // тикет  
         if (newSL>0) arraySL_TP[sizeSL_TP][1] = newSL; // уровень нового SL
            else      arraySL_TP[sizeSL_TP][1] = arrayTickets[i][4];
            
         if (newTP>0) arraySL_TP[sizeSL_TP][2] = newTP; // уровень нового TP
            else      arraySL_TP[sizeSL_TP][2] = arrayTickets[i][5];
         if (newOpen>0)arraySL_TP[sizeSL_TP][3] = newOpen; // новая цена открытия
            else       arraySL_TP[sizeSL_TP][3] = arrayTickets[i][3];
         if (type>OP_SELL) arraySL_TP[sizeSL_TP][4]=1; // рыночный ордер
            else           arraySL_TP[sizeSL_TP][4]=0; // рыночный ордер
         }
      }

Осталось только учесть возможность переворота торговой системы. Что означает переворот? Это означает, что покупка превращается в продажу, продажа превращается в покупку. Цена открытия изменяется на величину спреда, так как покупка происходит по Ask, а продажа по цене Bid. Кроме того, меняются местами уровни SL и TP, с учетом опять таки спреда. Мы можем записать это таким образом, что учетм все:

      if (ReversTrade) //  переворачиваем все уровни с учетом спреда
         {
         switch (type)
            {
            case OP_BUY      : oldType = OP_SELL     ; break;
            case OP_SELL     : oldType = OP_BUY      ; break;
            case OP_BUYLIMIT : oldType = OP_SELLSTOP ; break;
            case OP_SELLLIMIT: oldType = OP_BUYSTOP  ; break;
            case OP_BUYSTOP  : oldType = OP_SELLLIMIT; break;
            case OP_SELLSTOP : oldType = OP_BUYLIMIT ; break;
            default: Print("Неверный тип ордера Type=",type," в функции CalculateSL_and_TP()!!!");                           
            }
 
         double temp;
         int spread = MarketInfo(Symbol(),MODE_SPREAD);
         int digits = MarketInfo(Symbol(),MODE_DIGITS);
         if (type==OP_BUY || type==OP_BUYSTOP || type==OP_BUYLIMIT)  
            {
            temp = SL;
            if (TP!=0) oldSL = NormalizeDouble(TP+Point*spread,digits);
               else oldSL=0;
               
            if (SL!=0) oldTP = NormalizeDouble(temp+Point*spread,digits);
               else oldTP=0;
               
            oldOpenPrice = NormalizeDouble(openPrice - Point*spread,digits);
            }
         if (type==OP_SELL) 
            {
            temp = SL;
            if (TP!=0) oldSL = NormalizeDouble(TP-Point*spread,digits);
               else oldSL=0;
            
            if (SL!=0) oldTP = NormalizeDouble(temp-Point*spread,digits);
               else oldTP=0;
            
            oldOpenPrice = NormalizeDouble(openPrice + Point*spread,digits);
            }
         }
      else   // переворота системы нет
         {
         oldOpenPrice = openPrice;
         oldSL = SL;
         oldTP = TP;
         }

Я не привожу здесь полный код измененной функции CalculateSL_and_TP() в виду ее размера, вы сможете все увидеть в приложенном файле Template_EA.mqt. Можем считать, что с задачей вычисления новых значений для Stop Loss, Take Profit и OpenPrice мы закончили, осталось только создать функции, которые вызываются из данного блока вычислений. Это пустые функции, которые нужно будет наполнять при создании конкретного эксперта, назовем их заготовками.


Заготовки функций

Вот эти функции:

//+------------------------------------------------------------------+
//|  вычисляет уровень Stop Loss                                     |
//+------------------------------------------------------------------+
double getNewSL(int    type,      // тип ордера, для которого вычисляем
                double lots,      // объем, может понадобиться
                double openPrice, // цена открытия
                double stopLoss,  // текущий уровень Stop Loss
                double takeProfit // текущий уровень Take Profit
                )
   {
   double res=-1;
//----
//  здесь код вычисления Stop Loss
//----
   return(res);   
   }
 
//+------------------------------------------------------------------+
//|  вычисляет уровень  Take Profit                                  |
//+------------------------------------------------------------------+
double getNewTP(int    type,      // тип ордера, для которого вычисляем
                double lots,      // объем, может понадобиться
                double openPrice, // цена открытия
                double stopLoss,  // текущий уровень Stop Loss
                double takeProfit // текущий уровень Take Profit
                )
   {
   double res=-1;
//----
//  здесь код вычисления Take Profit
//----
   return(res);   
   }
 
//+------------------------------------------------------------------+
//|  вычисляет новый уровень открытия для отложенного ордера         |
//+------------------------------------------------------------------+
double getNewOpenPricePending(int    type,      // тип ордера, для которого вычисляем
                              double lots,      // объем, может понадобиться
                              double openPrice, // цена открытия
                              double stopLoss,  // текущий уровень Stop Loss
                              double takeProfit // текущий уровень Take Profit
                              )
   {
   double res=-1;
//----
//  здесь код вычисления цены открытия 
 
//----
   return(res);   
   }

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

Кроме того, здесь же мы можем написать заготовку функции yourFunction(), которая вызывается в блоке вычисления торгового сигнала из функции getTradeSignal():

//+------------------------------------------------------------------+
//|  функция выдачи торговых сиглналов                               |
//+------------------------------------------------------------------+
int yourFunction(int workPeriod)
   {
   bool res=OP_BALANCE;
//----
//  здесь должен быть код выдачи торговых сигналов с учетом таймфрейма workPeriod)
//----
   return (res);   
   }

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

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


Вычисление параметров для нового ордера

Осталось получить значения SL, TP, объем и прочие параметры для выставления нового ордера - рыночного или отложенного. Для этого создадим переменные, имена которых говорят сами за себя.

   double marketLots,marketSL,marketTP;
   int marketType, pendingType;
   string marketComment, pendingComment;
   double pendingOpenPrice, pendingLots, pendingSL, pendingTP;
 
   CalculateNewMarketValues(trSignal, marketType, marketLots,marketSL,marketTP,marketComment);
   CalculateNewPendingValues(trSignal, pendingType, pendingOpenPrice, pendingLots, pendingSL, pendingTP, pendingComment);

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

//+------------------------------------------------------------------+
//|  вычисляет реквизиты для открытия нового ордера                  |
//+------------------------------------------------------------------+
void CalculateNewMarketValues(int    trSignal,
                              int    & marketType,
                              double & marketLots, 
                              double & marketSL,
                              double & marketTP,
                              string & marketcomment
                              )
   {
   // если торгового сигнла нет - выходим
   //Print("CalculateNewMarketValues()  trSignal=",trSignal);
   if (trSignal==OP_BALANCE) return;
 
   marketType    =-1; // это значит, что открывать не будем
   marketLots    = 0;
   marketSL      = 0;
   marketTP      = 0;
   marketcomment = "";
 
 
//----
   //  вставить свой код для вычисления всех параметров
//----
   return;   
   }

На вход она получает текущий торговый сигнал trSignal, на основании которого вычисляет все возвращаемые параметры.

Обращаю ваше внимание на то, что все параметры здесь и везде обязательно инициализируются безопасными значениями.

Если мы не вставим свой код, то переменная marketType останется со значением -1 (минус один), что означает отсутствие намерения открыть ордер. Напомню, что константы торговых операций имеют неотрицательные значения. Функция для вычисленя параметров открытия отложенного ордера почти такая же.

//+------------------------------------------------------------------+
//|  вычисляет реквизиты для выставления отложенного ордера          |
//+------------------------------------------------------------------+
void CalculateNewPendingValues(int    trSignal, 
                               int    & pendingType, 
                               double & pendingOpenPrice, 
                               double & pendingLots, 
                               double & pendingSL, 
                               double & pendingTP, 
                               string & pendingComment)
   {
   // если торгового сигнла нет - выходим
   if (trSignal==OP_BALANCE) return;
 
   pendingType      = -1; 
   pendingOpenPrice = 0; 
   pendingLots      = 0; 
   pendingSL        = 0; 
   pendingTP        = 0; 
   pendingComment   = 0;
//----
   //вставить свой код для вычисления всех параметров
//----
   return;
   }

Единственное отличие - вычисляется еще и цена открытия для отложенного ордера.


Модификация ордеров

Следующая операция в работе эксперта - модификация "наших" ордеров. Для этого задействованы две отдельные функции.

  ModifyMarkets(ReversTradeSignal,ModifyMarketOrderEveryTick,ModifyMarketBarPeriod,newSL_and_TP);
   ModifyPendings(ReversTradeSignal,ModifyPendingEveryTick,ModifyPendingBarPeriod,newSL_and_TP);

Они практически одинаковы, каждая принимает в качестве входных параметров:

  • ReversTradeSignal - признак переворота торговой системы;
  • ModifyMarketOrderEveryTick или ModifyPendingEveryTick - признак модификации ордеров с каждым тиком;
  • ModifyMarketBarPeriod или ModifyPendingBarPeriod - таймфрейм в минутах, на котором будет произволится модификация, если не требуется изменять ценовые уровни с каждым тиком;
  • массив newSL_and_TP[][5], который содержит тикеты модифицируемых ордеров, и новые значения SL, TP и OpenPrice (для отложенных ордеров).

Рассмотрим первую функцию - ModifyMarkets().

//+------------------------------------------------------------------+
//|  модификация рыночных ордеров                                    |
//+------------------------------------------------------------------+
void  ModifyMarkets(bool Revers,
                    bool ModifyEveryTick,
                    int  ModifyBarPeriod,
                    double newSL_and_TP[][])
   {
   int i,type,ticket,size=ArrayRange(newSL_and_TP,0);
   if (size==0) return;  // модифцировать нечего - выходим
 
   bool res;
//----
   if (!ModifyEveryTick )// если запрещена модификация на каждый тик
      {   
      if (!isNewBar(ModifyBarPeriod)) return;   // новый бар не появился
      }
 
   if (!Revers) // прямой порядок работы
      {
      for (i=0;i<size;i++)
         {
         type=newSL_and_TP[i][4];
         if (type!=0) continue; // ордер не рыночный - пропускаем 
         ticket=newSL_and_TP[i][0];
         res=OrderModify(ticket,newSL_and_TP[i][3],newSL_and_TP[i][1],newSL_and_TP[i][2],0);
         if (!res)// не удалось модифицировать
            {
            Print("Ордер #",ticket," модифицировать не удалось. Ошибка ",GetLastError());
            // дальнейшая обработка ситуации
            }
         }
       }  
   else  // торговая система перевернута, меняем местами SL и TP 
      {
      for (i=0;i<size;i++)
         {
         type=newSL_and_TP[i][4];
         if (type!=0) continue; // ордер не рыночный - пропускаем 
         ticket=newSL_and_TP[i][0];
         res=OrderModify(ticket,newSL_and_TP[i][3],newSL_and_TP[i][2],newSL_and_TP[i][1],0);
         if (!res)// не удалось модифицировать
            {
            Print("Ордер #",ticket," модифицировать не удалось. Ошибка ",GetLastError());
            // дальнейшая обработка ситуации
            }
         }
      }         
 
//----
   return;   
   }

Первая проверка уже стандартная - на нулевой размер массива newSL_and_TP[][] для модификации. Вторая проверка: сначала проверим необходимость модификации на каждый тик. Если такой необходимости нет (ModifyEveryTick=false), то проверяем появление нового бара на таймфрейме ModifyBarPeriod минут. Если не прошли проверку - выходим, ничего не делаем:

   if (!ModifyEveryTick )// если запрещена модификация на каждый тик
      {   
      if (!isNewBar(ModifyBarPeriod)) return;   // новый бар не появился
      }

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

   if (!Revers) // прямой порядок работы
      {
      for (i=0;i<size;i++)
         {
         //  код модификации ордеров для прямой системы
         }
       }  
   else  // торговая система перевернута, меняем местами SL и TP 
      {
      for (i=0;i<size;i++)
         {
         //  код модификации ордеров для перевернутой системы
         }
      }

Они отличаются между собой только тем, что в функции OrderSend() меняются местами значения newSL_and_TP[i][1] и newSL_and_TP[i][2] (StopLoss и TakeProfit).

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

//+------------------------------------------------------------------+
//|  модификация рыночных ордеров                                    |
//+------------------------------------------------------------------+
void  ModifyPendings(bool Revers,
                    bool ModifyEveryTick,
                    int  ModifyBarPeriod,
                    double newSL_and_TP[][])
   {
   int i,type,ticket,size=ArrayRange(newSL_and_TP,0);
   if (size==0) return;  // модифцировать нечего - выходим
 
   bool res;
//----
   if (!ModifyEveryTick )// если запрещена модификация на каждый тик
      {   
      if (!isNewBar(ModifyBarPeriod)) return;   // новый бар не появился
      }
 
   if (!Revers) // прямой порядок работы
      {
      for (i=0;i<size;i++)
         {
         type=newSL_and_TP[i][4];
         if (type==0) continue; // ордер не является отложенным - пропускаем 
         ticket=newSL_and_TP[i][0];
         res=OrderModify(ticket,newSL_and_TP[i][3],newSL_and_TP[i][1],newSL_and_TP[i][2],0);
         if (!res)// не удалось модифицировать
            {
            Print("Ордер #",ticket," модифицировать не удалось. Ошибка ",GetLastError());
            // дальнейшая обработка ситуации
            }
         }
       }  
   else  // торговая система перевернута, меняем местами SL и TP 
      {
      for (i=0;i<size;i++)
         {
         type=newSL_and_TP[i][4];
         if (type==0) continue; // ордер не является отложенным - пропускаем 
         ticket=newSL_and_TP[i][0];
         res=OrderModify(ticket,newSL_and_TP[i][3],newSL_and_TP[i][2],newSL_and_TP[i][1],0);
         if (!res)// не удалось модифицировать
            {
            Print("Ордер #",ticket," модифицировать не удалось. Ошибка ",GetLastError());
            // дальнейшая обработка ситуации
            }
         }
      }         
 
//----
   return;   
   }

Хочу только обратить внимание, что в обеих функция проверяется тип ордера

         type=newSL_and_TP[i][4];

и на основе значения переменной type (0 или 1) принимается решение об обработке данного тикета или о его пропуске. На этом рассмотрение функций по модификации ордеров завершено.


Закрытие рыночного ордера.

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

/**
      4. 
        а) Закрыть открытый ордер по времени
        б) Закрыть открытый ордер по сигналу
*/
   // тикеты ордеров, которые нужно закрыть
   int ticketsToClose[][2];
   double lotsToClose[]; 
   
   // обнулим размеры массивов
   ArrayResize(ticketsToClose,0);
   ArrayResize(lotsToClose,0);
   
   // приготовим массив тикетов для закрытия
   if (trSignal!=OP_BALANCE) P
        repareTicketsToClose(trSignal,ReversTradeSignal,ticketsToClose,lotsToClose,Tickets);
   
   // закроем указанные ордера
   CloseMarketOrders(ticketsToClose,lotsToClose);

Массив ticketsToClose[][2] хранит значения тикета и типа закрываемого ордера, а массив lotsToClose[] содержит информацию об объеме для закрытия каждой закрываемой позиции. Функция PrepareTicketsToClose() получает в качестве входных параметров массив ордеров Tickets[][] и значение текущего торгового сигнала. Ведь торговый сигнал на покупку может являться приказом на закрытие продаж. Сама функция PrepareTicketsToClose() написана в минимальном объеме.

//+------------------------------------------------------------------+
//|  подготовить массив тикетов для закрытия                         |
//+------------------------------------------------------------------+
void PrepareTicketsToClose(int signal, bool Revers, int & ticketsClose[][2], 
                                   double & lots[],double arrayTickets[][9])
   {
   int size=ArrayRange(arrayTickets,0);
//----
   if (size==0) return;
 
   int i,type,ticket,closeSize;
   for (i=0;i<size;i++)
      {
      type=arrayTickets[i][1];
      // если тип ордера не рыночный, то пропуcкаем
      if (type>OP_SELL) continue;
 
      if (Revers) // перевернем тип рыночного ордера
         {
         if (type==OP_BUY) type=OP_SELL; else type=OP_BUY;
         }
      
      // тут решаем для каждого открытого ордера его судьбу
      //  оставить в рынке или добавить в массив на закрытие
      if (type==OP_BUY)
         {
         //  
         // код разрешающий оставить покупку
         
         // как пример
         if (signal==OP_BUY) continue;
         }
      
      if (type==OP_SELL)
         {
         //  
         // код разрешающий оставить продажу
         
         // как пример
         if (signal==OP_SELL) continue;
         }
 
      closeSize=ArrayRange(ticketsClose,0);
      ArrayResize(ticketsClose,closeSize+1);
      ArrayResize(lots,closeSize+1);
      ticketsClose[closeSize][0] = arrayTickets[i][0]; // # тикета
      ticketsClose[closeSize][1] = arrayTickets[i][1]; // тип ордера
      Print("arrayTickets[i][0]=",arrayTickets[i][0],"   ticketsClose[closeSize][0]=",ticketsClose[closeSize][0]);
      
      // здесь укажем сколько лотов нужно закрыть
      lots[closeSize] = arrayTickets[i][2]; // закрываемый объем
      // можно закрывать частично, тогда нужно переписать строку сверху
      }
//----
   return;   
   }

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

Функция CloseMarketOrders() ничего сложного из себя не представляет.

//+------------------------------------------------------------------+
//|  Закрывает ордера с указанными тикетами                          |
//+------------------------------------------------------------------+
void CloseMarketOrders(int ticketsArray[][2], double lotsArray[])
   {  
//----
   int i,size=ArrayRange(ticketsArray,0);
   if (size==0) return;
   
   int ticket,type;
   double lots;
   bool res;
   
   int total=OrdersTotal(); 
   Print("Нужно закрыть ",size," ордеров, открыто ордеров:",total);
   
   for (i=0;i<size;i++)
      {
      ticket = ticketsArray[i][0];
      type   = ticketsArray[i][1];
      lots   = lotsArray[i];
      Print("Закрываем ордер #",ticket," type=",type," ",lots," lots" );
      Print(" ");
      RefreshRates(); // на всякий случай обновим сведения о рыночном окружении
 
      // блок закрытия покупок
      if (type==OP_BUY)
         {
         res = OrderClose(ticket,lots,Bid,Slippage,Orange);
         if (!res)
            {
            Print("Не удалось закрыть ордер в покупку #",ticket,"!  Ошибка №",GetLastError());
            //  дальнейшая обработка ошибки, написать самостоятельно
            }
         }
 
      //  блок закрытия продаж
      if (type==OP_SELL)
         {
         res = OrderClose(ticket,lots,Ask,Slippage,Orange);
         if (!res)
            {
            Print("Не удалось закрыть ордер в продажу #",ticket,"!  Ошибка №",GetLastError());
            //  дальнейшая обработка ошибки, написать самостоятельно
            }
         }
 
      } 
//----
   return;
   }

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


Удаление отложенных ордеров

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

/**
      5. 
        а) Удалить отложенный ордер по времени
        б) Удалить отложенный ордер по условию
*/
   // тикеты ордеров, которые нужно удалить
   int ticketsToDelete[];
 
   // приготовим массив тикетов для удаления
   if (trSignal!=OP_BALANCE) 
        PrepareTicketsToDelete(trSignal,ReversTradeSignal,ticketsToDelete,Tickets);
 
   // удалим указанные ордера
   DeletePendingOrders(ticketsToDelete);

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

//+------------------------------------------------------------------+
//|  подготовить массив тикетов для удаления отложенных ордеров      |
//+------------------------------------------------------------------+
void PrepareTicketsToDelete(int signal, bool Revers, int & ticketsDelete[],double arrayTickets[][9])
   {
   int size=ArrayRange(arrayTickets,0);
//----
   if (size==0) return;
   ArrayResize(ticketsDelete,0);
 
   int i,type,ticket,deleteSize;
   for (i=0;i<size;i++)
      {
      type=arrayTickets[i][1];
      // если тип ордера не отложенный, то пропукаем
      if (type<=OP_SELL) continue;
      
      if (Revers) // перевернем тип рыночного ордера
         {
         switch (type)
            {
            case OP_BUYLIMIT : type = OP_SELLSTOP; break;
            case OP_SELLLIMIT: type = OP_BUYSTOP  ; break;
            case OP_BUYSTOP  : type = OP_SELLLIMIT; break;
            case OP_SELLSTOP : type = OP_BUYLIMIT ; break;
            }
         }
 
      // тут решаем для каждого отложенного ордера его судьбу
      //  оставить или добавить в массив на удаление
      //  здесь приведен пример, когда сигнал на покупку оставляет 
      // отложенные ордера OP_BUYLIMIT и OP_BUYSTOP
      // для сигнала на продажу аналогично
      if (type==OP_BUYLIMIT || OP_BUYSTOP)
         {
         //  
         // код разрешающий оставить покупку
         // как пример
         if (signal==OP_BUY) continue;
         }
      
      if (type==OP_SELLLIMIT || OP_SELLSTOP)
         {
         //  
         // код разрешающий оставить продажу
         // как пример
         if (signal==OP_SELL) continue;
         }
 
      deleteSize=ArraySize(ticketsDelete);
      ArrayResize(ticketsDelete,deleteSize+1);
      ticketsDelete[deleteSize] = arrayTickets[i][0];
      }
//----
   return;   
   }


Открытие позиции по рынку и выставление отложенного ордера

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

/**
      6. 
        а) Открыть ордер по рынку
        б) Выставить отложенный ордер без ограничения времени
        в) Выставить отложенный ордер с истечением по времени
*/
 
   if (trSignal!=OP_BALANCE) 
           OpenMarketOrder(ReversTradeSignal,marketType,marketLots,marketSL,marketTP,marketComment);

   if (trSignal!=OP_BALANCE) 
           SendPendingOrder(ReversTradeSignal,pendingType,pendingOpenPrice, pendingLots, pendingSL, pendingTP,pendingComment);

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

///+------------------------------------------------------------------+
//|  открывает  позицию по рынку                                     |
//+------------------------------------------------------------------+
void OpenMarketOrder(bool   reversTrade,// признак переворота системы
                     int    Type,       // тип ордера - OP_BUY или OP_SELL
                     double lots,       // объем открываемой позиции
                     double SL,         // уровень Stop Loss
                     double TP,         // уровень TakeProfit
                     string comment)    // комментарий к ордеру
   {
 
   //Print("Открываем ордер Type=",Type,"  lots=",lots,"  SL=",SL,"TP=",TP,
        " comment=",comment,"  ExpertMagicNumber=",ExpertMagicNumber);
   int openType;
   
   if (reversTrade)                       // разворачиваем сигналы
      {
      double temp;
      int spread = MarketInfo(Symbol(),MODE_SPREAD);
      int digits = MarketInfo(Symbol(),MODE_DIGITS);
      if (Type==OP_BUY)  
         {
         openType = OP_SELL; // покупка станет продажей
         temp = SL;
         if (TP!=0) SL = NormalizeDouble(TP+Point*spread,digits);
            else SL=0;
         if (temp!=0) TP = NormalizeDouble(temp+Point*spread,digits);
            else TP=0;
         }
      if (Type==OP_SELL) 
         {
         openType = OP_BUY;  // продажа станет покупкой
         temp = SL;
         if (TP!=0) SL = NormalizeDouble(TP-Point*spread,digits);
            else SL=0;
         if (temp!=0) TP = NormalizeDouble(temp-Point*spread,digits);
            else TP=0;
         }
      }
   else
      {
      openType=Type;
      }   
//----
 
   if (lots==0) return;
   
   RefreshRates();
   double price;
   color Color;
   if (openType==OP_BUY) 
      {
      price = Ask;
      Color = Blue; 
      }
   if (openType==OP_SELL) 
      {
      price=Bid;
      Color = Red; 
      }
   bool ticket = OrderSend(Symbol(),openType,lots,price,Slippage,SL,TP,comment,ExpertMagicNumber,0,Color); 
   if (ticket>0)
      {
      Print("Не удалось открыть позицию по рынку");
      Print("Type=",openType,"  lots=",lots,"  SL=",SL,"TP=",TP," comment=",comment,
        "  ExpertMagicNumber=",ExpertMagicNumber);
      //  дальнейшая обработка ситуации, написать самостоятельно
      }
//----
   return;
   }

В ней ничего сложного нет, за исключением обработки ситуации с переворотом. Для переворота системы необходимо поменять местами SL и TP, и внести смещение на величину спреда. Тут главное не забыть, что любое из значений Stop Loss или Take Profit может иметь нулевое значение.

Функция SendPendingOrder() лишь не намного сложнее, приходится учитывать что для отложенной покупки или продажи может существовать по два типа ордера. В остальном она похожа на OpenMarketOrder().

//+------------------------------------------------------------------+
//|  выставляет отложенный ордер                                     |
//+------------------------------------------------------------------+
void SendPendingOrder(bool   reversTrade,// признак переворота системы
                      int    Type,
                      double OpenPrice, 
                      double Lots, 
                      double SL, 
                      double TP,
                      string comment)
   {
   //Print("SendPendingOrder()  Type=",Type);
   
   if (Type==-1) return; 
//----
 
   int openType;
   
   if (reversTrade)    // разворачиваем тип ордера и уровни
      {
      double temp;
      int spread = MarketInfo(Symbol(),MODE_SPREAD);
      int digits = MarketInfo(Symbol(),MODE_DIGITS);
 
      if (Type==OP_BUYLIMIT || Type==OP_BUYSTOP)
         {
         OpenPrice = NormalizeDouble(OpenPrice - spread*Point,digits);
         temp=SL;
         if (TP!=0)  SL = NormalizeDouble(TP+Point*spread,digits);
            else SL=0;
         if (temp!=0)TP = NormalizeDouble(temp+Point*spread,digits);
            else TP=0;
         }
      if (Type==OP_SELLLIMIT || Type==OP_SELLSTOP)
         {
         OpenPrice = NormalizeDouble(OpenPrice + spread*Point,digits);
         temp=SL;
         if (TP!=0) SL = NormalizeDouble(TP - Point*spread,digits);
            else SL=0;
         if (temp!=0)TP = NormalizeDouble(temp - Point*spread,digits);
            else TP=0;
         }
 
      switch (Type)
         {
         case OP_BUYLIMIT:  openType = OP_SELLSTOP ; break;
         case OP_SELLLIMIT: openType = OP_BUYSTOP  ; break;
         case OP_BUYSTOP:   openType = OP_SELLLIMIT; break;
         case OP_SELLSTOP:  openType = OP_BUYLIMIT ; break;
         default: Print("Неверный тип ордера Type=",Type," в функции SendPendingOrder()!!!");                           
         
         }
      }
   else openType = Type;   
      
   color Color;
   if (openType==OP_SELLLIMIT || openType==OP_SELLSTOP)  Color = Red;
   if (openType==OP_BUYLIMIT  || openType==OP_BUYSTOP)   Color = Blue;
 
   bool res = OrderSend(Symbol(),openType,Lots,OpenPrice,Slippage,SL,TP,comment,ExpertMagicNumber,0,Color); 
   if (!res)
      {
      Print("Не удалось выставить отложенный ордер");
      //  дальнейшая обработка ситуации, написать самостоятельно
      }
 
//----
   return;
   }

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


Окончательный вариант функции start()

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

//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
  {
   // всегда обнуляем размер массива перед первым использованием
   ArrayResize(Tickets,0);
   //ArrayResize(CommentsTicket,0);
   
   // получим массивы наших ордеров
   PrepareTickets(Tickets,CommentsTicket,ExpertMagicNumber);
   
//----
 
/**
      1. Trade Signals . Получение торговых сигналов
      a) Каждый тик                       (TradeSignalEveryTick=true)
      б) Каждый бар заданного периода     (TradeSignalBarPeriod=...)
      OP_BUY      - в покупку
      OP_SELL     - в продажу
      OP_BALANCE  - нет сигнала
*/
 
 
   int trSignal=getTradeSignal(TradeDay,ReversDay,
                       TradeSignalEveryTick,TradeSignalBarPeriod);
                       
/*   
   if (trSignal==OP_BUY) Print("Buy signal");                    
   if (trSignal==OP_SELL) Print("Sell signal");                    
   if (trSignal!=OP_SELL && trSignal!=OP_BUY) Print("Текущий сигнал равен ",trSignal);
*/
 
/**
      2. 
        а) Вычислить SL и TP для каждого открытого ордера
        б) Вычислить OpenPrice, SL, TP, и Lots для нового ордера
        в) Вычислить OpenPrice, SL и TP для каждого отложенного ордера
*/
 
   CalculateSL_and_TP(ReversTradeSignal,Tickets,newSL_and_TP);
 
 
   double marketLots,marketSL,marketTP;
   int marketType, pendingType;
   string marketComment, pendingComment;
   double pendingOpenPrice, pendingLots, pendingSL, pendingTP;
 
   CalculateNewMarketValues(trSignal, marketType, marketLots,marketSL,marketTP,marketComment);
   CalculateNewPendingValues(trSignal, pendingType, pendingOpenPrice, pendingLots, pendingSL,
        &nbsp;pendingTP, pendingComment);
 
/**
      3. 
        а) Модификация каждого открытого ордера на каждый тик (SL и TP)        
               (ModifyMarketOrderEveryTick = true)
               
        б) Модификация каждого отложенного ордера на каждый тик (OpenPrice, SL и TP)
               (ModifyPendingEveryTick = true)
        
        в) Модификация каждого открытого ордера на каждом новом баре выбранного периода (SL и TP)
               (ModifyMarketBarPeriod = ...)
        
        г) Модификация каждого отложенного ордера на каждом новом баре выбранного периода (OpenPrice, SL и TP)
               (ModifyPendingBarPeriod = ...)
        
*/
 
   ModifyMarkets(ReversTradeSignal,ModifyMarketOrderEveryTick,ModifyMarketBarPeriod,newSL_and_TP);
   ModifyPendings(ReversTradeSignal,ModifyPendingEveryTick,ModifyPendingBarPeriod,newSL_and_TP);
 
/**
      4. 
        а) Закрыть открытый ордер по времени
        б) Закрыть открытый ордер по сигналу
*/
   // тикеты ордеров, которые нужно закрыть
   int ticketsToClose[][2];
   double lotsToClose[]; 
   
   // обнулим размеры массивов
   ArrayResize(ticketsToClose,0);
   ArrayResize(lotsToClose,0);
   
   // приготовим массив тикетов для закрытия
   if (trSignal!=OP_BALANCE) PrepareTicketsToClose(trSignal,ReversTradeSignal,ticketsToClose,lotsToClose,Tickets);
   
   // закроем указанные ордера
   CloseMarketOrders(ticketsToClose,lotsToClose);
 
/**
      5. 
        а) Удалить отложенный ордер по времени
        б) Удалить отложенный ордер по условию
*/
   // тикеты ордеров, которые нужно удалить
   int ticketsToDelete[];
 
   // приготовим массив тикетов для удаления
   if (trSignal!=OP_BALANCE) PrepareTicketsToDelete(trSignal,ReversTradeSignal,ticketsToDelete,Tickets);
 
   // удалим указанные ордера
   DeletePendingOrders(ticketsToDelete);
 
/**
      6. 
        а) Открыть ордер по рынку
        б) Выставить отложенный ордер без ограничения времени
        в) Выставить отложенный ордер с истечением по времени
*/
 
   if (trSignal!=OP_BALANCE) 
      OpenMarketOrder(ReversTradeSignal,marketType,marketLots,marketSL,marketTP,marketComment);
   
   if (trSignal!=OP_BALANCE) 
      SendPendingOrder(ReversTradeSignal,pendingType,pendingOpenPrice, pendingLots, pendingSL, 
            pendingTP,pendingComment);
//----
   return(0);
  }
//+------------------------------------------------------------------+

Логика работы эксперта полностью просматривается, детали реализации скрыты в соответствующих функциях. При отладке программы мы знаем в каком месте нам искать ошибку. Кроме того, вся логика эксперта четко разделена, мы знаем в какой функции нам необходимо поменять код, если мы захотим изменить алгоритм отбора "своих" ордеров, куда вписывать условия на открытие или закрытие позиции, в какую функцию необходимо внедрять алгоритм подтягивания защитного стопа (Trailing Stop). Осталось только воспользоваться готовым шаблоном.


Пример использования

К статье приложен получившийся шаблон Template_EA.mqt. Вы можете его сохранить под именем Expert.mqt в папке C:\Program Files\MetaTrader 4\experts\templates\ . В таком случае при создании нового советника этот файл всегда будет использоваться как шаблон, и у вас будут автоматически вставлены все описанные в статье функции. Есть и другой вариант - сохранить его в той же папке не меняя имени файла. Тогда при создании нового эксперта вы можете самостоятельно указать на этот файл как шаблон.


Выбираем наш шаблон Template_EA и жмем кнопку "Далее". Пусть мы хотим написать такую простую стратегию:

  • Сигнал на покупку образуется, когда сигнальная линия стохастика выходит из зоны перепроданности и пробивает заданный уровень DownLevel снизу вверх (по умолчанию значение 10)
  • Сигнал на продажу образуется, когда сигнальная линия стохастика выходит из зоны перекупленности и пробивает заданный уровень UpLevel сверху вниз (по умолчанию значени 90)
  • Защитный стоп ставится на расстоянии 100 пунктов от цены открытия (может меняться)
  • Уровень Take Profit cтавится на расстоянии 100 пунктов от цены открытия (может меняться)
  • Параметры стохастика задаются через внешние параметры эксперта и тоже могут меняться.

Назовем наш новый советник Osc_test и введем необходимые внешние параметры:


Жмем кнопку "Готово" и видим, что "Мастер создания советника" вставил эти параметры в наш советник, созданный на основе шаблона.

Таким образом, мы добавили необходимые параметры при создании советника:

//+------------------------------------------------------------------+
//|                                                     Osc_test.mq4 |
//|                      Copyright © 2007, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2007, MetaQuotes Software Corp."
#property link      "https://www.metaquotes.net"
 
//---- input parameters
extern int       Kperiod=18;//5;
extern int       Dperiod=14;//3;
extern int       slowPeriod=10;//3;
extern int       UpLevel=90;
extern int       DownLevel=10;
extern int       StopLoss=100;
extern int       TakeProfit=100;
extern double    Lots=0.1;
 
// константа "вне рынка"
#define OP_BALANCE 6

Теперь пропишем в функции yourFunction() код, который выдает сигналы на покупку и продажу.

//+------------------------------------------------------------------+
//|  функция выдачи торговых сиглналов                               |
//+------------------------------------------------------------------+
int yourFunction(int workPeriod)
   {
   bool res=OP_BALANCE;
//----
   double prevValue = iStochastic(Symbol(),workPeriod,Kperiod,Dperiod,slowPeriod,MODE_SMA,0,MODE_SIGNAL,2);
   double currValue = iStochastic(Symbol(),workPeriod,Kperiod,Dperiod,slowPeriod,MODE_SMA,0,MODE_SIGNAL,1);
 
   if (currValue>DownLevel && prevValue<DownLevel) res=OP_BUY;
   if (currValue<UpLevel && prevValue>UpLevel) res=OP_SELL;
 
//----
   return (res);   
   }

Это потребовало всего четыре строчки. Осталось уточнить только функцию CalculateNewMarketValue(), которая готовит реквизиты для открытия позиции по рынку.

//+------------------------------------------------------------------+
//|  вычисляет реквизиты для открытия нового ордера                  |
//+------------------------------------------------------------------+
void CalculateNewMarketValues(int    trSignal,
                              int    & marketType,
                              double & marketLots, 
                              double & marketSL,
                              double & marketTP,
                              string & marketcomment
                              )
   {
   // если торгового сигнла нет - выходим
   //Print("CalculateNewMarketValues()  trSignal=",trSignal);
   if (trSignal==OP_BALANCE) return;
 
   marketType    =-1; // это значит, что открывать не будем
   marketLots    = 0;
   marketSL      = 0;
   marketTP      = 0;
   marketcomment = "";
 
 
//----
   //  вставить свой код для вычисления всех параметров
 
   if (trSignal==OP_BUY  && StopLoss!=0) marketSL = Bid - StopLoss*Point;
   if (trSignal==OP_SELL && StopLoss!=0) marketSL = Ask + StopLoss*Point;
 
   if (trSignal==OP_BUY  && TakeProfit!=0) marketTP = Bid + TakeProfit*Point;
   if (trSignal==OP_SELL && TakeProfit!=0) marketTP = Ask - TakeProfit*Point;
 
   marketLots = Lots;
 
   if (trSignal==OP_BUY) marketType = OP_BUY;
   if (trSignal==OP_SELL) marketType = OP_SELL;
 
   marketcomment="test";
//----
   return;   
   }

Как видите, здесь также потребовалось добавить только 5 (пять!) строчек. Таким образом, мы написали ровно столько кода, сколько было необходимо для описания нашей простой стратегии. Это первое преимущество такого подхода.

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


Дополнительные достоинства

Но и это еще не все. Запустим тестирование полученного советника на любом символе и с любым таймфреймом. Пусть это будет EURUSD H1. Все настройки по умолчанию.


В данном случае не важно - будет прибыль или нет. Смотрим отчет о тестировании.


Всего сделок 430, из них продаж 241 и покупок 189. Теперь перевернем систему, там где мы продавали - будем покупать, а где покупали, будем продавать. Для этого ставим параметр ReversTradeSignal в значение true. Это признак переворота системы.


Запускаем тестирование, остальные параметры не меняем при этом. Смотрим отчет:


Действительно, теперь у нас покупок 241, а продаж 189. Количество покупок и продаж поменялость местами. Процент выигрышных сделок также перевернулся. Нам не пришлось переписывать советника, чтобы проверить поведение перевернутого советника!

Но и это еще не все. У нас есть такой параметр, как TradeDay, вы не забыли? По умолчанию он равен нулю, но если мы хотим, чтобы советник торговал только по пятницам? Ставим значение равное 5.


Запускаем тестирование, остальные параметры опять таки не трогаем. Смотрим результат. Мы видим отчет тестера, который говорит нам - что было бы, если советник торговал только по пятницам по перевернутой системе.


Мы видим, что из первоначльных 430 сделок, у нас осталось только 81. Значит остальные приходились на другие дни недели. Тот факт, что результат по прежнему прибыльный, нас не интересует. Допустим, мы хотим узнать, как торговал бы советник на истории, если бы ему было разрешено торговать все дни недели, кроме пятницы. Для этого у нас есть параметр ReversDay - переключаем его на true.


Запускаем тестирование, смотрим отчет.


Было 430 сделок, вычли сделки пятницы (81), получили 349. Все сходится: 430-81=349. Значит мы правильно произвели переворот торговых дней. И ничего специально переделывать в советнике нам не пришлось.


Заключение

Я вижу два больших недостатка в данной статье: с одной стороны она слишком конспективна, и в ней подробно не расписано объяснение той или иной функции. С другой стороны, она получилась слишком большой для первого восприятия. Но я надеюсь, что у данного подхода к созданию советников на MQL4 со временем появится множество сторонников. Этот подход лучше чем какой-либо другой подходит для командной работы, более правильным является именно создание совместным мозговым штурмом необходимых шаблонов, чем производить постоянное улучшение какого-то одного алгоритма торговли.

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

Главное достоинство: разработано один раз - используется всегда. И при этом вероятность ошибочного кода нового советника также уменьшается.

Прикрепленные файлы |
Osc_test.mq4 (36.96 KB)
Template_EA.mqt (35.74 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (61)
[Удален] | 21 авг. 2012 в 14:24
incognitos:
  • По всему коду проходят функция переворачивания. Это совсем не надо. Достаточно сделать ТОЛЬКО функцию открытия ордера, которая открывает ордер по указанному типу переворачивая Ask и Bid в зависимости от этого типа. (например OpenOrder(type, lot, setprice, sl, tp);) И ВСЁ!..
    А переключатель переворота сигнала должен быть на выходе из функции анализатора (модуля программы который согласно ТС по разным индикатором и методам выдаёт всего один сигнал - со значением в каком направлении открываться). Вот этот сигнал переворачиваем если надо, и дальше отправляем в функцию OpenOrder.
И так можно, наверно. Нужно смотреть как поведут себя операции с Stop Loss и Take Profit.
[Удален] | 21 авг. 2012 в 14:25
incognitos:
  • (А вообще, тесты показывают что перевороты сигналов индикаторов не работают, это давно бы пора в словарике каком-нить прописать как аксиому, чтобы каждый новый программист не тратил месяцы своей жизни на что давно всем известно и проверенно миллионами тестов до него.)

Не факт. Вы можете обнаружить, что в сильном тренде традиционные сигналы осцилляторов подведут вас под монастырь. Я переворачивал некоторые системы, работали :)
[Удален] | 21 авг. 2012 в 14:26
incognitos:
  • Мне кажется в функции "isNewBar(int timeFrame)" ошибка, в case 1 наверно должно быть i=1:
    switch (timeFrame) { 
       case 1 :  i=0; break; 
       case 5 :  i=2; break; 
       case 15 : i=3; break;

Не могу сказать, нужно проверять.
[Удален] | 29 сент. 2013 в 19:57
Замечательная статья. Спасибо большое автору.
x37773x
x37773x | 25 сент. 2015 в 12:56

Да, всё замечательно, только теперь уже билд 880 и там всё по-другому...

Где теперь шаблоны экспертов? :)

Как легко и просто опубликовать видео на MQL4.community Как легко и просто опубликовать видео на MQL4.community
Показать обычно легче, чем рассказать. Предлагается простой и бесплатный способ создания видеороликов с помощью CamStudio для публикации на форумах MQL4.community.
Консультант-советник трейдера на основе расширенного анализа MACD Консультант-советник трейдера на основе расширенного анализа MACD
Скрипт консультант-советник трейдера по принятию решения об открытии позиций на основании расширенного анализа состояния MACD по трем последним барам в реальном времени торгов на любом периоде, и для проведения анализа на истории.
Эксперты на основе популярных торговых систем и алхимия оптимизации торгового робота (Часть 3) Эксперты на основе популярных торговых систем и алхимия оптимизации торгового робота (Часть 3)
В данной статье автор продолжает обзор алгоритмов реализации простейших торговых систем и знакомит с механизацией бэктестинга. Статья будет полезна начинающим трейдерам и начинающим экспертописателям
Эксперты на основе популярных торговых систем и алхимия оптимизации торгового робота (Часть 2) Эксперты на основе популярных торговых систем и алхимия оптимизации торгового робота (Часть 2)
В данной статье автор продолжает обзор алгоритмов реализации простейших торговых систем и знакомит с некоторыми, актуальными подробностями использования результатов оптимизаций. Статья будет полезна начинающим трейдерам и начинающим экспертописателям