//+------------------------------------------------------------------+
//|                                               BasketExecutor.mqh |
//+------------------------------------------------------------------+
#ifndef BASKETEXECUTOR_MQH
#define BASKETEXECUTOR_MQH

#include "BasketInfo.mqh"

//+------------------------------------------------------------------+
//| CBasketExecutor                                                  |
//| Responsible solely for trade execution operations on baskets:    |
//| closing all legs and opening new legs with the basket comment    |
//| automatically prepended. Logs every ticket confirmation and the  |
//| final realized P&L after all legs are closed.                    |
//+------------------------------------------------------------------+
class CBasketExecutor
  {
private:
   string            m_prefix;            // basket comment prefix
   int               m_slippage;          // max slippage in points for market orders
   ulong             m_magic;             // magic number for orders opened by this executor

   bool              ClosePosition(ulong ticket, const string symbol,
                                   long pos_type, double volume);
   ENUM_ORDER_TYPE_FILLING   ResolveFilling(const string symbol) const;

public:
                     CBasketExecutor(void);
                    ~CBasketExecutor(void);

   //--- configuration
   void              Configure(const int slippage, const ulong magic);

   //--- basket operations
   bool              CloseBasket(const string basket_id);
   ulong             OpenBasketLeg(const string basket_id, const string symbol,
                                   ENUM_ORDER_TYPE order_type, double volume,
                                   double price, double sl, double tp);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CBasketExecutor::CBasketExecutor(void) : m_prefix("BASKET:"),
   m_slippage(10),
   m_magic(0)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CBasketExecutor::~CBasketExecutor(void)
  {
  }
//+------------------------------------------------------------------+
//| Configure                                                        |
//+------------------------------------------------------------------+
void CBasketExecutor::Configure(const int slippage, const ulong magic)
  {
   m_slippage = slippage;
   m_magic    = magic;
  }
//+------------------------------------------------------------------+
//| ResolveFilling                                                   |
//| SYMBOL_FILLING_MODE is a bitmask where:                          |
//|    bit 0 (value 1) = FOK supported                               |
//|    bit 1 (value 2) = IOC supported                               |
//| ORDER_FILLING_RETURN is always accepted as a fallback.           |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING CBasketExecutor::ResolveFilling(const string symbol) const
  {
   uint filling = (uint)::SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);

//--- bit 0 set means FOK is supported
   if((filling & 1) != 0)
      return(ORDER_FILLING_FOK);

//--- bit 1 set means IOC is supported
   if((filling & 2) != 0)
      return(ORDER_FILLING_IOC);

//--- RETURN is the universal fallback accepted by all brokers
   return(ORDER_FILLING_RETURN);
  }
//+------------------------------------------------------------------+
//| ClosePosition                                                    |
//| Closes a single position by ticket using a market order.         |
//+------------------------------------------------------------------+
bool CBasketExecutor::ClosePosition(ulong ticket, const string symbol,
                                    long pos_type, double volume)
  {
   MqlTradeRequest req;
   MqlTradeResult  res;
   ::ZeroMemory(req);
   ::ZeroMemory(res);

   req.action     = TRADE_ACTION_DEAL;
   req.position   = ticket;
   req.symbol     = symbol;
   req.volume     = volume;
   req.deviation  = m_slippage;
   req.magic      = m_magic;
   req.comment    = "basket close";
   req.type_filling = ResolveFilling(symbol);

//--- close a BUY with a SELL and vice versa
   if(pos_type == POSITION_TYPE_BUY)
     {
      req.type  = ORDER_TYPE_SELL;
      req.price = ::SymbolInfoDouble(symbol, SYMBOL_BID);
     }
   else
     {
      req.type  = ORDER_TYPE_BUY;
      req.price = ::SymbolInfoDouble(symbol, SYMBOL_ASK);
     }

   return(::OrderSend(req, res) && res.retcode == TRADE_RETCODE_DONE);
  }
//+------------------------------------------------------------------+
//| CloseBasket                                                      |
//+------------------------------------------------------------------+
bool CBasketExecutor::CloseBasket(const string basket_id)
  {
   string target_prefix = m_prefix + basket_id;
   double total_pnl     = 0.0;
   int    closed_count  = 0;
   bool   any_failed    = false;

   PrintFormat("CBasketExecutor: closing basket '%s'", basket_id);

//--- iterate positions; loop backward so index stays valid after any external close
   int total = ::PositionsTotal();
   for(int i = total - 1; i >= 0; i--)
     {
      ulong ticket = ::PositionGetTicket(i);
      if(ticket == 0)
         continue;

      if(!::PositionSelectByTicket(ticket))
         continue;

      string comment = ::PositionGetString(POSITION_COMMENT);
      if(::StringFind(comment, target_prefix) < 0)
         continue;

      string symbol   = ::PositionGetString(POSITION_SYMBOL);
      long   pos_type = ::PositionGetInteger(POSITION_TYPE);
      double volume   = ::PositionGetDouble(POSITION_VOLUME);
      double pnl      = ::PositionGetDouble(POSITION_PROFIT);

      total_pnl += pnl;

      bool ok = ClosePosition(ticket, symbol, pos_type, volume);

      if(ok)
        {
         closed_count++;
         PrintFormat("CBasketExecutor: closed ticket=%llu symbol=%s volume=%.2f pnl=%.2f",
                     ticket, symbol, volume, pnl);
        }
      else
        {
         any_failed = true;
         PrintFormat("CBasketExecutor: failed to close ticket=%llu symbol=%s",
                     ticket, symbol);
        }
     }

   PrintFormat("CBasketExecutor: basket '%s' closed %d legs, total realized P&L=%.2f %s",
               basket_id, closed_count, total_pnl,
               ::AccountInfoString(ACCOUNT_CURRENCY));

   return(!any_failed && closed_count > 0);
  }
//+------------------------------------------------------------------+
//| OpenBasketLeg                                                    |
//+------------------------------------------------------------------+
ulong CBasketExecutor::OpenBasketLeg(const string basket_id, const string symbol,
                                     ENUM_ORDER_TYPE order_type, double volume,
                                     double price, double sl, double tp)
  {
   MqlTradeRequest req;
   MqlTradeResult  res;
   ::ZeroMemory(req);
   ::ZeroMemory(res);

   string comment = m_prefix + basket_id;

   req.action         = TRADE_ACTION_DEAL;
   req.symbol         = symbol;
   req.volume         = volume;
   req.type           = order_type;
   req.price          = price;
   req.sl             = sl;
   req.tp             = tp;
   req.deviation      = m_slippage;
   req.magic          = m_magic;
   req.comment        = comment;
   req.type_filling   = ResolveFilling(symbol);

   if(!::OrderSend(req, res))
     {
      PrintFormat("CBasketExecutor: OpenBasketLeg failed for basket '%s' symbol=%s retcode=%d",
                  basket_id, symbol, res.retcode);
      return(0);
     }

   PrintFormat("CBasketExecutor: opened leg basket='%s' symbol=%s type=%s vol=%.2f ticket=%llu",
               basket_id, symbol, ::EnumToString(order_type), volume, res.deal);

   return(res.deal);
  }

#endif // BASKETEXECUTOR_MQH
//+------------------------------------------------------------------+
