Edgoro
Pine Script v6 → MQL5 expert advisor

strategy.entry in MQL5: market orders with CTrade

Short answer

strategy.entry("Long", strategy.long) becomes trade.Buy(lots, _Symbol) with a CTrade object from <Trade/Trade.mqh>, sent on the first tick of the bar after the signal, when Pine's emulator would fill. Pine also closes an open short first; on a hedging account MetaTrader 5 doesn't, so the Expert Advisor closes its opposite positions itself, then checks ResultRetcode().

Pinestrategy.entrystrategy.close_allstrategy.position_sizehour

MQL5CTrade::BuyCTrade::SellCTrade::PositionClosePositionGetTicketCTrade::ResultRetcode

  • MQL5: compiled in MetaEditor, 0 errors, 0 warnings
  • Pine: checked by Edgoro Pine Check, not run in TradingView

The Pine Script v6 code

A strategy that is always in the market: long after a fast EMA crosses above a slow one, short after it crosses below, and flat at a chosen hour each day.

Checked by Edgoro Pine Check with no errors and no warnings. Pine Check is a static checker, not TradingView's compiler, and this script was not run in TradingView.

Pine Script v6
//@version=6
strategy("EMA cross, always in the market", overlay = true, default_qty_type = strategy.fixed, default_qty_value = 1, commission_type = strategy.commission.percent, commission_value = 0.01, slippage = 2)

fastLen = input.int(9, "Fast EMA", minval = 1)
slowLen = input.int(21, "Slow EMA", minval = 1)
closeHour = input.int(21, "Close everything at this hour", minval = 0, maxval = 23)

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
crossUp = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)
endOfDay = hour == closeHour

// Orders are decided on the bar's close and fill at the next bar's open
if endOfDay
    strategy.close_all(comment = "End of day")
else if crossUp
    // closes an open short first, then opens the long
    strategy.entry("Long", strategy.long)
else if crossDown
    strategy.entry("Short", strategy.short)

plot(fast, "Fast EMA", color.teal)
plot(slow, "Slow EMA", color.orange)

The MQL5 program

The Expert Advisor with the same rules, trading a fixed volume on its chart's symbol. It sends real orders, so run it in the Strategy Tester or on a demo account first.

Save it as MQL5/Experts/strategy-entry-to-ctrade.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.

MQL5
//+------------------------------------------------------------------+
//| strategy.entry() / strategy.close_all() ported to CTrade         |
//| Expert Advisor: save in MQL5/Experts and compile (F7).           |
//| It sends real orders: run it in the Strategy Tester or on a demo |
//| account first.                                                   |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>

input int    InpFastLen   = 9;        // Fast EMA
input int    InpSlowLen   = 21;       // Slow EMA
input int    InpCloseHour = 21;       // Close everything at this hour (server time)
input double InpLots      = 0.10;     // Volume per trade, lots
input ulong  InpMagic     = 20261112; // Magic number
input ulong  InpSlippage  = 20;       // Maximum slippage, points

CTrade   trade;
int      fastHandle  = INVALID_HANDLE;
int      slowHandle  = INVALID_HANDLE;
datetime lastBarTime = 0;

int OnInit()
  {
   if(InpFastLen < 1 || InpSlowLen < 1 || InpCloseHour < 0 || InpCloseHour > 23)
      return INIT_PARAMETERS_INCORRECT;
   fastHandle = iMA(_Symbol, _Period, InpFastLen, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle = iMA(_Symbol, _Period, InpSlowLen, 0, MODE_EMA, PRICE_CLOSE);
   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE)
      return INIT_FAILED;
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);
   trade.SetTypeFillingBySymbol(_Symbol);
   return INIT_SUCCEEDED;
  }

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

// Rounds the lot size to the symbol's volume step and limits
double NormalizeLots(const double lots)
  {
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minV = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxV = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(step <= 0.0)
      return 0.0;
   double v = MathFloor(lots / step + 1e-9) * step;
   if(v < minV)
      return 0.0;
   return NormalizeDouble(MathMin(v, maxV), 8);
  }

// strategy.position_size: + for long, - for short, 0 when flat (this EA's positions only)
double PositionSize()
  {
   double size = 0.0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      double vol = PositionGetDouble(POSITION_VOLUME);
      size += PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? vol : -vol;
     }
   return size;
  }

// Closes this EA's positions of one type (or all of them with -1); false if one could not be closed
bool ClosePositions(const int type)
  {
   bool ok = true;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol || (ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      if(type >= 0 && PositionGetInteger(POSITION_TYPE) != type)
         continue;
      if(!trade.PositionClose(ticket) || trade.ResultRetcode() != TRADE_RETCODE_DONE)
        {
         PrintFormat("Close of #%I64u failed: retcode %u (%s)", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
         ok = false;
        }
     }
   return ok;
  }

// strategy.entry(): close the opposite side first (Pine reverses), then open if not already in that direction
void Enter(const bool isLong)
  {
   if(!ClosePositions(isLong ? POSITION_TYPE_SELL : POSITION_TYPE_BUY))
      return;
   double size = PositionSize();
   if((isLong && size > 0.0) || (!isLong && size < 0.0))
      return;  // already in: with pyramiding = 0, Pine ignores the entry too
   double lots = NormalizeLots(InpLots);
   if(lots <= 0.0)
     {
      Print("Volume below the symbol's minimum: no trade");
      return;
     }
   bool sent = isLong ? trade.Buy(lots, _Symbol, 0.0, 0.0, 0.0, "Long") : trade.Sell(lots, _Symbol, 0.0, 0.0, 0.0, "Short");
   if(!sent || trade.ResultRetcode() != TRADE_RETCODE_DONE)
      PrintFormat("%s failed: retcode %u (%s)", isLong ? "Buy" : "Sell", trade.ResultRetcode(), trade.ResultRetcodeDescription());
  }

void OnTick()
  {
   // A Pine strategy decides once per bar, on its close: do the same on the first tick of each new bar
   datetime barTime = iTime(_Symbol, _Period, 0);
   if(barTime == 0 || barTime == lastBarTime)
      return;

   double fast[], slow[];
   ArraySetAsSeries(fast, true);  // [0] = bar 1 (just closed), [1] = bar 2
   ArraySetAsSeries(slow, true);
   if(CopyBuffer(fastHandle, 0, 1, 2, fast) != 2 || CopyBuffer(slowHandle, 0, 1, 2, slow) != 2)
      return;  // not ready: lastBarTime unchanged, so the next tick tries again
   lastBarTime = barTime;

   bool crossUp   = fast[0] > slow[0] && fast[1] <= slow[1];
   bool crossDown = fast[0] < slow[0] && fast[1] >= slow[1];

   // Pine's `hour` is the hour of the closed bar's open time (exchange time zone); here, server time
   MqlDateTime closedBar;
   TimeToStruct(iTime(_Symbol, _Period, 1), closedBar);
   bool endOfDay = closedBar.hour == InpCloseHour;

   if(endOfDay)
      ClosePositions(-1);  // strategy.close_all()
   else if(crossUp)
      Enter(true);         // strategy.entry("Long", strategy.long)
   else if(crossDown)
      Enter(false);        // strategy.entry("Short", strategy.short)
  }
//+------------------------------------------------------------------+

Line by line

Pine Script v6
strategy("EMA cross, always in the market", overlay = true,
  default_qty_value = 1, commission_value = 0.01, slippage = 2)
MQL5
#include <Trade/Trade.mqh>
CTrade trade;
...
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFillingBySymbol(_Symbol);

A strategy becomes an Expert Advisor. Commission is a Strategy Tester setting, slippage becomes the deviation accepted on a market order, and the magic number marks this EA's positions.

Pine Script v6
// orders decided on the bar's close fill at the next open
MQL5
datetime barTime = iTime(_Symbol, _Period, 0);
if(barTime == 0 || barTime == lastBarTime)
   return;
...
CopyBuffer(fastHandle, 0, 1, 2, fast);

Acting on the first tick of a new bar, with the values of bar 1, is the moment Pine's broker emulator fills: the open of the next bar.

Pine Script v6
strategy.entry("Long", strategy.long)
MQL5
ClosePositions(POSITION_TYPE_SELL);
trade.Buy(lots, _Symbol, 0.0, 0.0, 0.0, "Long");

A price of 0.0 lets CTrade use the current Ask. Closing the open sell first reproduces Pine's reversal on any account type.

Pine Script v6
strategy.position_size
MQL5
for(int i = PositionsTotal() - 1; i >= 0; i--)
  {
   ulong ticket = PositionGetTicket(i);
   // same symbol and magic number only
   size += type == POSITION_TYPE_BUY ? vol : -vol;
  }

Pine has one net position per strategy; MetaTrader 5 lists every position of the account. Filter by symbol and magic number, add buys, subtract sells.

Pine Script v6
// pyramiding = 0 (the default)
MQL5
if((isLong && size > 0.0) || (!isLong && size < 0.0))
   return;

Pine ignores a second entry in the same direction unless pyramiding allows it. The EA does the same.

Pine Script v6
strategy.close_all(comment = "End of day")
MQL5
trade.PositionClose(ticket)  // each position of this EA

Close by ticket. PositionClose(_Symbol) would close a single position and ignore the magic number.

Pine Script v6
endOfDay = hour == closeHour
MQL5
MqlDateTime closedBar;
TimeToStruct(iTime(_Symbol, _Period, 1), closedBar);
bool endOfDay = closedBar.hour == InpCloseHour;

Both read the hour of the closed bar's open time, but Pine's hour is in the exchange's time zone and MetaTrader 5's in server time: set the input in server time.

Pitfalls

  • Hedging accounts don't reverse. trade.Buy() while a short is open leaves both open on a hedging account (ACCOUNT_MARGIN_MODE_RETAIL_HEDGING): the "reversal" becomes a locked position paying two spreads. On a netting account, a buy smaller than the short only reduces it. Close first, as the EA does.
  • true doesn't mean filled. trade.Buy() returns true when the request was sent; only TRADE_RETCODE_DONE (10009) means a deal. Common refusals: 10030 unsupported filling mode, 10014 invalid volume, 10019 not enough money and 10027 algo trading disabled.
  • One decision per bar. Without the new-bar check, OnTick() evaluates the cross on every tick and can open, close and reopen inside one candle.
  • Fills differ. Pine fills at the next bar's open plus its slippage setting; the EA pays the Ask to buy and receives the Bid to sell. On short timeframes the spread alone changes the results.
  • Quantity. Copying default_qty_value = 1 into 1 lot multiplies the size by 100,000 on forex: on TradingView, 1 is one unit of currency.

Checking that both give the same values

  1. Run the Pine strategy in TradingView and note the List of Trades (entry time and direction) for a few weeks.
  2. Run the EA in the MetaTrader 5 Strategy Tester on the same symbol, timeframe and dates, modelling "Every tick based on real ticks" or "1 minute OHLC".
  3. Each Pine entry at the open of a bar should match an EA deal in the first seconds of the same bar, once TradingView's time zone is converted to server time.
  4. Expect prices to differ by the spread and the feed, not the direction or the bar. A trade on one side only usually comes from a cross so close to a touch that the two feeds disagree.

Netting or hedging: check the account

AccountInfoInteger(ACCOUNT_MARGIN_MODE) returns ACCOUNT_MARGIN_MODE_RETAIL_NETTING, ACCOUNT_MARGIN_MODE_RETAIL_HEDGING or ACCOUNT_MARGIN_MODE_EXCHANGE. The program on this page works on both retail modes because it closes the opposite side before entering and counts positions by symbol and magic number. On a netting account, every EA and manual trade on the same symbol share one position, so two EAs on one symbol will fight; use one EA per symbol there.

These programs show one technique each; they are not trading systems, and none of this is a recommendation to trade. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

The same topic in each language

All MQL5 errors

Rather not port it by hand?

Edgoro can generate the MQL5 version for you from a plain-English description of the rules, and compiles it in MetaEditor before you get it. Test it on a demo account: a clean build isn't proof that a strategy works.