Convert an MQL5 Indicator to Pine Script v6, Step by Step
By HorizonAI Team · 13 min read · Intermediate
How to Convert an MQL5 Indicator to Pine Script v6
You have a custom MT5 indicator that shows exactly what you need, but your charting and alert workflow lives in TradingView. Copying its rules is not enough. MQL5 indicator buffers, OnCalculate() indexing, and MT5 alert timing don't translate word-for-word into Pine Script.
Short answer: convert an MQL5 indicator by first identifying its inputs, calculation series, plotted buffers, and signal rules, then rebuilding each piece with Pine v6 inputs, ta.* functions, plot() calls, and alertcondition(). Translate the indicator's logic, not its syntax. An MT5 indicator buffer normally becomes a Pine series, while OnCalculate() is replaced by Pine's automatic execution on every historical bar and price update.
For a moving-average crossover indicator, that means mapping MQL5's two MA handles and two output buffers to two ta.ema() series, mapping CopyBuffer() output to direct series values, and choosing whether alerts trigger intrabar or only after a bar closes. The finished script below preserves the visual crossover and uses confirmed-bar alerts, which is usually the closest match to a closed-candle MT5 signal.
Start by separating an indicator from an MT5 EA
Convert the source only if its job is calculation and display. Custom indicators can plot lines, histograms, arrows, and values in buffers. A Pine indicator can do those same chart-facing tasks.
An Expert Advisor is different. It can inspect positions, send trade requests, manage orders, and react to terminal events. A Pine indicator cannot become an executable MT5 EA, broker connection, or live-trading service simply through conversion. If the MQL5 file contains CTrade, OrderSend, PositionSelect, OnTick(), or risk-based lot calculations, first isolate the indicator portion, such as the signal line or entry marker.
That distinction also explains why an MQL5-to-Pine conversion is not usually a literal line-by-line rewrite. MQL5 is a general terminal language. Pine is purpose-built for chart calculations, visual outputs, alerts, and strategy testing. For a platform-level comparison before you choose a target, see MQL5 vs. Pine Script.
What maps cleanly, and what needs redesign
| MQL5 custom-indicator feature | Pine Script v6 equivalent | Conversion note |
|---|---|---|
input variables | input.int(), input.float(), input.bool() | Preserve defaults, limits, and labels. |
| Indicator buffer | series float | Most Pine calculation results are already series. No buffer registration needed. |
SetIndexBuffer() | Assignment to a series | Pine plots the assigned series directly. |
CopyBuffer() from an MA handle | ta.ema(), ta.sma(), and other ta.* calls | Prefer Pine's native calculation functions. |
DRAW_LINE plot | plot() | Match color, width, and display intent. |
DRAW_ARROW / signal buffer | plotshape() | Use a Boolean signal and a location. |
Alert() | alertcondition() | Decide whether the condition must wait for bar close. |
OnCalculate() | Script's automatic bar-by-bar evaluation | Pine has no user-written calculation event for standard indicators. |
MQL5 documents OnCalculate() as the event handler for custom-indicator calculations, including the rates_total and prev_calculated values used to manage recalculation. It also documents SetIndexBuffer() as the function that binds an array to an indicator buffer. Read the OnCalculate() reference and the SetIndexBuffer() reference when the source uses more than two buffers or mixes calculation and display buffers.
Read the MQL5 source as four separate jobs
Here is a compact MT5 EMA crossover indicator. It has two plotted buffers and alerts when the fast EMA crosses the slow EMA on the most recently closed bar. The handles do the EMA calculation; CopyBuffer() transfers each handle's values into buffers that the chart can draw.
#property copyright "HorizonAI example"
#property version "1.00"
#property indicator_chart_window
#property indicator_plots 2
#property indicator_buffers 2
#property indicator_label1 "Fast EMA"
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrAqua
#property indicator_width1 2
#property indicator_label2 "Slow EMA"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrOrange
#property indicator_width2 2
input int FastPeriod = 9;
input int SlowPeriod = 21;
input bool EnableAlerts = true;
double FastBuffer[];
double SlowBuffer[];
int fastHandle = INVALID_HANDLE;
int slowHandle = INVALID_HANDLE;
datetime lastAlertBar = 0;
int OnInit()
{
SetIndexBuffer(0, FastBuffer, INDICATOR_DATA);
SetIndexBuffer(1, SlowBuffer, INDICATOR_DATA);
ArraySetAsSeries(FastBuffer, true);
ArraySetAsSeries(SlowBuffer, true);
fastHandle = iMA(_Symbol, _Period, FastPeriod, 0, MODE_EMA, PRICE_CLOSE);
slowHandle = iMA(_Symbol, _Period, SlowPeriod, 0, MODE_EMA, PRICE_CLOSE);
if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
return(INIT_FAILED);
IndicatorSetString(INDICATOR_SHORTNAME, "EMA Cross (" +
IntegerToString(FastPeriod) + "/" +
IntegerToString(SlowPeriod) + ")");
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
if(fastHandle != INVALID_HANDLE)
IndicatorRelease(fastHandle);
if(slowHandle != INVALID_HANDLE)
IndicatorRelease(slowHandle);
}
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total < SlowPeriod + 2)
return(0);
if(BarsCalculated(fastHandle) < rates_total || BarsCalculated(slowHandle) < rates_total)
return(0);
if(CopyBuffer(fastHandle, 0, 0, rates_total, FastBuffer) <= 0)
return(0);
if(CopyBuffer(slowHandle, 0, 0, rates_total, SlowBuffer) <= 0)
return(0);
// Buffer index 0 is the still-forming bar. Evaluate indices 1 and 2 only.
if(EnableAlerts && time[0] != lastAlertBar)
{
bool bullishCross = FastBuffer[1] > SlowBuffer[1] && FastBuffer[2] <= SlowBuffer[2];
bool bearishCross = FastBuffer[1] < SlowBuffer[1] && FastBuffer[2] >= SlowBuffer[2];
if(bullishCross)
Alert(_Symbol, " ", EnumToString(_Period), ": bullish EMA crossover closed");
if(bearishCross)
Alert(_Symbol, " ", EnumToString(_Period), ": bearish EMA crossover closed");
lastAlertBar = time[0];
}
return(rates_total);
}
Don't begin by copying OnInit() or OnCalculate() into a Pine editor. Instead, label the four jobs in this source:
- Configuration: Fast period 9, slow period 21, and an alert switch.
- Calculations: Two EMAs, built through
iMA()handles. - Visual output:
FastBufferandSlowBufferare line plots. - Signal timing: compare buffer values at
[1]and[2], never the active bar at[0].
That inventory becomes the Pine script's blueprint. The handle creation, buffer binding, terminal cleanup, and copying disappear because Pine owns the calculation lifecycle. The input values, EMA formulas, line styles, and crossover conditions remain.
Map MQL5 series indexing before you touch the signal
The most expensive conversion error is reversing the bars. With a timeseries array in MQL5, index 0 is the current, still-forming bar; index 1 is the last closed bar. This example explicitly calls ArraySetAsSeries() on the buffers so its access pattern is clear.
Pine uses the same useful convention for a historical series: close is the active bar, close[1] is one bar ago, and close[2] is two bars ago. So the raw index translation is straightforward:
| Intent | MQL5 timeseries buffer | Pine series |
|---|---|---|
| Current bar | FastBuffer[0] | fastEma or fastEma[0] |
| Last closed bar | FastBuffer[1] | fastEma[1] |
| Two bars back | FastBuffer[2] | fastEma[2] |
The trap is timing, not syntax. The MQL5 example evaluates [1] and [2] once a new bar exists, so it reports a signal already confirmed by the bar close. ta.crossover(fastEma, slowEma) in Pine can become true while the real-time bar is still changing. To preserve closed-bar behavior, gate the crossover with barstate.isconfirmed.
TradingView's bar-states documentation explains why real-time bars need this extra decision. If you're converting a visual indicator that appears to change after reload, also read why Pine Script repaints and how to fix it before treating an intrabar condition as a completed signal.
Build the Pine v6 version step by step
Create a new TradingView indicator, then add the declaration and inputs first. overlay = true puts the MA lines on the price chart, which corresponds to the MQL5 indicator's indicator_chart_window property.
//@version=6
indicator("EMA Crossover, MT5 Conversion", overlay = true)
fastLength = input.int(9, "Fast EMA Length", minval = 1)
slowLength = input.int(21, "Slow EMA Length", minval = 2)
showSignals = input.bool(true, "Show Crossover Markers")
// A hidden plot keeps this standalone teaching fragment valid as an indicator.
plot(na, title = "Hidden Setup Plot", display = display.none)
Next, replace the MQL5 MA handles and copied buffers with direct Pine series. There is no equivalent of SetIndexBuffer() because fastEma and slowEma retain one value for every chart bar automatically.
//@version=6
indicator("EMA Crossover, MT5 Conversion", overlay = true)
fastLength = input.int(9, "Fast EMA Length", minval = 1)
slowLength = input.int(21, "Slow EMA Length", minval = 2)
fastEma = ta.ema(close, fastLength)
slowEma = ta.ema(close, slowLength)
plot(fastEma, "Fast EMA", color = color.aqua, linewidth = 2)
plot(slowEma, "Slow EMA", color = color.orange, linewidth = 2)
Finally, translate the alert logic. ta.crossover() expresses the same relationship as FastBuffer[1] > SlowBuffer[1] && FastBuffer[2] <= SlowBuffer[2], while barstate.isconfirmed delays the result until the bar closes. The shape markers aren't required for alerts, but they give you an immediate visual way to compare both platforms.
//@version=6
indicator("EMA Crossover, MT5 Conversion", overlay = true)
fastLength = input.int(9, "Fast EMA Length", minval = 1)
slowLength = input.int(21, "Slow EMA Length", minval = 2)
showSignals = input.bool(true, "Show Crossover Markers")
fastEma = ta.ema(close, fastLength)
slowEma = ta.ema(close, slowLength)
bullishCross = ta.crossover(fastEma, slowEma) and barstate.isconfirmed
bearishCross = ta.crossunder(fastEma, slowEma) and barstate.isconfirmed
plotshape(showSignals and bullishCross, title = "Bullish Cross", style = shape.triangleup,
location = location.belowbar, color = color.lime, size = size.tiny, text = "BUY")
plotshape(showSignals and bearishCross, title = "Bearish Cross", style = shape.triangledown,
location = location.abovebar, color = color.red, size = size.tiny, text = "SELL")
alertcondition(bullishCross, title = "Bullish EMA Crossover",
message = "Fast EMA crossed above slow EMA on the confirmed bar.")
alertcondition(bearishCross, title = "Bearish EMA Crossover",
message = "Fast EMA crossed below slow EMA on the confirmed bar.")
TradingView requires an alertcondition() call in an indicator to expose that condition in the Create Alert dialog. Its alerts documentation also distinguishes alert conditions from programmatic alert() calls. For this conversion, alertcondition() is the clearer choice because traders can choose the TradingView alert delivery options themselves.
Use this complete Pine Script v6 indicator
Paste this full version into the Pine Editor and add it to a chart. Set its symbol, timeframe, EMA lengths, and chart timezone to comparable settings before judging whether it matches the MT5 version.
//@version=6
indicator("EMA Crossover, MT5 Conversion", overlay = true, max_labels_count = 500)
// Inputs carried over from the MQL5 indicator.
fastLength = input.int(9, "Fast EMA Length", minval = 1)
slowLength = input.int(21, "Slow EMA Length", minval = 2)
showSignals = input.bool(true, "Show Crossover Markers")
colorBars = input.bool(false, "Color Bars After Confirmed Cross")
// MQL5 MA handles + output buffers become Pine series.
fastEma = ta.ema(close, fastLength)
slowEma = ta.ema(close, slowLength)
// Equivalent of two DRAW_LINE indicator plots.
plot(fastEma, "Fast EMA", color = color.aqua, linewidth = 2)
plot(slowEma, "Slow EMA", color = color.orange, linewidth = 2)
// Equivalent of comparing MQL5 buffers [1] and [2] after a candle has closed.
bullishCross = ta.crossover(fastEma, slowEma) and barstate.isconfirmed
bearishCross = ta.crossunder(fastEma, slowEma) and barstate.isconfirmed
plotshape(showSignals and bullishCross, title = "Bullish Cross", style = shape.triangleup,
location = location.belowbar, color = color.lime, size = size.tiny, text = "BUY")
plotshape(showSignals and bearishCross, title = "Bearish Cross", style = shape.triangledown,
location = location.abovebar, color = color.red, size = size.tiny, text = "SELL")
barcolor(colorBars ? bullishCross ? color.new(color.lime, 65) : bearishCross ? color.new(color.red, 65) : na : na)
alertcondition(bullishCross, title = "Bullish EMA Crossover",
message = "Fast EMA crossed above slow EMA on the confirmed bar.")
alertcondition(bearishCross, title = "Bearish EMA Crossover",
message = "Fast EMA crossed below slow EMA on the confirmed bar.")
This is an indicator, not a backtestable strategy. Keep it that way until the line values and crossover markers agree with your MT5 chart. If you later want testable entries, exits, commission, and slippage assumptions, convert the signal deliberately into a strategy() script. The simple moving-average crossover strategy guide shows the strategy-specific pieces that an indicator doesn't need.
Validate the conversion bar by bar
Visual similarity isn't proof of parity. A chart can look correct while its lines are shifted by one bar, sourced from different prices, or calculated on different sessions.
Use this validation sequence:
- Lock the environment. Open the same liquid symbol and timeframe in MT5 and TradingView. Use the same 9/21 EMA inputs. A broker-specific CFD feed and a TradingView exchange feed may not print identical candles, so start with major session hours and compare the direction of values rather than expecting every tick to match.
- Verify the source. This conversion uses
PRICE_CLOSEin MQL5 andclosein Pine. If your source callsiMA()withPRICE_MEDIAN, usehl2in Pine.PRICE_TYPICALnormally maps tohlc3. - Compare warm-up bars. An EMA needs prior observations. Scroll far enough left that both platforms have hundreds of earlier bars available, then compare the latest 20 closed bars.
- Check crossover timestamps. Mark the candle where the 9 EMA moves from below the 21 EMA to above it. The marker should sit on the same completed candle, not the following candle.
- Test alert timing. In TradingView's alert dialog, select the condition from the converted indicator and use a once-per-bar-close frequency if you want the operational behavior to match the code's confirmed-bar rule.
For multi-timeframe indicators, validate the base timeframe first. An MQL5 call such as iMA(_Symbol, PERIOD_H1, ...) asks the terminal for a second timeframe. Pine's equivalent usually requires request.security(), which has gaps and lookahead choices that must be specified. Don't insert request.security() until the single-timeframe conversion agrees. A multi-timeframe conversion is a separate design decision, not a search-and-replace job.
Common conversion mistakes that shift or repaint signals
❌ Mistake: converting an MQL5 closed-bar signal to a raw Pine crossover. ta.crossover(fastEma, slowEma) may flash during the live candle and disappear before it closes.
✅ Do this: Add and barstate.isconfirmed when the MQL5 code evaluates buffer indices [1] and [2]. Keep the alert configured for bar close.
❌ Mistake: treating MQL5 handles as Pine variables. Code copied from iMA() and CopyBuffer() can't be made Pine-compatible by changing semicolons or types.
✅ Do this: Replace a handle-plus-buffer pair with the native series function, such as ta.ema(close, 9). Pine already stores the historical series required by plot() and crossover functions.
❌ Mistake: forgetting the applied price. A 21-period EMA of close won't match a 21-period EMA of hlc3, even though the lengths match.
✅ Do this: Find the final argument of MQL5's iMA() call. Translate PRICE_OPEN to open, PRICE_HIGH to high, PRICE_LOW to low, PRICE_MEDIAN to hl2, and PRICE_TYPICAL to hlc3.
❌ Mistake: trying to convert drawing objects into plots without checking their purpose. MQL5 chart objects can be individually named, moved, and deleted; a Pine plotshape() redraws from series logic instead.
✅ Do this: For a repeated signal on every bar, use plotshape(). For persistent zones or limited annotations, use Pine lines, boxes, or labels and define their lifecycle explicitly.
❌ Mistake: calling the conversion complete because both scripts show two lines. Different data feeds, session rules, and MTF logic can hide a real mismatch.
✅ Do this: Compare the last 20 fully closed bars first, then record three crossover timestamps. Fix source, indexing, and timeframe differences before styling the indicator.
Pro tips for harder MQL5 indicators
Turn each buffer into a named Pine series before plotting. An MQL5 indicator may contain eight buffers but only three visible plots. Write a Pine series for every meaningful intermediate calculation, then decide which ones deserve a plot(). This makes debugging far easier than compressing the formula into one line.
Use na for an MQL5 EMPTY_VALUE-style gap. If an MT5 arrow buffer intentionally has no value on most bars, make the Pine plot value na when the event is absent. For example, plotshape(bullishCross) is naturally sparse because its first argument is Boolean.
Preserve the signal contract before improving it. First match the original 9/21 EMA inputs, close source, confirmed-bar timing, and marker placement. Only then add filters such as a 50 EMA trend filter, RSI 14 confirmation, or session limits. Otherwise, you won't know whether a changed result comes from the conversion or the new logic.
Treat multi-symbol logic as a fresh specification. MQL5 can pull another symbol through its terminal functions; Pine needs a request.security() call per requested series. State the symbol, timeframe, gap behavior, and lookahead rule in writing before coding it.
Generating this conversion without writing it yourself
If you can explain the source indicator's buffers and timing, HorizonAI can generate Pine Script v6 from that description, compile-check it, and let you refine the result in chat. It writes the code; you still add it to TradingView and configure any alerts there.
Convert this MQL5 custom indicator into a Pine Script v6 overlay indicator. It uses a 9-period EMA and a 21-period EMA calculated on close, plots both as 2-pixel lines, and marks a bullish crossover below the bar and bearish crossover above the bar. Match MT5 closed-candle behavior: only confirm a cross when the TradingView bar closes using
barstate.isconfirmed. Addalertcondition()calls for both confirmed signals. Use inputs for fast length, slow length, marker visibility, and optional bar coloring. Do not make it a strategy.
For an indicator with several buffers, paste the MQL5 source and add a second instruction: identify every visible buffer, every calculation-only buffer, the applied price, and any higher-timeframe call before generating the Pine version. HorizonAI can also convert scripts between Pine Script, MQL5, and NinjaScript in either direction, so you can keep the conversion discussion attached to the actual code.
FAQs
Can every MQL5 indicator be converted to Pine Script v6?
Most chart calculations, plots, and rule-based alerts can be recreated, but platform-specific terminal features may need redesign. Multi-symbol data, chart objects, custom DLL calls, and trading functions are not one-to-one Pine features.
Why is my converted Pine indicator one candle different from MT5?
Check whether the MQL5 signal uses the current buffer index [0] or closed-bar indices [1] and [2]. Then match Pine's timing with barstate.isconfirmed if the original waits for a completed candle.
Does CopyBuffer() have a direct Pine equivalent?
No. In Pine, a function such as ta.ema(close, 9) returns a historical series directly, so it replaces both the MQL5 calculation handle and the copied output buffer for most standard indicators.
Should I convert an MQL5 EA into a Pine indicator?
Only convert the EA's visual or signal logic into an indicator. Order placement, position management, and MT5 trade operations need a separate redesign and don't map to an indicator.
Final thoughts
A reliable MQL5-to-Pine conversion preserves four things: the source price, the lookback settings, the plot outputs, and the exact moment a signal becomes valid. The surrounding platform plumbing is different, so trying to preserve every function name creates brittle code.
Start with a single timeframe and compare 20 closed bars before adding filters, labels, or higher-timeframe requests. That one discipline catches most off-by-one and repainting mistakes while the script is still easy to audit.
Related articles
- How to Convert Pine Script to MQL5: A Manual Translation Guide — See the reverse workflow and how Pine series become MT5 calculations.
- MQL5 vs Pine Script: Which Trading Language Should You Learn? — Choose the right platform for the type of tool you want to build.
- Pine Script Repainting: Why It Happens and How to Fix It — Control real-time bar behavior and confirmed signals.
- Why Pine Script Alerts Fail, and the Fixes That Work — Configure and debug your converted crossover alerts.
- Simple Moving Average Crossover Strategy: Complete Guide — Turn a crossover concept into a structured strategy workflow.
- How to Code a Supertrend Indicator and an EA in MQL5 — Study indicator buffers and MT5 implementation patterns.
- Build a WaveTrend Oscillator in Pine Script v6 — Build a more involved Pine indicator with visual outputs.
- Pine Script Tutorial for Beginners: Build Your First TradingView Strategy in 10 Minutes — Get comfortable editing and running Pine code in TradingView.
Questions about converting MQL5 indicators to Pine Script v6? Join our Discord to discuss with other traders!
