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.
//@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.
//+------------------------------------------------------------------+
//| 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
strategy("EMA cross, always in the market", overlay = true,
default_qty_value = 1, commission_value = 0.01, slippage = 2)#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.
// orders decided on the bar's close fill at the next opendatetime 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.
strategy.entry("Long", strategy.long)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.
strategy.position_sizefor(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.
// pyramiding = 0 (the default)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.
strategy.close_all(comment = "End of day")trade.PositionClose(ticket) // each position of this EAClose by ticket. PositionClose(_Symbol) would close a single position and ignore the magic number.
endOfDay = hour == closeHourMqlDateTime 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. truedoesn't mean filled.trade.Buy()returnstruewhen the request was sent; onlyTRADE_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 = 1into 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
- Run the Pine strategy in TradingView and note the List of Trades (entry time and direction) for a few weeks.
- 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".
- 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.
- 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.