Trading systems virtualization to achieve equity line fitting – a myth ?

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

Did you ever think that you’d like to have a moving average over the equity of a trading system, so you can turn it on and off ? This would be called equity line fitting.

The frustrating aspect is that once you start doing this from the trading system that you want to curve it its equity line, you cannot control the process anymore, as the indicators of the equity line will report differently once you hit the switch.

So, what you have to do is to let the system paper trade somehow in parallel to your main trading system, and copy the trades in your main trading system when the switch is on, ignore paper trades when the switch is off.

This can be done by virtualization. Meaningly, you have to write a paper trade position management system, like a virtual account and output its “equity” to the main trading system, which will curve-fit this equity and decide whether to replicate or not the paper system’s trades.

Thus, we begin writing such a system, in file which we’ll call TradeVirtualization.mqh .

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
//+------------------------------------------------------------------+
//|                                          TradeVirtualization.mqh |
//|                                       Copyright Bogdan Caramalac |
//|                                           http://mqlmagazine.com |
//+------------------------------------------------------------------+
#property copyright "Copyright Bogdan Caramalac"
#property link      "http://mqlmagazine.com"
#property version "1.0"
 
#define FLAT        0
#define LONG        1
#define SHORT       2
 
struct VirtualSymbolData
  {
    string Symbol;    
    double position;
    double price;
    double sl;
    double tp;
    double marked_price;    
  };
 
struct VirtualEquityData
  {
   datetime opentime,closetime;
   double open,high,low,close;  
  };

We begin with the regular defines for position management, followed by two structures. The first is intended to store symbol data in, providing symbol, position (positive – long, negative – short), price (price of initiation for the position), marked price (last price at which the position was marked to market). The second is the intended to store virtual equity data : open and close times of an equity bar ; open, high, low, close for the equity within interval. Now it’s time for the VirtualAccount class.

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
class VirtualAccount
  {   
   public:
   int VirtualSymbolsCount;    
   VirtualSymbolData VirtualSymbolsTable[30]; 
   VirtualEquityData VirtualEquity[500];
   double VirtualLastAccountEquity;  
   int VirtualLastEquityPos;
   uint VirtualEquityBarSpan;
 
   void VirtualAccountSet(double Equity,uint TimeSpan)   
       {
        datetime tc=TimeCurrent();
        VirtualEquity[0].opentime=tc;
        VirtualEquity[0].open=Equity;
        VirtualEquity[0].high=Equity;
        VirtualEquity[0].low=Equity;
        VirtualEquity[0].close=Equity;
        VirtualEquity[0].closetime=tc;
        VirtualLastAccountEquity=VirtualEquity[0].close;
        VirtualEquityBarSpan=TimeSpan;
        VirtualLastEquityPos=0;
        return;
       }  
 
    int VirtualGetPositionType(int asset_index); 
    double VirtualGetPositionSize(int asset_index);
    void VirtualPositionSetSL(double asset_index,double sl);
    void VirtualPositionSetTP(double asset_index,double tp);
    void VirtualMarkToMarket(int asset_index);
    void VirtualChangePosition(int asset_index,int direction,double price,double lotsize);
    private:
    void VirtualAddNewEquity(double equity);
 
    public:
    VirtualAccount() 
       {
        VirtualAccountSet(AccountInfoDouble(ACCOUNT_EQUITY),0);
       };      
  };

The class contains an array of VirtualSymbolsData and its count, and an array with VirtualEquityData. Other key parameters are VirtualLastEquityPos and VirtualEquityBarSpan (the time interval of a bar, in seconds). VirtualLastAccountEquity is just a proxy for retrieving last recorded equity.

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
int VirtualAccount::VirtualGetPositionType(int asset_index)     
   {
    if (DoubleToString(NormalizeDouble(VirtualSymbolsTable[asset_index].position,2),2)=="0.00")
      return(FLAT);
    if (VirtualSymbolsTable[asset_index].position>0.00)
      return(LONG);
    if (VirtualSymbolsTable[asset_index].position<0.00)
      return(SHORT); 
    return(0);       
   }
 
double VirtualAccount::VirtualGetPositionSize(int asset_index)     
   {
    if (DoubleToString(NormalizeDouble(VirtualSymbolsTable[asset_index].position,2),2)=="0.00")
      return(0.00);
    return(NormalizeDouble(MathAbs(VirtualSymbolsTable[asset_index].position),2));       
   }       
 
 void VirtualAccount::VirtualPositionSetSL(double asset_index,double sl)
     {
      VirtualSymbolsTable[asset_index].sl=sl;
     }
 
 void VirtualAccount::VirtualPositionSetTP(double asset_index,double tp)
     {
      VirtualSymbolsTable[asset_index].tp=tp;
     }

VirtualGetPositionType() and VirtualGetPositionSize() are pure informational. The position size is always positive, like the POSITION_VOLUME requests to MQL5’s PositionInfoDouble(). The other two functions do what their name says they do.

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
void VirtualAccount::VirtualAddNewEquity(double equity)
    {
     datetime tc=TimeCurrent();
     uint diff;
     diff=tc-VirtualEquity[VirtualLastEquityPos].opentime;
     if (diff<=VirtualEquityBarSpan)
       {
        if (VirtualEquity[VirtualLastEquityPos].high<equity)
          VirtualEquity[VirtualLastEquityPos].high=equity;
        if (VirtualEquity[VirtualLastEquityPos].low>equity)
          VirtualEquity[VirtualLastEquityPos].low=equity;
        VirtualEquity[VirtualLastEquityPos].close=equity;
        diff=tc-VirtualEquity[VirtualLastEquityPos].closetime;           
        VirtualEquity[VirtualLastEquityPos].closetime=tc;           
        VirtualLastAccountEquity=equity;
        return;
       }
     if (VirtualLastEquityPos!=ArrayRange(VirtualEquity,0)-1)
       {           
        VirtualLastEquityPos++;
        VirtualEquity[VirtualLastEquityPos].opentime=tc;
        VirtualEquity[VirtualLastEquityPos].open=equity;
        VirtualEquity[VirtualLastEquityPos].high=equity;
        VirtualEquity[VirtualLastEquityPos].low=equity;
        VirtualEquity[VirtualLastEquityPos].close=equity;           
        VirtualEquity[VirtualLastEquityPos].closetime=tc;
        VirtualLastAccountEquity=equity;
        return;
       }
     else
       {
        for (int i=0;i<ArrayRange(VirtualEquity,0)-1;i++)
           VirtualEquity[i]=VirtualEquity[i+1];                  
        VirtualEquity[VirtualLastEquityPos].opentime=tc;
        VirtualEquity[VirtualLastEquityPos].open=equity;
        VirtualEquity[VirtualLastEquityPos].high=equity;
        VirtualEquity[VirtualLastEquityPos].low=equity;
        VirtualEquity[VirtualLastEquityPos].close=equity;           
        VirtualEquity[VirtualLastEquityPos].closetime=tc;
        VirtualLastAccountEquity=equity;
        return;
       }        
    }

At the beginning, the class is automatically set up with an equity equal to the account equity it runs on and a timespan of 0. The user may set the account to have a different startup equity and timespan. At every tick, VirtualMarkToMarket(-1) has to be called to ensure that all positions are marked to market with the new tick data. After each mark to market (either individual, with a positive parameter, or overall, with a negative parameter), a new equity value is calculated. Now the recording in the VirtualEquity[] begins from 0. If the timespan is not 0, and the new tick comes within the timespan from last equity bar opening time, the new tick is recorded on that bar, on the close field, stamping also the closetime field. High and low per equity bar is recalculated. If the timespan is 0, or if the new equity comes too late, it enters on the following bar, and so on, up to the maximum subscript (499). When it is to enter on 500, array is slided to the left one position and bar 499 starts to be recorded again. VirtualAddNewEquity() is called only from this class. Thus, it was designated to be a private method.

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
void VirtualAccount::VirtualMarkToMarket(int asset_index) //marks a new equity value
    {
     double fluctuation=0.00;
     double rprice,new_equity;
     int p;
     if (asset_index<0)//mark all
       {
        for (int i=0;i<VirtualSymbolsCount;i++)
           {
            if (DoubleToString(VirtualSymbolsTable[i].position,2)=="0.00")
              continue;
            if (NormalizeDouble(VirtualSymbolsTable[i].position,2)>0.00)
              rprice=NormalizeDouble(SymbolInfoDouble(VirtualSymbolsTable[i].Symbol,SYMBOL_BID),SymbolInfoInteger(VirtualSymbolsTable[i].Symbol,SYMBOL_DIGITS));
            if (NormalizeDouble(VirtualSymbolsTable[i].position,2)<0.00)
              rprice=NormalizeDouble(SymbolInfoDouble(VirtualSymbolsTable[i].Symbol,SYMBOL_ASK),SymbolInfoInteger(VirtualSymbolsTable[i].Symbol,SYMBOL_DIGITS));                
            fluctuation=fluctuation+MathAbs(VirtualSymbolsTable[i].position)*SymbolInfoDouble(VirtualSymbolsTable[i].Symbol,SYMBOL_TRADE_CONTRACT_SIZE)*(rprice-VirtualSymbolsTable[i].marked_price);               
            VirtualSymbolsTable[i].marked_price=rprice;
            if (DoubleToString(VirtualSymbolsTable[i].sl,5)!="0.00000")
              {
               if (VirtualSymbolsTable[i].position>0.00&&rprice<=VirtualSymbolsTable[i].sl)
                 {
                  VirtualSymbolsTable[i].position=0.00;                     
                  VirtualSymbolsTable[i].tp=0.00;
                  VirtualSymbolsTable[i].sl=0.00;
                 }
               if (VirtualSymbolsTable[i].position<0.00&&rprice>=VirtualSymbolsTable[i].sl)
                 {
                  VirtualSymbolsTable[i].position=0.00;                     
                  VirtualSymbolsTable[i].tp=0.00;
                  VirtualSymbolsTable[i].sl=0.00;
                 }                    
              }
            if (DoubleToString(VirtualSymbolsTable[i].tp,5)!="0.00000")
              {
               if (VirtualSymbolsTable[i].position>0.00&&rprice>=VirtualSymbolsTable[i].tp)
                 {
                  VirtualSymbolsTable[i].position=0.00;                     
                  VirtualSymbolsTable[i].tp=0.00;
                  VirtualSymbolsTable[i].sl=0.00;
                 }
               if (VirtualSymbolsTable[i].position<0.00&&rprice<=VirtualSymbolsTable[i].tp)
                 {
                  VirtualSymbolsTable[i].position=0.00;                     
                  VirtualSymbolsTable[i].tp=0.00;
                  VirtualSymbolsTable[i].sl=0.00;
                 }                    
              }                         
           }
        new_equity=VirtualEquity[VirtualLastEquityPos].close+fluctuation;
        VirtualAddNewEquity(new_equity);  
       }                      
     else
       {
        if (DoubleToString(VirtualSymbolsTable[asset_index].position,2)!="0.00")
          {
           if (NormalizeDouble(VirtualSymbolsTable[asset_index].position,2)>0.00)
             {
              p=LONG;
              rprice=NormalizeDouble(SymbolInfoDouble(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));
             }
           if (NormalizeDouble(VirtualSymbolsTable[asset_index].position,2)<0.00)
             {
              p=SHORT;
              rprice=NormalizeDouble(SymbolInfoDouble(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS));                
             }
           //if (p==LONG)
           //  Print("Market marking @ ",DoubleToString(rprice,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS))," for ",VirtualSymbolsTable[asset_index].Symbol," ",DoubleToString(VirtualSymbolsTable[asset_index].position,2)," LONG @ ",DoubleToString(VirtualSymbolsTable[asset_index].price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)),"last mark @ ",DoubleToString(VirtualSymbolsTable[asset_index].marked_price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS))," -> ",DoubleToString(new_equity,2)," by fl. ",DoubleToString(fluctuation,2));
           //if (p==SHORT)
           //  Print("Market marking @ ",DoubleToString(rprice,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS))," for ",VirtualSymbolsTable[asset_index].Symbol," ",DoubleToString(VirtualSymbolsTable[asset_index].position,2)," SHORT @ ",DoubleToString(VirtualSymbolsTable[asset_index].price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)),"last mark @ ",DoubleToString(VirtualSymbolsTable[asset_index].marked_price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS))," -> ",DoubleToString(new_equity,2)," by fl. ",DoubleToString(fluctuation,2));                
           fluctuation=fluctuation+MathAbs(VirtualSymbolsTable[asset_index].position)*SymbolInfoDouble(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_TRADE_CONTRACT_SIZE)*(rprice-VirtualSymbolsTable[asset_index].marked_price);               
           VirtualSymbolsTable[asset_index].marked_price=rprice;
           if (DoubleToString(VirtualSymbolsTable[asset_index].sl,5)!="0.00000")
             {
              if (VirtualSymbolsTable[asset_index].position>0.00&&rprice<=VirtualSymbolsTable[asset_index].sl)
                {
                 VirtualSymbolsTable[asset_index].position=0.00;                     
                 VirtualSymbolsTable[asset_index].tp=0.00;
                 VirtualSymbolsTable[asset_index].sl=0.00;
                }
              if (VirtualSymbolsTable[asset_index].position<0.00&&rprice>=VirtualSymbolsTable[asset_index].sl)
                {
                 VirtualSymbolsTable[asset_index].position=0.00;                     
                 VirtualSymbolsTable[asset_index].tp=0.00;
                 VirtualSymbolsTable[asset_index].sl=0.00;
                }                    
             }
           if (DoubleToString(VirtualSymbolsTable[asset_index].tp,5)!="0.00000")
             {
              if (VirtualSymbolsTable[asset_index].position>0.00&&rprice>=VirtualSymbolsTable[asset_index].tp)
                {
                 VirtualSymbolsTable[asset_index].position=0.00;                     
                 VirtualSymbolsTable[asset_index].tp=0.00;
                 VirtualSymbolsTable[asset_index].sl=0.00;
                }
              if (VirtualSymbolsTable[asset_index].position<0.00&&rprice<=VirtualSymbolsTable[asset_index].tp)
                {
                 VirtualSymbolsTable[asset_index].position=0.00;                     
                 VirtualSymbolsTable[asset_index].tp=0.00;
                 VirtualSymbolsTable[asset_index].sl=0.00;
                }                    
             }                                       
          }
        new_equity=VirtualEquity[VirtualLastEquityPos].close+fluctuation;
        VirtualAddNewEquity(new_equity);             
       }
     return;
    }

VirtualMarkToMarket() is the heart of the entire class. It can be called for a position or for all positions. It marks positions to the market, calculating the account equity. For each position it calculates the fluctuation in equity from the last marked price to the current market price, which becomes marked price. For the same position, the procedure may clear the stop loss and take profits once these are taken. The user has to call VirtualMarkToMarket(-1) at every tick to mark positions to market.

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
void VirtualAccount::VirtualChangePosition(int asset_index,int direction,double price,double lotsize) //direction is ORDER_TYPE_BUY or ORDER_TYPE_SELL
    {
     int p=VirtualGetPositionType(asset_index);
     double rprice,fluctuation,new_equity;
     double oldposition;            
     if (p==FLAT)
       {
        VirtualSymbolsTable[asset_index].price=price;
        VirtualSymbolsTable[asset_index].marked_price=price;
        VirtualSymbolsTable[asset_index].sl=0.00000;
        VirtualSymbolsTable[asset_index].tp=0.00000;
        if (direction==ORDER_TYPE_BUY)             
          VirtualSymbolsTable[asset_index].position=NormalizeDouble(lotsize,2);
        if (direction==ORDER_TYPE_SELL)
          VirtualSymbolsTable[asset_index].position=NormalizeDouble(-lotsize,2);
        //Print("Position set to ",VirtualSymbolsTable[asset_index].position," @ price ",DoubleToString(VirtualSymbolsTable[asset_index].price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS) ) );
       }
     else
       {
        oldposition=VirtualSymbolsTable[asset_index].position;
        //local mark to market
        VirtualMarkToMarket(asset_index);           
        //position update
        VirtualSymbolsTable[asset_index].price=price;           
        if (direction==ORDER_TYPE_BUY)
          {
           VirtualSymbolsTable[asset_index].position=NormalizeDouble(VirtualSymbolsTable[asset_index].position+lotsize,2);
           if (oldposition<0.00&&VirtualSymbolsTable[asset_index].position>0.00||DoubleToString(VirtualSymbolsTable[asset_index].position,5)=="0.00000")
             {
              VirtualSymbolsTable[asset_index].sl=0.00000;
              VirtualSymbolsTable[asset_index].tp=0.00000;
             }
          }
        if (direction==ORDER_TYPE_SELL)
          {
           VirtualSymbolsTable[asset_index].position=NormalizeDouble(VirtualSymbolsTable[asset_index].position-lotsize,2);
           if (oldposition>0.00&&VirtualSymbolsTable[asset_index].position<0.00||DoubleToString(VirtualSymbolsTable[asset_index].position,5)=="0.00000")
             {
              VirtualSymbolsTable[asset_index].sl=0.00000;
              VirtualSymbolsTable[asset_index].tp=0.00000;
             }
          }              
        //Print("Position set to ",VirtualSymbolsTable[asset_index].position," @ price ",DoubleToString(VirtualSymbolsTable[asset_index].price,SymbolInfoInteger(VirtualSymbolsTable[asset_index].Symbol,SYMBOL_DIGITS) ) );
       }
    return;
   }

To make a trade, you have to use VirtualChangePosition() which will open a position with the given direction , price and lot size on the specified asset index. This opening is similar to a deal. Thus, only ORDER_TYPE_BUY and ORDER_TYPE_SELL are supported for operation. No pending or StopLimit orders. The function only sets or modifies positions. It also clears the previous stop loss and take profit if a position reverses sign.

Now that the class is done, we can virtualize the portfolio moving average expert from the MQL5 : A portfolio moving average sample expert article in a new file, called VirtualPortfolioMA.mq5 .

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
//+------------------------------------------------------------------+
//|                                           VirtualPortfolioMA.mq5 |
//|                                       Copyright Bogdan Caramalac |
//|                                           http://mqlmagazine.com |
//+------------------------------------------------------------------+
#property copyright "Copyright Bogdan Caramalac"
#property link      "http://mqlmagazine.com"
#property version   "1.00"
 
#define FLAT        0
#define LONG        1
#define SHORT       2
 
input int Slippage=30;
input double MarginUsagePerPosition=0.5;
input bool VolatilityFilter=false;
input int VirtualStopLoss=300;
input int VirtualTakeProfit=1000;
input int SlowMALength=14;
input int FastMALength=9;
 
input int EquitySlowMAPeriod=14;
input int EquityFastMAPeriod=9;
input int RealStopLoss=300;
input int RealTakeProfit=1000;

The variables are quite the same, with the distinction that now stop loss , take profit and moving average periods are split in two : for the virtual account and for the account the system runs on.

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
#include <TradeVirtualization.mqh>
#include <MovingAverages.mqh>
 
int SymbolsCount;
 
struct IndicatorHandlersStruct
  {
   int SlowMovingAverageHandler;
   int FastMovingAverageHandler;
   int BBoverSTDHandler;
  };
 
struct SymbolData
  {
    string Symbol;
    IndicatorHandlersStruct IndicatorHandlers;
    datetime LastBarTime;
  };
 
SymbolData SymbolsTable[30];
 
VirtualAccount VirtualSystem;    
 
datetime VirtualLastBarTime;
string RealTrading="FORBIDDEN";

The structures are quite similar. The class is instanced in the VirtualSystem object. A VirtualLastBarTime is added to record the open time of the last equity bar from the virtual account. The RealTrading variable will record the trading state with respect to copying trades from the virtual account, that is, “STRAIGHT”, “REVERSED”, or “FORBIDDEN” (only “STRAIGHT” and “FORBIDDEN” are actually used).

The VirtualMakeIndicatorHanders() takes place of the former MakeIndicatorHandlers(). The code is the same, only the name is changed.

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
void VirtualPositionSetSLTP(int asset_index,int sl,int tp)
  {
   VirtualSystem.VirtualPositionSetSL(asset_index,sl);
   VirtualSystem.VirtualPositionSetSL(asset_index,tp);
  }  
 
void VirtualManagePosition(int asset_index,int operation)
  {
   MqlTradeRequest request;
   MqlTradeResult result;
   int p;
   p=VirtualSystem.VirtualGetPositionType(asset_index);
   double now_volume;
   double current_volume,v0;
   current_volume=VirtualSystem.VirtualGetPositionSize(asset_index);
   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)
     {
      request.volume=UnitsToLots( (MarginUsagePerPosition/100)*VirtualSystem.VirtualLastAccountEquity*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
      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));
        }
     }
   else//if (p==FLAT)
     {      
      if (p==LONG&&operation==SHORT)
        {
         request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*VirtualSystem.VirtualLastAccountEquity*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
         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)
        {
         request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*VirtualSystem.VirtualLastAccountEquity*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol );
         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)
        {
         request.volume=current_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==FLAT)
        {
         request.volume=current_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==LONG&&operation==LONG)//adjustment of the present LONG position
        {
         request.volume=UnitsToLots( (MarginUsagePerPosition/100)*VirtualSystem.VirtualLastAccountEquity*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-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
        {
         request.volume=UnitsToLots( (MarginUsagePerPosition/100)*VirtualSystem.VirtualLastAccountEquity*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-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 (request.volume>0.00)
     {       
      VirtualSystem.VirtualChangePosition(asset_index,request.type,request.price,request.volume);
      VirtualPositionSetSLTP(asset_index,VirtualStopLoss,VirtualTakeProfit);      
     }
   //deciding to replicate , stay out or reverse signals on real portfolio
   if (RealTrading!="FORBIDDEN")
     {
      if (RealTrading=="STRAIGHT")
        ManagePosition(asset_index,operation);
      else
        {
         if (operation==LONG)
           ManagePosition(asset_index,SHORT);
         if (operation==SHORT)
           ManagePosition(asset_index,LONG);
         if (operation==FLAT)
           ManagePosition(asset_index,FLAT);
        }
     }
   return;     
  }

This is the VirtualManagePosition(). It operates like ManagePosition() however its requests are directed towards the virtual account. At the end, after analysing the RealTrading variable, it decides whether to do nothing, to copy or to reverse virtual operations on the actual trading account.

The MASignal(), PositionSetSLTP(), and ManagePosition() remain the same as in the original expert.

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
void VirtualTradeLogics(int asset_index)
  {
   string s;
   double stddev[1];
   double bbhi[1];
   double bblo[1];
   int copycount;
   s=MASignal(asset_index);    
   if (VolatilityFilter==true)
     {
      copycount=CopyBuffer(SymbolsTable[asset_index].IndicatorHandlers.BBoverSTDHandler,0,0,1,stddev);
      copycount=CopyBuffer(SymbolsTable[asset_index].IndicatorHandlers.BBoverSTDHandler,2,0,1,bbhi);
     }
   if (VirtualSystem.VirtualGetPositionType(asset_index)==FLAT)
     {      
      if (s=="L")
        {
         if (VolatilityFilter==true)
           {
            if (stddev[0]>bbhi[0])
              VirtualManagePosition(asset_index,LONG);
           }
         else
           VirtualManagePosition(asset_index,LONG);
        }
      if (s=="S")
        {
         if (VolatilityFilter==true)
           {
            if (stddev[0]>bbhi[0])
              VirtualManagePosition(asset_index,SHORT);
           }
         else
           VirtualManagePosition(asset_index,SHORT);
        }
     }//else if (GetPositionType(asset_index)==FLAT)
   else//position is not flat
     {
      if (VirtualSystem.VirtualGetPositionType(asset_index)==LONG)
        {        
         if (VolatilityFilter==true)
           {
            if (s=="S"||stddev[0]<bbhi[0])
              VirtualManagePosition(asset_index,FLAT);
           }           
         else
           {            
            if (s=="S")
              {               
               VirtualManagePosition(asset_index,FLAT);           
              }
           }//if (VolatilityFilter==true)
        }
      if (VirtualSystem.VirtualGetPositionType(asset_index)==SHORT)
        {         
         if (VolatilityFilter==true)
           {
            if (s=="L"||stddev[0]<bbhi[0])
              VirtualManagePosition(asset_index,FLAT);
           }
         else
           {            
            if (s=="L")
              {               
               VirtualManagePosition(asset_index,FLAT);           
              }
           }//if (VolatilityFilter==true)        
        }//if (VirtualSystem.VirtualGetPositionType(asset_index)==SHORT)
     }           
   return;
  }

This is the former TradeLogics() , but modified in order to operate on the virtual account.

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
int PeriodToMinutes(ENUM_TIMEFRAMES p)
  {
   switch(p)
     {
      case PERIOD_M1:return(1);
      case PERIOD_M2:return(2);
      case PERIOD_M3:return(3);
      case PERIOD_M4:return(4);
      case PERIOD_M5:return(5);
      case PERIOD_M6:return(6);
      case PERIOD_M10:return(10);
      case PERIOD_M12:return(12);
      case PERIOD_M15:return(15);
      case PERIOD_M20:return(20);
      case PERIOD_M30:return(30);
      case PERIOD_H1:return(60);
      case PERIOD_H2:return(60*2);
      case PERIOD_H3:return(60*3);
      case PERIOD_H4:return(60*4);
      case PERIOD_H6:return(60*6);
      case PERIOD_H8:return(60*8);
      case PERIOD_H12:return(60*12);
      case PERIOD_D1:return(60*24);
      case PERIOD_W1:return(60*24*7);
      case PERIOD_MN1:return(60*24*30);
      default: return(0);
     }
  };

This function is used in OnInit() to setup the virtual account’s bar timespan , and it converts periods into minutes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   SymbolsTable[0].Symbol="EURUSD";
   SymbolsTable[1].Symbol="USDJPY";
   SymbolsTable[2].Symbol="GBPUSD";
   SymbolsTable[3].Symbol="USDCHF";
   SymbolsCount=1;
 
   VirtualSystem.VirtualAccountSet(AccountInfoDouble(ACCOUNT_EQUITY),PeriodToMinutes(Period())*60);  
   VirtualSystem.VirtualSymbolsCount=SymbolsCount;      
   for (int i=0;i<SymbolsCount;i++)
      VirtualSystem.VirtualSymbolsTable[i].Symbol=SymbolsTable[i].Symbol;
   VirtualMakeIndicatorHandlers();
   VirtualLastBarTime=VirtualSystem.VirtualEquity[VirtualSystem.VirtualLastEquityPos].opentime;
   RealTrading="FORBIDDEN";
   return(0);
  }

This is OnInit(). It prepares symbols like former OnInit() then copies symbols to the virtual account. Sets up virtual account and makes sure RealTrading is set to “FORBIDDEN”.

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
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {   
   double loweq;         
   double fast_ma_now,slow_ma_now,fast_ma_old,slow_ma_old;
   double EquityCloseBuffer[31];
   datetime datetime_array[1];   
   //marking to market virtual positions
   VirtualSystem.VirtualMarkToMarket(-1);
   //looping symbols to apply logics
   for (int i=0;i<SymbolsCount;i++)
      {
       CopyTime(SymbolsTable[i].Symbol,Period(),0,1,datetime_array);
       if (datetime_array[0]!=SymbolsTable[i].LastBarTime)
         {                  
          VirtualTradeLogics(i);
          SymbolsTable[i].LastBarTime=datetime_array[0];
         }//if (datetime_array[0]!=LastBarOccurred[i])
      }//for (int i=0;i<SymbolsCount;i++)   
 
   //now we check if we have enough equity bars ; if true, we analyse to see whether we activate or not live trading
   if (VirtualSystem.VirtualEquity[VirtualSystem.VirtualLastEquityPos].opentime!=VirtualLastBarTime)
     {
      if (VirtualSystem.VirtualLastEquityPos>ArrayRange(EquityCloseBuffer,0))  
        {
         for (int i=1;i<=ArrayRange(EquityCloseBuffer,0)-1;i++)
            {         
             EquityCloseBuffer[i]=StringToDouble( DoubleToString(VirtualSystem.VirtualEquity[VirtualSystem.VirtualLastEquityPos-ArrayRange(EquityCloseBuffer,0)+1+i].close,2) );             
             loweq=StringToDouble( DoubleToString(VirtualSystem.VirtualEquity[VirtualSystem.VirtualLastEquityPos-ArrayRange(EquityCloseBuffer,0)+1+i].low,2));                
            }                                    
        }
      fast_ma_now=SimpleMA(ArrayRange(EquityCloseBuffer,0)-1,EquityFastMAPeriod,EquityCloseBuffer);
      slow_ma_now=SimpleMA(ArrayRange(EquityCloseBuffer,0)-1,EquitySlowMAPeriod,EquityCloseBuffer);
      fast_ma_old=SimpleMA(ArrayRange(EquityCloseBuffer,0)-2,EquityFastMAPeriod,EquityCloseBuffer);
      slow_ma_old=SimpleMA(ArrayRange(EquityCloseBuffer,0)-2,EquitySlowMAPeriod,EquityCloseBuffer);
      //bullish ma cross on virtual system's equity line      
      if (fast_ma_now>slow_ma_now&&fast_ma_old<slow_ma_old)
        {          
         RealTrading="STRAIGHT";
         //replicating virtual portfolio into real
         //position sizes are calculated to be proportional with the real account equity
         for (int i=0;i<SymbolsCount;i++)
            ManagePosition(i,VirtualSystem.VirtualGetPositionType(i), 
                    UnitsToLots(
                     (VirtualSystem.VirtualGetPositionSize(i)*
                      AccountInfoDouble(ACCOUNT_EQUITY)/
                      VirtualSystem.VirtualLastAccountEquity)*
                      SymbolInfoDouble(SymbolsTable[i].Symbol,SYMBOL_TRADE_CONTRACT_SIZE)
                      ,SymbolsTable[i].Symbol) );
        }
      //bearish ma cross on virtual system's equity line        
      if (fast_ma_now<slow_ma_now&&fast_ma_old>slow_ma_old)
        {
         RealTrading="FORBIDDEN";
         //closing real positions
         for (int i=0;i<SymbolsCount;i++)
            ManagePosition(i,FLAT);         
        }             
      VirtualLastBarTime=VirtualSystem.VirtualEquity[VirtualSystem.VirtualLastEquityPos].opentime;
     }
   return;
  }

This is the OnTick(). Like former OnTick(), it has a symbol loop and applies the logics for each symbol, on the virtual account this time. It declares the EquityCloseBuffer[] where last values of virtual acount equity bars are copied, after each loop is finished. When this buffer contains enough values, a moving average cross on the virtual account equity line is calculated. When these moving average yield a bullish signal, RealTrading is set to “STRAIGHT”. Portfolio is being replicated from the virtual account on the actual account, proportional to the size of actual account. Of course, the VirtualManagePosition() will have subsequent calls to ManagePosition() and from this time on both accounts will work in the same time. When the moving average cross becomes bearish, RealTrading is set to “FORBIDDEN” and actual account positions are closed.

The OnDeinit() is exactly the same, as the indicator handlers are residing in the same SymbolsTable[].

Surprised? The portfolio run is more unstable than one pair run, as it was for the main trading system. The extra volatility translated into range movements on the equity line, which in turn translated into false positive signals given by moving averages on the equity line, making the virtualization to be a fiasco. There are at least three points to consider:

1. Sampling frequency
What is the optimal sampling frequency for the equity moving average? After all, we don’t know exactly how MetaTrader compresses equity data on bars so we don’t know its own sampling interval. Then, how to sample for the average ?

2. Optimal trading system for the equity line
Nobody said that moving averages over equity are the best way to fit. Other criterias may apply, same as in regular trading.

3. Should you trade all the instruments in the same virtual account, or trade in separate virtual accounts and fit each one’s equity ?

The main conclusion that yields is that trading systems are not to be based on cold, pure application of indicators. Indicators, as their name says, are indicating some of the actions that have to be taken, as counselors that give advices, influencing positively the results of trading systems, but their place is not at the core of trading systems. Rather, sound statistics and arbitrage relationships are at the base of effective trading systems.

File links:
TradeVirtualization.mqh
VirtualPortfolioMA.mq5

3 Responses to “ Trading systems virtualization to achieve equity line fitting - a myth ? ”

  1. Bogdan Caramalac, MQLmagazine sr.editor on July 10, 2010 at 4:08 pm

    Due to conceptual errors that I didn’t see at the time, when running virtualization over portfolios, the system will not account for the differences in pip value that exist between forex pairs; also it doesn’t seem to account for the difference between going long or going short (just bid and ask are changed). Alas, the pip value adjusting calculus was not possible in a normal fashion, requiring workarounds. However, since build 292, about July 10, SYMBOL_TRADE_TICK_VALUE and SYMBOL_TRADE_TICK_SIZE return correct values. Therefore, Virtualization.mqh code will be updated, but only in the download link file.
    Also, a new update will come soon, to allow pending orders.

  2. SystemTrader on May 13, 2011 at 4:19 pm

    This is good, just way more advanced than my knowledge of mql. After three months of thinking about how in the world I could make a simulated account inside an EA, I completed it yesterday. Basically, when a stop is placed, I assign the stop level to a variable, then watch for the trade to be opened. When the fake trade is closed, I add the new simulated balance to an array, which is basically the simulated account. I run a simple MA on the array, and that’s the equity curve. If SimAccountArray[0]<the equity curve, it sets the variable LiveTrade to 0, else it's 1. The only places this LiveTrade variable are checked are when placing an actual trade.

    The "problem" with my approach is that for each EA, accurately following trades in a simulated manner is unique to that EA and takes some time figuring it out.

  3. SystemTrader on May 13, 2011 at 4:32 pm

    Besides trading the equity curve by implementing a demo account via an array, you can also define the trading statistics you found in historical testing, and use them to create a scoring of the system. When the score is low, the system stops live trading, and if the stats “mend,” it will turn on again. So, anytime you don’t like how the system is performing, you’ll know that you’re still trading the same statistics you initially agreed to.