The Pine Script v6 code
A long-only strategy that buys when the close crosses above a 50 SMA, with a stop 300 ticks below and a target 600 ticks above the entry price.
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("Stop loss and take profit with strategy.exit", overlay = true, commission_type = strategy.commission.percent, commission_value = 0.01, slippage = 2)
len = input.int(50, "SMA length", minval = 1)
stopTicks = input.int(300, "Stop loss (ticks)", minval = 1)
targetTicks = input.int(600, "Take profit (ticks)", minval = 1)
sma = ta.sma(close, len)
longSignal = ta.crossover(close, sma)
if longSignal and strategy.position_size == 0
strategy.entry("Long", strategy.long)
// loss and profit are in ticks, measured from the entry's fill price
strategy.exit("Long exit", "Long", loss = stopTicks, profit = targetTicks)
plot(sma, "SMA", color.orange, 2)
The MQL5 program
The Expert Advisor with the same entry and the same stop and target in ticks, placed on the broker's server with the order. It sends real orders, so run it in the Strategy Tester or on a demo account first.
Save it as MQL5/Experts/stop-loss-take-profit-strategy-exit.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.
//+------------------------------------------------------------------+
//| strategy.exit(loss = ..., profit = ...) in MQL5: a stop loss and |
//| a take profit in ticks from the entry's fill price. |
//| 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 InpSmaLen = 50; // SMA length
input int InpStopTicks = 300; // Stop loss, ticks
input int InpTargetTicks = 600; // Take profit, ticks
input double InpLots = 0.10; // Volume, lots
input ulong InpMagic = 20261112; // Magic number
input ulong InpSlippage = 20; // Maximum slippage, points
CTrade trade;
int smaHandle = INVALID_HANDLE;
datetime lastBarTime = 0;
int OnInit()
{
if(InpSmaLen < 1 || InpStopTicks < 1 || InpTargetTicks < 1)
return INIT_PARAMETERS_INCORRECT;
smaHandle = iMA(_Symbol, _Period, InpSmaLen, 0, MODE_SMA, PRICE_CLOSE);
if(smaHandle == INVALID_HANDLE)
return INIT_FAILED;
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFillingBySymbol(_Symbol);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
IndicatorRelease(smaHandle);
}
// syminfo.mintick: the price step of the symbol (not always equal to _Point)
double TickSize()
{
double t = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
return t > 0.0 ? t : _Point;
}
double RoundToTick(const double price)
{
double t = TickSize();
return NormalizeDouble(MathRound(price / t) * t, _Digits);
}
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);
}
// Ticket of this EA's open position on the symbol, 0 if flat (strategy.position_size == 0)
ulong OurPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket != 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && (ulong)PositionGetInteger(POSITION_MAGIC) == InpMagic)
return ticket;
}
return 0;
}
// The broker refuses stops closer than SYMBOL_TRADE_STOPS_LEVEL points to the Bid (for a buy)
bool StopsAllowed(const double sl, const double tp)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double minDist = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
return bid - sl > minDist && tp - bid > minDist;
}
void OpenLong()
{
double lots = NormalizeLots(InpLots);
MqlTick tick;
if(lots <= 0.0 || !SymbolInfoTick(_Symbol, tick))
return;
// Stops from the price we ask for; corrected below from the real fill price
double sl = RoundToTick(tick.ask - InpStopTicks * TickSize());
double tp = RoundToTick(tick.ask + InpTargetTicks * TickSize());
if(!StopsAllowed(sl, tp))
{
Print("Stop or target closer than the broker's stops level: no trade");
return;
}
if(!trade.Buy(lots, _Symbol, tick.ask, sl, tp, "Long") || trade.ResultRetcode() != TRADE_RETCODE_DONE)
{
PrintFormat("Buy failed: retcode %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription());
return;
}
// Pine measures loss and profit from the fill price: re-anchor them if the order slipped
double fill = trade.ResultPrice();
if(fill <= 0.0 || MathAbs(fill - tick.ask) < TickSize() / 2)
return;
ulong ticket = OurPosition();
double newSl = RoundToTick(fill - InpStopTicks * TickSize());
double newTp = RoundToTick(fill + InpTargetTicks * TickSize());
if(ticket != 0 && StopsAllowed(newSl, newTp) && !trade.PositionModify(ticket, newSl, newTp))
PrintFormat("Could not move the stops to the fill price: retcode %u", trade.ResultRetcode());
}
void OnTick()
{
datetime barTime = iTime(_Symbol, _Period, 0);
if(barTime == 0 || barTime == lastBarTime)
return;
double sma[];
ArraySetAsSeries(sma, true); // [0] = bar 1 (just closed), [1] = bar 2
double c1 = iClose(_Symbol, _Period, 1);
double c2 = iClose(_Symbol, _Period, 2);
if(CopyBuffer(smaHandle, 0, 1, 2, sma) != 2 || c1 == 0.0 || c2 == 0.0)
return; // not ready: try again on the next tick
lastBarTime = barTime;
// if ta.crossover(close, sma) and strategy.position_size == 0: strategy.entry("Long", strategy.long)
if(c1 > sma[0] && c2 <= sma[1] && OurPosition() == 0)
OpenLong();
}
//+------------------------------------------------------------------+
Line by line
stopTicks = input.int(300, "Stop loss (ticks)", minval = 1)
// a tick is syminfo.mintickinput int InpStopTicks = 300; // Stop loss, ticks
double TickSize()
{
double t = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
return t > 0.0 ? t : _Point;
}A Pine tick is syminfo.mintick, the symbol's price step; in MQL5 that is SYMBOL_TRADE_TICK_SIZE. _Point is the value of the price's last digit and isn't always the same.
if longSignal and strategy.position_size == 0
strategy.entry("Long", strategy.long)if(c1 > sma[0] && c2 <= sma[1] && OurPosition() == 0)
OpenLong();Only when flat, judged on the closed bar at the first tick of the next one.
strategy.exit("Long exit", "Long", loss = stopTicks,
profit = targetTicks)double sl = RoundToTick(tick.ask - InpStopTicks * TickSize());
double tp = RoundToTick(tick.ask + InpTargetTicks * TickSize());
trade.Buy(lots, _Symbol, tick.ask, sl, tp, "Long");The stop and target go with the order and sit on the broker's server, so they work with the terminal closed. Prices are rounded to the tick size.
// loss and profit count from the fill pricedouble fill = trade.ResultPrice();
...
trade.PositionModify(ticket, newSl, newTp);If the order filled at another price than requested (slippage), the EA moves the stop and target to the same distances from the real fill price.
// no minimum distance in the broker emulatordouble minDist = (double)SymbolInfoInteger(_Symbol,
SYMBOL_TRADE_STOPS_LEVEL) * _Point;
return bid - sl > minDist && tp - bid > minDist;Brokers refuse stops closer to the price than SYMBOL_TRADE_STOPS_LEVEL points; the server would answer 10016 invalid stops. For a buy the distance is measured from the Bid.
strategy.exit("Long exit", "Long", stop = s, limit = l)trade.Buy(lots, _Symbol, ask, RoundToTick(s), RoundToTick(l));With prices instead of ticks, pass them as they are, rounded to the tick size.
Pitfalls
- Bid and Ask. In MetaTrader 5 a long's stop and target trigger on the Bid, while the entry is at the Ask. A stop 300 ticks below the Ask is 300 ticks minus the spread below the Bid chart you see, so it is hit sooner than on a TradingView chart without spread.
- Which comes first inside a bar. When the stop and the target are both inside one bar's range, TradingView's emulator infers the path from the bar's open, high, low and close; the MT5 tester with real ticks knows the order. The same bar can be a win on one and a loss on the other.
- Ticks, points and pips. On a 5-digit forex symbol a tick is 0.00001, so 300 ticks are 30 pips. On gold or indices the tick size can be bigger than the point: distances computed with
_Pointand unrounded prices are refused with 10015 invalid price or 10016 invalid stops. - Modifying to the same values.
PositionModify()with unchanged stops answers 10025 no changes; the EA only re-anchors when the fill differs by at least half a tick. - Freeze level. Close to the market, some brokers refuse to modify a position at all (10029 frozen). Sending the stops with the order avoids most of it.
Checking that both give the same values
- Run both on the same symbol and dates. For each stopped-out trade, the distance between entry and exit should be the stop in ticks: exactly in Pine, and in MetaTrader 5 from the fill price (plus any slippage on the exit).
- In the MetaTrader 5 tester's history, check each position's S/L and T/P against its open price: 300 and 600 ticks away.
- Count wins and losses on each. Differences should come from bars where both levels were inside the range, or from the spread.
Trailing stops
strategy.exit() with trail_points and trail_offset has no server-side twin for an Expert Advisor: the trailing stop in the terminal's position menu isn't available to programs. The EA moves the stop itself on each tick or each bar with trade.PositionModify(), only in the favourable direction and by at least one tick. The MQL5 trailing stop guide shows the code.
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.