Пишем утилиту для отбора и навигации по инструментам на языках MQL5 и MQL4

Roman Klymenko | 23 ноября, 2018

Введение

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

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

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

Начальный шаблон советника

Изначально создавать советник мы будем на языке MQL5. Однако, поскольку многие брокеры до сих пор не имеют в своем арсенале счетов на MetaTrader 5, в конце статьи мы переделаем полученный советник так, чтобы он стал работать и в торговой платформе MetaTrader 4.

Итак, начнем написание советника с шаблона, который практически ничем не отличается от того, который можно сгенерировать мастером MQL5:

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }
//+------------------------------------------------------------------+

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 

}

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

Единственная строка, которой данный шаблон отличается от того, который мог бы создать мастер MQL5, это #property strict. Она необходимо для того, чтобы наш советник в будущем корректно работал на MetaTrader 4. Поскольку на MetaTrader 5 она не оказывает существенного влияния, давайте сразу же внесем ее в наш шаблон.

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

Список символов, которые удовлетворяют нашим условиям, будем хранить в объекте типа CArrayString. Поэтому подключим к нашему советнику MQH-файл с описанием данного объекта:

#include <Arrays\ArrayString.mqh>

Также для работы с графиками нам будет необходим объект типа CChart. Подключим и его определение:

#include <Charts\Chart.mqh>

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

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

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

Входные параметры

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

sinput string        delimeter_01="";        // --- Настройки фильтрации ---
input bool           noSYMBmarketWath=true;  // Скрыть, если нет в Обзор рынка
input bool           noSYMBwithPOS=true;     // Скрыть, если есть позиции
input ValueOfSpread  hide_SPREAD=spread_b1;  // Скрыть, если спред
input uint           hide_PRICE_HIGH=0;      // Скрыть, если цена больше
input uint           hide_PRICE_LOW=0;       // Скрыть, если цена меньше
input bool           hideProhibites=true;    // Скрыть, если торговля запрещена
input bool           hideClosed=true;        // Скрыть, если рынок закрыт
input StartHour      hide_HOURS=hour_any;    // Показывать, если время открытия
input double         hideATRcents=0.00;      // Скрыть, если ATR менее, долларов
sinput string        delimeter_02="";        // --- Настройки графиков ---
input bool           addInfoWatch=false;     // Добавлять график в Обзор рынка
input bool           viewCandle=true;        // Открывать графики в виде свечей
input bool           viewVolumes=true;       // Показывать тиковые объемы
input bool           showInfoSymbol=true;    // Показывать направление движения
input bool           showNameSymbol=true;    // Показывать название символа

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

Перечисление ValueOfSpread определяет возможные условия по размеру спреда у символов, которые будут выводиться советником:

enum ValueOfSpread
  {
   spread_no,//Нет
   spread_b05,// > 0.05%
   spread_b1,// > 0.1% 
   spread_b15,// > 0.15% 
   spread_l15,// < 0.15% 
   spread_l1,// < 0.1% 
  }; 

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

Перечисление StartHour содержит список основных периодов, когда происходит открытие тех или иных рынков:

enum StartHour
  {
   hour_any, //Любое время
   hour_9am, // 9 утра
   hour_10am,// 10 утра 
   hour_4pm, // 16 вечера 
   hour_0am, // Полночь
  }; 

Значение 9 утра (или любое другое) не означает, что будут выводиться только символы, которые открываются ровно в указанное время. Оно значит, что будут отображены символы, которые открываются в этот час. Например, не только 9:00, но и в 9:05.

Соответственно, значение 16 вечера означает, что будут показаны только акции американского фондового рынка, открывающиеся в 16:30.

Значение 10 утра — это акции российского и европейского фондовых рынков.

В 9 утра открываются некоторые индексы.

Ну а Полночь — это время открытия рынка Forex, работающего круглосуточно.

Глобальные переменные

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

// префикс, который будет добавляться к именам всех создаваемых советником графических объектов:
string exprefix="finder";
// массив символов, которые удовлетворяют нашим условиям:
CArrayString arrPanel1;
// индекс текущего символа в массиве arrPanel1:
int panel1val;
// массив, в который мы будем помещать созданные советником графики (пока что один):
CChart charts[];
// массив, в который мы будем помещать указатели на созданные советником графики (пока что один):
long curChartID[];

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

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

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
#property strict

#include <Arrays\ArrayString.mqh>
#include <Charts\Chart.mqh>

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

enum ValueOfSpread
  {
   spread_no,//Нет
   spread_b05,// > 0.05 %
   spread_b1,// > 0.1 %
   spread_b15,// > 0.15 %
   spread_l15,// < 0.15 %
   spread_l1,// < 0.1 %
  }; 
enum StartHour
  {
   hour_any,//Любое время
   hour_9am,// 9 утра
   hour_10am,// 10 утра 
   hour_4pm,// 16 вечера 
   hour_0am,// Полночь
  }; 

input bool           noSYMBmarketWath=true; // Скрыть символы, которых нет в панели Обзор рынка
input bool           noSYMBwithPOS=true;    // Скрыть символы, по которым есть позиции
input ValueOfSpread  hide_SPREAD=spread_b1; // Скрыть символы, у которых спред
input uint           hide_PRICE_HIGH=0;     // Скрыть символы, у которых цена больше (0 - не скрывать)
input uint           hide_PRICE_LOW=0;      // Скрыть символы, у которых цена меньше (0 - не скрывать)
input bool           hideProhibites=true;   // Скрыть символы, торговля по которым запрещена
input StartHour      hide_HOURS=hour_any;   // Показывать только символы, открывающиеся в
input bool           viewCandle=true;       // Открывать графики в виде свечей

// префикс, который будет добавляться к именам всех создаваемых советником графических объектов:
string exprefix="finder";
// массив символов, которые удовлетворяют нашим условиям:
CArrayString arrPanel1;
// индекс текущего символа в массиве arrPanel1:
int panel1val;
// массив, в который мы будем помещать созданные советником графики (пока что один):
CChart charts[];
// массив, в который мы будем помещать указатели на созданные советником графики (пока что один):
long curChartID[];

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
  
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(reason!=REASON_CHARTCHANGE){
      ObjectsDeleteAll(0, exprefix);
   }
   EventKillTimer();
  }

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 

}

Функция фильтрации символов

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   start_symbols();

//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }

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

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

Итак, давайте посмотрим на функцию start_symbols. К слову, она также является оберткой для запуска других функций:

void start_symbols(){
   // устанавливаем в ноль (первый символ в массиве) индекс текущего символа в списке символов:
   panel1val=0;
   // подготавливаем список символов:
   prepare_symbols();
   // удаляем ранее созданные кнопки символов с графика:
   ObjectsDeleteAll(0, exprefix);
   // выводим список символов:
   show_symbols();
   // обновляем график, чтобы увидеть изменения:
   ChartRedraw(0);
}

Нам встретились еще 2 пользовательские функции: prepare_symbols и show_symbols. Первая формирует массив символов, удовлетворяющих нашим условиям. Вторая же выводит кнопки этих символов на график, где запущен советник.

В выводе кнопок на график нет ничего сложного. Сначала мы находим координаты X и Y, по которым нужно вывести очередную кнопку так, чтобы она не накладывалась на другие кнопки. Ну а потом просто выводим ее:

void show_symbols(){
   
   // инициализируем переменные для определения координат X и Y
   int btn_left=0;
   int btn_line=1;
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   
   // для каждого символа в массиве выводим кнопку на график
   // на кнопке будем писать название символа
   for( int i=0; i<arrPanel1.Total(); i++ ){
      if( btn_left>btn_right-BTN_WIDTH ){
         btn_line++;
         btn_left=0;
      }
      
      ObjectCreate(0, exprefix+"btn"+(string) i, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_COLOR,clrBlack); 
      ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TEXT,arrPanel1.At(i));    
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false);
      
      
      btn_left+=BTN_WIDTH;
   }
   
}

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

Вывод кнопок символов на график

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

void prepare_symbols(){
   // переменная для временного хранения названия символа
   string name;
   // переменная для хранения последних котировок по символу
   MqlTick lastme;
   
   // обнуляем массив символов, если в нем были какие-либо значения
   arrPanel1.Resize(0);
   
   // формируем временный массив tmpSymbols
   // он будет содержать все доступные символы
   CArrayString tmpSymbols;
   for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){
      tmpSymbols.Add(SymbolName(i, noSYMBmarketWath));
   }
   
   // здесь будет происходить проверка условий
   // и помещение символа в список символов,
   // если он удовлетворяет условиям
   for( int i=0; i<tmpSymbols.Total(); i++ ){
      name=tmpSymbols[i];
      
      // чистим название символа от лишних пробелов
      // мало ли откуда он мог к нам попасть
      StringTrimLeft(name);
      StringTrimRight(name);
      if( !StringLen(name) ){
         continue;
      }
      
      // далее будет происходить основная фильтрация символов
      // ...

      
      // если символ удовлетворяет всем нашим условиям, добавляем его в список
      arrPanel1.Add(name);
   }
}

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

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

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

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

Скрыть символы, по которым есть позиции:

      // Скрыть символы, по которым есть позиции
      bool isskip=false;
      if( noSYMBwithPOS ){
         // просматриваем список всех открытых позиций
         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // если по текущему символу есть позиция, тогда пропускаем
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }
      }

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

Скрыть символы, у которых спред:

      // если активны входящие параметры, использующие значение текущей цены по инструменту,
      // тогда пытаемся получить текущие значения
      if(hide_PRICE_HIGH>0 || hide_PRICE_LOW>0 || hide_SPREAD>0 ){
         SymbolInfoTick(name, lastme);
         if( lastme.bid==0 ){
            Alert("Получить значение BID не удалось. Некоторые функции фильтрации могут не работать.");
         }
      }
      if(hide_SPREAD>0 && lastme.bid>0){
         switch(hide_SPREAD){
            // если текущий спред больше 0.05% от цены - пропускаем символ
            case spread_b05:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.05 ){
                  isskip=true;
               }
               break;
            // если текущий спред больше 0.1% от цены - пропускаем символ
            case spread_b1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.1 ){
                  isskip=true;
               }
               break;
            // если текущий спред больше 0.15% от цены - пропускаем символ
            case spread_b15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.15 ){
                  isskip=true;
               }
               break;
            // если текущий спред меньше 0.15% от цены - пропускаем символ
            case spread_l15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.15 ){
                  isskip=true;
               }
               break;
            // если текущий спред меньше 0.1% от цены - пропускаем символ
            case spread_l1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.1 ){
                  isskip=true;
               }
               break;
         }
      }
      if(isskip){
         continue;
      }

Чем меньше спред, тем лучше. С этой точки зрения лучше всего работать с символами, у которых спред менее 0.05% от цены. Но не у всех брокеров можно найти такие хорошие условия. Особенно если вы торгуете на фондовом рынке.

Скрыть символы, у которых цена больше (0 - не скрывать):

      // Скрыть символы, у которых цена больше (0 - не скрывать)
      if(hide_PRICE_HIGH>0 && lastme.bid>0 && lastme.bid>hide_PRICE_HIGH){
         continue;
      }

Скрыть символы, у которых цена меньше (0 - не скрывать):

      if(hide_PRICE_LOW>0 && lastme.bid>0 && lastme.bid<hide_PRICE_LOW){
         continue;
      }

Скрыть символы, торговля по которым запрещена:

      if(hideProhibites){
         // если минимальный объем позиции по символу равен 0 - пропускаем
         if( SymbolInfoDouble(name, SYMBOL_VOLUME_MIN)==0 ) continue;
         // если по символу запрещено открывать позиции - пропускаем
         if(SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){
            continue;
         }
      }

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

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

      // получаем текущий день в переменную curDay
      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      // если ограничение по времени открытия рынков установлено, 
      // и получить время открытия по текущий акции для сегодняшнего дня удалось, тогда...
      if( hide_HOURS!=hour_any && SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

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

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

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

      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      
      bool sessionData=SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto);

      // скрыть символ, если рынок сегодня закрыт
      if( hideClosed && !sessionData ){
         continue;
      }
      
      // Показывать только символы, открывающиеся в
      // получаем текущий день в переменную curDay
      // если ограничение по времени открытия рынков установлено, 
      // и получить время открытия по текущий акции для сегодняшнего дня удалось, тогда...
      if( hide_HOURS!=hour_any && sessionData){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

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

      // Скрыть, если ATR менее, долларов
      if(hideATRcents>0){
         MqlRates rates[];
         ArraySetAsSeries(rates, true);
         double atr;
         if(CopyRates(name, PERIOD_D1, 1, 5, rates)==5){
            atr=0;
            for(int j=0; j<5; j++){
               atr+=rates[j].high-rates[j].low;
            }
            atr/=5;
            if( atr>0 && atr<hideATRcents ){
               continue;
            }
         }
      }

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

Механизм открытия графиков

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

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

Функция OnChartEvent имеет 4 входящих параметра. Самый парвый параметр, id, содержит идентификатор события, которое в данный момент было перехвачено функцией OnChartEvent. Чтобы понять, что функция OnChartEvent была вызывана именно после нажатия на какую-либо кнопку на графике, нужно сравнить значение данного параметра с нужным нам.

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

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // код, который будет выполнен при нажатии кнопки
         break;
   }

}

Как же нам понять, какая именно кнопка была нажата на графике? В этом нам поможет второй параметр функции OnChartEvent: параметр sparam. Для события CHARTEVENT_OBJECT_CLICK он содержит название кнопки, которую нажал пользователь. Так что нам осталось только сравнить это название с теми, которые генерирует наш советник, и если это кнопка нашего советника, то открыть график нужного символа. Название символа, который нужно открыть, мы возьмем из текста, который написан на кнопке. В результате у нас получится следующий код, который нужно поместить внутрь условия case CHARTEVENT_OBJECT_CLICK:

         // если в названии кнопки встречается строка, которая содержится во всех графических объектах,
         // создаваемых нашим советником, тогда...
         if( StringFind(sparam, exprefix+"btn")>=0 ){
            // помещаем в переменную panel1val номер текущей кнопки
            // то есть, позицию текущего символа в списке символов
            string tmpme=sparam;
            StringReplace(tmpme, exprefix+"btn", "");
            panel1val=(int) tmpme;
            
            // открываем график данного символа
            showcharts(ObjectGetString(0,sparam,OBJPROP_TEXT));
         }

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

void showcharts(string name){
   // если ранее уже были открыты графики, то закрываем их
   closecharts();
   
   // Добавляем символ в панель Обзор рынка, если его там нет
   // и если входной параметр "Добавлять график в Обзор рынка" равен true
   if( addInfoWatch ){
      SymbolSelect(name, true);
   }
   
   // открываем график и помещаем в массив curChartID идентификатор данного графика
   curChartID[ArrayResize(curChartID,ArraySize(curChartID)+1)-1]=charts[(uchar) ArrayResize(charts,ArraySize(charts)+1)-1].Open( name, PERIOD_D1 );
   
   // если входной параметр "Открывать графики в виде свечей" равен true,
   // то переводим график в режим отображения японских свечей
   if(viewCandle){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_MODE, CHART_CANDLES);
   }
   // если входной параметр "Показывать тиковые объемы" равен true,
   // то показываем тиковые объемы
   if(viewVolumes){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SHOW_VOLUMES, CHART_VOLUME_TICK);
   }
   // меняем масштаб графика до наиболее удобного
   ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SCALE, 2);
      
   // подождем треть секунды, чтобы все изменения успели внестись
   Sleep(333);
   // обновляем открытый график, чтобы внести на него все изменения
   ChartRedraw(curChartID[ArraySize(curChartID)-1]);
   
}
Закрытие ранее открытых советником графиков выполняет функция closecharts. Ее код весьма прост:
void closecharts(){
   // если массив графиков, открытых советником, не пуст, тогда...
   if(ArraySize(charts)){
      // последовательно закрываем все графики
      for( int i=0; i<ArraySize(charts); i++ ){
         charts[i].Close();
      }
      // очищаем массив графиков
      ArrayFree(charts);
   }
   // если массив идентификаторов графиков, открытых советником, не пуст, тогда очищаем его
   if(ArraySize(curChartID)){
      ArrayFree(curChartID);
   }
}

Выводим дополнительную информацию по символам

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

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

Для этого в функцию showcharts, перед вызовом функции Sleep, добавим следующий код:

   //выводим дополнительную информацию на график
   string msg="";
   if(showNameSymbol){
      StringAdd(msg, getmename_symbol(name)+"\r\n");
   }
   if(showInfoSymbol){
      StringAdd(msg, getmeinfo_symbol(name, false)+"\r\n");
   }
   if( StringLen(msg)>0 ){
      ChartSetString(curChartID[ArraySize(curChartID)-1], CHART_COMMENT, msg);
   }

Если входной параметр showNameSymbol равен true, то мы вызываем функцию getmename_symbol, которая вернет нам строку с названием символа. Если входной параметр showInfoSymbol равен true, то мы вызываем функцию getmeinfo_symbol, которая вернет нам строку с названием символа:

string getmename_symbol(string symname){
   return SymbolInfoString(symname, SYMBOL_DESCRIPTION);
}
string getmeinfo_symbol(string symname, bool show=true){
   MqlRates rates2[];
   ArraySetAsSeries(rates2, true);
   string msg="";

   if(CopyRates(symname, PERIOD_D1, 0, 1, rates2)>0){
      if(show){
         StringAdd(msg, (string) symname+": ");
      }
      StringAdd(msg, "D1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2) +"%");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2) +"%");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   if(CopyRates(symname, PERIOD_H1, 0, 1, rates2)>0){
      StringAdd(msg, ", H1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2)+"% (+"+DoubleToString(rates2[0].close-rates2[0].open, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2)+"% (-"+DoubleToString(rates2[0].open-rates2[0].close, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   
   return msg;
}

В результате мы увидим следующую информацию на открывшемся графике:

Выводим дополнительную информацию по символу

Управляем советником с клавиатуры

Пока мы еще находимся в функции OnChartEvent, давайте добавим туда реакцию на нажатие нескольких клавиш клавиатуры:

За обработку нажатия клавиш клавиатуры отвечает событие с id CHARTEVENT_KEYDOWN. При этом код нажатой клавиши передается в уже известном нам параметре sparam. Так что нам нужно просто добавить в оператор switch следующее условие:

      case CHARTEVENT_KEYDOWN:
         switch((int) sparam){
            case 45: //x
               ExpertRemove();
               break;
            case 19: //r
               start_symbols();
               break;
         }
         break;

Как видите, при нажатии клавиши R мы будем просто вызывать ранее созданную пользовательскую функцию start_symbols.

Добавляем навигацию по графикам

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

Добавим мы всего 3 кнопки: для перехода на следующий график, для перехода на предыдущий график и для закрытия графика.

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

Поэтому создавать кнопки мы будем в стандартной функции OnTimer. То есть, периодически мы будем проверять, открыт ли какой-либо график советником. Если график открыт, то есть ли кнопки на нем. И только если кнопок нет, тогда создадим их:

void OnTimer()
  {
   // если в массиве идентификаторов открытых графиков есть значения, тогда...
   uchar tmpCIDcnt=(uchar) ArraySize(curChartID);
   if(tmpCIDcnt>0 ){
      // если последний идентификатор в массиве не испорчен, тогда...
      if(curChartID[tmpCIDcnt-1]>0){
         // если на графике с данным идентификатором нет кнопок, тогда создадим их
         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }
      }
   }
   
  }

Создание кнопок на графике происходит в пользовательской функции createBTNS. Ее код весьма прост:

void createBTNS(long CID){
   ObjectCreate(CID, exprefix+"_p_btn_prev", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YDISTANCE,90); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_prev",OBJPROP_TEXT,"Prev chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_next", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YDISTANCE,65); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_next",OBJPROP_TEXT,"Next chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_close", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YDISTANCE,40); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_close",OBJPROP_TEXT,"Close chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_SELECTABLE,false); 
   
   // обновляем график, чтобы увидеть внесенные изменения
   ChartRedraw(CID);
}

В результате новый график примет следующий вид:

Кнопки навигации по списку символов

Добавляем реакцию на нажатие кнопок

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

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

Возможно, есть какие-то более удобные способы. Но я придумал только один способ, как можно реагировать на изменения, которые произошли на стороннем графике. Поможет нам в этом стандартная функция OnTimer. Если на графике есть наши кнопки, то мы просто будем проверять, нажата ли какая-нибудь из них. И если нажата, то будем выполнять нужное действие. В результате условие:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }

...будет переписано следующим образом:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }else{
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_prev",OBJPROP_STATE)==true ){
               prevchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_next",OBJPROP_STATE)==true ){
               nextchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_close",OBJPROP_STATE)==true ){
               closecharts();
               return;
            }
         }

Если нажата кнопка Prev chart, то вызываем функцию prevchart. Если нажата кнопка Next chart, то вызываем функцию nextchart. А при нажатии кнопки Close chart вызывается уже известная нам функция closecharts. Функции prevchart и nextchart похожи друг на друга:

void nextchart(){
   // если в списке символов есть следующий символ, то открываем его график
   // иначе закрываем текущий график
   if(arrPanel1.Total()>(panel1val+1)){
      panel1val++;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}
void prevchart(){
   // если в списке символов есть предыдущий символ, то открываем его график
   // иначе закрываем текущий график
   if(arrPanel1.Total()>(panel1val-1) && (panel1val-1)>=0){
      panel1val--;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}

Заключение

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

Конечно, можно придумать еще множество способов улучшить наш советник. Но и сейчас это уже полнофункциональный продукт, который способен существенно облегчить трейдеру отбор акций.

Переносим утилиту на язык MQL4

А теперь давайте попробуем перенести нашу утилиту на язык MQL4. Удивительно, но процес переноса будет заключаться в переписывании всего одного блока кода. И займет у нас минут 5. Так что давайте приступим.

Начнем с того, что создадим новый советник в редакторе MetaEditor версии 4. После чего перенесем в него исходный код нашего советника на MQL5.

Давайте попробуем сразу скомпилировать советник. Конечно, это нам не получится сделать. Но в результате мы получим список ошибок, которые нужно исправить. И таких ошибок всего 3:

Дважды кликните по первой ошибке, чтобы перейти на строку советника, где она была обнаружена.

Ошибка 'PositionsTotal' - function not defined. Ошибка обнаружена в следующем блоке кода функции prepare_symbols:

         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // если по текущему символу есть позиция, тогда пропускаем
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }

Одним из существенных отличий языков MQL4 и MQL5 является работа с позициями и ордерами. Поэтому для корректной работы советника в MetaTrader 4 данный блок кода нужно переписать следующим образом:

         int cntMyPos=OrdersTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol() == name ){
               isskip=true;
               break;
            }
         }

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

Другие ошибки. Возможно, вы не поверите, но на этом наши правки закончены. Все остальные ошибки устранились автоматически, так как они были как раз в исправленном блоке кода.