Build Your First Expert Advisor — MA Crossover Step by Step
You have the environment set up and the MQL5 reference at hand. Now it's time to build a real trading EA: a Moving Average crossover strategy. By the end of this article you will have a complete MA crossover EA that compiles, runs in the Strategy Tester, and places real buy/sell orders — with clear entry/exit rules and logic you can extend. This tutorial takes you from idea to working code in eight focused steps.
All code follows the official MQL5 event handlers and trade functions documentation.
The Strategy — Moving Average Crossover
Idea: Use two Moving Averages — a fast one (e.g. 20 periods) and a slow one (e.g. 50 periods).
- Buy signal: Fast MA crosses above the slow MA
- Sell signal: Fast MA crosses below the slow MA
This is a classic trend-following approach. We trade in the direction of the crossover.
Step 1: Define the Logic
Before writing a single line of MQL5, you must crystallise your trading logic. A clear spec prevents bugs and makes debugging easier. For our MA crossover EA, we define every choice explicitly.
| Component | Choice | Why This Choice |
|---|---|---|
| Fast MA | 20-period EMA, Close | EMA reacts faster than SMA to recent prices — ideal for crossover. Close is standard and widely used in backtests. |
| Slow MA | 50-period EMA, Close | 50 is a common "medium-term" period; it smooths noise and confirms trend direction. |
| Entry | One position at a time; new signal closes opposite and opens new | Keeps risk simple: no multiple overlapping positions. When a new crossover occurs, we close any opposite position and open the new one. |
| Exit | Opposite crossover closes the position | No separate exit rule — the next opposite crossover both closes the current trade and opens the reverse. |
Pro tip: Document your logic like this for every EA. Future you (and Google) will thank you when you revisit the code months later.
Step 2: Create the EA File
Create a new Expert Advisor from MetaEditor’s built-in template so you get the correct structure (OnInit, OnTick, OnDeinit) from the start.
- In MetaEditor, go to File — New and choose Expert Advisor (template).
- When prompted, name it
MACrossoverEAand leave the default parameters — the wizard creates the skeleton. - Save the file to MQL5\Experts. This folder is where MetaTrader 5 looks for EAs. If you save elsewhere, the EA will not appear in the Navigator.
Folder structure: MetaTrader 5 installs to a path like this:
C:\Users\YourName\AppData\Roaming\MetaQuotes\Terminal\...\MQL5\Experts
Your EA file MACrossoverEA.mq5 must live there (or in a subfolder) to be compiled and run.
Step 3: Inputs and Handles
We declare two kinds of variables: inputs (user-configurable) and handles (references to indicator instances).
Inputs appear in the EA properties dialog and the Strategy Tester. Users can change periods and lot size without editing code. The input keyword makes them persistent.
Handles are integer IDs returned by iMA(). They point to the indicator’s internal buffers. We create them once in OnInit() and reuse them in OnTick() via CopyBuffer(). Never create handles inside OnTick() — that would allocate new indicators on every tick and hurt performance.
The #property directives set metadata shown in MetaTrader (copyright, version). They are optional but good practice for professional EAs.
#property copyright "AlfaTactix Academy"
#property version "1.00"
input int InpFastPeriod = 20; // Fast MA period
input int InpSlowPeriod = 50; // Slow MA period
input double InpLotSize = 0.1; // Lot size
input int InpMagic = 12345; // Magic number
int g_fastHandle = INVALID_HANDLE;
int g_slowHandle = INVALID_HANDLE;
Step 4: OnInit — Create Indicator Handles
OnInit() runs once when the EA is attached to a chart (or when the Strategy Tester starts). Per the OnInit documentation, this is where you create indicator handles, validate inputs, and prepare resources.
Input validation: We reject invalid periods (e.g. fast ≥ slow, or zero/negative) and return INIT_PARAMETERS_INCORRECT. That stops the EA cleanly and shows an error in the Experts tab instead of crashing later.
iMA parameters: iMA(symbol, timeframe, period, shift, method, applied_price). We use PERIOD_CURRENT so the EA follows the chart’s timeframe, MODE_EMA for Exponential MA, and PRICE_CLOSE for close prices. If iMA() returns INVALID_HANDLE, the terminal logs an error and we return INIT_FAILED.
OnDeinit: When the EA is removed (chart closed, tester finished), OnDeinit() releases the handles with IndicatorRelease(). This frees memory and avoids leaks — a must when running many backtests.
int OnInit()
{
if(InpFastPeriod <= 0 || InpSlowPeriod <= 0 || InpFastPeriod >= InpSlowPeriod)
{
Print("Invalid MA periods. Fast must be < Slow.");
return(INIT_PARAMETERS_INCORRECT);
}
g_fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
g_slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(g_fastHandle == INVALID_HANDLE || g_slowHandle == INVALID_HANDLE)
{
Print("Failed to create MA handles. Error: ", GetLastError());
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(g_fastHandle != INVALID_HANDLE) IndicatorRelease(g_fastHandle);
if(g_slowHandle != INVALID_HANDLE) IndicatorRelease(g_slowHandle);
}
Step 5: OnTick — Detect Crossover and Trade
OnTick() is called on every price tick. That can be hundreds of times per candle. If we evaluate signals on every tick, we risk duplicate orders and multiple entries on the same bar. The solution: process signals only when a new bar forms.
New-bar check: We compare iTime(_Symbol, PERIOD_CURRENT, 0) (current bar's opening time) with a static variable lastBar. When they differ, a new bar has formed. We update lastBar and proceed; otherwise we return immediately. This is a lightweight and reliable pattern used in professional EAs.
Why three bars? To detect a crossover we need the state before and after the cross. With ArraySetAsSeries(buffer, true), index 0 = current bar, 1 = previous (closed) bar, 2 = bar before that. Crossover happens between bars 2 and 1:
- Buy: On bar 1, fast MA is above slow (
fast[1] > slow[1]), but on bar 2 it was below or equal (fast[2] <= slow[2]). That means the fast crossed above the slow. - Sell: Symmetrically,
fast[1] < slow[1]andfast[2] >= slow[2]means the fast crossed below.
CopyBuffer: We request 3 elements (indices 0, 1, 2). If CopyBuffer returns less than 3, there isn't enough history yet — we return and wait for the next tick. On a fresh chart, the first bars may not have sufficient data, so this guard is essential.
Flow: On a buy signal we close any sell positions first, then open a buy if we don't already have one. Same for sell. This keeps our "one position per direction" rule.
void OnTick()
{
static datetime lastBar = 0;
datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBar == lastBar) return; // Wait for new bar
lastBar = currentBar;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(g_fastHandle, 0, 0, 3, fast) < 3) return;
if(CopyBuffer(g_slowHandle, 0, 0, 3, slow) < 3) return;
bool buySignal = (fast[1] > slow[1]) && (fast[2] <= slow[2]);
bool sellSignal = (fast[1] < slow[1]) && (fast[2] >= slow[2]);
if(buySignal)
{
ClosePositions(POSITION_TYPE_SELL);
if(CountPositions(POSITION_TYPE_BUY) == 0)
OpenBuy();
}
else if(sellSignal)
{
ClosePositions(POSITION_TYPE_BUY);
if(CountPositions(POSITION_TYPE_SELL) == 0)
OpenSell();
}
}
Step 6: OpenBuy and OpenSell — OrderSend
OrderSend() sends a trading request to the broker. You fill a MqlTradeRequest structure and optionally receive the result in MqlTradeResult.
Key fields explained:
- action = TRADE_ACTION_DEAL — Immediate execution at market (not a pending order).
- symbol — The trading pair;
_Symbolis the chart’s symbol. - volume — Lot size; must comply with
SYMBOL_VOLUME_MINandSYMBOL_VOLUME_STEP. - type —
ORDER_TYPE_BUYorORDER_TYPE_SELL. - price — For market orders: use Ask for buys (you pay the ask) and Bid for sells (you receive the bid).
SymbolInfoDouble(_Symbol, SYMBOL_ASK/BID)returns the current price. - deviation — Max allowed slippage in points. 10 points is a common default; increase for volatile symbols if you see requotes.
- magic — Your EA’s identifier. Use it to filter positions and orders in
PositionSelect,OrderSelect, etc.
Error handling: If OrderSend() returns false, GetLastError() gives the reason (e.g. not enough margin, market closed, requote). Always log it for debugging.
void OpenBuy()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_BUY;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res))
Print("OpenBuy failed: ", GetLastError());
}
void OpenSell()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_SELL;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res))
Print("OpenSell failed: ", GetLastError());
}
Step 7: Close Positions and Count by Magic
In MetaTrader 5, an account can have positions from multiple EAs and manual trades. We must filter by symbol and magic number so our EA only manages its own positions.
CountPositions: Iterates over PositionsTotal() — the number of open positions. PositionGetTicket(i) returns the ticket; PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC), and PositionGetInteger(POSITION_TYPE) identify each position. We count only positions matching our symbol, magic, and type (BUY or SELL). This prevents opening a second buy when we already have one.
ClosePositions: To close a position, we send a counter-order — a sell to close a buy, or a buy to close a sell. Set req.action = TRADE_ACTION_DEAL, req.position = ticket (the position to close), and req.type / req.price to the opposite side. req.volume must equal the position’s volume (we close the whole position). The magic ensures the closing order is attributed to our EA.
Why iterate backwards? When we modify the positions list (e.g. close one), indices can shift. Looping from PositionsTotal()-1 down to 0 avoids skipping or double-processing.
int CountPositions(ENUM_POSITION_TYPE type)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
count++;
}
return count;
}
void ClosePositions(ENUM_POSITION_TYPE type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = _Symbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
req.deviation = 10;
req.magic = InpMagic;
req.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
req.price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
OrderSend(req, res);
}
}
Step 8: Compile and Test
1. Compile. In MetaEditor, press F7 (or Compile from the Build menu). The output panel shows errors and warnings. Fix any errors before proceeding. Warnings (e.g. unused variables) are optional but worth cleaning up for production code.
2. Open the Strategy Tester. In MetaTrader 5, go to View — Strategy Tester (or press Ctrl+R). The Tester panel appears at the bottom of the screen.
3. Configure the test. Select your EA (MACrossoverEA) from the dropdown. Choose a symbol (e.g. EURUSD), timeframe (H1 recommended for crossover strategies), and date range (e.g. the last 12 months for a meaningful backtest). Set the deposit and leverage if needed. Enable Visual mode if you want to watch the EA trade bar-by-bar.
4. Choose modeling quality. "Every tick" is most accurate but slowest. "1 minute OHLC" uses one tick per minute and is a good balance of speed and realism for daily/hourly strategies. "Open prices only" is fastest but least accurate — use only for quick sanity checks.
5. Run the test. Click Start. When finished, check the Results tab (profit, drawdown, trades) and the Journal tab (any Print() output and errors). The Graph tab shows equity curve. Use these to validate that your EA behaves as expected before considering Demo or Live deployment.
Summary — What You Built
| Part | Role |
|---|---|
| OnInit | Create iMA handles, validate inputs |
| OnDeinit | Release handles |
| OnTick | New-bar check, crossover logic, open/close |
| OpenBuy/OpenSell | OrderSend with MqlTradeRequest |
| ClosePositions | Close by symbol + magic + type |
| CountPositions | Avoid duplicate positions |
Full Code
Here is the complete EA in one block for copy-paste and verification. Assemble it in MetaEditor and save as MACrossoverEA.mq5 in MQL5\Experts.
//+------------------------------------------------------------------+
//| MACrossoverEA.mq5 |
//| MA Crossover Expert Advisor |
//+------------------------------------------------------------------+
#property copyright "AlfaTactix Academy"
#property version "1.00"
input int InpFastPeriod = 20; // Fast MA period
input int InpSlowPeriod = 50; // Slow MA period
input double InpLotSize = 0.1; // Lot size
input int InpMagic = 12345; // Magic number
int g_fastHandle = INVALID_HANDLE;
int g_slowHandle = INVALID_HANDLE;
int OnInit()
{
if(InpFastPeriod <= 0 || InpSlowPeriod <= 0 || InpFastPeriod >= InpSlowPeriod)
{
Print("Invalid MA periods. Fast must be < Slow.");
return(INIT_PARAMETERS_INCORRECT);
}
g_fastHandle = iMA(_Symbol, PERIOD_CURRENT, InpFastPeriod, 0, MODE_EMA, PRICE_CLOSE);
g_slowHandle = iMA(_Symbol, PERIOD_CURRENT, InpSlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(g_fastHandle == INVALID_HANDLE || g_slowHandle == INVALID_HANDLE)
{
Print("Failed to create MA handles. Error: ", GetLastError());
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(g_fastHandle != INVALID_HANDLE) IndicatorRelease(g_fastHandle);
if(g_slowHandle != INVALID_HANDLE) IndicatorRelease(g_slowHandle);
}
void OnTick()
{
static datetime lastBar = 0;
datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBar == lastBar) return;
lastBar = currentBar;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(g_fastHandle, 0, 0, 3, fast) < 3) return;
if(CopyBuffer(g_slowHandle, 0, 0, 3, slow) < 3) return;
bool buySignal = (fast[1] > slow[1]) && (fast[2] <= slow[2]);
bool sellSignal = (fast[1] < slow[1]) && (fast[2] >= slow[2]);
if(buySignal)
{
ClosePositions(POSITION_TYPE_SELL);
if(CountPositions(POSITION_TYPE_BUY) == 0) OpenBuy();
}
else if(sellSignal)
{
ClosePositions(POSITION_TYPE_BUY);
if(CountPositions(POSITION_TYPE_SELL) == 0) OpenSell();
}
}
void OpenBuy()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_BUY;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res)) Print("OpenBuy failed: ", GetLastError());
}
void OpenSell()
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = InpLotSize;
req.type = ORDER_TYPE_SELL;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
req.deviation = 10;
req.magic = InpMagic;
if(!OrderSend(req, res)) Print("OpenSell failed: ", GetLastError());
}
int CountPositions(ENUM_POSITION_TYPE type)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
count++;
}
return count;
}
void ClosePositions(ENUM_POSITION_TYPE type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
if(PositionGetInteger(POSITION_TYPE) != type) continue;
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = _Symbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
req.deviation = 10;
req.magic = InpMagic;
req.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
req.price = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
OrderSend(req, res);
}
}
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
INVALID_HANDLE from iMA | Symbol or timeframe not available, or bad parameters | Ensure the symbol is in Market Watch, chart is open, and periods are valid (fast < slow, both > 0). |
Not enough money (134) | Insufficient margin for the lot size | Reduce InpLotSize, increase deposit in Strategy Tester, or check leverage. |
Requote (10004) | Price moved before order filled | Increase req.deviation (e.g. 20–50 points) or accept requotes and retry. |
| EA does not open trades | AutoTrading off, or "Allow live trading" unchecked | Click AutoTrading in MT5 toolbar (Ctrl+E), and in EA Properties enable "Allow Algo Trading". |
Trade not allowed (64) | Broker or account restrictions | Verify the symbol allows trading, market is open, and the account permits automated trading. |
| Duplicate orders on same bar | No new-bar check in OnTick | Add the lastBar / iTime check as in Step 5 — evaluate signals only once per candle. |
Check the Experts tab in MetaTrader 5 for error codes and Print() messages. GetLastError() returns the numeric code; refer to MQL5 error constants.
Next Steps — Add Risk Management
This EA has no Stop Loss or Take Profit. In EA Risk Management, you'll add position sizing, SL/TP, trailing stops, and best practices to protect your account.
Bonus Tip: Want to build the same MA crossover EA without writing code? Try AlfaTactix Strategy Builder free — design moving average crossovers, RSI filters, and risk rules visually, then export production-ready MQL5 in minutes. Compare variants side-by-side before refining in MetaEditor. The same logic, zero typing.