How to build an MT4 Expert Advisor for Forex + Crypto (Beginner tutorial + Script!)
Hi! As many of you may be familiar with Metatrader 4 and Forex but are unsure how to get started, I decided to write a tutorial as well as introduce a script that implements a basic trading strategy. I will include lots of relevant details and screenshots, so find a comfy seat, have some coffee if you like and let's get started!
Introduction to MT4 and Expert Advisors
MetaTrader 4 is a popular trading application offered by many Forex brokers that allows one to trade the markets as well as access advanced charting features and technical analysis tools, including many indicators. Even more interesting is the ability to use algorithmic trade bots, for when you want to be on "auto-pilot" or make complex trade decisions on the fly. It uses its own language, the MQL4 script language, which is based on the C/C++ family of languages. If you have a C family background, such as C,C#, or even Javascript, the features of MQL4 should feel familiar to you. If you don't have much programming experience, there are many great beginner tutorials elsewhere that can teach you the basics. Feel free to brush up if you need! This tutorial will assume some programming experience, so if an area feels unfamiliar to you, you may study the particular language construct and come back when ready. Now on to the structure of an MT4 program!
Program Structure
An MT4 script is built around the concept of event handlers, similar to those in JavaScript, which are called when important trading events occur. For expert advisors, this is limited to three key event handlers, which form the life cycle of the Expert Advisor. They are:
- OnInit() - Fired when a script is first initialized
- OnTick() - Fired when the particular chart the EA is attached to encounters a new tick/update
- OnDeinit() - Fired before the EA terminates and is used to clean up any resources you may have used during EA operations.
With these components, you may create an empty shell EA, which looks like this:
int OnInit() {
}
void OnDeinit(const int reason) {
}
void OnTick() {
}
Here you can see the three basic handlers, and the code that you write (besides global variables & your own functions) will be contained in the '{' and '}' brackets which delimit the start and end of each event handler! With this let's move on to global variables, and the basic parameters most expert advisors share. Then we will proceed to create some helper functions, and devise our trading strategy.
Global variables and basic parameters
At the header of general expert advisor templates, you will find some variables which control how your EA will trade.
In programming, a variable is simply a place to store information, be it a number, text, or whatever.
In MQL4, variables are typed, meaning you must first declare what they will store. A list of basic data types in MQL4 can be found here: https://docs.mql4.com/basis/types
In EAs, the most important types to master are the numerical types. These are int
for whole numbers (1,2,3,4) and double
for decimal numbers requiring precision, such as Forex price data. For now we will be looking at the default variables declared at the beginning of a basic MQL4 script. These can be accessed anywhere else in the program, because they are outside of any containing function's brackets '{' and '}'. These are as follows:
//External Variables
extern double FixedLotSize = 0.01;
extern double StopLoss = 50;
extern double TakeProfit = 100;
It is a good idea to use these variables in your own Expert Advisors, and I will break down what they mean, and how they are used by the EA. First, you will notice the extern
keyword. This means that the value can be modified in the MetaTrader 4 GUI, before you launch, and when you double click on the icon of the running EA.
Next, you will see that the data type is double
, it means that it is a decimal number, and it is a good rule of thumb to represent price data as double, as that is the type MT4 will feed into your EA for communicating exchange rates. Following the type is the variable name, which is the word you will use to identify & access the variable throughout your code. Here the three variables are named FixedLotSize
, StopLoss
, and TakeProfit
. I will now explain the purpose of these variables and how they modify the behavior of the trading strategy you will be building.
The FixedLotSize
variable represents the size of the "lots" that your EA will open position on when trading, in Forex parlance. This EA is set to open mini lots "0.01", keep in mind that not all brokers allow lots of this size, so check with yours for support. Also note that the size of a lot you can open is dependent on the equity that you have and your margin, since it represents the units of currency being traded in a single position, so keep that in mind when changing this parameter.
Next are the StopLoss
and TakeProfit
. They are used to indicate (in pips) the stop loss and take profit to be set on an order opened by the EA. This is the dip below the current rate that an order should be closed if it is losing money (in the case of StopLoss, or gaining money, in the case of TakeProfit
. Ensure that these are set to reasonable values for your needs and profit maximization. Now that we covered the basics, let's devise a simple strategy.
The Strategy
If you have knowledge of Technical Analysis, you are sure to be familiar with the tried and true candlestick chart.
We are going to use this guide as a visual metaphor for developing our trading strategy.
Notice that in this chart, white bars represent market up movements, and black bars represent a down movement where the closing price of the period was lower than the opening price. What we are going to do is represent the ratio of black bars and white bars from the past over a period of time we will call the "window". This will give us insight into the current market conditions and enable us to act on those conditions by placing orders if the price moves according to our criterion.
You can conceptualize the window and the information we want to glean in this graphic:
Once we have the count of white bars to black bars, we want to decide how we will enter and exit the market based on the following decision table:
Market Trend | Action If Price Rises | Action If Price Dips |
---|---|---|
Mostly White (Uptrend) | Sell order is placed | Buy order is placed |
Mostly Black (Downtrend) | Buy order is placed | Sell order is placed |
Let's add some new parameters to our EA in preparation of this strategy.
extern int Window = 8;
extern int MaxOrders = 5;
extern long Interval = 300;
First, we have a new integer variable, Window
. This represents the number of bars the EA will consider as described above. Increasing it will cause the EA to consider a larger period of time when determining the trend.
Next, we have the integer variable , MaxOrders
. This represents the maximum number of orders our EA will be allowed to open. Set this one according to your equity and risk tolerance. Generally, increasing it will make the EA trade more actively.
Finally, there is the long
integer, Interval
. This represents the time in seconds between trade decisions this EA will make. Generally you will want it to match the time period of the chart it is attached to, but you may let it vary. For instance, you may want the bot to make open positions every 5 minutes whilst you are viewing an hourly chart. Lower values may result in more trading activity.
Now that you understand how each of these new parameters relate to our overall trade plan, we will add some new global variables and our own helper functions to make implementation easier. Afterwards, we are on to coding our OnTick
handler where all the action will happen. Pay close attention to this section as several new concepts will be introduced.
Our helper functions and OnTick handler
In this section, we will introduce several new variables that implement our trading criterion and introduce some new constructs of the MQL4 language, including how to define your own functions. Let's start with some variables that indicate the price changes our EA will require to open and close trades, respectively. We are calling them Delta
and Threshold
.
//Changes (in pips) required for order open and closing:
extern int Delta = 100;
extern int Threshold = 200;
The Delta
variable represents the change in pips from the previous bar at which we wish to enter the market, and the Threshold
variable represents the change in pips at which we will close out a profitable trade. Try setting these to ideal values for your currency symbol and timeframe. Next, we need a way to store our open orders so the EA can keep track of them, and close them out as necessary. To hold this information, we will use two new MQL4 features, structs and arrays. We will also learn about enumerations, used to represent a finite set of options, which we will use to represent whether a particular order is a buy order or a sell order. Let's begin.
int OrderPos = 0;
enum direction {BUY,SELL};
struct OrderStruct {
int ticket_num;
double price;
direction trade_dir;
};
OrderStruct OrderQueue[];
MqlRates bars[];
direction CurrentDirection;
Here we have one new global integer variable, OrderPos
, it is going to be used in OnTick
to keep track of our current position in the order queue. Next we introduce an enumeration, direction
. Enumerations allow you to define your own types which represent a set of options. We will use this enumeration later in our OrderStruct
structure, and it will represent whether the order placed was a buy or sell order. Next we have our structure, OrderStruct
. A structure also lets you define a type, but it is a special kind of type that stores information in the form of other variables. You can think of a structure as a way of storing related information. Here we will be storing three points of information: the ticket number, which comes from the MQL4 API when a ticket is opened, the price at which our order was opened, and then our direction type, in the variable trade_dir
, where we track whether we opened a buy or sell order. One of these will be created for every trade the bot opens, and stored in our next MQL4 concept, the array. Arrays are simply lists of related items of the same type and are declared by putting [] brackets after the variable name like so:
OrderStruct OrderQueue[];
This creates an array of OrderStruct structures called OrderQueue. This is where we will store details of all of the trades our EA is allowed to make.
Finally we have declared one more global array, bars
, which is of type MqlRates
. This is a built-in structure that allows us to receive Open,High,Low,Close, and Volume historical data from the MQL4 API. We will use this extensively in the OnTick
function. Let us now discuss our helper functions. We will start by explaining what it means to define a function.
Defining functions
You have seen the builtin functions, such as OnInit
and OnTick
above. Like C, you can also define your own functions. You may think of a function as a named chunk of code serving a specific purpose. These functions can then be called anywhere in your code where you need to tap into their functionality. Let's create the ones we need.
Helpers 1 & 2 - Opening & Closing Trades
MQL4 has an excellent API for interacting with the trading engine. It includes two key functions, OrderSend
and OrderClose
. We will be creating three helpers centered around this API, one for when we wish to open a buy order, another
for when we wish to open a sell order, and finally one for closing profitable trades. Let's take a look:
int OpenBuyOrder(double LotSize, double sl, double tp) {
int Ticket = OrderSend(Symbol(), OP_BUY,LotSize,Ask,Slippage,StopLoss,tp,"CoinFlip",1337,0,clrViolet);
return Ticket;
}
int OpenSellOrder(double LotSize, double sl, double tp) {
int Ticket = OrderSend(Symbol(), OP_SELL,LotSize,Bid,Slippage,sl,tp,"CoinFlip",1337,0,clrBlueViolet);
return Ticket;
}
void CloseOrder(int index, double price) {
if (OrderQueue[index].ticket_num != 0) {
int success = OrderClose(OrderQueue[index].ticket_num,FixedLotSize,price,Slippage);
}
}
You may notice that the OrderSend
API includes several options, including a number for identifying our EA, a name - "CoinFlip", and the color we wish to place symbols on the chart when the EA makes the trade. For brevity, I won't cover all of these, but you can find an explanation of the trading API here:
https://docs.mql4.com/trading
Also, each of our order opening functions, OpenBuyOrder
and OpenSellOrder
return an integer, Ticket
, which we will
remember in our OrderStruct
structure that we created earlier. This will happen in the OnTick()
handler that will be introduced soon.
Finally, we have our CloseOrder
function which takes two parameters. The first is an integer index
which is the index of the order in our OrderQueue
array we created. The second is a double price
which is the price our EA has decided to close the order at. We will also decide the criterion for order closing in the OnTick()
handler coming up. Next, we will consider a simple utility function for determining the precision we will need when calculating the pip movements in market prices.
Helper 3 - Pip points
Since brokers may provide quotes for currency pairs at variable decimal places, we need a way to determine the smallest point to use in our calculations. This handy helper function does just that:
double PipPoint() {
double up = 0.0;
if (Digits == 2 || Digits == 3) up = 0.01;
else if (Digits == 4 || Digits == 5) up = 0.0001;
return up;
}
double UsePoint = PipPoint();
Here we define the PipPoint()
function that determines the smallest pip point for the pair the EA is attached to. We then create a new global by calling the function, UsePoint
, that we can refer to throughout our script when we need to make price calculations. Let us now introduce a helper for fetching the market historical data.
Helper 4 - Get the current window of bars
MQL4 has an API for getting past open, high, low, close and volume historical data. We want to store this in the bars
global
array we mentioned earlier so it will be available to us in our OnTick()
handler. To do this we will introduce a GetWindow
helper function which uses the CopyRates
API and fills in our bars
array. We then return the resultant success or failure code, that will let us know if the server has sufficient data to create our window. You can read more about CopyRates
here:
https://docs.mql4.com/series/copyrates
Here is our code listing for the GetWindow
function:
int GetWindow() {
int success = CopyRates(Symbol(),Period(),0,Window,bars);
return success;
}
Notice that we first get the current currency symbol and time frame the EA is attached to using the built-in Symbol()
and Period()
functions. Then we use our global Window
parameter, a key aspect of our strategy, to get the time span we are interested in. Then the line return success;
simply returns our status code, so we can check later if we have all the data we need. It is now finally time to implement the heart of our trading strategy, the OnTick()
handler. Fasten your seat belts!
OnTick()
Before we introduce our OnTick
function, we must make one last change to our source code to initialize our OrderQueue
array so it is ready to hold the orders we will be opening. We will do this in the OnInit
handler described in the introduction. This is what it will look like now
int OnInit()
{
OrderPos = 0;
ArrayResize(OrderQueue,MaxOrders);
for (int i = 0; i < MaxOrders - 1; i++) {
OrderQueue[i].ticket_num = 0;
}
return(INIT_SUCCEEDED);
}
Here we use the MQL4 ArrayResize
function to size our OrderQueue
array to the maximum number of orders our EA will create. Then we loop through and set a clean ticket number for each one so the EA will know the slots are unused. Finally, we return INIT_SUCCEEDED
so we know the initialization was a success.
Now, we are going to discuss the heart of our trading engine, the OnTick()
handler. This will be a large code listing, and finer points will be discussed below:
void OnTick()
{
static long tickCount;
if(OrderPos == MaxOrders)
OrderPos = 0;
int blackCount = 0;
int whiteCount = 0;
double oldAsk;
double oldBid;
static bool runCount = true;
int success = GetWindow();
if (success == -1) {
Print("Failure");
}
oldAsk = bars[1].high;
oldBid = bars[1].low;
for (int i = 1; i < Window - 1; i++) {
double o = bars[i].open;
double c = bars[i].close;
if ( o > c) {
blackCount++;
} else if (c > o) {
whiteCount++;
}
}
if (OrderQueue[OrderPos].ticket_num == 0 && (tickCount % Interval) == 0) {
//Decide if order should be opened:
double DiffPips;
if (blackCount > whiteCount) {
//Sells at rise
Print("Mostly Black..Will sell at rise greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((Bid - oldBid)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenSellOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Ask;
OrderQueue[OrderPos].trade_dir = SELL;
OrderPos++;
} else {
Print("Mostly Black..Will buy at dip greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((oldAsk - Ask)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenBuyOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Bid;
OrderQueue[OrderPos].trade_dir = BUY;
OrderPos++;
}
}
} else if (whiteCount > blackCount) {
//Buys at dip
Print("Mostly White..Will buy at dip greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((oldAsk - Ask)/UsePoint,Digits));
if (DiffPips >= Delta ) {
OrderQueue[OrderPos].ticket_num = OpenBuyOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Bid;
OrderQueue[OrderPos].trade_dir = BUY;
OrderPos++;
} else {
Print("Mostly White..Will sell at rise greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((Bid - oldBid)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenSellOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Ask;
OrderQueue[OrderPos].trade_dir = SELL;
OrderPos++;
}
}
}
};
tickCount++;
for (int OrderIndex = 0; OrderIndex < MaxOrders - 1; OrderIndex++) {
//Decide if order should be closed:
double DiffPips;
double orderPrice = OrderQueue[OrderIndex].price;
if (OrderQueue[OrderIndex].ticket_num != 0) {
if (OrderQueue[OrderIndex].trade_dir == BUY) {
DiffPips = NormalizeDouble((Bid - orderPrice)/UsePoint,Digits);
if (DiffPips < 0)
return;
if (DiffPips >= Threshold) {
CloseOrder(OrderPos,Bid);
OrderQueue[OrderIndex].ticket_num = 0;
}
} else if (OrderQueue[OrderIndex].trade_dir == SELL) {
DiffPips = NormalizeDouble((orderPrice - Ask)/UsePoint,Digits); //MathAbs((NormalizeDouble(((orderPrice - Ask)/MarketInfo(Symbol(),MODE_POINT)),MarketInfo(Symbol(),MODE_DIGITS)))/UsePoint);
if (DiffPips >= Threshold) {
CloseOrder(OrderPos,Ask);
OrderQueue[OrderIndex].ticket_num = 0;
}
}
}
}
}
Let's start with our variables. We have a set of integers for counting the number of "black" and "white" bars in our window, and two doubles for storing the price of the previous bar on the chart. They are:
int blackCount = 0;
int whiteCount = 0;
double oldAsk;
double oldBid;
We then set the oldAsk
and oldBid
variables to the last session's high and low, respectively. After that, we then get our window, check for success, and set up a loop that tallies those bars that are black and those that are white. This looks like:
int success = GetWindow();
if (success == -1) {
Print("Failure");
}
oldAsk = bars[1].high;
oldBid = bars[1].low;
for (int i = 1; i < Window - 1; i++) {
double o = bars[i].open;
double c = bars[i].close;
if ( o > c) {
blackCount++;
} else if (c > o) {
whiteCount++;
}
}
Now that we have this tally, we can make our trading decisions as described in the Strategy section. We use a modulus and a tick counter to limit our trading activity to the defined interval we set in our Interval
parameter. We also make sure the order ticket is clear and free for us to use. The if statement looks like this:
if (OrderQueue[OrderPos].ticket_num == 0 && (tickCount % Interval) == 0) {
//Trade decisions as described in strategy section
}
Inside this statement will be our interaction with the trade system using the helper functions we described, and implementing the decisions of the strategy we described. Let's briefly describe these and then go over our order closing engine that closes out profitable positions once criteria have been met.
Refer back to the decision table in the Strategy section. We had:
Market Trend | Action If Price Rises | Action If Price Dips |
---|---|---|
Mostly White (Uptrend) | Sell order is placed | Buy order is placed |
Mostly Black (Downtrend) | Buy order is placed | Sell order is placed |
Now, let's see how this looks in code. We are going to use our UsePoint
variable to calculate the difference of the last price and new price in pips, as well as implement a different strategy if there are more white bars (up market), and the opposing strategy if there are more black bars (down market). To do this we use several nested if statements and it looks like this:
if (blackCount > whiteCount) {
//Sells at rise
Print("Mostly Black..Will sell at rise greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((Bid - oldBid)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenSellOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Ask;
OrderQueue[OrderPos].trade_dir = SELL;
OrderPos++;
} else {
Print("Mostly Black..Will buy at dip greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((oldAsk - Ask)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenBuyOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Bid;
OrderQueue[OrderPos].trade_dir = BUY;
OrderPos++;
}
}
} else if (whiteCount > blackCount) {
//Buys at dip
Print("Mostly White..Will buy at dip greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((oldAsk - Ask)/UsePoint,Digits));
if (DiffPips >= Delta ) {
OrderQueue[OrderPos].ticket_num = OpenBuyOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Bid;
OrderQueue[OrderPos].trade_dir = BUY;
OrderPos++;
} else {
Print("Mostly White..Will sell at rise greater than: ", Delta);
DiffPips = MathAbs(NormalizeDouble((Bid - oldBid)/UsePoint,Digits));
if (DiffPips >= Delta) {
OrderQueue[OrderPos].ticket_num = OpenSellOrder(FixedLotSize,CalculateStopLoss(),0);
OrderQueue[OrderPos].price = Ask;
OrderQueue[OrderPos].trade_dir = SELL;
OrderPos++;
}
}
}
Studying the code, you will see that if the pip difference in price meets or exceeds our Delta
, the appropriate market position will be placed. Let us now briefly discuss order closing.
As a last step, at every tick the EA checks to see if any of the positions opened thus far should be closed.
It does this by seeing if the price at current market rates has moved a certain threshold past the price at which the order was placed. If so, it promptly closes the position. Let's see this in code:
for (int OrderIndex = 0; OrderIndex < MaxOrders - 1; OrderIndex++) {
//Decide if order should be closed:
double DiffPips;
double orderPrice = OrderQueue[OrderIndex].price;
if (OrderQueue[OrderIndex].ticket_num != 0) {
Print("Current Order != 0");
if (OrderQueue[OrderIndex].trade_dir == BUY) {
DiffPips = NormalizeDouble((Bid - orderPrice)/UsePoint,Digits);
if (DiffPips < 0)
return;
if (DiffPips >= Threshold) {
CloseOrder(OrderPos,Bid);
OrderQueue[OrderIndex].ticket_num = 0;
}
} else if (OrderQueue[OrderIndex].trade_dir == SELL) {
DiffPips = NormalizeDouble((orderPrice - Ask)/UsePoint,Digits); //MathAbs((NormalizeDouble(((orderPrice - Ask)/MarketInfo(Symbol(),MODE_POINT)),MarketInfo(Symbol(),MODE_DIGITS)))/UsePoint);
if (DiffPips >= Threshold) {
CloseOrder(OrderPos,Ask);
OrderQueue[OrderIndex].ticket_num = 0;
}
}
That's a wrap! With a complete OnTick()
handler, you now have a powerful trading system. Get out and earn those pips!
Conclusion
Phew! We have covered a lot of ground, and the basics of how trading strategies are built. I hope you have learned a lot from this tutorial and that it has inspired you to build your own trading strategies. If any of this is confusing to you, fret not! Your skill level will grow with practice, and programming Expert Advisors will surely give you a fresh perspective during your manual day trading as well. I am offering the full source code to the expert advisor discussed here to all who upvote and leave their email or contact info in the comments section. If this tutorial was useful to you, feel free to re-steem and share with others! Thanks to all and I look forward to providing more tutorials and strategies in the future.
"Any sufficiently advanced technology is indistinguishable from magic."
-Arthur C. Clarke
Thanks a ton for this! Wish you got more of a payout.