Selection and navigation utility in MQL5 and MQL4: Adding "homework" tabs and saving graphical objects

Roman Klymenko | 5 February, 2019

Introduction

In the previous article, we developed the utility for sorting out and selecting symbols with an appropriate entry point. We learned how to sort out symbols by various parameters, as well as navigate through symbols using specially designed buttons. However, the picture is not so rosy in terms of symbol selection. Currently, we have to write the tickers of the selected instruments on a piece of paper, which has a very negative impact on the planet's forest populations.

In this article, we will save trees from destruction and will learn how to automatically save graphical objects created on a chart so that you do not have to constantly create them in the future.

Applying conditional compilation features

First, let's simplify porting the utility to the MQL4 language. In the previous article, we replaced one code block with another to let the program work in MQL4. Now we face a more difficult task. We can either carry out development in a single language, for example, MQL5, and then constantly replace blocks of code not working in MQL4 with the necessary ones, or we can simultaneously develop two programs: in MQL5 and MQL4.

Neither option is optimal. We should either constantly replace the blocks (in each version) not working in MQL4, or keep in mind the parts of the code we changed to implement that changes into the utility in another language.

Therefore, we will use a different approach. Both MQL5 and MQL4 support conditional compilation directives allowing the execution of one code block or another depending on the conditions. Among the directives, there is a construction executed depending on the current MQL language version. Its main syntax is as follows:

      #ifdef __MQL5__ 
         // code block to be executed only in MQL5
      #else 
         // code block to be executed only in MQL4
      #endif 

Let's use it to let our checkSYMBwithPOS function work correctly both on MQL5 and MQL4 without the need for its constant replacement:

bool checkSYMBwithPOS(string name){
   // Hide symbols having positions and orders
   bool isskip=false;
   if( noSYMBwithPOS ){
      #ifdef __MQL5__ 
         // view the list of all open positions
         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // skip if there is a position for the current symbol
            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 
         // view the list of all open positions
         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;
}

Further in the article, we will immediately port code blocks that do not work in MQL4 using this construction.

Homework tabs

To save the Earth's forests, we will create three tabs that are to display only the previously selected symbols. Let's name these tabs Long, Short and Range. Of course, you do not have to add solely upward, downward or flat symbols there. You can use them at your discretion.

As a result, the chart our utility is launched at features yet another row consisting of four buttons: the All button and the three previously described ones.

The All button is pressed by default, which means the list of all symbols fitting our filters is to be displayed below:

Adding tabs for selecting homework

Thus, the objective has been set. Now we have to implement it. To do this, we will have to re-write a small part of our utility.

Arrays for storing tab contents. First, let's add variables for storing the contents of our tabs. Previously, we only had one tab and its contents was stored in the arrPanel1 variable. We will add similar variables for other tabs:

// array symbols displayed in the appropriate tab:
CArrayString arrPanel1;
CArrayString arrPanel2;
CArrayString arrPanel3;
CArrayString arrPanel4;

In addition, we will create another array to be able to access the tabs in a loop. The array will store pointers to all four previously created arrays:

// array for combining all tabs
CArrayString *arrPanels[4];

Let's initialize the array in the OnInit() function:

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

Tab headers. Since working with tabs is to be performed in a loop, it would be perfect to store the tab names there as well accessing them from the loop. Therefore, let's create the array with tab names:

// Tab names array
string panelNames[4]={"All", "LONG", "SHORT", "Range"};

Auxiliary variables. Another change concerns the panel1val variable. We have changed its name to panelval. This is a purely cosmetic amendment but it should be noted.

The cur_panel parameter containing the index of the currently active tab has also been added. The variable type is uchar. This means it may take values from 0 to 255, which is quite enough, because we have only 4 tabs.

By default, the first tab (having index 0 in the array) is active. Therefore, we will add the string assigning the value of 0 to the variable in the OnInit() function. Finally, the OnInit() function takes its final form:

int OnInit()
  {
   // index of the currently active tab
   cur_panel=0;
   // initialize the array of tabs
   arrPanels[0]=&arrPanel1;
   arrPanels[1]=&arrPanel2;
   arrPanels[2]=&arrPanel3;
   arrPanels[3]=&arrPanel4;
   
   start_symbols();

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

Other changes. Listing all implemented changes is unnecessary since many of them are too insignificant. So let's move on to the main changes now. As for small changes, you can detect them on your own by comparing the source code of the new utility with the one attached to the previous article.

The main changes are mainly the ones related to displaying our new tabs. We decided to work with our tabs in a loop. Let's see how we are going to do that.

Since we have a line with tabs, we need to display it somehow. To achieve this, we create a separate function. Its code is provided below:

void show_panel_buttons(){
   int btn_left=0;
   // define the maximum possible x axis coordinate for displaying tabs.
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   string tmpName="";
   
   for( int i=0; i<ArraySize(panelNames); i++ ){
      // if the start coordinate of the new button exceeds the maximum possible one,
      // move to the new line.
      if( btn_left>btn_right-BTN_WIDTH ){
         btn_line++;
         btn_left=0;
      }
      // if the "homework" tabs feature symbols, add their number
      // to the tab name
      tmpName=panelNames[i];
      if(i>0 && arrPanels[i].Total()>0 ){
         tmpName+=" ("+(string) arrPanels[i].Total()+")";
      }
      
      // display tab buttons
      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 the button tab is currently active,
      // make it pressed
      if( cur_panel == i ){
         ObjectSetInteger(0,exprefix+"panels"+(string) i,OBJPROP_STATE, true);
      }
      
      btn_left+=BTN_WIDTH;
   }

}

We will call the function in the start_symbols function before calling the show_symbols function displaying the symbol buttons.

The show_symbols function itself has also changed, although slightly. Now we display only the buttons of the symbols located at the currently active tab:

   // display a button on the chart for each symbol in the array of the
   // currently active tab
   // we will write a symbol name on the button
   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;
   }

Buttons for adding symbols to tabs. Now we need to add symbols to a selected tab. We will do this with the help of new buttons Add LONG, Add SHORT and Add Range on the open chart page.

If you remember, in the last article, we implemented the ability to click on the desired symbol button. After clicking on a symbol, a chart is opened containing the buttons block for navigating through the entire symbol list in the lower left corner. We will add our buttons to this block. Depending on whether this symbol is in the corresponding tab, the button will either add it to the tab or delete it from there.

The button block is displayed using the createBTNS function. Add the loop of adding new buttons to it:

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

Block of navigation buttons

To enable pressing the new buttons, add the check code of the button status to the OnTimer() function. Everything is the same as we did in the last article:

            for( uchar i=1; i<ArraySize(panelNames); i++ ){
               // if the removal from tab button is pressed, first, remove the symbol and open the symbol chart again
               if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelfrom"+(string) i,OBJPROP_STATE)==true ){
                  delToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1]));
                  curchart();
                  return;
               }
               // if the adding to tab button is pressed, first, add the symbol and open the next symbol chart
               if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_panelto"+(string) i,OBJPROP_STATE)==true ){
                  addToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-1]));
                  nextchart();
                  return;
               }
            }

The delToPanel function first removes the symbol from the selected tab and then updates either all symbol buttons on the chart the utility is launched at or only the header buttons:

void delToPanel(uchar num, string name){
   // move along the entire array and remove the first element
   // with its name similar to our symbol
   for(int i=0; i<arrPanels[num].Total(); i++){
      if( arrPanels[num].At(i)==name ){
         arrPanels[num].Delete(i);
         break;
      }
   }
   // if the tab is currently open,
   if(num==cur_panel){
      initial_btn_line();
      // remove previously created symbol buttons from the chart:
      ObjectsDeleteAll(0, exprefix);
      // display the updated symbol list:
      show_panel_buttons();
      show_symbols();
   }else{
      // if any other tab is open, simply update the header buttons
      upd_panel_title();
   }
   
   
}

The addToPanel function is the opposite to the one we have just considered. It adds a symbol to the tab. Besides that, it also checks if the symbol is present on other tabs. If the symbol is present, it is removed from there:

void addToPanel(uchar num, string name){
   // add a symbol to the tab
   arrPanels[num].Add(name);
   // remove a symbol from other tabs
   // if present
   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();
      // remove previously created symbol buttons from the chart:
      ObjectsDeleteAll(0, exprefix);
      // display the symbol list:
      show_panel_buttons();
      show_symbols();
   }else{
      upd_panel_title();
   }
}

Saving tabs contents between the utility launches. What if we accidentally close the EA? Then all our efforts will be wasted. Should we start adding everything again? Let's make sure that symbol lists we add to the homework tabs are saved to the file and restored upon further opening.

We used the CArrayString type objects for storing the lists of selected symbols for a reason. One of the many advantages of objects of this type are standard methods that make it easy to both send the entire contents of an array to a file and restore an array from a file. Let's use them to save the contents of the arrays to a file before closing the utility. In other words, we should add the call of our new savePanels function to the standard OnDeinit() function:

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

The array contents will be restored in the standard OnInit() function:

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

Adding a header to identify the current parameters

If you trade in different markets, you will have to constantly tune the utility when switching from one market to another. After all, if you are currently going to trade the American stock market waiting for a breakthrough in the first hour and a half after the market opening, then you do not need shares of the European or Russian market, which have opened long time ago. Similarly, if you are trading in the Russian market, you do not need US stocks.

In order not to be distracted and focus solely on the necessary symbols, it would be reasonable to create separate sets of parameters for markets of different countries, as well as for the Forex market, and upload each of set files when it is necessary. This is quite simple and takes seconds of time. However, it is difficult to understand what settings are uploaded at the moment.

To see the applied set of parameters, we will add the cmt input, where we write down an explanation concerning the market we are currently working with:

input string         cmt=""; //Parameters for (eng)

We will display this comment in a line with buttons of our tabs:

Displaying the current set header

To achieve this, add the following code block to the show_panel_buttons function after displaying all the buttons:

   // display a comment if specified:
   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);
   }

In addition to identifying the current set of parameters, the cmt input helps us separate the symbol lists in the homework tabs. After all, if we add the symbol to a homework tab for working on a US market, we do not need that symbol when working in the Russian stock market. Set files having different sets of parameters should also feature separate lists for the homework tabs.

To implement them, we have to slightly modify the code which saves arrays to the file and restores them from the file. Let's consider the modified function for saving to a file as an example:

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

Saving graphical objects

Another issue we need to solve to work with charts is auto saving and restoring graphical objects we created on the chart. If we set a level on the chart, we expect to see it again after closing the chart window and re-opening it. We surely do not want to place dozens of levels every time we open a symbol chart.

The code we have written so far works equally well in both MQL5 and MQL4. This is not the case with the functions that save and restore graphical objects. In MQL4, the graphical object type and its separate properties are described by constants of numeric type, while MQL5 applies enumerations (enum type) for that. This is why it is quite difficult to save them to a file and restore them. At least I could not cope with this task in a general sense. The functionality of saving graphical objects for MQL4 is of greater help to us here. Theoretically, it can save any graphical object (I have not tested it with all objects, so exceptions are possible). The MQL5 functionality allows it working only with horizontal lines, labels and text fields. If you need to save other graphical objects, you have to implement it on your own.

MQL4. Since the code of saving graphical objects is simpler for MQL4, let's start with functions for this language. The function for saving geographical objects to a file:

   void savechart(ulong id){
      // save graphical objects only if the 
      // "Save created graphical objects" parameter = true
      if(saveGraphics){
         // get a symbol name
         string tmpName="";
         if(cur_panel<ArraySize(arrPanels)){
            tmpName=arrPanels[cur_panel][panelval];
         }
         tmpName=clean_symbol_name(tmpName);
         StringReplace(tmpName, " ", "");
         
         // clear the graphical object array
         saveG.Resize(0);
         
         // add all user-created graphical objects and their properties to the array
         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);
            }
         }
         // save the array contents to the file
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Save(fh);
            FileClose(fh);
         }
      }
   }

As you can see, we do not retain all the properties of the object, but only 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 and OBJPROP_TEXT. If any of the applied graphical objects is saved incorrectly while working with the utility, this means not all applied properties have been saved. In this case, simply add saving missing properties to this function, so that this type of graphical objects is supported as well.

Now let's have a look at the function that uploads graphical objects from a file and displays them on the chart:

   void loadchart(ulong id){
      // display graphical objects only if the 
      // "Save created graphical objects" input = true
      if(saveGraphics){
         // get a symbol name
         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="~";
         
         // clear the graphical object array
         saveG.Resize(0);
         // upload the list of graphical objects from the file to the array
         fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI); 
         if(fh>=0){ 
            saveG.Load(fh);
            FileClose(fh); 
         }
         // display graphical objects on the chart successively
         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"){
                        // the object type always goes first in the line
                        // so that we initially create an object and form its properties afterwards
                        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. As already mentioned, these functions in MQL5 are not as efficient:

   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;
               // we can only work with OBJ_HLINE, OBJ_TEXT and OBJ_LABEL object types,
               //therefore, we skip objects of other types
               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"){
                        // create an object depending on its type
                        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);
         }
         
         
      }
   }

If we take a closer look, we will notice that we have to use a separate string for creating an object for objects of different types, whereas in MQL4, one string is enough for all objects. This is also the case with object properties. In MQL4, we used one property creation string per its type (string, real or integer). In MQL5, every property requires a separate string of its creation.

Combining languages. Let's use the conditional compilation, so that the EA applies the necessary version of the functions depending on the language:

#ifdef __MQL5__ 
   void savechart(ulong id){
      // MQL5 function
   }
   void loadchart(ulong id){
      // ...
   }
#else 
   void savechart(ulong id){
      // MQL4 function
   }
   void loadchart(ulong id){
      // ...
   }
#endif 

Applying the functions. Now let's add calling the functions to the appropriate parts of the program.

The call of the loadchart function is added inside the showcharts function, which opens the chart in accordance with the button we have pressed.

The call of the chart saving function is to be added to the blocks of code related to responding to pressing the chart navigation buttons: Next chart, Prev chart and Close chart, as well as the buttons for adding/removing a symbol from the homework tabs.

Using the finviz.com website for preliminary selection of stocks

In the previous article, we mentioned that the symbol list for filtration can be taken not only from the list of symbols offered by the broker, but also from an input. First, this allows displaying only the limited set of symbols in the necessary order. Second, the custom set of symbols can be used to perform preliminary filtration on the finviz.com or a similar website.

In the previous article, we formed a set of several inputs allowing users to sort symbols by price, ATR, etc. However, these features pale in comparison to the finviz.com website screener. Most importantly, MQL4 features no possibility to sort symbols by a real volume, while in many strategies based on trading levels, this is a very important indicator. The finviz.com website allows you to sort by the stock's average volume, as well as the volume traded within the current day.

Adding the ability to obtain the symbol list from the input parameter. To use a third-party symbol list, let's add three additional inputs to the utility:

input string         ""; // Symbols only (separator - ; or space)
input string         ""; // Add prefix to symbols
input string         ""; // Add suffix to symbols

We need the onlySymbolsPrefix and onlySymbolsSuffix parameters if names of symbols offered by your broker are different from official tickers. Some brokers add the .us suffix for US stocks and the .eu suffix for European stocks, while some brokers add the m suffix to any tickers. Brokers may also add # at the beginning of stock tickers.

Adding the ability to import symbols from a file. Looking ahead, I will say right away that we are going to have an issue with importing symbols from the input. This issue concerns the maximum string length. While using the input, we are limited to 15-20 tickers at most. Therefore, the input can be used only to limit working instruments by a small number of symbols.

Therefore, in addition to the input, you may also place the necessary symbols to the symbols.txt file created in the Files folder.

Implementation in the code. Let's divide the process of forming the list of symbols for the All tab into two blocks.

The first block checks if there are symbols in the file or input. If yes, the result array is filled by them. The array is added to the OnInit() function:

         // if the "Symbols only (separator - ; or space)" input
         // features any data
         if( StringLen(onlySymbols)>0 ){
            // divide the line from the input into the array elements
            // the elements are separated by ;
            StringSplit(onlySymbols,StringGetCharacter(";",0),result); 
            if( ArraySize(result)>1 ){
            }else{
               // if only one value is present in the array as a result of a split,
               // then the split has failed, and apparently the separator in the line is space
               // therefore, now we divide the line from the input into the array elements
               // and space is used as the elements separator
               StringSplit(onlySymbols,StringGetCharacter(" ",0),result); 
            }
         // otherwise, check if the Files folder contains the symbols.txt file
         }else if( FileIsExist("symbols.txt") ){
            // if the file exists, place its contents into the 'outfile' temporary variable
            int filehandle=FileOpen("symbols.txt",FILE_READ|FILE_TXT); 
            if(filehandle>=0){
               string outfile=FileReadString(filehandle);
               // if the 'outfile' variable contains a string,
               // attempt to split it into the array elements
               // first, by using separator ; followed by space
               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);
            }
         }

In the prepare_symbols function, we first check if the result array features any data and use them if it does. Otherwise, we use either all symbols offered by the broker or the ones added to the Market Watch panel for further sorting:

   // if the array features more than two symbols, use them
   // preliminarily adding the required suffix or prefix if necessary
   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);
      }
   // otherwise, use all symbols provided by a broker
   }else{
      for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){
         tmpSymbols.Add(SymbolName(i, noSYMBmarketWath));
      }
   }

Forming a symbol list using the finviz.com screener. Finally, let's take a look at how to import tickers selected on finviz.com to our utility.

All is simple. After the sorting, go to the Tickers tab on the screener page. You will see the cloud consisting of selected tickers' names. Select them all, copy and paste either to the symbols.txt file, or to the input. If there are several pages containing sorting results, go to the next page and do the same.

Conclusion

We have done quite a lot of work and made our utility more functional. Now we can easily use it to select stocks forgetting about paper notes. I hope, the planet's forests will appreciate that. =)