◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / How to Convert a TradingView Strategy into a MetaTrader EA
MetaTrader

How to Convert a TradingView Strategy into a MetaTrader EA

This guide walks through what it actually takes to convert a TradingView Pine strategy into a MetaTrader Expert Advisor, focusing on the semantic gap between Pine's declarative bar-series model and MQL's event-driven OnTick loop. It is educational only and not financial advice; nothing here implies any strategy is profitable. Trading involves a real risk of loss, so always test any converted EA on a demo account first before considering live use.

Why "Convert TradingView to MetaTrader" Is Harder Than It Looks

When traders ask how to convert TradingView to MetaTrader, they usually picture a line-by-line translation: replace ta.crossover() with a MetaTrader equivalent, swap strategy.entry() for an order call, and ship it. In practice the two platforms model time and price in fundamentally different ways, and that mismatch is where most converted EAs quietly go wrong.

Pine Script is declarative and series-based. Your script conceptually runs once per bar across the whole history, and variables like close, high, or a moving average are series — arrays indexed by bar. TradingView's Strategy Tester evaluates your logic on each historical bar and, by default, fills orders on the open of the next bar. The engine handles the bookkeeping for you: position state, order sequencing, and the fact that a closed bar's values never change.

MQL (both MQL4 and MQL5) is event-driven and imperative. Your OnTick() function fires on every incoming tick — potentially dozens of times inside a single one-minute candle. There is no built-in "run once per bar," no automatic next-bar fill, and no framework tracking whether you are already in a position. You have to reconstruct all of that by hand. So the real task is not translating syntax; it is re-implementing Pine's execution model faithfully inside a tick loop. Miss that, and the EA can look identical in code while behaving nothing like the backtest you approved.

The Semantic Gap: Bars, Ticks, and the Forming Candle

The single most important idea to internalise is the difference between a closed bar and the forming bar. In Pine, when you reference close inside your condition, on a historical bar that value is final — the candle has closed and will never change. On the live (rightmost) bar it is the current price, but Pine's default calc_on_every_tick=false means your strategy logic is evaluated on bar close, so you rarely trip over it.

In MQL, OnTick() runs constantly, and the current bar (shift 0) is still forming. Its close, high, and low change with every tick until the candle completes. If you read a signal from shift 0, you are reading a value that can flip back and forth for the entire duration of the candle. A crossover that appears mid-candle can vanish before the bar closes. This produces the classic complaint: "my EA takes trades the TradingView backtest never showed."

The fix has two parts. First, gate your logic to a new bar so the strategy evaluates once per candle, like Pine does. Second, read your signals from the last closed bar (shift 1), never the forming bar (shift 0). Here is the standard new-bar gate in MQL5:

5
// Field declared at global scope
datetime g_last_bar_time = 0;

bool IsNewBar()
{
   datetime current_bar_time = iTime(_Symbol, _Period, 0);
   if(current_bar_time != g_last_bar_time)
   {
      g_last_bar_time = current_bar_time;
      return true;
   }
   return false;
}

With this gate, the body of your OnTick() runs only when a fresh candle opens — meaning the previous candle (shift 1) has just closed and its values are final. That is the MQL equivalent of Pine evaluating on bar close, and it eliminates the forming-bar noise that otherwise makes conversions diverge from the original.

What Commonly Breaks: Look-Ahead and Crossover Edge

Two subtler bugs bite even experienced developers converting from Pine.

Reintroduced look-ahead bias. TradingView's default of filling strategy.entry() on the next bar's open is a look-ahead safeguard — you decide on a closed bar and act afterward. When people port to MQL and read the forming bar (shift 0) then immediately send an order, they collapse decision and action into the same still-forming candle. That is effectively look-ahead: the EA acts on information the historical backtest treated as not-yet-final. Reading from shift 1 inside a new-bar gate restores the honest "decide on closed data, act now" ordering.

Crossover edge detection. Pine's ta.crossover(a, b) is a true edge: it fires only on the single bar where a moves from at-or-below b to above b. A naive MQL port checks fast > slow and fires — but that condition is true on every bar for as long as fast stays above slow, so the EA keeps re-triggering. You must compare two consecutive closed bars to detect the crossing moment itself:

5
// Requires indicator handles created in OnInit(), e.g. via iMA()
bool CrossedUp(int handle_fast, int handle_slow)
{
   double fast[], slow[];
   // Copy the last two *closed* values: shift 1 = last closed bar, shift 2 = the one before
   if(CopyBuffer(handle_fast, 0, 1, 2, fast) < 2) return false;
   if(CopyBuffer(handle_slow, 0, 1, 2, slow) < 2) return false;
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);

   // fast was at-or-below slow on the older closed bar, and is above on the most recent closed bar
   return (fast[1] <= slow[1] && fast[0] > slow[0]);
}

Note the indexing: after CopyBuffer with ArraySetAsSeries(..., true), [0] is the most recent copied value (the last closed bar) and [1] is the bar before it. This mirrors Pine's ta.crossover semantics exactly — one signal, on the edge, from confirmed data. Getting this wrong is the difference between a handful of trades and a stream of duplicates.

Order Timing and Position Management

Pine hides an enormous amount of order-management machinery. strategy.entry("L", strategy.long) will, by default, reverse a short before going long, respect pyramiding limits, and never stack unintended duplicate entries. In MQL, none of that is automatic — you own it.

Before sending any order, check whether you already hold a position for this symbol and this EA. Without that check, the new-bar gate will happily open a fresh position every time the signal condition persists across bars. In MQL5 you filter positions by symbol and by a unique non-zero magic number so your EA only manages its own trades and never touches manual or other-EA positions:

5
bool HasOpenPosition(long magic)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == magic)
         return true;
   }
   return false;
}

The other timing pitfall is stop-loss placement. In Pine you often express risk with strategy.exit() and stop/limit offsets that the tester applies cleanly. In MQL, a market order with no stop-loss is a real, uncontrolled position on a live account. Every entry in a faithful conversion must carry a genuine non-zero stop-loss, sized from an input rather than a hard-coded literal, and the lot size should likewise come from an input so the EA is configurable rather than silently trading a fixed size.

A Minimal, Correct MQL5 Entry Skeleton

Pulling the pieces together, here is a compact MQL5 skeleton that shows the shape of a faithful conversion: a new-bar gate, closed-bar crossover reads, a magic-aware position check, a real stop-loss, an input-driven lot size, and result verification. It is deliberately a scaffold, not a finished strategy — the point is the execution discipline, not the indicator choice.

5
#include <Trade\Trade.mqh>

input double InpLots       = 0.10;     // lot size from an input, never a silent literal
input int    InpStopPoints = 300;      // stop-loss distance in points (non-zero)
input int    InpFastMA     = 10;
input int    InpSlowMA     = 30;
input long   InpMagic      = 20260630; // unique, non-zero magic number

CTrade   trade;
datetime g_last_bar_time = 0;
int      h_fast = INVALID_HANDLE, h_slow = INVALID_HANDLE;

bool HasOpenPosition(long magic)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == magic)
         return true;
   }
   return false;
}

int OnInit()
{
   h_fast = iMA(_Symbol, _Period, InpFastMA, 0, MODE_EMA, PRICE_CLOSE);
   h_slow = iMA(_Symbol, _Period, InpSlowMA, 0, MODE_EMA, PRICE_CLOSE);
   if(h_fast == INVALID_HANDLE || h_slow == INVALID_HANDLE)
      return INIT_FAILED;
   trade.SetExpertMagicNumber(InpMagic);
   return INIT_SUCCEEDED;
}

void OnTick()
{
   // 1) Run once per bar, mirroring Pine's on-close evaluation
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == g_last_bar_time) return;
   g_last_bar_time = t;

   // 2) Read the crossover from CLOSED bars only (shift 1 and shift 2)
   double fast[], slow[];
   if(CopyBuffer(h_fast, 0, 1, 2, fast) < 2) return;
   if(CopyBuffer(h_slow, 0, 1, 2, slow) < 2) return;
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   bool crossed_up = (fast[1] <= slow[1] && fast[0] > slow[0]);
   if(!crossed_up) return;

   // 3) Don't stack duplicates — one position per symbol AND this EA's magic
   if(HasOpenPosition(InpMagic)) return;

   // 4) Build a real, non-zero stop-loss
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double sl  = ask - InpStopPoints * _Point;

   // 5) Send and VERIFY the result
   if(!trade.Buy(InpLots, _Symbol, ask, sl, 0.0, "tv-conversion"))
   {
      PrintFormat("Buy send failed, retcode=%d", trade.ResultRetcode());
      return;
   }
   if(trade.ResultRetcode() != TRADE_RETCODE_DONE)
      PrintFormat("Buy not filled cleanly, retcode=%d", trade.ResultRetcode());
}

Every guard here maps to something Pine did for you invisibly: the new-bar gate replaces on-close evaluation, the shift-1 reads replace final historical values, the magic-scoped position check replaces default entry de-duplication, and the retcode check replaces the tester's assumption that fills simply happen. If you are converting an MQL4 strategy instead, the same rules apply but the calls differ — you would use OrderSend() with OP_BUY, test OrderSend() < 0 and call GetLastError(), and select positions with OrderSelect(). Never mix the two dialects in one file.

Validate the Conversion Before Trusting It

A converted EA that compiles is not a converted strategy that matches. The final and non-negotiable step is verification, because the whole value of porting from TradingView is preserving behaviour you already studied.

Start by comparing signal-for-signal on the same instrument, timeframe, and date range. Run the MetaTrader Strategy Tester in every-tick or real-tick mode and line up the entries against your TradingView Strategy Tester list. If the EA fires on bars the Pine strategy skipped, you almost certainly have a forming-bar or crossover-edge bug from the sections above. If it enters on the same bar but at a different price, check your fill timing against Pine's next-bar-open default. Small, explainable differences (spread, slippage, broker feed) are normal; systematic extra or missing trades are not.

Watch specifically for repainting. If a signal that existed on the forming bar disappears once the bar closes, your live EA and your backtest will disagree forever — the closed-bar reads are what make the strategy non-repainting in MQL. Confirm that a re-run over identical data produces identical trades; determinism is a good proxy for having removed all look-ahead.

If you would rather not hand-audit every one of these traps, ForexCodes' MetaTrader tooling (including the EA Auditor and English-to-MQL / Pine-to-MQL helpers) exists to flag exactly these conversion pitfalls — but treat any output as a starting point to review, not a finished system. Whatever route you take, keep the framing honest: this is educational material, not financial advice, and no conversion — however faithful — implies a strategy is profitable. Trading carries a genuine risk of loss. Test on a demo account across varied market conditions until behaviour is stable before you ever consider real capital.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro