Edgoro
Expert Advisor · MQL5

Partial close a position in MQL5 (hedging and netting)

Short answer

On a hedging account, call trade.PositionClosePartial(ticket, volume) with a volume rounded down to SYMBOL_VOLUME_STEP that leaves at least SYMBOL_VOLUME_MIN open. On a netting account that method returns false without sending anything; send an opposite deal of the volume to close instead, trade.Sell(volume) for a long.

UsesCTrade::PositionClosePartialACCOUNT_MARGIN_MODEHistorySelectByPositionSYMBOL_VOLUME_STEPCTrade::PositionModify

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.

MQL5
#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_HEDGING means 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. The 1e-9 absorbs 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 by part on 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 returns false before sending anything, so ResultRetcode() 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 / opened with 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.

All MQL5 errors

Want it inside a complete EA?

Scaling out is one sentence in a strategy description: Edgoro's MT5 EA generator turns it into an EA that rounds volumes to the broker's step and handles both account types. A clean build proves the code compiles, not that a strategy works: test it on a demo account.