DEV Community

Omais Ali
Omais Ali

Posted on

Taking a partial close at the first target in MQL5 without doing it twice

A common exit plan is to close half the position at a first target and let the rest run. It sounds like three lines of code. In practice the first version usually has at least one of these bugs:

  1. It closes half again on the next tick, and the next, until the position is gone.
  2. It forgets that the partial was taken after a terminal restart and takes it again.
  3. It sends a volume the broker rejects, such as 0.05 when the step is 0.1.
  4. It leaves a remainder below the minimum volume, which can never be closed partially again.

This tutorial builds a partial close that avoids all four, for a hedging account.

Step 1: the inputs

#include <Trade\Trade.mqh>

input long   MagicNumber       = 20261102;
input int    FirstTargetPoints = 300;    // profit distance for the partial close, in points
input double ClosePercent      = 50.0;   // percent of the original volume to close

CTrade trade;

int OnInit()
{
   if(FirstTargetPoints <= 0 || ClosePercent <= 0.0 || ClosePercent >= 100.0)
      return INIT_PARAMETERS_INCORRECT;
   trade.SetExpertMagicNumber(MagicNumber);
   return INIT_SUCCEEDED;
}
Enter fullscreen mode Exit fullscreen mode

The target is in points so the code can compare it with a price distance without any conversion guesswork.

Step 2: remember the partial without storing anything

The easiest way to know whether the partial was already taken is to compare the position's current volume with the volume it was opened with. On a hedging account a partial close keeps the same position ticket and only reduces POSITION_VOLUME. The original volume is in the deal history, so it survives restarts, recompiles and a move to another VPS:

// Total volume that entered this position, read from the deal history.
double OpenedVolume(const long positionId)
{
   if(!HistorySelectByPosition(positionId))
      return 0.0;
   double volume = 0.0;
   int total = HistoryDealsTotal();
   for(int i = 0; i < total; i++)
   {
      ulong deal = HistoryDealGetTicket(i);
      if(deal == 0)
         continue;
      if(HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
         volume += HistoryDealGetDouble(deal, DEAL_VOLUME);
   }
   return volume;
}
Enter fullscreen mode Exit fullscreen mode

If the current volume is smaller than the opened volume by at least half a volume step, something has already been closed, and the EA should not take the partial again. No global variables, no files, no order comments.

Step 3: a volume the broker accepts

// Round down to a whole number of volume steps.
double RoundDownToStep(const double volume, const double step)
{
   if(step <= 0.0 || volume <= 0.0)
      return 0.0;
   double steps = MathFloor(volume / step + 0.0000001);
   return NormalizeDouble(steps / (1.0 / step), 8);
}
Enter fullscreen mode Exit fullscreen mode

The small tolerance stops 0.3 divided by 0.1 from becoming 2.9999999 and rounding down a whole step.

Step 4: the partial close itself

void ManagePartials()
{
   double step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double point  = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if(PositionGetInteger(POSITION_MAGIC) != MagicNumber)
         continue;

      double current   = PositionGetDouble(POSITION_VOLUME);
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double price     = PositionGetDouble(POSITION_PRICE_CURRENT);
      long   type      = PositionGetInteger(POSITION_TYPE);
      long   id        = PositionGetInteger(POSITION_IDENTIFIER);

      // Profit distance in points, positive when the trade is in profit.
      double gained = (type == POSITION_TYPE_BUY) ? (price - openPrice) / point
                                                  : (openPrice - price) / point;
      if(gained < FirstTargetPoints)
         continue;

      double opened = OpenedVolume(id);
      if(opened <= 0.0)
         continue;                         // history not available yet, try next tick
      if(current < opened - step / 2.0)
         continue;                         // partial already taken

      double closeVol = RoundDownToStep(opened / (100.0 / ClosePercent), step);
      if(closeVol < minVol)
         continue;                         // too small to split, leave it to the main exit
      if(current - closeVol < minVol)
         continue;                         // remainder would be below the minimum

      if(!trade.PositionClosePartial(ticket, closeVol))
      {
         PrintFormat("Partial close failed on %I64u: %u %s",
                     ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
         continue;
      }
      if(trade.ResultRetcode() != TRADE_RETCODE_DONE)
         PrintFormat("Partial close on %I64u returned %u", ticket, trade.ResultRetcode());
   }
}

void OnTick()
{
   ManagePartials();
}
Enter fullscreen mode Exit fullscreen mode

Note that OpenedVolume() calls HistorySelectByPosition(), which replaces whatever history was selected before. If other code in the EA works with a history range, select it again after this loop.

Step 5: what to test

  1. Open 0.10 lots with a 0.01 step and ClosePercent at 50. The partial should be exactly 0.05, once.
  2. Restart the terminal after the partial. Nothing should be closed again.
  3. Use a symbol with a 0.1 step and open 0.1 lots. The partial should be skipped, not sent as 0.05.
  4. Open 0.30 lots and close 50 percent. The result should be 0.15 with a 0.01 step, not 0.14.
  5. Run it in the Strategy Tester with every tick based on real ticks and check the deal list for one partial per position.

Netting accounts

On a netting account every add to the position is another DEAL_ENTRY_IN, so the opened volume grows each time the EA scales in, and a position can be reversed by a DEAL_ENTRY_INOUT deal. If your EA adds to positions on netting, track the volume at the moment the target logic starts instead of the full entry total, or treat each add as its own plan.

Common follow ups

Many plans move the stop to breakeven after the partial. Do that in a separate step that reads the position again, so a failed partial never moves the stop on its own. And keep the main exit in charge of the remainder: the partial close only decides how much to bank early, not when the trade is over.

Top comments (0)