Creating a Multi-Currency Expert Advisor for MetaTrader 5

(This post will be interesting only to the MQL coders.)
Multi-currency expert advisors were possible 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 the MetaTrader 5 allow a wide use of the EAs that can trade in multiple currency pairs simultaneously. In the upcoming ATC2010, the multi-currency EAs will have a huge competitive advantage over single-currency EAs due to the position size limitations. While it’s probably too late to write your own multi-currency EA when there are only 13 days left for the registration, I, nevertheless would like to explain how such an expert advisor can be created and what properties should it posses.
Here’s what a good multi-currency expert advisor should be:

  • Scalable — EA shouldn’t be created to work with 2 or 3 pairs, it should be easily scalable to work with any amount of currency pairs without losses in functionality. And the opposite should also be true — if you need only 2 pairs, it should be easy to decrease the amount of the traded pairs.
  • Flexible — Each parameter of the EA for each pair should be modifiable; the currency pairs themselves should also be changeable. More than that, a good multi-currency EA should work with any asset with any number of digits after the dot.
  • 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 make it possible to upgrade, improve and develop the expert advisor in the future.
  • So, where do we start? It’s good to start with the input parameters. For example, we want an EA with up to 4 currency pairs (which can be increased to any amount 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 amount of periods (determined by the input parameter) before closing. If an opposite signal is generated, a position is closed. If a second signal in the same direction is generated, a position’s duration is reset. No stop-loss or take-profit. Let’s see our input parameters:

    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 lot size
    input double Lots1 = 1;
    input double Lots2 = 1;
    input double Lots3 = 1;
    input double Lots4 = 1;
     
    // Tolerated slippage in pips, pips are fractional
    input int Slippage1 = 50; 	
    input int Slippage2 = 50;
    input int Slippage3 = 50;
    input int Slippage4 = 50;
     
    // Text Strings
    input string OrderComment = "MultiCurrencyExample";

    // 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 lot size
    input double Lots1 = 1;
    input double Lots2 = 1;
    input double Lots3 = 1;
    input double Lots4 = 1;

    // Tolerated slippage in pips, pips are fractional
    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 4 versions (one for each currency pair) and they can be extended by adding 5th, 6th and so on. Empty string for the currency pair means that the particular pair functionality won’t be used at all. So, the EA can even be decreased to being a single-pair.
    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 digits after dot in the quote
            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
    };

    protected:
    string symbol; // Currency pair to trade
    ENUM_TIMEFRAMES timeframe; // Timeframe
    long digits; // Number of digits after dot in the quote
    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;
    }

    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);
    }

    Trade.SetDeviationInPoints(Slippage);

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

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

    Deinitialization simply tells that the currency pair isn’t ready for trading anymore:

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

    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 the 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 what position is currently open
        GetPositionStates();
     
        // PeriodToHold position has passed, it should be close
        if (HoldPeriod == 0)
        {
            if (HaveShortPosition) ClosePrevious(ORDER_TYPE_BUY);
            else if (HaveLongPosition) ClosePrevious(ORDER_TYPE_SELL);
        }
     
        // Checking 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;
        }
    }

    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 what position is currently open
    GetPositionStates();

    // PeriodToHold position has passed, it should be close
    if (HoldPeriod == 0)
    {
    if (HaveShortPosition) ClosePrevious(ORDER_TYPE_BUY);
    else if (HaveLongPosition) ClosePrevious(ORDER_TYPE_SELL);
    }

    // Checking 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 the 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;
    }

    We need to declare our trading objects for each currency pair as the global variables before going to the standard expert advisor functions:

    CMultiCurrencyExample TradeObject1, TradeObject2, TradeObject3, TradeObject4;

    Each currency pair is initialized with its own input parameters and only if it’s set as active (string is not 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);
    }

    return(0);
    }

    Usually big OnTick() is simplified to some basic checks for validation and calls for the main trading functions of our class:

    void OnTick()
    {
        // Is trade allowed?
        if (!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return;
        if (TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == false) return;
     
        // Have the 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();
    }

    // Have the 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.zip.
    This EA can be backtested inside the MT5 Strategy Tester. Unfortunately, as of Build 321, there is a bug in the tester that produces different results depending on the attached currency pair. Forward testing works fine and the results don’t depend on the attached currency pair.
    You can freely use this code to create or modify your own expert advisors for multi-currency trading or anything else.
    Update: The code snippets in this post and in the EA file were updated to comply with the most current (July 7, 2011, MT5 Build 470) MQL standards. There were two changes: Type() methods changed to PositionType() in GetPositionStates() (in accordance with MT5 Build 381 change); Deinit() method of the main trading class was moved from protected to public methods of the class (my fault for making it protected, MetaQuotes’ fault for allowing us to call protected members directly back then).

    If you have any comments or questions on multi-currency expert advisors or their implementation in MetaTrader 5 platform, please, reply using the form below.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    75 − = sixty five