A Simple Profitable EA – an MQL4 programming tutorial
18 minute read
In this article, we’ll program a simple profitable MT4 EA based on a set-and-forget strategy.
This tutorial builds on the foundations from the article How to code an EA, where we learnt how to program an expert advisor (i.e. a trading robot) by thinking at a high level.
We’ll use those foundations to develop a full EA with fully working code. Along the way, we’ll reinforce the top down approach to development, which keeps code (and your head-space) clear and scalable.
Define the strategy
The first step in development is to specify the trading steps that the EA will automate in natural language (for example, in English).
This is because software development begins from what a human needs, and humans speak natural language. From these initial human needs, we write code to tell a computer what to do, because computers can’t comprehend subtle meanings in natural language like humans can. So, code can be thought of as a bridge between natural language and the computer’s CPU.
This is also why we constantly reinforce the idea that the better your code can be abstracted to reflect natural language, the better you’ll have bridged the gap between human and computer. Your code will be easier for you to learn, manage and scale.
This initial specification step doesn’t contain fun coding, but it’s one of the most important parts of software development from a business solution engineer’s perspective. That’s because without knowing the high level human need that we’re trying to solve, any coding is completely pointless.
We’ll keep the specifications simple in this article, as follows: our EA shall automate a set-and-forget strategy, similar to how someone who invests in a stock would buy some shares, then wait until it has increased in value, then sell it off. These are how the trading steps are specified:
- If the MACD indicator is above 0, open a Buy position. If the MACD indicator is below 0, open a Sell position. Keep only 1 open position at a time. This is the “set” part of “set-and-forget”.
- Wait until any time in the future. This is the “forget” part of “set-and-forget”. If we find that we’re in profit by any amount, close the position – but only if the MACD indicator is below 0 (if we have a Buy position) or above 0 (if we have a Sell position).
Notice that the above trading steps are written in a very human-like step-by-step way, matching what a trader would do in real time. This way of specifying the EA is fitting, because this EA is a program that automates what a trader does, except faster.
Now, with reference to these trading steps, let’s move on to code it.
Model a trader’s high level behavior
A trader’s high level behavior is to make an entry, modify the position if required during its lifetime, then exit. This cycle repeats itself over and over again. Therefore, begin with a code structure that mimics this behavior.
You can find this foundational code structure from the article How to code an EA. We’ll begin with this and flesh it out. Here’s a reminder of the code structure we’re starting with:
// Infrastructure (changes sometimes): describes high-level trader behavior void OnTick(void) {...} void CheckAndDoExits(void) {...} void CheckAndDoModifies(void) {...} void CheckAndDoEntries(void) {...} void Exit(const bool buyorsell) {...} void Entry(const bool buyorsell) {...} // Infrastructure (never changes, only improves): utilities int OpenOrder(const bool buyorsell) {...} bool CloseOrders(const bool buyorsell) {...} bool CloseOrder(const int ticket) {...} // Infrastructure (changes slightly): models the decision-making process bool Conditions_Entry(const bool buyorsell) {...} bool Conditions_Exit(const bool buyorsell) {...} // Changes often: strategy-specific rules bool Condition_1_Entry(const bool buyorsell) {...} bool Condition_2_Entry(const bool buyorsell) {...} bool Condition_3_Entry(const bool buyorsell) {...} bool Condition_1_Exit(const bool buyorsell) {...} bool Condition_2_Exit(const bool buyorsell) {...}
We’ll name the EA “Algo Trader”. Create a blank text file Algo Trader.mq4
in your MT4 Experts folder. Open the file in MetaEditor.
Paste the foundational code into the file. Be sure to read through the original article to get the full code inside each function.
The foundational code won’t compile as it is, because we left a few things out as a learning exercise. We’ll now make some small adjustments so that the foundational code you pasted above can fully compile.
Set strict
property
First, set #property strict
at the top of the file:
#property strict
We should always set the strict property to enable more well-defined program behavior and safeguard data types.
Fill in OrderSend(...)
Next, fill in the OrderSend(...)
line. Before, it was:
int OpenOrder(const bool buyorsell) {
...
const int ticket = OrderSend(...);
...
Now:
int OpenOrder(const bool buyorsell) {
...
const int ticket = OrderSend(
_Symbol, //string symbol
buyorsell ? OP_BUY : OP_SELL, //int cmd
0.01, //double volume
buyorsell ? Ask : Bid, //double price
0, //int slippage
0.0, //double stoploss
0.0 //double takeprofit
);
...
OrderSend
is a built-in MQL4 function that requests the broker to open an order. You should refer to the MQL4 reference for OrderSend as you code, in order to decide how to fill in the function’s parameters as you need.
_Symbol
is a built-in MQL4 keyword that automatically refers to the symbol of the chart that the EA us running on. OP_BUY
and OP_SELL
are built-in MQL4 enumerations that tell OrderSend
to open a Buy Market Order and Sell Market Order. Ask
and Bid
are built-in MQL4 keywords that automatically refer to the ask and bid prices.
A Buy order is placed at the current asking price, and a Sell order opens at the current bidding price – we have to get this right otherwise errors will occur. This is general financial market mechanics – it’s simple to figure out if we understand the spread.
In the code above, we’ve set OrderSend
to open a Buy or Sell market order of lot size 0.01. We’ve set no stop loss and no take profit.
There are better ways to develop this piece of code into a more reusable version, but we’ll progress to that gradually.
Aside: The ternary operator
We use the ternary operator to flip Buy and Sell scenarios, keeping code compressed with less duplication.
In this niche, this pattern is appropriate because of how many algorithms have a flipped Buy and Sell scenario.
Writing two OrderSend
blocks would be less declarative and would duplicate code. Duplicate code makes maintenance difficult and creates more bugs.
Fill in OrderClose(...)
Next, fill in the OrderClose(...)
line. Before, it was:
bool CloseOrder(const int ticket) {
const bool success = OrderClose(...);
...
Now:
bool CloseOrder(const int ticket) {
if (!OrderSelect(ticket, SELECT_BY_TICKET)) return false;
if (OrderType() == OP_BUY || OrderType() == OP_SELL) {} else return false;
const bool success = OrderClose(
ticket,
OrderLots(),
(OrderType() == OP_BUY) ? Bid : Ask,
0
);
...
OrderSelect
is a built-in MQL4 function that retrieves the properties of an order, making the property values available via subsequent calls to built-in MQL4 functions like OrderType()
and OrderLots()
.
While programming, refer to the MQL4 reference for OrderSelect to find out the values you should pass to it.
Aside: Program flow breaks; avoid nesting
In the code above, if
statements are used as guards to prevent program flow past certain points.
The first if
statement breaks program flow if the order selection failed, and the second one asserts that the order should be a live one (as opposed to a pending order), otherwise program flow should break.
In some other programming languages and ecosystems, developers might not like empty braces {}
, or even statements like continue
or break
. For example, in Python, you might often see code that’s indented 3 or 4 levels in, which might be preferred by some developers in that ecosystem.
However, it’s important to remember that different styles serve different thinking patterns.
In this particular language and application, we deal often with a pattern where several guards must be passed in order to validate program flow. Therefore, guard-like assertion statements with breaks is appropriate. We’ll use this pattern often, and prefer it over nesting conditionals.
Also importantly, this is a compiled C-like language that doesn’t have an interpreter to catch exceptions. In such a language, program flow and errors must be managed very precisely on a line-by-line basis. Therefore, it’s more manageable to have such a program flow in a recipe-like way with shallow levels of nesting.
In our code, we’ll avoid nesting code beyond 3 levels in. If our code starts to nest more than 3 levels deep, it’s a sign that we should begin to extract parts of it into separate functions.
Simplify Conditions_Entry
Next, clean up Conditions_Entry
to return false
for now, to say that there’s no condition for entry:
bool Conditions_Entry(const bool buyorsell) {
return (
false
);
}
Entirely remove the unused functions Condition_1_Entry
, Condition_2_Entry
and Condition_3_Entry
.
Write Conditions_Exit
Next, prepare the Conditions_Exit
function in a similar way returning false
for now, to say that there’s no exit condition:
bool Conditions_Exit(const bool buyorsell) {
return (
false
);
}
Implement ArrayAdd
Finally, the foundation code will have one more issue preventing it from compiling: an unknown function ArrayAdd
.
ArrayAdd
is a reusable utility function that we’ll create for ourselves so that we can use it everywhere in code. It’s natural to say “add an item to the array”, and we do this in code often enough that it makes sense to create a utility and forget how it’s really done.
The following code implements ArrayAdd
. Paste it at the bottom of the file:
void ArrayAdd(int &arr[], const int x) {
if (ArrayResize(arr, ArraySize(arr) + 1) > 0) {
arr[ArraySize(arr) - 1] = x;
}
}
Now, you can use ArrayAdd
whenever you need to add an item to an array: if you have a dynamic array arr
and new value x
, then you can add it onto the end of the array by saying ArrayAdd(arr, x)
.
This is abstraction at a low level. As we keep reinforcing, the more you can create code that reads like the high level business logic, the better you’ll manage.
Exercise: The above implementation can be improved in a several ways. First of all, it currently only works with integer types, and it can be made to work with more types. How can you make it work with pointers? Also, it doesn’t return an error signal if it fails. Can you work out how to improve on all these points without duplicating code?
We originally used the ArrayAdd
function within a loop. So, some might argue that using this function in that situation is not optimal. For example, it might be more computationally efficient to assign a reserved size to the array outside the loop first.
However, in this case, abstraction is worth more than a negligible optimization.
Compile and run the foundation code
Now, in MetaEditor, click Compile. It should happen successfully and a distributable EA file Algo Trader.ex4
should be produced within the same directory as your source code file.
Open MetaTrader, find this new EA in the Navigator, and test-run it using the Strategy Tester.
Hooray! It runs.
It does nothing, which is perfect: it should not trade at all, because the foundation code has no conditions for entering a trade.
Now that we have fully compilable and running foundation code, let’s implement the specific set-and-forget strategy.
Move utilities into include files
Before we move on, the file Algo Trader.mq4
is beginning to look quite large. Some functions we’ve written are useful as utilities no matter what strategy we’re implementing. They can be moved into separate files to keep our main file readable.
These functions are good candidates for being moved into separate code files:
// Infrastructure (never changes, only improves): utilities
int OpenOrder(const bool buyorsell) {...}
bool CloseOrders(const bool buyorsell) {...}
bool CloseOrder(const int ticket) {...}
void ArrayAdd(int &arr[], const int x) {...}
Cut and paste the OpenOrder
function into a new text file called OpenOrder.mqh
.
Cut and paste the CloseOrders
and CloseOrder
functions into a new text file called CloseOrder.mqh
.
Cut and paste the ArrayAdd
function into a new text file called ArrayAdd.mqh
.
Now in the main code file Algo Trader.mq4
, place this code at the top of the file, under the #property strict
line:
#include "OpenOrder.mqh"
#include "CloseOrder.mqh"
#include "ArrayAdd.mqh"
Now, our main code file is clearer to read, and we can use it to focus on the higher level strategy program flow.
Now, let’s move on to implement the unique set-and-forget strategy, beginning with the entry logic.
Implement the entry strategy
Referring back to the specifications, the entry strategy is:
If the MACD indicator is above 0, open a Buy position. If the MACD indicator is below 0, open a Sell position. Keep only 1 open position at a time.
Our foundation code already has the capability to make an entry based on a decision, so we only need to implement the decision to enter within Conditions_Entry
.
MACD is above 0
In Conditions_Entry
, assert a condition Condition_MACDIsAboveOrBelow0
as follows:
bool Conditions_Entry(const bool buyorsell) {
return (
Condition_MACDIsAboveOrBelow0(buyorsell)
);
}
Condition_MACDIsAboveOrBelow0
will either check if MACD is above 0, or if MACD is below 0. Which rule it checks depends on the boolean switch that we pass to it. Here’s its implementation:
bool Condition_MACDIsAboveOrBelow0(const bool abovebelow) {
const double c = GetValue_MACD(1);
if (c == EMPTY_VALUE) return false;
return (
abovebelow ? (c > 0) : (c < 0)
);
}
In the code above, the function parameter abovebelow
is a boolean switch used to switch what we ask the function. Setting abovebelow
to true
shall ask the function to check if MACD is above 0, and setting to false
will ask it to check if MACD is below 0.
This technique of using a boolean switch abovebelow
coincides with the buyorsell
boolean switch technique. By design, we’ve made it so that when we call this function in the entry rules, we can set abovebelow
to the value of buyorsell
, so that in the Buy scenario we assert that MACD is above 0, and in the Sell scenario we assert that MACD is below 0.
The program flow in the above code also shows a paradigm specific to the domain of MQL4 programming, whenever dealing with an indicator. First, we retrieve the value of an inbuilt or custom indicator. Then, we ensure that the value is not EMPTY_VALUE
– this is an inbuilt MQL4 value used to indicate “no value”, and is often used as a placeholder for values that don’t exist or haven’t yet been calculated. If we have an empty value for the indicator, we should not proceed with program flow, and should return false
to indicate that the decision to enter must fail. If we pass through this guard because we received a proper value, then we return the truth of the assertion that the value should be above or below 0.
It’s important to guard the usage of indicator values in this way, because for example EMPTY_VALUE
is internally implemented as a very high number, so the assertion EMPTY_VALUE > 0
is true. So, not guarding for empty cases will create dangerously silent buggy behavior, which will be overlooked in code that isn’t structured in a disciplined way.
Now, we need to implement GetValue_MACD
.
We abstract the calling of the indicator into a lower level function, because it prevents code duplication when the indicator is called from different places of the program.
More importantly, we’re establishing an important infrastructural pattern. Not all custom indicators follow the same conventions, so the GetValue_<xxx>
layer of the infrastructure is created to promise higher layers a normalized and cleaned value; in particular a value of EMPTY_VALUE
if the indicator’s calculation fails.
In a highly developed infrastructure, this normalized GetValue_<xxx>
layer will become very important in order to be able to build generic functions.
In this particular case, the implementation of GetValue_MACD
is simple:
double GetValue_MACD(const int shift) {
return iMACD(_Symbol, 0, 6, 13, 4, PRICE_CLOSE, MODE_MAIN, shift);
}
Again, you may not see the value of creating a function just to replace a single line of code. The value of creating this layer will be more obvious when we deal with custom indicators and increase the flexibility of the infrastructure in the future.
For now, copy the above function into the main code file.
In the code, iMACD
is an inbuilt MQL4 function for running the inbuilt MACD indicator and getting its values. We’ve set the MACD indicator to have parameters: Fast EMA 6, Slow EMA 13 and MACD SMA 4. While programming, you should refer to the MQL4 reference for iMACD to find out how to set the function arguments to get what you need.
Now, hit Compile and run the EA in the Strategy Tester.
Hooray! It runs.
However, it keeps buying while MACD is above 0, and keeps selling if MACD is below 0, without limit. That’s ok, because we haven’t finished just yet.
Only 1 open order
The trading steps are to:
Keep only 1 open position at a time.
So, we want to make an entry only if we don’t already have an open order. This is an entry condition.
First, we’ll write a utility OrderIsOpen
so that we can say “… if OrderIsOpen
then…” in our logic. Here’s the implementation:
bool OrderIsOpen() {
return (OrdersTotal() > 0);
}
In the code above, OrdersTotal
is an inbuilt MQL4 function that tells us the current number of tickets on the MT4 application’s Trade tab.
For now, this seems like a useless function that replaces only one line of code. However, that’s only because our infrastructure is still not fully mature. For example, currently this EA’s infrastructure assumes that it’s the only thing trading on the account. No other manual or automated trading must be going on at the same time, because this EA will interfere.
When more matured, this abstraction of OrderIsOpen
will have a more complex implementation.
There’s also not only one way to implement it – in certain situations its internal implementation might use cache variables to determine if a position is open.
For now, copy the above code. Since it’s more like a utility that can be reused, put the code inside a new file OrderIsOpen.mqh
. Then #include
it inside the main code file along with the other includes:
#include "OpenOrder.mqh"
#include "CloseOrder.mqh"
#include "ArrayAdd.mqh"
#include "OrderIsOpen.mqh" // <-- Added
Identifying and shelving reusable code in this way is important.
Now, insert “no OrderIsOpen
” as one of the entry conditions, because we only want to enter if no order is already open:
bool Conditions_Entry(const bool buyorsell) {
return (
!OrderIsOpen() && // <-- Added
Condition_MACDIsAbove0(buyorsell)
);
}
Now, hit Compile and run the EA in the Strategy Tester.
Hooray! Only one order is opened now – perfect. It waits forever, because we haven’t implemented the exit strategy yet.
Slight optimization
Since Conditions_Entry
is run twice every tick, once for the buy scenario and once for the sell scenario, and the OrderIsOpen
check doesn’t depend on buyorsell
, we can save some processing by moving OrderIsOpen
into a higher level of program flow.
Remove it from Conditions_Entry
:
bool Conditions_Entry(const bool buyorsell) {
return (
//!OrderIsOpen() && // <-- Removed
Condition_MACDIsAbove0(buyorsell)
);
}
Place it as a guarding condition in CheckAndDoEntries
:
void CheckAndDoEntries(void) {
if (OrderIsOpen()) return; // <-- Added
if (Conditions_Entry(true)) Entry(true);
if (Conditions_Entry(false)) Entry(false);
}
Hit Compile and run the EA in the Strategy Tester again. Make sure that nothing has changed after your code edit: only one order should open.
This is called a regression test to ensure that code refactoring changes have not destroyed established behavior.
During development, it’s good practice to develop in small compilable steps.
Implement the exit strategy
Referring back to the specifications, the exit strategy is:
If we find that we’re in profit by any amount, close the position – but only if the MACD indicator is below 0 (if we have a Buy position) or above 0 (if we have a Sell position).
Our foundational code already has the capability to execute an exit – that is, to close orders. Now, all we need to do is implement the conditions to do so.
Order is in profit
In Conditions_Exit
, add the condition OrderIsInProfit
to assert that the open order must be in profit:
bool Conditions_Exit(const bool buyorsell) {
return (
OrderIsInProfit() // <-- Added
);
}
Now, implement OrderIsInProfit
:
bool OrderIsInProfit(void) {
if (!OrderIsOpen()) return false;
if (!OrderSelect(0, SELECT_BY_POS)) return false;
return (
NormalizeDouble(OrderProfit() + OrderCommission() + OrderSwap(), 2) > 0.0
);
}
In the above code, we check first whether there’s an open order. If not, it doesn’t make sense to flow on, so the function should return false
. Next, it selects the first seen open order; if this operation fails, the function should return false
. If program flow passes through, we now have the open order’s details, and we assert that the order’s net profit after all broker fees is above zero.
Exercise: This is a naive implementation. For one, this function assumes a simplistic scenario where this EA is the only thing trading on the account. Secondly, it assumes a strategy that opens only one order, so it doesn’t calculate the aggregate profit. It’ll work for this strategy, but it’s not very reusable for other scenarios. It’s possible to greatly improve this function with a few more lines of code.
For now, copy the above code into a new file called OrderIsInProfit.mqh
and #include
it at the top of the main file:
#include "OpenOrder.mqh"
#include "CloseOrder.mqh"
#include "ArrayAdd.mqh"
#include "OrderIsOpen.mqh"
#include "OrderIsInProfit.mqh" // <-- Added
Hit Compile and run the EA in the Strategy Tester.
Hooray! It runs.
We see that the EA is making entries, then making exit as soon as any profit is made – that is, as long as it has made at least a single cent, after paying for broker commissions and swaps.
This is an important point on the strategy level: if commissions and swaps aren’t considered, then although it might look like the order is closing at a better price on the chart, we might still be making a loss due to fees. The algorithmic exit rule OrderIsInProfit
ensures a real net profit after fees, not just comparing the close price with the open price.
MACD is below 0
You’ll see that the EA is making entries and exits extremely often, because right now it only needs to make 1 cent in order to exit. Let’s move on to implement the second exit criteria:
… close the position – but only if the MACD indicator is below 0 (if we have a Buy position) or above 0 (if we have a Sell position).
We already implemented a function Condition_MACDIsAboveOrBelow0
that can tell us if MACD is above or below 0. We can reuse this in the exit rule.
In Conditions_Exit
, use Condition_MACDIsAboveOrBelow0
to assert that the MACD must be below 0 in the Buy scenario, and above 0 in the Sell scenario:
bool Conditions_Exit(const bool buyorsell) {
return (
OrderIsInProfit() &&
Condition_MACDIsAboveOrBelow0(!buyorsell) // <-- Added
);
}
In the Buy scenario, we want to assert that MACD is below 0. So, in the Buy scenario we want to pass false
to Condition_MACDIsAboveOrBelow0
to assert the “below 0” version of the check. In the Sell scenario, we want to pass true
. Therefore, passing !buyorsell
to the function automatically takes care of both the Buy and Sell scenarios. This is made possible by our design pattern using boolean switches.
Hit Compile and run the EA in the Strategy Tester.
Hooray! It runs.
Now it exits a Buy position whenever it’s profitable, but only when MACD is below 0. And vice versa for a Sell position. The strategy is complete.
Here’s what the Tester Graph looks like, depending on which Symbol you run it on and date range:
Congratulations, the strategy is fully implemented.
Enhancement: Add an input
Currently, the EA is hard-coded to open an order with lot size 0.01. Let’s add an input so that you can control this in MT4.
Create a new text file called inputs.mqh
and #include
it at the top of the main file:
#include "OpenOrder.mqh"
#include "CloseOrder.mqh"
#include "ArrayAdd.mqh"
#include "OrderIsOpen.mqh"
#include "OrderIsInProfit.mqh"
#include "inputs.mqh" // <-- Added
Now open the file inputs.mqh
and insert the following code, including the comment:
input double I_LotSize = 0.01; // Lot Size
In the above code, input
is a special inbuilt MQL4 modifier that says to make this variable a user input in the EA inputs dialog.
Hit Compile. In MT4, drag the EA from the Navigator onto a chart as if you’re about to attach it. You’ll see that the EA ‘Inputs’ tab now has a ‘Lot Size’ user input.
Now, the code needs to actually use this new variable. So, open file OpenOrder.mqh
and replace the hard-coded 0.01
with I_LotSize
:
int OpenOrder(const bool buyorsell) {
...
const int ticket = OrderSend(
_Symbol,
buyorsell ? OP_BUY : OP_SELL,
I_LotSize, // 0.01 // <-- Replaced
buyorsell ? Ask : Bid,
0,
0.0,
0.0
);
...
Save the file. Then, open the main code file Algo Trader.mq4
and hit Compile – done!
Now the EA will open orders at the Lot Size set by the user.
Final words
We’ve learnt how to program a fully working MT4 EA that trades a set-and-forget strategy. Along the way, we demonstrated important concepts:
- Foundational principles: designing code from a high level, compiling often, guarding program flow, identifying reusable code and building a library.
- Domain-specific paradigms: getting indicator values cleanly.
- Style and technique: Avoiding excessive indentation, and usage of the ternary operator to account for both Buy and Sell scenarios without duplicating code.
There are several ways in which this code can be improved. For now, we’ll leave you with a few things to think about as an exercise:
- How can the infrastructure be improved so that this EA can work alongside manual trading and other EAs on the same account?
- Certain processes like checking exit rules can be suppressed if no order is open.
- How can the code be made to work on a renko chart?
- What if we need to perform 2, or 10, different strategies in the same program? How can this infrastructure be gracefully extended without destroying the model?
- Can any of our conditions be made even more generic?
Don’t worry, each point can be solved as the need arises. The main takeaway is to follow foundational principles and keep code organized.
If you like our article and it helped you, feel free to share it, or reach out to our bespoke service for custom development.