Creating a Multi-Currency Expert Advisor for MetaTrader 5

Multi-currency expert advisors have been a possibility even with MetaTrader 4, but there were too many problems with building, testing, and using them. The new object-oriented model and improved Strategy Tester of MetaTrader 5 allow a wide use of the EAs that can trade in multiple currency pairs simultaneously. This guide explains how such an expert advisor can be created and what properties should it possess.

Here is how a good multi-currency expert advisor should be:

  1. Scalable — EA shouldn't be created to work with just two or three pairs. It should be easily scalable to work with any number of currency pairs without a loss in functionality. The opposite should also be true — if you need only two currency pairs, it should be easy to reduce the number of the pairs it trades.
  2. Flexible — Each parameter of the EA for each currency pair should be modifiable; currency pairs themselves should also be changeable. More than that, a good multi-currency EA should work with any asset with any number of decimal places and other parameters.
  3. Properly encapsulated — Separating the trading logic from the common functions of the EA and the input parameters from the normal properties should be done to let you easily upgrade, improve, and develop the expert advisor in the future.

So, where shall one start? It is good to start with the input parameters. For example, you want an EA with up to four currency pairs (which can be increased to any number easily) that will trade a very simple (and rather stupid) strategy — go long when the previous bar closed positively and go short when the bar closed negatively. Positions are held open for a certain number of periods (determined by the input parameter) before closing. If an opposite signal is generated, the position is closed. If a second signal in the same direction is generated, the position's duration is reset. No stop-loss or take-profit is used. Let's see the input parameters:

// Trading instruments:
input string CurrencyPair1 = "EURUSD";
input string CurrencyPair2 = "GBPUSD";
input string CurrencyPair3 = "USDJPY";
input string CurrencyPair4 = "";

// Timeframes:
input ENUM_TIMEFRAMES TimeFrame1 = PERIOD_M15;
input ENUM_TIMEFRAMES TimeFrame2 = PERIOD_M30;
input ENUM_TIMEFRAMES TimeFrame3 = PERIOD_H1;
input ENUM_TIMEFRAMES TimeFrame4 = PERIOD_M1;

// Period to hold the position open:
input int PeriodToHold1 = 1;
input int PeriodToHold2 = 2;
input int PeriodToHold3 = 3;
input int PeriodToHold4 = 4;

// Basic position size:
input double Lots1 = 1;
input double Lots2 = 1;
input double Lots3 = 1;
input double Lots4 = 1;

// Tolerated slippage in points:
input int Slippage1 = 50;       
input int Slippage2 = 50;
input int Slippage3 = 50;
input int Slippage4 = 50;

// Text strings:
input string OrderComment = "MultiCurrencyExample";

As you see, each parameter has four versions (one for each currency pair) and they can be extended by adding the fifth, sixth, and so on. Empty string for the currency pair means that the particular pair's functionality won't be used at all. So, you can even downgrade the EA to a single-pair version.

The next step is the class definition:

class CMultiCurrencyExample
{
    private:
        bool HaveLongPosition;
        bool HaveShortPosition;
        int LastBars;
        int HoldPeriod;
        int PeriodToHold;
        bool Initialized;
        void GetPositionStates();
        void ClosePrevious(ENUM_ORDER_TYPE order_direction);
        void OpenPosition(ENUM_ORDER_TYPE order_direction);

    protected:
                string            symbol;                    // Currency pair to trade.
                ENUM_TIMEFRAMES   timeframe;                 // Timeframe.
                long              digits;                    // Number of decimal places.
                double            lots;                      // Position size.
                CTrade            Trade;                     // Trading object.
                CPositionInfo     PositionInfo;              // Position Info object.

    public:
                                  CMultiCurrencyExample();               // Constructor.
                                 ~CMultiCurrencyExample() { Deinit(); }  // Destructor.
                bool              Init(string Pair, 
                                       ENUM_TIMEFRAMES Timeframe, 
                                       int PerTH,
                                       double PositionSize,
                                       int Slippage);
                void              Deinit();
                bool              Validated();
                void              CheckEntry();                        // Main trading function.
};

Variables and functions that are natural to this particular EA are declared as private — they won't be inherited by the derivative classes in the future. Functions and variables that can be used in almost any EA are declared as protected. Functions that will be used outside the class should be declared as public.

The class destructor is defined inside the declaration and calls a deinitialization function. The constructor is very simple, it just sets the flag that the currency pair hasn't been initialized yet so, that the EA doesn't trade it before initialization:

CMultiCurrencyExample::CMultiCurrencyExample()
{
    Initialized = false;
}

The Init() method initializes a pair so it can be used in trading. The input parameters are transferred as the arguments of the function and are stored inside the class's properties:

bool CMultiCurrencyExample::Init(string Pair, 
                                 ENUM_TIMEFRAMES Timeframe, 
                                 int PerTH,
                                 double PositionSize,
                                 int Slippage)
{
    symbol = Pair;
    timeframe = Timeframe;
    digits = SymbolInfoInteger(symbol, SYMBOL_DIGITS);
    lots = PositionSize;

    Trade.SetDeviationInPoints(Slippage);

    PeriodToHold = PerTH;
    HoldPeriod = 0;
    LastBars = 0;

    Initialized = true;
    Print(symbol, " initialized.");
    return(true);
}

The deinitialization simply tells that the currency pair isn't ready for trading anymore:

CMultiCurrencyExample::Deinit()
{
    Initialized = false;
    Print(symbol, " deinitialized.");
}

The Validated() method returns the current initialization state:

bool CMultiCurrencyExample::Validated()
{
    return (Initialized);
}

CheckEntry() is the main trading function of the pair. It checks if the new bar has arrived, decrements the position holding counter, monitors open positions, closes outdated positions, opens new positions, closes the old ones on the signal, and resets the position period counter if needed.

void CMultiCurrencyExample::CheckEntry()
{
    // Trade on new bars only.
    if (LastBars != Bars(symbol, timeframe)) LastBars = Bars(symbol, timeframe);
    else return;

    MqlRates rates[];
    ArraySetAsSeries(rates, true);
    int copied = CopyRates(symbol, timeframe, 1, 1, rates);
    if (copied <= 0) Print("Error copying price data: ", GetLastError());

        // Period counter for open positions.
   if (HoldPeriod > 0) HoldPeriod--;
        
        // Check which position is currently open.
        GetPositionStates();

        // PeriodToHold position has passed, it should be closed.
        if (HoldPeriod == 0)
        {
        if (HaveShortPosition) ClosePrevious(ORDER_TYPE_BUY);
      else if (HaveLongPosition) ClosePrevious(ORDER_TYPE_SELL);
   }
   
        // Checking the previous candle.
        if (rates[0].close > rates[0].open) // Bullish.
        {
                if (HaveShortPosition) ClosePrevious(ORDER_TYPE_BUY);
                if (!HaveLongPosition) OpenPosition(ORDER_TYPE_BUY);
                else HoldPeriod = PeriodToHold;
        }
        else if (rates[0].close < rates[0].open) // Bearish.
        {
                if (HaveLongPosition) ClosePrevious(ORDER_TYPE_SELL);
                if (!HaveShortPosition) OpenPosition(ORDER_TYPE_SELL);
                else HoldPeriod = PeriodToHold;
        }
}

A simple function that detects the current position states:

void CMultiCurrencyExample::GetPositionStates()
{
    // Is there a position on this currency pair?
    if (PositionInfo.Select(symbol))
    {
        if (PositionInfo.PositionType() == POSITION_TYPE_BUY)
        {
            HaveLongPosition = true;
            HaveShortPosition = false;
        }
        else if (PositionInfo.PositionType() == POSITION_TYPE_SELL)
        { 
            HaveLongPosition = false;
            HaveShortPosition = true;
        }
    }
    else
    {
        HaveLongPosition = false;
        HaveShortPosition = false;
    }
}

A basic method for closing a position:

void CMultiCurrencyExample::ClosePrevious(ENUM_ORDER_TYPE order_direction)
{
    if (PositionInfo.Select(symbol))
    {
        double Price;
        if (order_direction == ORDER_TYPE_BUY) Price = SymbolInfoDouble(symbol, SYMBOL_ASK);
        else
            if (order_direction == ORDER_TYPE_SELL) Price = SymbolInfoDouble(symbol, SYMBOL_BID);
        Trade.PositionOpen(symbol, order_direction, lots, Price, 0, 0, OrderComment + symbol);
        if ((Trade.ResultRetcode() != 10008) &&
            (Trade.ResultRetcode() != 10009) &&
            (Trade.ResultRetcode() != 10010))
            Print("Position Close Return Code: ", Trade.ResultRetcodeDescription());
        else
        {
            HaveLongPosition = false;
            HaveShortPosition = false;
            HoldPeriod = 0;
        }
    }
}

The same for opening:

void CMultiCurrencyExample::OpenPosition(ENUM_ORDER_TYPE order_direction)
{
    double Price;
    if (order_direction == ORDER_TYPE_BUY) Price = SymbolInfoDouble(symbol, SYMBOL_ASK);
    else if (order_direction == ORDER_TYPE_SELL) Price = SymbolInfoDouble(symbol, SYMBOL_BID);
    Trade.PositionOpen(symbol, order_direction, lots, Price, 0, 0, OrderComment + symbol);
    if ((Trade.ResultRetcode() != 10008) && 
        (Trade.ResultRetcode() != 10009) && 
        (Trade.ResultRetcode() != 10010))
        Print("Position Open Return Code: ", Trade.ResultRetcodeDescription());
    else
        HoldPeriod = PeriodToHold;
}

Trading objects have to be declared for each currency pair as global scope variables before proceeding to standard expert advisor functions:

CMultiCurrencyExample TradeObject1, TradeObject2, TradeObject3, TradeObject4;

Each currency pair is initialized with its own input parameters and only if it is set as active (the input parameter string isn't empty):

int OnInit()
{
    // Initialize all objects.
    if (CurrencyPair1 != "")
        if (!TradeObject1.Init(CurrencyPair1, TimeFrame1, PeriodToHold1, Lots1, Slippage1))
        {
            TradeObject1.Deinit();
            return(-1);
        }
    if (CurrencyPair2 != "")
        if (!TradeObject2.Init(CurrencyPair2, TimeFrame2, PeriodToHold2, Lots2, Slippage2))
        {
            TradeObject2.Deinit();
            return(-1);
        }
    if (CurrencyPair3 != "")
        if (!TradeObject3.Init(CurrencyPair3, TimeFrame3, PeriodToHold3, Lots3, Slippage3))
        {
            TradeObject3.Deinit();
            return(-1);
        }
    if (CurrencyPair4 != "")
        if (!TradeObject4.Init(CurrencyPair4, TimeFrame4, PeriodToHold4, Lots4, Slippage4))
        {
            TradeObject4.Deinit();
            return(-1);
        }
        return(0);
}

A normally large OnTick() function's body is simplified to some basic checks for validation and calls for the main trading functions of the class:

void OnTick()
{
    // Is trade allowed?
    if (!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return;
    if (TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == false) return;

    // Trade objects initialized?
    if ((CurrencyPair1 != "") && (!TradeObject1.Validated())) return;
    if ((CurrencyPair2 != "") && (!TradeObject2.Validated())) return;
    if ((CurrencyPair3 != "") && (!TradeObject3.Validated())) return;
    if ((CurrencyPair4 != "") && (!TradeObject4.Validated())) return;

    if (CurrencyPair1 != "") TradeObject1.CheckEntry();
    if (CurrencyPair2 != "") TradeObject2.CheckEntry();
    if (CurrencyPair3 != "") TradeObject3.CheckEntry();
    if (CurrencyPair4 != "") TradeObject4.CheckEntry();
}

You can download the full code of this EA here: MultiCurrencyExample.mq5.

This EA can be backtested inside the MT5 Strategy Tester.

You can freely use this code to create or modify your own expert advisors for multi-currency trading or anything else.

If you have any comments or questions on coding a multi-currency expert advisor in MetaTrader 5 platform, you are welcome on our expert advisors forum for discussion and help.


If you want to get news of the most recent updates to our guides or anything else related to Forex trading, you can subscribe to our monthly newsletter.

© 2005–2021

EarnForex.com

Design — Mart Studio

Forex trading bears intrinsic risks of loss. You must understand that Forex trading, while potentially profitable, can make you lose your money. Never trade with the money that you cannot afford to lose! Trading with leverage can wipe your account even faster.

CFDs are leveraged products and as such loses may be more than the initial invested capital. Trading in CFDs carry a high level of risk thus may not be appropriate for all investors.