Guidelines for writing portfolio expert advisors in MQL5

[Versiunea romaneasca] [MQLmagazine.com in romana] [English edition]

Portfolio expert advisors, as well as the more complex multiasset expert advisors, were awaited by the retail trading community ever since limitations of the MT4 backtester became obvious, that is, about 4 – 5 years ago. The difference between multiasset and portfolio EAs is that multiasset EAs have an integrated strategy where each asset is a component, whether portfolio EAs replicate same strategy, with just some parameter differences, over more instruments. Of course, in the MT4 context, normal EAs could have been run on separate instruments and obtain portfolio effects, however backtesting could never have covered these portfolio effects because it was not possible to backtest on more than one instrument.

The main advantage of portfolying is the yield averaging while spreading the risk. The degree up to which this happens, is dependant on the overall portfolio correlation. The lesser overall correlated, the lesser nonsysthematic risk left uncovered. However, if you don’t pick the portfolio right, instead to balance your overall trading system, it will add extra instability.

A portfolio trading system must be able to:
– apply similar trading logics to a large number of assets;
– differentiate trading logics from one instrument to another;
– control exposure.

It requires a pretty large data structure to take make things manageable.

For instance, this one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
input double MarginUsagePerPosition=0.5;
 
int SymbolsCount;
 
struct IndicatorHandlersStruct
  {
   //...
  };
 
struct SystemParametersStruct
  {
   //...
  };
 
struct SymbolData
  {
    string Symbol;
    IndicatorHandlersStruct IndicatorHandlers;
    SystemParametersStruct SystemParameters;
  };
 
SymbolData SymbolsTable[300];

The first variable, MarginUsagePerPosition is a sort of asset allocation parameter, by controlling the maximal volume of each position. SymbolsCount tells how many symbols are used and it can be used for enumeration, while the big SymbolsTable[] will contain all the needed data, from symbols to handlers. For instance, the following structure will be set up to calculate 4 moving averages (two on M5 and two on H1) per each symbol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct IndicatorHandlersStruct
  {
   int MovingAveragesHandlers14[2];
   int MovingAveragesHandlers9[2];
  };
 
struct SymbolData
  {
    string Symbol;
    IndicatorHandlersStruct IndicatorHandlers;
    datetime LastBarTime;
  };
 
SymbolData SymbolsTable[300];

Before OnInit() we have to write a procedure to fill in the SymbolsTable[] with the indicator handlers. MakeIndicatorHandlers() is being called from OnInit() to separate the symbols setup from the indicators setup. While you can easily alter OnInit() to change the list, by adding, deleting or making an automated instrument selector, the MakeIndicatorHandlers() will remain static, with the purpose to fill the structure with the needed indicator handlers.

1
2
3
4
5
6
7
8
9
10
11
12
void MakeIndicatorHandlers()
  {
   datetime lastbar[1];
   for (int i=0;i<SymbolsCount;i++)
      {
       SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[0]=iTEMA(SymbolsTable[i].Symbol,PERIOD_M5,14,0,PRICE_OPEN);
       SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[0]=iTEMA(SymbolsTable[i].Symbol,PERIOD_M5,9,0,PRICE_OPEN);
       SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[1]=iTEMA(SymbolsTable[i].Symbol,PERIOD_H1,14,0,PRICE_OPEN);
       SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[1]=iTEMA(SymbolsTable[i].Symbol,PERIOD_H1,9,0,PRICE_OPEN);  
      }
   return;
  }

Within OnInit(), we set up the symbols table. For instance, we are making it to work on 3 forex pairs: EURUSD, USDCHF, GBPJPY:

1
2
3
4
5
6
7
8
9
int OnInit()
  {
   SymbolsTable[0].Symbol="EURUSD";
   SymbolsTable[1].Symbol="USDCHF";
   SymbolsTable[2].Symbol="GBPJPY";
   SymbolsCount=3;
   MakeIndicatorHandlers();
   return(0);
  }

And now OnTick():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void OnTick()
  {
   datetime datetime_array[1];
   for (int i=0;i<SymbolsCount;i++)
      {
       CopyTime(SymbolsTable[i].Symbol,PERIOD_M5,0,1,datetime_array);
       if (datetime_array[0]!=SymbolsTable[i].LastBarTime)
         {               
          TradeLogics(i);
          LastBarTime[i]=datetime_array[0];    
         }//if (datetime_array[0]!=SymbolsTable[i].LastBarTime)
      }//for (int i=0;i<SymbolsCount;i++)
   return;
  }

Now all the pieces are complete. OnTick() will check for each instrument if a new bar appeared and will call TradeLogics() for every instrument in the array, which will implement decision and trading.

We will not stop on TradeLogics() in this article, because TradeLogics() is EA specific. But a few principles should be guides while writing the TradeLogics(). The first division of the TradeLogics() is the asset class that it deals with. It can’t deal with equities the same way it deals with forex. Equities don’t run 24 hours a day 5 days a week. They have specific trading sessions and gaps from a day to another, not mentioning from a week to another. So this is the first division, the asset class. The second, is that it is better to separate the trading functions from the TradeLogics(). For instance, the TradeLogics() could just command how to adjust the position on a given instrument, and leave the trading function to a ManagePosition() procedure.
Of course, TradeLogics() can be in practice triggered by OnTrade() or OnChartEvent(), or even a CEP engine’s EventsCallback().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
double UnitsToLots(double units,string symbol)
  {
  double dlotsize=SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
  double mag=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
  double mini=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
  double big=1/mag;    
  double size=dlotsize/big;
  double dlots0,dlots1,back0,back1;
  dlots0=NormalizeDouble(units/size,0)*mag;
  dlots1=dlots0+mag;
  back0=dlots0*big*size;
  back1=dlots1*big*size;
  if (back1-units<units-back0)
    {
     if (dlots1<mini)
       return(mini);  
     else  
       return(dlots1);
    }
  else
    {
     if (dlots0==0)
        return(mini);
     else
       {
        if (dlots0<mini)
          return(mini);
        else
          return(dlots0);
       }//else if (dlots0==0)
    }//else if if (back1-units<units-back0)
  }

UnitsToLots() is a port of a function that I wrote in MQL4 times, and it was working at that time with MarketInfo(). UnitsToLots() will return the appropriate number of lots correspondant to a volume given in units. For instance it may answer 1.0 for 100000 units of EURUSD.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define FLAT        0
#define LONG        1
#define SHORT       2
 
int GetPositionType(int asset_index)
  {
   string symbol=SymbolsTable[asset_index].Symbol;
   bool sel=PositionSelect(symbol);
   if (sel==false)
     return(FLAT);
   else
    {
     long p=PositionGetInteger(POSITION_TYPE);
     if (p==POSITION_TYPE_BUY)
       return(LONG);
     else
       return(SHORT);
    }
  }

This function is a simple proxy to get a straight position type value. The value for POSITION_TYPE_BUY is 0 in MQL5, and PositionInfoInteger() will answer 0 even if the position was not previously selected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
void ManagePosition(int asset_index,int operation,double forcevolume=0.00)
  {   
   MqlTradeRequest request;
   MqlTradeResult result;
   int p=GetPositionType(asset_index);
   double now_volume;
   double current_volume,v0;
   if (PositionSelect(SymbolsTable[asset_index].Symbol)==true)
     current_volume=PositionGetDouble(POSITION_VOLUME);
   else
     current_volume=0;
   v0=current_volume;   
   request.action=TRADE_ACTION_DEAL;
   request.symbol=SymbolsTable[asset_index].Symbol; 
   request.deviation=Slippage;
   request.type_filling=ORDER_FILLING_AON;
   request.type_time=ORDER_TIME_GTC;
   if (p==FLAT)
     {
      if (DoubleToString(forcevolume,2)=="0.00")
        request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
      else
        request.volume=forcevolume;
      if (operation==LONG)
        {
         request.type=ORDER_TYPE_BUY;
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
        }
      if (operation==SHORT)
        {
         request.type=ORDER_TYPE_SELL;
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
        }
      if (operation==FLAT)
        return;
     }
   else//if (p==FLAT)
     {     
      if (p==LONG&&operation==SHORT)
        {
         if (DoubleToString(forcevolume,2)=="0.00")
           request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
         else
           request.volume=current_volume+forcevolume;
         request.type=ORDER_TYPE_SELL;         
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));         
        }
      if (p==SHORT&&operation==LONG)
        {
         if (DoubleToString(forcevolume,2)=="0.00")         
           request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
         else
           request.volume=current_volume+forcevolume;
         request.type=ORDER_TYPE_BUY;
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
        }
      if (p==LONG&&operation==FLAT)
        {       
         if (DoubleToString(forcevolume,2)=="0.00")
           request.volume=current_volume;
         else
           request.volume=forcevolume;
         request.type=ORDER_TYPE_SELL;         
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));         
        }
      if (p==SHORT&&operation==FLAT)
        {
         if (DoubleToString(forcevolume,2)=="0.00")           
           request.volume=current_volume;
         else
           request.volume=forcevolume;
         request.type=ORDER_TYPE_BUY;         
         request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));         
        } 
      if (p==LONG&&operation==LONG)//adjustment of the present LONG position
        {
         if (DoubleToString(forcevolume,2)=="0.00")
           request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-current_volume;
         else
           request.volume=forcevolume-current_volume;
         if (NormalizeDouble(request.volume,2)>0.00)
           {         
            request.type=ORDER_TYPE_BUY;
            request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
           }
         else //we close the difference
           {
            request.volume=MathAbs(request.volume);
            request.type=ORDER_TYPE_SELL;         
            request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));         
           }
        }        
      if (p==SHORT&&operation==SHORT)//adjustment of the present SHORT position
        {
         if (DoubleToString(forcevolume,2)=="0.00")
           request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-current_volume;
         else
           request.volume=forcevolume-current_volume;
         if (NormalizeDouble(request.volume,2)>0.00)
           {
            request.type=ORDER_TYPE_SELL;         
            request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));                  
           }
         else
           {
            request.volume=MathAbs(request.volume);
            request.type=ORDER_TYPE_BUY;
            request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));            
           }
        }        
     }//if (p==FLAT)]  
   if (DoubleToString(request.volume,2)!="0.00")
     {
      OrderSend(request,result);     
      PositionSetSLTP(StopLoss,TakeProfit);
     }     
  }

What does ManagePosition() do ? It opens a position on the given asset by index, upon request, if it is flattened before ; or it reverts the current position, by recalculating the new lot size (calculates new lot size with the formula, adds to current volume and reverses operation). It can also add or cut from the current position. The forcevolume parameter can be used to force a given volume into a trade, bypassing calculus.
How about setting a stop loss ? Nothing easier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void PositionSetSLTP(int asset_index,int sl,int tp)
  {
   bool todosl, todotp;
   MqlTradeRequest request;
   MqlTradeResult result;   
   double price=0.00;
   int p=GetPositionType(asset_index);
   if (p==LONG||p==SHORT)
     price=PositionGetDouble(POSITION_PRICE_OPEN);
   request.action=TRADE_ACTION_SLTP;
   request.symbol=SymbolsTable[asset_index].Symbol;   
   todosl=false;
   todotp=false;
   if (DoubleToString(sl,4)!="0.0000")
     {
      todosl=true;      
      if (p==LONG)
        request.sl=NormalizeDouble(price-sl*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
      if (p==SHORT)
        request.sl=NormalizeDouble(price+sl*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
     }
   if (DoubleToString(tp,4)!="0.0000")
     {      
      todotp=true;
      if (p==LONG)
        request.tp=NormalizeDouble(price+tp*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
      if (p==SHORT)
        request.tp=NormalizeDouble(price-tp*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
     }
   request.deviation=Slippage;   
   if (todosl==true||todotp==true)
     OrderSend(request,result);
  }

Note that the OrderSend() calls are not followed by an analysis of the result. We didn’t dwelve into retcodes because some of them are not completely clear to us.

And finally OnDeinit(), which destroys the indicator handlers.

1
2
3
4
5
6
7
8
9
10
11
void OnDeinit(const int reason)
  {
   for (int i=0;i<SymbolsCount;i++)
      {   
       IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[0]);
       IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[0]);
       IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[1]);
       IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[1]);
      }
   return;
  }