Утилита для отбора и навигации на MQL5 и MQL4: добавляем вкладки "домашки" и сохраняем графические объекты

Roman Klymenko | 24 декабря, 2018

Введение

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

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

Используем возможности условной компиляции

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

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

Поэтому давайте пойдем другим путем. Как MQL5, так и MQL4 поддерживают директивы условной компиляции, которые позволяют в зависимости от условий выполнять либо один блок кода, либо другой. Среди этих директив есть и такая конструкция, которая выполняется в зависимости от текущей версии языка MQL. Её основной синтаксис следующий:

      #ifdef __MQL5__ 
         // блок кода, который будет выполняться только на MQL5
      #else 
         // блок кода, который будет выполняться только на MQL4
      #endif 

Давайте воспользуемся ею, чтобы наша функция checkSYMBwithPOS корректно работала и на языке MQL5, и на языке  MQL4 — без необходимости ее постоянной замены:

bool checkSYMBwithPOS(string name){
   // Скрыть символы, по которым есть позиции или ордера
   bool isskip=false;
   if( noSYMBwithPOS ){
      #ifdef __MQL5__ 
         // просматриваем список всех открытых позиций
         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;
                  }
               }
            }
         }
      #else 
         // просматриваем список всех открытых позиций
         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;
            }
         }
      #endif 
   }
   return isskip;
}

Далее в статье мы будем сразу портировать не работающие в MQL4 блоки кода с помощью данной конструкции.

Вкладки для домашнего задания

Чтобы спасти лесной массив нашей планеты, мы создадим 3 вкладки, на которых будут отображаться только те инструменты, которые мы предварительно выбрали среди других. Назовем эти вкладки: Long, Short и Range. Но, конечно, не обязательно добавлять в них только инструменты, которые идут вверх, вниз, или стоят в боковике. Вы можете использовать их по своему усмотрению.

В итоге, на графике, где будет запущена наша утилита, появится еще один ряд из 4 кнопок: кнопки All и трех ранее описанных кнопок.

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

Добавляем вкладки для отбора домашнего задания

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

Массивы для хранения содержимого вкладок. Во-первых, добавим переменные для хранения содержимого наших вкладок. Раньше у нас была только одна вкладка и её содержимое хранилось в переменной arrPanel1. Добавим аналогичные переменные для других вкладок:

// массивы символов, которые выводятся в соответствующей вкладке:
CArrayString arrPanel1;
CArrayString arrPanel2;
CArrayString arrPanel3;
CArrayString arrPanel4;

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

// массив для объединения всех вкладок
CArrayString *arrPanels[4];

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

   arrPanels[0]=&arrPanel1;
   arrPanels[1]=&arrPanel2;
   arrPanels[2]=&arrPanel3;
   arrPanels[3]=&arrPanel4;

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

// Массив с названиями вкладок
string panelNames[4]={"All", "LONG", "SHORT", "Range"};

Вспомогательные переменные. Еще одно изменение касается переменной panel1val. Мы изменили ее имя на panelval. Это чисто косметическое изменение. Но о нем нельзя не упомянуть.

Также была добавлена переменная cur_panel, содержащая индекс активной в данный момент вкладки. Тип данной переменной — uchar. То есть она может принимать значения от 0 до 255, чего вполне достаточно, ведь у нас всего 4 вкладки.

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

int OnInit()
  {
   // индекс активной в данный момент вкладки
   cur_panel=0;
   // инициализируем массив вкладок
   arrPanels[0]=&arrPanel1;
   arrPanels[1]=&arrPanel2;
   arrPanels[2]=&arrPanel3;
   arrPanels[3]=&arrPanel4;
   
   start_symbols();

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

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

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

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

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

}

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

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

   // для каждого символа в массиве активной в данный момент вкладки
   // выводим кнопку на график
   // на кнопке будем писать название символа
   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;
   }

Кнопки добавления символов во вкладки. С выводом вкладок разобрались. Теперь нам нужно как-то добавлять в выбранную вкладку символы. Делать это мы будем с помощью новых кнопок Add LONG, Add SHORT, Add Range на странице открытого графика.

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

Данный блок кнопок выводится с помощью функции createBTNS. Добавим в нее цикл вывода новых кнопок:

   for( int i=ArraySize(panelNames)-1; i>0; i-- ){
      isyes=false;
      if(arrPanels[i].Total()){
         for(int j=0; j<arrPanels[i].Total(); j++){
            if( arrPanels[i].At(j)==name ){
               isyes=true;
               break;
            }
         }
      }
      if( isyes ){
         ObjectCreate(CID, exprefix+"_p_btn_panelfrom"+(string) i, OBJ_BUTTON, 0, 0, 0);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_XDISTANCE,110); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_YDISTANCE,tmpHeight);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_SELECTABLE,false); 
         ObjectSetString(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_TEXT,"Del "+panelNames[i]);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_BGCOLOR,clrPink); 
      }else{
         ObjectCreate(CID, exprefix+"_p_btn_panelto"+(string) i, OBJ_BUTTON, 0, 0, 0);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_XDISTANCE,110); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_YDISTANCE,tmpHeight);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_CORNER,CORNER_LEFT_LOWER); 
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_SELECTABLE,false); 
         ObjectSetString(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_TEXT,"Add "+panelNames[i]);
         ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(string) i,OBJPROP_BGCOLOR,clrHoneydew); 
      }
      tmpHeight+=25;
   }

Блок кнопок навигации

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

            for( uchar i=1; i<ArraySize(panelNames); i++ ){
               // если нажата кнопка удаления из вкладки, то сначала удаляем символ, а потом снова открываем график данного символа
               if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_STATE)==true ){
                  delToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1]));
                  curchart();
                  return;
               }
               // если нажата кнопка добавления во вкладку, то сначала добавляем символ, а потом открываем график следующего символа
               if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelto"+(string) i,OBJPROP_STATE)==true ){
                  addToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1]));
                  nextchart();
                  return;
               }
            }

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

void delToPanel(uchar num, string name){
   // проходим по всему массиву, и удаляем первый элемент,
   // название которого аналогично нашему символу
   for(int i=0; i<arrPanels[num].Total(); i++){
      if( arrPanels[num].At(i)==name ){
         arrPanels[num].Delete(i);
         break;
      }
   }
   // если в данный момент открыта данная вкладка, то
   if(num==cur_panel){
      initial_btn_line();
      // удаляем ранее созданные кнопки символов с графика:
      ObjectsDeleteAll(0, exprefix);
      // выводим обновленный список символов:
      show_panel_buttons();
      show_symbols();
   }else{
      // если открыта любая другая вкладка, то просто обновляем кнопки заголовка
      upd_panel_title();
   }
   
   
}

Функция addToPanel противоположна только что рассмотренной. Она добавляет символ во вкладку. Но помимо добавления она также проверяет, нет ли данного символа в других вкладках. Если есть, то символ удаляется оттуда:

void addToPanel(uchar num, string name){
   // добавляем символ во вкладку
   arrPanels[num].Add(name);
   // удаляем символ из других вкладок,
   // если он там есть
   for( int j=1; j<ArraySize(arrPanels); j++ ){
      if(j==num) continue;
      for(int i=0; i<arrPanels[j].Total(); i++){
         if( arrPanels[j].At(i)==name ){
            if( panelval==i && i>0 ){
               panelval--;
            }
            arrPanels[j].Delete(i);
            break;
         }
      }
   }
   if(num==cur_panel){
      initial_btn_line();
      // удаляем ранее созданные кнопки символов с графика:
      ObjectsDeleteAll(0, exprefix);
      // выводим список символов:
      show_panel_buttons();
      show_symbols();
   }else{
      upd_panel_title();
   }
}

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

Для хранения списков отобранных символов не зря использовались объекты типа CArrayString. Одним из множества плюсов объектов данного типа являются стандартные методы, позволяющие легко как сбрасывать все содержимое массива в файл, так и восстанавливать массив из файла. Давайте воспользуемся ими, чтобы перед закрытием утилиты сохранять содержимое массивов в файл. То есть, добавим в стандартную функцию OnDeinit() вызов нашей новой функции savePanels:

void savePanels(){
   for( int i=1; i<ArraySize(arrPanels); i++ ){
      fh=FileOpen(exprefix+"panel"+(string) (i+1)+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
      if(fh>=0){ 
         arrPanels[i].Save(fh);
         FileClose(fh);
      }
   }
}

Соответственно, восстанавливать содержимое массивов будем в стандартной функции OnInit():

   for( int i=1; i<ArraySize(arrPanels); i++ ){
      fh=FileOpen(exprefix+"panel"+(string) (i+1)+".bin",FILE_READ|FILE_BIN|FILE_ANSI); 
      if(fh>=0){ 
         arrPanels[i].Load(fh);
         FileClose(fh); 
      }
   }

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

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

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

Чтобы сразу видеть, какой набор параметров загружен, давайте добавим входящий параметр cmt, в который мы будем записывать пояснение по тому, с каким рынком в данный момент работаем:

input string         cmt=""; //Параметры для (eng)

Выводить данный комментарий мы будем в строке с кнопками наших вкладок:

Выводим заголовок текущего набора

Для этого в функцию show_panel_buttons, после вывода всех кнопок, достаточно добавить следующий блок кода:

   // выводим комментарий, если он задан:
   if(StringLen(cmt)>0){
      string tmpCMT=cmt;
      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);
   }

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

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

void savePanels(){
   string tmpCmt=cmt;
   StringReplace(tmpCmt, " ", "_");
   for( int i=1; i<ArraySize(arrPanels); i++ ){
      fh=FileOpen(exprefix+"panel"+(string) (i+1)+tmpCmt+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
      if(fh>=0){ 
         arrPanels[i].Save(fh);
         FileClose(fh);
      }
   }
}

Сохраняем графические объекты

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

Код, который мы писали до сих пор, одинаково работал как в MQL5, так и в MQL4. А вот с функциями, которые сохраняют и восстанавливают графические объекты, у нас будут проблемы. Если в MQL4 тип графического объекта и его отдельные свойства описывались константами числового типа, то в MQL5 для этого стали использоваться перечисления (тип enum). Из-за чего оказалось весьма проблематично сохранять их в файл и восстанавливать оттуда. По крайней мере я с этой задачей не смог справиться в общем смысле. Если перейти к сути, то функционал сохранения графических объектов для MQL4 у нас будет более функциональный, если так можно выразиться. Теоретически он сможет сохранять любой графический объект (на практике я не тестировал его со всеми объектами, так что возможны исключения). А вот функционал для MQL5 сможет работать только с горизонтальными линиями, метками и текстовыми полями. Если вам нужна возможность сохранять другие графические объекты, вам придется реализовать ее самостоятельно.

MQL4. Так как для MQL4 код сохранения графических объектов проще, давайте начнем с функций именно для данного языка. Итак, функция сохранения графических объектов в файл:

   void savechart(ulong id){
      // сохраняем графические объекты только если входящий параметр 
      // "Сохранять созданные граф. объекты" = true
      if(saveGraphics){
         // получаем название символа
         string tmpName="";
         if(cur_panel<ArraySize(arrPanels)){
            tmpName=arrPanels[cur_panel][panelval];
         }
         tmpName=clean_symbol_name(tmpName);
         StringReplace(tmpName, " ", "");
         
         // очищаем массив графических объектов
         saveG.Resize(0);
         
         // добавляем в массив все графические объекты, созданные пользователем, а также свойства этих объектов
         int obj_total=ObjectsTotal((long) id); 
         string name;
         string tmpObjLine="";
         for(int i=0;i<obj_total;i++){
            name = ObjectName((long) id, i);
            if( StringFind(name, exprefix)<0 && StringFind(name, "fix")<0 && StringFind(name, "take")<0 && StringFind(name, "stop loss")<0 && StringFind(name, "sell")<0 && StringFind(name, "buy")<0 ){
               tmpObjLine=name;
               
               StringAdd(tmpObjLine, "|int~OBJPROP_TYPE~"+(string)(int) OBJPROP_TYPE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TYPE));
               StringAdd(tmpObjLine, "|int~OBJPROP_COLOR~"+(string)(int) OBJPROP_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_COLOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_STYLE~"+(string)(int) OBJPROP_STYLE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STYLE));
               StringAdd(tmpObjLine, "|int~OBJPROP_WIDTH~"+(string)(int) OBJPROP_WIDTH+"~"+(string) ObjectGetInteger(id, name, OBJPROP_WIDTH));
               StringAdd(tmpObjLine, "|int~OBJPROP_TIME~"+(string)(int) OBJPROP_TIME+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIME));
               StringAdd(tmpObjLine, "|int~OBJPROP_TIMEFRAMES~"+(string)(int) OBJPROP_TIMEFRAMES+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIMEFRAMES));
               StringAdd(tmpObjLine, "|int~OBJPROP_ANCHOR~"+(string)(int) OBJPROP_ANCHOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_ANCHOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_XDISTANCE~"+(string)(int) OBJPROP_XDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XDISTANCE));
               StringAdd(tmpObjLine, "|int~OBJPROP_YDISTANCE~"+(string)(int) OBJPROP_YDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YDISTANCE));
               StringAdd(tmpObjLine, "|int~OBJPROP_STATE~"+(string)(int) OBJPROP_STATE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STATE));
               StringAdd(tmpObjLine, "|int~OBJPROP_XSIZE~"+(string)(int) OBJPROP_XSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XSIZE));
               StringAdd(tmpObjLine, "|int~OBJPROP_YSIZE~"+(string)(int) OBJPROP_YSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YSIZE));
               StringAdd(tmpObjLine, "|int~OBJPROP_XOFFSET~"+(string)(int) OBJPROP_XOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XOFFSET));
               StringAdd(tmpObjLine, "|int~OBJPROP_YOFFSET~"+(string)(int) OBJPROP_YOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YOFFSET));
               StringAdd(tmpObjLine, "|int~OBJPROP_BGCOLOR~"+(string)(int) OBJPROP_BGCOLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BGCOLOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_BORDER_COLOR~"+(string)(int) OBJPROP_BORDER_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BORDER_COLOR));
               StringAdd(tmpObjLine, "|double~OBJPROP_PRICE~"+(string)(int) OBJPROP_PRICE+"~"+(string) ObjectGetDouble(id, name, OBJPROP_PRICE));
               StringAdd(tmpObjLine, "|string~OBJPROP_TEXT~"+(string)(int) OBJPROP_TEXT+"~"+(string) ObjectGetString(id, name, OBJPROP_TEXT));
               
               saveG.Add(tmpObjLine);
            }
         }
         // сохраняем содержимое массива в файл
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Save(fh);
            FileClose(fh);
         }
      }
   }

Как можно заметить, мы сохраняем не все свойства объекта, а только OBJPROP_COLOR, OBJPROP_STYLE, OBJPROP_WIDTH, OBJPROP_TIME, OBJPROP_TIMEFRAMES, OBJPROP_ANCHOR, OBJPROP_XDISTANCE, OBJPROP_YDISTANCE, OBJPROP_STATE, OBJPROP_XSIZE, OBJPROP_YSIZE, OBJPROP_XOFFSET, OBJPROP_YOFFSET, OBJPROP_BGCOLOR, OBJPROP_BORDER_COLOR, OBJPROP_PRICE, OBJPROP_TEXT. Если при работе с утилитой какой-то из графических объектов, использованных вами, будет сохранен неверно, значит не все свойства, используемые им, были сохранены. В этом случае вам достаточно добавить в данную функцию сохранение недостающих свойств, и данный тип графических объектов также станет поддерживаться.

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

   void loadchart(ulong id){
      // отображаем графические объекты только если входящий параметр 
      // "Сохранять созданные граф. объекты" = true
      if(saveGraphics){
         // получаем название символа
         string tmpName="";
         if(cur_panel<ArraySize(arrPanels)){
            tmpName=arrPanels[cur_panel][panelval];
         }
         tmpName=clean_symbol_name(tmpName);
         StringReplace(tmpName, " ", "");
         
         string tmpObjLine[];
         string tmpObjName="";
         string sep1="|";
         string sep2="~";
         
         // очищаем массив графических объектов
         saveG.Resize(0);
         // загружаем в массив список графических объектов из файла
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Load(fh);
            FileClose(fh); 
         }
         // последовательно выводим графические объекты на график
         for( int i=0; i<saveG.Total(); i++ ){
            StringSplit(saveG.At(i), StringGetCharacter(sep1,0), tmpObjLine);
            for( int j=0; j<ArraySize(tmpObjLine); j++ ){
               if(j>0){
                  string tmpObjSubLine[];
                  StringSplit(tmpObjLine[j], StringGetCharacter(sep2,0), tmpObjSubLine);
                  if(ArraySize(tmpObjSubLine)==4){
                     if(tmpObjSubLine[0]=="int"){
                        // первым в строке всегда идет тип объекта
                        // так что мы всегда сначала будем создавать объект, а потом формировать его свойства
                        if(tmpObjSubLine[1]=="OBJPROP_TYPE"){
                           ObjectCreate(id, tmpObjName, (int) tmpObjSubLine[3], 0, 0, 0);
                        }else if( (int) tmpObjSubLine[3] >= 0 ){
                           ObjectSetInteger(id, tmpObjName, (int) tmpObjSubLine[2], (int) tmpObjSubLine[3]);
                        }
                     }else if(tmpObjSubLine[0]=="double"){
                        if( (double) tmpObjSubLine[3] >= 0 ){
                           ObjectSetDouble(id, tmpObjName, (int) tmpObjSubLine[2], (double) tmpObjSubLine[3]);
                        }
                     }else if(tmpObjSubLine[0]=="string"){
                        if( StringLen(tmpObjSubLine[3]) > 0 ){
                           ObjectSetString(id, tmpObjName, (int) tmpObjSubLine[2], tmpObjSubLine[3]);
                        }
                     }
                  }
               }else{
                  tmpObjName=tmpObjLine[j];
               }
            }
            ObjectSetInteger(id, tmpObjName, OBJPROP_SELECTABLE, true);
         }
         
         
      }
   }

MQL5. Как уже говорилось, для MQL5 эти функции отличаются не в лучшую сторону:

   void savechart(ulong id){
      if(saveGraphics){
         string tmpName="";
         if(cur_panel<ArraySize(arrPanels)){
            tmpName=arrPanels[cur_panel][panelval];
         }
         tmpName=clean_symbol_name(tmpName);
         StringReplace(tmpName, " ", "");
         
         saveG.Resize(0);
         
         int obj_total=ObjectsTotal((long) id); 
         string name;
         string tmpObjLine="";
         for(int i=0;i<obj_total;i++){
            name = ObjectName((long) id, i);
            if( StringFind(name, exprefix)<0 && StringFind(name, "fix")<0 && StringFind(name, "take")<0 && StringFind(name, "stop loss")<0 && StringFind(name, "sell")<0 && StringFind(name, "buy")<0 ){
               tmpObjLine=name;
               // мы умеем работать только с объектами типа OBJ_HLINE, OBJ_TEXT и OBJ_LABEL,
               //поэтому объекты других типов пропускаем
               if( ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_HLINE && ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_TEXT && ObjectGetInteger(id, name, OBJPROP_TYPE)!=OBJ_LABEL ){
                  continue;
               }
               StringAdd(tmpObjLine, "|int~OBJPROP_TYPE~"+(string)(int) OBJPROP_TYPE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TYPE));
               StringAdd(tmpObjLine, "|int~OBJPROP_COLOR~"+(string)(int) OBJPROP_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_COLOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_STYLE~"+(string)(int) OBJPROP_STYLE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STYLE));
               StringAdd(tmpObjLine, "|int~OBJPROP_WIDTH~"+(string)(int) OBJPROP_WIDTH+"~"+(string) ObjectGetInteger(id, name, OBJPROP_WIDTH));
               StringAdd(tmpObjLine, "|int~OBJPROP_TIME~"+(string)(int) OBJPROP_TIME+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIME));
               StringAdd(tmpObjLine, "|int~OBJPROP_TIMEFRAMES~"+(string)(int) OBJPROP_TIMEFRAMES+"~"+(string) ObjectGetInteger(id, name, OBJPROP_TIMEFRAMES));
               StringAdd(tmpObjLine, "|int~OBJPROP_ANCHOR~"+(string)(int) OBJPROP_ANCHOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_ANCHOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_XDISTANCE~"+(string)(int) OBJPROP_XDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XDISTANCE));
               StringAdd(tmpObjLine, "|int~OBJPROP_YDISTANCE~"+(string)(int) OBJPROP_YDISTANCE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YDISTANCE));
               StringAdd(tmpObjLine, "|int~OBJPROP_STATE~"+(string)(int) OBJPROP_STATE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_STATE));
               StringAdd(tmpObjLine, "|int~OBJPROP_XSIZE~"+(string)(int) OBJPROP_XSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XSIZE));
               StringAdd(tmpObjLine, "|int~OBJPROP_YSIZE~"+(string)(int) OBJPROP_YSIZE+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YSIZE));
               StringAdd(tmpObjLine, "|int~OBJPROP_XOFFSET~"+(string)(int) OBJPROP_XOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_XOFFSET));
               StringAdd(tmpObjLine, "|int~OBJPROP_YOFFSET~"+(string)(int) OBJPROP_YOFFSET+"~"+(string) ObjectGetInteger(id, name, OBJPROP_YOFFSET));
               StringAdd(tmpObjLine, "|int~OBJPROP_BGCOLOR~"+(string)(int) OBJPROP_BGCOLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BGCOLOR));
               StringAdd(tmpObjLine, "|int~OBJPROP_BORDER_COLOR~"+(string)(int) OBJPROP_BORDER_COLOR+"~"+(string) ObjectGetInteger(id, name, OBJPROP_BORDER_COLOR));
               StringAdd(tmpObjLine, "|double~OBJPROP_PRICE~"+(string)(int) OBJPROP_PRICE+"~"+(string) ObjectGetDouble(id, name, OBJPROP_PRICE));
               StringAdd(tmpObjLine, "|string~OBJPROP_TEXT~"+(string)(int) OBJPROP_TEXT+"~"+(string) ObjectGetString(id, name, OBJPROP_TEXT));
               
               saveG.Add(tmpObjLine);
            }
         }
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Save(fh);
            FileClose(fh);
         }
      }
   }
   void loadchart(ulong id){
      if(saveGraphics){
         string tmpName="";
         if(cur_panel<ArraySize(arrPanels)){
            tmpName=arrPanels[cur_panel][panelval];
         }
         tmpName=clean_symbol_name(tmpName);
         StringReplace(tmpName, " ", "");
         string tmpObjLine[];
         string tmpObjName="";
         string sep1="|";
         string sep2="~";
         
         saveG.Resize(0);
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Load(fh);
            FileClose(fh); 
         }
         for( int i=0; i<saveG.Total(); i++ ){
            StringSplit(saveG.At(i), StringGetCharacter(sep1,0), tmpObjLine);
            for( int j=0; j<ArraySize(tmpObjLine); j++ ){
               if(j>0){
                  string tmpObjSubLine[];
                  StringSplit(tmpObjLine[j], StringGetCharacter(sep2,0), tmpObjSubLine);
                  if(ArraySize(tmpObjSubLine)==4){
                     if(tmpObjSubLine[0]=="int"){
                        // создаем объект в зависимости от его типа
                        if(tmpObjSubLine[1]=="OBJPROP_TYPE"){
                           switch((int) tmpObjSubLine[3]){
                              case 1:
                                 ObjectCreate(id, tmpObjName, OBJ_HLINE, 0, 0, 0);
                                 break;
                              case 101:
                                 ObjectCreate(id, tmpObjName, OBJ_TEXT, 0, 0, 0);
                                 break;
                              case 102:
                                 ObjectCreate(id, tmpObjName, OBJ_LABEL, 0, 0, 0);
                                 break;
                           }
                        }else if( (int) tmpObjSubLine[3] >= 0 ){
                           if(tmpObjSubLine[1]=="OBJPROP_COLOR"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_COLOR, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_STYLE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_STYLE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_WIDTH"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_WIDTH, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_TIME"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_TIME, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_TIMEFRAMES"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_TIMEFRAMES, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_ANCHOR"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_ANCHOR, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_XDISTANCE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_XDISTANCE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_YDISTANCE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_YDISTANCE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_STATE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_STATE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_XSIZE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_XSIZE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_YSIZE"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_YSIZE, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_XOFFSET"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_XOFFSET, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_YOFFSET"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_YOFFSET, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_BGCOLOR"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_BGCOLOR, (int) tmpObjSubLine[3]);
                           }else if(tmpObjSubLine[1]=="OBJPROP_BORDER_COLOR"){
                              ObjectSetInteger(id, tmpObjName, OBJPROP_BORDER_COLOR, (int) tmpObjSubLine[3]);
                           }
                        }
                     }else if(tmpObjSubLine[0]=="double"){
                        if( (double) tmpObjSubLine[3] >= 0 ){
                           if(tmpObjSubLine[1]=="OBJPROP_PRICE"){
                              ObjectSetDouble(id, tmpObjName, OBJPROP_PRICE, (double) tmpObjSubLine[3]);
                           }
                        }
                     }else if(tmpObjSubLine[0]=="string"){
                        if( StringLen(tmpObjSubLine[3]) > 0 ){
                           if(tmpObjSubLine[1]=="OBJPROP_TEXT"){
                              ObjectSetString(id, tmpObjName, OBJPROP_TEXT, tmpObjSubLine[3]);
                           }
                        }
                     }
                  }
               }else{
                  tmpObjName=tmpObjLine[j];
               }
            }
            ObjectSetInteger(id, tmpObjName, OBJPROP_SELECTABLE, true);
         }
         
         
      }
   }

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

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

#ifdef __MQL5__ 
   void savechart(ulong id){
      // функция для MQL5
   }
   void loadchart(ulong id){
      // ...
   }
#else 
   void savechart(ulong id){
      // функция для MQL4
   }
   void loadchart(ulong id){
      // ...
   }
#endif 

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

Вызов функции loadchart добавим внутрь функции showcharts, которая открывает график, на кнопку которого мы нажали.

Вызов функции сохранения графика нужно будет добавить в блоки кода реакции на нажатие кнопок навигации по графику: Next chart, Prev chart, Close chart и кнопок добавления/удаления символа из вкладок домашнего задания.

Используем сайт finviz.com для предварительного отбора акций

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

Конечно, в прошлой статье мы создали набор из некоторых входящих параметров, позволяющих фильтровать символы по цене, ATR и т.п. Но реализованные нами возможности, по сравнению со скринером сайта finviz.com, мягко говоря смешны. И самое главное, в MQL4 вообще нет возможности фильтровать инструменты по реальному объему. А во многих стратегиях торговли от уровней это очень важный показатель. На сайте finviz.com же можно фильтровать как по среднему объему акции, так и по тому, какой объем по акции наторгован за текущий день.

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

input string         ""; // Только символы (разделитель - ; или пробел)
input string         ""; // Добавить префикс к символам
input string         ""; // Добавить суффикс к символам

Параметры onlySymbolsPrefix и onlySymbolsSuffix нам нужны в том случае, если у вашего брокера названия инструментов отличаются от официальных тикеров. У многих брокеров так и бывает. Кто-то добавляет к названию тикера суффикс .us для акций американского фондового рынка, и .eu для европейских акций. Кто-то к тикерам любых акций добавляет суффикс m. А некоторые брокеры в начале тикеров акций добавляют символ #.

Добавляем возможность импорта символов из файла. Забегая наперед скажу сразу, что с импортом символов из входящего параметра у нас возникнет проблема. И это проблема с максимальной длиной данной строки. Используя входящий параметр мы окажемся ограничены максимум 15-20 тикерами. Больше туда вставить не получится. Поэтому входящий параметр можно использовать разве что для ограничения рабочих инструментов небольшим количеством символов.

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

Реализация в коде. Давайте разобьем процесс формирования списка символов для вкладки All на два блока.

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

         // если во входящем параметре "Только символы (разделитель - ; или пробел)"
         // есть какие-то данные
         if( StringLen(onlySymbols)>0 ){
            // разбиваем строку из входящего параметра на элементы массива
            // разделителем элементов будет знак ;
            StringSplit(onlySymbols,StringGetCharacter(";",0),result); 
            if( ArraySize(result)>1 ){
            }else{
               // если в результате разбиения в массиве содержится только одно значение,
               // значит разбить не удалось, и видимо разделителем в строке является пробел
               // поэтому теперь разбиваем строку из входящего параметра на элементы массива
               // и разделителем элементов будет пробел
               StringSplit(onlySymbols,StringGetCharacter(" ",0),result); 
            }
         // иначе проверяем, существует ли в папке Files файл symbols.txt
         }else if( FileIsExist("symbols.txt") ){
            // если файл существует, то помещаем его содержимое во временную переменную outfile
            int filehandle=FileOpen("symbols.txt",FILE_READ|FILE_TXT); 
            if(filehandle>=0){
               string outfile=FileReadString(filehandle);
               // если переменная outfile содержит какую-либо строку,
               // то пытаемся разбить ее на элементы массива,
               // используя для этого сначала разделитель ;, а потом пробел
               if(StringLen(outfile)>0){
                  StringSplit(outfile,StringGetCharacter(";",0),result); 
                  if( ArraySize(result)>1 ){
                  }else{
                     StringSplit(outfile,StringGetCharacter(" ",0),result); 
                  }
                  if( ArraySize(result)>1 ){
                     from_txt=true;
                  }
               }
               FileClose(filehandle);
            }
         }

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

   // если в массиве больше двух символов, то используем их,
   // предварительно, при необходимости, добавляя им нужны1 суффикс или префикс
   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(noSYMBmarketWath); i++ ){
         tmpSymbols.Add(SymbolName(i, noSYMBmarketWath));
      }
   }

Формируем список символов с помощью скринера finviz.com. Напоследок давайте рассмотрим, как импортировать отобранные на сайте finviz.com тикеры в нашу утилиту.

На самом деле все просто. Вам достаточно на странице скринера, уже после фильтрации, перейти на вкладку Tickers. В результате перед вами отобразится облако из названий отобранных тикеров. Выделяем их все, копируем и вставляем либо в файл symbols.txt, либо во входящий параметр. Если страниц с результатами фильтрации больше одной, то переходим на следующую страницу, и делаем то же самое.

Заключение

Сегодня мы сделали достаточно большой объем работы. И наша утилита стала более функциональной. Теперь ее можно без каких-либо проблем использовать для отбора акций, забыв навсегда про бумажные листочки домашнего задания. Надеюсь, хотя бы «зеленые» планеты скажут нам спасибо =)