Developing a trading Expert Advisor from scratch (Part 19): New order system (II)

Daniel Jose | 17 August, 2022

Introduction

In the previous article, Developing a trading Expert Advisor from scratch (Part 18), we implemented some fixes, changes and adjustments in the order system, aiming at creating a system that would allow different trading on NETTING and HEDGING accounts, since there are differences in the account operations. For the NETTING type, the system generates an average price, and you have only one open position for an asset. On HEDGING accounts, you can have multiple open positions, each of which has individual limits. You can buy and sell the same assets at the same time. This can only be done on HEDGING accounts. This is the foundation, based on which options trading can be understood.

But now it is time to finally make the order system completely visual so that we can eliminate the message box and analyze what values are in each position without it. We can do this just by looking at the new order system. This will allow us to adjust several things at a time. Also, we will be able to easily know the profit and loss limits of an OCO position or a pending OCO order, since the EA will display the relevant information in real time, not requiring any extra calculations.

Although this is the first part of the implementation, we are not starting from scratch: we will modify the existing system by adding more objects and events to the chart of the asset we are trading.


1.0. Planning

Planning of the system we are using here is not particularly difficult: we will modify the existing system by changing only the system that represents orders on the chart. This is the main idea which seems quite simple. But practically it requires much creativity, since we are going to manipulate and model data so that the MetaTrader 5 platform does all the hard work for us.

There are several ways to model data, each having its pros and cons.

If you think that this is not easy to implement, take a look at the following code part of the C_HLineTrade class:

inline void SetLineOrder(ulong ticket, double price, eHLineTrade hl, bool select)
{
        string sz0 = def_NameHLineTrade + (string)hl + (string)ticket, sz1;
                                
        ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0);

//... The rest of the code.... 

The highlighted part shows exactly that we can create as many horizontal lines as we want, and they will receive events in a completely independent way. All we need to do is to implement events based on the name that each of the lines will have, since the names will be unique. The MetaTrader 5 platform will take care of the rest. The result will look something like this:


Although this already seems like something ideal, this modeling will not be enough to achieve the result we really need. The idea can be implemented. But the data modeling currently available in the EA is not ideal, because we cannot have an unlimited number of objects based on one name. We need to make some changes that require a fairly deep code modification.

We will now start to implement this new data modeling method, but we will only change what is necessary for this, while maintaining the entire code stable, because it should continue to work as steadily as possible. All work will be performed by the MetaTrader 5 platform, we will only indicate how the platform should understand our modeling.


2.0. Implementation

The first modification is that we change C_HLineTrade into the new C_ObjectsTrade class. This new class will be able to support what we need—a way to link an unlimited number of objects.

Let's start by looking at the original definitions in the following code.

class C_ObjectsTrade
{
//+------------------------------------------------------------------+
#define def_NameObjectsTrade 	"SMD_OT"
#define def_SeparatorInfo       '*'
#define def_IndicatorTicket0    1
//+------------------------------------------------------------------+
        protected:
                enum eIndicatorTrade {IT_NULL, IT_STOP= 65, IT_TAKE, IT_PRICE};
//+------------------------------------------------------------------+

// ... The rest of the class code

Here we have the initial base which we are going to implement. It will be expanded in the future, but for now I want the system to remain stable despite it is being modified and has new data modeling.

Even within the 'protected' declaration, we have the following functions:

inline double GetLimitsTake(void) const { return m_Limits.TakeProfit; }
//+------------------------------------------------------------------+
inline double GetLimitsStop(void) const { return m_Limits.StopLoss; }
//+------------------------------------------------------------------+
inline bool GetLimitsIsBuy(void) const { return m_Limits.IsBuy; }
//+------------------------------------------------------------------+
inline void SetLimits(double take, double stop, bool isbuy)
{
        m_Limits.IsBuy = isbuy;
        m_Limits.TakeProfit = (m_Limits.TakeProfit < 0 ? take : (isbuy ? (m_Limits.TakeProfit > take ? m_Limits.TakeProfit : take) : (take > m_Limits.TakeProfit ? m_Limits.TakeProfit : take)));
        m_Limits.StopLoss = (m_Limits.StopLoss < 0 ? stop : (isbuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop)));
}
//+------------------------------------------------------------------+
inline int GetBaseFinanceLeveRange(void) const { return m_BaseFinance.Leverange; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceIsDayTrade(void) const { return m_BaseFinance.IsDayTrade; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceTakeProfit(void) const { return m_BaseFinance.FinanceTake; }
//+------------------------------------------------------------------+
inline int GetBaseFinanceStopLoss(void) const { return m_BaseFinance.FinanceStop; }

Currently, these functions just serve as a security measure for another scheme that we will implement in the future. Even though we can implement the data and the parsing performed in another location, it is good to leave some things as low in the inheritance chain as possible. Even if the return values will only be used by derived classes, I do not want to allow this directly: I do not want the derived class to access the values that are inside this C_ObjectsTrade object class, because that would break the idea of object class encapsulation, making it difficult for future modifications or bug fixes, if the derived class changes the value of the base class without making the relevant changes through a procedure call.

To minimize call overlap as much as possible, all functions are declared inline: this slightly increases the size of the executable, but results in a more secure system.

Now we come to private declarations.

//+------------------------------------------------------------------+
        private :
                string  m_SelectObj;
                struct st00
                {
                        double  TakeProfit,
                                StopLoss;
                        bool    IsBuy;
                }m_Limits;
                struct st01
                {
                        int     FinanceTake,
                                FinanceStop,
                                Leverange;
                        bool    IsDayTrade;
                }m_BaseFinance;
//+------------------------------------------------------------------+
                string MountName(ulong ticket, eIndicatorTrade it)
                {
                        return StringFormat("%s%c%c%c%d", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket);
                }
//+------------------------------------------------------------------+

The most important part is the highlighted fragment, which will model the names of the objects. I am keeping the basics which are still available in the system. This is because we first create and modify the modeling, keeping the system stable. Then we will add new objects, while this will be done quite easily, quickly. Furthermore, we will maintain the stability already achieved.

Although the code has undergone many more changes than shown here, I will only focus on new functions as well as the changes that were considerable compared to previous codes.

The first function is shown below:

inline string CreateIndicatorTrade(ulong ticket, eIndicatorTrade it, bool select)
{
        string sz0 = MountName(ticket, it);
                                
        ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, 0, 0, 0);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (it == IT_PRICE ? clrBlue : (it == IT_STOP ? clrFireBrick : clrForestGreen)));
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, 1);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, false);
        ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true);
        ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (string)ticket + " "+StringSubstr(EnumToString(it), 3, 10));
                                
        return sz0;
}

For now, it will only create a horizontal line. Pay attention to the name generation code; also note that the colors will now be defined internally by the code and not by the user.

Then we overload the same function, as can be seen below.

inline string CreateIndicatorTrade(ulong ticket, double price, eIndicatorTrade it, bool select)
{
        if (price <= 0)
        {
                RemoveIndicatorTrade(ticket, it);
                return NULL;
        }
        string sz0 = CreateIndicatorTrade(ticket, it, select);
        ObjectMove(Terminal.Get_ID(), sz0, 0, 0, price);
                                
        return sz0;
}

Do not confuse these two functions, because although they appear to be the same, they are actually different. Overload is quite common: we create a simple function and then add new parameters to it to accumulate a certain type of modeling. If we did not implement it via overloading, we would sometimes have to repeat the same code sequence. This is dangerous, because we can forget to declare something. Also, it is not very practical, so we overload the function to make one call instead of several.

One thing that should be mentioned here is the part that is highlighted in this second version. There is no need to create it here, we could do it in another place. But, as can be seen, when we try to create some object with the zero price, in fact it must be destroyed.

To actually see the moment this happens, take a look at the code below:

class C_Router : public C_ObjectsTrade
{

// ... Internal class code ....

                void UpdatePosition(int iAdjust = -1)
                        {

// ... Internal function code ...

                                for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ul = PositionGetInteger(POSITION_TICKET);
                                        m_bContainsPosition = true;
                                        CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);
                                        CreateIndicatorTrade(ul, take = PositionGetDouble(POSITION_TP), IT_TAKE, true);
                                        CreateIndicatorTrade(ul, stop = PositionGetDouble(POSITION_SL), IT_STOP, true);

// ... The rest of the code...

Every time the EA receives the OnTrade event, it will execute the above function and will try to create an indicator on the selected points, but if the user removes the limit, it will become zero. Therefore, when called, it will actually delete the indicator from the chart, saving us from useless objects in memory. Thus we have gain at some points, since the check will be done right at the moment of creation.

But we still have a problem with overloading, because some people may not fully understand how it is used in real code. To understand this, take a look at the two code parts below:

class C_OrderView : public C_Router
{
        private  :
//+------------------------------------------------------------------+
        public   :
//+------------------------------------------------------------------+
                void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1)
                        {                       
                                SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false);
                                CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false);
                        }
//+------------------------------------------------------------------+

// ... Rest of the code...
class C_Router : public C_ObjectsTrade
{

// ... Class code ...

                void UpdatePosition(int iAdjust = -1)
                        {
// ... Function code ....
                                for(int i0 = p; i0 >= 0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
                                {
                                        ul = PositionGetInteger(POSITION_TICKET);
                                        m_bContainsPosition = true;
                                        CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);

// ... The rest of the code...

Note that in both cases we have the same name of the function being used. Also, they are both part of the same C_ObjectsTrade class. However, even in this case the compiler can distinguish between them, which is because of the number of parameters. If you look closely, you will see that the only difference is an additional 'price' parameter, but there may also be some others. As you can see, it is much easier to use one call to copy all the code that is present in one of the overloaded versions, so in the end we have cleaner code which is easier to maintain.

Now let us get back to the C_ObjectsTrade class. The next function we need to understand looks like this:

bool GetInfosOrder(const string &sparam, ulong &ticket, double &price, eIndicatorTrade &it)
{
        string szRet[];
        char szInfo[];
                                
        if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
        if (szRet[0] != def_NameObjectsTrade) return false;
        StringToCharArray(szRet[1], szInfo);
        it = (eIndicatorTrade)szInfo[0];
        ticket = (ulong) StringToInteger(szRet[2]);
        price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE);
                                
        return true;
}

In fact, it is the heart, mind and body of the entire new system. Although it seems quite simple, it does a job that is essential for the entire EA to function as our new modeling system requires it.

Pay close attention to the highlighted code, in particular to the StringSplit function. If it did not exist in MQL5, we would have to code it. Fortunately, MQL5 has it, so we will use this function to the fullest. What it does is decompose the name of the object into the required data. When an object name is created, it is modeled in a very specific way, and due to this we can undo this coding model so StringSplit will undo what the StringFormat function does.

The rest of the function captures the data present in the object name so we can test it and use it later. That is, MetaTrader 5 generates the data for us, we decompose it in order to know what happened and then tell MetaTrader 5 which steps it should take. Our purpose is to make MetaTrader 5 work for us. I do not create a model from scratch; instead, I am modeling the interface and the EA from scratch. Therefore, we should benefit from the support offered by MetaTrader 5 instead of looking for an external solution.

In the code below we will do something very similar to what we did above:

inline void RemoveAllsIndicatorTrade(bool bFull)
{
        string sz0, szRet[];
        int i0 = StringLen(def_NameObjectsTrade);
                                
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        for (int c0 = ObjectsTotal(Terminal.Get_ID(), -1, -1); c0 >= 0; c0--)
        {
                sz0 = ObjectName(Terminal.Get_ID(), c0, -1, -1);
                if (StringSubstr(sz0, 0, i0) == def_NameObjectsTrade)
                {
                        if (!bFull)
                        {
                                StringSplit(sz0, def_SeparatorInfo, szRet);
                                if (StringToInteger(szRet[2]) == def_IndicatorTicket0) continue;
                        }
                }else continue;                                         
                ObjectDelete(Terminal.Get_ID(), sz0);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
}

Every time we remove a line from the chart, whether it is a position that will be closed or a limit level that will be removed, the corresponding object must be removed, just like when the EA is removed from the chart. We need to delete the objects, but we also have a set of lines that should not be deleted unless absolutely necessary: this is Ticket0, it should not be deleted unless extremely necessary. To avoid the deletion, let us use the highlighted code. Without this, we would need to create this Ticket0 anew every time, because this ticket is very important in another code part which we will discuss later.

In all other times we need to delete something specific. For this, we will use another removal function which is shown below.

inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it != NULL) && (it != IT_PRICE))
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, it));
        else
        {
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_PRICE));
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_TAKE));
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_STOP));
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
}

The next new routine can be seen below:

inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
{
        double ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal());
        ObjectMove(Terminal.Get_ID(), MountName(ticket, it), 0, 0, price);
        if (it == IT_PRICE)
        {
                ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_TAKE), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad))));
                ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_STOP), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad)));
        }
}

It will place objects on the price axis. But don't get too attached to it, as it will soon cease to exist for various reasons. Among them is the one which we discussed in another article of this series: Multiple indicators on one chart (Part 05): Converting MetaTrader 5 into RAD(I) system. This article has a table showing objects that can use Cartesian coordinates for positioning, and these coordinates are X and Y. Price and time coordinates, despite being useful in some cases, are not always convenient: when we want to position elements that have to be positioned at certain points on the screen, although it will be faster to develop things using price and time coordinates, they are much more difficult to work with than the X and Y system.

We will make changes next time, while now our purpose is to create an alternative system to the one used so far.

Next, we have the last important function in the C_ObjectsTrade class. It is shown in the following code:

inline double GetDisplacement(const bool IsBuy, const double Vol, eIndicatorTrade it) const
{
        int i0 = (it == IT_TAKE ? m_BaseFinance.FinanceTake : m_BaseFinance.FinanceStop),
            i1 = (it == IT_TAKE ? (IsBuy ? 1 : -1) : (IsBuy ? -1 : 1));
        return (Terminal.AdjustPrice(i0 * (Vol / m_BaseFinance.Leverange) * Terminal.GetAdjustToTrade() / Vol) * i1);
}

This function will make conversion between the values specified in the Chart Trader for a pending order to be placed or a position that will be opened by market.

All these changes have been implemented to transform the C_HLineTrade function into C_ObjectsTrade. However, these changes also required some other changes. For example, the class that also has changed considerably is C_ViewOrder. Some parts of this class simply ceased to exist, because there is no point in their existence, while the remaining functions have been changed. The functions that deserve special attention are highlighted below.

The first one is the function for initializing the data coming from the Chart Trader.

void InitBaseFinance(int nContracts, int FinanceTake, int FinanceStop, bool b1)
{                       
        SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false);
        CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false);
}

The highlighted parts are where Ticket0 is actually created. This ticket is used to place a pending order using the mouse and keyboard: (SHIFT) to buy, (CTRL) to sell. Previously, lines were created at this point, which were then used to indicate where the order would be located. Now things are much simpler: the same as we see an order to be placed, we will also see a pending order or an open position. It means that we will always be checking the system. It is like if you were to assemble a vehicle and all the time you were checking its brakes so that when you actually have to use them you would know how it would behave.

The big problem with a lengthy code is that when we create function, we can only know that it is working at the time it is actually used. But now the system is always checked — even if we do not use all the functions, they are constantly being checked due to the code reuse in different places.

The last routine that I will mention in this article is shown below. It will place a pending order. Note that it has become extremely compact compared to the same function in previous articles.

inline void MoveTo(uint Key)
{
        static double local = 0;
        datetime dt;
        bool    bEClick, bKeyBuy, bKeySell, bCheck;
        double  take = 0, stop = 0, price;
                                
        bEClick  = (Key & 0x01) == 0x01;    //Let mouse button click
        bKeyBuy  = (Key & 0x04) == 0x04;    //Pressed SHIFT
        bKeySell = (Key & 0x08) == 0x08;    //Pressed CTRL  
        Mouse.GetPositionDP(dt, price);
        if (bKeyBuy != bKeySell)
        {
                Mouse.Hide();
                bCheck = CheckLimits(price);
        } else Mouse.Show();
        PositionAxlePrice((bKeyBuy != bKeySell ? price : 0), def_IndicatorTicket0, IT_PRICE, (bCheck ? 0 : GetBaseFinanceTakeProfit()), (bCheck ? 0 : GetBaseFinanceStopLoss()), GetBaseFinanceLeveRange(), bKeyBuy);
        if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, local = price);
        local = (local != price ? 0 : local);
}

The reason is that now there will be a new rule in the system, so the function has "lost some weight" and has become more compact.


Conclusion

I have presented here some changes that will be used in the next article. The purpose of all this is to make them simpler and to show things that can be different at different times. My idea is that everyone follows and learns how to program an EA that will be used to help you with operations, which is why I do not just present a finished and ready-to-use system. I want to show that there are problems to be solved, and to present the path that I took to solve the issues and problems that arise during development. I hope you understand this point. Because if the idea was to create a system and present it in a ready form, I would better do so and sell the idea, but this is not my intention...