The code
An Expert Advisor that watches the chart symbol's positions and closes part of each one once, at the profit you set. It sends real closing orders, so try it on a demo account first.
Save it as MQL5/Experts/partial-close-position.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.
#include <Trade/Trade.mqh>
input int InpTriggerPoints = 400; // Profit, in points, that triggers the partial close
input double InpClosePercent = 50.0; // Share of the original volume to close, %
input bool InpMoveToEntry = true; // Then move the stop to the entry price
input long InpMagic = -1; // Magic number to manage (-1 = every position on this symbol)
CTrade trade;
// Volume rounded down to the symbol's step (0 if below the minimum)
double FloorVolume(const double lots)
{
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
if(step <= 0.0)
return 0.0;
double v = MathFloor(lots / step + 1e-9) * step;
return v < vmin ? 0.0 : NormalizeDouble(v, 8);
}
// Volume the position was opened with: the sum of its entry deals. Survives restarts, unlike a flag in memory.
double OpenedVolume(const long positionId)
{
double opened = 0.0;
if(!HistorySelectByPosition(positionId))
return 0.0;
for(int i = 0; i < HistoryDealsTotal(); i++)
{
ulong deal = HistoryDealGetTicket(i);
if(deal > 0 && HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
opened += HistoryDealGetDouble(deal, DEAL_VOLUME);
}
return opened;
}
void ManagePartials()
{
bool hedging = AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;
double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
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 volume = PositionGetDouble(POSITION_VOLUME);
double tp = PositionGetDouble(POSITION_TP);
double price = isBuy ? tick.bid : tick.ask;
if((isBuy ? price - open : open - price) < InpTriggerPoints * _Point)
continue;
double opened = OpenedVolume(PositionGetInteger(POSITION_IDENTIFIER));
if(opened <= 0.0 || volume < opened - 1e-8)
continue; // already reduced once: don't close again
double part = FloorVolume(opened * InpClosePercent / 100.0);
if(part <= 0.0 || volume - part < vmin - 1e-8)
{
PrintFormat("#%I64u: %.2f lots can't be split into %.0f%% and a remainder of at least %.2f", ticket, volume, InpClosePercent, vmin);
continue; // e.g. 0.01 lots on a symbol whose minimum is 0.01
}
// CTrade's PositionClosePartial() only works on hedging accounts; on netting, an opposite deal reduces the position
bool ok = hedging ? trade.PositionClosePartial(ticket, part)
: (isBuy ? trade.Sell(part, _Symbol) : trade.Buy(part, _Symbol));
if(!ok || trade.ResultRetcode() != TRADE_RETCODE_DONE)
{
PrintFormat("Partial close of #%I64u failed: retcode %u (%s)", ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
continue;
}
PrintFormat("#%I64u: closed %.2f of %.2f lots at %s", ticket, part, volume, DoubleToString(trade.ResultPrice(), _Digits));
// The position keeps its ticket on a hedging account; on netting, select it by symbol again
if(InpMoveToEntry && (hedging ? PositionSelectByTicket(ticket) : PositionSelect(_Symbol)))
if(!trade.PositionModify(PositionGetInteger(POSITION_TICKET), open, tp))
PrintFormat("Stop to entry failed: retcode %u (%s)", trade.ResultRetcode(), trade.ResultRetcodeDescription());
}
}
void OnTick()
{
ManagePartials();
}How it works
hedging.ACCOUNT_MARGIN_MODE_RETAIL_HEDGINGmeans several positions per symbol, each with its own ticket. The other modes are netting: one position per symbol.OpenedVolume(). The volume the position was opened with is the sum of its entry deals in the history. If the current volume is smaller, it was already reduced: no second partial close, even after the EA restarts. A flag in memory would be lost on restart and can't tell positions apart.FloorVolume(). Half of 0.15 lots is 0.075, which a 0.01 step doesn't accept: rounding down gives 0.07 and leaves 0.08 open. The1e-9absorbs floating-point error (0.3 / 0.1 is 2.9999999999999996 in a double).- The remainder check. What stays open must be at least
SYMBOL_VOLUME_MIN. A 0.01-lot position on a symbol whose minimum is 0.01 can't be split at all, and the EA says so instead of failing. - The netting branch.
trade.Sell(part, _Symbol)against a long reduces it byparton a netting account. On a hedging account the same call would open a separate short, which is why the branch depends on the account mode. - Stop to entry. On a hedging account the position keeps its ticket after a partial close; on netting,
PositionSelect(_Symbol)finds it again before the stop is moved.
Pitfalls
PositionClosePartial()on a netting account. The standard library returnsfalsebefore sending anything, soResultRetcode()stays 0 and no error appears in the Experts tab. Check the account mode as the EA does.- A volume off the step. Closing exactly half often gives a volume the symbol doesn't accept, refused with 10014 invalid volume. Round down to the step, and check what remains.
- Closing half again on every tick. Without a "done already" test, each tick after the trigger closes another half until the minimum volume is left.
- The stop at entry right after. If price has come back close to the entry by the time the stop moves, the new stop can sit inside the stops level and fail with 10016 invalid stops.
- Outside trading hours. A partial close is a deal like any other: when the market is closed it fails with 10018.
Variations
- Two targets. Close a third at the first target and a third at the second: compare
volume / openedwith 1, 2/3 and 1/3 to know which step the position is at. - A target in money. Use
PositionGetDouble(POSITION_PROFIT)instead of a distance in points, for example "close half at +100 in account currency". - Break-even with costs. The EA moves the stop to the plain entry price. Move stop-loss to break-even adds commission and swap to that level.
- A resting order on netting accounts. On a netting account, a sell limit for half the volume at the target reduces the long when it fills, without the EA watching. On a hedging account the same order would open a new short.
Questions
Does a partial close change the position's ticket in MT5?
No. On a hedging account the position keeps its ticket and its identifier, and the history shows a closing deal for the volume that was closed. MetaTrader 4 worked differently: it opened a new ticket for the remaining volume.
Why does PositionClosePartial() return false?
On a netting account it always does: the standard library only sends it on hedging accounts. Otherwise, read trade.ResultRetcode(): an invalid volume (10014), a closed market (10018) or a frozen position (10029) are the usual answers, and 0 means nothing was sent.
How do I close half of a trade in MQL5?
Take the opened volume, multiply by 0.5, round down to SYMBOL_VOLUME_STEP and check that the rest is at least SYMBOL_VOLUME_MIN. Then close that volume: PositionClosePartial() on hedging accounts, an opposite deal on netting accounts. With a 0.01 step, half of 0.15 lots is 0.07.
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.