Here is how a good
- 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.
- 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. - 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
// 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
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
If you have any comments or questions on coding a