(This post will be interesting only to the MQL coders.)
Here’s what a good
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
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
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
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