English 中文 Español Deutsch 日本語 Português
Утилита для отбора и навигации на MQL5 и MQL4: добавляем автоматичекий поиск паттернов с показом найденных символов

Утилита для отбора и навигации на MQL5 и MQL4: добавляем автоматичекий поиск паттернов с показом найденных символов

MetaTrader 5Статистика и анализ | 29 января 2019, 13:22
3 112 2
Roman Klymenko
Roman Klymenko

Введение

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

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

Уже по традиции, наша утилита будет работать одновременно и в MQL4, и в MQL5. Забегая наперед сразу скажем, что в MQL5 открытие вкладок автофильтрации происходит медленнее, чем в MQL4. Это происходит в тех случаях, если по запрошенному символу нет истории на нужную глубину. В таких случаях MetaTrader 5 сам запросит историю с торгового сервере и посторит нужные таймфреймы.

Поэтому если при нажатии на вкладку ничего не происходит или вообще все кнопки исчезли, то не нервничайте и просто подождите. Через десяток-другой секунд нужные вам символы отобразятся. Все же, даже 20 секунд ожидания — это быстрее, чем вручную просматривать графики сотни инструментов. С другой стороны в MetaTrader 4 вам нужно самостоятельно обеспечить историю по нужным символам и таймфреймам, что также требует времени и внимания к таким мелочам.

Более того, если у вашего компьютера небольшое количество оперативной памяти (или вы работаете с виртуальной машины, которой доступно небольшое количество ОЗУ, в пределах 1 Гб), то в MQL5 работа советника при открытии вкладок автофильтрации вообще может прекращаться ошибкой нехватки памяти. В MQL4 подобной проблемы нет, так как вся история по таймфреймам подкачивается независимо и на разную глубину. Решить данную проблему в MQL5 можно ограничением в настройках "Макс. баров в окне".

Добавляем функционал вкладок автофильтрации

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

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

string panelNamesAddon[9]={"Air Level", "Parabolic", "Gap", "4 weeks Min/Max", "365 days Min/Max", "Round levels", "Mostly Up/Down", "All time high/low", "High=Close"};

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

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

void show_panel_buttons(){
   int btn_left=0;
   // определяем максимально возможную координату по оси x, на которой можно будет выводить вкладки.
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   string tmpName="";
   
   for( int i=0; i<ArraySize(panelNames); i++ ){
      // если координата начала новой кнопки больше максимально возможной,
      // то переходим на новую строку.
      if( btn_left>btn_right-BTN_WIDTH ){
         btn_line++;
         btn_left=0;
      }
      // если во вкладке "домашки" есть символы, то к названию вкладки
      // добавляем их количество
      tmpName=panelNames[i];
      if(i>0 && arrPanels[i].Total()>0 ){
         tmpName+=" ("+(string) arrPanels[i].Total()+")";
      }
      
      // выводим кнопки вкладок
      ObjectCreate(0, exprefix+"panels"+(string) i, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_COLOR,clrBlack); 
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_BGCOLOR,clrSilver); 
      ObjectSetString(0,exprefix+"panels"+(string) i,OBJPROP_TEXT,tmpName);
      ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_SELECTABLE,false);
      // если в данный момент активна вкладка данной кнопки,
      // то делаем ее нажатой
      if( cur_panel == i ){
         ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_STATE, true);
      }
      
      btn_left+=BTN_WIDTH;
   }
   // Если вывод вкладок автофильтрации разрешен входящим параметром, то выведем их
   if(useAddonsLevels){
      for( int i=0; i<ArraySize(panelNamesAddon); i++ ){
         if( btn_left>btn_right-BTN_WIDTH ){
            btn_line++;
            btn_left=0;
         }
         tmpName=panelNamesAddon[i];
         // Если вкладка называется Gap, то выведем на нее текущее значение входящего параметра,
         // определяющего процент гэпа
         if(tmpName=="Gap"){
            StringAdd(tmpName, " ("+(string) gap_min+"%)");
         }
         
         ObjectCreate(0, exprefix+"panels"+(string) (i+ArraySize(panelNames)), OBJ_BUTTON, 0, 0, 0);
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_XDISTANCE,btn_left); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_XSIZE,BTN_WIDTH); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_YSIZE,BTN_HEIGHT); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_FONTSIZE,8); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_COLOR,clrBlack); 
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_BGCOLOR,clrLightSteelBlue); 
         ObjectSetString(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_TEXT,tmpName);
         ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_SELECTABLE,false);
         if( cur_panel == i+ArraySize(panelNames) ){
            ObjectSetInteger(0,exprefix+"panels"+(string) (i+ArraySize(panelNames)),OBJPROP_STATE, true);
         }
         
         btn_left+=BTN_WIDTH;
      }
   }

   // выводим комментарий, если он задан:
   if(StringLen(cmt)>0){
      string tmpCMT=cmt;
      if(from_txt){
         StringAdd(tmpCMT, ", from txt");
      }
      ObjectCreate(0, exprefix+"title", OBJ_EDIT, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"title",OBJPROP_XDISTANCE,btn_left+11); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_XSIZE,133); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_COLOR,clrGold); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_BGCOLOR,clrNONE); 
      ObjectSetInteger(0,exprefix+"title",OBJPROP_BORDER_COLOR,clrBlack);
      ObjectSetString(0,exprefix+"title",OBJPROP_TEXT,tmpCMT);
      ObjectSetInteger(0,exprefix+"title",OBJPROP_SELECTABLE,false);
   }
   

}

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

sinput string        delimeter_05="";      // --- Дополнительные вкладки ---
input bool           useAddonsLevels=true; // Вывести доп. вкладки
input bool           addons_infowatch=true;// Скрыть символ, если нет в Обзор рынка
input int            addon_tabs_scale=3;   // Масштаб доп. вкладок (0-5)

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

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

Добавляем вкладки автофильтрации

Но вернемся к коду. Мы научились добавлять вкладки автофильтрации. Но пока что при клике на них никаких символов выводиться не будет. Так как сам функционал вывода символов еще не реализован. Все кнопки открытой в данный момент вкладки выводятся с помощью функции show_symbols. Именно сюда мы добавим функционал вывода вкладок автофильтрации. В результате функция примет следующий вид:

void show_symbols(){
   
   // инициализируем переменные для определения координат X и Y
   int btn_left=0;
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   btn_line++;
   
   
   
   if( cur_panel==0 ){
      ObjectCreate(0, exprefix+"clear_"+(string) cur_panel, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_COLOR,clrBlack); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_BGCOLOR,clrPaleTurquoise); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_SELECTABLE,false);
      ObjectSetString(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_TEXT,"Clear All");
      btn_left+=BTN_WIDTH;
      ObjectCreate(0, exprefix+"showpos", OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_COLOR,clrBlack); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_BGCOLOR,clrPaleTurquoise); 
      ObjectSetInteger(0,exprefix+"showpos",OBJPROP_SELECTABLE,false);
      ObjectSetString(0,exprefix+"showpos",OBJPROP_TEXT,"Show Pos");
      btn_left+=BTN_WIDTH;
   }else if( cur_panel<ArraySize(arrPanels) && arrPanels[cur_panel].Total()>0 ){
      ObjectCreate(0, exprefix+"clear_"+(string) cur_panel, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_COLOR,clrBlack); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_BGCOLOR,clrPaleTurquoise); 
      ObjectSetInteger(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_SELECTABLE,false);
      ObjectSetString(0,exprefix+"clear_"+(string) cur_panel,OBJPROP_TEXT,"Clear");
      btn_left+=BTN_WIDTH;
      ObjectCreate(0, exprefix+"new_"+(string) cur_panel, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_COLOR,clrBlack); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_BGCOLOR,clrPaleTurquoise); 
      ObjectSetInteger(0,exprefix+"new_"+(string) cur_panel,OBJPROP_SELECTABLE,false);
      ObjectSetString(0,exprefix+"new_"+(string) cur_panel,OBJPROP_TEXT,"Open All");
      btn_left+=BTN_WIDTH;
   }
   
   // если номер текущей открытой вкладки больше, чем количество элементов массива вкладок домашнего задания
   // и вкладки со всеми символами,
   // значит это вкладка автофильтрации
   if(cur_panel>(ArraySize(arrPanels)-1)){
      MqlRates rates[];
      ArraySetAsSeries(rates, true);
      int tmpNumAddon=cur_panel-ArraySize(arrPanels);
      addonArr.Resize(0);
      arrTT.Resize(0);
      string addonName;
      
      CArrayString tmpSymbols;
      if( tmpNumAddon==0 && air_only_home ){
         for( int j=1; j<ArraySize(arrPanels); j++ ){
            for( int k=0; k<arrPanels[j].Total(); k++ ){
               string curName=arrPanels[j].At(k);
               bool isYes=false;
               for( int r=0; r<tmpSymbols.Total(); r++ ){
                  if(tmpSymbols.At(r)==curName){
                     isYes=true;
                     break;
                  }
               }
               if(!isYes){
                  tmpSymbols.Add(arrPanels[j].At(k));
               }
            }
         }
      }else{
         if( ArraySize(result)>1 ){
            for(int j=0;j<ArraySize(result);j++){
               StringReplace(result[j], " ", "");
               if(StringLen(result[j])<1){
                  continue;
               }
               tmpSymbols.Add(onlySymbolsPrefix+result[j]+onlySymbolsSuffix);
            }
         }else{
            for( int i=0; i<SymbolsTotal(addons_infowatch); i++ ){
               tmpSymbols.Add(SymbolName(i, addons_infowatch));
            }
         }
      }
      
      switch(tmpNumAddon){
         case 0: // air levels
            // код вывода содержимого вкладки
            break;
         case 1: // parabolic
            // код вывода содержимого вкладки
            break;
         case 2: // Gap
            // код вывода содержимого вкладки
            break;
         case 3: //4 weeks min/max
            // код вывода содержимого вкладки
            break;
         case 4: //365 days min/max
            // код вывода содержимого вкладки
            break;
         case 5: // round levels
            // код вывода содержимого вкладки
            break;
         case 6: // mostly up/down
            // код вывода содержимого вкладки
            break;
         case 7: // all time high/low
            // код вывода содержимого вкладки
            break;
         case 8: // high=close
            // код вывода содержимого вкладки
            break;
      }
      
      // для каждого символа в массиве выводим кнопку на график
      // на кнопке будем писать название символа
      for( int i=0; i<addonArr.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,addonArr.At(i));    
         ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false);
         if( arrTT.At(i)>0 ){
            ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TOOLTIP,(string) arrTT.At(i));    
         }
   
         if( checkSYMBwithPOS(addonArr.At(i)) ){
            ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_BGCOLOR,clrPeachPuff);
         }
         
         btn_left+=BTN_WIDTH;
      }
      
      // поскольку кнопки мы вывели, выходим из функции
      return;
   }
   
   // для каждого символа в массиве активной в данный момент вкладки
   // выводим кнопку на график
   // на кнопке будем писать название символа
   for( int i=0; i<arrPanels[cur_panel].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,arrPanels[cur_panel].At(i));    
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false);

      if( !noSYMBwithPOS || cur_panel>0 ){
         if( checkSYMBwithPOS(arrPanels[cur_panel].At(i)) ){
            ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_BGCOLOR,clrPeachPuff);
         }
      }
      
      btn_left+=BTN_WIDTH;
   }
   
}

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

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

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

            for( int i=0; i<tmpSymbols.Total(); i++ ){
               addonName=tmpSymbols[i];
               // код, определяющий, выводить символ или нет
            }

Воздушные уровни (проторговки)

При торговле от уровней все входы выполняются возле проторговок. То есть в ситуации, когда цена на каждом баре своим high или low бьется в одну и ту же или близкую цену. Например, как показано на рисунке:

Пример проторговки

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

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

Как правило, проторговки ищут на 5-минутных графиках. Но это не обязательное условие. Кто-то может работать и на 15-минутках, а кто-то на 30-минутных графиках. Считается, что чем старше таймфрейм, на котором обнаружена проторговка, тем качественнее будет вход.

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

sinput string        delimeter_06=""; // --- Доп. вкладка Воздушные уровни ---
input bool           air_level_m5=true; // Искать воздушные уровни на M5
input bool           air_level_m15=true; // Искать воздушные уровни на M15
input bool           air_level_m30=false; // Искать воздушные уровни на M30
input bool           air_level_h1=false; // Искать воздушные уровни на H1
input bool           air_level_h4=false; // Искать воздушные уровни на H4
input bool           air_level_d1=false; // Искать воздушные уровни на D1

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

Также добавим еще несколько входящих параметров:

input uchar          air_level_count=4; // Количество баров уровня
input uchar          air_level_offset=3; // Смещение относительно уровня, пунктов
input bool           air_level_cur_day=true; // Только уровни по направлению сегодняшнего дня
input bool           air_level_prev_day=true; // Только уровни по направлению вчерашнего дня
input bool           air_only_home=false; // Искать только в "домашке"

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

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

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

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

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

Ну а теперь давайте посмотрим на сам код, фильтрующий символы, на которых в данный момент есть проторговки. Ниже представлен код для таймфрейма M5:

               if(air_level_m5 && CopyRates(addonName, PERIOD_M5, 0, air_level_count+1, rates)==air_level_count+1){
                  if( (!air_level_cur_day || getmeinfoday_symbol(addonName, 0)<=0) && (!air_level_prev_day || getmeinfoday_symbol(addonName, 1)<=0) && rates[0].high<rates[1].high ){
                     bool isOk=true;
                     for( int j=1; j<air_level_count; j++ ){
                        if( MathAbs(rates[1].high-rates[j+1].high) <= air_level_offset*SymbolInfoDouble(addonName, SYMBOL_POINT) ){
                        }else{
                           isOk=false;
                        }
                     }
                     if(isOk && !skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(rates[1].high);
                     }
                  }else if( (!air_level_cur_day || getmeinfoday_symbol(addonName, 0)>=0) && (!air_level_prev_day || getmeinfoday_symbol(addonName, 1)>=0) && rates[0].low>rates[1].low ){
                     bool isOk=true;
                     for( int j=1; j<air_level_count; j++ ){
                        if( MathAbs(rates[1].low-rates[j+1].low) <= air_level_offset*SymbolInfoDouble(addonName, SYMBOL_POINT) ){
                        }else{
                           isOk=false;
                        }
                     }
                     if(isOk && !skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(rates[1].low);
                     }
                  }
               }

Здесь и во всех остальных примерах кода вкладок автофильтрации мы сразу же после вызова функции CopyRates начинаем работу с результатами ее вызова. Это не совсем правильно. В справочнике по языку MQL рекомендуется подождать некоторое время, чтобы утилита успела получить и записать данные в массив. Но даже если мы будем ждать 50 миллисекунд, то при проверке 100 символов это уже дополнительная задержка в 5 секунд. А у многих брокеров символов фондового рынка не 100, а многие сотни. И при использовании какой-либо задержки ожидание вывода содержимого вкладки может существенно затянуться.

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

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

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

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

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

Параболические закругления

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

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

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

Пример параболического закругления

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

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

               if(CopyRates(addonName, PERIOD_M5, 0, 6, rates)==6){
                  if( rates[0].low>rates[1].low && rates[1].low>rates[2].low && rates[2].low<=rates[3].low && rates[3].low<=rates[4].low && rates[4].low<=rates[5].low ){
                     if(!skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(0);
                     }
                  }else if( rates[0].high<rates[1].high && rates[1].high<rates[2].high && rates[2].high>=rates[3].high && rates[3].high>=rates[4].high && rates[4].high>=rates[5].high ){
                     if(!skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(0);
                     }
                  }
               }

То есть если low текущего бара больше low предыдущего. При этом low предыдущего бара больше low предпредыдущего. И при этом остальные 3 предыдущих бара либо равны по low друг другу, либо каждый из них меньше другого. Другими словами, если сначала идут 3 бара вниз, а потом 2 бара вверх, то будем считать это началом параболического закругления в Long.

Гэпы

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

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

Исходный код данной вкладки прост:

               if(CopyRates(addonName, PERIOD_D1, 0, 2, rates)==2){
                  if( rates[0].open>rates[1].close+(rates[0].open*(gap_min/100)) || rates[0].open<rates[1].close-(rates[0].open*(gap_min/100)) ){
                     if(!skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(0);
                     }
                  }
               }

4-недельные минимумы/максимумы

Список инструментов, по которым цена в данный момент является максимальной/минимальной за 4 недели, позволяет узнать вкладка 4 weeks Min/Max. Её код:

               if(CopyRates(addonName, PERIOD_W1, 0, 4, rates)==4){
                  bool newMin=true;
                  bool newMax=true;
                  if( rates[0].close!=rates[0].high && rates[0].close!=rates[0].low ){
                     newMin=false;
                     newMax=false;
                  }else{
                     for( int j=1; j<4; j++ ){
                        if( rates[0].high < rates[j].high ){
                           newMax=false;
                        }
                        if( rates[0].low > rates[j].low ){
                           newMin=false;
                        }
                     }
                  }
                  if( newMin || newMax ){
                     if(!skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(0);
                     }
                  }
               }

Годовые минимумы/максимумы

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

               if(CopyRates(addonName, PERIOD_W1, 0, 52, rates)==52){
                  bool newMin=true;
                  bool newMax=true;
                  if( rates[0].close!=rates[0].high && rates[0].close!=rates[0].low ){
                     newMin=false;
                     newMax=false;
                  }else{
                     for( int j=1; j<52; j++ ){
                        if( rates[0].high < rates[j].high ){
                           newMax=false;
                        }
                        if( rates[0].low > rates[j].low ){
                           newMin=false;
                        }
                     }
                  }
                  if( newMin || newMax ){
                     if(!skip_symbol(addonName)){
                        addonArr.Add(addonName+" (M5)");
                        arrTT.Add(0);
                     }
                  }
               }

Цена возле круглых уровней

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

Круглой ценой будем считать цену, которая заканчивается на 0 или 50 центов. Например, 125 долларов 0 центов, или 79 долларов 50 центов.

В результате у нас получится следующий код:

               switch((int) SymbolInfoInteger(addonName, SYMBOL_DIGITS)){
                  case 0:
                     break;
                  case 2:
                     if(CopyRates(addonName, PERIOD_M5, 0, 1, rates)==1){
                        double tmpRound=rates[0].close - (int) rates[0].close;
                        if( (tmpRound>0.46 && tmpRound<0.54) || tmpRound>0.96 || tmpRound<0.04 ){
                           if(!skip_symbol(addonName)){
                              addonArr.Add(addonName+" (M5)");
                              arrTT.Add(0);
                           }
                        }
                     }
                     break;
               }
То есть, мы будем определять круглые цены только для инструментов, в ценах которых имеются 2 знака после запятой. Если вы работаете с другими инструментами, просто по аналогии добавьте для них собственную проверку.

Большую часть времени вверх/вниз

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

sinput string        delimeter_08=""; // --- Доп. вкладка Mostly Up/Down ---
input int            mostly_count=15; // Проверять последние, дней
input int            mostly_percent=90; // Процент в одну сторону, более

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

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

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

Итак, код данной вкладки:

               if(CopyRates(addonName, PERIOD_D1, 1, mostly_count, rates)==mostly_count){
                     int mostlyLong=0;
                     int mostlyShort=0;
                     for( int j=0; j<mostly_count; j++ ){
                        if(rates[j].close>rates[j].open){
                           mostlyLong++;
                        }else if(rates[j].close<rates[j].open){
                           mostlyShort++;
                        }
                     }
                     if( !mostlyLong || !mostlyShort ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }else if( ((mostlyLong*100)/(mostlyLong+mostlyShort)) >= mostly_percent ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }else if( ((mostlyShort*100)/(mostlyLong+mostlyShort)) >= mostly_percent ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }
               }

Каждый бар с новым high/low

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

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

sinput string        delimeter_09=""; // --- Доп. вкладка All time High/Low ---
input ENUM_TIMEFRAMES alltime_period=PERIOD_D1; // Период
input int            alltime_count=15; // Проверять последние, баров

При этом код фильтрации будет следующим:

               if(CopyRates(addonName, alltime_period, 1, alltime_count, rates)==alltime_count){
                     bool alltimeHigh=true;
                     bool alltimeLow=true;
                     for( int j=1; j<alltime_count; j++ ){
                        if(rates[j].high>rates[j-1].high){
                           alltimeHigh=false;
                        }
                        if(rates[j].low<rates[j-1].low){
                           alltimeLow=false;
                        }
                     }
                     if( alltimeHigh || alltimeLow ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }
               }

Сессия закрылась на максимумах/минимумах дня

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

  • до открытия сессии инструменты, по которым в предыдущий день цена закрылась на своих максимумах/минимумах;
  • после открытия сессии инструменты, которые в данный момент находятся на своих максимумах/минимумах за день.

Есть мнение, что если цена закрылась на максимуме/минимуме за день, значит покупатель/продавец еще не успел реализовать свои планы. А значит, на следующий день цена пойдет в ту же сторону.

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

sinput string        delimeter_10=""; // --- Доп. вкладка High=Close ---
input ENUM_TIMEFRAMES highclose_period=PERIOD_D1; // Период
input int            highclose_offset=0; // Погрешность от максимумом/минимумов, пунктов

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

Итак, код нашей вкладки:

               if(CopyRates(addonName, highclose_period, 0, 1, rates)==1){
                     if( rates[0].close+highclose_offset >= rates[0].high ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }else if( rates[0].close-highclose_offset <= rates[0].low ){
                        addonArr.Add(addonName);
                        arrTT.Add(0);
                     }
               }

Добавление собственных вкладок

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

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

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

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

Во-вторых, в оператор switch функции show_symbols нужно добавить новый case, со значением, на 1 больше максимального из используемых значений. А уже внутри оператора case пишется код проверки, удовлетворяет ли текущий символ условиям. Шаблон кода будет следующим:

         case номер: // название вкладки
            for( int i=0; i<tmpSymbols.Total(); i++ ){
               addonName=tmpSymbols[i];

               // код проверки
            }
            
            break;

Заключение

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

При этом сегодня мы вообще ни разу не переписывали код в зависимости от версии языка MQL. Все что было написано сегодня, будет сразу работать и на MQL4, и на MQL5.

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

Нет-нет, я не объявляю войну классам. Тем более что классы уже добавлены в языке MQL4 и в CodeBase появляются коды с ними. Просто предлагаю на время отложить данную возможность языка MQL5.

Прикрепленные файлы |
finder.ex5 (270.41 KB)
finder.mq5 (188.2 KB)
finder4.ex4 (178.96 KB)
finder4.mq4 (188.2 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Andrey Paradox
Andrey Paradox | 29 янв. 2019 в 15:06

Спасибо, интересная работа, изучим.

Igor Makanu
Igor Makanu | 29 янв. 2019 в 19:46
 Поэтому, возможно, стоит на время забыть о всяких классах и других уникальных возможностях MQL5, и вести разработку так, чтобы ваши труды были доступны как можно большему количеству трейдеров?

тогда почему используете классы из стандартной библиотеки МТ ?

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

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

Исследование методов свечного анализа (Часть I): Проверка существующих паттернов Исследование методов свечного анализа (Часть I): Проверка существующих паттернов
В данной статье рассмотрим известные свечные модели(паттерны) и исследуем насколько они актуальны и эффективны в сегодняшних реалиях. Свечной анализ появился более 20 лет назад и с тех пор стал достаточно популярным. Некоторые даже считают, что японские свечи самый удобный и легко воспринимаемый формат отображения цен активов.
ZigZag всему голова (Часть I): Разработка базового класса индикатора ZigZag всему голова (Часть I): Разработка базового класса индикатора
Многие исследователи не уделяют должного внимания определению характера поведения цены. При этом используются сложные методы, которые очень часто являются просто «чёрными ящиками», такие как: машинное обучение или нейронные сети. В таких случаях самым важным является такой — «Какие данные подать на вход для обучения той или иной модели?»
Создание графических интерфейсов для экспертов и индикаторов на базе .Net Framework и C# Создание графических интерфейсов для экспертов и индикаторов на базе .Net Framework и C#
Простой и быстрый способ создания графических окон при помощи редактора Visual Studio с последующей интеграцией в код MQL советника. Статья расчитана на широкий круг читателей, и не требует каких-либо познаний в C# и технологии .Net.
Мартингейл как основа долгосрочной торговой стратегии Мартингейл как основа долгосрочной торговой стратегии
В данной статье мы подробно рассмотрим такую систему, как мартингейл. Подумаем, можно ли ее применять, и как ее применять, чтобы максимально снизить риски. Самый главный недостаток этой простой системы — есть вероятность потерять весь депозит. И это необходимо учитывать в своей торговле, если вы все таки решите использовать данную торговую систему.