How to convert a TradingView Pine strategy signal into an MQL5 EA
Porting a Pine strategy to MQL5 looks like a translation exercise. It is not. Pine describes *intent* — "go long when this condition was true on a closed bar" — while MQL5 asks you to specify *mechanics*: which tick, which order type, which filling mode, which stop level. Most of the bugs people hit come from that gap, not from getting the maths wrong.
The two execution models are not the same
Pine strategies run once per bar by default (calc_on_every_tick = false), calculate on the closed bar for the historical series, then fire the broker emulator. MQL5 EAs run on every tick, and nothing stops you from trading mid-bar unless you write the guard yourself.
Pine also hides a great deal: strategy.entry() picks volume, order type, and position accounting for you. strategy.exit() maintains the stop and target legs. In MQL5 you own all of that, including the fact that a broker's SYMBOL_TRADE_STOPS_LEVEL may reject your stop outright.
The good news is that Pine's default bar-close behaviour maps cleanly onto a "new bar" check in MQL5. Do that first and your port is already honest.
What maps to what
| Pine | MQL5 |
|---|---|
| ta.sma(close, n) | iMA() handle, then CopyBuffer() |
| close[1] | CopyBuffer() with ArraySetAsSeries(arr, true) |
| strategy.entry("Long", strategy.long) | CTrade::Buy() or OrderSend() |
| strategy.close() | CTrade::PositionClose() |
| strategy.exit(stop=, limit=) | SL/TP arguments on entry, or PositionModify() |
| barstate.isconfirmed | SERIES_LASTBAR_DATE comparison |
| strategy.position_size | PositionSelect() + POSITION_VOLUME |
Before: the Pine version
//@version=5
strategy("SMA Cross", overlay=true)
fast = ta.sma(close, 20)
slow = ta.sma(close, 50)
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
strategy.entry("Short", strategy.short)
Short, readable, and completely dependent on the emulator for sizing and fills.
After: the MQL5 version
#property version "1.00"
#include <Trade\Trade.mqh>
CTrade trade;
input int InpFast = 20;
input int InpSlow = 50;
input double InpLots = 0.10;
input int InpSLPoints = 400;
input int InpTPPoints = 600;
input ulong InpMagic = 20240501;
int hFast = INVALID_HANDLE;
int hSlow = INVALID_HANDLE;
int OnInit()
{
hFast = iMA(_Symbol, PERIOD_CURRENT, InpFast, 0, MODE_SMA, PRICE_CLOSE);
hSlow = iMA(_Symbol, PERIOD_CURRENT, InpSlow, 0, MODE_SMA, PRICE_CLOSE);
if(hFast == INVALID_HANDLE || hSlow == INVALID_HANDLE)
return(INIT_FAILED);
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(10);
trade.SetTypeFillingBySymbol(_Symbol);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(hFast != INVALID_HANDLE) IndicatorRelease(hFast);
if(hSlow != INVALID_HANDLE) IndicatorRelease(hSlow);
}
bool HasPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetTicket(i) == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == InpMagic)
return(true);
}
return(false);
}
void OnTick()
{
static datetime lastBar = 0;
datetime barTime = (datetime)SeriesInfoInteger(_Symbol, PERIOD_CURRENT,
SERIES_LASTBAR_DATE);
if(barTime == lastBar)
return;
lastBar = barTime;
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(hFast, 0, 0, 3, fast) < 3) return;
if(CopyBuffer(hSlow, 0, 0, 3, slow) < 3) return;
// [1] is the bar that just closed, [2] the one before it
bool crossUp = (fast[2] <= slow[2] && fast[1] > slow[1]);
bool crossDown = (fast[2] >= slow[2] && fast[1] < slow[1]);
if(!crossUp && !crossDown) return;
if(HasPosition()) return;
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double sl = 0.0, tp = 0.0;
if(crossUp)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
sl = (InpSLPoints > 0) ? ask - InpSLPoints * point : 0.0;
tp = (InpTPPoints > 0) ? ask + InpTPPoints * point : 0.0;
trade.Buy(InpLots, _Symbol, 0.0, sl, tp, "SMA cross up");
}
else
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
sl = (InpSLPoints > 0) ? bid + InpSLPoints * point : 0.0;
tp = (InpTPPoints > 0) ? bid - InpTPPoints * point : 0.0;
trade.Sell(InpLots, _Symbol, 0.0, sl, tp, "SMA cross down");
}
}
Note the indices. Pine's ta.crossover(fast, slow) on bar N is fast[1] <= slow[1] && fast > slow. Once bar N has closed and we are on bar N+1, that bar is [1], so the crossover becomes fast[2] <= slow[2] && fast[1] > slow[1]. Getting this off by one is the single most common porting error and it will make your results look plausible but wrong.
The traps that bite
Volume. strategy.percent_of_equity has no MQL5 twin. You have to compute lots from equity, then round down to SYMBOL_VOLUME_STEP, clamp to SYMBOL_VOLUME_MIN and SYMBOL_VOLUME_MAX, and check OrderCalcMargin before sending. Rounding up on a small account produces rejected orders, not trades.
Stops. InpSLPoints above assumes a point-based stop. Brokers quote minimum distance in points too, but for five-digit symbols a "400 point" stop is 4 pips — check SYMBOL_TRADE_STOPS_LEVEL and return early if InpSLPoints * point is below it.
Position accounting. Hedging accounts let you hold both directions; netting accounts will offset them. HasPosition() handles the common case, but if you want Pine's behaviour exactly, you should close before reversing.
Repainting. Anything built on request.security() with lookahead_on, or indicators that reference the developing bar, will not reproduce in MQL5. If the Pine logic can't survive being evaluated only on closed bars, the port is not worth starting.
Fills. Pine's emulator fills at bar open or close with zero spread and no slippage by default. MQL5 fills at the live ask or bid with spread already applied. Two EAs with identical logic will produce different equity curves because of this alone.
Check the port properly
Once it compiles, run it in the Strategy Tester on real tick data with your actual commission profile — not on "Open prices only", which flatters bar-close systems and hides the spread cost. A sensible benchmark is a simple, well-tested EA on the same symbol and timeframe; [see a real backtest](https://mql.ranartech.com/r/UuB1iF34aId9) of a EURUSD H1 EA (+$23, 26% win rate, 185 trades) to get a feel for what a small, honest edge looks like after costs.
If your port cannot beat a plain SMA cross on the same data, the problem was probably never the language.
Build & backtest your own EA free →