DEV Community

Karan Rajput
Karan Rajput

Posted on Originally published at codeflowos.com

Pine Script to MQL5: the three timing bugs that make an EA trade differently

A Pine Script strategy and its MQL5 version can use the same indicators and still trade on different bars. Here are the three timing bugs behind most of that, and a small EA to check for them.

1. Reading the wrong bar

In Pine, close[1] is the last closed bar. In MQL5 you choose the index yourself. In OnTick(), shift 0 is the bar that is still forming. A signal that is calculated on the close should read shift 1 and 2.

2. Running once per tick instead of once per bar

TradingView runs a strategy once per bar by default. OnTick() runs on every tick, so without a new-bar check the same signal can fire many times. Save the time of the last processed bar and return early if it has not changed. Save it only after your data reads succeeded, so another tick can retry if the indicator is not ready.

3. Missing the rules that strategy.entry() applies for you

strategy.entry() reverses an opposite position and stops at the pyramiding limit. In MQL5, CTrade only sends what you tell it to. Close the opposite side, count your open positions and check the return code of every request.

A signal-only EA to check the first two

This EA reads the last two closed bars of a 10 and 30 period EMA and prints a cross. It places no trades, so you can compare it with the chart first:

int      fastHandle       = INVALID_HANDLE;
int      slowHandle       = INVALID_HANDLE;
datetime lastProcessedBar = 0;

int OnInit()
{
   fastHandle = iMA(_Symbol, _Period, 10, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, 30, 0, MODE_EMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
      return INIT_FAILED;
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   if(fastHandle != INVALID_HANDLE) IndicatorRelease(fastHandle);
   if(slowHandle != INVALID_HANDLE) IndicatorRelease(slowHandle);
}

void OnTick()
{
   const datetime currentBar = iTime(_Symbol, _Period, 0);
   if(currentBar == 0 || currentBar == lastProcessedBar)
      return;

   double fast[2], slow[2];
   if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 ||
      CopyBuffer(slowHandle, 0, 1, 2, slow) != 2)
      return;

   lastProcessedBar = currentBar;
   if(fast[0] <= slow[0] && fast[1] > slow[1]) Print("Closed-bar EMA cross up.");
   if(fast[0] >= slow[0] && fast[1] < slow[1]) Print("Closed-bar EMA cross down.");
}
Enter fullscreen mode Exit fullscreen mode

CopyBuffer() starts at shift 1, so index 0 is shift 2 and index 1 is shift 1. Run it on a chart, put the same EMAs on the same symbol and timeframe in TradingView, and compare every printed cross with the cross on the chart. If one is a bar early or late, fix the shift before you add any orders.

The complete file, with input parameters, is on GitHub: https://github.com/karanpixorce-prog/pine-to-mql5-signal-ea

Where to go next

The full conversion guide has a mapping table, a list of what does not convert and a verification checklist: https://codeflowos.com/blog/pine-script-to-mql5-guide

I built CodeFlowOS, a tool that produces a first-draft MQL5 file from Pine Script: https://codeflowos.com/pine-script-to-mql5. Review and test its output, like any converted code.

This is educational, not financial advice.

Top comments (1)

Collapse
 
stratcorealpha profile image
Arnold Holm •

One more parity check after the signals line up: log the signal bar, order request and actual fill separately. A crossover on the same closed bar can still produce different trades if one version fills at the close and the other at the next available tick.

A useful test case is a crossover on the final bar before a session gap. Compare the three timestamps and fill prices, then repeat with an opposite position already open. That separates bar-index errors from fill timing and reversal handling. Which Pine order-fill settings do you use as the reference for this EA?