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
.mq5files go inMQL5/Expertsfor EAs orMQL5/Indicatorsfor indicators. - The
CTradeclass 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.
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
- Pine Script vs MQL5 — Which Programming Language Should You Learn? — the decision this guide assumes you already made
- MQL5 vs Pine Script — Which Trading Language Should You Learn? — a deeper feature-by-feature comparison
- How to Create Your First MetaTrader 5 Indicator — the indicator-side version of this conversion
- How to Create a Trading Bot for MT5 (No Coding Required) — building an EA from a plain-English description
- How to Automate Your Trading Strategy on MetaTrader 5 — deploying and running the EA once it compiles
- Simple Moving Average Crossover Strategy: Complete Guide — the strategy this guide converts in the worked example
- How to Backtest a Trading Strategy (Complete Step-by-Step Guide) — validating the MQL5 version in the Strategy Tester
- 7 Best Pine Script Strategies for TradingView (With Code) — source strategies worth converting next
Questions about converting Pine Script to MQL5? Join our Discord to discuss with other traders!
