How to Convert Pine Script to MQL5: A Manual Translation Guide

How to Convert Pine Script to MQL5: A Manual Translation Guide

By HorizonAI Team · 12 min read · Advanced

You built a strategy in Pine Script, backtested it on TradingView, and it looks good. Now you want it running as an Expert Advisor in MetaTrader 5, where it can manage real orders on your broker's server. So you paste your Pine code into MetaEditor and... nothing. The two languages barely share a keyword.

Converting Pine Script to MQL5 is not a find-and-replace job. It's a concept-by-concept translation between two very different execution models. This guide maps every piece of a Pine strategy to its MQL5 equivalent, walks through a full SMA-crossover conversion end to end, and shows you the pitfalls that silently break a "working" conversion.

The short answer

To convert Pine Script to MQL5, you rebuild the logic against MQL5's event model rather than translating line by line. Pine runs your whole script bar by bar with implicit price series; MQL5 splits the work into OnInit, OnTick, and OnDeinit handlers with explicit indicator handles and manual order management. The mapping is consistent: ta.sma/ta.rsi become iMA/iRSI handles read through CopyBuffer; plot() becomes indicator buffers; strategy.entry/strategy.close become CTrade methods like trade.Buy() and trade.PositionClose(); alert() becomes Alert() or SendNotification().

Here is the fuller version. A Pine strategy() script is a single program re-executed on every historical and live bar, and TradingView handles position accounting, order fills, and equity for you. An MQL5 EA is an event-driven C-like program: MetaTrader calls OnTick() every time the price changes, and you are responsible for detecting new bars, reading indicator values, sizing positions, tagging orders with a magic number, and closing them. Get those five responsibilities right and the strategy behaves the same; miss one and it drifts. The rest of this guide is that mapping in detail, with a complete working example you can compile.

Before you start: what you need

  • MetaTrader 5 with MetaEditor (bundled free with any MT5 install). Your .mq5 files go in MQL5/Experts for EAs or MQL5/Indicators for indicators.
  • The CTrade class from the Standard Library — one #include <Trade\Trade.mqh> line, no extra install.
  • The original Pine script open beside you, and a clear idea of whether you are converting an indicator (draws on the chart, no orders) or a strategy/EA (places trades). The two use different MQL5 entry points, and mixing them up is the first thing that goes wrong.

If you are still deciding whether MQL5 is worth learning at all versus staying in Pine, read Pine Script vs MQL5 — which to learn first. This guide assumes you have already decided to make the jump.

Why the two languages disagree

Pine Script is chart-bound and declarative. Every variable that references close, high, or a ta.* function is secretly a whole time series, and the [] history operator (close[1]) reaches back through it. You never open a file, allocate a buffer, or write a loop over bars. TradingView runs your script once per bar, newest calculations last, and the runtime tracks your simulated positions.

MQL5 is event-driven and imperative, closer to C++. Nothing runs on a loop you can see. MetaTrader fires callbacks:

  • OnInit() — once, when the program loads. Set up indicator handles and configuration here.
  • OnTick() — every incoming tick (price change) for an EA. This is where your trading logic lives.
  • OnCalculate() — every bar for a custom indicator, where you fill buffers.
  • OnDeinit() — once, on removal. Release handles and clean up.

Because OnTick fires on every tick, not every bar, a huge share of conversion bugs come from logic that a Pine author assumed ran once per bar suddenly running dozens of times inside one candle. Keep that difference in mind; it drives half the pitfalls later.

Concept map: Pine to MQL5

Script structure

In Pine, one declaration sets the mode:

//@version=6
strategy("My Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

In MQL5, an EA has no single declaration. It has handlers and #property directives:

#property copyright "Your Name"
#property version   "1.00"

#include <Trade\Trade.mqh>

int OnInit()
{
   return(INIT_SUCCEEDED);
}

void OnTick()
{
   // trading logic runs here
}

void OnDeinit(const int reason)
{
   // cleanup runs here
}

Everything Pine did implicitly at the top of the file now has an explicit home in one of these three functions.

The series model and the history operator

This is the biggest conceptual jump. Pine's close is a series; close[1] is last bar's close, close[2] the bar before. It reads newest-first and it's always available.

MQL5 gives you price data through functions or arrays, and arrays are oldest-first by default. To get Pine-style newest-first indexing you must call ArraySetAsSeries(arr, true):

double closes[];
ArraySetAsSeries(closes, true);        // index 0 = current bar, like Pine
CopyClose(_Symbol, _Period, 0, 3, closes);
double lastClose = closes[1];          // equivalent to Pine's close[1]

Skip the ArraySetAsSeries call and closes[0] becomes the oldest bar in the copied range, not the newest. That single missing line inverts your entire strategy and is the most common silent conversion bug. More on this in the pitfalls section.

Built-in indicators

Pine's ta.* functions return a series directly:

rsiValue = ta.rsi(close, 14)
fastMA   = ta.sma(close, 9)

MQL5 uses a two-step handle pattern. First create a handle in OnInit (once), then copy values from it when you need them:

int rsiHandle;

int OnInit()
{
   rsiHandle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE);
   if(rsiHandle == INVALID_HANDLE) return(INIT_FAILED);
   return(INIT_SUCCEEDED);
}

double GetRSI()
{
   double buf[];
   ArraySetAsSeries(buf, true);
   if(CopyBuffer(rsiHandle, 0, 0, 1, buf) < 1) return(0.0);
   return(buf[0]);
}

The handle is created once because it's expensive; CopyBuffer reads the computed values cheaply on demand. The iMA function documented in the MQL5 reference follows the same pattern, as does every other i* indicator. CopyBuffer's parameters (handle, buffer number, start, count, destination) are worth learning once from the CopyBuffer docs because you will call it constantly.

Plotting

Pine's plot() draws a line on the chart in one call:

plot(fastMA, "Fast SMA", color=color.aqua, linewidth=2)

MQL5 custom indicators declare buffers and bind them:

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
#property indicator_label1  "RSI"
#property indicator_type1   DRAW_LINE

double RsiBuffer[];

int OnInit()
{
   SetIndexBuffer(0, RsiBuffer, INDICATOR_DATA);
   return(INIT_SUCCEEDED);
}

Inside an EA you rarely plot at all. If you need a value on screen for debugging, Comment("RSI: " + DoubleToString(rsiValue, 2)) is the quick equivalent of a Pine plot. Full chart drawings use the ObjectCreate family, which maps loosely to Pine's line.new and label.new.

Order logic

Pine's strategy engine hides order management. You say what you want:

if longCondition
    strategy.entry("Long", strategy.long)

if exitCondition
    strategy.close("Long")

MQL5 makes it explicit through the CTrade class. You place, close, and modify positions yourself, and you check whether a position already exists:

#include <Trade\Trade.mqh>
CTrade trade;

if(longCondition && !PositionSelect(_Symbol))
   trade.Buy(0.10, _Symbol);          // 0.10 lots

if(exitCondition && PositionSelect(_Symbol))
   trade.PositionClose(_Symbol);

CTrade wraps the lower-level OrderSend call and handles most of the request structure for you. The CTrade class reference lists every method, but Buy, Sell, PositionClose, and PositionModify cover most conversions. For stops and targets, pass them into the trade call: trade.Buy(0.10, _Symbol, 0, slPrice, tpPrice).

Alerts and notifications

Pine:

alert("Long signal fired", alert.freq_once_per_bar)

MQL5 has three separate functions depending on where you want the message:

Alert("Long signal fired");                 // popup + sound in the terminal
SendNotification("Long signal fired");      // push to the MT5 mobile app
SendMail("Signal", "Long signal fired");    // email, if configured

There is no single alert() that does everything; pick the channel you actually want.

Full worked example: an SMA crossover

Here is a complete conversion. Start with a standard Pine v6 crossover strategy: go long when the 9-period SMA crosses above the 21-period SMA, close when it crosses back below.

//@version=6
strategy("SMA Crossover", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

fastLen = input.int(9, "Fast SMA Length")
slowLen = input.int(21, "Slow SMA Length")

fastMA = ta.sma(close, fastLen)
slowMA = ta.sma(close, slowLen)

plot(fastMA, "Fast SMA", color=color.aqua, linewidth=2)
plot(slowMA, "Slow SMA", color=color.orange, linewidth=2)

longCondition  = ta.crossover(fastMA, slowMA)
closeCondition = ta.crossunder(fastMA, slowMA)

if longCondition
    strategy.entry("Long", strategy.long)

if closeCondition
    strategy.close("Long")

Now the MQL5 Expert Advisor that does the same thing. Save it as SmaCrossover.mq5 in MQL5/Experts and compile with F7:

//+------------------------------------------------------------------+
//|                                                 SmaCrossover.mq5 |
//+------------------------------------------------------------------+
#property copyright "HorizonAI"
#property version   "1.00"

#include <Trade\Trade.mqh>

input int    FastLength  = 9;
input int    SlowLength  = 21;
input double LotSize     = 0.10;
input long   MagicNumber = 20260717;

CTrade trade;
int    fastHandle;
int    slowHandle;

int OnInit()
{
   fastHandle = iMA(_Symbol, _Period, FastLength, 0, MODE_SMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, SlowLength, 0, MODE_SMA, PRICE_CLOSE);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
      return(INIT_FAILED);

   trade.SetExpertMagicNumber(MagicNumber);
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   IndicatorRelease(fastHandle);
   IndicatorRelease(slowHandle);
}

void OnTick()
{
   // Act once per completed bar, not on every tick
   static datetime lastBarTime = 0;
   datetime currentBarTime = iTime(_Symbol, _Period, 0);
   if(currentBarTime == lastBarTime) return;
   lastBarTime = currentBarTime;

   double fast[], slow[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);

   // Pull the last three values so we can compare closed bars [1] and [2]
   if(CopyBuffer(fastHandle, 0, 0, 3, fast) < 3) return;
   if(CopyBuffer(slowHandle, 0, 0, 3, slow) < 3) return;

   bool crossUp   = fast[2] <= slow[2] && fast[1] > slow[1];   // ta.crossover
   bool crossDown = fast[2] >= slow[2] && fast[1] < slow[1];   // ta.crossunder

   bool hasPosition = PositionSelect(_Symbol);

   if(crossUp && !hasPosition)
      trade.Buy(LotSize, _Symbol);

   if(crossDown && hasPosition)
      trade.PositionClose(_Symbol);
}

Walk the mapping: input.int became input int; ta.sma became two iMA handles created in OnInit; the ta.crossover/ta.crossunder calls became explicit comparisons on the closed bars [1] and [2]; strategy.entry became trade.Buy; strategy.close became trade.PositionClose. The default_qty_type=strategy.percent_of_equity had no direct equivalent, so it became a fixed LotSize input (more on that below).

Notice the crossover uses bars [1] and [2], the two most recently closed bars, never bar [0]. That's deliberate, and it's the difference between a strategy that repaints and one that does not.

The forming-bar problem (and repainting)

Pine strategies that reference close on the current bar can repaint, because the current bar's close keeps changing until the bar finishes. The same trap exists in MQL5, made worse by OnTick firing continuously.

Bar [0] in MQL5 is the current, still-forming candle. Its values change on every tick. If you place trades based on fast[0] crossing slow[0], your EA will fire, reverse, and re-fire many times inside a single candle as the price wobbles, and its live behavior will never match its backtest.

The fix is the pattern in the example above: only evaluate signals once per new bar, and only on closed bars ([1] and [2]). MQL5 actually makes non-repainting logic easier than Pine here, because the new-bar guard is a simple, explicit check rather than a barstate.isconfirmed condition you have to remember. The same wait-for-confirmation discipline shows up on the Pine side when automating a Supertrend strategy with alerts, where firing only on the closed bar is what keeps signals honest.

Common conversion mistakes

❌ Forgetting ArraySetAsSeries. Your MA values load oldest-first, fast[1] points at ancient history, and no crossover ever fires. ✅ Call ArraySetAsSeries(arr, true) on every price and buffer array right after you declare it, so index 0 is the newest bar exactly like Pine.

❌ Trading off the forming bar [0]. The EA looks great in a quick visual test, then live-trades erratically and never matches the backtest. ✅ Gate logic behind a new-bar check and read closed bars [1]/[2].

❌ No magic number. Your EA closes or counts positions opened manually or by another EA on the same symbol, because PositionSelect(_Symbol) does not know who opened the trade. ✅ Set trade.SetExpertMagicNumber() and filter by magic when you manage more than one system per symbol.

❌ Copying percent_of_equity as a fixed lot. Pine's default_qty_value=10 sizes each trade at 10% of equity; a hardcoded 0.10 lot ignores account size and risk entirely. ✅ Compute lots from balance and stop distance (see the next section).

❌ Assuming the backtest carries over. A green Pine backtest tells you nothing about the MQL5 version. Different fill model, different spread handling, different indexing. ✅ Re-run the converted EA in the MT5 Strategy Tester before you trust it.

Pro tips for cleaner conversions

Size positions from risk, not a fixed lot. To reproduce Pine's percentage sizing, calculate lots from account equity and your stop distance:

double RiskLots(double riskPercent, double stopPoints)
{
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskMoney = balance * riskPercent / 100.0;
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double lots      = riskMoney / (stopPoints * tickValue);
   return(NormalizeDouble(lots, 2));
}

Mind 1-based versus 0-based counting in loops. Pine's for i = 0 to length - 1 and MQL5's for(int i = 0; i < length; i++) look similar, but MQL5 arrays and the Copy* functions use a count argument, not an end index. Asking for 3 values gives you indices 0, 1, and 2.

Validate every handle. An iMA that returns INVALID_HANDLE (bad symbol, wrong timeframe) fails silently later. Check it in OnInit and return INIT_FAILED so MetaTrader tells you immediately.

Keep the Pine version as your spec. When the MQL5 numbers disagree with TradingView, the Pine script is your reference implementation. Compare indicator values bar by bar on the same symbol and timeframe to find where they diverge.

Generating the conversion without writing MQL5 yourself

Doing the translation by hand teaches you exactly what each MQL5 line does, which is why this guide walks it in full. When you would rather skip the boilerplate, HorizonAI converts scripts between Pine Script, MQL5, and NinjaScript in any direction, from chat. Paste your Pine strategy in and describe the target:

Convert this Pine Script v6 SMA crossover strategy to an MQL5 Expert Advisor. Use 9 and 21 period SMAs, act only on closed bars to avoid repainting, tag orders with a magic number, and expose fast length, slow length, and lot size as inputs.

You get back a compile-checked .mq5 file. HorizonAI runs it through a real MQL5 compiler, so syntax errors get caught and explained rather than guessed at. From there you can keep editing in the same chat:

Change the fixed lot size to risk-based position sizing at 1% of account balance per trade, using the stop-loss distance.

HorizonAI writes and validates the code. Running it, backtesting it in the MT5 Strategy Tester, and connecting it to your broker stay in your hands. The tool generates the script; it never places the trades.

Try it free →

FAQs

Can I convert a Pine indicator, not just a strategy?

Yes, and it's usually simpler. An indicator has no order logic to translate. You map plot() calls to indicator buffers with SetIndexBuffer, put the calculation in OnCalculate instead of OnTick, and use #property indicator_separate_window or indicator_chart_window to place it. See creating your first MT5 indicator for a from-scratch walkthrough.

Why does my converted EA behave differently in the Strategy Tester?

Almost always one of three things: a missing ArraySetAsSeries inverting your array, logic reading the forming bar [0] instead of closed bars, or lot sizing that no longer matches Pine's percent-of-equity. Check those three before anything else.

Do Pine's request.security multi-timeframe calls translate?

Conceptually yes. Instead of request.security, you create a second indicator handle or copy rates with an explicit timeframe argument (for example iMA(_Symbol, PERIOD_H1, ...) while running on M15). There is no lookahead flag to worry about because you always read closed higher-timeframe bars.

Is the converted code ready to trade live?

No conversion is ready to trade live on faith. Compile it, run it in the MT5 Strategy Tester across enough history, then forward-test on a demo account. The language changed; your risk process should not.

Final thoughts

Converting Pine to MQL5 is really about internalizing one shift: from a chart-bound script that runs top to bottom every bar to an event-driven program where you own the indicator handles, the bar timing, and the orders. Once the concept map clicks, most conversions become mechanical.

One tip to leave with: build the new-bar guard first, before any indicator or trade logic. It's the single change that makes an MQL5 EA match its Pine backtest, and retrofitting it later means re-testing everything.

Related articles

Questions about converting Pine Script to MQL5? Join our Discord to discuss with other traders!