The code
An Expert Advisor that manages positions already open on its chart's symbol, including manual ones when the magic number is -1. It modifies real stops, so try it on a demo account first.
Save it as MQL5/Experts/move-stop-loss-to-break-even.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.
#include <Trade/Trade.mqh>
input int InpTriggerPoints = 300; // Profit, in points, that moves the stop to break-even
input int InpLockPoints = 20; // Extra points locked in beyond break-even
input bool InpCoverCosts = true; // Break-even after commission and swap, not just the entry price
input long InpMagic = -1; // Magic number to manage (-1 = every position on this symbol)
CTrade trade;
double RoundToTick(const double price)
{
double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
if(tick <= 0.0)
return NormalizeDouble(price, _Digits);
return NormalizeDouble(MathRound(price / tick) * tick, _Digits);
}
// Price distance that pays back the commission and swap of a position. Commission is often charged again on
// the closing deal, so the entry commission is counted twice.
double CostDistance(const long positionId, const bool isBuy, const double volume, const double open, const double swap)
{
double commission = 0.0;
if(HistorySelectByPosition(positionId))
for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
{
ulong deal = HistoryDealGetTicket(i);
if(deal > 0)
commission += HistoryDealGetDouble(deal, DEAL_COMMISSION);
}
double cost = -(2.0 * commission + swap); // commission is negative, swap can be either
if(cost <= 0.0)
return 0.0;
double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double perTick = 0.0; // money made by this volume over one tick
if(tick <= 0.0 || !OrderCalcProfit(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, _Symbol, volume, open,
isBuy ? open + tick : open - tick, perTick) || perTick <= 0.0)
return 0.0;
return MathCeil(cost / perTick) * tick;
}
void ManageBreakEven()
{
double stops = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
double freeze = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL) * _Point;
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
return;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0 || PositionGetString(POSITION_SYMBOL) != _Symbol)
continue;
if(InpMagic >= 0 && PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
bool isBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
double open = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
double price = isBuy ? tick.bid : tick.ask; // the price the position would close at
if((isBuy ? price - open : open - price) < InpTriggerPoints * _Point)
continue; // not far enough in profit yet
double offset = InpLockPoints * _Point;
if(InpCoverCosts)
offset += CostDistance(PositionGetInteger(POSITION_IDENTIFIER), isBuy, PositionGetDouble(POSITION_VOLUME),
open, PositionGetDouble(POSITION_SWAP));
double be = RoundToTick(isBuy ? open + offset : open - offset);
if(sl != 0.0 && (isBuy ? sl >= be : sl <= be))
continue; // already at or beyond break-even: nothing to send
double room = MathMax(stops, _Point); // the stop must stay at least the stops level from the price
if(isBuy ? price - be < room : be - price < room)
continue; // too close for this broker: wait for more profit
if(freeze > 0.0 && ((sl != 0.0 && MathAbs(price - sl) < freeze) || (tp != 0.0 && MathAbs(price - tp) < freeze)))
continue; // inside the freeze level: the server refuses any change
if(!trade.PositionModify(ticket, be, tp) || trade.ResultRetcode() != TRADE_RETCODE_DONE)
PrintFormat("Break-even on #%I64u failed: retcode %u (%s)", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
else
PrintFormat("#%I64u: stop moved to %s (entry %s)", ticket, DoubleToString(be, _Digits), DoubleToString(open, _Digits));
}
}
void OnTick()
{
ManageBreakEven();
}How it works
- Profit from the closing price. A buy's profit is measured from the Bid and a sell's from the Ask, the prices they would close at. The trigger is in points, so 300 means 30 pips on a five-digit EUR/USD quote.
CostDistance(). It adds up the commission of the position's deals withHistorySelectByPosition(), counts it twice because many brokers charge it again on the closing deal, adds the swap, and turns that amount into a price distance withOrderCalcProfit()over one tick. That works for any symbol and account currency, without hand-coded pip values.sl >= be(buy) orsl <= be(sell). If the stop is already at or beyond break-even, nothing is sent. That makes the move happen once, from state the server keeps, so a restart can't send it twice.room. The new stop must stay at least the stops level away from the price. If the trigger is reached but the stop would be too close, the EA waits for more profit instead of sending a request that would be refused.- The freeze check. Within
SYMBOL_TRADE_FREEZE_LEVELof the current stop or target, the server refuses any change to the position. PositionModify(ticket, be, tp). The take-profit is passed back unchanged; passing 0 would remove it.
Pitfalls
- Break-even at the entry price still loses money. On an account charging 3.50 per lot per side, a one-lot position stopped exactly at its entry loses 7 in commission, plus any swap. That's why the EA adds the costs; set
InpCoverCoststo false if you want the plain entry price. - Sending the same stop on every tick. Without the "already there" check, every tick after the trigger sends an identical stop and gets 10025 no changes.
- A trigger smaller than the stops level plus the offset. The new stop then lands inside the broker's minimum distance and is refused with 10016 invalid stops. Keep the trigger well above
SYMBOL_TRADE_STOPS_LEVELplusInpLockPoints. - Moving the stop inside the freeze level. Near the current stop or target, the change fails with 10029 frozen.
- Netting accounts. Adding to a position moves
POSITION_PRICE_OPENto the volume-weighted average, so the break-even level moves too, and the stop applies to the whole netted position.
Variations
- Then trail. Break-even is often the first step of a trailing stop. MQL5 trailing stop continues from here with fixed-distance and ATR trailing.
- After a partial close. Close half at the first target, then move the rest to break-even: partial close does both.
- Trigger at 1R. Instead of fixed points, trigger when profit equals the initial risk. The original stop is on the position's entry deal: read
HistoryDealGetDouble(deal, DEAL_SL)for theDEAL_ENTRY_INdeal. - Only this EA's trades. Set
InpMagicto the magic number of the EA that opens the positions, so the two programs can run side by side.
Questions
Should a break-even stop include the spread?
Not for a zero gross result. A buy is entered at the Ask and its stop triggers at the Bid, so a stop at the entry price closes when the Bid is back at the price you paid: the spread was already paid on entry. Commission and swap are separate costs, which the EA adds.
How do I move the stop to break-even only once?
Compare the position's current stop with the break-even level and send nothing when it's already there or beyond. That state is kept by the server, so it survives restarts, unlike a boolean flag in the EA's memory.
Can an EA move manual trades to break-even?
Yes. Manual trades have the magic number 0: run the EA with InpMagic set to 0 for manual trades only, or -1 for every position on the chart's symbol.
Can a trade whose stop is at break-even still lose money?
Yes. A stop-loss becomes a market order when it triggers, so in a fast market or over a weekend gap it can fill below a buy's stop (above a sell's). The stop limits the loss in normal conditions; it doesn't cap it.
This program is a minimal example written to show one technique, not a trading system, and none of it 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.