MQL4 was the first language we ever loved. Twelve years and a few hundred EAs later, we still think there's no faster way to go from "I have an idea" to "I have a bot running on a chart." MT4 won't win any beauty contests in 2026, but it works, the API is dead simple, and every broker on the planet supports it. By the end of this walkthrough, you'll have a real, compilable Expert Advisor with entry signals, ATR-based stops, proper position sizing, and order management. The whole thing.
No theory lectures. Code you can paste into MetaEditor and run today.
Prerequisites
- MetaTrader 4 installed (free from any MT4 broker)
- MetaEditor (comes bundled with MT4 — press F4 to open it)
- A demo account — and we mean it, do NOT develop on a live account. We've seen people blow real money testing half-finished code. Don't be that person.
Step 1: The EA Skeleton
Every EA lives and dies by three functions. That's it. Three. OnInit() fires when you drop the EA on a chart, OnDeinit() fires when you pull it off, and OnTick() fires on every single price update in between. Master these and you understand 80% of how MQL4 works:
//+------------------------------------------------------------------+
//| MyFirstEA.mq4 |
//| A simple trend-following EA built step by step |
//| https://www.quantumcona.com |
//+------------------------------------------------------------------+
#property copyright "Quantumcona"
#property link "https://www.quantumcona.com"
#property strict // Enable strict compilation mode (catches more errors)
//--- Input parameters (user can adjust in MT4's EA settings panel)
input double RiskPercent = 1.0; // Risk % per trade
input int EMA_Fast = 20; // Fast EMA period
input int EMA_Slow = 50; // Slow EMA period
input int ATR_Period = 14; // ATR period for stop loss
input double ATR_SL_Mult = 1.5; // Stop loss = ATR × this multiplier
input double TP_Ratio = 2.0; // Take profit = SL distance × this
input int MagicNumber = 10001; // Unique ID for this EA's orders
input int Slippage = 3; // Max allowed slippage in points
//+------------------------------------------------------------------+
//| Initialization — runs ONCE when EA is loaded onto chart |
//+------------------------------------------------------------------+
int OnInit()
{
Print("MyFirstEA initialized on ", Symbol(), " | Timeframe: ", Period());
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Deinitialization — runs when EA is removed or chart is closed |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("MyFirstEA removed. Reason code: ", reason);
}
//+------------------------------------------------------------------+
//| OnTick — runs on EVERY incoming price tick |
//+------------------------------------------------------------------+
void OnTick()
{
// We'll build this step by step below
}Key concepts:
#property strict— non-negotiable. We refuse to debug EAs that don't have this. It catches type mismatches and undeclared variables at compile time instead of letting them silently wreck your trades at 3am.inputparameters show up in MT4's settings dialog so users can tweak values without touching your source code.MagicNumber— this one trips up more beginners than anything. It's a tag on every order your EA places. Without it, you can't run two EAs on the same chart without them stomping on each other's trades. Pick a unique number. Treat it as sacred.
Step 2: The New Bar Filter
Your EA does NOT need to recalculate everything on every tick. For candle-based strategies, you only care about the moment a new bar opens. This little pattern saves you from burning CPU cycles and, more importantly, from accidentally placing duplicate orders:
void OnTick()
{
// Only run logic once per new bar (not every tick)
static datetime lastBarTime = 0;
if (Time[0] == lastBarTime) return;
lastBarTime = Time[0];
// --- Trading logic goes below this line ---
}Why this matters: Without this filter, your EA hammers through its logic potentially hundreds of times a minute. Completely pointless for a strategy that only cares about closed candles. Worse, you'll get weird bugs — duplicate signals, excessive logging, orders firing twice. One static datetime check. That's all it takes to fix it.
Step 3: Entry Signal — EMA Crossover
The EMA crossover. Yes, it's the "Hello World" of trading strategies. No, it won't make you rich. But it's the perfect signal to learn with because the logic is crystal clear. We compare the previous bar's state to the current bar's state to catch the exact crossover moment:
void OnTick()
{
static datetime lastBarTime = 0;
if (Time[0] == lastBarTime) return;
lastBarTime = Time[0];
// Calculate EMAs on the PREVIOUS completed bar (index 1)
// Index 0 = current (still forming) bar, Index 1 = last completed bar
double emaFastNow = iMA(NULL, 0, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE, 1);
double emaSlowNow = iMA(NULL, 0, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE, 1);
double emaFastPrev = iMA(NULL, 0, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE, 2);
double emaSlowPrev = iMA(NULL, 0, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE, 2);
// Detect crossover: fast was below slow, now it's above
bool bullishCross = (emaFastPrev <= emaSlowPrev) && (emaFastNow > emaSlowNow);
bool bearishCross = (emaFastPrev >= emaSlowPrev) && (emaFastNow < emaSlowNow);
// Only trade if we don't already have a position
if (CountMyOrders() == 0)
{
if (bullishCross)
OpenTrade(OP_BUY);
else if (bearishCross)
OpenTrade(OP_SELL);
}
}Important: Use bar index 1, the completed bar. Never index 0. Bar zero is still forming. Its values change with every tick. If you generate signals off bar 0, congratulations, you've built a repainting EA that looks amazing in backtests and falls apart live. We've reviewed hundreds of EAs with this exact bug.
Step 4: Position Sizing — Risk-Based Lot Calculation
If you're hardcoding lots = 0.1 — stop. Right now. Fixed lot sizing is the fastest way to either blow a small account or waste a large one. The right approach? Let the math figure out your lot size based on how much you're willing to lose if the trade goes wrong:
//+------------------------------------------------------------------+
//| Calculate lot size based on risk percentage and stop distance |
//+------------------------------------------------------------------+
double CalculateLotSize(double stopDistancePoints)
{
if (stopDistancePoints <= 0) return MarketInfo(Symbol(), MODE_MINLOT);
// How much money are we willing to risk?
double riskMoney = AccountBalance() * RiskPercent / 100.0;
// Value of 1 point movement for 1 lot
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
if (tickValue <= 0) tickValue = 1.0;
// Lot size = risk money / (stop distance × tick value)
double lots = riskMoney / (stopDistancePoints * tickValue);
// Round down to broker's lot step
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
lots = MathFloor(lots / lotStep) * lotStep;
// Clamp to broker's min/max
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
if (lots < minLot) lots = minLot;
if (lots > maxLot) lots = maxLot;
return NormalizeDouble(lots, 2);
}The formula: ( \text{Lots} = \frac{\text{AccountBalance} \times \text{RiskPercent} / 100}{\text{StopDistance} \times \text{TickValue}} )
Wide stop in a volatile market? You automatically trade smaller. Tight stop in calm conditions? Bigger position. Either way, the dollar amount you lose on a stopped-out trade is always the same percentage of your account. This is how professionals size positions. Everything else is gambling with extra steps.
Step 5: Opening a Trade — The Complete Function
//+------------------------------------------------------------------+
//| Open a trade with ATR-based stop loss and risk-based position size |
//+------------------------------------------------------------------+
void OpenTrade(int orderType)
{
// Calculate ATR for dynamic stop loss
double atr = iATR(NULL, 0, ATR_Period, 1);
if (atr <= 0) return; // No valid ATR data
double slDistance = atr * ATR_SL_Mult; // Stop loss distance in price
double slPoints = slDistance / Point; // Convert to points for lot calc
double tpDistance = slDistance * TP_Ratio; // Take profit distance
double sl, tp, entryPrice;
if (orderType == OP_BUY)
{
entryPrice = Ask;
sl = entryPrice - slDistance;
tp = entryPrice + tpDistance;
}
else // OP_SELL
{
entryPrice = Bid;
sl = entryPrice + slDistance;
tp = entryPrice - tpDistance;
}
// Normalize to broker's digit precision
sl = NormalizeDouble(sl, Digits);
tp = NormalizeDouble(tp, Digits);
// Calculate position size
double lots = CalculateLotSize(slPoints);
// Send the order
int ticket = OrderSend(
Symbol(), // Current chart symbol
orderType, // OP_BUY or OP_SELL
lots, // Position size
entryPrice, // Entry price (Ask for buy, Bid for sell)
Slippage, // Max slippage allowed
sl, // Stop loss price
tp, // Take profit price
"MyFirstEA", // Comment (visible in trade history)
MagicNumber, // Magic number to identify our orders
0, // Expiration (0 = no expiry)
orderType == OP_BUY ? clrBlue : clrRed // Arrow color on chart
);
if (ticket > 0)
Print("Order opened: #", ticket, " | Type: ",
(orderType == OP_BUY ? "BUY" : "SELL"),
" | Lots: ", lots, " | SL: ", sl, " | TP: ", tp);
else
Print("OrderSend FAILED! Error: ", GetLastError());
}Critical details:
- Buys execute at Ask, sells at Bid. Mix these up and your EA will silently open trades at the wrong price.
NormalizeDouble()every single price level — SL, TP, entry. Brokers are picky about decimal precision. Skip this andOrderSend()will reject your order with a cryptic error.- Always check the ticket returned by
OrderSend(). A value less than 1 means failure. PrintGetLastError()or you'll be guessing in the dark.
Step 6: Counting Our Orders
Before firing off a new trade, you need to check whether you already have one open. Skip this and your EA will happily stack positions on every signal — a fast track to blowing your account:
//+------------------------------------------------------------------+
//| Count open orders belonging to this EA on this symbol |
//+------------------------------------------------------------------+
int CountMyOrders()
{
int count = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != MagicNumber) continue;
if (OrderType() == OP_BUY || OrderType() == OP_SELL)
count++;
}
return count;
}Why MagicNumber matters: Picture this: you've got two EAs on EURUSD. One's a scalper, one's a swing trader. Without MagicNumber, the scalper's CountMyOrders() sees the swing trade and thinks "we're already in a position" — so it never trades. Or worse, CloseAllMyOrders() wipes out both. The magic number is the only thing keeping them in their own lanes.
Step 7: Closing Orders
At some point you'll need to kill all open positions — maybe an opposite signal fires, maybe you've hit a drawdown limit. Here's the function we use in practically every EA:
//+------------------------------------------------------------------+
//| Close all orders for this EA on this symbol |
//+------------------------------------------------------------------+
void CloseAllMyOrders()
{
// ALWAYS loop backward — closing orders shifts the index of remaining ones
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol()) continue;
if (OrderMagicNumber() != MagicNumber) continue;
bool result = false;
switch (OrderType())
{
case OP_BUY:
result = OrderClose(OrderTicket(), OrderLots(),
MarketInfo(Symbol(), MODE_BID), Slippage, clrRed);
break;
case OP_SELL:
result = OrderClose(OrderTicket(), OrderLots(),
MarketInfo(Symbol(), MODE_ASK), Slippage, clrRed);
break;
case OP_BUYLIMIT:
case OP_BUYSTOP:
case OP_SELLLIMIT:
case OP_SELLSTOP:
result = OrderDelete(OrderTicket());
break;
}
if (!result)
Print("Failed to close order #", OrderTicket(), " Error: ", GetLastError());
}
}The backward loop rule: If we had a dollar for every time we've fixed this bug in someone else's code, we'd retire. Loop forward through orders, close the one at index 2, and now index 3 has shifted down to index 2 — you just skipped an order. The fix is dead simple: loop backward. OrdersTotal()-1 down to 0. Tattoo it on your forearm if you have to.
The Complete EA — All Together
Here's the entire EA in one shot — copy it into MetaEditor, hit F7, and you should get zero errors:
//+------------------------------------------------------------------+
//| MyFirstEA.mq4 |
//| EMA Crossover with ATR Stop Loss and Risk-Based Sizing |
//| https://www.quantumcona.com — Algo Trading Masterclass |
//+------------------------------------------------------------------+
#property copyright "Quantumcona"
#property link "https://www.quantumcona.com"
#property strict
input double RiskPercent = 1.0;
input int EMA_Fast = 20;
input int EMA_Slow = 50;
input int ATR_Period = 14;
input double ATR_SL_Mult = 1.5;
input double TP_Ratio = 2.0;
input int MagicNumber = 10001;
input int Slippage = 3;
int OnInit()
{
Print("EA started on ", Symbol());
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
Print("EA stopped. Code: ", reason);
}
void OnTick()
{
static datetime lastBarTime = 0;
if (Time[0] == lastBarTime) return;
lastBarTime = Time[0];
double emaFastNow = iMA(NULL, 0, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE, 1);
double emaSlowNow = iMA(NULL, 0, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE, 1);
double emaFastPrev = iMA(NULL, 0, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE, 2);
double emaSlowPrev = iMA(NULL, 0, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE, 2);
bool bullishCross = (emaFastPrev <= emaSlowPrev) && (emaFastNow > emaSlowNow);
bool bearishCross = (emaFastPrev >= emaSlowPrev) && (emaFastNow < emaSlowNow);
if (CountMyOrders() == 0)
{
if (bullishCross) OpenTrade(OP_BUY);
else if (bearishCross) OpenTrade(OP_SELL);
}
}
double CalculateLotSize(double stopDistancePoints)
{
if (stopDistancePoints <= 0) return MarketInfo(Symbol(), MODE_MINLOT);
double riskMoney = AccountBalance() * RiskPercent / 100.0;
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
if (tickValue <= 0) tickValue = 1.0;
double lots = riskMoney / (stopDistancePoints * tickValue);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
lots = MathFloor(lots / lotStep) * lotStep;
lots = MathMax(lots, MarketInfo(Symbol(), MODE_MINLOT));
lots = MathMin(lots, MarketInfo(Symbol(), MODE_MAXLOT));
return NormalizeDouble(lots, 2);
}
void OpenTrade(int orderType)
{
double atr = iATR(NULL, 0, ATR_Period, 1);
if (atr <= 0) return;
double slDist = atr * ATR_SL_Mult;
double tpDist = slDist * TP_Ratio;
double sl, tp, price;
if (orderType == OP_BUY)
{ price = Ask; sl = price - slDist; tp = price + tpDist; }
else
{ price = Bid; sl = price + slDist; tp = price - tpDist; }
sl = NormalizeDouble(sl, Digits);
tp = NormalizeDouble(tp, Digits);
double lots = CalculateLotSize(slDist / Point);
int ticket = OrderSend(Symbol(), orderType, lots, price, Slippage,
sl, tp, "MyFirstEA", MagicNumber, 0,
orderType == OP_BUY ? clrBlue : clrRed);
if (ticket > 0) Print("Opened #", ticket);
else Print("Error: ", GetLastError());
}
int CountMyOrders()
{
int count = 0;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber) continue;
if (OrderType() == OP_BUY || OrderType() == OP_SELL) count++;
}
return count;
}
void CloseAllMyOrders()
{
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber) continue;
if (OrderType() == OP_BUY)
OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrRed);
else if (OrderType() == OP_SELL)
OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrRed);
}
}Testing Your EA
Do not — we repeat, do NOT — slap this on a live chart and walk away. Backtest first. Always.
- Compile — F7 in MetaEditor. If strict mode catches something, fix it before you do anything else.
- Open Strategy Tester — Ctrl+R in MT4.
- Configure:
- Expert Advisor: MyFirstEA
- Symbol: EURUSD (or whatever you fancy)
- Period: H1 (solid starting timeframe, not too noisy)
- Model: "Every tick" for accuracy, "Open prices only" if you're impatient
- Date range: At least 2 years — anything less and you're fooling yourself
- Run — Hit Start, go get coffee, come back and face the results honestly
Reading the Results
Don't just look at the bottom line profit. That number lies. Here's what actually matters:
- Profit Factor > 1.5 — Below that, your edge is paper-thin and probably won't survive real spreads and slippage
- Max Drawdown < 20% — Higher than that and you WILL panic-close the EA during a losing streak. Trust us.
- Total Trades > 100 — Fewer trades means your results are basically random
- Equity Curve — Should look like a bumpy escalator going up, not a heart monitor
Common Beginner Mistakes
| Mistake | Fix |
|---|---|
| Using Ask to close a buy | Buys close at Bid: OrderClose(ticket, lots, Bid, ...) |
| Looping forward through orders | Always loop backward: for(i=OrdersTotal()-1; i>=0; i--) |
| Not using MagicNumber | Always tag orders so multiple EAs coexist |
| Trading on bar 0 signals | Use bar index 1 — bar 0 hasn't closed yet |
| Fixed lot size | Use risk-based sizing — adapts to account size and volatility |
No #property strict | Always use it — catches bugs at compile time |
What's Next
You've got a working EA. Not a profitable one — let's be honest, a vanilla EMA crossover isn't going to retire you — but a solid skeleton you can build real strategies on top of. Here's where we'd go next:
- Risk Management Safeguards → — Drawdown limits, daily trade caps, spread filters. The stuff that keeps your EA from going rogue.
- Order Management Patterns → — Pending orders, OCO logic, lot splitting. Where EA development gets genuinely interesting.
- Backtesting Deep Dive → — Because most people read Strategy Tester reports wrong.
This tutorial gets you from zero to "hey, my EA actually runs." That's the easy part. The hard part? Proper error recovery, multi-timeframe confirmation, session management, drawdown circuit breakers — the hundred things that separate a demo toy from a bot you'd trust with real money. Our team at Quantumcona has built over 100 production EAs across every strategy type you can think of.
This is Article 15 of 20 in the Algo Trading Masterclass by Quantumcona.


