//+------------------------------------------------------------------+
//|                                         PartialCloseExecutor.mqh |
//+------------------------------------------------------------------+
#ifndef PARTIALCLOSEEXECUTOR_MQH
#define PARTIALCLOSEEXECUTOR_MQH

#define PCE_MAX_RETRIES 3

//+------------------------------------------------------------------+
//| CPartialCloseExecutor                                            |
//| Sends the actual partial close order. A partial close in MQL5 is |
//| a normal TRADE_ACTION_DEAL request against the ticket with a     |
//| volume smaller than the full position volume; the trade server   |
//| reduces the position rather than opening an opposite one. This   |
//| class isolates that mechanics so the engine never touches        |
//| OrderSend() directly.                                            |
//+------------------------------------------------------------------+
class CPartialCloseExecutor
  {
private:
   ulong             m_last_deal_ticket;
   bool              IsRetryable(const int error_code) const;
   ENUM_ORDER_TYPE_FILLING SelectFillingMode(const string symbol) const;

public:
                     CPartialCloseExecutor(void);
                    ~CPartialCloseExecutor(void);

   bool              ExecutePartialClose(const ulong position_ticket,
                                         const string symbol,
                                         const long position_type,
                                         const double volume);
   ulong             LastDealTicket(void) const { return(m_last_deal_ticket); }
  };

//+------------------------------------------------------------------+
//| Constructor.                                                     |
//+------------------------------------------------------------------+
CPartialCloseExecutor::CPartialCloseExecutor(void)
  {
   m_last_deal_ticket = 0;
  }

//+------------------------------------------------------------------+
//| Destructor.                                                      |
//+------------------------------------------------------------------+
CPartialCloseExecutor::~CPartialCloseExecutor(void)
  {
  }

//+------------------------------------------------------------------+
//| IsRetryable                                                      |
//| Returns true for trade server errors worth retrying, such as a   |
//| requote or a momentary lack of liquidity, and false for errors   |
//| that will never succeed on retry, such as an invalid volume.     |
//+------------------------------------------------------------------+
bool CPartialCloseExecutor::IsRetryable(const int error_code) const
  {
   switch(error_code)
     {
      case TRADE_RETCODE_REQUOTE:
      case TRADE_RETCODE_PRICE_CHANGED:
      case TRADE_RETCODE_PRICE_OFF:
      case TRADE_RETCODE_TIMEOUT:
      case TRADE_RETCODE_CONNECTION:
         return(true);
      default:
         return(false);
     }
  }

//+------------------------------------------------------------------+
//| SelectFillingMode                                                |
//| Returns a filling mode the symbol actually advertises support    |
//| for. A request built with an unsupported filling mode is         |
//| rejected client-side by the terminal (ERR_TRADE_SEND_FAILED,     |
//| error 4756) before it ever reaches the trade server, so this     |
//| check must run before every request, not just once at startup,   |
//| since different symbols on the same account can differ.          |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING CPartialCloseExecutor::SelectFillingMode(const string symbol) const
  {
   long filling_flags = ::SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);

   if((filling_flags & SYMBOL_FILLING_FOK) != 0)
      return(ORDER_FILLING_FOK);
   if((filling_flags & SYMBOL_FILLING_IOC) != 0)
      return(ORDER_FILLING_IOC);

   return(ORDER_FILLING_RETURN);
  }

//+-------------------------------------------------------------------+
//| ExecutePartialClose                                               |
//| Sends a reduced-volume market order to partially close a position.|
//| Retries a bounded number of times on transient trade server       |
//| errors and gives up immediately on non-retryable errors, logging  |
//| the ticket, volume, price, and result in every case.              |
//+------------------------------------------------------------------+
bool CPartialCloseExecutor::ExecutePartialClose(const ulong position_ticket,
      const string symbol,
      const long position_type,
      const double volume)
  {
   MqlTradeRequest request;
   MqlTradeResult  result;
   ::ZeroMemory(request);
   ::ZeroMemory(result);

   double close_price = (position_type == POSITION_TYPE_BUY)
                        ? ::SymbolInfoDouble(symbol, SYMBOL_BID)
                        : ::SymbolInfoDouble(symbol, SYMBOL_ASK);

   request.action       = TRADE_ACTION_DEAL;
   request.position     = position_ticket;
   request.symbol       = symbol;
   request.volume       = volume;
   request.price        = close_price;
   request.deviation    = 10;
   request.type         = (position_type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
   request.type_filling = SelectFillingMode(symbol);

   int attempt = 0;
   bool sent = false;

   while(attempt < PCE_MAX_RETRIES && !sent)
     {
      attempt++;
      ::ZeroMemory(result);
      bool send_ok = ::OrderSend(request, result);

      if(send_ok && (result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED))
        {
         sent = true;
         m_last_deal_ticket = result.deal;
         ::PrintFormat("CPartialCloseExecutor: closed ticket=%I64u volume=%.2f price=%.5f "
                       "deal=%I64u attempt=%d",
                       position_ticket, volume, result.price, result.deal, attempt);
         return(true);
        }

      ::PrintFormat("CPartialCloseExecutor: attempt %d failed for ticket=%I64u volume=%.2f "
                    "retcode=%d comment=%s",
                    attempt, position_ticket, volume, result.retcode, result.comment);

      if(!IsRetryable((int)result.retcode))
        {
         //--- non-retryable error, stop immediately
         break;
        }

      //--- refresh price before retrying a requote or stale-price error
      close_price = (position_type == POSITION_TYPE_BUY)
                    ? ::SymbolInfoDouble(symbol, SYMBOL_BID)
                    : ::SymbolInfoDouble(symbol, SYMBOL_ASK);
      request.price = close_price;
     }

   if(!sent)
     {
      ::PrintFormat("CPartialCloseExecutor: giving up on ticket=%I64u after %d attempts",
                    position_ticket, attempt);
     }

   return(sent);
  }

#endif // PARTIALCLOSEEXECUTOR_MQH
//+------------------------------------------------------------------+