Tips from a professional programmer (Part II): Storing and exchanging parameters between an Expert Advisor, scripts and external programs

Malik Arykov | 4 June, 2021

Contents


Introduction

In this article, we are going to discuss parameters which can be restored after terminal restart (shutdown). All examples are real working code segments from my Cayman project.


Parameter storage locations


Parameter examples

Where to store these parameters?
Storage
Type Scope Lifetime
Terminal global variables double All charts 4 weeks after the last call
Graphical objects Any. Strings <= 63 characters Current chart Chart lifetime
Order comments Strings <= 23 characters All charts Terminal lifetime
Text files Any. Unlimited All charts File lifetime


Terminal global variables

The terminal global variables are available from any chart. Their scope can be limited by including additional components to the variable name, such as ChartId, Symbol, or Period. What cannot be changes is the variable type. You cannot save the text.

There is a lifehack: pack/unpack integer values. As you know, double takes up 8 bytes (64 bits). Please check the following example: it shows how to store multiple integer values in one variable. The most important thing is to determine the bit size of their maximum values.

// -----------------------------------------------------------------------------
// Example of packing/unpacking integer values to/from a global variable       |
// using bitwise operations                                                    |
// -----------------------------------------------------------------------------
void OnStart() {
    
    int     value10 = 10; // max = 255 (8 bits)
    int     value20 = 300; // max = 65535 (16 bits)
    bool    value30 = true; // max = 1 (1 bit)
    
    // pack the values into 25 bits (8+16+1)
    // 39 bits (64-25) remain free
    ulong packedValue = 
        (value10 << 17) + // reserve space (16+1) for value20, value30
        (value20 << 1) + // reserve space (1) for value30
        value30;
    
    // save the global variable
    string nameGVar = "temp";
    GlobalVariableSet(nameGVar, packedValue);
    
    // read the global variable
    packedValue = (ulong)GlobalVariableGet(nameGVar);
    
    // unpack the values
    // 0xFF, 0xFFFF, 0x1 - bit masks of maximal values
    int value11 = (int)((packedValue >> 17) & 0xFF);
    int value21 = (int)((packedValue >> 1) & 0xFFFF);
    bool value31 = (bool)(packedValue & 0x1);
    
    // compare the values
    if (value11 == value10 && value21 == value20 && value31 == value30) Print("OK");
    else PrintFormat("0x%X / 0x%X /0x%X / 0x%X", packedValue, value11, value21, value31);
}


Graphical objects

Can you store script parameters in graphical objects? Why not. Set the object property OBJPROP_PRICE = 0 — in this case the object is visually "hidden" but is accessible within the program. For reliability, such an object can be saved in a chart template. Parameter accessing logic is as follows: if there is an object, extract the parameters; if there is no object, set the default values.


Order comments

The maximum order comment length is limited to 23 characters. What can be stored in a comment? For example, SOP/H1/SS/C2/Br/Br/Br. Where (left to right)

Why do we need this? For example, this data can be used for analyzing deals. Here is how I use it: when a pending order triggers, I extract the value of the closing algorithm and create a virtual stop analyzer AnalyserVirtSL, which will then close the deal under certain conditions.


Text files

This is perhaps the most reliable and universal way to store recovery parameters. You can set up access classes once and then use them whenever and wherever you need.


Application settings

Part of the AppSettings.txt settings file:

# -------------------------------------------------------------------
# Expert Advisor and script settings
# File encoding = UCS-2 LE with BOM (required!!!) // it is Unicode
# -------------------------------------------------------------------
TimeEurWinter = 10:00 # European session beginning winter time (server time)
TimeEurSummer = 09:00 # European session beginning summer time (server time)
ColorSessionEur = 224,255,255 # European session color
ColorSessionUsd = 255,240,245 # American session color
NumberColorDays = 10 # the number of highlighted days (sessions)


The AppSettings.mqh class

#property copyright "Copyright 2020, Malik Arykov"
#property link      "malik.arykov@gmail.com"
#property strict

#include <Cayman/Params.mqh>

// application parameter names
#define APP_TIME_EUR_SUMMER "TimeEurSummer"
#define APP_TIME_EUR_WINTER "TimeEurWinter"
#define APP_TIME_TRADE_ASIA "TimeTradeAsia"
#define APP_COLOR_SESSION_EUR "ColorSessionEur"
#define APP_COLOR_SESSION_USD "ColorSessionUsd"
#define APP_NUMBER_COLOR_DAYS "NumberColorDays"

// -----------------------------------------------------------------------------
// General settings of the Expert Advisor and Scripts                          |
// -----------------------------------------------------------------------------
class AppSettings {
private:
    Params  *m_params;
public:
    // set in the AppSettings.txt file
    string  TimeEurSummer; // European session beginning summer time
    string  TimeEurWinter; // European session beginning winter time
    string  TimeTradeAsia; // Asian corridor trading end time
    color   ColorSessionEur; // European session color
    color   ColorSessionUsd; // American session color
    int     NumberColorDays; // Number of highlighted days

    // set by the program
    string  PeriodTrends; // Trend calculation periods (D1,H4)
    string  TradePlan; // Trading direction (brief plan)
    bool    IsValid; // Parameter validity
    
    // methods
    AppSettings();
    ~AppSettings() { delete m_params; };
    void Dump(string sender);
};

// -----------------------------------------------------------------------------
// Constructor                                                                 |
// -----------------------------------------------------------------------------
AppSettings::AppSettings() {

    IsValid = true;
    m_params = new Params();
    m_params.Load(PATH_APP_SETTINGS);
    if (m_params.Total() == 0) {
        PrintFormat("%s / ERROR: Invalid file / %s", __FUNCTION__, PATH_APP_SETTINGS);
        IsValid = false;
        return;
    }
        
    TimeEurWinter = m_params.GetValue(APP_TIME_EUR_WINTER);
    TimeEurSummer = m_params.GetValue(APP_TIME_EUR_SUMMER);
    TimeTradeAsia = m_params.GetValue(APP_TIME_TRADE_ASIA);
    ColorSessionEur = StringToColor(m_params.GetValue(APP_COLOR_SESSION_EUR));
    ColorSessionUsd = StringToColor(m_params.GetValue(APP_COLOR_SESSION_USD));
    NumberColorDays = (int)StringToInteger(m_params.GetValue(APP_NUMBER_COLOR_DAYS));
}

// -----------------------------------------------------------------------------
// Print settings parameters                                                   | 
// -----------------------------------------------------------------------------
void AppSettings::Dump(string sender) {
    PrintFormat("sender=%s / %s", sender, PATH_APP_SETTINGS);
    PrintFormat("%s = %s", APP_TIME_EUR_WINTER, TimeEurWinter);
    PrintFormat("%s = %s", APP_TIME_EUR_SUMMER, TimeEurSummer);
    PrintFormat("%s = %s", APP_TIME_TRADE_ASIA, TimeTradeAsia);
    PrintFormat("%s = %s / %s", APP_COLOR_SESSION_EUR, ColorToString(ColorSessionEur), ColorToString(ColorSessionEur, true));
    PrintFormat("%s = %s / %s", APP_COLOR_SESSION_USD, ColorToString(ColorSessionEur), ColorToString(ColorSessionEur, true));
    PrintFormat("%s = %i", APP_NUMBER_COLOR_DAYS, NumberColorDays);
}



Features

The AppSettings class declaration is located in the Uterminal.mqh file which is connected to an Expert Advisor and to any script via #include.

extern AppSettings  *gAppSettings; // application settings

With this solution you can:


Analyzer parameters

The Cayman Expert Advisor manages various analyzers such as AnalyzerTrend, AnalyserLevel, AnalyserVirtSL. Each analyzer is linked to a specific timeframe. It means that the analyzer is only launched when a new bar emerges on the specified timeframe. Analyzer examples are stored in the text file, with the Key = Value strings. For example, the H4 trading level analyzer stores its parameters in the Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt file

Below is the file contents with comments (the real file has no comments)

// trading level parameters
nameObj=Lev607A160E // trading level name
kindLevel=1 // level type (1 - resistance)
riskValue=1.00 // deal volume upon level breakout (1)
riskUnit=1 // deal volume change unit (1 - % of funds for margin)
algClose=2 // deal closing algorithm (2 – two correction bars)
ticketNew=0 // ticket of a deal opened upon level breakout
ticketOld=0 // ticket to close a deal upon level breakout
profits=0 // planned profit in points
losses=0 // planned loss in points
// analyzer parameters
symbol=EURUSD // symbol name
period=16388 // analyzer period (H4)
time0Bar=1618603200 // zero bar time (sec)
typeAnalyser=5 // analyzer type
colorAnalyser=16711935 // color for analyzer results
resultAnalyser=Lev607A160E, H4, 20:00, RS // analyzer results

There is a base class Analyser which can save and restore parameters of any analyzer. When an Expert Advisor is restarted (for example, after switching timeframes), analyzers restore parameters from the relevant text files. If the time for a new bar has not yet come, the analysis is not restarted. Analyzer results (resultAnalyser, colorAnalyser) calculated at the previous bar are displayed in the Expert Advisor comments.


Passing script parameters to an Expert Advisor

The SetTradeLevel script allows setting the parameters of a trading level. One object (straight line, trend line or rectangle) is selected on the chart. The SetTradeLevel script finds the selected object (trading level) and sets its parameters.

SetTradeLevel script parameters

Next, the script saves the parameters to Files\Cayman\Params\128968168864101576\exp_05_Lev607A160E_H4.txt and sends the command and the path to the file via the SendCommand function.

// -----------------------------------------------------------------------------
// Send level parameters to the Expert Advisor                                 |
// -----------------------------------------------------------------------------
NCommand SendCommand() {

    // load level parameters (if any)
    Params *params = new Params();
    string speriod = UConvert::PeriodToStr(_Period);
    params.Load(PREFIX_EXPERT, anaLevel, gNameLev, speriod);

    // define the command
    NCommand cmd = 
        (gKindLevel == levUnknown) ? cmdDelete :
        (params.Total() > 0) ? cmdUpdate :
        cmdCreate;

    // save parameters
    params.Clear();
    params.Add(PARAM_NAME_OBJ, gNameLev);
    params.Add(PARAM_TYPE_ANALYSER, IntegerToString(anaLevel));
    params.Add(PARAM_PERIOD, IntegerToString(_Period));
    params.Add(PARAM_KIND_LEVEL, IntegerToString(gKindLevel));
    params.Add(PARAM_RISK_VALUE, DoubleToString(gRiskValue, 2));
    params.Add(PARAM_RISK_UNIT, IntegerToString(gRiskUnit));
    params.Add(PARAM_ALG_CLOSE, IntegerToString(gAlgClose));
    params.Add(PARAM_TICKET_OLD, IntegerToString(gTicketOld));
    params.Add(PARAM_PROFITS, IntegerToString(gProfits));
    params.Add(PARAM_LOSSES, IntegerToString(gLosses));
    params.Save();
    
    // send a command to the Expert Advisor
    params.SendCommand(cmd);
    delete params;
    
    return cmd;
}


The params.SendCommand(cmd) function is as follows:

// -----------------------------------------------------------------------------
// Send a command to the Expert Advisor                                        |
// -----------------------------------------------------------------------------
void Params::SendCommand(NCommand cmd) {
    string nameObj = NAME_OBJECT_CMD;
    ObjectCreate(0, nameObj, OBJ_LABEL, 0, 0, 0);
    ObjectSetString(0, nameObj, OBJPROP_TEXT, m_path);
    ObjectSetInteger(0, nameObj, OBJPROP_ZORDER, cmd);
    ObjectSetInteger(0, nameObj, OBJPROP_TIMEFRAMES, 0);
}

Every tick (OnTick), the Expert Advisor checks the existence of the object named NAME_OBJECT_CMD via the CheckExpernalCommand() function. If it exists, the command and the path to the file with the analyzer parameters are read, and the object is immediately deleted. Next, the Expert Advisor searches for a running analyzer by the file name. If cmd == cmdDelete, then the analyzer is deleted. If cmd == cmdUpdate, then the analyzer parameters are updated from the file. If cmd == cmdNew, then a new analyzer is created with parameters from the file.

Here is the full text of the Params class which encapsulates the logic for working with parameter files (Key=Value strings).

#property copyright "Copyright 2020, Malik Arykov"
#property link      "malik.arykov@gmail.com"

#include <Arrays/ArrayString.mqh>
#include <Cayman/UConvert.mqh>
#include <Cayman/UFile.mqh>

// -----------------------------------------------------------------------------
// Parameter class (key=value strings with # comments)                           |
// -----------------------------------------------------------------------------
class Params {
private:
    string  m_path; // path to parameter file
    NCommand m_cmd; // command for the Expert Advisor
    CArrayString *m_items; // array of pairs {key=value}
    int Find(string key);
public:
    Params();
    ~Params() { delete m_items; };
    void Clear() { m_items.Clear(); };
    int Total() { return m_items.Total(); };
    string Path() { return m_path; };
    CArrayString *Items() { return m_items; };
    void Add(string line) { m_items.Add(line); };
    bool Add(string key, string value);
    string GetValue(string key);
    void Load(string prefix, int typeAnalyser, string nameObj, string speriod);
    void Load(string path);
    void Save();
    void SendCommand(NCommand cmd);
    NCommand TakeCommand();
    void Dump(string sender);
};

// -----------------------------------------------------------------------------
// Default constructor                                                         |
// -----------------------------------------------------------------------------
Params::Params() {
    m_items = new CArrayString();
}

// -----------------------------------------------------------------------------
// Add a key=value pair                                                        | 
// -----------------------------------------------------------------------------
bool Params::Add(string key, string value) {

    int j = Find(key);
    string line = key + "=" + value;
    if (j >= 0) { // update
        m_items.Update(j, line);
        return false;
    }
    else { // add
        m_items.Add(line);
        return true;
    }
}

// -----------------------------------------------------------------------------
// Get the value of a parameter by key                                         |
// -----------------------------------------------------------------------------
string Params::GetValue(string key) {

    // find the key
    int j = Find(key);
    if (j < 0) return NULL; // no key
    
    // check the separator
    string line = m_items.At(j);
    j = StringFind(line, "=");
    if (j < 0) { // no =
        PrintFormat("%s / ERROR: Invalid string %s", __FUNCTION__, line);
        return NULL;
    }
    
    // return the value
    return UConvert::Trim(StringSubstr(line, j + 1));
}

// -----------------------------------------------------------------------------
// Find the value of a parameter by key                                        |
// -----------------------------------------------------------------------------
int Params::Find(string key) {

    int index = -1;
    for (int j = 0; j < m_items.Total(); j++) {
        if (StringFind(m_items.At(j), key) == 0) {
            index = j;
            break;
        }
    }
    return index;
}

// -----------------------------------------------------------------------------
// Load parameters                                                             |
// -----------------------------------------------------------------------------
void Params::Load(string prefix, int typeAnalyser, string nameObj, string speriod) {
    string nameFile = StringFormat("%s%02i_%s_%s.txt", prefix, typeAnalyser, nameObj, speriod);
    m_path = StringFormat("%s%s/%s", PATH_PARAMS, IntegerToString(ChartID()), nameFile);
    if (FileIsExist(m_path)) Load(m_path);
}

// -----------------------------------------------------------------------------
// Load parameters                                                             |
// -----------------------------------------------------------------------------
void Params::Load(string path) {
  
    m_path = path;
    if (!FileIsExist(m_path)) return;

    //PrintFormat("%s / %s", __FUNCTION__, m_path);
    string text = UFile::LoadText(m_path);
    if (text == NULL) return;
    
    // split the text into lines
    string line, lines[];
    int numLines = StringSplit(text, DLM_LINE, lines);
    for (int j = 0; j < numLines; j++) {
        line = lines[j];
        // delete the comment
        int k = StringFind(line, "#");
        if (k == 0) continue; // the whole string is a comment
        if (k > 0) line = StringSubstr(line, 0, k);
        // add a non-empty string
        if (line != "") m_items.Add(line);
    }
}

// -----------------------------------------------------------------------------
// Save parameters                                                             |
// -----------------------------------------------------------------------------
void Params::Save() {

    string text = "";
    for (int j = 0; j < m_items.Total(); j++) {
        text += m_items.At(j) + "\n";
    }
    // rewrite the existing file
    UFile::SaveText(text, m_path, true);
}

// -----------------------------------------------------------------------------
// Send a command to the Expert Advisor                                        | 
// -----------------------------------------------------------------------------
void Params::SendCommand(NCommand cmd) {
    string nameObj = NAME_OBJECT_CMD;
    ObjectCreate(0, nameObj, OBJ_LABEL, 0, 0, 0);
    ObjectSetString(0, nameObj, OBJPROP_TEXT, m_path);
    ObjectSetInteger(0, nameObj, OBJPROP_ZORDER, cmd);
    ObjectSetInteger(0, nameObj, OBJPROP_TIMEFRAMES, 0);
}

// -----------------------------------------------------------------------------
// Receive a command from the script                                           |
// -----------------------------------------------------------------------------
NCommand Params::TakeCommand() {
    string nameObj = NAME_OBJECT_CMD;
    if (ObjectFind(0, nameObj) < 0) return cmdUnknown;
    
    m_path = ObjectGetString(0, nameObj, OBJPROP_TEXT);
    m_cmd = (NCommand)ObjectGetInteger(0, nameObj, OBJPROP_ZORDER);
    ObjectDelete(0, nameObj);
    Load(m_path);
    
    return m_cmd;
}

// -----------------------------------------------------------------------------
// |Dump parameters                                                            |                                                             
// -----------------------------------------------------------------------------
void Params::Dump(string sender) {
    for (int j = 0; j < m_items.Total(); j++) {
        PrintFormat("%s / %s", sender, m_items.At(j));
    }
}

For MQL5 Fans: when changing the m_items type to CHashMap, the code of the Add, GetValue, Find functions will be significantly reduced. But the Params class is also used in MQL4. Furthermore, parameter access speed is not important in this case, as the parameters are read once to initialize local variables. Why haven't I remade the class for CHashMap for MQL5? Probably because I worked in a bank for a long time. Financial software developers have a very important principle: If it works, don't touch it! ;-)


Passing parameters to external programs

The data exchange unit between different systems is de facto a json file. Previously it was an xml file. The main advantages of json files are:

For example, there is a Bar class with the following fields: m_time, m_open, m_high, m_low, m_close, m_body. Where m_body is the candlestick color: white, black or doji. The Bar class has a ToJson() method which generates a json string

string Bar::ToJson() {
        return "{" +
        "\n\t\"symbol\":\"" + _Symbol + "\"," +
        "\n\t\"period\":" + IntegerToString(_Period) + "," +
        "\n\t\"digits\":" + IntegerToString(_Digits) + "," +
        "\n\t\"timeBar\":\"" + TimeToStr(m_time) + "\"," +
        "\n\t\"open\":" + DoubleToString(m_open, _Digits) + "," +
        "\n\t\"high\":" + DoubleToString(m_high, _Digits) + "," +
        "\n\t\"low\":" + DoubleToString(m_low, _Digits) + "," +
        "\n\t\"close\":" + DoubleToString(m_close, _Digits) + "," +
        "\n\t\"body\":" + IntegerToString(m_body) + "," +
        "\n}";
}


We could use StringFormat instead, but this would cause problems while rearranging or deleting values. Formatting “\n\t” could be deleted since there are quite a lot of online json formatting services. One of them is JSON Parser. Set the receiving of a valid json once and use the bar.ToJson() function whenever you need it.

An external program, for example a C# application, can convert a json file of any complexity into an object. How to transfer a json file from MQL? It is very simple. Load (save) the json file, for example, to the Files/Json terminal directory. An external program monitors this directory for new files. Having found a file, the program reads it, converts it into an object and immediately deletes the file or moves it to the archive (for statistics).


Receiving parameters from external programs

Connecting a json library (or reinventing the wheel) to MQL programs causes extra trouble. A better solution is to pass text files with Key=Value strings. Files can be processed using the Params class (see above). The Expert Advisor and the Indicator are candidates for receiving parameters from external programs or scripts. For example, you need to call the CheckExternalCommand() function in the OnTick handler, which will check the existence of files in the Files/ExtCmd directory. When a file is found, it should read, process (accept the parameters) and delete the file.

So, we have considered methods for receiving and passing parameters between MQL and external programs. Now think about the following: Why do MQL programs need DLLs? Such programs are not accepted by the MQL Market. There is only one reason — security, since you can access anything from a DLL.


Passing parameters to a smartphone

For further operations, I will use the Android app WirePusher. This is a wonderful service (free and with no adds). I do not know if there is something like this for iPhone. If there are any iPhone fans reading this article, please share in comments. 

To start using the service:

Then launch the script (do not forget to write your id instead of asterisks in id = “********”

void OnStart() {
    string id = "**********"; // your smartphone id in WirePusher
    WirePusher("Profit $1000", "Deal", "Closed", id);
}

// ------------------------------------------------------------------------------------------------
// Send notification to smartphone via the WirePusher web service
// Add https://wirepusher.com to Terminal/Service/Settings/Experts/Allow WebRequest
// message - notification text
// title - notification title (for example, Attention / Alert / Deal)
// type - notification type (for example, Triggered pending order / Levels breakout / Closed)
// id - unique smartphone id from the WirePusher Android-app
// ------------------------------------------------------------------------------------------------
bool WirePusher(string message, string title, string type, string id) {

    char data[]; // HTTP message body data array
    char result[]; // Web service response data array
    string answer; // Web service response headers
    string url = "https://wirepusher.com/send?id={id}&title={title}&message={message}&type={type}";
    
    StringReplace(url, "{id}", id);
    StringReplace(url, "{type}", type);
    StringReplace(url, "{title}", title);
    StringReplace(url, "{message}", message);
    
    ResetLastError();
    int rcode = WebRequest("GET", url, NULL, 3000, data, result, answer);
    if (rcode != 200) {
        PrintFormat("%s / error=%i / url=%s / answer=%s / %s", 
            __FUNCTION__, GetLastError(), url, answer, CharArrayToString(result));
        return false;
    }
    PrintFormat("%s / %s / %s", __FUNCTION__, title, message);
    return true;
}

In the Cayman EA, the WirePusher function is called in AnalyserTrade when:

  • A pending order triggers
  • The price breaks though a trading level
  • A deal closes

An individual sound can be assigned to each notification type on WirePusher. Previously, I had a "ta-da" sound for deals closed with profit and a "bomb" sound for those closed with loss. But then I got tired of bombs.


Conclusion

The most reliable and convenient method for storing parameters is using text files. Moreover, file operations are completely supported/cached in any operating system (application).